Repository navigation
TCube laser: closed-loop (power) mode and the DiodeLaser interface, verified on the 642 nm rig - #66
Merged
Merged
Conversation
…-checked on the 642 nm rig (0.3.0)
Implements dev/output/plan-laser-modes.md (rev 3): DiodeLaser under
LightSource; ConstantCurrent / ConstantPhotocurrent mode types;
TCubeLaser{M} with a PhotodiodeLoop; setcurrent!, setoutputpower!,
setlevel!, measured_current, measured_photocurrent,
indicated_output_power and loop_status; mode-dispatched current and
power panels (range shown, out-of-range textbox turns red, no command or
read on open); SimDiodeLaser twin; contract test and API map walk to the
device leaves. setpower throws for DiodeLaser; mode is a required
keyword (plan section 8.1 contingency). Brings in the Kinesis boolean
split from fix/revert-cppbool.
Hardware-checked on TLD001 64849775 with a power meter before the fibre:
open loop at 70/90/110 mA is correct, and closed loop regulates from 1 to
5 mW (measured = requested - 0.46 mW, so the 224.2 W/A calibration holds).
Above ~5 mW closed loop held ~21 mW, because the controller's photodiode
reading clips at ~3213 counts; that needs the PD range / TIA gain set-up
redone (CALIBRATION.md). Fixed from the rig: setpoints are ignored with the
output off (now sent right after enabling, zeroed before disabling);
polling starts first; signed photocurrent with 0x8000 = over range; pot
needs adjust mode and the clamp is the controller's reported limit.
Adds CALIBRATION.md with the calibration procedure, today's measurements
and the manual's PD range / gain procedure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2 nm rig A setpoint jumped from 0 locks the TLD001's loop at ~21 mW / 90 mA whatever was requested (3 of 3 at 10 mW), while the same target reached in steps regulates exactly, as the Kinesis application does (9.30 mW). light_on and setoutputpower! now ramp upward closed-loop steps, RAMP_STEP_mW = 3 mW every RAMP_STEP_S = 10 ms (40 mW in ~0.2 s; the USB write is the floor). Measured with a power meter before the fibre: 10 / 20 / 40 mW requested gave 9.30 / 19.04 / 38.74 mW. The mechanism is not known; the ramp is empirical. The photodiode UNDER-range flag warns instead of refusing: it was set at 1 mW while the loop regulated correctly. CALIBRATION.md records the 09-29 session, the PD range / gain check (1 mA in range, 386 uA at the limit) and corrects the previous day's diagnosis: the 98 uA "clip" was this lock, not the photodiode channel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…llback; verified to 70 mW Jumps from 0 to 10, 40 and 70 mW regulated correctly later the same night (9.30/9.31/9.30/9.31 mW at 10 mW, 68.5 mW at 70 mW), so the default is one write, the fastest. The ramp that worked around the earlier lock (3 of 3 failures at 10 mW, cause unknown) stays in the driver as an opt-in fallback, RAMP_STEP_mW[] = 3.0 / RAMP_STEP_S[] = 0.01, verified 10 of 10 at 10-70 mW. Closed loop measured over 1-70 mW: 0.56, 1.55, 2.54, 4.52, 9.30, 19.04, 38.74, 68.59 mW for 1, 2, 3, 5, 10, 20, 40, 70 mW requested (224.2 W/A). Tests cover both the default and the ramp. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…150 mA Measured at the laser head (before the filter): 22.23 mW at 90 mA vs 20.67 at the usual position (T = 0.93), 73.5 mW at 70 mW requested, 75.9 at 72. The diode is an Ushio HL6366DG (80 mW rated, 90 mW absolute maximum), so 70 mW at the usual position is ~94 % of rating; max_current 150 mA leaves the loop headroom over the ~142 mA it needs. Also records today's 13 s at the limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e fake SDK Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Member
|
Ali, three things from Keith's review of MicroscopeControl.
|
This was referenced Sep 29, 2026
Merged
Resolved so that 0.2.4's released TCube safety behaviour holds on this branch's API: - light_on keeps this branch's structure (require_clamp, intended_code, send_setpoint_ramped) with 0.2.4's failure handling: if the setpoint fails after the enable, the disable's status is checked, is_on becomes false only if it succeeded and true otherwise, and both failures are logged (review blocker 1). Open loop checks drive_current against the ceiling before the enable, and light_on warns when it sends 0 because nothing was requested. - zero_then_disable (light_off, shutdown) is 0.2.4's: one write of 0 with no status read and no read-back wait, logged if it fails, then the disable (review should-fix 4). Three assertions of this branch's call sequences change accordingly, and "a zero that cannot be confirmed" becomes "a zero that fails". - The fake SDK keeps this branch's controller model; 0.2.4's names (stored, output_on, enable_log, reset!(stored=)) are views of it, so test/tcube_output_order.jl runs unchanged. - Project.toml takes main's 0.2.5-DEV; README and skill pins stay at v0.2.4; rig-causes takes 0.2.4's row for the ignored-setpoint hazard. tcube_output_order.jl errors at this commit: it builds TCubeLaser without `mode` and calls setpower, which the next commits restore (decision 0035). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s failed-disable path Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ds in open loop, old export_state keys and properties.power kept (decision 0035) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…sweep 0.3.0 references Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lamp above max_current Both modes zero and disable the output before the mode command. Open loop lowers the max-current pot only when the controller's limit exceeds max_current, never raises it. The closed-loop enter_mode! drops its own disable and clears pd.max_current_clamp first. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…re emitting require_clamp reads the status word and the controller's limit afresh and refuses if the loop bit is gone or the limit is above the programmed clamp. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…driver knows, and detect a loop lock Deletes the RAMP_STEP_mW and RAMP_STEP_S globals (new in #66, never released) in favour of ramp_step_mW, ramp_step_s, lock_check_s and lock_ratio keywords. light_on ramps from 0 and setoutputpower! from the previous request's code, not from the stale LD_GetLaserSetPoint. check_lock refuses a measured photocurrent above lock_ratio x the request and the output is disabled. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the click Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d, drop SETPOINT_NEEDS_OUTPUT Docstring examples use the placeholder serial and the documented diode rating. CALIBRATION.md keeps the current calibration and gains a short Controller facts section; the session diary and the ceiling section are cut (to dev/output/t16-pr66-calibration-rig-history.md). The stale bench-table comment in TCubeLaserControl.jl is deleted. The SETPOINT_NEEDS_OUTPUT constant's text now lives on send_setpoint. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d only after full success require_clamp refused only a limit more than 1 mA above the recorded clamp, which is more than one pot step (0.83 mA): at a 160 mA ceiling a pot one step up (160.74 mA) passed. It now refuses a limit above max_current, or more than half a step above the clamp. enter_mode! records the clamp only after the whole closed-loop sequence succeeded, so a failure after programming the pot (at LD_SetClosedLoopMode, say) leaves it NaN. setoutputpower!'s docstring said an under-range flag with the output on refuses; the code warns, as the rig showed the loop still regulates there. Lane 2 report items 1, 2 and 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mode defaults to ConstantCurrent(), and closed loop requires max_current and accepts the ramp and lock keywords (stored, not simulated), so one construction line serves a rig and its simulated twin (captain's ruling). Tests: the power-mode re-check at a 160 mA ceiling (one pot step) and a re-initialize failing at LD_SetClosedLoopMode; SimDiodeLaser() is now a ConstantCurrent twin. Suite: 1682 pass, 3 broken, 0 fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it; initialize records a failed disable light_on and setoutputpower! zero the setpoint before the disable on any failure after the enable, setoutputpower! covers a send failure as well as a lock, and a failed initial disable in initialize sets is_on true. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…hangelog entry Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… construction, never raising Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the limit-drift wording Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…document light_on's re-ramp from 0 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s version numbers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fresh when recorded off Final-check nits N1 and N2 on c17d729, under the captain's ruling that no configuration that initialized in 0.2.4 may newly fail. N1: lower_open_loop_clamp! catches a failed search (adjust mode refused, a position that does not read back, or even the lowest position above max_current), re-reads the controller's limit, warns that max_current is enforced in software only, and lets initialize go on. The search has only ever lowered the potentiometer (raise = false), so nothing is less safe than before it ran; a failed re-read keeps the earlier reading, an upper bound for the same reason. This also absorbs the residual of a max_current at or above 17.25 mA but under a controller's real position-20 floor. Closed loop still refuses, since there the clamp is the only protection. N2: setcurrent! decides "on" from is_on, else from a fresh status read. The polled word can still report the output on for about one poll after a light_off, and a send then waited out its 1 s confirm and threw where 0.2.4 did not. Tests: the fake gains stale_bits, a polled status word that lags until an LD_RequestStatusBits. "open loop never newly fails" now proves both N1 paths (a pot walked to its floor, and adjust mode refused): initialize succeeds, warns, and the software ceiling holds. The stale-bit test asserts outcomes both ways: a stale "off" with the output on sends and confirms, and a stale "on" after light_off sends nothing and light_on then applies the request. One assertion changed: setcurrent! with the output recorded off now makes a fresh read (LD_RequestStatusBits, then LD_GetStatusBits). Suite: 1722 pass, 3 broken, 0 fail. tcube_output_order.jl (0.2.4's cases, unedited): 39/39. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds a closed-loop ("power") mode for the TCube laser, the
DiodeLaserinterface it needs, and the hardware facts learned bringing it up on the 642 nm rig. Original work by @AliKNS; review changes are on top.It targets
mainas a non-breaking 0.2.5 change (decisions 0033 and 0035). The version stays main's-DEV. Every 0.2.4 line keeps working and means what it meant, includingTCubeLaser("serial"; max_current = ..., properties = ...)andsetpower(laser, mA). Closed loop is opt-in by keyword.test/tcube_output_order.jl, 0.2.4's own TCube cases, passes unchanged on an open-loop laser built withoutmodeand driven bysetpower.DiodeLaser <: LightSource, theConstantCurrentandConstantPhotocurrentmode types, andTCubeLaser{M}with aPhotodiodeLoop. Themodekeyword defaults toConstantCurrent(), which is 0.2.4's behaviour.mode = ConstantPhotocurrent(). It requireswa_calibration,tia_range,tec_stabilised,propertiesandmax_current. The last is the clampinitializeprograms, and it is the only real protection in that mode.setcurrent!,setoutputpower!,setlevel!,measured_current,measured_photocurrent,indicated_output_powerandloop_status.setpoweron aConstantCurrentlaser forwards tosetcurrent!. On aConstantPhotocurrentlaser it throws, namingsetoutputpower!, so an mA call can never become an mW one.properties.power.export_state.export_stateadds the new keys and keeps 0.2.4's six.TCubeLaser's defaultpropertiesstay 0.2.4's("mW", 0, 100). In open loop they are labels only.properties.is_on.SimDiodeLasertakes the same keywords asTCubeLaser, minus the serial. The contract test and the API map walk the type hierarchy to its leaves.ramp_step_mWdefaults toInf, a single write;ramp_step_ssets the step interval.lock_check_s = 0.2andlock_ratio = 1.5: a photocurrent above 1.5x the request after 0.2 s zeroes and disables the output, then throws.CALIBRATION.mdkeeps how to calibrate and a new "Controller facts (TLD001)" section. The rig diary and ceiling sections are cut here, for the rig's own repository to keep.Safety changes from review (all on the fake SDK, none yet on hardware)
light_on, and a failed send, confirm or lock check insetoutputpower!. If a disable fails,properties.is_onstaystrueand the log says the output may still be on.initializezeroes and disables the output before the mode command, in both modes.light_onandsetoutputpower!re-read the status word and the current limit before emitting. They refuse if the loop bit is gone, or if the limit is abovemax_currentor more than 0.5 mA above the programmed clamp.initializesucceeds.What an open-loop 0.2.4 caller now sees (CHANGELOG, Changed)
None of these is yet run on hardware; each needs the 642 nm rig check.
initializestarts background polling (LD_StartPolling).shutdownand a failedinitializestop it.initializezeroes the setpoint and disables the output before the mode command, soproperties.is_onis false afterwards.initializemay lower the max-current potentiometer when the controller's limit is abovemax_current. By construction it never raises it.initializecarries on, as in 0.2.4:max_currentbelow the potentiometer's floor (about 17.25 mA);light_onandsetcurrent!wait up to 1 s for the controller's setpoint read-back and throw if it does not confirm.light_onthen zeroes and disables the output.setcurrent!with the output off sends nothing, andlight_onapplies it after the enable. When the driver recorded the output off, a fresh status read decides.gui(laser)on an open-loopTCubeLaseropens the diode-laser current panel (mA).Hardware verification (642 nm rig, TLD001 64849775, power meter before the fibre; on @AliKNS's 10f1aba)
0x8000meaning over range.Rig checks owed on this head, beyond the open-loop list above:
light_onandsetoutputpower!;light_off.Test plan
test/tcube_output_order.jlpasses 39/39, unedited.🤖 Generated with Claude Code