Skip to content

feat: rate-aware laser-safety RATE_LL + trigger overrides for 60 Hz mode - #130

Draft
boringethan wants to merge 8 commits into
nextfrom
feature/129-60hz-rate-ll
Draft

feat: rate-aware laser-safety RATE_LL + trigger overrides for 60 Hz mode#130
boringethan wants to merge 8 commits into
nextfrom
feature/129-60hz-rate-ll

Conversation

@boringethan

Copy link
Copy Markdown
Contributor

Refs #129

What

  • apply_laser_power(trigger_freq_hz=...): rescales the bundled EE_RATE_LL/OPT_RATE_LL safety-FPGA floor by 40/freq (baseline 70313 ticks = 22,500 µs min inter-pulse period at 40 Hz → 46875 ticks = 15,000 µs at 60 Hz — the same 0.9× proportional margin). MotionInterface.apply_laser_power passes its resolved default trigger frequency automatically. None/40 leave the baseline untouched; explicit per-key user-config overrides still win.
  • trigger_overrides_for_rate(rate_hz): returns {TriggerFrequencyHz, LaserPulseSkipDelayUsec} with the dark-frame pulse displacement scaled by the period. Without this, every dark frame at 60 Hz would trip the interlock: the post-dark interval would be 16,667−1,800 = 14,867 µs, below the 15,000 µs scaled floor. Scaled (1,200 µs) the margin stays ~3 % and the displaced pulse still clears the 648 µs exposure window.
  • enable_aggregator_fsin(rate_hz=40): reserved byte carries the rate, matching sensor-fw#78 (40 sent as legacy 1 so older firmware keeps working).

⚠️ Safety review requested

This intentionally changes the programmed laser-safety rate window when (and only when) a 60 Hz trigger is selected. At 60 Hz with unchanged pulse width (500 µs), average optical power rises 1.5×. Pulse-width limits (UL 1,000 µs) are untouched. Requesting laser/safety owner sign-off before this ships in any release.

Companion PRs

  • sensor-fw#78 (FSIN rate select + budget scaling)
  • bloodflow-app#327 (captureRateHz config flag) — app PR depends on this one (imports trigger_overrides_for_rate).

Status

  • Unit tests: laser 13/13 (3 new scaling tests + skip-delay test), sensor 12 pass, interface 14 pass
  • Bench validation — pending, rig currently occupied; will comment results here.

🤖 Generated with Claude Code

boringethan and others added 4 commits July 4, 2026 23:09
The bundled EE_RATE_LL/OPT_RATE_LL payloads encode a 22,500 us minimum
inter-pulse period (0.9x the 40 Hz period) — a 60 Hz trigger would trip
the interlock. apply_laser_power(trigger_freq_hz=...) now rescales the
floor by 40/freq (60 Hz -> 15,000 us, same proportional margin);
MotionInterface.apply_laser_power passes its resolved default trigger
frequency automatically. 40 Hz / None leave the baseline untouched, and
explicit per-key user-config overrides still win.

Refs #129

Co-Authored-By: Claude Fable 5 <[email protected]>
Matches sensor-fw#78: reserved byte carries the rate (40 sent as legacy
1 so older firmware keeps working; 60 selects the new 60 Hz generator).

Refs #129

Co-Authored-By: Claude Fable 5 <[email protected]>
… rate

An unscaled 1800 us LaserPulseSkipDelayUsec at 60 Hz puts the post-dark
inter-pulse interval (14867 us) below the scaled RATE_LL floor
(15000 us) — an interlock trip on every dark frame. Scaling it by
40/rate (1200 us at 60 Hz) keeps the same ~3% margin as 40 Hz while
still clearing the 648 us exposure window.

Refs #129

Co-Authored-By: Claude Fable 5 <[email protected]>
…are RATE_LL scaling, rate whitelist

- CalibrationWorkflow skip/window frame math now uses the run's resolved
  trigger frequency instead of the fixed CAPTURE_HZ constant (windows
  landed at wrong wall-times at 60 Hz).
- SUPPORTED_CAPTURE_RATES_HZ = (40, 60) in omotion.config as the shared
  whitelist; trigger_overrides_for_rate and enable_aggregator_fsin now
  reject anything else (0 previously encoded as reserved=0 = DISABLE and
  reported success; unvalidated rates would have half-configured a
  weakened safety window).
- apply_laser_power fails loudly if a non-baseline rate is requested but
  the RATE_LL entries are missing from laser_params (silently-unscaled
  floor = interlock trip on every pulse = dark laser with no error), and
  rescales stored per-key user-config RATE_LL overrides the same way as
  the bundled baseline (they are calibrated for 40 Hz; written verbatim
  at 60 Hz they would exceed the pulse period).
- Baseline 40 Hz literal now read from DEFAULT_TRIGGER_CONFIG.

Refs #129

Co-Authored-By: Claude Fable 5 <[email protected]>
@boringethan

Copy link
Copy Markdown
Contributor Author

Bench-validated 2026-07-04 with hardware register readback (full results on issue #129): RATE_LL 22,500→15,000 µs at 60 Hz and restored at 40 Hz; no interlock faults laser-on at either rate; 40 Hz clinical scan clean. Post-open review fixes: fail-loud missing-entry check, user-config override rescaling, SUPPORTED_CAPTURE_RATES_HZ whitelist, CalibrationWorkflow rate-aware frame math. ⚠️ Laser/safety owner sign-off still requested before release. Marking ready for review.

@boringethan
boringethan marked this pull request as ready for review July 5, 2026 07:01
…y tracking

- enable_camera_fsin_ext(rate_hz=40): reserved byte carries the trigger
  rate so firmware retimes the camera VTS (sensor-fw#78/#80); 40 encodes
  as legacy 1 for old-firmware compat; whitelist-validated.
- ScanWorkflow.start_scan resolves the request's trigger frequency and
  passes it to the FSIN-external enable.
- trigger_overrides_for_rate: LaserPulseDelayUsec now tracks the
  exposed-row band shift from the per-rate VTS ((2768-1845) rows x
  9.0318 us + 100 us = 8436 us at 60 Hz — bench-measured optimum,
  matches the datasheet timing model exactly). 40 Hz keeps the default.

Bench-validated end-to-end 2026-07-05 (see sdk#129): 60 Hz laser-on
clinical scan 8/8 cameras 30 s zero gaps with signal parity to 40 Hz,
followed by a clean 40 Hz regression on the same session.

Refs #129

Co-Authored-By: Claude Fable 5 <[email protected]>
@boringethan

Copy link
Copy Markdown
Contributor Author

Second commit (e94088d): enable_camera_fsin_ext(rate_hz) (ScanWorkflow passes the resolved trigger frequency) + trigger_overrides_for_rate now moves LaserPulseDelayUsec with the per-rate VTS exposure-band shift (100 → 8436 µs at 60 Hz — bench-swept, matches the datasheet timing model exactly). 16 laser tests + 51 total pass. End-to-end 60 Hz validation results on sensor-fw#80. Note the ordering invariant the validation surfaced: the scaled RATE_LL floor must be written before any 60 Hz trigger runs, or the interlock latches until power-cycle — the app's connect-time apply_laser_power satisfies this by construction.

trigger_overrides_for_rate now always emits every rate-managed key
(explicit baselines at 40 Hz) so a live switch back restores them;
set_capture_rate merges the overrides into the default trigger config
and re-applies the laser config so the RATE_LL floor matches the new
rate BEFORE any trigger runs at it, rolling back the config on a failed
floor write. Camera VTS retime rides on the next scan start.

Bench (2026-07-05): one session, connect at 40 -> switch to 60 (floor
readback 15,000 us, next scan 60 Hz clean) -> switch back to 40 (floor
22,500 us, next scan 40.00 Hz clean).

Refs #129

Co-Authored-By: Claude Fable 5 <[email protected]>
@boringethan

Copy link
Copy Markdown
Contributor Author

Third commit (93a0225): MotionInterface.set_capture_rate(rate_hz) — live 40/60 switch. Re-resolves the trigger default (all rate-managed keys now explicitly emitted so switching back restores baselines) and re-applies the RATE_LL floor before any trigger runs at the new rate, with config rollback on failure. Bench-validated round trip in one session: 40→60 (floor 15,000 µs readback, next scan 60 Hz clean) →40 (floor 22,500 µs, next scan 40.00 Hz clean). 34 tests pass.

…rrides

trigger_overrides_for_rate keeps the bench-validated 8436 us pulse
delay at 60 Hz (full signal) and now documents why it is a known
limitation: at that position the pulse transient sits inside the FPGA
SPI push window and can glitch the marginal SPI links (cams 6/8) at
scan start — 14/16 all-16, clinical mask unaffected. The clean fix
(re-pin the exposure band to ~100 us via the sensor's r_init_man) is
tracked on sensor-fw#68.

Refs #129

Co-Authored-By: Claude Fable 5 <[email protected]>
@boringethan boringethan added the 60hz Sampling Mode Tabled 60 Hz capture mode feature — see bloodflow-app#327 for the resurrection runbook label Jul 5, 2026
@boringethan
boringethan marked this pull request as draft July 5, 2026 21:08
Per the 'Ultrasound & Laser Safety Limits Calculator' AEL sheet
(λ=795 nm, 500 µs @ 40 Hz, T=300 s, 3 mm beam, duty 2.0%): the
average-power AEL rows scale as 1/rate, so the 60 Hz gate width scales
with the period (500 -> 333 µs), holding duty at the 40 Hz-validated
2.0%. Per-pulse energy then drops ∝ t while the per-pulse AEL falls
only as t^0.75, so single-pulse and pulse-train (C5/N) margins
strictly improve.

- trigger_overrides_for_rate emits the scaled LaserPulseWidthUsec
  (baseline value at 40 Hz, like every rate-managed key).
- apply_laser_power now also rescales EE/OPT_PULSE_WIDTH_UL
  (1000 -> 667 µs) so the safety FPGA ENFORCES the shortened gate at
  60 Hz rather than merely permitting it; same fail-loud bookkeeping
  as RATE_LL.

Resurrect-time validation note: the optical pulse is driver-shaped at
~494 µs regardless of gate width, so a 333 µs gate clips it — verify
delivered energy with a power meter before clinical use at 60 Hz.

Refs #129

Co-Authored-By: Claude Fable 5 <[email protected]>
@boringethan

Copy link
Copy Markdown
Contributor Author

Added while tabled (9ef8704, per Ethan): IEC 60825 pulse-width scalingLaserPulseWidthUsec 500→333 µs at 60 Hz (constant 2.0% duty per the AEL calculator sheet) and EE/OPT_PULSE_WIDTH_UL interlock ceiling 1000→667 µs so the safety FPGA enforces it. 18 laser tests pass. Tracked as sdk#132 (Backlog, 1.6.0 milestone, 60hz Sampling Mode label). Optical energy verification flagged for resurrect time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

60hz Sampling Mode Tabled 60 Hz capture mode feature — see bloodflow-app#327 for the resurrection runbook

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant