Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7c6cb9e
Update TCubeLaserControl.jl documentation for closed-loop calibration…
AliKNS Sep 29, 2026
3c54051
Update DAQmx source to track the main branch
AliKNS Sep 29, 2026
258c42e
TCube laser: closed-loop (power) mode, DiodeLaser interface, hardware…
AliKNS Sep 29, 2026
db046c2
TCube closed loop: ramp upward setpoints; verified to 40 mW on the 64…
AliKNS Sep 29, 2026
40260f7
Record the Kinesis handover power-cycle and the ramp's USB-bound speed
AliKNS Sep 29, 2026
9689d22
TCube closed loop: single-write setpoints by default, ramp kept as fa…
AliKNS Sep 29, 2026
7c93add
642 nm rig ceiling: HL6366DG rating, path transmission 0.93, 70 mW / …
AliKNS Sep 29, 2026
10f1aba
CLAUDE.md: DiodeLaser, the two regulation modes, SimDiodeLaser and th…
AliKNS Sep 29, 2026
1c240d5
PI N-472: NUL-terminated connect string, checked connect, retryable o…
AliKNS Sep 29, 2026
3315703
Merge remote-tracking branch 'origin/0.3rc1' into n472-init-fixes-t16
kalidke Sep 29, 2026
7bc9436
Move PR #67 to 0.3rc1 and correct its release notes
kalidke Sep 29, 2026
7237b2f
Check every N472 initialize step and close on failure; fake-GCS2 tests
kalidke Sep 29, 2026
28c3818
Review nits: CLAUDE.md provenance, simpler setvel return
kalidke Sep 29, 2026
e17155a
Merge main (0.2.4 and the 0.3rc1 fold, #69) into laser-642-power-mode
kalidke Sep 29, 2026
e07069f
TCubeLaser: max_current has no default in closed loop; test light_on'…
kalidke Sep 29, 2026
f89e5f7
Keep 0.2.4 working: mode defaults to ConstantCurrent, setpower forwar…
kalidke Sep 29, 2026
d6344b8
Fold #66's changelog into Unreleased as a non-breaking 0.2.5 change; …
kalidke Sep 29, 2026
4a1439e
Pass max_current in the two remaining closed-loop test constructions
kalidke Sep 29, 2026
b4c16da
TCubeLaser.initialize leaves the output off and lowers an open-loop c…
kalidke Sep 29, 2026
e769cce
Closed-loop light_on and setoutputpower! re-check the controller befo…
kalidke Sep 29, 2026
2360e1c
Move the ramp to per-laser PhotodiodeLoop fields, ramp from what the …
kalidke Sep 29, 2026
c962c3c
Diode laser panel: the output toggle's label follows the driver, not …
kalidke Sep 29, 2026
eabb0f8
TCube docs: placeholder serials, cut the rig diary from CALIBRATION.m…
kalidke Sep 29, 2026
e36d3e0
Power-mode clamp re-check catches a one-step pot drift; clamp recorde…
kalidke Sep 29, 2026
891193a
SimDiodeLaser: constructed exactly as TCubeLaser
kalidke Sep 29, 2026
83a1514
Revert the boolean binding change: the header says four bytes
kalidke Sep 24, 2026
7a4cb59
Split the two Kinesis boolean roles; they have different right answers
kalidke Sep 24, 2026
36e1323
Record both Kinesis header variants; no single struct layout fits
kalidke Sep 24, 2026
8ee364d
Correct the BOOL docstring against the vendor headers, and attach it
kalidke Sep 24, 2026
4720e1b
TCubeLaser: one cleanup helper for a failure while the diode may be l…
kalidke Sep 29, 2026
6e4f535
TCubeLaser: default properties back to 0.2.4's; drop the power_unit c…
kalidke Sep 29, 2026
c0231af
Open loop: warn below the pot's floor instead of failing; lowering by…
kalidke Sep 29, 2026
29ee845
Open-loop and closed-loop sends follow what the driver knows; correct…
kalidke Sep 29, 2026
44b4fc8
Share the diode-laser keyword rules; reword the SimDiodeLaser claim; …
kalidke Sep 29, 2026
c17d729
Merge branch-model-main (8342359, #69's head): CLAUDE.md states 0033'…
kalidke Sep 29, 2026
fe5bb10
Open loop never newly fails at the pot; setcurrent! reads the output …
kalidke Sep 29, 2026
e5d534c
Merge main (6dbbecb: v0.2.4, #69's branch model) into n472-init-fixes
kalidke Sep 29, 2026
2ca73eb
CHANGELOG: initialize(::N472) throwing on a failed setup step is not …
kalidke Sep 29, 2026
ac8c3e3
Merge #70 (8ee364d): Kinesis boolean arguments as four bytes
kalidke Sep 29, 2026
6519584
Merge #67 (2ca73eb): PI N-472 connect, setup steps, retry, stopmotion
kalidke Sep 29, 2026
48cd1e0
Merge #66 (fe5bb10): TCube laser closed-loop (power) mode and the Dio…
kalidke Sep 29, 2026
8277631
Release 0.2.5: drop -DEV; one CHANGELOG section with the upgrade note
kalidke Sep 29, 2026
7977efc
TCube light_on: refuse above the stored current limit; roll back a fa…
kalidke Sep 29, 2026
a4cff38
Review nits on the C commit: the 0.2.5 note names the refusal; two do…
kalidke Sep 29, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
/docs/Manifest.toml
/docs/build/
.DS_Store
# local-only symlink to the lab instrument archive on the NAS (see CLAUDE.md)
/manuals
# Local run output: test records, logs, handoffs (lab decision 0028)
dev/output/
347 changes: 346 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

71 changes: 69 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ What CI actually runs, deliberately thin (`.github/workflows/CI.yml`):
Batch fixups into one push rather than pushing each review round separately;
every push to an open pull request starts a fresh run.

Tests use simulated devices only (`SimCamera`, `SimStage3d`/`SimStage2d`/`SimStage1d`, `SimLight`) - no hardware required. GLMakie needs a display: run under `xvfb-run -a` on a headless Linux box (CI does this). Test sets: "Simulated Camera", "Simulated Stage", "Simulated Light Source", "Export State".
Tests use simulated devices only (`SimCamera`, `SimStage3d`/`SimStage2d`/`SimStage1d`, `SimLight`, `SimDiodeLaser`) plus a fake Kinesis SDK (`test/tcube_fake_sdk.jl`) that the real `TCubeLaser` driver runs against - no hardware required. GLMakie needs a display: run under `xvfb-run -a` on a headless Linux box (CI does this). Test sets: "Simulated Camera", "Simulated Stage", "Simulated Light Source", "Export State".

## Architecture

Expand All @@ -64,7 +64,8 @@ MicroscopeControl.jl uses a **three-layer architecture** leveraging Julia's mult
│ Abstract types + contracts │ Concrete device drivers │
│ - CameraInterface │ - SimulatedCamera, DCAM4 │
│ - StageInterface │ - SimulatedStage, PI, MCL │
│ - LightSourceInterface │ - SimulatedLight, TCube │
│ - LightSourceInterface │ - SimulatedLight, TCube, │
│ (+ DiodeLaser) │ SimDiodeLaser │
│ - DAQInterface │ - NIDAQcard │
│ - SLMInterface │ - OK_XEM (FPGA) │
│ - AttenuatorInterface │ - LCC1620 │
Expand Down Expand Up @@ -101,6 +102,8 @@ Interfaces define method signatures with throwing `error("<name> not implemented

**LightSource**: `setpower`, `light_on`, `light_off`

**DiodeLaser** (`<: LightSource`; `TCubeLaser{M}`, `SimDiodeLaser{M}` with `M` = `ConstantCurrent` or `ConstantPhotocurrent`, fixed at construction, `mode=` required): `setcurrent!` (mA, `ConstantCurrent` only), `setoutputpower!` (mW at the laser output, `ConstantPhotocurrent` only), `setlevel!` (0..1 of the declared range, both), `measured_current`, `measured_photocurrent`, `indicated_output_power`, `loop_status`, `regulation_mode`, `supported_modes`. `setpower` throws on a `DiodeLaser`. Mode-shared methods are written against the bare `TCubeLaser`, mode-specific ones against `TCubeLaser{ConstantCurrent}` / `{ConstantPhotocurrent}`; never a `where M` method on a generic `test/contract.jl` checks. `subtypes` is one level deep, so the contract test and API map walk to the leaves (`device_types`). Closed-loop calibration and the 642 nm rig's measured facts: `src/hardware_implementations/tcube_laser/CALIBRATION.md`.

**DAQ**: `showdevices`, `showchannels`, `createtask`, `setvoltage`, `readvoltage`, `deletetask`

**Attenuator**: `setdrivevoltage`, `getdrivevoltage`, `settransmission`, `gettransmission`, `set_calibration!`
Expand All @@ -119,6 +122,23 @@ Hardware implementations use `ccall` for vendor SDKs:
- `mcl_stage/*.jl` - Mad City Labs NanoDrive
- Serial devices (CrystaLaser, Vortran, Triggerscope) use `LibSerialPort`

Rules at the `ccall` boundary, learned from the C-867 servo bug (v0.1.1), the
N-472 connect string that worked only by accident of `filter`, and the N-472
`stopmotion` that never worked:
- A `Ptr{Cchar}` argument (GCS2 axes lists, USB descriptions) gets a Julia
`String`, which is always NUL-terminated. Never a `Vector{UInt8}` with the
zeros filtered out, and never a `Vector{String}`; join axes with a space first.
- A GCS2 `BOOL*` argument is 32-bit (`Cuint`/`Cint`), one element per axis,
never `UInt8`.
- Set a driver's `connectionstatus` only after the connect call's return value
is checked, and clear it and the device id in `shutdown`, so a failed or
closed object can be initialized again and a stale id cannot close another
object's connection.

The test suite must never command attached hardware. Driver tests replace the
vendor wrappers with a recorder (`test/tcube_fake_sdk.jl`,
`test/pi_n472_fake_sdk.jl`), so they run on every machine and never reach a DLL.

### Camera Image Data Convention

**Convention:** Image data is stored and displayed as column-major `(H, W, N)` arrays where `data[row, col]` = `data[y, x]`.
Expand Down Expand Up @@ -186,3 +206,50 @@ pull request that raises `Z`, and merge the same fix into `main`.
hand, after checking the same `lab/tests` coverage.

What the numbers mean (decision 0033): before 1.0, in `0.Y.Z` **raising `Z` is any non-breaking change, new features included, and raising `Y` is an interface break** -- `0.2.4 -> 0.3.0` declares a break and `0.2.4 -> 0.2.5` a compatible release, which is also how Julia's `^0.2` compat bound reads them. A break is anything that lets working downstream code behave differently: a signature, an export, or what a call returns or throws. A bug fix that changes behaviour only on a path that was already broken is not a break. Config types are built by keyword (lab decision 0035): adding a field with a default is not a break, and a positional argument's meaning never changes (add a keyword and deprecate the old form instead). Hardware verification is not tracked in this repo; it is recorded by the downstream rig repo that pins to a given tag. The merge gate is the local suite (see "Testing policy" above) plus `test/contract.jl`'s "Interface Contract" testset, which guards the no-ambiguous-exports and core-method invariants described above; CI confirms it on a reduced matrix.

## Instrument documentation archive (`manuals/`)

`manuals/` is a **local-only symlink** to the lab-wide instrument archive on
the NAS — vendor manuals, SDK headers, API references and the driver-facing
notes that explain why a binding is the way it is. It is gitignored and must
**never** be committed: git stores a symlink as a mode-120000 blob, and on a
Windows rig without the symlink privilege that checks out as a text file
containing the path, which is worse than nothing. Each clone makes its own.

```bash
# Linux (any of the four hosts)
ln -sfn /mnt/nas/lidkelab/Projects/lab_instruments manuals
```
```bat
REM Windows rig, from the repo root. Needs an elevated prompt, OR Developer
REM Mode enabled once (Settings > Privacy & security > For developers).
mklink /D manuals \\192.168.1.21\lidke-lrs\Projects\lab_instruments
```

`[limitation]` The junction form, `mklink /J`, does **not** work here: junctions
resolve only to local volumes, so a UNC target fails. `/D` is required, and it
is the one step in this arrangement that needs a privilege — once per rig, not
once per clone. This has not been run on either rig yet; if `/D` is refused,
say so rather than reaching for a mapped drive letter, which differs between
user sessions and services.

Nothing else is required — no environment variable and no shell profile edit.
If `manuals/` is absent, make it with the line above.

Layout is manufacturer first, then model: `manuals/Thorlabs/TLD001/`,
`manuals/Hamamatsu/C11440-22CU/`, with shared vendor SDKs under
`manuals/<Manufacturer>/SDK/<sdk-id>/`. Start at `manuals/INDEX.md`; the rules
for adding anything are in `manuals/README.md`.

Within a model directory, `source/` is the vendor original, verbatim and never
renamed, and `docs/` is the working copy with a predictable name. A
`BINDING.md`, where one exists, is the distillate a driver author actually
needs — for example `manuals/Thorlabs/TLD001/BINDING.md` records that a Kinesis
C++ boolean must be *passed* as a 4-byte `Cuint` but *read back* as a 1-byte
`Bool`, which this package got wrong twice in opposite directions.

`[policy]` When a driver's behaviour turns on a vendor fact -- a struct layout,
an ABI width, a scaling constant, a status bit -- record it in that model's
`BINDING.md` and cite the document in `source/` it came from. Three of the four
defects in v0.2.3 were found by a rig holding hardware rather than by review,
because the vendor fact was not written down anywhere a reviewer could check.
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-DEV"
version = "0.2.5"
authors = ["klidke@unm.edu"]

[deps]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ MicroscopeControl.jl is organized to ensure scalability and easy integration of
- Simulated stage for testing

### Light Sources
- Thorlabs TCube laser diode controller
- Thorlabs TCube laser diode controller (TLD001), open loop (constant current) or closed loop on the monitor photodiode (see `src/hardware_implementations/tcube_laser/CALIBRATION.md`)
- Simulated laser diode (`SimDiodeLaser`) for testing either mode
- CrystaLaser 561nm
- Vortran 488nm laser
- Simulated light source for testing
Expand Down
79 changes: 58 additions & 21 deletions skills/mc-api-map/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,29 @@ not necessarily the one you are about to call. The case that exposed this was
sth)`, the map listed that and nothing else, and the 1-arg `export_state(laser)` every
lifecycle loop calls fell through to the throwing `AbstractInstrument` stub.
**[fixed in v0.2.3]** — `export_state(::TCubeLaser)` exists and
`which(export_state, Tuple{TCubeLaser})` lands on it. The 2-arg method is still
there as a deprecated forwarder that warns and delegates (removal scheduled for
a future 0.3.0), so the map may still list *that* signature: adding the 1-arg
method was the fix, not deleting the other one. The blind spot
is a property of the generator rather than of that driver, and an installed map
generated against an older pinned tag still shows the old signature. `light_on` is a
live example (below). When the listed signature is not the one you are calling, check
the exact tuple with `hasmethod`/`which`.
`which(export_state, Tuple{TCubeLaser})` lands on it. v0.2.3 kept the 2-arg method
as a deprecated forwarder; **[guarantee]** 0.2.5 keeps it (deprecated, removed
at the next breaking release), so `hasmethod(export_state, Tuple{TCubeLaser,Any})`
is still true and an installed map lists both signatures. The blind spot is a property of the generator
rather than of that driver, and an installed map generated against an older pinned
tag still shows the old signature. A current example: the generator's inherited
check asks about the 1-arg tuple only, so a 2-arg generic whose only method is on an
abstract intermediate (`setpower(::DiodeLaser, ::Float64)`, the deprecated forwarder in open loop and refusal in closed loop, and
`setlevel!(::DiodeLaser, ::Float64)`, the shared implementation) does not appear in a
`TCubeLaser` or `SimDiodeLaser` section at all. When the listed signature is not the
one you are calling, check the exact tuple with `hasmethod`/`which`.

**[guarantee]** Parametric devices are listed once, under the bare name
(`### TCubeLaser`), and a method written against one instantiation is printed with
it: `setcurrent!(TCubeLaser{ConstantCurrent}, Float64)`,
`setoutputpower!(TCubeLaser{ConstantPhotocurrent}, Float64)`. A method written
against the bare type (`light_on(TCubeLaser)`) serves both modes. The generator
walks the type tree to its non-abstract leaves, so drivers beneath the abstract
intermediate `DiodeLaser` are listed under `## LightSource`.
**[limitation]** `InteractiveUtils.subtypes` is one level deep: downstream code that
enumerates lights with `subtypes(LightSource)` gets `DiodeLaser` in place of
`TCubeLaser` and `SimDiodeLaser`, silently. Walk to the leaves
(`isabstracttype(S) ? recurse : keep`), as the map generator and `test/contract.jl` do.

**[limitation]** `MLSLM` and `Triggerscope4` sit outside the `AbstractInstrument` hierarchy (`SLM`
and `TRIG` are `abstract type ... end` with no supertype). A lifecycle name missing
Expand All @@ -129,20 +144,27 @@ where dispatch lands and whether that is the concrete type:
```julia
using MicroscopeControl

# true for every LightSource, because the interface stub exists
# false from 0.2.5: the 2-arg light_on stub was removed (up to v0.2.x it was true
# for every LightSource and landed on a throwing stub)
hasmethod(light_on, Tuple{TCubeLaser,Float64})
# -> true
# -> false

# where the call actually goes
which(light_on, Tuple{TCubeLaser,Float64}).sig
# -> Tuple{typeof(light_on), LightSource, Float64} (the throwing stub: no
# driver implements 2-arg light_on)
# resolves, but lands on the DiodeLaser-level method: on ConstantCurrent a deprecated
# forwarder to setcurrent!, on ConstantPhotocurrent a refusal naming setoutputpower! / setlevel!
which(setpower, Tuple{TCubeLaser{ConstantCurrent},Float64}).sig
# -> Tuple{typeof(setpower), DiodeLaser, Float64}

# mode-specific: exists only on the instantiation for that mode
hasmethod(setcurrent!, Tuple{TCubeLaser{ConstantPhotocurrent},Float64})
# -> true, but it is the throwing DiodeLaser stub; check which(...).sig
which(setcurrent!, Tuple{TCubeLaser{ConstantCurrent},Float64}).sig
# -> Tuple{typeof(setcurrent!), TCubeLaser{ConstantCurrent}, Float64}

which(export_state, Tuple{TCubeLaser}).sig
# -> Tuple{typeof(export_state), TCubeLaser} (real driver code from v0.2.3;
# up to v0.2.2, the throwing stub.
# Tuple{TCubeLaser,Any} still resolves,
# to the deprecated forwarder)
# Tuple{TCubeLaser,Any} is the
# deprecated forwarder, kept in 0.2.5)

which(getdata, Tuple{SimCamera}).sig
# -> Tuple{typeof(getdata), SimCamera} (real driver code)
Expand All @@ -156,13 +178,22 @@ in the upstream repo uses:

```julia
has_specific(f, T, args...) =
hasmethod(f, Tuple{T, args...}) && which(f, Tuple{T, args...}).sig.parameters[2] === T
hasmethod(f, Tuple{T, args...}) &&
Base.unwrap_unionall(which(f, Tuple{T, args...}).sig).parameters[2] === T

has_specific(initialize, SimCamera) # true
has_specific(initialize, ThorCamCSCCamera) # false
has_specific(setpower, SimLight, Float64) # true
has_specific(setpower, TCubeLaser, Float64) # false: lands on the DiodeLaser method
has_specific(light_on, TCubeLaser) # true: mode-shared, written on the bare type
has_specific(setcurrent!, TCubeLaser{ConstantCurrent}, Float64) # true: mode-specific
```

The `unwrap_unionall` matters from 0.2.5: a `where`-method has a `UnionAll`
signature whose `.parameters` throws. Pass the bare `TCubeLaser` for a mode-shared
method and the instantiation for a mode-specific one; `has_specific(setcurrent!,
TCubeLaser, Float64)` is false because no method is written on the bare type.

For `gui` the right question is "does dispatch avoid the `AbstractInstrument`
stub", since the interface-level `gui` is the intended shared implementation:
`which(gui, Tuple{T}).sig.parameters[2] !== AbstractInstrument`.
Expand All @@ -178,10 +209,16 @@ per device.

## Arity traps the map makes visible

- **[limitation]** `light_on` is declared `light_on(::LightSource, ipower::Float64)` at the interface
and implemented as `light_on(::T)` by all five drivers. The 2-arg call throws for
every light source. The map lists `light_on(TCubeLaser)` etc. as device-specific;
a 2-arg form never appears.
- **[guarantee]** `light_on` takes the light only. Up to v0.2.4 the interface
declared `light_on(::LightSource, ipower::Float64)`, which no driver implemented
and which threw for every light; 0.2.5 replaced it with a 1-arg stub, so the 2-arg
call is now a plain `MethodError`. Set the level first, then `light_on`.
- **[guarantee]** `setpower` on a `DiodeLaser` (`TCubeLaser`, `SimDiodeLaser`)
resolves to the `DiodeLaser` method: on a `ConstantCurrent` laser it forwards to
`setcurrent!` (mA) with a deprecation warning; on a `ConstantPhotocurrent` laser it
throws and names `setoutputpower!` (mW at the laser output) and `setlevel!`
(unit-free `0..1`). It is unchanged on
the other lights. None of this shows in the map (the 2-arg blind spot above).
- `move` takes `Float64` positions. `move(stage, 1, 2, 3)` with integers is a
`MethodError`, not a stub error.
- The SmarAct `MCS2Stage` has `move(MCS2Stage, Float64, Float64[, Float64])` in
Expand Down
43 changes: 43 additions & 0 deletions skills/mc-api-map/references/gui-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,47 @@ panel is observably read-only. The fix is scoped to the shared light panel;
it is not a guarantee about the others, and `gui(::DAQ)` still calls
`showdevices`/`showchannels` at construction.

From 0.2.5 this panel serves only the lights that are not a `DiodeLaser`
(`CrystaLaser`, `VortranLaser`, `DaqTrLight`, `SimLight`); a `DiodeLaser` has its
own, below.

## `gui(::DiodeLaser)` (0.2.5)

Dispatches on `regulation_mode(laser)` to `current_panel` (`ConstantCurrent`:
slider in mA over `min_current .. effective_max_current(laser)`, calling
`setcurrent!`) or `power_panel` (`ConstantPhotocurrent`: slider in mW **at the
laser output** over `properties.min_power .. max_power`, calling
`setoutputpower!`, with a basis line naming `wa_calibration`, the TIA range and
the TEC state; the range is printed as `[lo mW - hi mW]`). Enumerated from
`lightsource_interface/diode_laser_gui.jl`; `TCubeLaser` and `SimDiodeLaser`
carry every field.

| Field | Read by | Notes |
|---|---|---|
| `unique_id::String` | header label and window title | both panels |
| `properties.is_on` | initial state of the on/off toggle | both panels; the toggle, not the slider's bottom, is "off" |
| `properties.min_power`, `max_power` | slider range and textbox bounds | `power_panel` only; mW at the laser output |
| `min_current` | slider floor | `current_panel` only |
| `effective_max_current(laser)` (a method) | slider ceiling | `current_panel` only; falls back to `max_current`, `TCubeLaser` takes the smallest of `max_current`, `controller_max_current`, `max_setcurrent` |
| `threshold_current` | header text (`"unknown"` when `NaN`) | `current_panel`; also `loop_status`'s `below_threshold` |
| `drive_current` | slider start and "commanded" line | `current_panel` only |
| `pd.wa_calibration`, `pd.tia_range`, `pd.tec_stabilised` | basis line; `wa_calibration` also for the readout's indicated power | `power_panel` only |
| `pd.output_power_requested`, `pd.photocurrent_requested` | slider start and "commanded" line | `power_panel` only |

`properties.power` is **not** read by either panel.

Methods called, each only on a user action: `setcurrent!`/`setoutputpower!` (slider
or textbox), `light_on`/`light_off` (toggle), `loop_status` (the Read button, or
the Poll toggle at 2 Hz, which stops when turned off or the window closes).

`[guarantee]` Opening either panel issues **no command and no read**: the readout
shows "not read yet" until Read is pressed or Poll (off at construction) is
turned on. On a `SimDiodeLaser` this is checkable: `laser.log` gains nothing when
the panel opens. `[guarantee]` The textbox accepts only a number within the
slider's range, the same bounds the driver enforces; anything else is not
applied and the border turns red. An entry moves the slider, so it issues exactly
one command. The "commanded" line is refreshed from the device's fields only after
the command returns, and a refusal is shown in the panel as `refused: <message>`,
not thrown out of the callback.

`gui(::TRIG)` exists for `Triggerscope4`; `MLSLM` has no `gui` at all.
Loading
Loading