feat(gui): add FT601Connection class, USB interface selection in V65/V7

- Add FT601Connection in radar_protocol.py using ftd3xx library with
  proper setChipConfiguration re-enumeration handling (close, wait 2s,
  re-open) and 4-byte write alignment
- Add USB Interface dropdown to V65 Tk GUI (FT2232H default, FT601 option)
- Add USB Interface combo to V7 PyQt dashboard with Live/File mode toggle
- Fix mock frame_start bit 7 in both FT2232H and FT601 connections
- Use FPGA range data from USB packets instead of recomputing in Python
- Export FT601Connection from v7/hardware.py and v7/__init__.py
- Add 7 FT601Connection tests (91 total in test_GUI_V65_Tk.py)
This commit is contained in:
Jason
2026-04-16 16:19:13 +05:45
parent f393e96d69
commit b22cadb429
6 changed files with 336 additions and 28 deletions
+38 -10
View File
@@ -59,7 +59,7 @@ except (ModuleNotFoundError, ImportError):
# Import protocol layer (no GUI deps) # Import protocol layer (no GUI deps)
from radar_protocol import ( from radar_protocol import (
RadarProtocol, FT2232HConnection, RadarProtocol, FT2232HConnection, FT601Connection,
DataRecorder, RadarAcquisition, DataRecorder, RadarAcquisition,
RadarFrame, StatusResponse, RadarFrame, StatusResponse,
NUM_RANGE_BINS, NUM_DOPPLER_BINS, WATERFALL_DEPTH, NUM_RANGE_BINS, NUM_DOPPLER_BINS, WATERFALL_DEPTH,
@@ -388,10 +388,11 @@ class RadarDashboard:
BANDWIDTH = 500e6 # Hz — chirp bandwidth BANDWIDTH = 500e6 # Hz — chirp bandwidth
C = 3e8 # m/s — speed of light C = 3e8 # m/s — speed of light
def __init__(self, root: tk.Tk, connection: FT2232HConnection, def __init__(self, root: tk.Tk, mock: bool,
recorder: DataRecorder, device_index: int = 0): recorder: DataRecorder, device_index: int = 0):
self.root = root self.root = root
self.conn = connection self._mock = mock
self.conn: FT2232HConnection | FT601Connection | None = None
self.recorder = recorder self.recorder = recorder
self.device_index = device_index self.device_index = device_index
@@ -485,6 +486,16 @@ class RadarDashboard:
style="Accent.TButton") style="Accent.TButton")
self.btn_connect.pack(side="right", padx=4) self.btn_connect.pack(side="right", padx=4)
# USB Interface selector (production FT2232H / premium FT601)
self._usb_iface_var = tk.StringVar(value="FT2232H (Production)")
self.cmb_usb_iface = ttk.Combobox(
top, textvariable=self._usb_iface_var,
values=["FT2232H (Production)", "FT601 (Premium)"],
state="readonly", width=20,
)
self.cmb_usb_iface.pack(side="right", padx=4)
ttk.Label(top, text="USB:", font=("Menlo", 10)).pack(side="right")
self.btn_record = ttk.Button(top, text="Record", command=self._on_record) self.btn_record = ttk.Button(top, text="Record", command=self._on_record)
self.btn_record.pack(side="right", padx=4) self.btn_record.pack(side="right", padx=4)
@@ -1018,15 +1029,17 @@ class RadarDashboard:
# ------------------------------------------------------------ Actions # ------------------------------------------------------------ Actions
def _on_connect(self): def _on_connect(self):
if self.conn.is_open: if self.conn is not None and self.conn.is_open:
# Disconnect # Disconnect
if self._acq_thread is not None: if self._acq_thread is not None:
self._acq_thread.stop() self._acq_thread.stop()
self._acq_thread.join(timeout=2) self._acq_thread.join(timeout=2)
self._acq_thread = None self._acq_thread = None
self.conn.close() self.conn.close()
self.conn = None
self.lbl_status.config(text="DISCONNECTED", foreground=RED) self.lbl_status.config(text="DISCONNECTED", foreground=RED)
self.btn_connect.config(text="Connect") self.btn_connect.config(text="Connect")
self.cmb_usb_iface.config(state="readonly")
log.info("Disconnected") log.info("Disconnected")
return return
@@ -1036,6 +1049,16 @@ class RadarDashboard:
if self._replay_active: if self._replay_active:
self._replay_stop() self._replay_stop()
# Create connection based on USB Interface selector
iface = self._usb_iface_var.get()
if "FT601" in iface:
self.conn = FT601Connection(mock=self._mock)
else:
self.conn = FT2232HConnection(mock=self._mock)
# Disable interface selector while connecting/connected
self.cmb_usb_iface.config(state="disabled")
# Open connection in a background thread to avoid blocking the GUI # Open connection in a background thread to avoid blocking the GUI
self.lbl_status.config(text="CONNECTING...", foreground=YELLOW) self.lbl_status.config(text="CONNECTING...", foreground=YELLOW)
self.btn_connect.config(state="disabled") self.btn_connect.config(state="disabled")
@@ -1062,6 +1085,8 @@ class RadarDashboard:
else: else:
self.lbl_status.config(text="CONNECT FAILED", foreground=RED) self.lbl_status.config(text="CONNECT FAILED", foreground=RED)
self.btn_connect.config(text="Connect") self.btn_connect.config(text="Connect")
self.cmb_usb_iface.config(state="readonly")
self.conn = None
def _on_record(self): def _on_record(self):
if self.recorder.recording: if self.recorder.recording:
@@ -1110,6 +1135,9 @@ class RadarDashboard:
f"Opcode 0x{opcode:02X} is hardware-only (ignored in replay)")) f"Opcode 0x{opcode:02X} is hardware-only (ignored in replay)"))
return return
cmd = RadarProtocol.build_command(opcode, value) cmd = RadarProtocol.build_command(opcode, value)
if self.conn is None:
log.warning("No connection — command not sent")
return
ok = self.conn.write(cmd) ok = self.conn.write(cmd)
log.info(f"CMD 0x{opcode:02X} val={value} ({'OK' if ok else 'FAIL'})") log.info(f"CMD 0x{opcode:02X} val={value} ({'OK' if ok else 'FAIL'})")
@@ -1148,7 +1176,7 @@ class RadarDashboard:
if self._replay_active or self._replay_ctrl is not None: if self._replay_active or self._replay_ctrl is not None:
self._replay_stop() self._replay_stop()
if self._acq_thread is not None: if self._acq_thread is not None:
if self.conn.is_open: if self.conn is not None and self.conn.is_open:
self._on_connect() # disconnect self._on_connect() # disconnect
else: else:
# Connection dropped unexpectedly — just clean up the thread # Connection dropped unexpectedly — just clean up the thread
@@ -1547,17 +1575,17 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if args.live: if args.live:
conn = FT2232HConnection(mock=False) mock = False
mode_str = "LIVE" mode_str = "LIVE"
else: else:
conn = FT2232HConnection(mock=True) mock = True
mode_str = "MOCK" mode_str = "MOCK"
recorder = DataRecorder() recorder = DataRecorder()
root = tk.Tk() root = tk.Tk()
dashboard = RadarDashboard(root, conn, recorder, device_index=args.device) dashboard = RadarDashboard(root, mock, recorder, device_index=args.device)
if args.record: if args.record:
filepath = os.path.join( filepath = os.path.join(
@@ -1582,8 +1610,8 @@ def main():
if dashboard._acq_thread is not None: if dashboard._acq_thread is not None:
dashboard._acq_thread.stop() dashboard._acq_thread.stop()
dashboard._acq_thread.join(timeout=2) dashboard._acq_thread.join(timeout=2)
if conn.is_open: if dashboard.conn is not None and dashboard.conn.is_open:
conn.close() dashboard.conn.close()
if recorder.recording: if recorder.recording:
recorder.stop() recorder.stop()
root.destroy() root.destroy()
+200 -6
View File
@@ -6,6 +6,7 @@ Pure-logic module for USB packet parsing and command building.
No GUI dependencies — safe to import from tests and headless scripts. No GUI dependencies — safe to import from tests and headless scripts.
USB Interface: FT2232H USB 2.0 (8-bit, 50T production board) via pyftdi USB Interface: FT2232H USB 2.0 (8-bit, 50T production board) via pyftdi
FT601 USB 3.0 (32-bit, 200T premium board) via ftd3xx
USB Packet Protocol (11-byte): USB Packet Protocol (11-byte):
TX (FPGA→Host): TX (FPGA→Host):
@@ -22,7 +23,7 @@ import queue
import logging import logging
import contextlib import contextlib
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any, ClassVar
from enum import IntEnum from enum import IntEnum
@@ -200,7 +201,9 @@ class RadarProtocol:
range_i = _to_signed16(struct.unpack_from(">H", raw, 3)[0]) range_i = _to_signed16(struct.unpack_from(">H", raw, 3)[0])
doppler_i = _to_signed16(struct.unpack_from(">H", raw, 5)[0]) doppler_i = _to_signed16(struct.unpack_from(">H", raw, 5)[0])
doppler_q = _to_signed16(struct.unpack_from(">H", raw, 7)[0]) doppler_q = _to_signed16(struct.unpack_from(">H", raw, 7)[0])
detection = raw[9] & 0x01 det_byte = raw[9]
detection = det_byte & 0x01
frame_start = (det_byte >> 7) & 0x01
return { return {
"range_i": range_i, "range_i": range_i,
@@ -208,6 +211,7 @@ class RadarProtocol:
"doppler_i": doppler_i, "doppler_i": doppler_i,
"doppler_q": doppler_q, "doppler_q": doppler_q,
"detection": detection, "detection": detection,
"frame_start": frame_start,
} }
@staticmethod @staticmethod
@@ -433,7 +437,191 @@ class FT2232HConnection:
pkt += struct.pack(">h", np.clip(range_i, -32768, 32767)) pkt += struct.pack(">h", np.clip(range_i, -32768, 32767))
pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767)) pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767))
pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767)) pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767))
pkt.append(detection & 0x01) # Bit 7 = frame_start (sample_counter == 0), bit 0 = detection
det_byte = (detection & 0x01) | (0x80 if idx == 0 else 0x00)
pkt.append(det_byte)
pkt.append(FOOTER_BYTE)
buf += pkt
self._mock_seq_idx = (start_idx + num_packets) % NUM_CELLS
return bytes(buf)
# ============================================================================
# FT601 USB 3.0 Connection (premium board only)
# ============================================================================
# Optional ftd3xx import (FTDI's proprietary driver for FT60x USB 3.0 chips).
# pyftdi does NOT support FT601 — it only handles USB 2.0 chips (FT232H, etc.)
try:
import ftd3xx # type: ignore[import-untyped]
FTD3XX_AVAILABLE = True
_Ftd3xxError: type = ftd3xx.FTD3XXError # type: ignore[attr-defined]
except ImportError:
FTD3XX_AVAILABLE = False
_Ftd3xxError = OSError # fallback for type-checking; never raised
class FT601Connection:
"""
FT601 USB 3.0 SuperSpeed FIFO bridge — premium board only.
The FT601 has a 32-bit data bus and runs at 100 MHz.
VID:PID = 0x0403:0x6030 or 0x6031 (FTDI FT60x).
Requires the ``ftd3xx`` library (``pip install ftd3xx`` on Windows,
or ``libft60x`` on Linux). This is FTDI's proprietary USB 3.0 driver;
``pyftdi`` only supports USB 2.0 and will NOT work with FT601.
Public contract matches FT2232HConnection so callers can swap freely.
"""
VID = 0x0403
PID_LIST: ClassVar[list[int]] = [0x6030, 0x6031]
def __init__(self, mock: bool = True):
self._mock = mock
self._dev = None
self._lock = threading.Lock()
self.is_open = False
# Mock state (reuses same synthetic data pattern)
self._mock_frame_num = 0
self._mock_rng = np.random.RandomState(42)
def open(self, device_index: int = 0) -> bool:
if self._mock:
self.is_open = True
log.info("FT601 mock device opened (no hardware)")
return True
if not FTD3XX_AVAILABLE:
log.error(
"ftd3xx library required for FT601 hardware — "
"install with: pip install ftd3xx"
)
return False
try:
self._dev = ftd3xx.create(device_index, ftd3xx.OPEN_BY_INDEX)
if self._dev is None:
log.error("No FT601 device found at index %d", device_index)
return False
# Verify chip configuration — only reconfigure if needed.
# setChipConfiguration triggers USB re-enumeration, which
# invalidates the device handle and requires a re-open cycle.
cfg = self._dev.getChipConfiguration()
needs_reconfig = (
cfg.FIFOMode != 0 # 245 FIFO mode
or cfg.ChannelConfig != 0 # 1 channel, 32-bit
or cfg.OptionalFeatureSupport != 0
)
if needs_reconfig:
cfg.FIFOMode = 0
cfg.ChannelConfig = 0
cfg.OptionalFeatureSupport = 0
self._dev.setChipConfiguration(cfg)
# Device re-enumerates — close stale handle, wait, re-open
self._dev.close()
self._dev = None
import time
time.sleep(2.0) # wait for USB re-enumeration
self._dev = ftd3xx.create(device_index, ftd3xx.OPEN_BY_INDEX)
if self._dev is None:
log.error("FT601 not found after reconfiguration")
return False
log.info("FT601 reconfigured and re-opened (index %d)", device_index)
self.is_open = True
log.info("FT601 device opened (index %d)", device_index)
return True
except (OSError, _Ftd3xxError) as e:
log.error("FT601 open failed: %s", e)
self._dev = None
return False
def close(self):
if self._dev is not None:
with contextlib.suppress(Exception):
self._dev.close()
self._dev = None
self.is_open = False
def read(self, size: int = 4096) -> bytes | None:
"""Read raw bytes from FT601. Returns None on error/timeout."""
if not self.is_open:
return None
if self._mock:
return self._mock_read(size)
with self._lock:
try:
data = self._dev.readPipe(0x82, size, raw=True)
return bytes(data) if data else None
except (OSError, _Ftd3xxError) as e:
log.error("FT601 read error: %s", e)
return None
def write(self, data: bytes) -> bool:
"""Write raw bytes to FT601. Data must be 4-byte aligned for 32-bit bus."""
if not self.is_open:
return False
if self._mock:
log.info(f"FT601 mock write: {data.hex()}")
return True
# Pad to 4-byte alignment (FT601 32-bit bus requirement).
# NOTE: Radar commands are already 4 bytes, so this should be a no-op.
remainder = len(data) % 4
if remainder:
data = data + b"\x00" * (4 - remainder)
with self._lock:
try:
written = self._dev.writePipe(0x02, data, raw=True)
return written == len(data)
except (OSError, _Ftd3xxError) as e:
log.error("FT601 write error: %s", e)
return False
def _mock_read(self, size: int) -> bytes:
"""Generate synthetic radar packets (same pattern as FT2232H mock)."""
time.sleep(0.05)
self._mock_frame_num += 1
buf = bytearray()
num_packets = min(NUM_CELLS, size // DATA_PACKET_SIZE)
start_idx = getattr(self, "_mock_seq_idx", 0)
for n in range(num_packets):
idx = (start_idx + n) % NUM_CELLS
rbin = idx // NUM_DOPPLER_BINS
dbin = idx % NUM_DOPPLER_BINS
range_i = int(self._mock_rng.normal(0, 100))
range_q = int(self._mock_rng.normal(0, 100))
if abs(rbin - 20) < 3:
range_i += 5000
range_q += 3000
dop_i = int(self._mock_rng.normal(0, 50))
dop_q = int(self._mock_rng.normal(0, 50))
if abs(rbin - 20) < 3 and abs(dbin - 8) < 2:
dop_i += 8000
dop_q += 4000
detection = 1 if (abs(rbin - 20) < 2 and abs(dbin - 8) < 2) else 0
pkt = bytearray()
pkt.append(HEADER_BYTE)
pkt += struct.pack(">h", np.clip(range_q, -32768, 32767))
pkt += struct.pack(">h", np.clip(range_i, -32768, 32767))
pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767))
pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767))
# Bit 7 = frame_start (sample_counter == 0), bit 0 = detection
det_byte = (detection & 0x01) | (0x80 if idx == 0 else 0x00)
pkt.append(det_byte)
pkt.append(FOOTER_BYTE) pkt.append(FOOTER_BYTE)
buf += pkt buf += pkt
@@ -600,6 +788,12 @@ class RadarAcquisition(threading.Thread):
if sample.get("detection", 0): if sample.get("detection", 0):
self._frame.detections[rbin, dbin] = 1 self._frame.detections[rbin, dbin] = 1
self._frame.detection_count += 1 self._frame.detection_count += 1
# Accumulate FPGA range profile data (matched-filter output)
# Each sample carries the range_i/range_q for this range bin.
# Accumulate magnitude across Doppler bins for the range profile.
ri = int(sample.get("range_i", 0))
rq = int(sample.get("range_q", 0))
self._frame.range_profile[rbin] += abs(ri) + abs(rq)
self._sample_idx += 1 self._sample_idx += 1
@@ -607,11 +801,11 @@ class RadarAcquisition(threading.Thread):
self._finalize_frame() self._finalize_frame()
def _finalize_frame(self): def _finalize_frame(self):
"""Complete frame: compute range profile, push to queue, record.""" """Complete frame: push to queue, record."""
self._frame.timestamp = time.time() self._frame.timestamp = time.time()
self._frame.frame_number = self._frame_num self._frame.frame_number = self._frame_num
# Range profile = sum of magnitude across Doppler bins # range_profile is already accumulated from FPGA range_i/range_q
self._frame.range_profile = np.sum(self._frame.magnitude, axis=1) # data in _ingest_sample(). No need to synthesize from doppler magnitude.
# Push to display queue (drop old if backed up) # Push to display queue (drop old if backed up)
try: try:
+56 -1
View File
@@ -16,7 +16,7 @@ import unittest
import numpy as np import numpy as np
from radar_protocol import ( from radar_protocol import (
RadarProtocol, FT2232HConnection, DataRecorder, RadarAcquisition, RadarProtocol, FT2232HConnection, FT601Connection, DataRecorder, RadarAcquisition,
RadarFrame, StatusResponse, Opcode, RadarFrame, StatusResponse, Opcode,
HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE, HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE,
NUM_RANGE_BINS, NUM_DOPPLER_BINS, NUM_RANGE_BINS, NUM_DOPPLER_BINS,
@@ -312,6 +312,61 @@ class TestFT2232HConnection(unittest.TestCase):
self.assertFalse(conn.write(b"\x00\x00\x00\x00")) self.assertFalse(conn.write(b"\x00\x00\x00\x00"))
class TestFT601Connection(unittest.TestCase):
"""Test mock FT601 connection (mirrors FT2232H tests)."""
def test_mock_open_close(self):
conn = FT601Connection(mock=True)
self.assertTrue(conn.open())
self.assertTrue(conn.is_open)
conn.close()
self.assertFalse(conn.is_open)
def test_mock_read_returns_data(self):
conn = FT601Connection(mock=True)
conn.open()
data = conn.read(4096)
self.assertIsNotNone(data)
self.assertGreater(len(data), 0)
conn.close()
def test_mock_read_contains_valid_packets(self):
"""Mock data should contain parseable data packets."""
conn = FT601Connection(mock=True)
conn.open()
raw = conn.read(4096)
packets = RadarProtocol.find_packet_boundaries(raw)
self.assertGreater(len(packets), 0)
for start, end, ptype in packets:
if ptype == "data":
result = RadarProtocol.parse_data_packet(raw[start:end])
self.assertIsNotNone(result)
conn.close()
def test_mock_write(self):
conn = FT601Connection(mock=True)
conn.open()
cmd = RadarProtocol.build_command(0x01, 1)
self.assertTrue(conn.write(cmd))
conn.close()
def test_write_pads_to_4_bytes(self):
"""FT601 write() should pad data to 4-byte alignment."""
conn = FT601Connection(mock=True)
conn.open()
# 3-byte payload should be padded internally (no error)
self.assertTrue(conn.write(b"\x01\x02\x03"))
conn.close()
def test_read_when_closed(self):
conn = FT601Connection(mock=True)
self.assertIsNone(conn.read())
def test_write_when_closed(self):
conn = FT601Connection(mock=True)
self.assertFalse(conn.write(b"\x00\x00\x00\x00"))
class TestDataRecorder(unittest.TestCase): class TestDataRecorder(unittest.TestCase):
"""Test HDF5 recording (skipped if h5py not available).""" """Test HDF5 recording (skipped if h5py not available)."""
+2 -1
View File
@@ -26,6 +26,7 @@ from .models import (
# Hardware interfaces — production protocol via radar_protocol.py # Hardware interfaces — production protocol via radar_protocol.py
from .hardware import ( from .hardware import (
FT2232HConnection, FT2232HConnection,
FT601Connection,
RadarProtocol, RadarProtocol,
Opcode, Opcode,
RadarAcquisition, RadarAcquisition,
@@ -89,7 +90,7 @@ __all__ = [ # noqa: RUF022
"USB_AVAILABLE", "FTDI_AVAILABLE", "SCIPY_AVAILABLE", "USB_AVAILABLE", "FTDI_AVAILABLE", "SCIPY_AVAILABLE",
"SKLEARN_AVAILABLE", "FILTERPY_AVAILABLE", "SKLEARN_AVAILABLE", "FILTERPY_AVAILABLE",
# hardware — production FPGA protocol # hardware — production FPGA protocol
"FT2232HConnection", "RadarProtocol", "Opcode", "FT2232HConnection", "FT601Connection", "RadarProtocol", "Opcode",
"RadarAcquisition", "RadarFrame", "StatusResponse", "DataRecorder", "RadarAcquisition", "RadarFrame", "StatusResponse", "DataRecorder",
"STM32USBInterface", "STM32USBInterface",
# processing # processing
+39 -10
View File
@@ -13,13 +13,14 @@ RadarDashboard is a QMainWindow with six tabs:
6. Settings — Host-side DSP parameters + About section 6. Settings — Host-side DSP parameters + About section
Uses production radar_protocol.py for all FPGA communication: Uses production radar_protocol.py for all FPGA communication:
- FT2232HConnection for real hardware - FT2232HConnection for production board (FT2232H USB 2.0)
- FT601Connection for premium board (FT601 USB 3.0) — selectable from GUI
- Unified replay via SoftwareFPGA + ReplayEngine + ReplayWorker - Unified replay via SoftwareFPGA + ReplayEngine + ReplayWorker
- Mock mode (FT2232HConnection(mock=True)) for development - Mock mode (FT2232HConnection(mock=True)) for development
The old STM32 magic-packet start flow has been removed. FPGA registers The old STM32 magic-packet start flow has been removed. FPGA registers
are controlled directly via 4-byte {opcode, addr, value_hi, value_lo} are controlled directly via 4-byte {opcode, addr, value_hi, value_lo}
commands sent over FT2232H. commands sent over FT2232H or FT601.
""" """
from __future__ import annotations from __future__ import annotations
@@ -55,6 +56,7 @@ from .models import (
) )
from .hardware import ( from .hardware import (
FT2232HConnection, FT2232HConnection,
FT601Connection,
RadarProtocol, RadarProtocol,
RadarFrame, RadarFrame,
StatusResponse, StatusResponse,
@@ -142,7 +144,7 @@ class RadarDashboard(QMainWindow):
) )
# Hardware interfaces — production protocol # Hardware interfaces — production protocol
self._connection: FT2232HConnection | None = None self._connection: FT2232HConnection | FT601Connection | None = None
self._stm32 = STM32USBInterface() self._stm32 = STM32USBInterface()
self._recorder = DataRecorder() self._recorder = DataRecorder()
@@ -364,7 +366,7 @@ class RadarDashboard(QMainWindow):
# Row 0: connection mode + device combos + buttons # Row 0: connection mode + device combos + buttons
ctrl_layout.addWidget(QLabel("Mode:"), 0, 0) ctrl_layout.addWidget(QLabel("Mode:"), 0, 0)
self._mode_combo = QComboBox() self._mode_combo = QComboBox()
self._mode_combo.addItems(["Mock", "Live FT2232H", "Replay"]) self._mode_combo.addItems(["Mock", "Live", "Replay"])
self._mode_combo.setCurrentIndex(0) self._mode_combo.setCurrentIndex(0)
ctrl_layout.addWidget(self._mode_combo, 0, 1) ctrl_layout.addWidget(self._mode_combo, 0, 1)
@@ -377,6 +379,13 @@ class RadarDashboard(QMainWindow):
refresh_btn.clicked.connect(self._refresh_devices) refresh_btn.clicked.connect(self._refresh_devices)
ctrl_layout.addWidget(refresh_btn, 0, 4) ctrl_layout.addWidget(refresh_btn, 0, 4)
# USB Interface selector (production FT2232H / premium FT601)
ctrl_layout.addWidget(QLabel("USB Interface:"), 0, 5)
self._usb_iface_combo = QComboBox()
self._usb_iface_combo.addItems(["FT2232H (Production)", "FT601 (Premium)"])
self._usb_iface_combo.setCurrentIndex(0)
ctrl_layout.addWidget(self._usb_iface_combo, 0, 6)
self._start_btn = QPushButton("Start Radar") self._start_btn = QPushButton("Start Radar")
self._start_btn.setStyleSheet( self._start_btn.setStyleSheet(
f"QPushButton {{ background-color: {DARK_SUCCESS}; color: white; font-weight: bold; }}" f"QPushButton {{ background-color: {DARK_SUCCESS}; color: white; font-weight: bold; }}"
@@ -1001,7 +1010,8 @@ class RadarDashboard(QMainWindow):
self._conn_ft2232h = self._make_status_label("FT2232H") self._conn_ft2232h = self._make_status_label("FT2232H")
self._conn_stm32 = self._make_status_label("STM32 USB") self._conn_stm32 = self._make_status_label("STM32 USB")
conn_layout.addWidget(QLabel("FT2232H:"), 0, 0) self._conn_usb_label = QLabel("USB Data:")
conn_layout.addWidget(self._conn_usb_label, 0, 0)
conn_layout.addWidget(self._conn_ft2232h, 0, 1) conn_layout.addWidget(self._conn_ft2232h, 0, 1)
conn_layout.addWidget(QLabel("STM32 USB:"), 1, 0) conn_layout.addWidget(QLabel("STM32 USB:"), 1, 0)
conn_layout.addWidget(self._conn_stm32, 1, 1) conn_layout.addWidget(self._conn_stm32, 1, 1)
@@ -1167,7 +1177,7 @@ class RadarDashboard(QMainWindow):
about_lbl = QLabel( about_lbl = QLabel(
"<b>AERIS-10 Radar System V7</b><br>" "<b>AERIS-10 Radar System V7</b><br>"
"PyQt6 Edition with Embedded Leaflet Map<br><br>" "PyQt6 Edition with Embedded Leaflet Map<br><br>"
"<b>Data Interface:</b> FT2232H USB 2.0 (production protocol)<br>" "<b>Data Interface:</b> FT2232H USB 2.0 (production) / FT601 USB 3.0 (premium)<br>"
"<b>FPGA Protocol:</b> 4-byte register commands, 0xAA/0xBB packets<br>" "<b>FPGA Protocol:</b> 4-byte register commands, 0xAA/0xBB packets<br>"
"<b>Map:</b> OpenStreetMap + Leaflet.js<br>" "<b>Map:</b> OpenStreetMap + Leaflet.js<br>"
"<b>Framework:</b> PyQt6 + QWebEngine<br>" "<b>Framework:</b> PyQt6 + QWebEngine<br>"
@@ -1224,7 +1234,7 @@ class RadarDashboard(QMainWindow):
# ===================================================================== # =====================================================================
def _send_fpga_cmd(self, opcode: int, value: int): def _send_fpga_cmd(self, opcode: int, value: int):
"""Send a 4-byte register command to the FPGA via FT2232H.""" """Send a 4-byte register command to the FPGA via USB (FT2232H or FT601)."""
if self._connection is None or not self._connection.is_open: if self._connection is None or not self._connection.is_open:
logger.warning(f"Cannot send 0x{opcode:02X}={value}: no connection") logger.warning(f"Cannot send 0x{opcode:02X}={value}: no connection")
return return
@@ -1287,16 +1297,26 @@ class RadarDashboard(QMainWindow):
if "Mock" in mode: if "Mock" in mode:
self._replay_mode = False self._replay_mode = False
self._connection = FT2232HConnection(mock=True) iface = self._usb_iface_combo.currentText()
if "FT601" in iface:
self._connection = FT601Connection(mock=True)
else:
self._connection = FT2232HConnection(mock=True)
if not self._connection.open(): if not self._connection.open():
QMessageBox.critical(self, "Error", "Failed to open mock connection.") QMessageBox.critical(self, "Error", "Failed to open mock connection.")
return return
elif "Live" in mode: elif "Live" in mode:
self._replay_mode = False self._replay_mode = False
self._connection = FT2232HConnection(mock=False) iface = self._usb_iface_combo.currentText()
if "FT601" in iface:
self._connection = FT601Connection(mock=False)
iface_name = "FT601"
else:
self._connection = FT2232HConnection(mock=False)
iface_name = "FT2232H"
if not self._connection.open(): if not self._connection.open():
QMessageBox.critical(self, "Error", QMessageBox.critical(self, "Error",
"Failed to open FT2232H. Check USB connection.") f"Failed to open {iface_name}. Check USB connection.")
return return
elif "Replay" in mode: elif "Replay" in mode:
self._replay_mode = True self._replay_mode = True
@@ -1368,6 +1388,7 @@ class RadarDashboard(QMainWindow):
self._start_btn.setEnabled(False) self._start_btn.setEnabled(False)
self._stop_btn.setEnabled(True) self._stop_btn.setEnabled(True)
self._mode_combo.setEnabled(False) self._mode_combo.setEnabled(False)
self._usb_iface_combo.setEnabled(False)
self._demo_btn_main.setEnabled(False) self._demo_btn_main.setEnabled(False)
self._demo_btn_map.setEnabled(False) self._demo_btn_map.setEnabled(False)
n_frames = self._replay_engine.total_frames n_frames = self._replay_engine.total_frames
@@ -1417,6 +1438,7 @@ class RadarDashboard(QMainWindow):
self._start_btn.setEnabled(False) self._start_btn.setEnabled(False)
self._stop_btn.setEnabled(True) self._stop_btn.setEnabled(True)
self._mode_combo.setEnabled(False) self._mode_combo.setEnabled(False)
self._usb_iface_combo.setEnabled(False)
self._demo_btn_main.setEnabled(False) self._demo_btn_main.setEnabled(False)
self._demo_btn_map.setEnabled(False) self._demo_btn_map.setEnabled(False)
self._status_label_main.setText(f"Status: Running ({mode})") self._status_label_main.setText(f"Status: Running ({mode})")
@@ -1462,6 +1484,7 @@ class RadarDashboard(QMainWindow):
self._start_btn.setEnabled(True) self._start_btn.setEnabled(True)
self._stop_btn.setEnabled(False) self._stop_btn.setEnabled(False)
self._mode_combo.setEnabled(True) self._mode_combo.setEnabled(True)
self._usb_iface_combo.setEnabled(True)
self._demo_btn_main.setEnabled(True) self._demo_btn_main.setEnabled(True)
self._demo_btn_map.setEnabled(True) self._demo_btn_map.setEnabled(True)
self._status_label_main.setText("Status: Radar stopped") self._status_label_main.setText("Status: Radar stopped")
@@ -1954,6 +1977,12 @@ class RadarDashboard(QMainWindow):
self._set_conn_indicator(self._conn_ft2232h, conn_open) self._set_conn_indicator(self._conn_ft2232h, conn_open)
self._set_conn_indicator(self._conn_stm32, self._stm32.is_open) self._set_conn_indicator(self._conn_stm32, self._stm32.is_open)
# Update USB label to reflect which interface is active
if isinstance(self._connection, FT601Connection):
self._conn_usb_label.setText("FT601:")
else:
self._conn_usb_label.setText("FT2232H:")
gps_count = self._gps_packet_count gps_count = self._gps_packet_count
if self._gps_worker: if self._gps_worker:
gps_count = self._gps_worker.gps_count gps_count = self._gps_worker.gps_count
+1
View File
@@ -25,6 +25,7 @@ if USB_AVAILABLE:
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from radar_protocol import ( # noqa: F401 — re-exported for v7 package from radar_protocol import ( # noqa: F401 — re-exported for v7 package
FT2232HConnection, FT2232HConnection,
FT601Connection,
RadarProtocol, RadarProtocol,
Opcode, Opcode,
RadarAcquisition, RadarAcquisition,