From c2e8283f52934144e00c7a12966769c010ae01ca Mon Sep 17 00:00:00 2001 From: boringethan Date: Tue, 21 Apr 2026 17:53:48 -0700 Subject: [PATCH 1/4] feat: migrate to MotionInterface stable-handle API Pairs with openmotion-sdk feature/connection-redesign. motion_connector.py: - on_connected/on_disconnected/on_data_received collapsed into a single _on_handle_state_changed(handle, old, new, reason) wired to each of the three stable handles (console/left/right). Branches on handle.name. - All `motion_interface.console_module` -> `motion_interface.console`. - All `motion_interface.sensors[X]` and `.sensors.get(X)` patterns -> `motion_interface.left` / `motion_interface.right` directly, or `getattr(motion_interface, side, None)` where side is dynamic. - Console-status thread teardown moved into the new state handler's DISCONNECTED branch. motion_singleton.py: - Drop acquire_motion_interface() (removed from SDK). Just construct MotionInterface(); main.py calls .start() once at startup. main.py: - Drop qasync QEventLoop + asyncio glue. Call motion_interface.start() before app.exec() and motion_interface.stop() in handle_exit. The SDK's daemon monitor thread runs alongside the Qt event loop; no bridging needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- main.py | 56 +++------- motion_connector.py | 249 ++++++++++++++++++++++---------------------- motion_singleton.py | 10 +- 3 files changed, 146 insertions(+), 169 deletions(-) diff --git a/main.py b/main.py index 12bfde0..3d2ce3d 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,13 @@ import sys import os -import asyncio import warnings import logging import argparse from PyQt6.QtGui import QGuiApplication, QIcon from PyQt6.QtQml import QQmlApplicationEngine, qmlRegisterSingletonInstance -from qasync import QEventLoop from motion_connector import MOTIONConnector +from motion_singleton import motion_interface from version import get_version # set PYTHONPATH=%cd%\..\OpenMOTION-PyLib;%PYTHONPATH% @@ -93,56 +92,27 @@ def main(): print("Error: Failed to load QML file") sys.exit(-1) - loop = QEventLoop(app) - asyncio.set_event_loop(loop) - - async def main_async(): - """Start MOTION monitoring before event loop runs.""" - logger.info("Starting MOTION monitoring...") - await connector._interface.start_monitoring() - - async def shutdown(): - """Ensure MOTIONConnector stops monitoring before closing.""" - logger.info("Shutting down MOTION monitoring...") - connector._interface.stop_monitoring() - - pending_tasks = [t for t in asyncio.all_tasks() if not t.done()] - if pending_tasks: - logger.info(f"Cancelling {len(pending_tasks)} pending tasks...") - for task in pending_tasks: - task.cancel() - await asyncio.gather(*pending_tasks, return_exceptions=True) - - logger.info("LIFU monitoring stopped. Application shutting down.") + # The SDK now owns its own daemon connection-monitor thread; no + # asyncio loop required. start() returns once any already-attached + # devices have completed their CONNECTING transition (or wait_timeout). + logger.info("Starting MOTION monitoring...") + motion_interface.start(wait=True, wait_timeout=2.0) def handle_exit(): - """Ensure QML cleans up before Python exit without blocking.""" + """Stop the monitor cleanly before Qt tears down.""" logger.info("Application closing...") + try: + motion_interface.stop() + except Exception as e: + logger.warning("Error stopping MotionInterface: %s", e) + engine.deleteLater() - # Schedule shutdown but do NOT block the loop - asyncio.ensure_future(shutdown()).add_done_callback(lambda _: loop.stop()) - - engine.deleteLater() # Ensure QML engine is destroyed - - # Connect shutdown process to app quit event app.aboutToQuit.connect(handle_exit) try: - with loop: - loop.run_until_complete(main_async()) - loop.run_forever() - except RuntimeError as e: - if "Event loop stopped before Future completed" in str(e): - # Graceful shutdown — expected if closing while a future is active - logger.warning( - "App closed while a Future was still running (safe to ignore)" - ) - else: - logger.error(f"Runtime error: {e}") + sys.exit(app.exec()) except KeyboardInterrupt: logger.info("Application interrupted by user.") - finally: - loop.close() if __name__ == "__main__": diff --git a/motion_connector.py b/motion_connector.py index 8f57c94..2f572c5 100644 --- a/motion_connector.py +++ b/motion_connector.py @@ -326,7 +326,7 @@ def run(self): self.progress.emit(-1, "Requesting DFU mode…") self._connector._console_mutex.lock() try: - ok = motion_interface.console_module.enter_dfu() + ok = motion_interface.console.enter_dfu() finally: self._connector._console_mutex.unlock() @@ -410,7 +410,7 @@ def run(self): if self._target == "CONSOLE": self._connector._console_mutex.lock() try: - ok = motion_interface.console_module.enter_dfu() + ok = motion_interface.console.enter_dfu() finally: self._connector._console_mutex.unlock() else: @@ -419,7 +419,7 @@ def run(self): sensor_tag = "left" if self._target == "SENSOR_LEFT" else "right" sensor_mutex.lock() try: - ok = motion_interface.sensors[sensor_tag].enter_dfu() + ok = getattr(motion_interface, sensor_tag).enter_dfu() finally: sensor_mutex.unlock() @@ -593,7 +593,7 @@ def _download_jed_from_github(self) -> Path: def _program_jed(self, jed_path: Path, channels: list) -> None: programmer = FpgaPageProgrammer( - motion_interface.console_module, + motion_interface.console, verify=self._verify, erase_timeout=35.0, refresh_timeout=10.0, @@ -647,7 +647,7 @@ def run(self): CAMERA_MASK = 0xFF # All cameras else: CAMERA_MASK = 1 << (self.camera_index - 1) - status_map = motion_interface.sensors["left"].get_camera_status(CAMERA_MASK) + status_map = motion_interface.left.get_camera_status(CAMERA_MASK) if not status_map: logger.error("Failed to get camera status map.") return None @@ -668,7 +668,7 @@ def run(self): logger.debug(f"FPGA configuration started for camera {cam_idx + 1}") start_time = time.time() - if not motion_interface.sensors["left"].program_fpga( + if not motion_interface.left.program_fpga( camera_position=(1 << cam_idx), manual_process=False ): logger.error(f"Failed to program FPGA for camera {cam_idx + 1}") @@ -680,7 +680,7 @@ def run(self): if not (status & (1 << 1) and status & (1 << 3)): # Not configured self.update_status.emit(f"conf {cam_idx + 1}") logger.debug(f"Configuring registers for camera {cam_idx + 1}") - if not motion_interface.sensors["left"].camera_configure_registers( + if not motion_interface.left.camera_configure_registers( 1 << cam_idx ): logger.error( @@ -690,14 +690,14 @@ def run(self): logger.debug("Setting test pattern...") self.update_status.emit("set live") - if not motion_interface.sensors["left"].camera_configure_test_pattern( + if not motion_interface.left.camera_configure_test_pattern( CAMERA_MASK, 0x04 ): logger.error("Failed to set test pattern.") return None # Get status - status_map = motion_interface.sensors["left"].get_camera_status(CAMERA_MASK) + status_map = motion_interface.left.get_camera_status(CAMERA_MASK) if not status_map: logger.error("Failed to get camera status.") return None @@ -710,7 +710,7 @@ def run(self): logger.error(f"Camera {cam_idx + 1} missing in status map.") return None logger.debug( - f"Camera {self.camera_index} status: 0x{status:02X} - {motion_interface.sensors['left'].decode_camera_status(status)}" + f"Camera {self.camera_index} status: 0x{status:02X} - {motion_interface.left.decode_camera_status(status)}" ) if not ( @@ -724,14 +724,14 @@ def run(self): start_time = time.time() try: logger.debug("Capturing histogram...") - if not motion_interface.sensors["left"].camera_capture_histogram( + if not motion_interface.left.camera_capture_histogram( CAMERA_MASK ): logger.error("Capture failed.") else: logger.debug("Capture successful, retrieving histogram...") time.sleep(0.005) # Wait for capture to complete - histogram = motion_interface.sensors["left"].camera_get_histogram( + histogram = motion_interface.left.camera_get_histogram( CAMERA_MASK ) if histogram is None: @@ -1271,10 +1271,13 @@ def _load_laser_params(self, config_dir): return [] def connect_signals(self): - """Connect LIFUInterface signals to QML.""" - motion_interface.signal_connect.connect(self.on_connected) - motion_interface.signal_disconnect.connect(self.on_disconnected) - motion_interface.signal_data_received.connect(self.on_data_received) + """Subscribe to per-handle state changes on the SDK interface.""" + for handle in ( + motion_interface.console, + motion_interface.left, + motion_interface.right, + ): + handle.signal_state_changed.connect(self._on_handle_state_changed) def _get_fpga_scale(self, label: str, name: str): """Retrieve the scale factor for a given `label` and function `name` from models/FpgaModel.js. @@ -1514,7 +1517,7 @@ def _start_runlog(self): # _console_mutex is a QRecursiveMutex so re-locking is safe if we're already in startTrigger self._console_mutex.lock() try: - fw_ver = motion_interface.console_module.get_version() + fw_ver = motion_interface.console.get_version() finally: self._console_mutex.unlock() except Exception as e: @@ -1583,7 +1586,7 @@ def set_laser_power_from_config(self, interface): # ------------------------------------------------------------------ user_cfg: dict = {} try: - cfg_obj = interface.console_module.read_config() + cfg_obj = interface.console.read_config() if cfg_obj is not None: user_cfg = cfg_obj.json_data or {} print(user_cfg) @@ -1667,7 +1670,7 @@ def set_laser_power_from_config(self, interface): f"data={[f'0x{b:02X}' for b in dataToSend]}" ) - if not interface.console_module.write_i2c_packet( + if not interface.console.write_i2c_packet( mux_index=muxIdx, channel=channel, device_addr=i2cAddr, @@ -1701,7 +1704,7 @@ def _write_drive_cl(ch: int, thresh, gain: float, label: str) -> bool: f"[Connector] Writing user-config {label} DRIVE CL: " f"raw={raw}, gain={gain_f} → {[f'0x{b:02X}' for b in data]}" ) - return interface.console_module.write_i2c_packet( + return interface.console.write_i2c_packet( mux_index=1, channel=ch, device_addr=0x41, reg_addr=0x10, data=data ) @@ -1725,7 +1728,7 @@ def getUserConfigJson(self) -> str: editor. Returns '{}' on error or when no config is available. """ try: - cfg = motion_interface.console_module.read_config() + cfg = motion_interface.console.read_config() if cfg is None: return "{}" # cfg.json_data expected to be a dict-like object @@ -1763,7 +1766,7 @@ def _worker(text): self.userConfigError.emit(msg) return - cfg = motion_interface.console_module.read_config() + cfg = motion_interface.console.read_config() if cfg is None: msg = "Failed to read existing user config from device" logger.error(msg) @@ -1772,7 +1775,7 @@ def _worker(text): cfg.json_data = parsed - updated = motion_interface.console_module.write_config(cfg) + updated = motion_interface.console.write_config(cfg) if updated is None: msg = "Failed to write user configuration to device" logger.error(msg) @@ -1942,7 +1945,7 @@ def powerCamerasOn(self, target: str): f"Enabling camera power mask=0x{MASK_ALL:02X} on {target.capitalize()}" ) - ok = motion_interface.sensors[target].enable_camera_power(MASK_ALL) + ok = getattr(motion_interface, target).enable_camera_power(MASK_ALL) if ok: logger.info(f"{target.capitalize()}: Power enabled") else: @@ -1959,7 +1962,7 @@ def powerCamerasOff(self, target: str): f"Disabling camera power mask=0x{MASK_ALL:02X} on {target.capitalize()}" ) - ok = motion_interface.sensors[target].disable_camera_power(MASK_ALL) + ok = getattr(motion_interface, target).disable_camera_power(MASK_ALL) if ok: logger.info(f"{target.capitalize()}: Power disabled") else: @@ -1987,8 +1990,8 @@ def captureHistogramToCSV( f"Capturing {capture_type} for {sensor_side} camera {camera_index} with SN {serial_number}" ) - sensor = self._interface.sensors.get(sensor_side) - if sensor is None: + sensor = getattr(self._interface, sensor_side, None) + if sensor is None or not sensor.is_connected(): logger.error("%s sensor not connected.", sensor_side.capitalize()) return @@ -2016,9 +2019,9 @@ def captureHistogramToCSV( # Get camera temperature try: - temperature = self._interface.sensors[ - sensor_side - ].imu_get_temperature() + temperature = getattr( + self._interface, sensor_side + ).imu_get_temperature() logger.info(f"Camera temperature: {temperature}°C") except Exception as e: logger.error(f"Failed to get camera temperature: {e}") @@ -2229,46 +2232,48 @@ def _calculate_weighted_mean_std_dev(self, histogram_data): logger.error(f"Error calculating weighted mean: {e}") return 0.0, 0.0 - @pyqtSlot(str, str) - def on_connected(self, descriptor, port): - """Handle device connection.""" - print(f"Device connected: {descriptor} on port {port}") - if descriptor.upper() == "SENSOR_LEFT": - self._leftSensorConnected = True - if descriptor.upper() == "SENSOR_RIGHT": - self._rightSensorConnected = True - elif descriptor.upper() == "CONSOLE": - self._consoleConnected = True - - self.signalConnected.emit(descriptor, port) - self.connectionStatusChanged.emit() - self.update_state() + def _on_handle_state_changed(self, handle, old, new, reason): + """Single state-change handler wired to console/left/right. - @pyqtSlot(str, str) - def on_disconnected(self, descriptor, port): - """Handle device disconnection.""" - if descriptor.upper() == "SENSOR_LEFT": - self._leftSensorConnected = False - elif descriptor.upper() == "SENSOR_RIGHT": - self._rightSensorConnected = False - elif descriptor.upper() == "CONSOLE": - self._consoleConnected = False - - # Stop status thread - if self._console_status_thread: - self._console_status_thread.stop() - self._console_status_thread = None + Replaces the old signal_connect/signal_disconnect pair. ``handle`` + is the stable MotionConsole/MotionSensor instance; ``new`` is a + ConnectionState enum. + """ + from omotion import ConnectionState + + is_now_connected = (new == ConnectionState.CONNECTED) + is_now_lost = (new == ConnectionState.DISCONNECTED) + name = handle.name + + if name == "left": + if is_now_connected: + self._leftSensorConnected = True + elif is_now_lost: + self._leftSensorConnected = False + elif name == "right": + if is_now_connected: + self._rightSensorConnected = True + elif is_now_lost: + self._rightSensorConnected = False + elif name == "console": + if is_now_connected: + self._consoleConnected = True + elif is_now_lost: + self._consoleConnected = False + if self._console_status_thread: + self._console_status_thread.stop() + self._console_status_thread = None + + if is_now_connected: + self.signalConnected.emit(name, "") + elif is_now_lost: + self.signalDisconnected.emit(name, "") + # CONNECTING/DISCONNECTING are intermediate; UI doesn't need a + # legacy connect/disconnect emission for them. - self.signalDisconnected.emit(descriptor, port) self.connectionStatusChanged.emit() self.update_state() - @pyqtSlot(str, str) - def on_data_received(self, descriptor, message): - """Handle incoming data from the MOTION device.""" - logger.info(f"Data received from {descriptor}: {message}") - self.signalDataReceived.emit(descriptor, message) - @pyqtSlot(str) def querySensorInfo(self, target: str): """Fetch and emit device information with mutex protection and event-based UI updates.""" @@ -2279,9 +2284,9 @@ def querySensorInfo(self, target: str): mutex.lock() try: - fw_version = motion_interface.sensors[sensor_tag].get_version() + fw_version = getattr(motion_interface, sensor_tag).get_version() logger.info(f"Version: {fw_version}") - hw_id = motion_interface.sensors[sensor_tag].get_hardware_id() + hw_id = getattr(motion_interface, sensor_tag).get_hardware_id() device_id = base58.b58encode(bytes.fromhex(hw_id)).decode() # Emit signal for async UI update self.sensorDeviceInfoReceived.emit(fw_version, device_id) @@ -2302,11 +2307,11 @@ def queryConsoleInfo(self): """Fetch and emit device information.""" self._console_mutex.lock() try: - fw_version = motion_interface.console_module.get_version() + fw_version = motion_interface.console.get_version() logger.info(f"Version: {fw_version}") - hw_id = motion_interface.console_module.get_hardware_id() + hw_id = motion_interface.console.get_hardware_id() device_id = base58.b58encode(bytes.fromhex(hw_id)).decode() - board_id = motion_interface.console_module.read_board_id() + board_id = motion_interface.console.read_board_id() self.consoleDeviceInfoReceived.emit(fw_version, device_id, str(board_id)) logger.info( f"Console Device Info - Firmware: {fw_version}, Device ID: {device_id}, Board ID: {board_id}" @@ -2324,7 +2329,7 @@ def queryConsoleLatestVersionInfo(self): return self._console_mutex.lock() try: - info = motion_interface.console_module.get_latest_version_info() + info = motion_interface.console.get_latest_version_info() logger.info(f"Latest version info: {info}") # Emit whatever structure the console module returns (QVariant-compatible) self.latestVersionInfoReceived.emit(info) @@ -2352,7 +2357,7 @@ def querySensorLatestVersionInfo(self, target: str): mutex.lock() try: # sensor modules may expose get_latest_version_info similar to console - info = motion_interface.sensors[sensor_tag].get_latest_version_info() + info = getattr(motion_interface, sensor_tag).get_latest_version_info() logger.info(f"Latest sensor ({sensor_tag}) version info: {info}") self.latestSensorVersionInfoReceived.emit(target, info) finally: @@ -2577,7 +2582,7 @@ def queryConsoleTemperature(self): """Fetch and emit Console Temperature data.""" self._console_mutex.lock() try: - temp1, temp2, temp3 = motion_interface.console_module.get_temperatures() + temp1, temp2, temp3 = motion_interface.console.get_temperatures() logger.info( f"Console Temperature Data - Temp1: {temp1}, Temp2: {temp2}, Temp3: {temp3}" ) @@ -2597,9 +2602,9 @@ def querySensorTemperature(self, target: str): mutex.lock() try: - imu_temp = motion_interface.sensors[ - sensor_tag - ].imu_get_temperature() + imu_temp = getattr( + motion_interface, sensor_tag + ).imu_get_temperature() logger.info(f"Temperature Data - IMU Temp: {imu_temp}") # Emit signal for async UI update self.temperatureSensorUpdated.emit(imu_temp) @@ -2621,7 +2626,7 @@ def setRGBState(self, state): logger.error(f"Invalid RGB state value: {state}") return - if motion_interface.console_module.set_rgb_led(state) == state: + if motion_interface.console.set_rgb_led(state) == state: logger.info(f"RGB state set to: {state}") else: logger.error(f"Failed to set RGB state to: {state}") @@ -2635,7 +2640,7 @@ def queryRGBState(self): """Fetch and emit RGB state.""" self._console_mutex.lock() try: - state = motion_interface.console_module.get_rgb_led() + state = motion_interface.console.get_rgb_led() state_text = {0: "Off", 1: "IND1", 2: "IND2", 3: "IND3"}.get( state, "Unknown" ) @@ -2671,7 +2676,7 @@ def queryFpgaVersions(self): try: for name, mux_idx, channel, i2c_addr, reg_addr in FPGAS: try: - data, data_len = motion_interface.console_module.read_i2c_packet( + data, data_len = motion_interface.console.read_i2c_packet( mux_index=mux_idx, channel=channel, device_addr=i2c_addr, @@ -2699,7 +2704,7 @@ def queryFpgaVersions(self): def queryTriggerConfig(self): self._console_mutex.lock() try: - trigger_setting = motion_interface.console_module.get_trigger_json() + trigger_setting = motion_interface.console.get_trigger_json() if trigger_setting: if isinstance(trigger_setting, str): updateTrigger = json.loads(trigger_setting) @@ -2725,7 +2730,7 @@ def setTrigger(self, triggerjson): try: json_trigger_data = json.loads(triggerjson) - trigger_setting = motion_interface.console_module.set_trigger_json( + trigger_setting = motion_interface.console.set_trigger_json( data=json_trigger_data ) if trigger_setting: @@ -2757,7 +2762,7 @@ def startTrigger(self, triggerjson=None): if triggerjson: json_trigger_data = json.loads(triggerjson) - trigger_setting = motion_interface.console_module.set_trigger_json( + trigger_setting = motion_interface.console.set_trigger_json( data=json_trigger_data ) if not trigger_setting: @@ -2766,7 +2771,7 @@ def startTrigger(self, triggerjson=None): logger.info(f"Trigger Setting: {trigger_setting}") - success = motion_interface.console_module.start_trigger() + success = motion_interface.console.start_trigger() if success: # Start the per-run log now self._start_runlog() @@ -2819,7 +2824,7 @@ def stopTrigger(self): # (4) Tell console to stop firing self._console_mutex.lock() try: - motion_interface.console_module.stop_trigger() + motion_interface.console.stop_trigger() finally: self._console_mutex.unlock() @@ -2843,7 +2848,7 @@ def querySensorAccelerometer(self, target: str): mutex.lock() try: - accel = motion_interface.sensors[sensor_tag].imu_get_accelerometer() + accel = getattr(motion_interface, sensor_tag).imu_get_accelerometer() logger.info( f"Accel (raw): X={accel[0]}, Y={accel[1]}, Z={accel[2]}" ) @@ -2861,7 +2866,7 @@ def querySensorAccelerometer(self, target: str): def querySensorGyroscope(self): """Fetch and emit Gyroscope data.""" try: - gyro = motion_interface.sensors["left"].imu_get_gyroscope() + gyro = motion_interface.left.imu_get_gyroscope() logger.info(f"Gyro (raw): X={gyro[0]}, Y={gyro[1]}, Z={gyro[2]}") self.gyroscopeSensorUpdated.emit(gyro[0], gyro[1], gyro[2]) except Exception as e: @@ -2882,7 +2887,7 @@ def configureCamera(self, target: str, cam_mask: int): # firmware ACKs OW_FPGA_PROG_SRAM before the FPGA is actually # usable, so an immediate camera_configure_registers races the # FPGA bringup and intermittently fails. - sensor = motion_interface.sensors[sensor_tag] + sensor = getattr(motion_interface, sensor_tag) cam_pos = cam_mask.bit_length() - 1 status_map = sensor.get_camera_status(cam_mask) @@ -2912,17 +2917,17 @@ def configureCamera(self, target: str, cam_mask: int): exposure = 600 print(f"Switching camera to {cam_mask}") cam_position = cam_mask.bit_length() - 1 - passed_sw = motion_interface.sensors[sensor_tag].switch_camera( + passed_sw = getattr(motion_interface, sensor_tag).switch_camera( cam_position ) print(f"Setting gain to {gain}") - passed_gain = motion_interface.sensors[sensor_tag].camera_set_gain( + passed_gain = getattr(motion_interface, sensor_tag).camera_set_gain( gain ) print(f"Setting exposure to {exposure}") - passed_exposure = motion_interface.sensors[ - sensor_tag - ].camera_set_exposure(0, us=exposure) + passed_exposure = getattr( + motion_interface, sensor_tag + ).camera_set_exposure(0, us=exposure) print( f"Camera {sensor_tag} with mask {cam_mask} configured with gain {gain} and exposure {exposure}" ) @@ -2955,7 +2960,7 @@ def sendPingCommand(self, target: str): try: if target == "CONSOLE": self._console_mutex.lock() - if motion_interface.console_module.ping(): + if motion_interface.console.ping(): logger.info("Ping command sent successfully") return True else: @@ -2963,7 +2968,7 @@ def sendPingCommand(self, target: str): return False elif target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": sensor_tag = "left" if target == "SENSOR_LEFT" else "right" - if motion_interface.sensors[sensor_tag].ping(): + if getattr(motion_interface, sensor_tag).ping(): logger.info("Ping command sent successfully") return True else: @@ -2986,7 +2991,7 @@ def sendLedToggleCommand(self, target: str): if target == "CONSOLE": self._console_mutex.lock() try: - if motion_interface.console_module.toggle_led(): + if motion_interface.console.toggle_led(): logger.info("Toggle command sent successfully") return True else: @@ -3000,7 +3005,7 @@ def sendLedToggleCommand(self, target: str): mutex.lock() try: - if motion_interface.sensors[sensor_tag].toggle_led(): + if getattr(motion_interface, sensor_tag).toggle_led(): logger.info("Toggle command sent successfully") return True else: @@ -3022,12 +3027,12 @@ def sendEchoCommand(self, target: str): expected_data = b"Hello FROM Test Application!" if target == "CONSOLE": self._console_mutex.lock() - echoed_data, data_len = motion_interface.console_module.echo( + echoed_data, data_len = motion_interface.console.echo( echo_data=expected_data ) elif target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": sensor_tag = "left" if target == "SENSOR_LEFT" else "right" - echoed_data, data_len = motion_interface.sensors[sensor_tag].echo( + echoed_data, data_len = getattr(motion_interface, sensor_tag).echo( echo_data=expected_data ) else: @@ -3053,7 +3058,7 @@ def getFsyncCount(self): """Get the Fsync count from the console.""" self._console_mutex.lock() try: - fsync_count = motion_interface.console_module.get_fsync_pulsecount() + fsync_count = motion_interface.console.get_fsync_pulsecount() logger.info(f"Fsync Count: {fsync_count}") return fsync_count except Exception as e: @@ -3067,7 +3072,7 @@ def getLsyncCount(self): """Get the Fsync count from the console.""" self._console_mutex.lock() try: - lsync_count = motion_interface.console_module.get_lsync_pulsecount() + lsync_count = motion_interface.console.get_lsync_pulsecount() logger.debug(f"Lsync Count: {lsync_count}") return lsync_count except Exception as e: @@ -3096,7 +3101,7 @@ def i2cReadBytes( if target == "CONSOLE": self._console_mutex.lock() fpga_data, fpga_data_len = ( - motion_interface.console_module.read_i2c_packet( + motion_interface.console.read_i2c_packet( mux_index=mux_idx, channel=channel, device_addr=i2c_addr, @@ -3155,7 +3160,7 @@ def i2cWriteBytes( if target == "CONSOLE": self._console_mutex.lock() - if motion_interface.console_module.write_i2c_packet( + if motion_interface.console.write_i2c_packet( mux_index=mux_idx, channel=channel, device_addr=i2c_addr, @@ -3183,13 +3188,13 @@ def softResetSensor(self, target: str): self._console_mutex.lock() try: if target == "CONSOLE": - if motion_interface.console_module.soft_reset(): + if motion_interface.console.soft_reset(): logger.info("Software Reset Sent") else: logger.error("Failed to send Software Reset") elif target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": sensor_tag = "left" if target == "SENSOR_LEFT" else "right" - if motion_interface.sensors[sensor_tag].soft_reset(): + if getattr(motion_interface, sensor_tag).soft_reset(): logger.info("Software Reset Sent") else: logger.error("Failed to send Software Reset") @@ -3202,7 +3207,7 @@ def softResetSensor(self, target: str): def scanI2C(self, mux: int, chan: int) -> list[str]: self._console_mutex.lock() try: - addresses = motion_interface.console_module.scan_i2c_mux_channel(mux, chan) + addresses = motion_interface.console.scan_i2c_mux_channel(mux, chan) hex_addresses = [hex(addr) for addr in addresses] logger.info(f"Devices found on MUX {mux} channel {chan}: {hex_addresses}") return hex_addresses @@ -3215,7 +3220,7 @@ def scanI2C(self, mux: int, chan: int) -> list[str]: def getTecEnabled(self) -> bool: self._console_mutex.lock() try: - self._tec_dac = motion_interface.console_module.tec_voltage() + self._tec_dac = motion_interface.console.tec_voltage() logger.info(f"TEC DAC Setting: {self._tec_dac}") self.tecDacChanged.emit() return True @@ -3230,7 +3235,7 @@ def setFanLevel(self, speed: int) -> bool: """Set console fan PWM level (0..100).""" self._console_mutex.lock() try: - if motion_interface.console_module.set_fan_speed(fan_speed=speed) == speed: + if motion_interface.console.set_fan_speed(fan_speed=speed) == speed: logger.info(f"Fan set to {speed}%") return True logger.error("Failed to set Fan Speed") @@ -3254,7 +3259,7 @@ def readFanFeedback(self): rpms = [] for fan_idx in range(1, 4): try: - rpm = motion_interface.console_module.get_fan_rpm(fan_index=fan_idx) + rpm = motion_interface.console.get_fan_rpm(fan_index=fan_idx) rpms.append(int(rpm) if rpm is not None and rpm >= 0 else -1) except Exception as e: logger.error(f"Error reading fan {fan_idx} RPM: {e}") @@ -3297,7 +3302,7 @@ def setTecTrip(self, res: int) -> bool: self._console_mutex.lock() try: # Delegate to console module - result = motion_interface.console_module.set_ta_gain_resistor(res) + result = motion_interface.console.set_ta_gain_resistor(res) if result: logger.info(f"TA gain resistor set to {res} ohms") return True @@ -3330,7 +3335,7 @@ def readUserConfig(self): def _do_read_user_config(self): self._console_mutex.lock() try: - config = motion_interface.console_module.read_config() + config = motion_interface.console.read_config() if config is None: msg = "Failed to read user configuration from device" logger.error(msg) @@ -3382,7 +3387,7 @@ def setUserConfig( def _do_write_user_config(self, tec_trip, opt_gain, opt_thresh, ee_gain, ee_thresh): self._console_mutex.lock() try: - config = motion_interface.console_module.read_config() + config = motion_interface.console.read_config() if config is None: msg = "Failed to read user configuration before writing" logger.error(msg) @@ -3407,7 +3412,7 @@ def _do_write_user_config(self, tec_trip, opt_gain, opt_thresh, ee_gain, ee_thre config.set("EE_GAIN", ee_gain) config.set("EE_THRESH", ee_thresh) - updated = motion_interface.console_module.write_config(config) + updated = motion_interface.console.write_config(config) if updated is None: msg = "Failed to write user configuration to device" logger.error(msg) @@ -3486,8 +3491,8 @@ def getCameraHistogram( self, target: str, camera_index: int, test_pattern_id: int = 4 ): logger.info(f"Getting histogram for camera {camera_index + 1}") - sensor = motion_interface.sensors.get(target) - if sensor is None: + sensor = getattr(motion_interface, target, None) + if sensor is None or not sensor.is_connected(): logger.error("%s sensor not connected.", target.capitalize()) self.histogramReady.emit([]) return @@ -3568,7 +3573,7 @@ def queryCameraPowerStatus(self, target: str): logger.info(f"Querying camera power status for {sensor_tag} sensor") # Query power status for all cameras - sensor = motion_interface.sensors[sensor_tag] + sensor = getattr(motion_interface, sensor_tag) power_status = sensor.get_camera_power_status() if power_status is not None: @@ -3612,7 +3617,7 @@ def setFanControl(self, target: str, fan_on: bool): ) # Set fan control state - sensor = motion_interface.sensors[sensor_tag] + sensor = getattr(motion_interface, sensor_tag) result = sensor.set_fan_control(fan_on) if result: @@ -3646,7 +3651,7 @@ def getFanControlStatus(self, target: str): mutex.lock() try: # Get fan control status - sensor = motion_interface.sensors[sensor_tag] + sensor = getattr(motion_interface, sensor_tag) status = sensor.get_fan_control_status() return status @@ -3667,7 +3672,7 @@ def tec_voltage(self, value=None): try: if value is None: # GET operation - self._tec_dac = motion_interface.console_module.tec_voltage() + self._tec_dac = motion_interface.console.tec_voltage() logger.debug(f"TEC DAC Setting: {self._tec_dac}") run_logger.info( "TEC Setpoint Voltage - volt: %.6f ", float(self._tec_dac) @@ -3675,7 +3680,7 @@ def tec_voltage(self, value=None): else: # SET operation - motion_interface.console_module.tec_voltage(value) + motion_interface.console.tec_voltage(value) logger.debug(f"TEC voltage set to: {value}") self._tec_dac = value run_logger.info( @@ -3699,7 +3704,7 @@ def tec_status(self): self._console_mutex.lock() try: - v, i, p, t, ok = motion_interface.console_module.tec_status() + v, i, p, t, ok = motion_interface.console.tec_status() R_TH = ( 1 / ((float(v) / (V_REF / 2 * R_3)) - 1 / R_3 + 1 / R_1) - R_2 @@ -3761,12 +3766,12 @@ def pdu_mon(self): """ self._console_mutex.lock() try: - pdu = motion_interface.console_module.read_pdu_mon() + pdu = motion_interface.console.read_pdu_mon() if pdu is None: logger.error("PDU MON: no data") return {"ok": False, "error": "no data"} - temp1, temp2, temp3 = motion_interface.console_module.get_temperatures() + temp1, temp2, temp3 = motion_interface.console.get_temperatures() # Cache for QML bindings self._pdu_raws = list(pdu.raws) diff --git a/motion_singleton.py b/motion_singleton.py index 526aa65..19823e7 100644 --- a/motion_singleton.py +++ b/motion_singleton.py @@ -1,6 +1,8 @@ # motion_singleton.py -from omotion.Interface import MOTIONInterface +# +# Single shared MotionInterface for the test app. The interface owns its +# own connection-monitor daemon thread; ``main.py`` calls +# ``motion_interface.start()`` once after the QML engine is loaded. +from omotion import MotionInterface -motion_interface, console_connected, left_sensor, right_sensor = ( - MOTIONInterface.acquire_motion_interface() -) +motion_interface = MotionInterface() From 85a5978badba933ad210ba665393c38d38cce13e Mon Sep 17 00:00:00 2001 From: boringethan Date: Tue, 21 Apr 2026 18:08:44 -0700 Subject: [PATCH 2/4] fix: align test-app to new descriptor naming (lowercase handle.name) The SDK now emits "left"/"right"/"console" via signal_state_changed and expects matching strings as method arguments. The previous "SENSOR_LEFT", "SENSOR_RIGHT", and "CONSOLE" tags were the old descriptor format. - pages/{Sensor,Settings,Console,Demo}.qml: every quoted device tag ("SENSOR_LEFT" -> "left", "SENSOR_RIGHT" -> "right", "CONSOLE" -> "console"), 71 sites total. Comments updated where they reference the old constants. - motion_connector.py: same string substitution (75 sites). Removed the now-redundant `sensor_tag = "left" if target == "SENSOR_LEFT" else "right"` re-derivation in the DFU enter path; the target IS the tag. Co-Authored-By: Claude Opus 4.7 (1M context) --- motion_connector.py | 119 ++++++++++++++++++++++---------------------- pages/Console.qml | 12 ++--- pages/Demo.qml | 4 +- pages/Sensor.qml | 36 +++++++------- pages/Settings.qml | 44 ++++++++-------- 5 files changed, 107 insertions(+), 108 deletions(-) diff --git a/motion_connector.py b/motion_connector.py index 2f572c5..9c238ed 100644 --- a/motion_connector.py +++ b/motion_connector.py @@ -209,7 +209,7 @@ def __init__( connector: "MOTIONConnector", tag: str, filename: str, - target: str = "CONSOLE", + target: str = "console", ): super().__init__() self._connector = connector @@ -251,7 +251,7 @@ def run(self): repo_name = ( _CONSOLE_FW_REPO_NAME - if self._target == "CONSOLE" + if self._target == "console" else _SENSOR_FW_REPO_NAME ) gh = GitHubReleases(_CONSOLE_FW_REPO_OWNER, repo_name, timeout=30) @@ -407,19 +407,18 @@ def run(self): self.progress.emit(-1, "Requesting DFU mode…") # Request DFU mode on the correct module with appropriate mutex - if self._target == "CONSOLE": + if self._target == "console": self._connector._console_mutex.lock() try: ok = motion_interface.console.enter_dfu() finally: self._connector._console_mutex.unlock() else: - # SENSOR_LEFT or SENSOR_RIGHT + # left or right sensor_mutex = self._connector._get_sensor_mutex(self._target) - sensor_tag = "left" if self._target == "SENSOR_LEFT" else "right" sensor_mutex.lock() try: - ok = getattr(motion_interface, sensor_tag).enter_dfu() + ok = getattr(motion_interface, self._target).enter_dfu() finally: sensor_mutex.unlock() @@ -991,7 +990,7 @@ def _cleanup_fw_token(self, token: str) -> None: def beginConsoleFirmwareDownload(self, tag: str) -> None: """Download motion-console-fw.bin for the selected release tag into a temp location.""" logger.info(f"beginConsoleFirmwareDownload {tag}") - target = "CONSOLE" + target = "console" if not tag or tag == "N/A": self.consoleFirmwareUpdateError.emit(target, "No release tag selected.") return @@ -1023,9 +1022,9 @@ def beginConsoleFirmwareDownload(self, tag: str) -> None: @pyqtSlot(str, str) def beginDeviceFirmwareDownload(self, target: str, tag: str) -> None: - """Generic: download firmware binary for a device target (CONSOLE, SENSOR_LEFT, SENSOR_RIGHT).""" + """Generic: download firmware binary for a device target ("console", "left", "right").""" logger.info(f"beginDeviceFirmwareDownload target={target} tag={tag}") - if target not in ("CONSOLE", "SENSOR_LEFT", "SENSOR_RIGHT"): + if target not in ("console", "left", "right"): self.consoleFirmwareUpdateError.emit(target, "Invalid update target.") return if not tag or tag == "N/A": @@ -1039,7 +1038,7 @@ def beginDeviceFirmwareDownload(self, target: str, tag: str) -> None: self._set_console_fw_busy(True) filename = ( - "motion-console-fw.bin" if target == "CONSOLE" else "motion-sensor-fw.bin" + "motion-console-fw.bin" if target == "console" else "motion-sensor-fw.bin" ) self._fw_download_thread = _ConsoleFirmwareDownloadThread( @@ -1066,7 +1065,7 @@ def beginDeviceFirmwareFromLocal(self, target: str, local_path: str) -> None: The QML side will receive the same ready signal and show the confirm dialog. """ logger.info(f"beginDeviceFirmwareFromLocal target={target} path={local_path}") - if target not in ("CONSOLE", "SENSOR_LEFT", "SENSOR_RIGHT"): + if target not in ("console", "left", "right"): self.consoleFirmwareUpdateError.emit(target, "Invalid update target.") return try: @@ -1078,7 +1077,7 @@ def beginDeviceFirmwareFromLocal(self, target: str, local_path: str) -> None: return fname = p.name # Validate filename - if target == "CONSOLE": + if target == "console": if fname != "motion-console-fw.bin": self.consoleFirmwareUpdateError.emit( target, "Filename must be motion-console-fw.bin" @@ -1113,7 +1112,7 @@ def _on_console_fw_download_ready( ) -> None: self.consoleFirmwareDownloadReady.emit(token, tag, filename, target) - def _on_console_fw_failed(self, message: str, target: str = "CONSOLE") -> None: + def _on_console_fw_failed(self, message: str, target: str = "console") -> None: self.consoleFirmwareUpdateError.emit(target, message) self._set_console_fw_busy(False) @@ -1128,13 +1127,13 @@ def startConsoleFirmwareUpdate(self, token: str) -> None: """Flash the previously-downloaded firmware using DFU.""" if not token or token not in self._fw_temp_files: self.consoleFirmwareUpdateError.emit( - "CONSOLE", "Firmware download token is missing/invalid." + "console", "Firmware download token is missing/invalid." ) self._set_console_fw_busy(False) return if self._fw_flash_thread is not None: self.consoleFirmwareUpdateError.emit( - "CONSOLE", "Firmware flashing is already in progress." + "console", "Firmware flashing is already in progress." ) return _, bin_path, _, target = self._fw_temp_files[token] @@ -1165,7 +1164,7 @@ def startConsoleFirmwareUpdate(self, token: str) -> None: self._fw_flash_thread.start() def _on_console_fw_finished( - self, token: str, success: bool, message: str, target: str = "CONSOLE" + self, token: str, success: bool, message: str, target: str = "console" ) -> None: self._cleanup_fw_token(token) self.consoleFirmwareUpdateFinished.emit(target, bool(success), str(message)) @@ -1444,18 +1443,18 @@ def setScaleOverride(self, label: str, name: str, scale: float) -> None: def _get_sensor_mutex(self, sensor_tag: str) -> QRecursiveMutex: """Get the appropriate mutex for the given sensor.""" - if sensor_tag == "SENSOR_LEFT": + if sensor_tag == "left": return self._left_sensor_mutex - elif sensor_tag == "SENSOR_RIGHT": + elif sensor_tag == "right": return self._right_sensor_mutex else: raise ValueError(f"Invalid sensor tag: {sensor_tag}") def _get_sensor_side(self, sensor_tag: str) -> str: """Convert sensor tag to sensor side string.""" - if sensor_tag == "SENSOR_LEFT": + if sensor_tag == "left": return "left" - elif sensor_tag == "SENSOR_RIGHT": + elif sensor_tag == "right": return "right" else: raise ValueError(f"Invalid sensor tag: {sensor_tag}") @@ -2278,8 +2277,8 @@ def _on_handle_state_changed(self, handle, old, new, reason): def querySensorInfo(self, target: str): """Fetch and emit device information with mutex protection and event-based UI updates.""" try: - if target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + if target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" mutex = self._get_sensor_mutex(target) mutex.lock() @@ -2345,13 +2344,13 @@ def querySensorLatestVersionInfo(self, target: str): logger.info(f"querySensorLatestVersionInfo({target}): GitHub disabled, skipping.") return try: - if target != "SENSOR_LEFT" and target != "SENSOR_RIGHT": + if target != "left" and target != "right": logger.error( f"Invalid target for sensor latest version query: {target}" ) return - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + sensor_tag = "left" if target == "left" else "right" mutex = self._get_sensor_mutex(target) mutex.lock() @@ -2596,8 +2595,8 @@ def queryConsoleTemperature(self): def querySensorTemperature(self, target: str): """Fetch and emit Temperature data with mutex protection and event-based UI updates.""" try: - if target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + if target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" mutex = self._get_sensor_mutex(target) mutex.lock() @@ -2842,8 +2841,8 @@ def stopTrigger(self): def querySensorAccelerometer(self, target: str): """Fetch and emit Accelerometer data with mutex protection and event-based UI updates.""" try: - if target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + if target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" mutex = self._get_sensor_mutex(target) mutex.lock() @@ -2876,8 +2875,8 @@ def querySensorGyroscope(self): def configureCamera(self, target: str, cam_mask: int): """Configure camera with mutex protection and event-based UI updates.""" try: - if target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + if target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" mutex = self._get_sensor_mutex(target) mutex.lock() @@ -2958,7 +2957,7 @@ def configureAllCameras(self, target: str): def sendPingCommand(self, target: str): """Send a ping command to HV device.""" try: - if target == "CONSOLE": + if target == "console": self._console_mutex.lock() if motion_interface.console.ping(): logger.info("Ping command sent successfully") @@ -2966,8 +2965,8 @@ def sendPingCommand(self, target: str): else: logger.error("Failed to send ping command") return False - elif target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + elif target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" if getattr(motion_interface, sensor_tag).ping(): logger.info("Ping command sent successfully") return True @@ -2981,14 +2980,14 @@ def sendPingCommand(self, target: str): logger.error(f"Error sending ping command: {e}") return False finally: - if target == "CONSOLE": + if target == "console": self._console_mutex.unlock() @pyqtSlot(str, result=bool) def sendLedToggleCommand(self, target: str): """Send a LED Toggle command to device with mutex protection.""" try: - if target == "CONSOLE": + if target == "console": self._console_mutex.lock() try: if motion_interface.console.toggle_led(): @@ -2999,8 +2998,8 @@ def sendLedToggleCommand(self, target: str): return False finally: self._console_mutex.unlock() - elif target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + elif target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" mutex = self._get_sensor_mutex(target) mutex.lock() @@ -3025,13 +3024,13 @@ def sendEchoCommand(self, target: str): """Send Echo command to device.""" try: expected_data = b"Hello FROM Test Application!" - if target == "CONSOLE": + if target == "console": self._console_mutex.lock() echoed_data, data_len = motion_interface.console.echo( echo_data=expected_data ) - elif target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + elif target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" echoed_data, data_len = getattr(motion_interface, sensor_tag).echo( echo_data=expected_data ) @@ -3050,7 +3049,7 @@ def sendEchoCommand(self, target: str): logger.error(f"Error sending Echo command: {e}") return False finally: - if target == "CONSOLE": + if target == "console": self._console_mutex.unlock() @pyqtSlot(result=int) @@ -3098,7 +3097,7 @@ def i2cReadBytes( f"i2c_addr=0x{int(i2c_addr):02X}, offset=0x{int(offset):02X}, read_len={int(data_len)}" ) - if target == "CONSOLE": + if target == "console": self._console_mutex.lock() fpga_data, fpga_data_len = ( motion_interface.console.read_i2c_packet( @@ -3119,14 +3118,14 @@ def i2cReadBytes( ) # Print as hex bytes separated by spaces return list(fpga_data[:fpga_data_len]) - elif target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": + elif target == "left" or target == "right": logger.error("I2C Read Not Implemented") return [] except Exception as e: logger.error(f"Error sending i2c read command: {e}") return [] finally: - if target == "CONSOLE": + if target == "console": self._console_mutex.unlock() @pyqtSlot(str, int, int, int, int, list, result=bool) @@ -3158,7 +3157,7 @@ def i2cWriteBytes( byte_data = bytes(sanitized_data) - if target == "CONSOLE": + if target == "console": self._console_mutex.lock() if motion_interface.console.write_i2c_packet( mux_index=mux_idx, @@ -3172,14 +3171,14 @@ def i2cWriteBytes( else: logger.error("Write I2C Failed") return False - elif target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": + elif target == "left" or target == "right": logger.debug("I2C Write Not Implemented") return True except Exception as e: logger.error(f"Error sending i2c write command: {e}") return False finally: - if target == "CONSOLE": + if target == "console": self._console_mutex.unlock() @pyqtSlot(str) @@ -3187,13 +3186,13 @@ def softResetSensor(self, target: str): """reset hardware Sensor device.""" self._console_mutex.lock() try: - if target == "CONSOLE": + if target == "console": if motion_interface.console.soft_reset(): logger.info("Software Reset Sent") else: logger.error("Failed to send Software Reset") - elif target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + elif target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" if getattr(motion_interface, sensor_tag).soft_reset(): logger.info("Software Reset Sent") else: @@ -3530,7 +3529,7 @@ def readSafetyStatus(self): for label, channel in channels.items(): status = self.i2cReadBytes( - "CONSOLE", muxIdx, channel, i2cAddr, offset, data_len + "console", muxIdx, channel, i2cAddr, offset, data_len ) if status: statuses[label] = status[0] @@ -3564,8 +3563,8 @@ def readSafetyStatus(self): def queryCameraPowerStatus(self, target: str): """Query camera power status for all cameras on the specified sensor with mutex protection.""" try: - if target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + if target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" mutex = self._get_sensor_mutex(target) mutex.lock() @@ -3606,8 +3605,8 @@ def queryCameraPowerStatus(self, target: str): def setFanControl(self, target: str, fan_on: bool): """Set fan control state on the specified sensor with mutex protection.""" try: - if target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + if target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" mutex = self._get_sensor_mutex(target) mutex.lock() @@ -3644,8 +3643,8 @@ def setFanControl(self, target: str, fan_on: bool): def getFanControlStatus(self, target: str): """Get fan control status from the specified sensor with mutex protection.""" try: - if target == "SENSOR_LEFT" or target == "SENSOR_RIGHT": - sensor_tag = "left" if target == "SENSOR_LEFT" else "right" + if target == "left" or target == "right": + sensor_tag = "left" if target == "left" else "right" mutex = self._get_sensor_mutex(target) mutex.lock() @@ -3875,7 +3874,7 @@ def run(self): for label, channel in channels.items(): status = self.connector.i2cReadBytes( - "CONSOLE", muxIdx, channel, i2cAddr, offset, data_len + "console", muxIdx, channel, i2cAddr, offset, data_len ) if status: statuses[label] = status[0] @@ -3912,10 +3911,10 @@ def run(self): # tcm_raw = self.connector.getLsyncCount() tcl_raw = self.connector.i2cReadBytes( - "CONSOLE", muxIdx, 4, i2cAddr, 0x10, 4 + "console", muxIdx, 4, i2cAddr, 0x10, 4 ) pdc_raw = self.connector.i2cReadBytes( - "CONSOLE", muxIdx, 7, i2cAddr, 0x1C, 2 + "console", muxIdx, 7, i2cAddr, 0x1C, 2 ) # Represent raw byte arrays as hex for easier reading diff --git a/pages/Console.qml b/pages/Console.qml index bd7e38c..83faa25 100644 --- a/pages/Console.qml +++ b/pages/Console.qml @@ -284,7 +284,7 @@ Rectangle { } onClicked: { - if(MOTIONInterface.sendPingCommand("CONSOLE")){ + if(MOTIONInterface.sendPingCommand("console")){ pingResult.text = "Ping SUCCESS" pingResult.color = "green" }else{ @@ -338,7 +338,7 @@ Rectangle { } onClicked: { - if(MOTIONInterface.sendLedToggleCommand("CONSOLE")) + if(MOTIONInterface.sendLedToggleCommand("console")) { toggleLedResult.text = "LED Toggled" toggleLedResult.color = "green" @@ -392,7 +392,7 @@ Rectangle { onClicked: { - if(MOTIONInterface.sendEchoCommand("CONSOLE")) + if(MOTIONInterface.sendEchoCommand("console")) { echoResult.text = "Echo SUCCESS" echoResult.color = "green" @@ -904,7 +904,7 @@ Rectangle { if (dir === "Read") { // console.log(`READ from ${fpga.label} @ 0x${offset.toString(16)}`); - let result = MOTIONInterface.i2cReadBytes("CONSOLE", muxIdx, channel, i2cAddr, offset, length); + let result = MOTIONInterface.i2cReadBytes("console", muxIdx, channel, i2cAddr, offset, length); if (result.length === 0) { console.error("Read failed or returned empty array."); @@ -983,7 +983,7 @@ Rectangle { // console.log("Data to send:", dataToSend.map(b => "0x" + b.toString(16).padStart(2, "0")).join(" ")); - let success = MOTIONInterface.i2cWriteBytes("CONSOLE", muxIdx, channel, i2cAddr, offset, dataToSend); + let success = MOTIONInterface.i2cWriteBytes("console", muxIdx, channel, i2cAddr, offset, dataToSend); if (success) { // console.log("Write successful."); @@ -1387,7 +1387,7 @@ Rectangle { enabled: parent.enabled // Disable MouseArea when the button is disabled onClicked: { // console.log("Soft Reset Triggered") - MOTIONInterface.softResetSensor("CONSOLE") + MOTIONInterface.softResetSensor("console") } onEntered: { diff --git a/pages/Demo.qml b/pages/Demo.qml index cf82d3e..376a0e7 100644 --- a/pages/Demo.qml +++ b/pages/Demo.qml @@ -157,7 +157,7 @@ Rectangle { // console.log("Data to send:", dataToSend.map(b => "0x" + b.toString(16).padStart(2, "0")).join(" ")); - let success = MOTIONInterface.i2cWriteBytes("CONSOLE", muxIdx, channel, i2cAddr, offset, dataToSend); + let success = MOTIONInterface.i2cWriteBytes("console", muxIdx, channel, i2cAddr, offset, dataToSend); if (success) { // console.log("Write successful."); @@ -195,7 +195,7 @@ Rectangle { const data_len = parseInt(myFn.data_size.replace("B", "")) / 8; // console.log(`READ from ${fModel.label} @ 0x${offset.toString(16)}`); - let result = MOTIONInterface.i2cReadBytes("CONSOLE", muxIdx, channel, i2cAddr, offset, data_len); + let result = MOTIONInterface.i2cReadBytes("console", muxIdx, channel, i2cAddr, offset, data_len); if (result.length === 0) { // console.log("Read failed or returned empty array."); diff --git a/pages/Sensor.qml b/pages/Sensor.qml index d3112c0..80cc9d4 100644 --- a/pages/Sensor.qml +++ b/pages/Sensor.qml @@ -72,7 +72,7 @@ Rectangle { return } - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; // console.log("Sensor Updating all states for", sensor_tag); MOTIONInterface.querySensorInfo(sensor_tag) @@ -89,7 +89,7 @@ Rectangle { // console.log("Page Loaded - Sensor Already Connected. Fetching Info..."); updateStates(); // Also query camera power status for the selected sensor - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; MOTIONInterface.queryCameraPowerStatus(sensor_tag); // Start fan status polling fanStatusTimer.start(); @@ -112,7 +112,7 @@ Rectangle { running: false repeat: true onTriggered: { - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; let isConnected = (sensorSelector.currentIndex === 0) ? MOTIONInterface.leftSensorConnected : MOTIONInterface.rightSensorConnected; @@ -135,7 +135,7 @@ Rectangle { if (MOTIONInterface.leftSensorConnected || MOTIONInterface.rightSensorConnected) { infoTimer.start() // One-time info fetch // Automatically query camera power status when sensor connects - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; MOTIONInterface.queryCameraPowerStatus(sensor_tag); // Start fan status polling fanStatusTimer.start(); @@ -436,7 +436,7 @@ Rectangle { } onClicked: { - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; if(MOTIONInterface.sendPingCommand(sensor_tag)){ pingResult.text = "Ping SUCCESS" pingResult.color = "green" @@ -493,7 +493,7 @@ Rectangle { } onClicked: { - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; if(MOTIONInterface.sendEchoCommand(sensor_tag)) { echoResult.text = "Echo SUCCESS" echoResult.color = "green" @@ -550,7 +550,7 @@ Rectangle { } onClicked: { - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; if(MOTIONInterface.sendLedToggleCommand(sensor_tag)) { toggleLedResult.text = "LED Toggled" toggleLedResult.color = "green" @@ -611,7 +611,7 @@ Rectangle { } onClicked: { - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; let newFanState = !fanControlOn; if (MOTIONInterface.setFanControl(sensor_tag, newFanState)) { @@ -993,8 +993,8 @@ Rectangle { if (selectedIndex === 8) { cameraMask = 0xFF; // All Cameras } - let sensor_tag = "SENSOR_LEFT"; - (sensorSelector.currentIndex === 0) ? sensor_tag = "SENSOR_LEFT": sensor_tag = "SENSOR_RIGHT"; + let sensor_tag = "left"; + (sensorSelector.currentIndex === 0) ? sensor_tag = "left": sensor_tag = "right"; // console.log("Test Camera Mask: " + cameraMask.toString(16)); if(cameraMask == 0xFF){ MOTIONInterface.configureAllCameras(sensor_tag); @@ -1045,8 +1045,8 @@ Rectangle { } onClicked: { let selectedIndex = cameraDropdown.currentIndex; - let sensor_tag = "SENSOR_LEFT"; - (sensorSelector.currentIndex === 0) ? sensor_tag = "SENSOR_LEFT": sensor_tag = "SENSOR_RIGHT"; + let sensor_tag = "left"; + (sensorSelector.currentIndex === 0) ? sensor_tag = "left": sensor_tag = "right"; if (selectedIndex < 8) { // Single camera - get its serial number from the properties @@ -1138,8 +1138,8 @@ Rectangle { } onClicked: { let selectedIndex = cameraDropdown.currentIndex; - let sensor_tag = "SENSOR_LEFT"; - (sensorSelector.currentIndex === 0) ? sensor_tag = "SENSOR_LEFT": sensor_tag = "SENSOR_RIGHT"; + let sensor_tag = "left"; + (sensorSelector.currentIndex === 0) ? sensor_tag = "left": sensor_tag = "right"; if (selectedIndex < 8) { // Single camera - get its serial number from the properties @@ -1269,7 +1269,7 @@ Rectangle { MOTIONInterface.powerCamerasOn(target) // Automatically query power status after powering on - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; MOTIONInterface.queryCameraPowerStatus(sensor_tag) } } @@ -1326,7 +1326,7 @@ Rectangle { MOTIONInterface.powerCamerasOff(target) // Automatically query power status after powering off - let sensor_tag = (sensorSelector.currentIndex === 0) ? "SENSOR_LEFT" : "SENSOR_RIGHT"; + let sensor_tag = (sensorSelector.currentIndex === 0) ? "left" : "right"; MOTIONInterface.queryCameraPowerStatus(sensor_tag) } } @@ -1618,8 +1618,8 @@ Rectangle { anchors.fill: parent enabled: parent.enabled // Disable MouseArea when the button is disabled onClicked: { - let sensor_tag = "SENSOR_LEFT"; - (sensorSelector.currentIndex === 0) ? sensor_tag = "SENSOR_LEFT": sensor_tag = "SENSOR_RIGHT"; + let sensor_tag = "left"; + (sensorSelector.currentIndex === 0) ? sensor_tag = "left": sensor_tag = "right"; // console.log("Soft Reset Triggered") MOTIONInterface.softResetSensor(sensor_tag) } diff --git a/pages/Settings.qml b/pages/Settings.qml index 74449d0..926e977 100644 --- a/pages/Settings.qml +++ b/pages/Settings.qml @@ -56,10 +56,10 @@ Rectangle { property string consoleFwStageText: "" property int consoleFwPercent: -1 property string consoleFwMessage: "" - // Current firmware update target (CONSOLE, SENSOR_LEFT, SENSOR_RIGHT) - property string fwUpdateTarget: "CONSOLE" + // Current firmware update target ("console", "left", "right") + property string fwUpdateTarget: "console" // Target used when opening the upload dialog - property string fwUploadTarget: "CONSOLE" + property string fwUploadTarget: "console" // User configuration values (editable by user) property real userTecTrip: 0.00 @@ -137,11 +137,11 @@ Rectangle { } function refreshSensorInfo(target) { - if (target === "SENSOR_LEFT" && MOTIONInterface.leftSensorConnected) { + if (target === "left" && MOTIONInterface.leftSensorConnected) { MOTIONInterface.querySensorInfo(target) MOTIONInterface.querySensorLatestVersionInfo(target) } - if (target === "SENSOR_RIGHT" && MOTIONInterface.rightSensorConnected) { + if (target === "right" && MOTIONInterface.rightSensorConnected) { MOTIONInterface.querySensorInfo(target) MOTIONInterface.querySensorLatestVersionInfo(target) } @@ -267,7 +267,7 @@ Rectangle { return db - da }) - if (target === "SENSOR_LEFT") { + if (target === "left") { if (info.latest && info.latest.tag_name) { leftLatestFirmware = info.latest.tag_name leftLatestFirmwareDate = info.latest.published_at || "" @@ -278,7 +278,7 @@ Rectangle { leftReleasesModel = names var idxL = leftReleasesModel.indexOf(leftLatestFirmware) leftLatestIndex = idxL >= 0 ? idxL : 0 - } else if (target === "SENSOR_RIGHT") { + } else if (target === "right") { if (info.latest && info.latest.tag_name) { rightLatestFirmware = info.latest.tag_name rightLatestFirmwareDate = info.latest.published_at || "" @@ -297,10 +297,10 @@ Rectangle { // Newer signal (preferred): includes target so Settings can show both L/R. function onSensorDeviceInfoReceivedEx(target, fwVersion, devId) { - if (target === "SENSOR_LEFT") { + if (target === "left") { leftSensorFirmwareVersion = fwVersion leftSensorDeviceId = devId - } else if (target === "SENSOR_RIGHT") { + } else if (target === "right") { rightSensorFirmwareVersion = fwVersion rightSensorDeviceId = devId } @@ -335,7 +335,7 @@ Rectangle { fwProgressDialog.close() consoleFwToken = "" fwResultDialog.title = success ? "Firmware Update Complete" : "Firmware Update Failed" - var prefix = (target === "CONSOLE") ? "Console: " : (target === "SENSOR_LEFT") ? "Left sensor: " : "Right sensor: " + var prefix = (target === "console") ? "Console: " : (target === "left") ? "Left sensor: " : "Right sensor: " fwResultDialog.message = prefix + message fwResultDialog.open() } @@ -400,8 +400,8 @@ Rectangle { onTriggered: { refreshConsoleInfo() refreshFpgaInfo() - refreshSensorInfo("SENSOR_LEFT") - refreshSensorInfo("SENSOR_RIGHT") + refreshSensorInfo("left") + refreshSensorInfo("right") if (MOTIONInterface.consoleConnected) MOTIONInterface.readUserConfig() } @@ -554,12 +554,12 @@ Rectangle { if (idx < 0) idx = file.lastIndexOf("\\\\") var fname = idx >= 0 ? file.substring(idx + 1) : file - if (fwUploadTarget === "CONSOLE" && fname !== "motion-console-fw.bin") { + if (fwUploadTarget === "console" && fname !== "motion-console-fw.bin") { fwErrorDialog.message = "Filename must be motion-console-fw.bin" fwErrorDialog.open() return } - if ((fwUploadTarget === "SENSOR_LEFT" || fwUploadTarget === "SENSOR_RIGHT") && fname !== "motion-sensor-fw.bin") { + if ((fwUploadTarget === "left" || fwUploadTarget === "right") && fname !== "motion-sensor-fw.bin") { fwErrorDialog.message = "Filename must be motion-sensor-fw.bin" fwErrorDialog.open() return @@ -765,7 +765,7 @@ Rectangle { Text { text: { - var label = (fwUpdateTarget === "CONSOLE") ? "console" : (fwUpdateTarget === "SENSOR_LEFT") ? "left sensor" : "right sensor" + var label = (fwUpdateTarget === "console") ? "console" : (fwUpdateTarget === "left") ? "left sensor" : "right sensor" return "Update " + label + " firmware to " + consoleFwSelectedTag + "?" } color: "white" @@ -1657,7 +1657,7 @@ Rectangle { if (!tag || tag === "") tag = consoleLatestFirmware if (tag === "Upload File...") { - fwUploadTarget = "CONSOLE" + fwUploadTarget = "console" fwUploadDialog.open() return } @@ -1727,7 +1727,7 @@ Rectangle { anchors.fill: parent enabled: parent.enabled hoverEnabled: true - onClicked: refreshSensorInfo("SENSOR_LEFT") + onClicked: refreshSensorInfo("left") onEntered: if (parent.enabled) parent.color = "#34495E" onExited: parent.color = parent.enabled ? "#2C3E50" : "#7F8C8D" } @@ -1797,14 +1797,14 @@ Rectangle { if (!tag || tag === "") tag = leftLatestFirmware if (tag === "Upload File...") { - fwUploadTarget = "SENSOR_LEFT" + fwUploadTarget = "left" fwUploadDialog.open() return } consoleFwPercent = -1 consoleFwMessage = "" consoleFwStageText = "Starting…" - MOTIONInterface.beginDeviceFirmwareDownload("SENSOR_LEFT", tag) + MOTIONInterface.beginDeviceFirmwareDownload("left", tag) fwProgressDialog.open() } onEntered: if (parent.enabled) parent.color = "#C0392B" @@ -1867,7 +1867,7 @@ Rectangle { anchors.fill: parent enabled: parent.enabled hoverEnabled: true - onClicked: refreshSensorInfo("SENSOR_RIGHT") + onClicked: refreshSensorInfo("right") onEntered: if (parent.enabled) parent.color = "#34495E" onExited: parent.color = parent.enabled ? "#2C3E50" : "#7F8C8D" } @@ -1937,14 +1937,14 @@ Rectangle { if (!tag || tag === "") tag = rightLatestFirmware if (tag === "Upload File...") { - fwUploadTarget = "SENSOR_RIGHT" + fwUploadTarget = "right" fwUploadDialog.open() return } consoleFwPercent = -1 consoleFwMessage = "" consoleFwStageText = "Starting…" - MOTIONInterface.beginDeviceFirmwareDownload("SENSOR_RIGHT", tag) + MOTIONInterface.beginDeviceFirmwareDownload("right", tag) fwProgressDialog.open() } onEntered: if (parent.enabled) parent.color = "#C0392B" From 8f9b5809fd86e33cebdcb192acd7cc89eca0bb76 Mon Sep 17 00:00:00 2001 From: George Vigelette Date: Tue, 28 Apr 2026 19:44:11 -0400 Subject: [PATCH 3/4] update workflow to reflect naming convention --- .github/workflows/release-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index bb6e88b..e4c152a 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -6,7 +6,7 @@ on: branches: [main, next] tags: - "[0-9]+.[0-9]+.[0-9]+" - - "pre-[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+" permissions: contents: write @@ -66,7 +66,7 @@ jobs: run: | if [[ "$GITHUB_REF" == refs/tags/* ]]; then TAG="${GITHUB_REF#refs/tags/}" - # Strip leading 'v' only; keep 'pre-' for pre-release versions + # Strip leading 'v' only; -rc.x suffix is preserved as-is VER="${TAG#v}" else VER=$(git describe --tags --dirty --always --long 2>/dev/null || echo "dev-${GITHUB_SHA::8}") @@ -96,7 +96,7 @@ jobs: echo "ARTIFACT_ZIP=OpenMotion-TestApp-${TAG}.zip" >> "$GITHUB_OUTPUT" # pre-release flag - if [[ "$TAG" == pre-* ]]; then + if [[ "$TAG" == *-rc.* ]]; then echo "PRERELEASE=true" >> "$GITHUB_OUTPUT" else echo "PRERELEASE=false" >> "$GITHUB_OUTPUT" From c542329294b87f46ff55bbb21732e7e97bec7364 Mon Sep 17 00:00:00 2001 From: boringethan Date: Tue, 12 May 2026 16:22:37 -0700 Subject: [PATCH 4/4] feat(ui): float-precision trigger frequency (Demo trigger panel) Cherry-picks the trigger-frequency precision change from 1e6576f without the JSON camelCase key conversion or the temporary float-coercion shim. The DoubleValidator (1.00-100.00, 2 decimals) and parseFloat now match the firmware's float frequencyHz; PascalCase JSON keys stay consistent with the rest of the trigger config and the firmware's GET response. --- pages/Demo.qml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pages/Demo.qml b/pages/Demo.qml index 376a0e7..3c8da5f 100644 --- a/pages/Demo.qml +++ b/pages/Demo.qml @@ -1620,8 +1620,12 @@ Rectangle { Layout.preferredHeight: 32 enabled: MOTIONInterface.consoleConnected font.pixelSize: 12 - text: "40" - validator: IntValidator { bottom: 1; top: 100 } + text: "40.00" + validator: DoubleValidator { bottom: 1.0; top: 100.0; decimals: 2; notation: DoubleValidator.StandardNotation } + onEditingFinished: { + var val = parseFloat(text) + if (!isNaN(val)) text = val.toFixed(2) + } background: Rectangle { radius: 6 color: "#2B2B2E" @@ -1741,7 +1745,7 @@ Rectangle { page1.pdcMax = NaN; var json_trigger_data = { - "TriggerFrequencyHz": parseInt(fsFrequency.text), + "TriggerFrequencyHz": parseFloat(fsFrequency.text), "TriggerPulseWidthUsec": parseInt(fsPulseWidth.text), "LaserPulseDelayUsec": parseInt(lsDelay.text), "LaserPulseWidthUsec": parseInt(lsPulseWidth.text),