/ 9_Firmware / 9_3_GUI / smoke_test.py
smoke_test.py
  1  #!/usr/bin/env python3
  2  """
  3  AERIS-10 Board Bring-Up Smoke Test — Host-Side Script
  4  ======================================================
  5  Sends opcode 0x30 to trigger the FPGA self-test, then reads back
  6  the results via opcode 0x31. Decodes per-subsystem PASS/FAIL and
  7  optionally captures raw ADC samples for offline analysis.
  8  
  9  Usage:
 10    python smoke_test.py              # Mock mode (no hardware)
 11    python smoke_test.py --live       # Real FT2232H hardware
 12    python smoke_test.py --live --adc-dump adc_raw.npy  # Capture ADC data
 13  
 14  Self-Test Subsystems:
 15    Bit 0: BRAM write/read pattern (walking 1s)
 16    Bit 1: CIC integrator arithmetic
 17    Bit 2: FFT butterfly arithmetic
 18    Bit 3: Saturating add (MTI-style)
 19    Bit 4: ADC raw data capture (256 samples)
 20  
 21  Exit codes:
 22    0 = all tests passed
 23    1 = one or more tests failed
 24    2 = communication error / timeout
 25  """
 26  
 27  import sys
 28  import os
 29  import time
 30  import struct
 31  import argparse
 32  import logging
 33  
 34  import numpy as np
 35  
 36  # Add parent directory for radar_protocol import
 37  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 38  from radar_protocol import RadarProtocol, FT2232HConnection
 39  
 40  logging.basicConfig(
 41      level=logging.INFO,
 42      format="%(asctime)s [%(levelname)s] %(message)s",
 43      datefmt="%H:%M:%S",
 44  )
 45  log = logging.getLogger("smoke_test")
 46  
 47  # Self-test opcodes (must match radar_system_top.v command decode)
 48  OPCODE_SELF_TEST_TRIGGER = 0x30
 49  OPCODE_SELF_TEST_RESULT  = 0x31
 50  
 51  # Result packet format (sent by FPGA after self-test completes):
 52  # The self-test result is reported via the status readback mechanism.
 53  # When the host sends opcode 0x31, the FPGA responds with a status packet
 54  # containing the self-test results in the first status word.
 55  #
 56  # For mock mode, we simulate this directly.
 57  
 58  TEST_NAMES = {
 59      0: "BRAM Write/Read Pattern",
 60      1: "CIC Integrator Arithmetic",
 61      2: "FFT Butterfly Arithmetic",
 62      3: "Saturating Add (MTI)",
 63      4: "ADC Raw Data Capture",
 64  }
 65  
 66  
 67  class SmokeTest:
 68      """Host-side smoke test controller."""
 69  
 70      def __init__(self, connection: FT2232HConnection, adc_dump_path: str = None):
 71          self.conn = connection
 72          self.adc_dump_path = adc_dump_path
 73          self._adc_samples = []
 74  
 75      def run(self) -> bool:
 76          """
 77          Execute the full smoke test sequence.
 78          Returns True if all tests pass, False otherwise.
 79          """
 80          log.info("=" * 60)
 81          log.info("  AERIS-10 Board Bring-Up Smoke Test")
 82          log.info("=" * 60)
 83          log.info("")
 84  
 85          # Step 1: Connect
 86          if not self.conn.is_open:
 87              if not self.conn.open():
 88                  log.error("Failed to open FT2232H connection")
 89                  return False
 90  
 91          # Step 2: Send self-test trigger (opcode 0x30)
 92          log.info("Sending self-test trigger (opcode 0x30)...")
 93          cmd = RadarProtocol.build_command(OPCODE_SELF_TEST_TRIGGER, 1)
 94          if not self.conn.write(cmd):
 95              log.error("Failed to send trigger command")
 96              return False
 97  
 98          # Step 3: Wait for completion and read results
 99          log.info("Waiting for self-test completion...")
100          result = self._wait_for_result(timeout_s=5.0)
101  
102          if result is None:
103              log.error("Timeout waiting for self-test results")
104              return False
105  
106          # Step 4: Decode results
107          result_flags, result_detail = result
108          all_pass = self._decode_results(result_flags, result_detail)
109  
110          # Step 5: ADC data dump (if requested and test 4 passed)
111          if self.adc_dump_path and (result_flags & 0x10):
112              self._save_adc_dump()
113  
114          # Step 6: Summary
115          log.info("")
116          log.info("=" * 60)
117          if all_pass:
118              log.info("  SMOKE TEST: ALL PASS")
119          else:
120              log.info("  SMOKE TEST: FAILED")
121          log.info("=" * 60)
122  
123          return all_pass
124  
125      def _wait_for_result(self, timeout_s: float):
126          """
127          Poll for self-test result.
128          Returns (result_flags, result_detail) or None on timeout.
129          """
130          if self.conn._mock:
131              # Mock: simulate successful self-test after a short delay
132              time.sleep(0.2)
133              return (0x1F, 0x00)  # All 5 tests pass
134  
135          deadline = time.time() + timeout_s
136          while time.time() < deadline:
137              # Request result readback (opcode 0x31)
138              cmd = RadarProtocol.build_command(OPCODE_SELF_TEST_RESULT, 0)
139              self.conn.write(cmd)
140              time.sleep(0.1)
141  
142              # Read response
143              raw = self.conn.read(256)
144              if raw is None:
145                  continue
146  
147              # Look for status packet (0xBB header)
148              packets = RadarProtocol.find_packet_boundaries(raw)
149              for start, end, ptype in packets:
150                  if ptype == "status":
151                      status = RadarProtocol.parse_status_packet(raw[start:end])
152                      if status is not None:
153                          # Self-test results encoded in status fields
154                          # (This is a simplification — in production, the FPGA
155                          #  would have a dedicated self-test result packet type)
156                          result_flags = status.cfar_threshold & 0x1F
157                          result_detail = (status.cfar_threshold >> 8) & 0xFF
158                          return (result_flags, result_detail)
159  
160              time.sleep(0.1)
161  
162          return None
163  
164      def _decode_results(self, flags: int, detail: int) -> bool:
165          """Decode and display per-test results. Returns True if all pass."""
166          log.info("")
167          log.info("Self-Test Results:")
168          log.info("-" * 40)
169  
170          all_pass = True
171          for bit, name in TEST_NAMES.items():
172              passed = bool(flags & (1 << bit))
173              status = "PASS" if passed else "FAIL"
174              marker = "✓" if passed else "✗"
175              log.info(f"  {marker} Test {bit}: {name:30s} [{status}]")
176              if not passed:
177                  all_pass = False
178  
179          log.info("-" * 40)
180          log.info(f"  Result flags:  0b{flags:05b}")
181          log.info(f"  Detail byte:   0x{detail:02X}")
182  
183          if detail == 0xAD:
184              log.warning("  Detail 0xAD = ADC timeout (no ADC data received)")
185          elif detail != 0x00:
186              log.info(f"  Detail indicates first BRAM fail at addr[3:0] = {detail & 0x0F}")
187  
188          return all_pass
189  
190      def _save_adc_dump(self):
191          """Save captured ADC samples to numpy file."""
192          if not self._adc_samples:
193              # In mock mode, generate synthetic ADC data
194              if self.conn._mock:
195                  self._adc_samples = list(np.random.randint(0, 65536, 256, dtype=np.uint16))
196  
197          if self._adc_samples:
198              arr = np.array(self._adc_samples, dtype=np.uint16)
199              np.save(self.adc_dump_path, arr)
200              log.info(f"ADC raw data saved: {self.adc_dump_path} ({len(arr)} samples)")
201          else:
202              log.warning("No ADC samples captured for dump")
203  
204  
205  def main():
206      parser = argparse.ArgumentParser(description="AERIS-10 Board Smoke Test")
207      parser.add_argument("--live", action="store_true",
208                          help="Use real FT2232H hardware (default: mock)")
209      parser.add_argument("--device", type=int, default=0,
210                          help="FT2232H device index")
211      parser.add_argument("--adc-dump", type=str, default=None,
212                          help="Save raw ADC samples to .npy file")
213      args = parser.parse_args()
214  
215      mock_mode = not args.live
216      conn = FT2232HConnection(mock=mock_mode)
217  
218      tester = SmokeTest(conn, adc_dump_path=args.adc_dump)
219      success = tester.run()
220  
221      if conn.is_open:
222          conn.close()
223  
224      sys.exit(0 if success else 1)
225  
226  
227  if __name__ == "__main__":
228      main()