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>
This commit is contained in:
Thomas Ales
2026-09-08 07:37:10 -05:00
parent aeac5fe4d6
commit c7eb33891c
5 changed files with 186 additions and 18 deletions
+46 -6
View File
@@ -210,6 +210,38 @@ class HeliosLaser:
logger.error(f"Failed to read the reply to '{command}': {e}")
return True
# Section 6 of the operator's manual, under Syntax:
#
# 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.
#
# (The command table repeats it: "Query the command to confirm it was
# accepted.") A setter that only writes therefore cannot report whether
# it worked, and the panel's next status poll reads back the old value —
# which looks exactly like the GUI refusing the operator's number.
SET_RETRIES = 3
SET_SETTLE_S = 0.05 # let the controller store it before reading
def _write_verified(self, mnemonic: str, value: int) -> bool:
"""Write `value` to `mnemonic`, and confirm the controller took it.
Returns False if the read-back never matches, leaving the controller
holding whatever value it kept — the caller is expected to say so
rather than let the discarded write pass for a successful one.
"""
for attempt in range(1, self.SET_RETRIES + 1):
if not self._write_command(f"{mnemonic} {value}"):
return False
time.sleep(self.SET_SETTLE_S)
readback = self._query_int(mnemonic)
if readback == value:
return True
logger.warning(
f"'{mnemonic} {value}' not accepted: controller reports "
f"{readback} (attempt {attempt}/{self.SET_RETRIES})")
return False
def _query_int(self, command: str) -> Optional[int]:
"""Query a value that should parse as an int; None if absent/unparseable."""
raw = self._query(command)
@@ -235,20 +267,28 @@ class HeliosLaser:
logger.error(f"Period {period_ns} ns out of range (8000-60000)")
return False
command = f"LDF {period_ns}"
return self._write_command(command)
return self._write_verified("LDF", period_ns)
def set_current_ma(self, current: int) -> bool:
"""Set pump diode current in mA."""
"""Set pump diode pulse current (LDS) in mA.
False means the controller did not take the value — see
_write_verified. The manual's range for LDS is 0-7000 mA; the
2000 mA ceiling here is this rig's limit, not the protocol's.
"""
if not (0 <= current <= 2000):
logger.error(f"Current {current} mA out of range (0-2000)")
return False
command = f"LDS {current}"
return self._write_command(command)
return self._write_verified("LDS", current)
def set_pulse_mode(self, mode: PulseMode) -> bool:
"""Set pulse mode."""
"""Set pulse mode.
Note from the manual's LDG entry: "LDF has to be set again after LDG
is changed, except for single pulse triggering" — so a caller that
changes the mode has to re-send the frequency.
"""
command = f"LDG {mode.value}"
return self._write_command(command)