Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8aefc4f61 | |||
| 5b84af68f6 | |||
| 846a0debe8 | |||
| e979363730 | |||
| 2e9a848908 | |||
| 3366ac6417 | |||
| 607399ec28 | |||
| f48448970b | |||
| ebd96c90ce | |||
| db80baf34d | |||
| 33d21da7f2 | |||
| 18901be04a | |||
| 9f899b96e9 | |||
| f895c0244c | |||
| c82b25f7a0 | |||
| 2539d46d93 | |||
| 88ca1910ec | |||
| d0b3a4c969 |
Binary file not shown.
|
After Width: | Height: | Size: 378 KiB |
@@ -24,6 +24,7 @@ ADAR1000_AGC::ADAR1000_AGC()
|
|||||||
, saturation_event_count(0)
|
, saturation_event_count(0)
|
||||||
{
|
{
|
||||||
memset(cal_offset, 0, sizeof(cal_offset));
|
memset(cal_offset, 0, sizeof(cal_offset));
|
||||||
|
if (holdoff_frames == 0) holdoff_frames = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ void USBHandler::reset() {
|
|||||||
start_flag_received = false;
|
start_flag_received = false;
|
||||||
buffer_index = 0;
|
buffer_index = 0;
|
||||||
current_settings.resetToDefaults();
|
current_settings.resetToDefaults();
|
||||||
|
fault_ack_received = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void USBHandler::processUSBData(const uint8_t* data, uint32_t length) {
|
void USBHandler::processUSBData(const uint8_t* data, uint32_t length) {
|
||||||
@@ -23,6 +24,18 @@ void USBHandler::processUSBData(const uint8_t* data, uint32_t length) {
|
|||||||
|
|
||||||
DIAG("USB", "processUSBData: %lu bytes, state=%d", (unsigned long)length, (int)current_state);
|
DIAG("USB", "processUSBData: %lu bytes, state=%d", (unsigned long)length, (int)current_state);
|
||||||
|
|
||||||
|
// FAULT_ACK: host sends exactly 4 bytes [0x40, 0x00, 0x00, 0x00].
|
||||||
|
// Requires exact 4-byte packet length: settings packets are always
|
||||||
|
// >= 82 bytes, so a lone 4-byte payload is unambiguous. Scanning
|
||||||
|
// inside larger packets would false-trigger on the IEEE 754
|
||||||
|
// encoding of 2.0 (0x4000000000000000) embedded in settings doubles.
|
||||||
|
static const uint8_t FAULT_ACK_SEQ[4] = {0x40, 0x00, 0x00, 0x00};
|
||||||
|
if (length == 4 && memcmp(data, FAULT_ACK_SEQ, 4) == 0) {
|
||||||
|
fault_ack_received = true;
|
||||||
|
DIAG("USB", "FAULT_ACK received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (current_state) {
|
switch (current_state) {
|
||||||
case USBState::WAITING_FOR_START:
|
case USBState::WAITING_FOR_START:
|
||||||
processStartFlag(data, length);
|
processStartFlag(data, length);
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ public:
|
|||||||
// Reset USB handler
|
// Reset USB handler
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
|
// Fault-acknowledgement: host sends FAULT_ACK (0x40) to clear
|
||||||
|
// system_emergency_state and exit the safe-mode blink loop.
|
||||||
|
bool isFaultAckReceived() const { return fault_ack_received; }
|
||||||
|
void clearFaultAck() { fault_ack_received = false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RadarSettings current_settings;
|
RadarSettings current_settings;
|
||||||
USBState current_state;
|
USBState current_state;
|
||||||
@@ -38,6 +43,7 @@ private:
|
|||||||
static constexpr uint32_t MAX_BUFFER_SIZE = 256;
|
static constexpr uint32_t MAX_BUFFER_SIZE = 256;
|
||||||
uint8_t usb_buffer[MAX_BUFFER_SIZE];
|
uint8_t usb_buffer[MAX_BUFFER_SIZE];
|
||||||
uint32_t buffer_index;
|
uint32_t buffer_index;
|
||||||
|
bool fault_ack_received;
|
||||||
|
|
||||||
void processStartFlag(const uint8_t* data, uint32_t length);
|
void processStartFlag(const uint8_t* data, uint32_t length);
|
||||||
void processSettingsData(const uint8_t* data, uint32_t length);
|
void processSettingsData(const uint8_t* data, uint32_t length);
|
||||||
|
|||||||
@@ -627,7 +627,7 @@ typedef enum {
|
|||||||
|
|
||||||
static SystemError_t last_error = ERROR_NONE;
|
static SystemError_t last_error = ERROR_NONE;
|
||||||
static uint32_t error_count = 0;
|
static uint32_t error_count = 0;
|
||||||
static bool system_emergency_state = false;
|
static volatile bool system_emergency_state = false;
|
||||||
|
|
||||||
// Error handler function
|
// Error handler function
|
||||||
SystemError_t checkSystemHealth(void) {
|
SystemError_t checkSystemHealth(void) {
|
||||||
@@ -2054,6 +2054,10 @@ int main(void)
|
|||||||
HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin);
|
HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin);
|
||||||
HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin);
|
HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin);
|
||||||
HAL_Delay(250);
|
HAL_Delay(250);
|
||||||
|
if (usbHandler.isFaultAckReceived()) {
|
||||||
|
system_emergency_state = false;
|
||||||
|
usbHandler.clearFaultAck();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared");
|
DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \
|
|||||||
test_gap3_idq_periodic_reread \
|
test_gap3_idq_periodic_reread \
|
||||||
test_gap3_emergency_state_ordering \
|
test_gap3_emergency_state_ordering \
|
||||||
test_gap3_overtemp_emergency_stop \
|
test_gap3_overtemp_emergency_stop \
|
||||||
test_gap3_health_watchdog_cold_start
|
test_gap3_health_watchdog_cold_start \
|
||||||
|
test_gap3_fault_ack_clears_emergency
|
||||||
|
|
||||||
# Tests that need platform_noos_stm32.o + mocks
|
# Tests that need platform_noos_stm32.o + mocks
|
||||||
TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only
|
TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only
|
||||||
@@ -178,6 +179,9 @@ test_gap3_overtemp_emergency_stop: test_gap3_overtemp_emergency_stop.c
|
|||||||
test_gap3_health_watchdog_cold_start: test_gap3_health_watchdog_cold_start.c
|
test_gap3_health_watchdog_cold_start: test_gap3_health_watchdog_cold_start.c
|
||||||
$(CC) $(CFLAGS) $< -o $@
|
$(CC) $(CFLAGS) $< -o $@
|
||||||
|
|
||||||
|
test_gap3_fault_ack_clears_emergency: test_gap3_fault_ack_clears_emergency.c
|
||||||
|
$(CC) $(CFLAGS) $< -o $@
|
||||||
|
|
||||||
# Tests that need platform_noos_stm32.o + mocks
|
# Tests that need platform_noos_stm32.o + mocks
|
||||||
$(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ)
|
$(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ)
|
||||||
$(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@
|
$(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* test_gap3_fault_ack_clears_emergency.c
|
||||||
|
*
|
||||||
|
* Verifies the FAULT_ACK clear path for system_emergency_state:
|
||||||
|
* - USBHandler detects exactly [0x40, 0x00, 0x00, 0x00] in a 4-byte packet
|
||||||
|
* - Detection is false-positive-free: larger packets (settings data) carrying
|
||||||
|
* the same bytes as a subsequence must NOT trigger the ack
|
||||||
|
* - Main-loop blink logic clears system_emergency_state on receipt
|
||||||
|
*
|
||||||
|
* Logic extracted from USBHandler.cpp + main.cpp to mirror the actual code
|
||||||
|
* paths without requiring HAL headers.
|
||||||
|
******************************************************************************/
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* ── Simulated USBHandler state ─────────────────────────────────────────── */
|
||||||
|
static bool fault_ack_received = false;
|
||||||
|
static volatile bool system_emergency_state = false;
|
||||||
|
|
||||||
|
static const uint8_t FAULT_ACK_SEQ[4] = {0x40, 0x00, 0x00, 0x00};
|
||||||
|
|
||||||
|
/* Mirrors USBHandler::processUSBData() detection logic */
|
||||||
|
static void sim_processUSBData(const uint8_t *data, uint32_t length)
|
||||||
|
{
|
||||||
|
if (data == NULL || length == 0) return;
|
||||||
|
if (length == 4 && memcmp(data, FAULT_ACK_SEQ, 4) == 0) {
|
||||||
|
fault_ack_received = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* (normal state machine omitted — not under test here) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mirrors one iteration of the blink loop in main.cpp */
|
||||||
|
static void sim_blink_iteration(void)
|
||||||
|
{
|
||||||
|
/* HAL_GPIO_TogglePin + HAL_Delay omitted */
|
||||||
|
if (fault_ack_received) {
|
||||||
|
system_emergency_state = false;
|
||||||
|
fault_ack_received = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("=== Gap-3 FAULT_ACK clears system_emergency_state ===\n");
|
||||||
|
|
||||||
|
/* Test 1: exact 4-byte FAULT_ACK packet sets the flag */
|
||||||
|
printf(" Test 1: exact FAULT_ACK packet detected... ");
|
||||||
|
fault_ack_received = false;
|
||||||
|
const uint8_t ack_pkt[4] = {0x40, 0x00, 0x00, 0x00};
|
||||||
|
sim_processUSBData(ack_pkt, 4);
|
||||||
|
assert(fault_ack_received == true);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 2: flag cleared and system_emergency_state exits blink loop */
|
||||||
|
printf(" Test 2: blink loop exits on FAULT_ACK... ");
|
||||||
|
system_emergency_state = true;
|
||||||
|
fault_ack_received = true;
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == false);
|
||||||
|
assert(fault_ack_received == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 3: blink loop does NOT exit without ack */
|
||||||
|
printf(" Test 3: blink loop holds without ack... ");
|
||||||
|
system_emergency_state = true;
|
||||||
|
fault_ack_received = false;
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == true);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 4: settings-sized packet carrying [0x40,0x00,0x00,0x00] as first
|
||||||
|
* 4 bytes does NOT trigger ack (IEEE 754 double 2.0 = 0x4000000000000000) */
|
||||||
|
printf(" Test 4: settings packet with 2.0 double does not false-trigger... ");
|
||||||
|
fault_ack_received = false;
|
||||||
|
uint8_t settings_pkt[82];
|
||||||
|
memset(settings_pkt, 0, sizeof(settings_pkt));
|
||||||
|
/* First 4 bytes look like FAULT_ACK but packet length is 82 */
|
||||||
|
settings_pkt[0] = 0x40; settings_pkt[1] = 0x00;
|
||||||
|
settings_pkt[2] = 0x00; settings_pkt[3] = 0x00;
|
||||||
|
sim_processUSBData(settings_pkt, sizeof(settings_pkt));
|
||||||
|
assert(fault_ack_received == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 5: 3-byte packet (truncated) does not trigger */
|
||||||
|
printf(" Test 5: truncated 3-byte packet ignored... ");
|
||||||
|
fault_ack_received = false;
|
||||||
|
const uint8_t short_pkt[3] = {0x40, 0x00, 0x00};
|
||||||
|
sim_processUSBData(short_pkt, 3);
|
||||||
|
assert(fault_ack_received == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 6: wrong opcode byte in 4-byte packet does not trigger */
|
||||||
|
printf(" Test 6: wrong opcode (0x28 AGC_ENABLE) not detected as FAULT_ACK... ");
|
||||||
|
fault_ack_received = false;
|
||||||
|
const uint8_t agc_pkt[4] = {0x28, 0x00, 0x00, 0x01};
|
||||||
|
sim_processUSBData(agc_pkt, 4);
|
||||||
|
assert(fault_ack_received == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 7: multiple blink iterations — loop stays active until ack */
|
||||||
|
printf(" Test 7: loop stays active across multiple iterations until ack... ");
|
||||||
|
system_emergency_state = true;
|
||||||
|
fault_ack_received = false;
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == true);
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == true);
|
||||||
|
/* Now ack arrives */
|
||||||
|
sim_processUSBData(ack_pkt, 4);
|
||||||
|
assert(fault_ack_received == true);
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf("\n=== Gap-3 FAULT_ACK: ALL 7 TESTS PASSED ===\n\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -169,11 +169,11 @@ endfunction
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Clamp a wider signed value to [-7, +7]
|
// Clamp a wider signed value to [-7, +7]
|
||||||
function signed [3:0] clamp_gain;
|
function signed [3:0] clamp_gain;
|
||||||
input signed [4:0] val; // 5-bit to handle overflow from add
|
input signed [5:0] val; // 6-bit: covers [-22,+22] (max |gain|+step = 7+15)
|
||||||
begin
|
begin
|
||||||
if (val > 5'sd7)
|
if (val > 6'sd7)
|
||||||
clamp_gain = 4'sd7;
|
clamp_gain = 4'sd7;
|
||||||
else if (val < -5'sd7)
|
else if (val < -6'sd7)
|
||||||
clamp_gain = -4'sd7;
|
clamp_gain = -4'sd7;
|
||||||
else
|
else
|
||||||
clamp_gain = val[3:0];
|
clamp_gain = val[3:0];
|
||||||
@@ -246,15 +246,15 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
// Use inclusive counts/peaks (accounting for simultaneous valid_in)
|
// Use inclusive counts/peaks (accounting for simultaneous valid_in)
|
||||||
if (wire_frame_sat_incr || frame_sat_count > 8'd0) begin
|
if (wire_frame_sat_incr || frame_sat_count > 8'd0) begin
|
||||||
// Clipping detected: reduce gain immediately (attack)
|
// Clipping detected: reduce gain immediately (attack)
|
||||||
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) -
|
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain[3], agc_gain}) -
|
||||||
$signed({1'b0, agc_attack}));
|
$signed({2'b00, agc_attack}));
|
||||||
holdoff_counter <= agc_holdoff; // Reset holdoff
|
holdoff_counter <= agc_holdoff; // Reset holdoff
|
||||||
end else if ((wire_frame_peak_update ? max_iq[14:7] : frame_peak[14:7])
|
end else if ((wire_frame_peak_update ? max_iq[14:7] : frame_peak[14:7])
|
||||||
< agc_target) begin
|
< agc_target) begin
|
||||||
// Signal too weak: increase gain after holdoff expires
|
// Signal too weak: increase gain after holdoff expires
|
||||||
if (holdoff_counter == 4'd0) begin
|
if (holdoff_counter == 4'd0) begin
|
||||||
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) +
|
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain[3], agc_gain}) +
|
||||||
$signed({1'b0, agc_decay}));
|
$signed({2'b00, agc_decay}));
|
||||||
end else begin
|
end else begin
|
||||||
holdoff_counter <= holdoff_counter - 4'd1;
|
holdoff_counter <= holdoff_counter - 4'd1;
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -103,6 +103,15 @@ class Opcode(IntEnum):
|
|||||||
STATUS_REQUEST = 0xFF
|
STATUS_REQUEST = 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
# MCU-only commands — NOT dispatched to the FPGA opcode switch.
|
||||||
|
# These values have no corresponding case in radar_system_top.v.
|
||||||
|
# Listed here so the GUI can build and send them via build_command().
|
||||||
|
# contract_parser.py filters MCU_ONLY_OPCODES out of the Python/Verilog
|
||||||
|
# bidirectional check.
|
||||||
|
FAULT_ACK = 0x40 # Exact 4-byte CDC packet; clears system_emergency_state
|
||||||
|
MCU_ONLY_OPCODES: frozenset[int] = frozenset({0x40})
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Data Structures
|
# Data Structures
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -586,6 +586,135 @@ class TestSoftwareFPGA(unittest.TestCase):
|
|||||||
self.assertEqual(fpga.agc_holdoff, 0x0F)
|
self.assertEqual(fpga.agc_holdoff, 0x0F)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Test: live vs replay physical-unit parity — regression guard for unit drift
|
||||||
|
#
|
||||||
|
# Uses AST parse of workers.py (not inspect.getsource / import) so the test
|
||||||
|
# runs in headless CI without PyQt6 — v7.workers imports PyQt6 unconditionally
|
||||||
|
# at workers.py:24, and other worker tests here already use skipUnless(
|
||||||
|
# _pyqt6_available()). Contract enforcement must not be gated on GUI deps.
|
||||||
|
#
|
||||||
|
# Asserts on AST nodes (Call / Attribute / BinOp), not source substrings, so
|
||||||
|
# false-pass on comments or docstring wording is impossible.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestLiveReplayPhysicalUnitsParity(unittest.TestCase):
|
||||||
|
"""Contract: live path (RadarDataWorker._run_host_dsp) and replay path
|
||||||
|
(ReplayWorker._emit_frame) both derive bin-to-physical conversion from
|
||||||
|
WaveformConfig — same source of truth, identical (range_m, velocity_ms)
|
||||||
|
for identical detections.
|
||||||
|
|
||||||
|
Regression context: before the fix, live path used
|
||||||
|
RadarSettings.velocity_resolution (default 1.0 in models.py:113) while
|
||||||
|
replay used WaveformConfig.velocity_resolution_mps (~5.343). Live GUI
|
||||||
|
therefore under-reported velocity by factor ~5.34x vs replay for
|
||||||
|
identical frames. See test_v7.py:449 for the WaveformConfig pin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_method(class_name: str, method_name: str):
|
||||||
|
"""Return AST FunctionDef for class_name.method_name from workers.py,
|
||||||
|
without importing v7.workers (PyQt6-independent)."""
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
path = Path(__file__).parent / "v7" / "workers.py"
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
||||||
|
for item in node.body:
|
||||||
|
if isinstance(item, ast.FunctionDef) and item.name == method_name:
|
||||||
|
return item
|
||||||
|
raise RuntimeError(f"{class_name}.{method_name} not found in workers.py")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_attribute_chain(tree, chain):
|
||||||
|
"""True if AST tree contains a dotted attribute access matching chain.
|
||||||
|
|
||||||
|
Chain ('self', '_settings', 'range_resolution') matches
|
||||||
|
``self._settings.range_resolution`` exactly.
|
||||||
|
"""
|
||||||
|
import ast
|
||||||
|
for n in ast.walk(tree):
|
||||||
|
if isinstance(n, ast.Attribute):
|
||||||
|
parts = [n.attr]
|
||||||
|
cur = n.value
|
||||||
|
while isinstance(cur, ast.Attribute):
|
||||||
|
parts.append(cur.attr)
|
||||||
|
cur = cur.value
|
||||||
|
if isinstance(cur, ast.Name):
|
||||||
|
parts.append(cur.id)
|
||||||
|
parts.reverse()
|
||||||
|
if tuple(parts) == tuple(chain):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_call_to(tree, func_name):
|
||||||
|
"""True if AST tree contains a call to a bare name (func_name())."""
|
||||||
|
import ast
|
||||||
|
for n in ast.walk(tree):
|
||||||
|
if (isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
|
||||||
|
and n.func.id == func_name):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_dbin_minus(tree, literal):
|
||||||
|
"""True if AST tree contains ``dbin - <literal>`` binary op."""
|
||||||
|
import ast
|
||||||
|
for n in ast.walk(tree):
|
||||||
|
if (isinstance(n, ast.BinOp) and isinstance(n.op, ast.Sub)
|
||||||
|
and isinstance(n.left, ast.Name) and n.left.id == "dbin"
|
||||||
|
and isinstance(n.right, ast.Constant)
|
||||||
|
and n.right.value == literal):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_live_path_uses_waveform_config(self):
|
||||||
|
"""RadarDataWorker.__init__ must instantiate WaveformConfig() into
|
||||||
|
self._waveform; _run_host_dsp must read self._waveform.range_resolution_m
|
||||||
|
/ velocity_resolution_mps — not self._settings equivalents."""
|
||||||
|
init = self._parse_method("RadarDataWorker", "__init__")
|
||||||
|
self.assertTrue(self._has_call_to(init, "WaveformConfig"),
|
||||||
|
"RadarDataWorker.__init__ must instantiate WaveformConfig() into self._waveform.")
|
||||||
|
method = self._parse_method("RadarDataWorker", "_run_host_dsp")
|
||||||
|
self.assertTrue(
|
||||||
|
self._has_attribute_chain(method, ("self", "_waveform", "range_resolution_m")),
|
||||||
|
"Live path must read self._waveform.range_resolution_m.")
|
||||||
|
self.assertTrue(
|
||||||
|
self._has_attribute_chain(method, ("self", "_waveform", "velocity_resolution_mps")),
|
||||||
|
"Live path must read self._waveform.velocity_resolution_mps. "
|
||||||
|
"RadarSettings.velocity_resolution default 1.0 caused ~5.34x "
|
||||||
|
"underreport vs replay (test_v7.py:449 pins ~5.343).")
|
||||||
|
self.assertFalse(self._has_attribute_chain(
|
||||||
|
method, ("self", "_settings", "range_resolution")),
|
||||||
|
"Live path still reads stale RadarSettings.range_resolution.")
|
||||||
|
self.assertFalse(self._has_attribute_chain(
|
||||||
|
method, ("self", "_settings", "velocity_resolution")),
|
||||||
|
"Live path still reads stale RadarSettings.velocity_resolution.")
|
||||||
|
|
||||||
|
def test_live_path_doppler_center_not_hardcoded(self):
|
||||||
|
"""_run_host_dsp must derive doppler_center from frame shape, not
|
||||||
|
use hardcoded ``dbin - 16`` — mirrors processing.py:520."""
|
||||||
|
method = self._parse_method("RadarDataWorker", "_run_host_dsp")
|
||||||
|
self.assertFalse(self._has_dbin_minus(method, 16),
|
||||||
|
"Hardcoded doppler_center=16 breaks if frame shape changes. "
|
||||||
|
"Use frame.detections.shape[1] // 2 like processing.py:520.")
|
||||||
|
|
||||||
|
def test_replay_path_still_uses_waveform_config(self):
|
||||||
|
"""Parity half: replay path (ReplayWorker._emit_frame) must keep
|
||||||
|
reading self._waveform.range_resolution_m / velocity_resolution_mps —
|
||||||
|
guards against someone breaking the replay side of the invariant."""
|
||||||
|
method = self._parse_method("ReplayWorker", "_emit_frame")
|
||||||
|
self.assertTrue(self._has_attribute_chain(
|
||||||
|
method, ("self", "_waveform", "range_resolution_m")),
|
||||||
|
"Replay path lost WaveformConfig range source of truth.")
|
||||||
|
self.assertTrue(self._has_attribute_chain(
|
||||||
|
method, ("self", "_waveform", "velocity_resolution_mps")),
|
||||||
|
"Replay path lost WaveformConfig velocity source of truth.")
|
||||||
|
|
||||||
|
|
||||||
class TestSoftwareFPGASignalChain(unittest.TestCase):
|
class TestSoftwareFPGASignalChain(unittest.TestCase):
|
||||||
"""SoftwareFPGA.process_chirps with real co-sim data."""
|
"""SoftwareFPGA.process_chirps with real co-sim data."""
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import numpy as np
|
|||||||
|
|
||||||
from PyQt6.QtCore import QThread, QObject, QTimer, pyqtSignal
|
from PyQt6.QtCore import QThread, QObject, QTimer, pyqtSignal
|
||||||
|
|
||||||
from .models import RadarTarget, GPSData, RadarSettings
|
from .models import RadarTarget, GPSData, RadarSettings, WaveformConfig
|
||||||
from .hardware import (
|
from .hardware import (
|
||||||
RadarAcquisition,
|
RadarAcquisition,
|
||||||
RadarFrame,
|
RadarFrame,
|
||||||
@@ -84,6 +84,7 @@ class RadarDataWorker(QThread):
|
|||||||
self._recorder = recorder
|
self._recorder = recorder
|
||||||
self._gps = gps_data_ref
|
self._gps = gps_data_ref
|
||||||
self._settings = settings or RadarSettings()
|
self._settings = settings or RadarSettings()
|
||||||
|
self._waveform = WaveformConfig()
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
# Frame queue for production RadarAcquisition → this thread
|
# Frame queue for production RadarAcquisition → this thread
|
||||||
@@ -97,6 +98,9 @@ class RadarDataWorker(QThread):
|
|||||||
self._byte_count = 0
|
self._byte_count = 0
|
||||||
self._error_count = 0
|
self._error_count = 0
|
||||||
|
|
||||||
|
def set_waveform(self, wf: "WaveformConfig") -> None:
|
||||||
|
self._waveform = wf
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._running = False
|
self._running = False
|
||||||
if self._acquisition:
|
if self._acquisition:
|
||||||
@@ -169,8 +173,8 @@ class RadarDataWorker(QThread):
|
|||||||
The FPGA already does: FFT, MTI, CFAR, DC notch.
|
The FPGA already does: FFT, MTI, CFAR, DC notch.
|
||||||
Host-side DSP adds: clustering, tracking, geo-coordinate mapping.
|
Host-side DSP adds: clustering, tracking, geo-coordinate mapping.
|
||||||
|
|
||||||
Bin-to-physical conversion uses RadarSettings.range_resolution
|
Bin-to-physical conversion uses self._waveform (WaveformConfig) to keep
|
||||||
and velocity_resolution (should be calibrated to actual waveform).
|
live and replay units aligned. Override via set_waveform() if needed.
|
||||||
"""
|
"""
|
||||||
targets: list[RadarTarget] = []
|
targets: list[RadarTarget] = []
|
||||||
|
|
||||||
@@ -180,8 +184,11 @@ class RadarDataWorker(QThread):
|
|||||||
|
|
||||||
# Extract detections from FPGA CFAR flags
|
# Extract detections from FPGA CFAR flags
|
||||||
det_indices = np.argwhere(frame.detections > 0)
|
det_indices = np.argwhere(frame.detections > 0)
|
||||||
r_res = self._settings.range_resolution
|
r_res = self._waveform.range_resolution_m
|
||||||
v_res = self._settings.velocity_resolution
|
v_res = self._waveform.velocity_resolution_mps
|
||||||
|
n_doppler = (frame.detections.shape[1] if frame.detections.ndim == 2
|
||||||
|
else self._waveform.n_doppler_bins)
|
||||||
|
doppler_center = n_doppler // 2
|
||||||
|
|
||||||
for idx in det_indices:
|
for idx in det_indices:
|
||||||
rbin, dbin = idx
|
rbin, dbin = idx
|
||||||
@@ -190,8 +197,9 @@ class RadarDataWorker(QThread):
|
|||||||
|
|
||||||
# Convert bin indices to physical units
|
# Convert bin indices to physical units
|
||||||
range_m = float(rbin) * r_res
|
range_m = float(rbin) * r_res
|
||||||
# Doppler: centre bin (16) = 0 m/s; positive bins = approaching
|
# Doppler: centre bin = 0 m/s; positive bins = approaching.
|
||||||
velocity_ms = float(dbin - 16) * v_res
|
# Derived from frame shape — mirrors processing.py:520.
|
||||||
|
velocity_ms = float(dbin - doppler_center) * v_res
|
||||||
|
|
||||||
# Apply pitch correction if GPS data available
|
# Apply pitch correction if GPS data available
|
||||||
raw_elev = 0.0 # FPGA doesn't send elevation per-detection
|
raw_elev = 0.0 # FPGA doesn't send elevation per-detection
|
||||||
|
|||||||
@@ -108,12 +108,23 @@ class ConcatWidth:
|
|||||||
|
|
||||||
def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]:
|
def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]:
|
||||||
"""Parse the Opcode enum from radar_protocol.py.
|
"""Parse the Opcode enum from radar_protocol.py.
|
||||||
Returns {opcode_value: OpcodeEntry}.
|
Returns {opcode_value: OpcodeEntry}, excluding MCU_ONLY_OPCODES.
|
||||||
|
MCU-only opcodes have no FPGA case statement and must not appear in
|
||||||
|
the bidirectional Python/Verilog contract check.
|
||||||
"""
|
"""
|
||||||
if filepath is None:
|
if filepath is None:
|
||||||
filepath = GUI_DIR / "radar_protocol.py"
|
filepath = GUI_DIR / "radar_protocol.py"
|
||||||
text = filepath.read_text()
|
text = filepath.read_text()
|
||||||
|
|
||||||
|
# Extract MCU_ONLY_OPCODES set so we can exclude those values below.
|
||||||
|
mcu_only: set[int] = set()
|
||||||
|
m_set = re.search(r'MCU_ONLY_OPCODES[^=]*=\s*frozenset\(\{([^}]*)\}\)', text)
|
||||||
|
if m_set:
|
||||||
|
for tok in m_set.group(1).split(','):
|
||||||
|
tok = tok.strip()
|
||||||
|
if tok.startswith(('0x', '0X')):
|
||||||
|
mcu_only.add(int(tok, 16))
|
||||||
|
|
||||||
# Find the Opcode class body
|
# Find the Opcode class body
|
||||||
match = re.search(r'class Opcode\b.*?(?=\nclass |\Z)', text, re.DOTALL)
|
match = re.search(r'class Opcode\b.*?(?=\nclass |\Z)', text, re.DOTALL)
|
||||||
if not match:
|
if not match:
|
||||||
@@ -123,7 +134,8 @@ def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]
|
|||||||
for m in re.finditer(r'(\w+)\s*=\s*(0x[0-9a-fA-F]+)', match.group()):
|
for m in re.finditer(r'(\w+)\s*=\s*(0x[0-9a-fA-F]+)', match.group()):
|
||||||
name = m.group(1)
|
name = m.group(1)
|
||||||
value = int(m.group(2), 16)
|
value = int(m.group(2), 16)
|
||||||
opcodes[value] = OpcodeEntry(name=name, value=value)
|
if value not in mcu_only:
|
||||||
|
opcodes[value] = OpcodeEntry(name=name, value=value)
|
||||||
return opcodes
|
return opcodes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
[](https://github.com/NawfalMotii79/PLFM_RADAR)
|
[](https://github.com/NawfalMotii79/PLFM_RADAR)
|
||||||
[](https://github.com/NawfalMotii79/PLFM_RADAR/pulls)
|
[](https://github.com/NawfalMotii79/PLFM_RADAR/pulls)
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
AERIS-10 is an open-source, low-cost 10.5 GHz phased array radar system featuring Pulse Linear Frequency Modulated (LFM) modulation. Available in two versions (3km and 20km range), it's designed for researchers, drone developers, and serious SDR enthusiasts who want to explore and experiment with phased array radar technology.
|
AERIS-10 is an open-source, low-cost 10.5 GHz phased array radar system featuring Pulse Linear Frequency Modulated (LFM) modulation. Available in two versions (3km and 20km range), it's designed for researchers, drone developers, and serious SDR enthusiasts who want to explore and experiment with phased array radar technology.
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ The AERIS-10 main sub-systems are:
|
|||||||
|
|
||||||
- **Main Board** containing:
|
- **Main Board** containing:
|
||||||
- **DAC** - Generates the RADAR Chirps
|
- **DAC** - Generates the RADAR Chirps
|
||||||
- **2x Microwave Mixers (LT5552)** - For up-conversion and IF-down-conversion
|
- **2x Microwave Mixers (LTC5552)** - For up-conversion and IF-down-conversion
|
||||||
- **4x 4-Channel Phase Shifters (ADAR1000)** - For RX and TX chain beamforming
|
- **4x 4-Channel Phase Shifters (ADAR1000)** - For RX and TX chain beamforming
|
||||||
- **16x Front End Chips (ADTR1107)** - Used for both Low Noise Amplifying (RX) and Power Amplifying (TX) stages
|
- **16x Front End Chips (ADTR1107)** - Used for both Low Noise Amplifying (RX) and Power Amplifying (TX) stages
|
||||||
- **XC7A50T FPGA** - Handles RADAR Signal Processing on the upstream FTG256 board:
|
- **XC7A50T FPGA** - Handles RADAR Signal Processing on the upstream FTG256 board:
|
||||||
@@ -92,7 +91,7 @@ The AERIS-10 main sub-systems are:
|
|||||||
### Processing Pipeline
|
### Processing Pipeline
|
||||||
|
|
||||||
1. **Waveform Generation** - DAC creates LFM chirps
|
1. **Waveform Generation** - DAC creates LFM chirps
|
||||||
2. **Up/Down Conversion** - LT5552 mixers handle frequency translation
|
2. **Up/Down Conversion** - LTC5552 mixers handle frequency translation
|
||||||
3. **Beam Steering** - ADAR1000 phase shifters control 16 elements
|
3. **Beam Steering** - ADAR1000 phase shifters control 16 elements
|
||||||
4. **Signal Processing (FPGA)**:
|
4. **Signal Processing (FPGA)**:
|
||||||
- Raw ADC data capture
|
- Raw ADC data capture
|
||||||
|
|||||||
Reference in New Issue
Block a user