Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,71 @@ the README's Installation section: in `0.x.y`, `x` is the breaking component
and `y` is the non-breaking one (releases are tagged; between them `main` carries
the next version with `-DEV`).

## [Unreleased]

### Fixed

- `TCubeLaser`: every fresh read of the controller (the status word, the current limit, the
potentiometer position, the W/A read-back, `initialize`'s limit read and `tcube_get_current`)
now sends its request twice before reading. The 642 nm rig's TLD001 answers one request behind
(2026-09-29), so a single request could let `light_on`'s current-limit gate pass a
potentiometer raised since the previous read, and the enable then ran above `max_current`
until the setpoint landed.
- `TCubeLaser` power mode: `check_lock` makes its own readings and status requests and refuses
in both directions. Besides a photocurrent above `lock_ratio` times the request, it refuses when
the controller reports its current limit reached (status bit `0x400`) or the photocurrent is
below the request divided by `lock_ratio`; the output is then zeroed and disabled, as for a
suspected lock. It used to read the polled cache and test only the high side, so a loop driven
to the clamp -- by a request the clamp cannot reach, or by a photodiode giving fewer counts per
mW than at calibration -- went unnoticed.
- `PhotodiodeLoop` refuses a `lock_check_s` below 0.1 s (`LOCK_CHECK_MIN_S`): 0 was accepted and
disabled the lock check. A construction passing a smaller value now throws.
- `TCubeLaser`: `measured_current` (and so `loop_status`) accepts the raw reading -32768, which
the Kinesis header defines as -220 mA, instead of throwing.
- `TCubeLaser`: any failure during the calibration-reference re-check (a mismatch, a mode refusal or a
command error) latches until the next `initialize`, and the diode is not re-lit on every retried `light_on`.
- `TCubeLaser`: a safety check that refuses while the output may be on (the stored-limit
check, and in power mode also a mode, TIA-range or clamp change, or an over-range photodiode) now zeroes and disables
the output before it throws, instead of leaving the diode lit in the fault.
- `TCubeLaser` open-loop `initialize` confirms open loop with a fresh status read after
`LD_SetOpenLoopMode`, and refuses if the controller stays in closed loop.
- `TCubeLaser` power mode: `check_lock` also runs the current-limit test (`0x400`) at a zero request.
- `TCubeLaser` power mode: `setoutputpower!` decides on fresh status and photocurrent reads, and
`light_on` and `setoutputpower!` refuse a photodiode range that no longer matches `tia_range`.
- `TCubeLaser`: the header's potentiometer floor, 17.25 mA, no longer gates anything. Open-loop
`initialize` lowers the potentiometer for any `max_current` below the controller's limit, and
power-mode construction no longer refuses a `max_current` under 17.25 mA; the limit the
controller reports decides (the 642 nm rig's unit reads 16.74 mA at the lowest position).

### Added

- A calibration reference for power mode: `ref_current_mA`, `ref_photocurrent_A` and `ref_ratio`
(default 1.5) on `PhotodiodeLoop`, `TCubeLaser` and `SimDiodeLaser` (which stores them and does
not check). With a reference, the first power-mode `light_on` after each `initialize` runs the
diode in open loop at `ref_current_mA` for `REFERENCE_DWELL_S` (0.1 s) before reading the photodiode,
and refuses unless the photocurrent is within a factor `ref_ratio` of `ref_photocurrent_A`; the
output is off after the check either way, and a mismatch refuses every later `light_on` until the
next `initialize`. Without one, that `light_on` warns once that the check is skipped. See `CALIBRATION.md`.
**Every rig that uses power mode should record one** (`ref_current_mA`, `ref_photocurrent_A`),
with its next W/A measurement; `CALIBRATION.md` step 3b says how.

### Changed

- `TCubeLaser` timings, against 0.2.5, at the default `lock_check_s` (0.2 s) and `REQUEST_WAIT_S`
(0.1 s; a fresh read now sends its request twice, 0.2 s, where it was 0.1 s):
- a power-mode `light_on` takes about 0.6 s longer (`require_clamp` two fresh reads, +0.2 s; `check_lock`
adds its photocurrent and status reads, +0.4 s);
- a power-mode `setoutputpower!` with the output on takes about 0.8 s longer for a request above zero
and about 0.6 s for a zero request (`require_clamp` +0.2 s, the fresh photocurrent read +0.2 s,
`check_lock` +0.4 s, or +0.2 s at zero); with the output off, about 0.2 s longer;
- an open-loop `light_on` takes about 0.1 s longer (one fresh limit read), and a `setcurrent!` with
the output off about 0.1 s longer (one fresh status read);
- `initialize` takes about 0.8 s longer in power mode (eight fresh reads: status 2, potentiometer 2, limit 3,
W/A 1, for one potentiometer setting; each further setting adds 0.2 s) and about 0.3 s in open loop
(one limit read, more if the potentiometer is lowered, plus 0.2 s for the open-loop confirm);
- the first power-mode `light_on` after `initialize` also runs the calibration-reference re-check:
`REFERENCE_DWELL_S` (0.1 s), three fresh reads (0.6 s) and the setpoint confirm, about 0.7 s plus the confirm.

## [0.2.5] - 2026-09-29

A non-breaking release. It brings the TCube laser's closed-loop (power) mode and
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "MicroscopeControl"
uuid = "aa70d9ae-4a1e-49fd-870a-8ccfd99f4c3e"
version = "0.2.5"
version = "0.2.6-DEV"
authors = ["klidke@unm.edu"]

[deps]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,10 @@ construction line serves a system and its simulated twin. `mode` defaults to
`ConstantCurrent()`.
In `ConstantPhotocurrent` mode `wa_calibration`, `tia_range`, `tec_stabilised`,
`properties` and `max_current` are required, and the loop keywords `ramp_step_mW`,
`ramp_step_s`, `lock_check_s` and `lock_ratio` are accepted into the
[`PhotodiodeLoop`](@ref). `[limitation]` the simulation neither ramps nor
checks for a loop lock; it only stores them. `threshold_current` defaults to
`ramp_step_s`, `lock_check_s`, `lock_ratio`, `ref_current_mA`, `ref_photocurrent_A`
and `ref_ratio` are accepted into the [`PhotodiodeLoop`](@ref). `[limitation]` the
simulation neither ramps, checks for a loop lock, nor re-checks a calibration
reference; it only stores them. `threshold_current` defaults to
65 mA and, in `ConstantCurrent` mode, `max_current` to 160 mA (the 642 nm
diode's numbers); the model fields are documented on the type.

Expand All @@ -122,6 +123,9 @@ function SimDiodeLaser(;
ramp_step_s::Union{Nothing,Real}=nothing,
lock_check_s::Union{Nothing,Real}=nothing,
lock_ratio::Union{Nothing,Real}=nothing,
ref_current_mA::Union{Nothing,Real}=nothing,
ref_photocurrent_A::Union{Nothing,Real}=nothing,
ref_ratio::Union{Nothing,Real}=nothing,
efficiency::Float64=1.2,
responsivity::Union{Nothing,Float64}=nothing,
responsivity_drift::Float64=0.0,
Expand All @@ -133,7 +137,8 @@ function SimDiodeLaser(;
)
name = "SimDiodeLaser"
pd = LightSourceInterface.diode_loop_from_keywords(mode, name; wa_calibration, tia_range,
tec_stabilised, properties, max_current, ramp_step_mW, ramp_step_s, lock_check_s, lock_ratio)
tec_stabilised, properties, max_current, ramp_step_mW, ramp_step_s, lock_check_s, lock_ratio,
ref_current_mA, ref_photocurrent_A, ref_ratio)
max_current = something(max_current, 160.0)
props = something(properties, LightSourceProperties("mA", 0.0, false, min_current, max_current))
resp = something(responsivity, pd === nothing ? 1 / 224.2 : 1 / pd.wa_calibration)
Expand Down
35 changes: 33 additions & 2 deletions src/hardware_implementations/tcube_laser/CALIBRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ your mW into a photocurrent setpoint through two numbers you must supply:
| Number | Keyword | What it is | Where it comes from |
|---|---|---|---|
| W/A factor | `wa_calibration` | optical power at the laser output per amp of monitor photocurrent | measured with a power meter (this document) |
| Calibration reference | `ref_current_mA`, `ref_photocurrent_A` | an open-loop current and the photocurrent in A read at it, re-checked at the first `light_on` after every `initialize` | measured with the W/A factor (step 3b) |
| TIA range | `tia_range` | full scale of the photodiode amplifier, in A: `10e-6`, `100e-6`, `1e-3` or `10e-3` | the rear-panel DIP switch on the TLD001; `initialize` refuses to start if the controller reports a different one |

The conversion both ways is
Expand All @@ -31,6 +32,7 @@ and the endpoints of `setlevel!` and the panel slider).
| Measurement plane | power meter **before the fibre** (coupling efficiency drifts, so the meter is never read after it) |
| Measured by | Ali Kazemi Nasaban Shotorban, recorded in `helpers.jl`, Oct 2024 |
| Threshold | ~65 mA |
| Calibration reference | none recorded yet (see step 3b) |

Verification, in closed loop with those two factors: power commanded through
the formula above versus power measured before the fibre, in mW. Single
Expand Down Expand Up @@ -63,6 +65,7 @@ laser = TCubeLaser("00000000";
tia_range = 1e-3, # A, the rear-panel DIP switch
tec_stabilised = missing, # true / false once known; `missing` is honest until then
threshold_current = 65.0, # mA
# ref_current_mA = 90.0, ref_photocurrent_A = <measured>, # step 3b; without it light_on warns
max_current = 150.0, # mA, your diode's rating: programmed into the controller as the loop's clamp
properties = LightSourceProperties("mW", 0.0, false, 1.0, 70.0)) # [1 mW, 70 mW]: the 70 is max_power, your diode's rating

Expand All @@ -79,11 +82,13 @@ rig's TLD001, serial `64849775`). Facts the driver's docstrings already state
are marked with where.

- A plain `LD_SetMaxCurrentDigPot` is ignored: adjust mode (`LD_EnableMaxCurrentAdjust`) and a pause before leaving it are needed, 2026-09-28 (driver: `set_digpot!`, `CLAMP_WAIT_S`).
- The header's potentiometer scale (`position * 220 / 255` mA) is wrong for this unit: position 204 gave 160.74 mA and 194 gave 152.43 mA, 2026-09-28 (driver: `DIGPOT_STEP_ESTIMATE_mA`).
- The header's potentiometer scale (`position * 220 / 255` mA) is wrong for this unit: position 204 gave 160.74 mA and 194 gave 152.43 mA, 2026-09-28; the manual gives about 0.7 mA per step (p.28, p.38); position 203 read 159.9 mA, and the limit readback is stable across fresh reads, 2026-09-29. The limit readback is what the driver trusts; `DIGPOT_STEP_ESTIMATE_mA` keeps the header's 220/255 mA, the larger step, only to choose the next position, so moves approach `max_current` from below.
- The potentiometer position does not survive a controller power cycle, 2026-09-29; `initialize` re-programs it every time and power-mode `light_on` re-checks it.
- The controller ignores a setpoint sent while its output is off and then runs on a stale stored setpoint (on this rig the diode went to its ~160 mA limit); the setpoint read-back is stale with the output off (65530), 2026-09-28 (driver: `send_setpoint`).
- The photodiode UNDER-range flag was set at 1 mW (4.5 uA on the 1 mA range) while the loop regulated correctly, 2026-09-29; the driver warns and does not refuse.
- The photocurrent reading is signed and `0x8000` means over range (seen at 110 mA open loop on the 1 mA range), 2026-09-28 (driver: `measured_photocurrent`).
- The controller answers one request behind: the first `LD_RequestStatusBits` after an enable returned the pre-enable bits even 0.5 s later; readings behave the same, 2026-09-29 (driver: `request_twice`).
- With no light the photocurrent reads raw 65532, i.e. -4 signed, 2026-09-29.
- A setpoint jumped up from 0 can lock the loop at ~90 mA / ~21 mW / ~98 uA whatever is requested: 3 of 3 at 10 mW, then 4 of 4 without the lock after a Kinesis CONST P session; stepped setpoints never failed (10 of 10), 2026-09-28/29 (driver: `PhotodiodeLoop`'s `ramp_step_mW` field, `check_lock`).
- One USB write takes about 15 ms, so ramp pauses below ~10 ms do not go faster, 2026-09-29.
- The controller must be power-cycled when switching between the Kinesis application and this driver, in either direction (`LD_Open` error 2, or "load device failed" in Kinesis, until then), 2026-09-29. Recorded only here.
Expand Down Expand Up @@ -161,7 +166,8 @@ plane**. Recalibrate when any of these changes:
measure the W/A factor again on the new range rather than assuming it carries over);
- the diode, its mount or the photodiode is replaced or realigned;
- the plane you want to quote power at changes;
- the verification (step 5) drifts by more than you can accept.
- the verification (step 5) drifts by more than you can accept;
- record a new calibration reference whenever the W/A factor is re-measured.

It is also worth re-running step 5 every few months: it takes minutes and tells
you whether the old factor still holds.
Expand Down Expand Up @@ -250,6 +256,31 @@ shutdown(laser)
Check the residuals: if power against photocurrent is not a straight line, the
range is wrong (step 1) or the photodiode is saturating.

### 3b. Record the calibration reference

Do this in the same session, on the same range and gain as the W/A factor. Pick one
current from the sweep at least 20 mA above threshold and at most the power-mode
`max_current`, whose indicated power is at most `max_power` (the constructor refuses
otherwise). On the 642 nm rig that is 90 mA (about 99 uA, about 22 mW at 224.2 W/A).

```julia
TCube = MicroscopeControl.HardwareImplementations.TCubeLaserControl
setcurrent!(laser, 90.0); sleep(1.0)
ref_photocurrent_A = TCube.photocurrent_from_raw(laser, TCube.read_photocurrent_word(laser), 1e-3) # decode with the tia_range the power-mode config will state
```

Then pass `ref_current_mA = 90.0, ref_photocurrent_A = ref_photocurrent_A` when you build
the power-mode laser (step 4). The first `light_on` after every `initialize` then drives the
diode in open loop at `ref_current_mA`, reads the photodiode, and refuses unless the reading
is within a factor `ref_ratio` (default 1.5) of `ref_photocurrent_A`, in either direction; the
output is off after the check. A range the reading does not follow, or a `tia_range`
relabelled with W/A kept, fails that check. Re-measure W/A and the reference whenever the
switch moves.

`[limitation]` the 642 nm rig's words of 2026-09-29 (375 / 1802 / 6172 at 70 / 80 / 110 mA)
are **not** a reference for its W/A factor. That factor dates from Oct 2024, and the gain was
re-optimised on 2026-09-28 (see above). Record the reference at the next W/A verification.

### 4. Build the laser in closed loop

Construct `TCubeLaser(...; mode = ConstantPhotocurrent(), ...)` with the three
Expand Down
Loading
Loading