Compare commits

...
10 Commits
Author SHA1 Message Date
James Bateman (hp-laptop)andClaude Opus 4.8 c2a004d6f1 Rename project to butterfly-edfa-rack; broaden descriptions beyond Koheron
The repo bridges both instruments (Koheron CTL200 seed + Agiltron M511
EDFA), so 'koheron-esp32-bridge' was too narrow. Align the repo name and
descriptions to the identifier the whole stack already uses
(butterfly_edfa_rack): README title, Makefile/platformio headers, and the
homeassistant/ copies. Device id, MQTT topics and entity ids are
unchanged, so no reflash or lost HA history. Sibling HA repo renamed to
butterfly-edfa-rack-homeassistant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 16:33:51 +01:00
James Bateman (hp-laptop)andClaude Opus 4.8 1a54819b3b homeassistant: sync big output readout + square shutdown
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 16:14:14 +01:00
James Bateman (hp-laptop)andClaude Opus 4.8 0be75bf029 homeassistant: sync single top SHUTDOWN button
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 16:04:42 +01:00
James Bateman (hp-laptop)andClaude Opus 4.8 91b0f96029 homeassistant: sync single-column Control layout
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 16:02:39 +01:00
James Bateman (hp-laptop)andClaude Opus 4.8 58c061bd9c homeassistant: sync override switch entity-id fix
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 15:44:13 +01:00
James Bateman (hp-laptop)andClaude Opus 4.8 f97d02a533 homeassistant: sync dashboard copy with the control-panel redesign + override switch
Keeps the firmware repo's homeassistant/ copy in step with
butterfly-rack-homeassistant (control-panel layout, read-only status
strip, Pump Limit Override switch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 14:22:32 +01:00
James Bateman (hp-laptop)andClaude Opus 4.8 ba1cd1ddcf Make the pump-1 1 W clamp overridable up to the 8000 mA protocol max
Adds a Pump Limit Override: the pump-1 current command is clamped to a
soft 4000 mA ceiling (the 1 W diode rating) by default, liftable to the
8000 mA hard protocol max only while the new amp_pump_override switch is
ON. Fail-safe: the override defaults OFF on every boot, and switching it
OFF immediately re-clamps a running setpoint back down to 4000 mA so the
booster cannot stay parked above 1 W. The HA pump-1 number now spans
0..8000 mA; values above 4000 take effect only with the override on.

PUMP_CURRENT_MAX_MA is split into PUMP_CURRENT_SOFT_MAX_MA (4000) and
PUMP_CURRENT_HARD_MAX_MA (8000). README/BOM updated. Builds clean
(flash 59.3%); not yet flashed (ESP32 needs USB, no OTA).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 14:21:56 +01:00
James Bateman (hp-laptop)andClaude Opus 4.8 ffadf4b37b Add bill of materials; link it from the README
Full parts list to rebuild the MOPA: seed (DFB1550LP + CTL200 + its
5.9 V/3 A supply), Agiltron M511 EYDFA booster + v1.10 adaptor board,
ESP32 bridge, fibre/attenuator/output-connector optics, and the 3V3
signal wiring table. Electrical specs not firmly in the lab notes are
flagged to confirm from the datasheet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-02 13:34:16 +01:00
James Bateman (hp-laptop)andClaude Opus 4.8 da466efdb3 Add Home Assistant dashboard and config for the rack
Minimal HA content to reproduce the Butterfly Rack GUI: a YAML-mode
dashboard (power graphs in mW, temperature graph, dual dBm/mW output
readout, staged seed/booster controls, emergency-off), MQTT template
sensors deriving mW from the dBm telemetry, and an emergency-shutdown
script. Snippets merge into configuration.yaml / scripts.yaml; the
dashboard file is copied whole. See homeassistant/README.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-11 16:55:04 +01:00
James Bateman (hp-laptop)andClaude Opus 4.8 b31065acd7 Add settable seed TEC setpoint and °C readout
Expose the Koheron CTL200 thermistor setpoint (rtset) as a settable HA
number in ohms, clamped to SEED_RT_SETPOINT_MIN/MAX_OHM. The former
read-only setpoint sensor is replaced by this control. Add a read-only
seed temperature in °C, computed in firmware from the actual thermistor
resistance via an NTC β-model (constants in config.h; update once the
DFB1550LP thermistor R25/β are confirmed).

Verified end-to-end over MQTT: set/clamp/restore of rtset and the °C
conversion (10 kΩ → 25.00 °C).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-11 16:41:53 +01:00
10 changed files with 605 additions and 83 deletions
+60
View File
@@ -0,0 +1,60 @@
# Bill of materials: Butterfly EDFA Rack (MOPA)
Everything needed to rebuild the 1550 nm MOPA that this firmware drives: a Koheron-driven DFB seed into an Agiltron M511 EYDFA booster, bridged to WiFi/MQTT by an ESP32. Quantities are per rack. Where an electrical spec is not firmly established in the lab notes it is marked "confirm from datasheet" and the datasheet is named; do not treat those as verified.
Datasheets and manuals referenced below live in the Obsidian vault under `Lab/Equipment/Lasers/` (`Butterfly_EDFA_Rack/` for the CTL200 guide, `Agiltron EDFA/` for the M511 manuals and `edfa.py`).
## Control / bridge electronics
| Item | Qty | Notes |
|------|-----|-------|
| ESP32-D0WD-V3 dev board (WROOM-32 class) with CP2102N USB-UART | 1 | The bridge MCU. This unit's MAC is `b0:cb:d8:8f:ba:10`; enumerates as `/dev/ttyUSB0`. Any WROOM-32 class board works; keep UART1 off the flash-clashing default pins (see wiring). |
| 5 V USB supply + data cable for the ESP32 | 1 | Powers the board and is the flash/console link (UART0). |
## Seed stage (master oscillator)
| Item | Qty | Notes |
|------|-----|-------|
| Thorlabs DFB1550LP | 1 | 1550 nm DFB laser diode in a 14-pin butterfly package with integrated TEC, thermistor and monitor photodiode. The seed. |
| Koheron CTL200 (v5+) digital laser-diode controller | 1 | Butterfly-mount current + TEC controller. ASCII serial at 115200 8N1. Firmware v0.21 on this unit. Default thermistor setpoint 10 kΩ (`rtset`). |
| Koheron 5.9 V / 3 A barrel power supply | 1 | Shipped with the CTL200. The CTL200 accepts 5 V or 6 V at the barrel; set SW3 ON for a 5 V supply (compliance voltages then drop by 1 V). The terminal-block `VCC` pin is a 3V3 output, not an input, so it must not be used to power the board. |
## Power-amplifier stage (booster)
| Item | Qty | Notes |
|------|-----|-------|
| Agiltron M511 High-Power EYDFA module, PN `EDFA-1C51113331111` | 1 | Two-stage Er/Yb-doped fibre amplifier, 1550 nm, factory APC setpoint 30 dBm (~1 W). This unit: SN `Y4022708`, UART device ID `0x0000006F`. Purchased via Laser Components, 2024-08-22. Pump-1 is the active booster stage; pump-2 is unused on this unit. Manual: `EDFA_511_23-40dBm Componenet_User Manual_e.pdf`, protocol: `EYDFA_M511 High-Power_UART protocol_V1_e.pdf`. |
| Agiltron M511 power adaptor board v1.10 (OEM connection board) | 1 | Breakout: XT30 power input + the P2 control header (3V3 UART). P2 pin order from the end opposite the XT30: N(3.3V), G, RX, SCK, MO, MI, TX, NS. Photo: `Agiltron EDFA/EDFA_connection_board.jpg`. |
| DC supply for the M511 via XT30 | 1 | The pump diodes draw several amps at full power; confirm voltage and current rating from the M511 manual before sourcing. Size it for the 1 W (≈4000 mA pump-1) operating point with headroom. |
## Optical / fibre
| Item | Qty | Notes |
|------|-----|-------|
| 1550 nm SMF patch cords (SMF-28), FC/APC | 3+ | Seed out → attenuator → EDFA in, and EDFA out → load. |
| Inline fixed optical attenuator (≈3 dB pad) | 1 | Keeps the EDFA input within the ≤5 dBm benchtop input spec during high-power runs. |
| High-power-rated output connector or fusion splice | 1 | **Required above 1 W** per the Agiltron manual; a plain connector can burn at the output. |
| Beam dump or calibrated power meter | 1 | To terminate the Class-4, >1 W, invisible 1550 nm output. Never leave it open. |
## Interconnect / wiring
| Item | Qty | Notes |
|------|-----|-------|
| 3V3 jumper wires (Dupont / ferruled) | — | ESP32 ↔ CTL200 terminal block, ESP32 ↔ M511 P2 header. |
| Common ground link | 1 | Tie ESP32, CTL200 and M511 grounds together. |
### Signal wiring
Both instrument links are 3V3 logic at 115200 8N1 (the CTL200 is 5V-tolerant). UART1 is remapped to GPIO25/26 because its default pins clash with the WROOM SPI flash.
| ESP32 pin | Goes to | Link |
|-----------|---------|------|
| GPIO17 (TX2) → | CTL200 RX (terminal block) | Koheron, ASCII, `\r\n`, `>>` prompt |
| GPIO16 (RX2) ← | CTL200 TX | |
| GPIO26 (TX1) → | M511 P2 pin 3 (RX) | Agiltron, binary 0x55AA packets |
| GPIO25 (RX1) ← | M511 P2 pin 7 (TX) | |
| GND | CTL200 GND and M511 P2 pin 2 (G) | common ground |
## Safety
Class 4, >1 W, invisible 1550 nm. The firmware current clamps (`SEED_CURRENT_MAX_MA`, and the soft/hard pump-1 ceilings `PUMP_CURRENT_SOFT_MAX_MA` / `PUMP_CURRENT_HARD_MAX_MA`, the latter reachable only with the Pump Limit Override switch on) are guardrails, not interlocks. Always drop the pump to 0 before unmating any fibre, ramp the pump gradually (it overshoots ~2× on a step up and settles in 2–4 s), and terminate the output in a power meter or beam dump. See the repo README for the measured pump-current vs output-power curve.
+1 -1
View File
@@ -1,4 +1,4 @@
# Koheron CTL200 -> WiFi/MQTT bridge on ESP32. # Butterfly EDFA Rack bridge (Koheron CTL200 seed + Agiltron M511 EDFA) on ESP32.
# #
# All commands run PlatformIO from the project-local venv, so you never need # All commands run PlatformIO from the project-local venv, so you never need
# `pio` on your PATH. Just `make`, `make upload`, `make monitor`. # `pio` on your PATH. Just `make`, `make upload`, `make monitor`.
+28 -73
View File
@@ -1,13 +1,8 @@
# koheron-esp32-bridge # butterfly-edfa-rack-firmware
ESP32 firmware that bridges the **Butterfly EDFA Rack** — a 1550 nm MOPA: ESP32 firmware that bridges the **Butterfly EDFA Rack** — a 1550 nm MOPA: a [Koheron CTL200](https://www.koheron.com/photonics/ctl200-digital-laser-controller/) driving a DFB seed into an Agiltron M511 EYDFA power amplifier (output up to ~1 W / 30 dBm) — to WiFi/MQTT for monitoring and control from Home Assistant (broker `192.168.0.102:1883`, anonymous).
a [Koheron CTL200](https://www.koheron.com/photonics/ctl200-digital-laser-controller/)
driving a DFB seed into an Agiltron M511 EYDFA power amplifier (output up to
~1 W / 30 dBm) — to WiFi/MQTT for monitoring and control from Home Assistant
(broker `192.168.0.102:1883`, anonymous).
The whole rack is presented as a **single HA device** (`butterfly_edfa_rack`). The whole rack is presented as a **single HA device** (`butterfly_edfa_rack`). Topics, mirroring the lab's EDFA→MQTT convention:
Topics, mirroring the lab's EDFA→MQTT convention:
``` ```
lab/laser/butterfly_edfa_rack/availability online | offline (LWT) lab/laser/butterfly_edfa_rack/availability online | offline (LWT)
@@ -16,8 +11,7 @@ lab/laser/butterfly_edfa_rack/seed/<key> Koheron CTL200 (increment 3
lab/laser/butterfly_edfa_rack/amp/<key> Agiltron M511 (increment 4) lab/laser/butterfly_edfa_rack/amp/<key> Agiltron M511 (increment 4)
lab/laser/butterfly_edfa_rack/set/<...> commands (increment 5) lab/laser/butterfly_edfa_rack/set/<...> commands (increment 5)
``` ```
HA discovery configs are published under `homeassistant/...`, all sharing one HA discovery configs are published under `homeassistant/...`, all sharing one `device` block so entities group under the single rack device.
`device` block so entities group under the single rack device.
## Optical architecture ## Optical architecture
@@ -35,24 +29,15 @@ The rack is a **MOPA** (master-oscillator power-amplifier) at 1550 nm:
**The M511 has two internal pump diodes**, and the firmware reads both: **The M511 has two internal pump diodes**, and the firmware reads both:
- **Pre-amp pump** — runs semi-independently at ~170 mA, lifts the seed to - **Pre-amp pump** — runs semi-independently at ~170 mA, lifts the seed to ~16 dBm (`amp/preamp_current_ma`, `amp/preamp_power_dbm`).
~16 dBm (`amp/preamp_current_ma`, `amp/preamp_power_dbm`). - **Booster pump = "pump-1"** — the controllable output stage (`amp/pump1_current_ma`, set via `set/amp/pump1_current_ma`).
- **Booster pump = "pump-1"** — the controllable output stage - **"Pump-2"** is a second booster channel that this unit leaves at 0 mA regardless of settings (per `Lab/Equipment/Lasers/Agiltron EDFA`); we don't drive it. It is read but not exposed as a control.
(`amp/pump1_current_ma`, set via `set/amp/pump1_current_ma`).
- **"Pump-2"** is a second booster channel that this unit leaves at 0 mA
regardless of settings (per `Lab/Equipment/Lasers/Agiltron EDFA`); we don't
drive it. It is read but not exposed as a control.
Run the booster in **ACC (constant-current)** mode — APC stepping quantises the Run the booster in **ACC (constant-current)** mode — APC stepping quantises the output. Below transparency the booster erbium fibre *absorbs* the pre-amp output, so output stays at the PD floor (−60 dBm) until the pump is high enough.
output. Below transparency the booster erbium fibre *absorbs* the pre-amp
output, so output stays at the PD floor (−60 dBm) until the pump is high
enough.
### Pump-1 vs output characterisation (2026-06-17) ### Pump-1 vs output characterisation (2026-06-17)
Two sweeps. The transparency sweep used ~6 mW (7.8 dBm) seed into the EDFA; the Two sweeps. The transparency sweep used ~6 mW (7.8 dBm) seed into the EDFA; the high-power sweep added a 3 dB pad (input 3.2 dBm, within the ≤5 dBm benchtop spec) and a direct output-to-power-meter connection.
high-power sweep added a 3 dB pad (input 3.2 dBm, within the ≤5 dBm benchtop
spec) and a direct output-to-power-meter connection.
| Pump-1 | Output-1 (internal PD) | note | | Pump-1 | Output-1 (internal PD) | note |
|-------:|------------------------|------| |-------:|------------------------|------|
@@ -66,32 +51,17 @@ spec) and a direct output-to-power-meter connection.
| 4000 mA | 30.1 dBm / 1026 mW | **1 W rated ceiling** | | 4000 mA | 30.1 dBm / 1026 mW | **1 W rated ceiling** |
| 4500 mA | 30.85 dBm / 1216 mW | over-rating; curve still ~linear (not saturated) | | 4500 mA | 30.85 dBm / 1216 mW | over-rating; curve still ~linear (not saturated) |
The unit (factory APC setpoint 30 dBm) comfortably exceeds 1 W: 1.22 W at The unit (factory APC setpoint 30 dBm) comfortably exceeds 1 W: 1.22 W at 4500 mA with the output still climbing ~0.74 dB/500 mA and module temp only ~30 °C — so its true saturated maximum is higher, but driving a 1 W-rated pump diode past rating trades lifetime for diminishing return. Pump current overshoots ~2× briefly on each step up and settles in 2–4 s, so **ramp gradually** rather than jumping to the setpoint.
4500 mA with the output still climbing ~0.74 dB/500 mA and module temp only
~30 °C — so its true saturated maximum is higher, but driving a 1 W-rated pump
diode past rating trades lifetime for diminishing return. Pump current
overshoots ~2× briefly on each step up and settles in 2–4 s, so **ramp
gradually** rather than jumping to the setpoint.
> **Internal PD vs external power meter:** at 4500 mA the internal PD reported > **Internal PD vs external power meter:** at 4500 mA the internal PD reported 1.22 W while a Thorlabs meter measured **1.05 W** at the output (~0.6 dB high / residual loss). Treat the `amp/output*_power_dbm` telemetry as indicative, ~0.5–0.6 dB optimistic, not a calibrated power reading. (The earlier lab notes saw the opposite sign through a different connection, so the offset is path-dependent.)
> 1.22 W while a Thorlabs meter measured **1.05 W** at the output (~0.6 dB
> high / residual loss). Treat the `amp/output*_power_dbm` telemetry as
> indicative, ~0.5–0.6 dB optimistic, not a calibrated power reading. (The
> earlier lab notes saw the opposite sign through a different connection, so
> the offset is path-dependent.)
> **Safety:** Class 4, >1 W, invisible 1550 nm. The firmware clamps pump-1 to > **Safety:** Class 4, >1 W, invisible 1550 nm. The firmware clamps pump-1 to a soft 4000 mA ceiling (≈ the 1 W rating, `PUMP_CURRENT_SOFT_MAX_MA`), liftable to the 8000 mA hard protocol max (`PUMP_CURRENT_HARD_MAX_MA`) only while the Pump Limit Override switch is on, and seed current to `SEED_CURRENT_MAX_MA` — guardrails against accidental over-drive, **not interlocks**. Above 1 W the Agiltron manual requires matched high-power connectors or a splice on the output, else the output fibre/amplifier can be damaged. Always drop the pump to 0 before unmating any fibre. Treat the output as hazardous unless it terminates in a power meter / beam dump.
> `PUMP_CURRENT_MAX_MA` (4000 mA ≈ the 1 W rating) and seed current to
> `SEED_CURRENT_MAX_MA` — guardrails against accidental over-drive, **not
> interlocks**. Above 1 W the Agiltron manual requires matched high-power
> connectors or a splice on the output, else the output fibre/amplifier can be
> damaged. Always drop the pump to 0 before unmating any fibre. Treat the
> output as hazardous unless it terminates in a power meter / beam dump.
## Hardware ## Hardware
- ESP32-D0WD-V3 dev board (WROOM-32 class), CP2102N USB bridge → `/dev/ttyUSB0`, Full parts list (seed, amplifier, supplies, fibre, wiring) is in [`BOM.md`](BOM.md).
MAC `b0:cb:d8:8f:ba:10`.
- ESP32-D0WD-V3 dev board (WROOM-32 class), CP2102N USB bridge → `/dev/ttyUSB0`, MAC `b0:cb:d8:8f:ba:10`.
- The ESP32 USB port is used **only** for flashing/console (UART0). - The ESP32 USB port is used **only** for flashing/console (UART0).
- Two instrument links on separate hardware UARTs: - Two instrument links on separate hardware UARTs:
@@ -103,19 +73,13 @@ gradually** rather than jumping to the setpoint.
| GPIO25 (RX1) ← | Agiltron M511 TX (P2 pin 7) | | | GPIO25 (RX1) ← | Agiltron M511 TX (P2 pin 7) | |
| GND | both grounds | | | GND | both grounds | |
All links are 3V3 logic (CTL200 is 5V-tolerant). UART1 is remapped off its All links are 3V3 logic (CTL200 is 5V-tolerant). UART1 is remapped off its default flash-clashing pins.
default flash-clashing pins.
> **Network:** the firmware pins a static IP `192.168.0.150` (gateway `.1`, > **Network:** the firmware pins a static IP `192.168.0.150` (gateway `.1`, /24) — see `USE_STATIC_IP` in `include/config.h`. This avoids the clash with the BKtel EDFA Pi on `.101` that a DHCP lease originally caused. `.150` is outside any device documented in the HA notes; move it if your DHCP pool overlaps.
> /24) — see `USE_STATIC_IP` in `include/config.h`. This avoids the clash with
> the BKtel EDFA Pi on `.101` that a DHCP lease originally caused. `.150` is
> outside any device documented in the HA notes; move it if your DHCP pool
> overlaps.
## Toolchain ## Toolchain
PlatformIO (Arduino framework), pinned in a project-local `.venv` so nothing PlatformIO (Arduino framework), pinned in a project-local `.venv` so nothing is installed globally.
is installed globally.
```bash ```bash
make # show targets make # show targets
@@ -129,28 +93,15 @@ A fresh checkout self-bootstraps the venv on first `make`.
## Configuration ## Configuration
Copy `include/secrets.example.h` → `include/secrets.h` (git-ignored) and fill Copy `include/secrets.example.h` → `include/secrets.h` (git-ignored) and fill in WiFi + MQTT details. Needed from increment 2 onward.
in WiFi + MQTT details. Needed from increment 2 onward.
## Increment roadmap ## Increment roadmap
- [x] **1 — toolchain check.** Blink + serial heartbeat; confirms build/upload/monitor. - [x] **1 — toolchain check.** Blink + serial heartbeat; confirms build/upload/monitor.
- [x] **2 — WiFi + MQTT + HA discovery.** Joins WiFi, connects to broker with an - [x] **2 — WiFi + MQTT + HA discovery.** Joins WiFi, connects to broker with an LWT availability topic, publishes diagnostic entities (uptime, RSSI, IP).
LWT availability topic, publishes diagnostic entities (uptime, RSSI, IP). - [x] **3 — Seed telemetry (read-only).** UART2 → Koheron `lason`/`ilaser`/`vlaser`/`rtset`/`rtact`/`itec`/`vtec`/`tboard`/`iphd`/`err`; published under `seed/...` as HA sensors. Verified live against CTL200 fw v0.21 (echo + value + `>>` framing, parsed correctly).
- [x] **3 — Seed telemetry (read-only).** UART2 → Koheron `lason`/`ilaser`/`vlaser`/ - [x] **4 — Amp telemetry (read-only).** UART1 → Agiltron `get_status` (binary 0x55AA, ported from `edfa.py`); publishes module/pre-amp temp, pre-amp & pump-1 currents, input/pre-amp/output dBm, warnings under `amp/...`. Verified against the live unit (SN Y4022708, PN EDFA-1C51113331111, device ID 0x0000006F). `amp/pump_enabled` reads the authoritative `get_settings` field.
`rtset`/`rtact`/`itec`/`vtec`/`tboard`/`iphd`/`err`; published under `seed/...` - [x] **5 — control.** `set/...` for seed (`lason`, `ilaser`) and amp (pump on/off, pump-1 ACC current) as HA switch/number entities, with firmware current clamps. Verified end-to-end: MQTT command → seed lasing → booster ramp to 27 dBm. Seed TEC setpoint (`rtset`, in ohms) is also settable, with a read-only °C readout derived from the thermistor. A **Pump Limit Override** switch (`set/amp/pump_override`) lifts the pump-1 clamp from the soft 4000 mA (1 W) guardrail to the 8000 mA protocol max for supervised over-rating; it defaults off on every boot and re-clamps a running pump back to 4000 mA when switched off. (Future: seed `tecon`, amp APC power, Koheron `save`.)
as HA sensors. Verified live against CTL200 fw v0.21 (echo + value + `>>`
framing, parsed correctly).
- [x] **4 — Amp telemetry (read-only).** UART1 → Agiltron `get_status` (binary
0x55AA, ported from `edfa.py`); publishes module/pre-amp temp, pre-amp &
pump-1 currents, input/pre-amp/output dBm, warnings under `amp/...`.
Verified against the live unit (SN Y4022708, PN EDFA-1C51113331111,
device ID 0x0000006F). `amp/pump_enabled` reads the authoritative
`get_settings` field.
- [x] **5 — control.** `set/...` for seed (`lason`, `ilaser`) and amp (pump
on/off, pump-1 ACC current) as HA switch/number entities, with firmware
current clamps. Verified end-to-end: MQTT command → seed lasing → booster
ramp to 27 dBm. (Future: seed `rtset`/`tecon`, amp APC power, Koheron `save`.)
## MQTT interface ## MQTT interface
@@ -164,6 +115,7 @@ State topics retained under `lab/laser/butterfly_edfa_rack/`:
| `seed/laser_enabled` | seed laser on (1/0) | | `seed/laser_enabled` | seed laser on (1/0) |
| `seed/current_ma`, `seed/voltage_v` | seed drive | | `seed/current_ma`, `seed/voltage_v` | seed drive |
| `seed/rt_setpoint_ohm`, `seed/rt_actual_ohm` | thermistor setpoint / actual | | `seed/rt_setpoint_ohm`, `seed/rt_actual_ohm` | thermistor setpoint / actual |
| `seed/temp_c` | seed diode temperature (from `rt_actual` via NTC β-model, indicative) |
| `seed/tec_current_a`, `seed/tec_voltage_v`, `seed/board_temp_c` | seed TEC / board | | `seed/tec_current_a`, `seed/tec_voltage_v`, `seed/board_temp_c` | seed TEC / board |
| `seed/photodiode_ma`, `seed/error` | seed monitor PD, CTL200 error code | | `seed/photodiode_ma`, `seed/error` | seed monitor PD, CTL200 error code |
| `amp/link` | Agiltron reachable? | | `amp/link` | Agiltron reachable? |
@@ -172,6 +124,7 @@ State topics retained under `lab/laser/butterfly_edfa_rack/`:
| `amp/pump1_current_ma`, `amp/pump1_set_ma` | booster pump actual / setpoint | | `amp/pump1_current_ma`, `amp/pump1_set_ma` | booster pump actual / setpoint |
| `amp/input_power_dbm`, `amp/output1_power_dbm`, `amp/output2_power_dbm` | optical powers | | `amp/input_power_dbm`, `amp/output1_power_dbm`, `amp/output2_power_dbm` | optical powers |
| `amp/pump_enabled`, `amp/warnings` | booster state, decoded warnings | | `amp/pump_enabled`, `amp/warnings` | booster state, decoded warnings |
| `amp/pump_override` | pump-1 limit override on (1/0) |
Command topics (publish to `lab/laser/butterfly_edfa_rack/set/`): Command topics (publish to `lab/laser/butterfly_edfa_rack/set/`):
@@ -179,5 +132,7 @@ Command topics (publish to `lab/laser/butterfly_edfa_rack/set/`):
|--------------|---------|--------| |--------------|---------|--------|
| `seed/laser` | `1` / `0` | Koheron `lason` | | `seed/laser` | `1` / `0` | Koheron `lason` |
| `seed/current_ma` | mA | Koheron `ilaser` (clamped ≤ `SEED_CURRENT_MAX_MA`) | | `seed/current_ma` | mA | Koheron `ilaser` (clamped ≤ `SEED_CURRENT_MAX_MA`) |
| `seed/rt_setpoint_ohm` | Ω | Koheron `rtset`, seed TEC setpoint (clamped `SEED_RT_SETPOINT_MIN/MAX_OHM`) |
| `amp/pump` | `1` / `0` | Agiltron pump on / off | | `amp/pump` | `1` / `0` | Agiltron pump on / off |
| `amp/pump1_current_ma` | mA | set ACC mode + pump-1 current (clamped ≤ `PUMP_CURRENT_MAX_MA`) | | `amp/pump1_current_ma` | mA | set ACC mode + pump-1 current (clamped ≤ soft 4000 mA, or ≤ hard 8000 mA when override on) |
| `amp/pump_override` | `1` / `0` | lift the pump-1 clamp from soft (4000 mA / 1 W) to hard (8000 mA). Default off; switching off re-clamps a running pump to soft |
+37
View File
@@ -0,0 +1,37 @@
# Home Assistant integration
Config for presenting the Butterfly EDFA Rack in Home Assistant. The rack's sensors, switches and numbers are auto-discovered over MQTT (the firmware publishes `homeassistant/.../config` topics), so these files only add what discovery cannot: derived mW power sensors and a dedicated dashboard.
Target: the HA instance on the lab broker host (`192.168.0.102`), running in Docker with its config bind-mounted at `~/HA/homeassistant/` (that is HA's `/config`).
## Files
| File | Goes to | Purpose |
|------|---------|---------|
| `configuration.snippet.yaml` | merge into `/config/configuration.yaml` | MQTT template sensors (dBm→mW for the EDFA input, pre-amp and output PDs) + the `butterfly-rack` YAML dashboard registration |
| `scripts.snippet.yaml` | merge into `/config/scripts.yaml` | `butterfly_edfa_shutdown` emergency-off script |
| `ui-lovelace-butterfly.yaml` | copy to `/config/ui-lovelace-butterfly.yaml` | the dashboard itself (graphs of power in mW + temperatures, dual dBm/mW output readout, staged seed/booster controls, emergency-off) |
The `*.snippet.yaml` files hold top-level keys (`mqtt:`, `lovelace:`, and the script) to merge into the matching HA files — they are not standalone configs. `ui-lovelace-butterfly.yaml` is copied whole.
## Deploy
From this directory, with the HA container named `homeassistant`:
```bash
host=lab@192.168.0.102
scp ui-lovelace-butterfly.yaml $host:/tmp/
ssh $host 'docker cp /tmp/ui-lovelace-butterfly.yaml homeassistant:/config/ui-lovelace-butterfly.yaml'
# then hand-merge the two snippets into /config/configuration.yaml and
# /config/scripts.yaml, and validate + restart:
ssh $host 'docker exec homeassistant python -m homeassistant --script check_config -c /config'
ssh $host 'docker restart homeassistant'
```
The dashboard reloads on browser refresh; changes to `configuration.yaml`/`scripts.yaml` need the restart. The dashboard appears in the sidebar as **Butterfly EDFA Rack** (`/butterfly-rack`).
## Notes
- The `*_mw` sensors are grouped onto the same `butterfly_edfa_rack` device via the shared `identifiers`.
- mW figures are derived from the internal-PD dBm telemetry, which reads ~0.5 dB optimistic — indicative, not calibrated.
- The seed temperature setpoint is the CTL200 thermistor resistance (`rtset`, ohms); the °C readout is a firmware-side NTC β-model. See the repo root README.
+55
View File
@@ -0,0 +1,55 @@
# Butterfly EDFA Rack — snippet to merge into Home Assistant's configuration.yaml.
# These are top-level keys; add them alongside your existing config (the rack's
# sensors/switches/numbers come in automatically over MQTT discovery, so only
# the derived mW power sensors and the dashboard registration live here).
#
# The internal PDs read ~0.5 dB optimistic, so the mW figures are indicative.
mqtt:
sensor:
- name: "Output Power mW"
unique_id: butterfly_edfa_rack_out_mw
state_topic: "lab/laser/butterfly_edfa_rack/amp/output1_power_dbm"
value_template: "{{ (10 ** (value | float / 10)) | round(1) }}"
unit_of_measurement: "mW"
device_class: power
state_class: measurement
availability_topic: "lab/laser/butterfly_edfa_rack/availability"
payload_available: "online"
payload_not_available: "offline"
device: { identifiers: ["butterfly_edfa_rack"] }
# EDFA input PD = seed AFTER the external attenuators (not seed output power).
- name: "Amp Input Power mW"
unique_id: butterfly_edfa_rack_ampin_mw
state_topic: "lab/laser/butterfly_edfa_rack/amp/input_power_dbm"
value_template: "{{ (10 ** (value | float / 10)) | round(3) }}"
unit_of_measurement: "mW"
device_class: power
state_class: measurement
availability_topic: "lab/laser/butterfly_edfa_rack/availability"
payload_available: "online"
payload_not_available: "offline"
device: { identifiers: ["butterfly_edfa_rack"] }
- name: "Pre-amp Power mW"
unique_id: butterfly_edfa_rack_preamp_mw
state_topic: "lab/laser/butterfly_edfa_rack/amp/preamp_power_dbm"
value_template: "{{ (10 ** (value | float / 10)) | round(1) }}"
unit_of_measurement: "mW"
device_class: power
state_class: measurement
availability_topic: "lab/laser/butterfly_edfa_rack/availability"
payload_available: "online"
payload_not_available: "offline"
device: { identifiers: ["butterfly_edfa_rack"] }
# YAML-mode dashboard registration. Points at ui-lovelace-butterfly.yaml, which
# must sit next to configuration.yaml in the HA config directory.
lovelace:
mode: storage
dashboards:
butterfly-rack:
mode: yaml
title: Butterfly EDFA Rack
icon: mdi:laser-pointer
show_in_sidebar: true
filename: ui-lovelace-butterfly.yaml
+19
View File
@@ -0,0 +1,19 @@
# Butterfly EDFA Rack — snippet to merge into Home Assistant's scripts.yaml.
# Emergency shutdown: ramp pump-1 to 0, disable the booster, then the seed.
butterfly_edfa_shutdown:
alias: Butterfly EDFA emergency shutdown
icon: mdi:alarm-light
mode: single
sequence:
- service: number.set_value
target:
entity_id: number.butterfly_edfa_rack_amp_pump_current
data:
value: 0
- service: switch.turn_off
target:
entity_id: switch.butterfly_edfa_rack_amp_pump
- service: switch.turn_off
target:
entity_id: switch.butterfly_edfa_rack_seed_laser
+321
View File
@@ -0,0 +1,321 @@
# Butterfly EDFA Rack (MOPA) — laser control panel.
# Registered in configuration.yaml (lovelace: dashboards: butterfly-rack).
#
# Entities are auto-discovered over MQTT; the *_mw power sensors are MQTT
# template sensors defined in configuration.butterfly.yaml (dBm -> mW).
# Styling uses card-mod (installed via HACS).
#
# Two tabs:
# Control — the panel: status strip, output readout, the two
# stages, emergency stop. Everything you touch.
# Diagnostics & Trends — the history graphs plus network / TEC / error
# detail, kept off the control tab so it stays clean.
title: Butterfly EDFA Rack
views:
# ===================================================================
# TAB 1 — CONTROL PANEL
# ===================================================================
- title: Control
path: control
icon: mdi:laser-pointer
type: sections
# Single column so the sections stack in operating order:
# Status -> Output -> Seed -> Booster -> Emergency.
max_columns: 1
sections:
# ---- Output readout (2 units) + SHUTDOWN (1 unit, square) -------
- type: grid
cards:
# Big output-power readout.
# HA's markdown card strips inline styles, so size/centre via
# card-mod piercing the ha-markdown shadow (the three spans are the
# value, the " mW" unit, and the "Output power" label).
- type: markdown
grid_options:
columns: 8
rows: 3
content: >
<div><span>{{ states('sensor.butterfly_edfa_rack_output_power_mw')
}}</span><span> mW</span><br><span>Output power</span></div>
card_mod:
style:
.: |
ha-card {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
}
ha-markdown$: |
div { text-align: center !important; width: 100%; line-height: 1.05; }
div span:nth-of-type(1) { font-size: 3.6rem; font-weight: 700; }
div span:nth-of-type(2) { font-size: 1.7rem; font-weight: 600; }
div span:nth-of-type(3) { color: var(--secondary-text-color); }
# One-tap emergency stop. Runs butterfly_edfa_shutdown: pump-1 to 0,
# booster off, then seed off (EDFA first, then seed). No confirmation
# so it acts immediately; the action is safe (it only turns things off).
- type: button
name: SHUTDOWN
icon: mdi:alarm-light
icon_height: 40px
show_state: false
grid_options:
columns: 4
rows: 3
tap_action:
action: call-service
service: script.butterfly_edfa_shutdown
card_mod:
style: |
ha-card {
background: #c0392b;
color: #fff;
--primary-text-color: #fff;
--paper-item-icon-color: #fff;
font-weight: 800;
letter-spacing: 0.06em;
height: 100%;
}
# ---- Rack status (full width) -----------------------------------
- type: grid
column_span: 2
cards:
- type: heading
heading: Butterfly EDFA Rack
heading_style: title
icon: mdi:laser-pointer
- type: heading
heading: 1550 nm MOPA · Class 4 · invisible 1550 nm — treat output as hazardous
heading_style: subtitle
- type: glance
state_color: true
columns: 4
grid_options:
columns: 12
# Read-only status lamps. The actual on/off toggles live in the
# Seed and Booster stage cards below (tap_action: none here so this
# strip does not duplicate those controls).
entities:
- entity: binary_sensor.butterfly_edfa_rack_seed_link
name: Seed link
tap_action:
action: none
- entity: binary_sensor.butterfly_edfa_rack_amp_link
name: Amp link
tap_action:
action: none
- entity: binary_sensor.butterfly_edfa_rack_seed_laser_enabled
name: Seed
tap_action:
action: none
- entity: switch.butterfly_edfa_rack_amp_pump
name: Booster
tap_action:
action: none
- type: tile
entity: sensor.butterfly_edfa_rack_amp_warnings
name: Status
icon: mdi:alert-decagram
grid_options:
columns: 12
card_mod:
style: |
ha-card {
{% set w = states('sensor.butterfly_edfa_rack_amp_warnings') %}
{% if w in ['none','Pump is OFF','unknown','unavailable'] or w == '' %}
--tile-color: var(--green-color, #43a047);
{% else %}
--tile-color: var(--red-color, #e53935);
{% endif %}
}
# ---- Output readout ---------------------------------------------
- type: grid
column_span: 1
cards:
- type: heading
heading: Output
heading_style: subtitle
icon: mdi:arrow-left-bold
- type: tile
entity: sensor.butterfly_edfa_rack_output_power
name: Output dBm
icon: mdi:sine-wave
grid_options:
columns: 6
- type: tile
entity: sensor.butterfly_edfa_rack_amp_input_power_mw
name: EDFA in
icon: mdi:arrow-right-bold
grid_options:
columns: 6
# ---- Stage 1: seed ----------------------------------------------
- type: grid
column_span: 1
cards:
- type: heading
heading: Seed · Stage 1 (CTL200)
heading_style: subtitle
icon: mdi:alpha-s-circle-outline
- type: entities
state_color: true
entities:
- entity: switch.butterfly_edfa_rack_seed_laser
name: Seed laser (ON first)
- entity: number.butterfly_edfa_rack_seed_set_current
name: Set current (mA)
- entity: number.butterfly_edfa_rack_seed_thermistor_setpoint
name: Temp setpoint (Ω)
- type: tile
entity: sensor.butterfly_edfa_rack_seed_current
name: Current
icon: mdi:current-dc
grid_options:
columns: 6
- type: tile
entity: sensor.butterfly_edfa_rack_seed_temperature
name: Seed temp
icon: mdi:thermometer
grid_options:
columns: 6
- type: tile
entity: sensor.butterfly_edfa_rack_seed_photodiode_current
name: Monitor PD
icon: mdi:current-dc
grid_options:
columns: 6
# ---- Stage 2: booster -------------------------------------------
- type: grid
column_span: 1
cards:
- type: heading
heading: Booster · Stage 2 (M511, ACC)
heading_style: subtitle
icon: mdi:alpha-b-circle-outline
- type: entities
state_color: true
entities:
- entity: switch.butterfly_edfa_rack_amp_pump
name: Pump enable
- entity: number.butterfly_edfa_rack_amp_pump_current
name: Pump-1 set (mA)
# Lifts the pump-1 clamp from 4000 mA (1 W) to the 8000 mA max.
- entity: switch.butterfly_edfa_rack_amp_pump_limit_override
name: Pump limit override (→ 8000 mA)
- type: tile
entity: sensor.butterfly_edfa_rack_amp_pump_1_current
name: Pump-1 current
icon: mdi:current-dc
grid_options:
columns: 6
- type: tile
entity: sensor.butterfly_edfa_rack_amp_module_temperature
name: Module temp
icon: mdi:thermometer
grid_options:
columns: 6
# ===================================================================
# TAB 2 — DIAGNOSTICS & TRENDS
# ===================================================================
- title: Diagnostics & Trends
path: diagnostics
icon: mdi:chart-line
type: sections
max_columns: 3
sections:
- type: grid
column_span: 2
cards:
- type: heading
heading: Trends
heading_style: subtitle
icon: mdi:chart-line
- type: history-graph
title: Output & pre-amp PD (mW)
hours_to_show: 1
entities:
- entity: sensor.butterfly_edfa_rack_output_power_mw
name: Output PD
- entity: sensor.butterfly_edfa_rack_pre_amp_power_mw
name: Pre-amp PD
- type: history-graph
title: EDFA input PD (mW) — seed after attenuators
hours_to_show: 1
entities:
- entity: sensor.butterfly_edfa_rack_amp_input_power_mw
name: EDFA input
- type: history-graph
title: Seed monitor PD (mA — relative, uncalibrated)
hours_to_show: 1
entities:
- entity: sensor.butterfly_edfa_rack_seed_photodiode_current
name: Seed monitor PD
- type: history-graph
title: Temperatures (°C)
hours_to_show: 1
entities:
- entity: sensor.butterfly_edfa_rack_seed_temperature
name: Seed
- entity: sensor.butterfly_edfa_rack_seed_board_temperature
name: Seed board
- entity: sensor.butterfly_edfa_rack_amp_module_temperature
name: Amp module
- entity: sensor.butterfly_edfa_rack_amp_pre_amp_temperature
name: Amp pre-amp
- type: grid
column_span: 1
cards:
- type: heading
heading: Seed detail
heading_style: subtitle
icon: mdi:alpha-s-circle-outline
- type: entities
entities:
- entity: sensor.butterfly_edfa_rack_seed_voltage
name: Seed voltage
- entity: sensor.butterfly_edfa_rack_seed_thermistor
name: Thermistor (actual)
- entity: sensor.butterfly_edfa_rack_seed_tec_current
name: TEC current
- entity: sensor.butterfly_edfa_rack_seed_tec_voltage
name: TEC voltage
- entity: sensor.butterfly_edfa_rack_seed_board_temperature
name: Board temp
- entity: sensor.butterfly_edfa_rack_seed_error_code
name: Error code
- type: heading
heading: Amp detail
heading_style: subtitle
icon: mdi:alpha-b-circle-outline
- type: entities
entities:
- entity: sensor.butterfly_edfa_rack_amp_input_power
name: Input power (dBm)
- entity: sensor.butterfly_edfa_rack_amp_pre_amp_power
name: Pre-amp power (dBm)
- entity: sensor.butterfly_edfa_rack_amp_pre_amp_current
name: Pre-amp current
- entity: sensor.butterfly_edfa_rack_amp_pre_amp_temperature
name: Pre-amp temp
- entity: sensor.butterfly_edfa_rack_amp_tec_current
name: TEC current
- entity: sensor.butterfly_edfa_rack_amp_warnings
name: Warnings
- type: heading
heading: Bridge (ESP32)
heading_style: subtitle
icon: mdi:chip
- type: entities
entities:
- entity: sensor.butterfly_edfa_rack_ip_address
name: IP address
- entity: sensor.butterfly_edfa_rack_wifi_signal
name: WiFi signal
- entity: sensor.butterfly_edfa_rack_uptime
name: Uptime
+27 -4
View File
@@ -26,10 +26,33 @@
// HA number sliders are capped here. The hardware allows 8000 mA pump, but // HA number sliders are capped here. The hardware allows 8000 mA pump, but
// the lab notes report ~2500 mA pump-1 -> ~27 dBm (~515 mW). Keep a margin. // the lab notes report ~2500 mA pump-1 -> ~27 dBm (~515 mW). Keep a margin.
#define SEED_CURRENT_MAX_MA 300 // CTL200 ilmax default is 1000; seed DFB is low-power #define SEED_CURRENT_MAX_MA 300 // CTL200 ilmax default is 1000; seed DFB is low-power
// Pump-1 clamp = the 1 W rated operating point (~4000 mA, measured 2026-06-17). // Pump-1 current clamp. Two ceilings:
// Raise deliberately only for supervised over-rating characterisation; it is a // SOFT (default) = the 1 W rated operating point (~4000 mA, measured
// guardrail against accidental over-drive, not a safety interlock. // 2026-06-17). Guards against accidental over-drive. NOT a safety interlock.
#define PUMP_CURRENT_MAX_MA 4000 // HARD = the Agiltron protocol / hardware maximum (8000 mA). Reachable only
// while the "Pump limit override" switch is ON (set/amp/pump_override), for
// supervised over-rating characterisation. Driving the 1 W-rated pump diode
// toward 8000 mA trades diode lifetime (and risks damage) for extra power.
// The HA number slider spans 0..HARD; a command above SOFT is clamped to SOFT
// unless the override is ON. Turning the override OFF re-clamps a running
// setpoint back down to SOFT immediately.
#define PUMP_CURRENT_SOFT_MAX_MA 4000
#define PUMP_CURRENT_HARD_MAX_MA 8000
// Seed thermistor setpoint (CTL200 `rtset`) clamp, in ohms. The CTL200 has no
// native °C setpoint; it regulates the seed TEC to hold the thermistor at this
// resistance. The default operating point is 10 kΩ. The range below spans
// roughly 39…13 °C on the β-model (a guardrail, not a diode-safe interlock —
// narrow it if the DFB1550LP datasheet gives a tighter window).
#define SEED_RT_SETPOINT_MIN_OHM 6000
#define SEED_RT_SETPOINT_MAX_OHM 16000
// NTC β-model for the read-only resistance→°C conversion (display only; the
// CTL200 does its own regulation in ohms). Assumes a 10 kΩ @ 25 °C NTC with
// β = 3435 K. Update if the seed thermistor's R25/β are known to differ.
#define SEED_THERMISTOR_R25_OHM 10000.0f
#define SEED_THERMISTOR_BETA_K 3435.0f
#define SEED_THERMISTOR_T25_K 298.15f
// ── Serial links to the instruments (wired in later increments) ───── // ── Serial links to the instruments (wired in later increments) ─────
// UART0 stays the USB console. The classic ESP32 has 3 hardware UARTs. // UART0 stays the USB console. The classic ESP32 has 3 hardware UARTs.
+1 -1
View File
@@ -1,4 +1,4 @@
; PlatformIO project for the Koheron CTL200 -> WiFi/MQTT bridge on ESP32. ; PlatformIO project for the Butterfly EDFA Rack bridge (Koheron seed + Agiltron M511 EDFA) on ESP32.
; Board: ESP32-D0WD-V3 (WROOM-32 class) on a CP2102N USB bridge. ; Board: ESP32-D0WD-V3 (WROOM-32 class) on a CP2102N USB bridge.
[env:esp32dev] [env:esp32dev]
+56 -4
View File
@@ -22,6 +22,13 @@ static Koheron koheron(Serial2, KOHERON_RX_PIN, KOHERON_TX_PIN, KOHERON_BA
static const uint8_t kAmpDevId[4] = {AGILTRON_DEV_ID}; static const uint8_t kAmpDevId[4] = {AGILTRON_DEV_ID};
static Agiltron agiltron(Serial1, AGILTRON_RX_PIN, AGILTRON_TX_PIN, AGILTRON_BAUD, kAmpDevId); static Agiltron agiltron(Serial1, AGILTRON_RX_PIN, AGILTRON_TX_PIN, AGILTRON_BAUD, kAmpDevId);
// Pump-1 limit override. false => clamp pump-1 commands to the SOFT (1 W)
// ceiling; true => allow up to the HARD (protocol) ceiling. Defaults OFF on
// every boot (fail-safe). pump1SetMa tracks the last commanded setpoint so the
// override can re-clamp a running pump down when it is switched back off.
static bool pumpOverride = false;
static int pump1SetMa = 0;
// Probe each instrument link once per boot to reveal its framing. // Probe each instrument link once per boot to reveal its framing.
#define KOHERON_RAW_PROBE 1 #define KOHERON_RAW_PROBE 1
#define AGILTRON_PROBE 1 #define AGILTRON_PROBE 1
@@ -120,8 +127,8 @@ static void publishDiscovery() {
binaryDiscovery("seed_laser_enabled", "Seed Laser Enabled", "seed/laser_enabled", "1", "0", nullptr, false); binaryDiscovery("seed_laser_enabled", "Seed Laser Enabled", "seed/laser_enabled", "1", "0", nullptr, false);
sensorDiscovery("seed_current", "Seed Current", "seed/current_ma", "mA", "current", "mdi:current-dc", false); sensorDiscovery("seed_current", "Seed Current", "seed/current_ma", "mA", "current", "mdi:current-dc", false);
sensorDiscovery("seed_voltage", "Seed Voltage", "seed/voltage_v", "V", "voltage", nullptr, false); sensorDiscovery("seed_voltage", "Seed Voltage", "seed/voltage_v", "V", "voltage", nullptr, false);
sensorDiscovery("seed_rt_setpoint", "Seed Thermistor Setpoint", "seed/rt_setpoint_ohm", "Ω", nullptr, "mdi:resistor", false);
sensorDiscovery("seed_rt_actual", "Seed Thermistor", "seed/rt_actual_ohm", "Ω", nullptr, "mdi:resistor", false); sensorDiscovery("seed_rt_actual", "Seed Thermistor", "seed/rt_actual_ohm", "Ω", nullptr, "mdi:resistor", false);
sensorDiscovery("seed_temp", "Seed Temperature", "seed/temp_c", "°C", "temperature", nullptr, false);
sensorDiscovery("seed_tec_current", "Seed TEC Current", "seed/tec_current_a", "A", "current", nullptr, false); sensorDiscovery("seed_tec_current", "Seed TEC Current", "seed/tec_current_a", "A", "current", nullptr, false);
sensorDiscovery("seed_tec_voltage", "Seed TEC Voltage", "seed/tec_voltage_v", "V", "voltage", nullptr, false); sensorDiscovery("seed_tec_voltage", "Seed TEC Voltage", "seed/tec_voltage_v", "V", "voltage", nullptr, false);
sensorDiscovery("seed_board_temp", "Seed Board Temperature", "seed/board_temp_c", "°C", "temperature", nullptr, false); sensorDiscovery("seed_board_temp", "Seed Board Temperature", "seed/board_temp_c", "°C", "temperature", nullptr, false);
@@ -132,6 +139,10 @@ static void publishDiscovery() {
switchDiscovery("seed_laser", "Seed Laser", "seed/laser_enabled", "seed/laser", "1", "0", "mdi:laser-pointer"); switchDiscovery("seed_laser", "Seed Laser", "seed/laser_enabled", "seed/laser", "1", "0", "mdi:laser-pointer");
numberDiscovery("seed_set_current", "Seed Set Current", "seed/current_ma", "seed/current_ma", numberDiscovery("seed_set_current", "Seed Set Current", "seed/current_ma", "seed/current_ma",
0, SEED_CURRENT_MAX_MA, 1, "mA", "mdi:current-dc"); 0, SEED_CURRENT_MAX_MA, 1, "mA", "mdi:current-dc");
// Seed TEC setpoint: settable in ohms (CTL200 `rtset`); state mirrors the
// `seed/rt_setpoint_ohm` telemetry. °C is exposed read-only via seed/temp_c.
numberDiscovery("seed_set_rt_setpoint", "Seed Thermistor Setpoint", "seed/rt_setpoint_ohm", "seed/rt_setpoint_ohm",
SEED_RT_SETPOINT_MIN_OHM, SEED_RT_SETPOINT_MAX_OHM, 10, "Ω", "mdi:thermometer");
// Amp (Agiltron M511) // Amp (Agiltron M511)
binaryDiscovery("amp_link", "Amp Link", "amp/link", AVAIL_ONLINE, AVAIL_OFFLINE, "connectivity", true); binaryDiscovery("amp_link", "Amp Link", "amp/link", AVAIL_ONLINE, AVAIL_OFFLINE, "connectivity", true);
@@ -148,7 +159,10 @@ static void publishDiscovery() {
// Amp controls // Amp controls
switchDiscovery("amp_pump", "Amp Pump", "amp/pump_enabled", "amp/pump", "1", "0", "mdi:flash"); switchDiscovery("amp_pump", "Amp Pump", "amp/pump_enabled", "amp/pump", "1", "0", "mdi:flash");
numberDiscovery("amp_set_pump_current", "Amp Pump Current", "amp/pump1_set_ma", "amp/pump1_current_ma", numberDiscovery("amp_set_pump_current", "Amp Pump Current", "amp/pump1_set_ma", "amp/pump1_current_ma",
0, PUMP_CURRENT_MAX_MA, 10, "mA", "mdi:current-dc"); 0, PUMP_CURRENT_HARD_MAX_MA, 10, "mA", "mdi:current-dc");
// Override the soft (1 W) pump-1 clamp up to the hard protocol ceiling.
switchDiscovery("amp_pump_override", "Amp Pump Limit Override", "amp/pump_override", "amp/pump_override",
"1", "0", "mdi:lock-open-variant");
} }
// ── State publishing ──────────────────────────────────────────────── // ── State publishing ────────────────────────────────────────────────
@@ -161,6 +175,14 @@ static void publishDiag() {
} }
// Each seed value is the raw string the CTL200 returns (already numeric text). // Each seed value is the raw string the CTL200 returns (already numeric text).
// NTC β-model: thermistor resistance (Ω) -> °C. Display-only approximation;
// the CTL200 regulates in ohms, not against this figure.
static float seedOhmToC(float r) {
float invT = 1.0f / SEED_THERMISTOR_T25_K
+ logf(r / SEED_THERMISTOR_R25_OHM) / SEED_THERMISTOR_BETA_K;
return 1.0f / invT - 273.15f;
}
struct SeedPoint { const char *cmd; const char *sub; }; struct SeedPoint { const char *cmd; const char *sub; };
static const SeedPoint seedPoints[] = { static const SeedPoint seedPoints[] = {
{"lason", "seed/laser_enabled"}, {"lason", "seed/laser_enabled"},
@@ -177,14 +199,18 @@ static const SeedPoint seedPoints[] = {
static void publishSeed() { static void publishSeed() {
bool any = false; bool any = false;
float rtActOhm = 0;
for (const auto &pt : seedPoints) { for (const auto &pt : seedPoints) {
String v = koheron.query(pt.cmd); String v = koheron.query(pt.cmd);
if (v.length()) { if (v.length()) {
mqtt.publish((String(TOPIC_BASE) + "/" + pt.sub).c_str(), v.c_str(), true); mqtt.publish((String(TOPIC_BASE) + "/" + pt.sub).c_str(), v.c_str(), true);
if (strcmp(pt.cmd, "rtact") == 0) rtActOhm = v.toFloat();
any = true; any = true;
} }
mqtt.loop(); // keep MQTT serviced between (potentially slow) UART round-trips mqtt.loop(); // keep MQTT serviced between (potentially slow) UART round-trips
} }
if (rtActOhm > 0)
mqtt.publish(TOPIC_BASE "/seed/temp_c", String(seedOhmToC(rtActOhm), 2).c_str(), true);
mqtt.publish(TOPIC_BASE "/seed/link", any ? AVAIL_ONLINE : AVAIL_OFFLINE, true); mqtt.publish(TOPIC_BASE "/seed/link", any ? AVAIL_ONLINE : AVAIL_OFFLINE, true);
} }
@@ -195,6 +221,7 @@ static void pub(const char *sub, const String &v) {
static void publishAmp() { static void publishAmp() {
AmpStatus s = agiltron.getStatus(); AmpStatus s = agiltron.getStatus();
mqtt.publish(TOPIC_BASE "/amp/link", s.valid ? AVAIL_ONLINE : AVAIL_OFFLINE, true); mqtt.publish(TOPIC_BASE "/amp/link", s.valid ? AVAIL_ONLINE : AVAIL_OFFLINE, true);
pub("amp/pump_override", pumpOverride ? "1" : "0"); // reflect override state
if (!s.valid) return; if (!s.valid) return;
pub("amp/module_temp_c", String(s.module_temp_c, 1)); pub("amp/module_temp_c", String(s.module_temp_c, 1));
pub("amp/preamp_temp_c", String(s.preamp_temp_c, 1)); pub("amp/preamp_temp_c", String(s.preamp_temp_c, 1));
@@ -230,6 +257,13 @@ static void onMqtt(char *topic, byte *payload, unsigned int len) {
koheron.query((String("ilaser ") + String(ma, 1)).c_str()); koheron.query((String("ilaser ") + String(ma, 1)).c_str());
pub("seed/current_ma", String(ma, 3)); pub("seed/current_ma", String(ma, 3));
} else if (t == TOPIC_CMD "/seed/rt_setpoint_ohm") {
long ohm = p.toInt();
if (ohm < SEED_RT_SETPOINT_MIN_OHM) ohm = SEED_RT_SETPOINT_MIN_OHM;
if (ohm > SEED_RT_SETPOINT_MAX_OHM) ohm = SEED_RT_SETPOINT_MAX_OHM; // firmware clamp
koheron.query((String("rtset ") + String(ohm)).c_str());
pub("seed/rt_setpoint_ohm", String(ohm));
} else if (t == TOPIC_CMD "/amp/pump") { } else if (t == TOPIC_CMD "/amp/pump") {
bool on = (p == "1" || p.equalsIgnoreCase("on") || p.equalsIgnoreCase(AVAIL_ONLINE)); bool on = (p == "1" || p.equalsIgnoreCase("on") || p.equalsIgnoreCase(AVAIL_ONLINE));
bool ok = on ? agiltron.pumpOn() : agiltron.pumpOff(); bool ok = on ? agiltron.pumpOn() : agiltron.pumpOff();
@@ -239,11 +273,29 @@ static void onMqtt(char *topic, byte *payload, unsigned int len) {
} else if (t == TOPIC_CMD "/amp/pump1_current_ma") { } else if (t == TOPIC_CMD "/amp/pump1_current_ma") {
int ma = (int)p.toFloat(); int ma = (int)p.toFloat();
if (ma < 0) ma = 0; if (ma < 0) ma = 0;
if (ma > PUMP_CURRENT_MAX_MA) ma = PUMP_CURRENT_MAX_MA; // firmware clamp int cap = pumpOverride ? PUMP_CURRENT_HARD_MAX_MA : PUMP_CURRENT_SOFT_MAX_MA;
if (ma > cap) ma = cap; // firmware clamp (soft unless overridden)
agiltron.setPump1Mode(Agiltron::ACC); // current-control mode (lab note: avoid APC steps) agiltron.setPump1Mode(Agiltron::ACC); // current-control mode (lab note: avoid APC steps)
bool ok = agiltron.setPump1Current(ma); bool ok = agiltron.setPump1Current(ma);
Serial.printf("[cmd] amp pump1 = %d mA -> %s\n", ma, ok ? "ack" : "NO-ACK"); pump1SetMa = ma;
Serial.printf("[cmd] amp pump1 = %d mA (cap %d) -> %s\n", ma, cap, ok ? "ack" : "NO-ACK");
pub("amp/pump1_set_ma", String(ma)); pub("amp/pump1_set_ma", String(ma));
} else if (t == TOPIC_CMD "/amp/pump_override") {
bool on = (p == "1" || p.equalsIgnoreCase("on") || p.equalsIgnoreCase(AVAIL_ONLINE));
pumpOverride = on;
Serial.printf("[cmd] pump limit override -> %s\n",
on ? "ON (hard 8000 mA)" : "OFF (soft 4000 mA)");
// Disabling the override must not leave the booster parked above 1 W: if the
// running setpoint is above the soft cap, re-clamp it down immediately.
if (!on && pump1SetMa > PUMP_CURRENT_SOFT_MAX_MA) {
pump1SetMa = PUMP_CURRENT_SOFT_MAX_MA;
agiltron.setPump1Mode(Agiltron::ACC);
agiltron.setPump1Current(pump1SetMa);
pub("amp/pump1_set_ma", String(pump1SetMa));
Serial.printf("[cmd] override off: re-clamped pump1 to %d mA\n", pump1SetMa);
}
pub("amp/pump_override", on ? "1" : "0");
} }
} }