Skip to content

Light panel: illumination mode is the root of the control - #164

Open
pskeshu wants to merge 2 commits into
gently-project:developmentfrom
pskeshu:feature/light-mode-root
Open

Light panel: illumination mode is the root of the control#164
pskeshu wants to merge 2 commits into
gently-project:developmentfrom
pskeshu:feature/light-mode-root

Conversation

@pskeshu

@pskeshu pskeshu commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

LED and laser are the two ways this instrument illuminates a sample, and the workflow alternates between them — LED to find embryos, laser to calibrate and acquire. They were two independent rows in the panel. Mode is now the first question, with each mode's controls disclosed beneath it.

The config was an echo

Mode cannot be derived from a value that is not read, and the laser config was not read. readAll polled beam and led from hardware; config was whatever the select's own onchange had last written — so the whole laser branch rested on a request that returned 200, which is the assumption #106 is made of.

DiSPIMLightSource.read() has had the answer all along and no route exposed it. GET /api/laser/configs now returns current beside the preset list. The string "unknown", which read() yields when the group cannot be queried, stays an em dash rather than becoming a preset name.

Both open is a state, not an impossibility

LED and Laser are two independent Micro-Manager config groups (devices/optical.py:26,131), so exclusivity is a policy this panel asserts, not an interlock the hardware enforces. Both paths are wanted separately — spim_head_focus opens the LED on the SPIM camera by design (plans/acquisition.py:1462) — and #106 is the overlap: calibrateSelected never closed the LED, so every vision frame was brightfield with a 50 ms laser gate on top and the detector hunted nuclei in a DIC-like image.

So mode() returns both and the panel says so. An either/or would have made the panel's own founding bug unrepresentable.

LED routed lines mode
brightfield Open none led
fluorescence Closed ≥1 laser
dark Closed none off
#106 Open ≥1 both → amber line
anything unread null

What a mode click does

Engaged and disclosed look different: filled when the device says a source is open, underlined when a branch is merely on screen so it can be configured on a dark rig.

Found in the browser, not in a test

  • Armed beam with nothing routed reads as mode off, so the laser branch is closed — and idleBeamNote was inside it, invisible in the one state it describes. That is the resting state of the rig. Hoisted to the panel root, with a test that says so.
  • The mode word ("LED + laser") had nowhere to go in a 246 px panel and was clipped. Dropped: the buttons are the readout, and the two states a word would have added (both, unread) each have their own line.
  • .lp-btn gets white-space: nowrap — "Set No" was wrapping to two lines.

Verification

Seven states driven through the live DOM (unknown, LED, off, armed-and-unrouted, routed-with-beam-off, laser emitting, both open), plus interception of the writes each mode button issues:

to LED    → laser/config {"config":"ALL OFF"} , led/set {"state":"Open"}
to Off    → laser/config {"config":"ALL OFF"} , led/set {"state":"Closed"}
to Laser  → led/set {"state":"Closed"}                     # no line routed

node --test tests/js/light-panel.test.mjs 18 pass · tests/test_light_panel_disclosure.py 10 pass · ruff, ruff format, mypy clean.

Rig check

getCurrentConfig on the Laser group is one more property read per 10 s poll, and this scope already logs Property read slow: 2.4s. Worth a glance at the device-layer log; the poll interval is one constant if it bites.

Relates to #106.

🤖 Generated with Claude Code

pskeshu and others added 2 commits September 6, 2026 08:37
The panel had LED and laser as two independent rows. They are not two
settings that happen to be adjacent — they are the two ways this
instrument illuminates a sample, and the workflow alternates between
them: the LED to find embryos, the laser to calibrate and acquire. So
mode is the first question the panel asks, and each mode's own controls
are disclosed beneath it.

THE CONFIG WAS AN ECHO

Mode cannot be derived from a value that is not read, and the laser
config was not read. `readAll` polled beam and led from hardware; config
was whatever the select's own `onchange` had last written. The entire
laser branch rested on a request that returned 200 — the assumption
gently-project#106 is made of. `DiSPIMLightSource.read()` has had the answer all
along and no route exposed it; `GET /api/laser/configs` now returns
`current` alongside the preset list. The string "unknown", which
`read()` yields when the group cannot be queried, stays an em dash
rather than becoming a preset name.

BOTH OPEN IS A STATE, NOT AN IMPOSSIBILITY

LED and Laser are two independent Micro-Manager config groups
(`devices/optical.py`), so their exclusivity is a policy this panel
asserts, not an interlock the hardware enforces. Both paths are wanted
separately — `spim_head_focus` opens the LED on the SPIM camera by
design — and gently-project#106 IS the overlap: `calibrateSelected` never closed the
LED, so every vision frame was brightfield with a 50 ms laser gate on
top and the detector hunted nuclei in a DIC-like image.

So `mode()` returns `both`, and the panel states it. Collapsing the
pair into an either/or would have made its own founding bug
unrepresentable.

Entering LED or Off mode gates the lines to ALL OFF, which is the
documented brightfield-safe state (spec §2.7); entering laser mode only
closes the LED. Routing a wavelength stays an explicit act, because it
is an emission decision. BeamEnabled is left alone throughout — setting
it No here would recreate the state gently-project#106 is about, that nothing sets
back.

Engaged and disclosed are different things and look different: filled
when the device says a source is open, underlined when a branch is
merely on screen so it can be configured on a dark rig.

FOUND IN THE BROWSER, NOT IN A TEST

Two things the DOM showed that the assertions had not:

- armed beam, nothing routed reads as mode `off`, so the laser branch
  is closed — and `idleBeamNote` was inside it, invisible in the one
  state it describes. It is at the panel root now, with a test that
  says so. This is the resting state of the rig.
- the mode word ("LED + laser") had nowhere to go in a 246 px panel and
  was clipped. Dropped: the buttons are the readout, and the two states
  a word would have added each have their own line already.

RIG CHECK

`getCurrentConfig` on the Laser group is one more property read per
10 s poll, and this scope already logs "Property read slow: 2.4s".
Watch the device-layer log; the poll is easy to slow down if it bites.

Verified in the browser across seven states (unknown, LED, off, armed
and unrouted, routed with the beam off, laser emitting, both open) and
by intercepting the writes each mode button issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t#106)

The other half of gently-project#106, and the one that ruined data rather than just
misreporting it. `calibrate_embryo` never closed the LED.

A session finds embryos in brightfield — that is what the LED is for.
Nothing closed it afterwards, so every frame the edge detector saw was
LED brightfield with a 50 ms laser gate on top, and Claude was asked to
find nuclei in a DIC-like image. The calibration pane's own method text
already said the LED alone shows no nuclei; the code did not act on it.

One call, in the one place both callers pass through: the Operate pane's
`POST /api/devices/embryos/{id}/calibrate` and the agent's own
`calibrate_embryo` tool. Closing the LED in the route would have left
the agent path broken, and the reverse.

Unconditional, because this path does no brightfield work of its own:
every frame comes from `capture_lightsheet_image` and there is no
head-focus phase — `spim_head_focus` is the plan that legitimately wants
the LED open, and it is not on this path.

A failed shutter close is logged, not fatal. The cost of an open LED is
a poor fit; refusing to calibrate because a status call timed out is
worse mid-session, and the Light panel now shows the LED state
throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pskeshu

pskeshu commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed f8eeaa1 — while checking whether enforcing exclusivity would make #106's state unreachable, I found the state was still being created on every calibration.

calibrate_embryo never closed the LED. A session finds embryos in brightfield (that is what the LED is for), nothing closed it afterwards, so every frame the edge detector saw was brightfield with a 50 ms laser gate on top — Claude asked to find nuclei in a DIC-like image. The calibration pane's own method text already said the LED alone shows no nuclei.

One call, at the point both callers route through (POST /api/devices/embryos/{id}/calibrate and the agent tool). Unconditional: this path does no brightfield work of its own — every frame comes from capture_lightsheet_image, and spim_head_focus, the plan that legitimately wants the LED open, is not on it. A failed close is logged rather than fatal; the cost is a poor fit, and aborting a calibration mid-session because a status call timed out is worse.

Still rig-gated: whether the beam actually fires. This fixes what the beam was firing into.

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.

1 participant