Skip to content

[feat] omicronxx: optimize speed when changing the light intensity#3302

Merged
pieleric merged 1 commit intodelmic:masterfrom
pieleric:feat-omicronxx-optimize-speed-when-changing-the-light-intensity
Jan 8, 2026
Merged

[feat] omicronxx: optimize speed when changing the light intensity#3302
pieleric merged 1 commit intodelmic:masterfrom
pieleric:feat-omicronxx-optimize-speed-when-changing-the-light-intensity

Conversation

@pieleric
Copy link
Copy Markdown
Member

The most typical change is to change just one channel. However, until
now, the driver would always update all channels. Changing a channel can
take ~50ms. It's not so long, but on systems with 4 channels, when
turning on and off one channel quickly, that would end up costing around
300ms overhead.

=> Only send commands for the channel that changed

Copy link
Copy Markdown

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 pull request optimizes the omicronxx driver to significantly reduce latency when changing light intensity by only sending commands to channels that have actually changed, rather than updating all channels regardless of whether they changed.

Key changes:

  • Implements state tracking via _current_power to remember previous power values for each channel
  • Modifies _updateIntensities() to compare current vs target power per channel and skip unchanged channels
  • Optimizes master device control to only toggle when transitioning between all-off and not-all-off states

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/odemis/driver/omicronxx.py
Comment thread src/odemis/driver/omicronxx.py Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 12, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

The driver now maintains per-instance per-device power state in _current_power, initialized to each device's maximum to represent an "all on" starting state. _updateIntensities compares per-device current (cp) vs target (tp) power and updates only when they differ: turning devices on before setting power when transitioning from 0→>0, clipping power to device max, and deactivating/setting power to 0 when turning off. Master handling is adjusted to turn on only when moving from all-off to non-all-off and to turn off when all devices become off. _current_power is updated after each iteration.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Test as Test / Client
    participant Driver as GenericxX Driver
    participant USB as USBAccesser
    participant Device as Omicron Device (per-channel)
    Note over Driver,USB: Driver holds _current_power and receives target intensities
    Test->>Driver: request intensity change (targets)
    Driver->>Driver: compute current_all_off, per-device cp, tp
    alt tp > cp and cp == 0 (turning on)
        Driver->>USB: send "turn on device" command
        USB->>Device: activate device
        Driver->>USB: send "set power" command (clipped)
        USB->>Device: set intensity
    else tp == 0 and cp > 0 (turning off)
        Driver->>USB: send "deactivate device" command
        USB->>Device: deactivate and set 0
    else cp != tp (power change while on)
        Driver->>USB: send "set power" command (clipped)
        USB->>Device: set intensity
    else cp == tp
        Note over Driver: skip device (no command)
    end
    alt master transition from all-off -> non-all-off
        Driver->>USB: send "turn on master" command
    else master transition to all-off
        Driver->>USB: send "turn off master" command
    end
    Driver->>Driver: update self._current_power with new states
    Driver->>Test: respond (completion)
Loading

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: optimizing speed when changing light intensity by only updating affected channels instead of all channels.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem (unnecessary overhead from updating all channels) and the solution (send commands only for changed channels).
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/odemis/driver/omicronxx.py (1)

649-649: Redundant initialization of _current_power.

This assignment is immediately overwritten at line 665 before being used. Consider removing this line or adding a comment explaining the defensive initialization.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1ba98d and 6ad6503.

📒 Files selected for processing (1)
  • src/odemis/driver/omicronxx.py (3 hunks)
🧰 Additional context used
🪛 GitHub Actions: Linting
src/odemis/driver/omicronxx.py

[error] 1-1: PNG metadata check detected forbidden chunks in file. Contains forbidden metadata chunks.

🪛 Ruff (0.14.8)
src/odemis/driver/omicronxx.py

705-705: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Agent
  • GitHub Check: CodeQL analysis (python)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: build (ubuntu-24.04)
🔇 Additional comments (3)
src/odemis/driver/omicronxx.py (3)

663-666: LGTM!

The "trick" to simulate all devices being on before turning them off is a clean approach to ensure proper initialization commands are sent. The comment clearly explains the rationale.


691-730: LGTM - Optimization logic is correct.

The per-channel comparison and selective command sending achieves the PR objective of avoiding unnecessary overhead. The logic correctly handles:

  • Skipping unchanged channels
  • Proper sequencing for on transitions (LightOn → setLightPower → activate)
  • Proper sequencing for off transitions (deactivate → LightOff → setLightPower)
  • Master device state transitions only when crossing the all-off boundary

705-722: Consider adding strict=True to zip() for defensive programming, but verify Python 3.10+ support first.

The strict=True parameter would raise a ValueError if self._devices, self._current_power, and power have different lengths, catching potential bugs early. However, this parameter requires Python 3.10+, and the project's minimum Python version could not be determined from the codebase. Verify the minimum supported Python version before applying this refactor.

Note: The current code is safe by design—these three iterables are guaranteed to have matching lengths because self._power.value is initialized with len(self._devices) elements, and both self._current_power and the power parameter are always synchronized to this length.

Copy link
Copy Markdown
Contributor

@tmoerkerken tmoerkerken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few unit tests would be nice to prove it works :)

The most typical change is to change just one channel. However, until
now, the driver would always update all channels. Changing a channel can
take ~50ms. It's not so long, but on systems with 4 channels, when
turning on and off one channel quickly, that would end up costing around
300ms overhead.

=> Only send commands for the channel that changed
@pieleric pieleric force-pushed the feat-omicronxx-optimize-speed-when-changing-the-light-intensity branch from 6ad6503 to df27742 Compare December 15, 2025 15:16
@pieleric
Copy link
Copy Markdown
Member Author

A few unit tests would be nice to prove it works :)

It's just an optimisation, so the behaviour should stay identical. That makes it a little harder to test, but with the help of copilot, I've added a wrapper to count the number of commands passed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/odemis/driver/omicronxx.py (1)

663-666: Document the initialization trick more clearly.

The comment describes the trick but doesn't explain why setting _current_power to max power (while hardware is actually off) is necessary. Consider adding a brief explanation that this ensures all channels get proper initialization commands (turn off + set to 0) on the first call to _updateIntensities.

src/odemis/driver/test/omicronxx_test.py (1)

21-23: Consider removing unused import.

The List type from the typing module is imported but doesn't appear to be used in the test file. Consider removing it to keep imports clean.

-from typing import Any, List
+from typing import Any
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ad6503 and df27742.

📒 Files selected for processing (2)
  • src/odemis/driver/omicronxx.py (3 hunks)
  • src/odemis/driver/test/omicronxx_test.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/odemis/driver/omicronxx.py (1)
src/odemis/model/_vattributes.py (5)
  • value (381-382)
  • value (385-388)
  • value (781-786)
  • value (789-790)
  • min (924-925)
src/odemis/driver/test/omicronxx_test.py (1)
src/odemis/driver/omicronxx.py (2)
  • USBAccesser (60-178)
  • sendCommand (124-151)
🪛 Ruff (0.14.8)
src/odemis/driver/omicronxx.py

705-705: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: build (ubuntu-22.04)
🔇 Additional comments (4)
src/odemis/driver/omicronxx.py (3)

649-649: Verify initialization synchronization with power.value.

The initialization of _current_power from self.power.value assumes that power.value returns a consistent snapshot. Since power is a ListContinuous VA that can be modified externally, ensure that this initialization happens before any subscribers can modify the value, or that concurrent modifications are handled safely.


692-693: No action needed—timing values are consistent.

The comment states "each command takes ~15 ms" while the PR description mentions "changing a single channel can take ~50 ms". These values are not inconsistent: a channel change involves multiple commands (typically 3 per operation: LightOn(), setLightPower(), and activate()). Therefore, 3 commands × ~15 ms ≈ 45–50 ms per channel, which aligns perfectly with the PR description. The two measurements refer to different granularities of the same operation, not contradictory claims.


697-697: Master state transitions are correctly guarded by state change conditions; however, manual master modifications won't be actively recovered.

The initial state (line 665) correctly triggers master turn-off on first call: _current_power is set to power.range[1], then _updateIntensities() is called with actual power (0), making the condition at line 727 (all_off and self._master and not current_all_off) evaluate true.

The turn-on condition (line 697) and turn-off condition (line 727) properly ensure master state changes only during device power transitions. However, if the master device is manually turned off/on externally (via direct device control), the logic won't detect or recover from the mismatch—the master state will remain inconsistent until the next power transition occurs (all_off or not all_off condition is triggered). This is acceptable only if external master modifications are not expected; otherwise, active state validation should be added.

src/odemis/driver/test/omicronxx_test.py (1)

42-61: LGTM! CountingUSBAccesser implementation is clean.

The wrapper correctly intercepts sendCommand calls while delegating all other attribute access to the wrapped instance. The type hints improve code clarity.

Comment thread src/odemis/driver/omicronxx.py
Comment thread src/odemis/driver/test/omicronxx_test.py
@pieleric pieleric merged commit 14ad8b2 into delmic:master Jan 8, 2026
5 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.

4 participants