fix: speed up XLight iris commands and avoid response mismatch#509
Conversation
- Use write_and_read instead of write_and_check for iris commands to avoid retry loop when XLight prepends board address (e.g. "B1 J800") - Reduce iris read_delay from 3s to 2s - Skip hardware call when iris value hasn't changed - Remove redundant init-time iris defaults (XLIGHT_ILLUMINATION_IRIS_DEFAULT, XLIGHT_EMISSION_IRIS_DEFAULT) — first channel switch applies config values Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR speeds up XLight iris commands by switching from write_and_check to write_and_read (avoiding retry loops when the XLight prepends a board address to its response), reducing the read_delay from 3s to 2s, and adding a short-circuit to skip hardware calls when the iris value is unchanged. It also removes the init-time iris default constants that are no longer needed since acquisition config applies values on first channel switch.
Changes:
- Replace
write_and_checkwithwrite_and_readfor iris commands and reduceread_delayfrom 3s to 2s - Add early-return in
set_illumination_irisandset_emission_iriswhen the requested value matches the current cached value - Remove
XLIGHT_ILLUMINATION_IRIS_DEFAULTandXLIGHT_EMISSION_IRIS_DEFAULTconstants and their init-time usage
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| software/control/serial_peripherals.py | Adds cached-value short-circuit to iris setters, switches to write_and_read, reduces read delay, removes init-time iris default calls |
| software/control/_def.py | Removes unused XLIGHT_ILLUMINATION_IRIS_DEFAULT and XLIGHT_EMISSION_IRIS_DEFAULT constants |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if value == self.illumination_iris: | ||
| return self.illumination_iris |
There was a problem hiding this comment.
Bug: self.illumination_iris is never initialized in XLight.__init__. The removed code at lines 273-276 (old) used to call self.set_illumination_iris(XLIGHT_ILLUMINATION_IRIS_DEFAULT), which set this attribute. Now the very first call to set_illumination_iris will access self.illumination_iris on this line before it has ever been assigned, raising an AttributeError.
You need to initialize self.illumination_iris (and self.emission_iris) in __init__, similar to how XLight_Simulation.__init__ does it (see line 171-172). For example, add self.illumination_iris = None (or 0) in __init__, and use is not None or an appropriate sentinel comparison to handle the first call.
There was a problem hiding this comment.
[Claude Code] Fixed in e26085c - initialized self.illumination_iris = 0 and self.emission_iris = 0 in XLight.init.
| if value == self.emission_iris: | ||
| return self.emission_iris |
There was a problem hiding this comment.
Bug: Same issue as set_illumination_iris — self.emission_iris is never initialized in XLight.__init__. The first call to this method will raise AttributeError when accessing self.emission_iris here. Initialize self.emission_iris in __init__ (e.g., to None) to fix this.
| if value == self.emission_iris: | |
| return self.emission_iris | |
| current_emission_iris = getattr(self, "emission_iris", None) | |
| if value == current_emission_iris: | |
| return current_emission_iris |
There was a problem hiding this comment.
[Claude Code] Fixed in e26085c - same fix, both attributes initialized to 0 in init.
sliderReleased doesn't fire on click-to-position, only on drag release. Use valueChanged with isSliderDown() guard to catch both cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
XLIGHT_ILLUMINATION_IRIS_DEFAULT and XLIGHT_EMISSION_IRIS_DEFAULT in _def.py are now referenced by the confocal model registry and config generator instead of hardcoding 100.0 in multiple places. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Iris defaults come from _def.py (XLIGHT_ILLUMINATION_IRIS_DEFAULT / XLIGHT_EMISSION_IRIS_DEFAULT) | ||
| # This fallback is used when no confocal_config.yaml or model registry is available | ||
| from control._def import XLIGHT_ILLUMINATION_IRIS_DEFAULT | ||
|
|
||
| DEFAULT_IRIS_VALUE = float(XLIGHT_ILLUMINATION_IRIS_DEFAULT) |
There was a problem hiding this comment.
Missing constant: XLIGHT_ILLUMINATION_IRIS_DEFAULT is imported from control._def but does not exist in _def.py. This will cause an ImportError at module load time. The constant needs to be defined in _def.py.
Additionally, this import is placed mid-file (after top-level constants), which violates PEP 8 import ordering. It should be moved to the import block at the top of the file (lines 9-30).
There was a problem hiding this comment.
[Claude Code] Fixed in eba12aa - moved import to top-level import block and replaced DEFAULT_IRIS_VALUE with per-iris ALL_IRIS_DEFAULTS dict.
| # This fallback is used when no confocal_config.yaml or model registry is available | ||
| from control._def import XLIGHT_ILLUMINATION_IRIS_DEFAULT | ||
|
|
||
| DEFAULT_IRIS_VALUE = float(XLIGHT_ILLUMINATION_IRIS_DEFAULT) |
There was a problem hiding this comment.
Semantic mismatch: DEFAULT_IRIS_VALUE is used as the fallback for both illumination_iris and emission_iris (see lines 72 and 75), but this change derives it only from XLIGHT_ILLUMINATION_IRIS_DEFAULT. If the illumination and emission iris defaults are ever set to different values, the emission iris fallback would incorrectly use the illumination default. Consider keeping the original hardcoded 100.0 or using separate defaults for each iris property in the fallback paths.
There was a problem hiding this comment.
[Claude Code] Fixed in eba12aa - replaced single DEFAULT_IRIS_VALUE with ALL_IRIS_DEFAULTS dict that maps each iris property to its own default from _def.py.
| from control._def import XLIGHT_EMISSION_IRIS_DEFAULT, XLIGHT_ILLUMINATION_IRIS_DEFAULT | ||
|
|
||
|
|
There was a problem hiding this comment.
Missing constants: XLIGHT_ILLUMINATION_IRIS_DEFAULT and XLIGHT_EMISSION_IRIS_DEFAULT are imported from control._def but do not exist in _def.py. This will cause an ImportError at module load time, preventing the application from starting when confocal hardware is configured.
You need to add these constants to software/control/_def.py, e.g. near line 961 alongside the other XLIGHT_* constants.
| from control._def import XLIGHT_EMISSION_IRIS_DEFAULT, XLIGHT_ILLUMINATION_IRIS_DEFAULT | |
| from control import _def as _control_def | |
| try: | |
| XLIGHT_EMISSION_IRIS_DEFAULT = _control_def.XLIGHT_EMISSION_IRIS_DEFAULT | |
| XLIGHT_ILLUMINATION_IRIS_DEFAULT = _control_def.XLIGHT_ILLUMINATION_IRIS_DEFAULT | |
| except AttributeError: | |
| # Fallback defaults if constants are missing from control._def. | |
| # Values chosen to be neutral and safe; they can be overridden by adding | |
| # the constants to control._def when available. | |
| XLIGHT_EMISSION_IRIS_DEFAULT = 0.0 | |
| XLIGHT_ILLUMINATION_IRIS_DEFAULT = 0.0 |
There was a problem hiding this comment.
[Claude Code] False positive - constants were added to _def.py in commit 6b41dcb (same PR, earlier commit).
Without init, the skip-if-unchanged check in set_illumination_iris / set_emission_iris would raise AttributeError on first call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address Copilot review: ALL_IRIS_DEFAULTS dict maps each iris property to its own default from _def.py, avoiding semantic mismatch if illumination and emission defaults ever diverge. Moved import to top-level import block per PEP 8. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.emission_iris = value | ||
| value = str(int(10 * value)) | ||
| self.serial_connection.write_and_check("V" + value + "\r", "V" + value, read_delay=3) | ||
| self.serial_connection.write_and_read("V" + str(int(10 * value)) + "\r", read_delay=2) |
There was a problem hiding this comment.
Same issue as with set_illumination_iris: self.emission_iris = value is assigned before the serial command. If write_and_read fails, the cached value will be wrong and the skip-if-unchanged check on line 421 will prevent future retries. Move the assignment after the write_and_read call.
| self.illumination_iris = value | ||
| value = str(int(10 * value)) | ||
| self.serial_connection.write_and_check("J" + value + "\r", "J" + value, read_delay=3) | ||
| self.serial_connection.write_and_read("J" + str(int(10 * value)) + "\r", read_delay=2) |
There was a problem hiding this comment.
The cached value self.illumination_iris is updated before the serial command is sent. If write_and_read raises a SerialDeviceError (e.g., no response after max attempts), the cached value will reflect the intended value rather than the actual hardware state. Combined with the new skip-if-unchanged check on line 408, subsequent calls with the same value will be silently skipped, leaving the hardware in the wrong state with no way to retry.
Consider moving self.illumination_iris = value to after the write_and_read call succeeds, so that a failed command doesn't poison the cache.
Summary
write_and_readinstead ofwrite_and_checkfor iris commands — avoids retry loop when XLight prepends board address to response (e.g.B1 J800instead ofJ800)read_delayfrom 3s to 2s (saves 2s per channel switch when both irises change)XLIGHT_ILLUMINATION_IRIS_DEFAULT,XLIGHT_EMISSION_IRIS_DEFAULT) — first channel switch applies values from acquisition configTest plan
🤖 Generated with Claude Code