-
Notifications
You must be signed in to change notification settings - Fork 47
feat: Multi-port illumination control + laser port naming refactor #481
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
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>
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
This PR refactors laser/illumination naming to be port-based (D1–D5), fixes the D3/D4 mapping, and adds a full multi-port illumination control path (firmware + Python + simulation) with firmware version reporting and extensive tests.
Changes:
- Introduces new multi-port illumination commands and state on the firmware side (per-port on/off and intensity, mask operations, port mapping utilities) plus nibble-encoded firmware version reporting.
- Adds Python-side support:
Microcontrollermulti-port methods with firmware-version detection, anIlluminationControllermulti-port API, and SimSerial enhancements to simulate the new protocol and state. - Refactors illumination constants and configuration to use port-based names (
ILLUMINATION_D1–D5) with legacy wavelength-based aliases, fixes the D3/D4 non-sequential mapping, and updates protocol/tests/configs accordingly.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| software/tests/test_multiport_illumination_protocol.py | Adds protocol-level tests to assert Python multi-port commands (bytes, masks, endian-ness) match firmware expectations, including D3/D4 mapping and legacy/new command interaction. |
| software/tests/test_multiport_illumination_edge_cases.py | Adds edge-case/integration tests for intensities, command layouts, legacy vs new command interactions, mapping utilities, firmware version behavior, and IlluminationController validation. |
| software/tests/test_multiport_illumination_bugs.py | Adds focused tests targeting likely multi-port bugs (D3/D4 mapping, combined commands, mask overlap, sequencing, controller/MCU sync, firmware version support). |
| software/tests/test_multiport_illumination.py | Adds higher-level tests for multi-port illumination behavior across SimSerial, Microcontroller, and IlluminationController, including simultaneous ports and invalid indices. |
| software/tests/control/test_firmware_protocol.py | Extends firmware/protocol consistency tests to cover new port-based illumination codes, legacy wavelength aliases, and bidirectional consistency across LED patterns and D1–D5 names. |
| software/machine_configs/illumination_channel_config.yaml | Updates machine YAML to correct D3/D4 controller port mapping (D3=14/561nm, D4=13/638nm) and channel controller_port references. |
| software/control/models/illumination_config.py | Updates IlluminationChannelConfig default controller_port_mapping to use ILLUMINATION_CODE constants for D1–D5 and LED patterns rather than raw integers. |
| software/control/microcontroller.py | Adds multi-port command constants to logging, extends SimSerial with multi-port state, legacy<->multi-port sync, and firmware version encoding; adds Microcontroller multi-port methods, firmware-version tracking/detection, and supports_multi_port(). |
| software/control/lighting.py | Adds NUM_ILLUMINATION_PORTS, imports/exports port/source mapping helpers, defines IlluminationController’s multi-port API including per-port state tracking and convenience operations (turn on multiple, get active ports). |
| software/control/core/core.py | Changes ImageArrayDisplayWindow.display_image to use source_code_to_port_index so D1–D5 routing respects the non-sequential D3/D4 source codes. |
| software/control/_def.py | Introduces multi-port command codes, port-based illumination codes (ILLUMINATION_D1–D5), legacy wavelength aliases, ILLUMINATION_PORT indices, source↔port mapping helpers, and documents illumination intensity scaling factor. |
| firmware/controller/test/test_protocol/test_protocol.cpp | Extends protocol unit tests to include MOVE_W2, new multi-port commands (uniqueness, range), axis ID for W2, and validates D1–D5 codes and wavelength aliases. |
| firmware/controller/test/test_illumination_utils/test_illumination_utils.cpp | Adds unit tests for intensity↔DAC conversions, firmware version nibble encode/decode, and port-mask helper utilities. |
| firmware/controller/test/test_illumination_mapping/test_illumination_mapping.cpp | Adds tests for C++ illumination source↔port index mapping, including non-sequential D3/D4 and round-trip consistency. |
| firmware/controller/test/test_command_layout/test_command_layout.cpp | Adds tests validating byte layouts of all new multi-port commands and the response packet layout (including version byte position). |
| firmware/controller/src/utils/illumination_utils.h | Introduces C++ inline helpers for intensity conversions, firmware version encoding/decoding, and port mask operations usable in tests and firmware. |
| firmware/controller/src/utils/illumination_mapping.h | Adds C++ inline mapping utilities for legacy illumination source codes to port indices and back, plus NUM_ILLUMINATION_PORTS for tests/utilities. |
| firmware/controller/src/serial_communication.cpp | Writes nibble-encoded firmware version into response byte 22 before CRC so the host can detect firmware version. |
| firmware/controller/src/init.cpp | Switches laser-related pin initialization from legacy names to new PIN_ILLUMINATION_D* and PIN_ILLUMINATION_INTERLOCK constants, keeping behavior but using port-based naming. |
| firmware/controller/src/globals.h | Declares NUM_ILLUMINATION_PORTS and per-port illumination_port_is_on/illumination_port_intensity globals for firmware multi-port tracking. |
| firmware/controller/src/globals.cpp | Initializes new global multi-port illumination state arrays and documents the global illumination_intensity_factor semantics. |
| firmware/controller/src/functions.h | Declares new multi-port illumination functions (source→port mapping, port→pin, per-port on/off/intensity, turn_off_all_ports) for use by command callbacks. |
| firmware/controller/src/functions.cpp | Implements legacy illumination using new D1–D5 names and adds multi-port control helpers (source→port index, port→pin, per-port on/off/intensity, and a turn_off_all_ports that clears all ports and LED matrix). |
| firmware/controller/src/constants_protocol.h | Defines new multi-port command IDs, D1–D5 port-based illumination constants, and legacy wavelength aliases in the firmware protocol constants. |
| firmware/controller/src/constants.h | Adds firmware version macros, introduces PIN_ILLUMINATION_D*/PIN_ILLUMINATION_INTERLOCK pin assignments, and defines legacy pin aliases plus an updated INTERLOCK_OK() macro. |
| firmware/controller/src/commands/light_commands.h | Declares callbacks for the new multi-port illumination commands. |
| firmware/controller/src/commands/light_commands.cpp | Implements callbacks for each new multi-port command and wires them to the underlying per-port helper functions. |
| firmware/controller/src/commands/commands.cpp | Registers the new multi-port command callbacks in the central cmd_map. |
| firmware/controller/main_controller_teensy41.ino | Updates the main loop’s interlock handling to use the new port-based pin names and to turn off all D1–D5 TTL pins when the interlock is triggered. |
Comments suppressed due to low confidence (1)
firmware/controller/src/functions.cpp:325
turn_off_illumination()only clears the state and GPIO for the currently selectedillumination_source, but does not turn off any other TTL ports that may have been enabled via the new multi-port APIs, while the SimSerial implementation and Python tests treatTURN_OFF_ILLUMINATIONas turning all ports off. This creates a divergence between simulated behavior and real firmware (and can leave additional ports on when callers expect everything off), especially in mixed legacy/new-command scenarios. Consider delegating legacyTURN_OFF_ILLUMINATIONto a helper that also invokesturn_off_all_ports()(or otherwise iterates all ports) so hardware behavior matches the multi-port model and the Python tests' expectations.
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;
switch(illumination_source)
{
case ILLUMINATION_SOURCE_LED_ARRAY_FULL:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_LEFT_HALF:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_RIGHT_HALF:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_LEFTB_RIGHTR:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_LOW_NA:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_LEFT_DOT:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_RIGHT_DOT:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_TOP_HALF:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_BOTTOM_HALF:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_EXTERNAL_FET:
break;
case ILLUMINATION_D1:
digitalWrite(PIN_ILLUMINATION_D1, LOW);
break;
case ILLUMINATION_D2:
digitalWrite(PIN_ILLUMINATION_D2, LOW);
break;
case ILLUMINATION_D3:
digitalWrite(PIN_ILLUMINATION_D3, LOW);
break;
case ILLUMINATION_D4:
digitalWrite(PIN_ILLUMINATION_D4, LOW);
break;
case ILLUMINATION_D5:
digitalWrite(PIN_ILLUMINATION_D5, LOW);
break;
}
illumination_is_on = false;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Maps illumination source code (e.g., ILLUMINATION_D1=11) to port index (0-4) | ||
| // Returns -1 for invalid source codes | ||
| int illumination_source_to_port_index(int source) | ||
| { | ||
| switch (source) | ||
| { | ||
| case ILLUMINATION_D1: return 0; | ||
| case ILLUMINATION_D2: return 1; | ||
| case ILLUMINATION_D3: return 2; | ||
| case ILLUMINATION_D4: return 3; | ||
| case ILLUMINATION_D5: return 4; | ||
| default: return -1; | ||
| } |
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.
The illumination_source_to_port_index mapping logic here duplicates the mapping implemented in utils/illumination_mapping.h, which defines the same function (with identical semantics) as an inline utility for tests and other code. Duplicating this mapping in two places makes it easy for the firmware implementation and the tested mapping to drift apart over time; it would be more maintainable to reuse the header utility (or a shared helper) in the firmware implementation rather than re-encoding the mapping here.
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.
[Claude Code] Fixed in commit 1d4f483 - functions.cpp now includes utils/illumination_mapping.h and uses the shared implementation instead of duplicating the mapping logic.
| def set_port_intensity(self, port_index: int, intensity: float): | ||
| """Set intensity for a specific port without changing on/off state. | ||
|
|
||
| Args: | ||
| port_index: Port index (0=D1, 1=D2, etc.) | ||
| intensity: Intensity percentage (0-100) | ||
| """ | ||
| if port_index < 0 or port_index >= NUM_ILLUMINATION_PORTS: | ||
| raise ValueError(f"Invalid port index: {port_index}") | ||
| self.microcontroller.set_port_intensity(port_index, intensity) | ||
| self.microcontroller.wait_till_operation_is_completed() | ||
| self.port_intensity[port_index] = intensity | ||
|
|
||
| def turn_on_port(self, port_index: int): | ||
| """Turn on a specific illumination port. | ||
|
|
||
| Args: | ||
| port_index: Port index (0=D1, 1=D2, etc.) | ||
| """ | ||
| if port_index < 0 or port_index >= NUM_ILLUMINATION_PORTS: | ||
| raise ValueError(f"Invalid port index: {port_index}") | ||
| self.microcontroller.turn_on_port(port_index) | ||
| self.microcontroller.wait_till_operation_is_completed() | ||
| self.port_is_on[port_index] = True | ||
|
|
||
| def turn_off_port(self, port_index: int): | ||
| """Turn off a specific illumination port. | ||
|
|
||
| Args: | ||
| port_index: Port index (0=D1, 1=D2, etc.) | ||
| """ | ||
| if port_index < 0 or port_index >= NUM_ILLUMINATION_PORTS: | ||
| raise ValueError(f"Invalid port index: {port_index}") | ||
| self.microcontroller.turn_off_port(port_index) | ||
| self.microcontroller.wait_till_operation_is_completed() | ||
| self.port_is_on[port_index] = False | ||
|
|
||
| def set_port_illumination(self, port_index: int, intensity: float, turn_on: bool): | ||
| """Set intensity and on/off state for a specific port in one command. | ||
|
|
||
| Args: | ||
| port_index: Port index (0=D1, 1=D2, etc.) | ||
| intensity: Intensity percentage (0-100) | ||
| turn_on: Whether to turn the port on | ||
| """ | ||
| if port_index < 0 or port_index >= NUM_ILLUMINATION_PORTS: | ||
| raise ValueError(f"Invalid port index: {port_index}") | ||
| self.microcontroller.set_port_illumination(port_index, intensity, turn_on) | ||
| self.microcontroller.wait_till_operation_is_completed() | ||
| self.port_intensity[port_index] = intensity | ||
| self.port_is_on[port_index] = turn_on | ||
|
|
||
| def turn_on_multiple_ports(self, port_indices: List[int]): | ||
| """Turn on multiple ports simultaneously. | ||
|
|
||
| Args: | ||
| port_indices: List of port indices to turn on (0=D1, 1=D2, etc.) | ||
| """ | ||
| if not port_indices: | ||
| return | ||
|
|
||
| # Build port mask and on mask | ||
| port_mask = 0 | ||
| on_mask = 0 | ||
| for port_index in port_indices: | ||
| if port_index < 0 or port_index >= NUM_ILLUMINATION_PORTS: | ||
| raise ValueError(f"Invalid port index: {port_index}") | ||
| port_mask |= 1 << port_index | ||
| on_mask |= 1 << port_index | ||
|
|
||
| self.microcontroller.set_multi_port_mask(port_mask, on_mask) | ||
| self.microcontroller.wait_till_operation_is_completed() | ||
| for port_index in port_indices: | ||
| self.port_is_on[port_index] = True | ||
|
|
||
| def turn_off_all_ports(self): | ||
| """Turn off all illumination ports.""" | ||
| self.microcontroller.turn_off_all_ports() | ||
| self.microcontroller.wait_till_operation_is_completed() | ||
| for i in range(NUM_ILLUMINATION_PORTS): | ||
| self.port_is_on[i] = False | ||
|
|
||
| def get_active_ports(self) -> List[int]: | ||
| """Get list of currently active (on) port indices. | ||
|
|
||
| Returns: | ||
| List of port indices that are currently on. | ||
| """ | ||
| return [i for i in range(NUM_ILLUMINATION_PORTS) if self.port_is_on[i]] |
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.
The high-level multi-port methods (set_port_intensity, turn_on_port, turn_off_port, set_port_illumination, turn_on_multiple_ports, turn_off_all_ports) call the new MCU commands unconditionally without checking microcontroller.supports_multi_port(). This means that if these APIs are used against legacy firmware (where supports_multi_port() is False), they will still send unsupported command codes, leading to confusing behavior instead of a clear, early error; given that firmware version detection and supports_multi_port() were added, these methods should check support and raise a descriptive exception when multi-port is not available.
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.
[Claude Code] Fixed in commit 1d4f483 - Added _check_multi_port_support() helper method called by all multi-port methods. Raises RuntimeError with clear message if firmware doesn't support multi-port commands.
| def test_version_detected_after_any_command(self, mcu): | ||
| """Firmware version should be detected after any command.""" | ||
| # Initially might be (0, 0) until we get a response | ||
| initial = mcu.firmware_version |
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.
Variable initial is not used.
| initial = mcu.firmware_version |
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.
[Claude Code] Fixed in commit 1d4f483 - Added assertion to use the initial variable: assert isinstance(initial, tuple) and len(initial) == 2
software/control/lighting.py
Outdated
|
|
||
| # Import and re-export mapping functions (canonical location is _def.py) | ||
| from control._def import source_code_to_port_index, port_index_to_source_code | ||
|
|
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 'source_code_to_port_index' is not used.
Import of 'port_index_to_source_code' is not used.
| __reexported_mapping_functions = (source_code_to_port_index, port_index_to_source_code) |
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.
[Claude Code] Fixed in commit 1d4f483 - Removed the re-exports. The canonical location is control._def, so re-exporting from lighting.py added confusion without value.
| """ | ||
|
|
||
| import pytest | ||
| from control._def import CMD_SET, ILLUMINATION_PORT, ILLUMINATION_CODE |
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 'ILLUMINATION_PORT' is not used.
| from control._def import CMD_SET, ILLUMINATION_PORT, ILLUMINATION_CODE | |
| from control._def import CMD_SET, ILLUMINATION_CODE |
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.
[Claude Code] Fixed in commit 1d4f483 - Removed unused ILLUMINATION_PORT import.
| import pytest | ||
| from control._def import CMD_SET, ILLUMINATION_PORT, ILLUMINATION_CODE | ||
| from control.microcontroller import Microcontroller, SimSerial | ||
| from control.lighting import IlluminationController, NUM_ILLUMINATION_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.
Import of 'NUM_ILLUMINATION_PORTS' is not used.
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.
[Claude Code] Fixed in commit 1d4f483 - Removed unused NUM_ILLUMINATION_PORTS import.
| """ | ||
|
|
||
| import pytest | ||
| from control._def import CMD_SET, ILLUMINATION_PORT, ILLUMINATION_CODE |
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 'ILLUMINATION_PORT' is not used.
| from control._def import CMD_SET, ILLUMINATION_PORT, ILLUMINATION_CODE | |
| from control._def import CMD_SET, ILLUMINATION_CODE |
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.
[Claude Code] Fixed in commit 1d4f483 - Removed unused ILLUMINATION_PORT import.
| import pytest | ||
| from control._def import CMD_SET, ILLUMINATION_PORT, ILLUMINATION_CODE | ||
| from control.microcontroller import Microcontroller, SimSerial | ||
| from control.lighting import IlluminationController, NUM_ILLUMINATION_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.
Import of 'NUM_ILLUMINATION_PORTS' is not used.
| from control.lighting import IlluminationController, NUM_ILLUMINATION_PORTS | |
| from control.lighting import IlluminationController |
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.
[Claude Code] Fixed in commit 1d4f483 - Removed unused NUM_ILLUMINATION_PORTS import.
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>
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
Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
firmware/controller/src/functions.cpp:325
turn_off_illuminationonly clears the active LED pattern and toggles the single TTL port corresponding toillumination_source, but it does not interact with the newillumination_port_is_onarray beyond the current source or callturn_off_all_ports(). This means that after using the new per-port APIs to turn on multiple ports, calling the legacyTURN_OFF_ILLUMINATIONcommand will leave other ports still on in firmware, diverging from the SimSerial behavior and the Python tests which expect it to turn off all ports. Consider updating this function (or the legacy callback) to delegate toturn_off_all_ports()for TTL ports so that legacy and new illumination paths stay consistent and all ports are safely turned off.
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;
switch(illumination_source)
{
case ILLUMINATION_SOURCE_LED_ARRAY_FULL:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_LEFT_HALF:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_RIGHT_HALF:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_LEFTB_RIGHTR:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_LOW_NA:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_LEFT_DOT:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_RIGHT_DOT:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_TOP_HALF:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_ARRAY_BOTTOM_HALF:
clear_matrix(matrix);
break;
case ILLUMINATION_SOURCE_LED_EXTERNAL_FET:
break;
case ILLUMINATION_D1:
digitalWrite(PIN_ILLUMINATION_D1, LOW);
break;
case ILLUMINATION_D2:
digitalWrite(PIN_ILLUMINATION_D2, LOW);
break;
case ILLUMINATION_D3:
digitalWrite(PIN_ILLUMINATION_D3, LOW);
break;
case ILLUMINATION_D4:
digitalWrite(PIN_ILLUMINATION_D4, LOW);
break;
case ILLUMINATION_D5:
digitalWrite(PIN_ILLUMINATION_D5, LOW);
break;
}
illumination_is_on = false;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
software/control/core/core.py
Outdated
| self.graphics_widget_3.img.setImage(image, autoLevels=False) | ||
| elif illumination_source == 13: | ||
| elif port_index == 3: # D4 (matches original code behavior) | ||
| self.graphics_widget_4.img.setImage(image, autoLevels=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.
display_image only handles port_index values 0, 1, and 3; when source_code_to_port_index(illumination_source) returns 2 (D3) or 4 (D5), the function falls through without updating any widget, so frames for those ports are never displayed. To preserve the previous behavior where unmapped sources defaulted to graphics_widget_1, you should either add explicit branches for port_index == 2/4 or add a final else that routes all remaining valid D ports to a sensible widget.
| self.graphics_widget_4.img.setImage(image, autoLevels=False) | |
| self.graphics_widget_4.img.setImage(image, autoLevels=False) | |
| else: | |
| # Any remaining valid D ports (e.g., D3/D5) fall back to default widget | |
| self.graphics_widget_1.img.setImage(image, autoLevels=False) |
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.
[Claude Code] Fixed in commit f37b88c - added else clause to handle D3 (port_index=2) and D5 (port_index=4), falling back to graphics_widget_1.
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>
Summary
This PR combines two related changes:
1. Laser Port Naming Refactor
ILLUMINATION_SOURCE_405NM) to port-based names (ILLUMINATION_D1)2. Multi-Port Illumination Control (NEW)
Enable multiple illumination ports to be ON simultaneously with independent intensities:
Firmware:
illumination_port_is_on[],illumination_port_intensity[])SET_PORT_INTENSITY,TURN_ON_PORT,TURN_OFF_PORT,SET_PORT_ILLUMINATION,SET_MULTI_PORT_MASK,TURN_OFF_ALL_PORTSPython:
Microcontrollermethods for multi-port control with port validationIlluminationControllerhigh-level API with state tracking andsupports_multi_port()checksSimSerialsimulation support for testingsource_code_to_port_index()andport_index_to_source_code()mapping functionsTests:
Documentation:
software/docs/illumination-control.mdcovering:illumination_intensity_factorscaling (0.6=LEDs, 0.8=laser, 1.0=full)IlluminationControllerexplaining API choiceTest Plan
Files Changed
Firmware
firmware/controller/src/constants_protocol.h- New command codesfirmware/controller/src/globals.h/cpp- Per-port state arraysfirmware/controller/src/functions.h/cpp- Port control functionsfirmware/controller/src/commands/light_commands.h/cpp- Command callbacksfirmware/controller/src/illumination_mapping.h- Source code ↔ port mappingPython
software/control/_def.py- New CMD_SET constants, mapping functionssoftware/control/microcontroller.py- Multi-port MCU methodssoftware/control/lighting.py- IlluminationController enhancementsDocumentation
software/docs/illumination-control.md- Comprehensive guide (NEW)🤖 Generated with Claude Code