Skip to content

Conversation

@hongquanli
Copy link
Contributor

@hongquanli hongquanli commented Jan 28, 2026

Summary

This PR combines two related changes:

1. Laser Port Naming Refactor

  • Rename wavelength-based constants (ILLUMINATION_SOURCE_405NM) to port-based names (ILLUMINATION_D1)
  • Fix D3/D4 non-sequential source code mapping (D3=14, D4=13)
  • Update documentation to clarify "Illumination Control TTL Ports"

2. Multi-Port Illumination Control (NEW)

Enable multiple illumination ports to be ON simultaneously with independent intensities:

Firmware:

  • 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 (v1.0)
  • Maintain backward compatibility with legacy single-source commands

Python:

  • Add Microcontroller methods for multi-port control with port validation
  • Add IlluminationController high-level API with state tracking and supports_multi_port() checks
  • Add SimSerial simulation support for testing
  • Add early firmware version detection during init
  • Add centralized source_code_to_port_index() and port_index_to_source_code() mapping functions

Tests:

  • 80 firmware tests (PlatformIO/Unity)
  • 29 Python tests covering protocol, edge cases, D3/D4 mapping

Documentation:

  • Add comprehensive software/docs/illumination-control.md covering:
    • Hardware port mapping (D1-D5 with port indices, source codes, DAC/GPIO)
    • Legacy vs Multi-port API comparison with examples
    • Firmware version requirements
    • Troubleshooting guide
  • Document illumination_intensity_factor scaling (0.6=LEDs, 0.8=laser, 1.0=full)
  • Document non-blocking nature of Microcontroller methods
  • Add class docstring to IlluminationController explaining API choice

Test Plan

  • All 29 Python tests pass
  • All 80 firmware tests pass
  • Hardware tested - All 9 manual tests passed:
    1. ✅ Firmware version detection (v1.0)
    2. ✅ Legacy API backward compatibility
    3. ✅ Single port control (new API)
    4. ✅ Multiple ports ON simultaneously
    5. ✅ D3/D4 non-sequential mapping
    6. ✅ IlluminationController high-level API
    7. ✅ Intensity levels (0-100%)
    8. ✅ Rapid switching (20 cycles)
    9. ✅ Error handling (invalid port rejection)

Files Changed

Firmware

  • firmware/controller/src/constants_protocol.h - New command codes
  • firmware/controller/src/globals.h/cpp - Per-port state arrays
  • firmware/controller/src/functions.h/cpp - Port control functions
  • firmware/controller/src/commands/light_commands.h/cpp - Command callbacks
  • firmware/controller/src/illumination_mapping.h - Source code ↔ port mapping

Python

  • software/control/_def.py - New CMD_SET constants, mapping functions
  • software/control/microcontroller.py - Multi-port MCU methods
  • software/control/lighting.py - IlluminationController enhancements

Documentation

  • software/docs/illumination-control.md - Comprehensive guide (NEW)

🤖 Generated with Claude Code

hongquanli and others added 18 commits January 26, 2026 23:50
…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>
Copy link
Contributor

Copilot AI left a 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: Microcontroller multi-port methods with firmware-version detection, an IlluminationController multi-port API, and SimSerial enhancements to simulate the new protocol and state.
  • Refactors illumination constants and configuration to use port-based names (ILLUMINATION_D1D5) 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 selected illumination_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 treat TURN_OFF_ILLUMINATION as 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 legacy TURN_OFF_ILLUMINATION to a helper that also invokes turn_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.

Comment on lines 373 to 385
// 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;
}
Copy link

Copilot AI Jan 28, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

Comment on lines 229 to 317
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]]
Copy link

Copilot AI Jan 28, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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
Copy link

Copilot AI Jan 28, 2026

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.

Suggested change
initial = mcu.firmware_version

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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


# 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

Copy link

Copilot AI Jan 28, 2026

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.

Suggested change
__reexported_mapping_functions = (source_code_to_port_index, port_index_to_source_code)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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
Copy link

Copilot AI Jan 28, 2026

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.

Suggested change
from control._def import CMD_SET, ILLUMINATION_PORT, ILLUMINATION_CODE
from control._def import CMD_SET, ILLUMINATION_CODE

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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
Copy link

Copilot AI Jan 28, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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
Copy link

Copilot AI Jan 28, 2026

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.

Suggested change
from control._def import CMD_SET, ILLUMINATION_PORT, ILLUMINATION_CODE
from control._def import CMD_SET, ILLUMINATION_CODE

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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
Copy link

Copilot AI Jan 28, 2026

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.

Suggested change
from control.lighting import IlluminationController, NUM_ILLUMINATION_PORTS
from control.lighting import IlluminationController

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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>
hongquanli and others added 2 commits January 27, 2026 22:14
…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>
Copy link
Contributor

Copilot AI left a 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_illumination only clears the active LED pattern and toggles the single TTL port corresponding to illumination_source, but it does not interact with the new illumination_port_is_on array beyond the current source or call turn_off_all_ports(). This means that after using the new per-port APIs to turn on multiple ports, calling the legacy TURN_OFF_ILLUMINATION command 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 to turn_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.

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)
Copy link

Copilot AI Jan 28, 2026

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

hongquanli and others added 3 commits January 27, 2026 22:20
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>
hongquanli and others added 5 commits January 27, 2026 23:08
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>
@hongquanli hongquanli merged commit ef375f2 into master Jan 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants