Files
PLFM_RADAR/9_Firmware/9_3_GUI/smoke_test.py
T
Jason 6a117dd324 fix: resolve ruff lint errors and add lint CI job
Remove unused imports (deque, sys, Opcode, struct, _REPLAY_ADJUSTABLE_OPCODES)
across 4 active Python files and refactor semicolons to separate statements
in radar_protocol.py. Add ruff lint job to CI workflow targeting only the
active files (excludes legacy GUI_V*.py and v7/).
2026-04-08 17:28:22 +03:00

228 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""
AERIS-10 Board Bring-Up Smoke Test — Host-Side Script
======================================================
Sends opcode 0x30 to trigger the FPGA self-test, then reads back
the results via opcode 0x31. Decodes per-subsystem PASS/FAIL and
optionally captures raw ADC samples for offline analysis.
Usage:
python smoke_test.py # Mock mode (no hardware)
python smoke_test.py --live # Real FT2232H hardware
python smoke_test.py --live --adc-dump adc_raw.npy # Capture ADC data
Self-Test Subsystems:
Bit 0: BRAM write/read pattern (walking 1s)
Bit 1: CIC integrator arithmetic
Bit 2: FFT butterfly arithmetic
Bit 3: Saturating add (MTI-style)
Bit 4: ADC raw data capture (256 samples)
Exit codes:
0 = all tests passed
1 = one or more tests failed
2 = communication error / timeout
"""
import sys
import os
import time
import argparse
import logging
import numpy as np
# Add parent directory for radar_protocol import
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from radar_protocol import RadarProtocol, FT2232HConnection
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("smoke_test")
# Self-test opcodes (must match radar_system_top.v command decode)
OPCODE_SELF_TEST_TRIGGER = 0x30
OPCODE_SELF_TEST_RESULT = 0x31
# Result packet format (sent by FPGA after self-test completes):
# The self-test result is reported via the status readback mechanism.
# When the host sends opcode 0x31, the FPGA responds with a status packet
# containing the self-test results in the first status word.
#
# For mock mode, we simulate this directly.
TEST_NAMES = {
0: "BRAM Write/Read Pattern",
1: "CIC Integrator Arithmetic",
2: "FFT Butterfly Arithmetic",
3: "Saturating Add (MTI)",
4: "ADC Raw Data Capture",
}
class SmokeTest:
"""Host-side smoke test controller."""
def __init__(self, connection: FT2232HConnection, adc_dump_path: str = None):
self.conn = connection
self.adc_dump_path = adc_dump_path
self._adc_samples = []
def run(self) -> bool:
"""
Execute the full smoke test sequence.
Returns True if all tests pass, False otherwise.
"""
log.info("=" * 60)
log.info(" AERIS-10 Board Bring-Up Smoke Test")
log.info("=" * 60)
log.info("")
# Step 1: Connect
if not self.conn.is_open:
if not self.conn.open():
log.error("Failed to open FT2232H connection")
return False
# Step 2: Send self-test trigger (opcode 0x30)
log.info("Sending self-test trigger (opcode 0x30)...")
cmd = RadarProtocol.build_command(OPCODE_SELF_TEST_TRIGGER, 1)
if not self.conn.write(cmd):
log.error("Failed to send trigger command")
return False
# Step 3: Wait for completion and read results
log.info("Waiting for self-test completion...")
result = self._wait_for_result(timeout_s=5.0)
if result is None:
log.error("Timeout waiting for self-test results")
return False
# Step 4: Decode results
result_flags, result_detail = result
all_pass = self._decode_results(result_flags, result_detail)
# Step 5: ADC data dump (if requested and test 4 passed)
if self.adc_dump_path and (result_flags & 0x10):
self._save_adc_dump()
# Step 6: Summary
log.info("")
log.info("=" * 60)
if all_pass:
log.info(" SMOKE TEST: ALL PASS")
else:
log.info(" SMOKE TEST: FAILED")
log.info("=" * 60)
return all_pass
def _wait_for_result(self, timeout_s: float):
"""
Poll for self-test result.
Returns (result_flags, result_detail) or None on timeout.
"""
if self.conn._mock:
# Mock: simulate successful self-test after a short delay
time.sleep(0.2)
return (0x1F, 0x00) # All 5 tests pass
deadline = time.time() + timeout_s
while time.time() < deadline:
# Request result readback (opcode 0x31)
cmd = RadarProtocol.build_command(OPCODE_SELF_TEST_RESULT, 0)
self.conn.write(cmd)
time.sleep(0.1)
# Read response
raw = self.conn.read(256)
if raw is None:
continue
# Look for status packet (0xBB header)
packets = RadarProtocol.find_packet_boundaries(raw)
for start, end, ptype in packets:
if ptype == "status":
status = RadarProtocol.parse_status_packet(raw[start:end])
if status is not None:
# Self-test results encoded in status fields
# (This is a simplification — in production, the FPGA
# would have a dedicated self-test result packet type)
result_flags = status.cfar_threshold & 0x1F
result_detail = (status.cfar_threshold >> 8) & 0xFF
return (result_flags, result_detail)
time.sleep(0.1)
return None
def _decode_results(self, flags: int, detail: int) -> bool:
"""Decode and display per-test results. Returns True if all pass."""
log.info("")
log.info("Self-Test Results:")
log.info("-" * 40)
all_pass = True
for bit, name in TEST_NAMES.items():
passed = bool(flags & (1 << bit))
status = "PASS" if passed else "FAIL"
marker = "" if passed else ""
log.info(f" {marker} Test {bit}: {name:30s} [{status}]")
if not passed:
all_pass = False
log.info("-" * 40)
log.info(f" Result flags: 0b{flags:05b}")
log.info(f" Detail byte: 0x{detail:02X}")
if detail == 0xAD:
log.warning(" Detail 0xAD = ADC timeout (no ADC data received)")
elif detail != 0x00:
log.info(f" Detail indicates first BRAM fail at addr[3:0] = {detail & 0x0F}")
return all_pass
def _save_adc_dump(self):
"""Save captured ADC samples to numpy file."""
if not self._adc_samples:
# In mock mode, generate synthetic ADC data
if self.conn._mock:
self._adc_samples = list(np.random.randint(0, 65536, 256, dtype=np.uint16))
if self._adc_samples:
arr = np.array(self._adc_samples, dtype=np.uint16)
np.save(self.adc_dump_path, arr)
log.info(f"ADC raw data saved: {self.adc_dump_path} ({len(arr)} samples)")
else:
log.warning("No ADC samples captured for dump")
def main():
parser = argparse.ArgumentParser(description="AERIS-10 Board Smoke Test")
parser.add_argument("--live", action="store_true",
help="Use real FT2232H hardware (default: mock)")
parser.add_argument("--device", type=int, default=0,
help="FT2232H device index")
parser.add_argument("--adc-dump", type=str, default=None,
help="Save raw ADC samples to .npy file")
args = parser.parse_args()
mock_mode = not args.live
conn = FT2232HConnection(mock=mock_mode)
tester = SmokeTest(conn, adc_dump_path=args.adc_dump)
success = tester.run()
if conn.is_open:
conn.close()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()