4 Commits

Author SHA1 Message Date
Thomas Ales 76ed7828cc Helios: read CRLF replies as lines, not as CR plus dead air
The rig transcript (tools/helios_lds_probe.py) settles where the panel's
32 mA came from, and it was never the laser: LDS reads 100 mA, answers for
itself, and takes a write of 900 mA on the first attempt. 32 is LCE — bit
5, "Door switch open" — arriving in the diode-current field.

Every reply is CRLF-terminated and padded with a blank line or two:

    b'LDS =    100 mA\r\n\r\n'
    b'LCE =     32\r\nBit 15..0: 0000 0000 0010 0000\r\n\r\n\r\n'

_read_line() read up to CR, so the final LF of every reply stayed in the
buffer, and the next read waited out the whole port timeout for a CR that
only the next command would bring. A second of dead air per query: replayed
against the transcript's byte timing, one status poll took 8.6 s against
the 1 s interval that schedules it. That is also what let the values drift
apart — a query whose deadline goes to a blocked read gives up while its
own reply is still on the wire, the next query flushes the port mid-line,
and the fragment it reads is "     32", the value half of LCE's reply.

Lines are now framed on CR, LF or CRLF out of a receive buffer that
_discard_input() clears along with the port, so nothing survives a flush
half-read. The same replay now polls in 0.59 s.

Tests carry the transcript's real framing (padded values, trailing blank
lines) instead of the tidied "LER = 0" it was guessed to be, plus the two
regressions: a late fragment must not become the next query's value, and a
reply must be readable without waiting out the port.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 08:01:27 -05:00
Thomas Ales eff589d901 Helios: don't let an unlabelled reply pass for a register's value
_value_in()'s last resort was to accept any line it could not attribute as
the answer to whatever had just been asked.  That fallback exists for the
serial numbers, which come back bare — but it applied to every query, so a
stray "32" could be read as a diode current of 32 mA.  32 is also what a
status register reads with bit 5 set (LER "Over voltage laser diode", LCE
"Door switch open", CCE "Q-switch under/over temperature"), which is
exactly the value the panel is stuck on.

Only CSR and HSR now accept an unlabelled reply; every other read has to
see its own mnemonic in the line.  A read that cannot be attributed returns
None, and the panel shows "laser: ? mA" instead of leaving the last good
value on screen looking live — a stale reading and a setpoint that refuses
to move are indistinguishable otherwise.

tools/helios_lds_probe.py is the diagnostic for the underlying question:
it talks to the controller with no reply parsing at all and prints every
byte, so the transcript says whether LDS answers for itself, whether the
write is taken, and which flags the registers hold before and after.  The
status-register tables move to hardware/helios_registers.py so the probe
can decode them without importing the Qt app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 07:48:06 -05:00
Thomas Ales c7eb33891c Helios diode current: stop the status poll overwriting the setpoint
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>
2026-09-08 07:37:10 -05:00
Thomas Ales aeac5fe4d6 commit 2026-09-05 20:29:53 -05:00