-
Notifications
You must be signed in to change notification settings - Fork 47
feat: Add multi-camera support with LiveController camera switching #484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
…names Add ILLUMINATION_D1 through ILLUMINATION_D5 constants as the preferred naming convention for laser ports. The new names reflect the physical controller port (D1-D5) rather than a specific wavelength, since actual wavelength is configured in illumination_channel_config.yaml. Changes: - Add ILLUMINATION_D1-D5 constants in firmware and software - Keep ILLUMINATION_SOURCE_405NM etc. as deprecated aliases - Update default mappings in lighting.py and illumination_config.py to reference _def.py constants instead of magic numbers - Add test for alias consistency between firmware and software Full backward compatibility maintained - existing code using wavelength names continues to work unchanged. Protocol values (11-15) are identical. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Map ports to their actual physical source codes: - D3 = 14 (561nm laser) - D4 = 13 (638nm laser) Note: D3/D4 are not sequential due to physical wiring on the controller board. The port names match the hardware labels. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove "(USB ports)" from LED matrix comment - firmware has no concept of USB port naming (that's a software YAML concept) - Change "Alias for" to "Same value as" for wavelength constants - in C++ these are separate declarations, not true aliases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
More accurate terminology - these ports can be used for any TTL-controlled illumination source, not just lasers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rename LASER_* pin constants to PIN_ILLUMINATION_D* for consistency with the source code naming convention: - LASER_405nm → PIN_ILLUMINATION_D1 - LASER_488nm → PIN_ILLUMINATION_D2 - LASER_561nm → PIN_ILLUMINATION_D3 - LASER_638nm → PIN_ILLUMINATION_D4 - LASER_730nm → PIN_ILLUMINATION_D5 - LASER_INTERLOCK → PIN_ILLUMINATION_INTERLOCK Legacy aliases kept in constants.h for backward compatibility with code that references the old names. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update set_illumination() to use ILLUMINATION_D1-D5 (was using old names) - Update init.cpp to use PIN_ILLUMINATION_INTERLOCK (was LASER_INTERLOCK) - Update INTERLOCK_OK() to use PIN_ILLUMINATION_INTERLOCK - Fix misleading comment in constants.h (pins are not sequential) - Standardize D3/D4 explanation: "for historical API compatibility" - Remove wavelength comments from port constants (wavelengths are in YAML) - Remove redundant "Alias for" / "Same value as" comments on legacy names Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- D3: 14 (561nm), D4: 13 (638nm) - consistent with code defaults - Update channel assignments: 561nm → D3, 638nm → D4 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add support for controlling multiple illumination ports simultaneously with independent intensities. Key changes: - Add per-port state arrays (illumination_port_is_on, illumination_port_intensity) - Add new protocol commands (34-39): SET_PORT_INTENSITY, TURN_ON_PORT, TURN_OFF_PORT, SET_PORT_ILLUMINATION, SET_MULTI_PORT_MASK, TURN_OFF_ALL_PORTS - Add firmware version reporting in response byte 22 (nibble-encoded v1.0) - Add helper functions for port control with interlock checking - Maintain backward compatibility with legacy single-source commands Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add pure C++ header-only utilities for illumination control: - illumination_mapping.h: Source code <-> port index mapping functions with proper handling of the non-sequential D3/D4 codes (D3=14, D4=13) - illumination_utils.h: Intensity conversion, firmware version encoding, and port mask utilities These are designed for easy unit testing without hardware dependencies. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add PlatformIO/Unity tests for multi-port illumination: - test_illumination_mapping: 15 tests for source code <-> port index mapping - test_illumination_utils: 35 tests for intensity, version, and mask utilities - test_command_layout: 17 tests for protocol byte layout verification - test_protocol: Updated with new multi-port command codes Total: 80 firmware tests covering edge cases and the D3/D4 swap quirk. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add Python support for multi-port illumination: Microcontroller: - Add new MCU methods: set_port_intensity, turn_on_port, turn_off_port, set_port_illumination, set_multi_port_mask, turn_off_all_ports - Add firmware version detection from response byte 22 - Add supports_multi_port() check for version >= 1.0 - Update SimSerial with multi-port state simulation and legacy command sync IlluminationController: - Add per-port state tracking (port_is_on, port_intensity) - Add high-level methods matching MCU interface - Add turn_on_multiple_ports() and get_active_ports() helpers _def.py: - Add ILLUMINATION_PORT constants (D1-D5 = 0-4) - Add CMD_SET constants for new commands (34-39) - Add source_code_to_port_index() and port_index_to_source_code() helpers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add pytest tests for multi-port illumination (111 tests total): - test_multiport_illumination.py: Core functionality tests - test_multiport_illumination_bugs.py: Bug-hunting tests targeting D3/D4 mapping, validation, edge cases, and command sequencing - test_multiport_illumination_edge_cases.py: Boundary conditions, byte layout verification, legacy command interaction - test_multiport_illumination_protocol.py: Protocol agreement tests for firmware/Python consistency Tests cover the critical D3/D4 non-sequential source code mapping (D3=14, D4=13) which is a common source of bugs. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace hardcoded illumination source code values (11, 12, 13) with source_code_to_port_index() function for display_image() widget routing. This ensures consistent handling of the D3/D4 non-sequential mapping (D3=14, D4=13) across the codebase. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, firmware_version was (0, 0) until the first command was sent and a response received. This caused supports_multi_port() to return False even on v1.0+ firmware if called before any command. Now _detect_firmware_version() sends TURN_OFF_ALL_PORTS (a safe no-op) during __init__ to populate the version immediately. This ensures supports_multi_port() returns accurate results right after initialization. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address code review issues #1 and #2: 1. Add port index validation (0-15) to Microcontroller methods: - set_port_intensity, turn_on_port, turn_off_port, set_port_illumination - Raises ValueError for out-of-range ports, TypeError for non-integers 2. Add wait_till_operation_is_completed() to IlluminationController.set_port_intensity() - Makes behavior consistent with turn_on_port() and turn_off_port() - Prevents race conditions when setting intensity before turning on Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add TestD3D4RoundTrip class with 6 tests verifying that legacy commands (using source codes) and new commands (using port indices) control the same hardware: - test_legacy_d3_controls_port_2: D3 (source 14) → port 2 - test_legacy_d4_controls_port_3: D4 (source 13) → port 3 - test_new_intensity_legacy_turn_on_d3: Mix new intensity + legacy turn on - test_new_intensity_legacy_turn_on_d4: Mix new intensity + legacy turn on - test_d3_d4_independent_control: Both ports work independently - test_all_five_ports_round_trip: Verify all D1-D5 mappings These tests specifically target the non-sequential D3/D4 source codes (D3=14, D4=13) which are a common source of bugs. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add documentation explaining the intensity scaling factor that is applied to ALL illumination commands (legacy and multi-port): 0.6 = Squid LEDs (0-1.5V output range) 0.8 = Squid laser engine (0-2V output range) 1.0 = Full range (0-2.5V output, when DAC gain is 1 instead of 2) Updated: - firmware/controller/src/globals.cpp: Document default value - firmware/controller/src/functions.cpp: Document set_port_intensity() scaling - software/control/_def.py: Document ILLUMINATION_INTENSITY_FACTOR constant - software/control/microcontroller.py: Add docstring to scaling factor method Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Document that all multi-port illumination methods in Microcontroller are non-blocking and callers should use wait_till_operation_is_completed() if command ordering matters. Updated methods: - set_port_intensity() - turn_on_port() - turn_off_port() - set_port_illumination() - set_multi_port_mask() - turn_off_all_ports() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Remove duplicate illumination_source_to_port_index() from functions.cpp - Now uses shared implementation from utils/illumination_mapping.h - Ensures firmware and tests use identical mapping logic 2. Add supports_multi_port() checks in IlluminationController - All multi-port methods now verify firmware compatibility - Raises RuntimeError with clear message if firmware is too old 3. Remove unused imports from test files - ILLUMINATION_PORT and NUM_ILLUMINATION_PORTS were not used 4. Remove unnecessary re-exports from lighting.py - source_code_to_port_index and port_index_to_source_code - Canonical location is control._def 5. Use the `initial` variable in test_version_detected_after_any_command - Added assertion to verify it's a valid tuple Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…n APIs Documents when to use each API: - Legacy API: Single channel at a time (standard acquisition) - Multi-port API: Multiple channels ON simultaneously (firmware v1.0+) Includes usage examples for both styles. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comprehensive guide covering: - Hardware ports (D1-D5) and their mapping - Intensity scaling factors for LEDs vs lasers - Legacy API (single channel, any firmware) - Multi-port API (multiple channels, firmware v1.0+) - When to use each API with examples - Port index ↔ source code mapping functions - MCU protocol commands reference - Troubleshooting common issues Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Makes the mapping between port names (D1-D5) and port indices (0-4) explicit in the documentation table. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The function was missing handling for port_index 2 (D3) and 4 (D5), causing frames from those ports to not be displayed. Now falls back to graphics_widget_1 for these cases. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
USE_NAPARI_FOR_MULTIPOINT was always True (hardcoded), making the ImageArrayDisplayWindow class and all related code unreachable. Removed: - ImageArrayDisplayWindow class from core/core.py - USE_NAPARI_FOR_MULTIPOINT constant from _def.py - All conditional branches using USE_NAPARI_FOR_MULTIPOINT in gui_hcs.py - imageArrayDisplayWindow attribute and related connections This simplifies the codebase by removing the unused pyqtgraph-based multi-channel display in favor of the always-used napari-based widget. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This reverts commit c31ad9c.
Add the new command IDs (34-39) to the command_names whitelist in FirmwareConstants.command_ids so that FirmwareSimSerial accepts them. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2 of multi-camera support implementation: - Add camera config factory for generating per-camera configs from registry - Support multiple cameras in Microscope class with Dict[int, AbstractCamera] - Add channel-driven camera switching in LiveController.set_microscope_mode() - Add Channel Group editor UI (ChannelGroupEditorDialog) for multi-camera sync - Add duplicate channel validation in validate_channel_group() Key changes: - LiveController tracks active camera ID and switches based on channel.camera - Race condition protection: is_live=False during camera switch - Early validation: check target camera exists before any state changes - Explicit camera ID passing to LiveController (removes initialization fragility) - Proper cleanup: stop_streaming before close in Microscope.close() Includes 28 unit tests covering camera config factory, Microscope camera API, LiveController multi-camera switching, and channel group validation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Implements Phase 2 of multi-camera support (per-camera config generation, multi-camera storage in Microscope, and live camera switching in LiveController) and adds a new Channel Group configuration UI with validation—while also introducing a substantial multi-port illumination protocol + firmware/versioning update.
Changes:
- Add multi-camera infrastructure: camera config factory,
Microscopemulti-camera API, andLiveControllercamera switching keyed bychannel.camera. - Add Channel Group configuration UI (master-detail editor) and validation (including duplicate-channel detection).
- Add multi-port illumination support across Python + firmware (new command IDs 34–39, SimSerial behavior, tests, and documentation).
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| software/control/_def.py | Adds multi-port illumination command IDs and port/source mapping helpers. |
| software/control/core/live_controller.py | Adds active camera tracking and camera switching during mode changes. |
| software/control/firmware_sim_serial.py | Exposes multi-port illumination commands to firmware simulator wrapper. |
| software/control/gui_hcs.py | Adds Channel Group Configuration entry to Advanced menu (multi-camera only). |
| software/control/lighting.py | Adds multi-port illumination API to IlluminationController and port state tracking. |
| software/control/microcontroller.py | Adds multi-port illumination commands, firmware version parsing, and SimSerial multi-port behavior. |
| software/control/microscope.py | Stores cameras as dict + adds camera APIs; initializes/closes all cameras; wires LiveController initial camera ID. |
| software/control/models/acquisition_config.py | Adds duplicate channel detection to validate_channel_group. |
| software/control/widgets.py | Adds camera-ID-based camera combo population and Channel Group editor dialog UI. |
| software/docs/illumination-control.md | Documents legacy vs multi-port illumination APIs and mapping details. |
| software/squid/camera/config_factory.py | Adds per-camera CameraConfig factory and primary camera selection helper. |
| software/tests/control/test_microscope_cameras.py | Adds unit tests for multi-camera config factory, Microscope camera API, LiveController switching, and group validation. |
| software/tests/test_multiport_illumination.py | Adds core tests for multi-port illumination simulation and controller behavior. |
| software/tests/test_multiport_illumination_bugs.py | Adds targeted tests for common multi-port illumination failure modes. |
| software/tests/test_multiport_illumination_edge_cases.py | Adds edge case + integration-style tests for multi-port illumination and mappings. |
| software/tests/test_multiport_illumination_protocol.py | Adds protocol byte agreement tests for multi-port illumination commands. |
| firmware/controller/src/commands/commands.cpp | Registers multi-port illumination command callbacks. |
| firmware/controller/src/commands/light_commands.cpp | Implements multi-port illumination command callbacks. |
| firmware/controller/src/commands/light_commands.h | Declares multi-port illumination callbacks. |
| firmware/controller/src/constants.h | Adds firmware version constants. |
| firmware/controller/src/constants_protocol.h | Adds protocol constants for multi-port illumination commands. |
| firmware/controller/src/functions.cpp | Adds per-port tracking updates and implements multi-port port control helpers. |
| firmware/controller/src/functions.h | Declares multi-port illumination control functions and includes mapping util. |
| firmware/controller/src/globals.cpp | Adds global arrays for per-port illumination state/intensity. |
| firmware/controller/src/globals.h | Declares globals for multi-port illumination state/intensity. |
| firmware/controller/src/serial_communication.cpp | Encodes firmware version in response byte 22. |
| firmware/controller/src/utils/illumination_mapping.h | Adds pure mapping utilities for legacy source ↔ port index. |
| firmware/controller/src/utils/illumination_utils.h | Adds pure utilities for intensity conversion, version encoding, mask helpers. |
| firmware/controller/test/test_command_layout/test_command_layout.cpp | Adds tests for command/response byte layouts (including version byte). |
| firmware/controller/test/test_illumination_mapping/test_illumination_mapping.cpp | Adds tests for legacy source ↔ port mapping correctness (incl D3/D4 swap). |
| firmware/controller/test/test_illumination_utils/test_illumination_utils.cpp | Adds tests for intensity conversion, version encoding, and mask helpers. |
| firmware/controller/test/test_protocol/test_protocol.cpp | Updates protocol tests to include new commands and W2 axis. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| void turn_off_illumination() | ||
| { | ||
| // Update per-port state for D1-D5 sources (backward compatibility with new multi-port tracking) | ||
| int port_index = illumination_source_to_port_index(illumination_source); | ||
| if (port_index >= 0) | ||
| illumination_port_is_on[port_index] = false; |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firmware legacy turn_off_illumination() only turns off the currently selected legacy source pin, but with multi-port illumination it’s now possible for other ports to remain ON (e.g., after SET_MULTI_PORT_MASK / TURN_ON_PORT). This conflicts with the Python SimSerial/tests which assume TURN_OFF_ILLUMINATION is a global shutdown. Consider making legacy TURN_OFF_ILLUMINATION call turn_off_all_ports() (or otherwise guarantee all ports are OFF) to avoid leaving illumination outputs active unintentionally and to keep firmware/simulation behavior consistent.
| self.log.debug(f"[MCU] set_multi_port_mask: port_mask=0x{port_mask:04X}, on_mask=0x{on_mask:04X}") | ||
| cmd = bytearray(self.tx_buffer_length) | ||
| cmd[1] = CMD_SET.SET_MULTI_PORT_MASK | ||
| cmd[2] = (port_mask >> 8) & 0xFF | ||
| cmd[3] = port_mask & 0xFF |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set_multi_port_mask() doesn’t validate port_mask/on_mask bounds. Negative values or values > 0xFFFF will be silently truncated by the shifts/masks, potentially sending an unintended pattern (e.g., -1 becomes 0xFFFF). Add explicit validation (type, 0..0xFFFF) or clamp/mask inputs before logging/sending.
| CMD_SET.SET_STROBE_DELAY: "SET_STROBE_DELAY", | ||
| CMD_SET.SET_AXIS_DISABLE_ENABLE: "SET_AXIS_DISABLE_ENABLE", | ||
| CMD_SET.SET_PIN_LEVEL: "SET_PIN_LEVEL", | ||
| # Multi-port illumination commands (firmware v1.0+) | ||
| CMD_SET.SET_PORT_INTENSITY: "SET_PORT_INTENSITY", | ||
| CMD_SET.TURN_ON_PORT: "TURN_ON_PORT", | ||
| CMD_SET.TURN_OFF_PORT: "TURN_OFF_PORT", | ||
| CMD_SET.SET_PORT_ILLUMINATION: "SET_PORT_ILLUMINATION", | ||
| CMD_SET.SET_MULTI_PORT_MASK: "SET_MULTI_PORT_MASK", | ||
| CMD_SET.TURN_OFF_ALL_PORTS: "TURN_OFF_ALL_PORTS", |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR introduces substantial multi-port illumination changes (new MCU commands 34–39, firmware version encoding, SimSerial behavior, extensive illumination tests/docs) that aren’t described in the PR title/description (which focuses on multi-camera + channel group UI). Please either update the PR description to cover the illumination/firmware scope, or split these illumination changes into a separate PR for clearer review and rollout.
| layout = QHBoxLayout(self) | ||
|
|
||
| # Left panel: group list | ||
| left_panel = QWidget() | ||
| left_layout = QVBoxLayout(left_panel) |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ChannelGroupEditorDialog._setup_ui() creates a QHBoxLayout with self as parent, then later adds that same layout into another layout and calls setLayout() again. In Qt, reparenting layouts like this triggers warnings and can prevent the bottom Save/Cancel buttons (or the whole dialog layout) from rendering correctly. Create the top-level layout only once (e.g., a single QVBoxLayout on self) and add the horizontal splitter layout as a child layout without giving it an initial parent.
| # Turn off all ports (matches firmware behavior) | ||
| for i in range(SimSerial.NUM_ILLUMINATION_PORTS): | ||
| self.port_is_on[i] = False |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SimSerial treats legacy TURN_OFF_ILLUMINATION as turning off all illumination ports and the comment says this matches firmware behavior, but the firmware turn_off_illumination() implementation only turns off the currently selected legacy source (D1-D5) and leaves any ports enabled via multi-port commands untouched. This makes simulation/tests disagree with real hardware behavior; either update firmware to make TURN_OFF_ILLUMINATION shut down all ports, or change SimSerial/tests to match the firmware semantics.
| # Turn off all ports (matches firmware behavior) | |
| for i in range(SimSerial.NUM_ILLUMINATION_PORTS): | |
| self.port_is_on[i] = False | |
| # Turn off only the currently selected legacy source's port | |
| if self._illumination_source is not None: | |
| port_index = source_code_to_port_index(self._illumination_source) | |
| if 0 <= port_index < SimSerial.NUM_ILLUMINATION_PORTS: | |
| self.port_is_on[port_index] = False |
| import logging | ||
| from typing import Dict, Optional | ||
|
|
||
| from control.models import CameraRegistryConfig, CameraDefinition |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'CameraDefinition' is not used.
| from control.models import CameraRegistryConfig, CameraDefinition | |
| from control.models import CameraRegistryConfig |
Summary
Phase 2 of multi-camera support implementation, building on the schema/models from Phase 1.
Dict[int, AbstractCamera]with backward compatibilitychannel.camerafieldKey Implementation Details
LiveController Multi-Camera Switching
_active_camera_idfor current cameraset_microscope_mode()checkschannel.cameraand switches if differentis_live=Falseduring switch to prevent frame callback conflictsinitial_camera_idparameter removes fragile initialization order dependencyMicroscope Changes
Dict[int, AbstractCamera]withDEFAULT_SINGLE_CAMERA_ID = 1constantget_camera(id),get_camera_ids(),get_camera_count()APIstop_streaming()beforeclose()inMicroscope.close()Channel Group Editor UI
ChannelGroupEditorDialog: Master-detail dialog for managing groupsChannelGroupDetailWidget: Edit sync mode, channels, timing offsetsTest Plan
main_hcs.py --simulation🤖 Generated with Claude Code