The current spin box could not hold a typed value: the 1 Hz status poll read LDS and wrote it straight into the spin box, so the operator's number was replaced by the laser's within a second — before Set could be pressed. The spin box is now seeded once on connect and belongs to the operator after that; the poll's reading goes to a read-back label beside the Set button, so the value about to be sent and the value the laser holds are separate readouts. The write itself was also unverified. Section 6 of the operator's manual: "Commands or set values can be discarded by the controller unintentionally. It is recommended to query the set value after the command is entered to confirm the actual value." set_current_ma() wrote LDS and returned True regardless, so a discarded write looked exactly like a good one. _write_verified() now writes, reads back, and retries up to three times; set_current_ma() and set_frequency_hz() use it, and HeliosWorker reports a refusal on the status line instead of echoing the requested value. Also from the manual, recorded but not acted on: LDS accepts 0-7000 mA (the driver's 2000 mA ceiling is this rig's, not the protocol's), LDF has to be re-sent after LDG changes, and the power-monitor mnemonic is HMP — which this laser does not implement, per the operator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.2 KiB
Known issues requiring on-rig verification
Questions that cannot be answered from the code alone. Check these the next time the hardware is available; each one gates a small code change.
uC480 camera: gain/exposure during active capture
The driver used to carry an (unused) _capture_paused context manager whose
docstring claimed many IDS cameras return IS_CANT_COMMUNICATE_WITH_DRIVER
(17) or IS_NO_SUCCESS (-1) when gain/exposure commands are issued during
active capture. set_exposure() and set_gain() never used it, and the
helper was deleted in the Phase-1 cleanup.
Bench check: with live streaming running, move the exposure and gain
sliders in camera_test_app.py and watch the log for those error codes.
If they appear, the setters need a stop-live/apply/restart sequence
(re-create the helper around the two call sites in
uc480_camera.py).
Helios: no output-power query
docs/hardware/HELIOS_DRIVER_README.md documents driver.get_power_mw(),
but HeliosLaser has no such method and no output-power mnemonic appears
anywhere in this repo's protocol notes. helios_test_app.py called it
anyway and raised AttributeError into a popup; the button is now disabled
and the handler reports the gap instead.
Answered (2026-09-08): the mnemonic is HMP (Table 6-3, "Laser Power
Monitor", read-only, 0-5000 mW) — but the table adds "not available on all
models", and the operator confirms this rig's laser has no power meter. So
the button stays disabled; what remains is to delete the Power Monitoring
group from helios_test_app.py and drop the power-monitoring claims from
docs/hardware/HELIOS_DRIVER_README.md.
Genesis laser: forked protocol implementations disagree
hardware/genesis_core.py and the reference implementation
tools/genesis_laser_gui.py disagree on ADC command bytes, LDD enable
polarity, shutter semantics, filtering, and scaling. Do not modify either
until the checklist in docs/genesis_verification.md
has been run on the bench.
lib/ueye_loader.so — still needed?
lib/ueye_loader.c is an LD_PRELOAD shim that dlopens
/usr/lib/libueye_api.so — yet nothing in the repo references it, and the
vendored SDK copy is lib/libueye_api64.so.3.82 (a different file). On the
rig, check whether the camera apps run without the shim; if they do, delete
lib/ueye_loader.{c,so}. Either way, record in SETUP.md where
libueye_api64.so.3.82 came from (IDS SDK version) and how the loader is
meant to be used.
Per-angle background: trigger round trip mid-scan
Every angle now captures its own background, so the scope switches from the
scan-time logic-AND trigger back to the single-record edge trigger and
returns to it once per angle (core/scope_sras.py:
configure_background_trigger → capture_background →
configure_scan_trigger). Before this, that transition happened once per
scan, with the stage idle and nothing depending on how long it took.
Bench check: run a multi-angle scan and watch the first row after each
background. If frames go missing at the start of an angle, the 0.2 s settle
in configure_scan_trigger is not enough for FastFrame to re-arm after an
AVERAGE-mode sequence, and the row-packing warning ("N frames acquired, M
expected") will say so in the log. Raise the settle rather than the ramp
buffer — the stage geometry is not what changed.
Auto-align: constants that are guesses until the rig confirms them
core/auto_align.py closes a loop over hardware whose gain nobody has
measured. Three numbers in AlignSettings/TAxisSettings are reasoned
defaults, not readings:
probe_steps = 200— the first move of every search, made only to learn how many millivolts a microstep is worth. Too small and each search wastes iterations doubling it (the status line says so: "moved N microsteps and the DC difference did not change"); too large and the first move overshoots by more than the platform should be asked to travel in one go.max_excursion_steps = 20000— the per-axis safety limit, measured from wherever the axis started. It exists to stop a runaway before the actuator reaches its end stop, so it has to be smaller than the real travel.hold_current_ma = 300— the run current (600 mA) and microstepping (32) are specified; the standstill current is half the run current by analogy with the GR axis, and has not been checked against the platform's weight.
Bench check: run one auto-align and read the log. The first search's iteration count is the probe verdict — 3 or 4 steps means the probe is about right, and a "did not change by 2 mV" line means it is too small. Convert the applied corrections into actuator travel and compare against the T-axis travel to set the excursion limit. Watch whether the platform holds its tilt between the two phases; if it sags, raise the hold current.
Auto-align: does the X phase's piston matter, and where is the pivot?
The X phase moves T1 alone, as specified. T1 is the only axis lying along X,
so it does tilt the platform along X — but moving one leg of three also lifts
the platform by a third of the move (tilt_response(X_TILT) returns a piston
of 1/3 alongside the 2/3 tilt). The search nulls the split-detector
difference, which a piston should not move, so the assumption is that the
piston is harmless. The piston-free alternative is T1 +1 with T0 and T2 at
−0.5 each.
Separately, the procedure assumes the tilt pivot is under the beam: if it is
not, applying the correction shifts the DC levels at the reference point
itself, and the Y phase then chases levels that no longer describe the rig.
The code reports this rather than compensating for it — AxisResult's
"back at the reference" reading after the X phase is exactly that
measurement.
Bench check: during an X search, watch DC1 + DC2 (the sum, not the
difference) on the scope. If the sum moves as T1 moves, the piston is
changing the amount of collected light and X_TILT should become the
piston-free triple. Then read the X phase's reference residual out of the
log: more than a few millivolts means the pivot is not under the beam, and
the Y phase's reference should be re-measured after the X correction instead
of reusing the operator's original numbers.