fix: enforce strict ruff lint (17 rule sets) across entire repo
- Expand ruff config from E/F to 17 rule sets (B, RUF, SIM, PIE, T20, ARG, ERA, A, BLE, RET, ISC, TCH, UP, C4, PERF) - Fix 907 lint errors across all Python files (GUI, FPGA cosim, schematics scripts, simulations, utilities, tools) - Replace all blind except-Exception with specific exception types - Remove commented-out dead code (ERA001) from cosim/simulation files - Modernize typing: deprecated typing.List/Dict/Tuple to builtins - Fix unused args/loop vars, ambiguous unicode, perf anti-patterns - Delete legacy GUI files V1-V4 - Add V7 test suite, requirements files - All CI jobs pass: ruff (0 errors), py_compile, pytest (92/92), MCU tests (20/20), FPGA regression (25/25)
This commit is contained in:
@@ -1,30 +1,26 @@
|
||||
"""
|
||||
v7.processing — Radar signal processing, packet parsing, and GPS parsing.
|
||||
v7.processing — Radar signal processing and GPS parsing.
|
||||
|
||||
Classes:
|
||||
- RadarProcessor — dual-CPI fusion, multi-PRF unwrap, DBSCAN clustering,
|
||||
association, Kalman tracking
|
||||
- RadarPacketParser — parse raw byte streams into typed radar packets
|
||||
(FIX: returns (parsed_dict, bytes_consumed) tuple)
|
||||
- USBPacketParser — parse GPS text/binary frames from STM32 CDC
|
||||
|
||||
Bug fixes vs V6:
|
||||
1. RadarPacketParser.parse_packet() now returns (dict, bytes_consumed) tuple
|
||||
so the caller knows exactly how many bytes to strip from the buffer.
|
||||
2. apply_pitch_correction() is a proper standalone function.
|
||||
Note: RadarPacketParser (old A5/C3 sync + CRC16 format) was removed.
|
||||
All packet parsing now uses production RadarProtocol (0xAA/0xBB format)
|
||||
from radar_protocol.py.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import time
|
||||
import logging
|
||||
import math
|
||||
from typing import Optional, Tuple, List, Dict
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .models import (
|
||||
RadarTarget, GPSData, ProcessingConfig,
|
||||
SCIPY_AVAILABLE, SKLEARN_AVAILABLE, FILTERPY_AVAILABLE, CRCMOD_AVAILABLE,
|
||||
SCIPY_AVAILABLE, SKLEARN_AVAILABLE, FILTERPY_AVAILABLE,
|
||||
)
|
||||
|
||||
if SKLEARN_AVAILABLE:
|
||||
@@ -33,9 +29,6 @@ if SKLEARN_AVAILABLE:
|
||||
if FILTERPY_AVAILABLE:
|
||||
from filterpy.kalman import KalmanFilter
|
||||
|
||||
if CRCMOD_AVAILABLE:
|
||||
import crcmod
|
||||
|
||||
if SCIPY_AVAILABLE:
|
||||
from scipy.signal import windows as scipy_windows
|
||||
|
||||
@@ -64,14 +57,14 @@ class RadarProcessor:
|
||||
|
||||
def __init__(self):
|
||||
self.range_doppler_map = np.zeros((1024, 32))
|
||||
self.detected_targets: List[RadarTarget] = []
|
||||
self.detected_targets: list[RadarTarget] = []
|
||||
self.track_id_counter: int = 0
|
||||
self.tracks: Dict[int, dict] = {}
|
||||
self.tracks: dict[int, dict] = {}
|
||||
self.frame_count: int = 0
|
||||
self.config = ProcessingConfig()
|
||||
|
||||
# MTI state: store previous frames for cancellation
|
||||
self._mti_history: List[np.ndarray] = []
|
||||
self._mti_history: list[np.ndarray] = []
|
||||
|
||||
# ---- Configuration -----------------------------------------------------
|
||||
|
||||
@@ -160,12 +153,11 @@ class RadarProcessor:
|
||||
h = self._mti_history
|
||||
if order == 1:
|
||||
return h[-1] - h[-2]
|
||||
elif order == 2:
|
||||
if order == 2:
|
||||
return h[-1] - 2.0 * h[-2] + h[-3]
|
||||
elif order == 3:
|
||||
if order == 3:
|
||||
return h[-1] - 3.0 * h[-2] + 3.0 * h[-3] - h[-4]
|
||||
else:
|
||||
return h[-1] - h[-2]
|
||||
return h[-1] - h[-2]
|
||||
|
||||
# ---- CFAR (Constant False Alarm Rate) -----------------------------------
|
||||
|
||||
@@ -234,7 +226,7 @@ class RadarProcessor:
|
||||
|
||||
# ---- Full processing pipeline -------------------------------------------
|
||||
|
||||
def process_frame(self, raw_frame: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
||||
def process_frame(self, raw_frame: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Run the full signal processing chain on a Range x Doppler frame.
|
||||
|
||||
Parameters
|
||||
@@ -289,34 +281,10 @@ class RadarProcessor:
|
||||
"""Dual-CPI fusion for better detection."""
|
||||
return np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0)
|
||||
|
||||
# ---- Multi-PRF velocity unwrapping -------------------------------------
|
||||
|
||||
def multi_prf_unwrap(self, doppler_measurements, prf1: float, prf2: float):
|
||||
"""Multi-PRF velocity unwrapping (Chinese Remainder Theorem)."""
|
||||
lam = 3e8 / 10e9
|
||||
v_max1 = prf1 * lam / 2
|
||||
v_max2 = prf2 * lam / 2
|
||||
|
||||
unwrapped = []
|
||||
for doppler in doppler_measurements:
|
||||
v1 = doppler * lam / 2
|
||||
v2 = doppler * lam / 2
|
||||
velocity = self._solve_chinese_remainder(v1, v2, v_max1, v_max2)
|
||||
unwrapped.append(velocity)
|
||||
return unwrapped
|
||||
|
||||
@staticmethod
|
||||
def _solve_chinese_remainder(v1, v2, max1, max2):
|
||||
for k in range(-5, 6):
|
||||
candidate = v1 + k * max1
|
||||
if abs(candidate - v2) < max2 / 2:
|
||||
return candidate
|
||||
return v1
|
||||
|
||||
# ---- DBSCAN clustering -------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def clustering(detections: List[RadarTarget],
|
||||
def clustering(detections: list[RadarTarget],
|
||||
eps: float = 100, min_samples: int = 2) -> list:
|
||||
"""DBSCAN clustering of detections (requires sklearn)."""
|
||||
if not SKLEARN_AVAILABLE or len(detections) == 0:
|
||||
@@ -339,8 +307,8 @@ class RadarProcessor:
|
||||
|
||||
# ---- Association -------------------------------------------------------
|
||||
|
||||
def association(self, detections: List[RadarTarget],
|
||||
clusters: list) -> List[RadarTarget]:
|
||||
def association(self, detections: list[RadarTarget],
|
||||
_clusters: list) -> list[RadarTarget]:
|
||||
"""Associate detections to existing tracks (nearest-neighbour)."""
|
||||
associated = []
|
||||
for det in detections:
|
||||
@@ -366,7 +334,7 @@ class RadarProcessor:
|
||||
|
||||
# ---- Kalman tracking ---------------------------------------------------
|
||||
|
||||
def tracking(self, associated_detections: List[RadarTarget]):
|
||||
def tracking(self, associated_detections: list[RadarTarget]):
|
||||
"""Kalman filter tracking (requires filterpy)."""
|
||||
if not FILTERPY_AVAILABLE:
|
||||
return
|
||||
@@ -412,158 +380,6 @@ class RadarProcessor:
|
||||
del self.tracks[tid]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Radar Packet Parser
|
||||
# =============================================================================
|
||||
|
||||
class RadarPacketParser:
|
||||
"""
|
||||
Parse binary radar packets from the raw byte stream.
|
||||
|
||||
Packet format:
|
||||
[Sync 2][Type 1][Length 1][Payload N][CRC16 2]
|
||||
Sync pattern: 0xA5 0xC3
|
||||
|
||||
Bug fix vs V6:
|
||||
parse_packet() now returns ``(parsed_dict, bytes_consumed)`` so the
|
||||
caller can correctly advance the read pointer in the buffer.
|
||||
"""
|
||||
|
||||
SYNC = b"\xA5\xC3"
|
||||
|
||||
def __init__(self):
|
||||
if CRCMOD_AVAILABLE:
|
||||
self.crc16_func = crcmod.mkCrcFun(
|
||||
0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000
|
||||
)
|
||||
else:
|
||||
self.crc16_func = None
|
||||
|
||||
# ---- main entry point --------------------------------------------------
|
||||
|
||||
def parse_packet(self, data: bytes) -> Optional[Tuple[dict, int]]:
|
||||
"""
|
||||
Attempt to parse one radar packet from *data*.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(parsed_dict, bytes_consumed) on success, or None if no valid packet.
|
||||
"""
|
||||
if len(data) < 6:
|
||||
return None
|
||||
|
||||
idx = data.find(self.SYNC)
|
||||
if idx == -1:
|
||||
return None
|
||||
|
||||
pkt = data[idx:]
|
||||
if len(pkt) < 6:
|
||||
return None
|
||||
|
||||
pkt_type = pkt[2]
|
||||
length = pkt[3]
|
||||
total_len = 4 + length + 2 # sync(2) + type(1) + len(1) + payload + crc(2)
|
||||
|
||||
if len(pkt) < total_len:
|
||||
return None
|
||||
|
||||
payload = pkt[4 : 4 + length]
|
||||
crc_received = struct.unpack("<H", pkt[4 + length : 4 + length + 2])[0]
|
||||
|
||||
# CRC check
|
||||
if self.crc16_func is not None:
|
||||
crc_calc = self.crc16_func(pkt[0 : 4 + length])
|
||||
if crc_calc != crc_received:
|
||||
logger.warning(
|
||||
f"CRC mismatch: got {crc_received:04X}, calc {crc_calc:04X}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Bytes consumed = offset to sync + total packet length
|
||||
consumed = idx + total_len
|
||||
|
||||
parsed = None
|
||||
if pkt_type == 0x01:
|
||||
parsed = self._parse_range(payload)
|
||||
elif pkt_type == 0x02:
|
||||
parsed = self._parse_doppler(payload)
|
||||
elif pkt_type == 0x03:
|
||||
parsed = self._parse_detection(payload)
|
||||
else:
|
||||
logger.warning(f"Unknown packet type: {pkt_type:02X}")
|
||||
|
||||
if parsed is None:
|
||||
return None
|
||||
return (parsed, consumed)
|
||||
|
||||
# ---- sub-parsers -------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _parse_range(payload: bytes) -> Optional[dict]:
|
||||
if len(payload) < 12:
|
||||
return None
|
||||
try:
|
||||
range_val = struct.unpack(">I", payload[0:4])[0]
|
||||
elevation = payload[4] & 0x1F
|
||||
azimuth = payload[5] & 0x3F
|
||||
chirp = payload[6] & 0x1F
|
||||
return {
|
||||
"type": "range",
|
||||
"range": range_val,
|
||||
"elevation": elevation,
|
||||
"azimuth": azimuth,
|
||||
"chirp": chirp,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing range packet: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _parse_doppler(payload: bytes) -> Optional[dict]:
|
||||
if len(payload) < 12:
|
||||
return None
|
||||
try:
|
||||
real = struct.unpack(">h", payload[0:2])[0]
|
||||
imag = struct.unpack(">h", payload[2:4])[0]
|
||||
elevation = payload[4] & 0x1F
|
||||
azimuth = payload[5] & 0x3F
|
||||
chirp = payload[6] & 0x1F
|
||||
return {
|
||||
"type": "doppler",
|
||||
"doppler_real": real,
|
||||
"doppler_imag": imag,
|
||||
"elevation": elevation,
|
||||
"azimuth": azimuth,
|
||||
"chirp": chirp,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing doppler packet: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _parse_detection(payload: bytes) -> Optional[dict]:
|
||||
if len(payload) < 8:
|
||||
return None
|
||||
try:
|
||||
detected = (payload[0] & 0x01) != 0
|
||||
elevation = payload[1] & 0x1F
|
||||
azimuth = payload[2] & 0x3F
|
||||
chirp = payload[3] & 0x1F
|
||||
return {
|
||||
"type": "detection",
|
||||
"detected": detected,
|
||||
"elevation": elevation,
|
||||
"azimuth": azimuth,
|
||||
"chirp": chirp,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing detection packet: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# USB / GPS Packet Parser
|
||||
# =============================================================================
|
||||
@@ -578,14 +394,9 @@ class USBPacketParser:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
if CRCMOD_AVAILABLE:
|
||||
self.crc16_func = crcmod.mkCrcFun(
|
||||
0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000
|
||||
)
|
||||
else:
|
||||
self.crc16_func = None
|
||||
pass
|
||||
|
||||
def parse_gps_data(self, data: bytes) -> Optional[GPSData]:
|
||||
def parse_gps_data(self, data: bytes) -> GPSData | None:
|
||||
"""Attempt to parse GPS data from a raw USB CDC frame."""
|
||||
if not data:
|
||||
return None
|
||||
@@ -607,12 +418,12 @@ class USBPacketParser:
|
||||
# Binary format: [GPSB 4][lat 8][lon 8][alt 4][pitch 4][CRC 2] = 30 bytes
|
||||
if len(data) >= 30 and data[0:4] == b"GPSB":
|
||||
return self._parse_binary_gps(data)
|
||||
except Exception as e:
|
||||
except (ValueError, struct.error) as e:
|
||||
logger.error(f"Error parsing GPS data: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _parse_binary_gps(data: bytes) -> Optional[GPSData]:
|
||||
def _parse_binary_gps(data: bytes) -> GPSData | None:
|
||||
"""Parse 30-byte binary GPS frame."""
|
||||
try:
|
||||
if len(data) < 30:
|
||||
@@ -637,6 +448,6 @@ class USBPacketParser:
|
||||
pitch=pitch,
|
||||
timestamp=time.time(),
|
||||
)
|
||||
except Exception as e:
|
||||
except (ValueError, struct.error) as e:
|
||||
logger.error(f"Error parsing binary GPS: {e}")
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user