From b31065acd7a5fd7a1cb1248db8fb147d1e4d20b3 Mon Sep 17 00:00:00 2001 From: "James Bateman (hp-laptop)" Date: Tue, 11 Aug 2026 16:41:53 +0100 Subject: [PATCH] =?UTF-8?q?Add=20settable=20seed=20TEC=20setpoint=20and=20?= =?UTF-8?q?=C2=B0C=20readout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 93 ++++++++++++------------------------------------ include/config.h | 15 ++++++++ src/main.cpp | 25 ++++++++++++- 3 files changed, 61 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 798580f..69bc587 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,8 @@ # koheron-esp32-bridge -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). +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). -The whole rack is presented as a **single HA device** (`butterfly_edfa_rack`). -Topics, mirroring the lab's EDFA→MQTT convention: +The whole rack is presented as a **single HA device** (`butterfly_edfa_rack`). Topics, mirroring the lab's EDFA→MQTT convention: ``` lab/laser/butterfly_edfa_rack/availability online | offline (LWT) @@ -16,8 +11,7 @@ lab/laser/butterfly_edfa_rack/seed/ Koheron CTL200 (increment 3 lab/laser/butterfly_edfa_rack/amp/ Agiltron M511 (increment 4) lab/laser/butterfly_edfa_rack/set/<...> commands (increment 5) ``` -HA discovery configs are published under `homeassistant/...`, all sharing one -`device` block so entities group under the single rack device. +HA discovery configs are published under `homeassistant/...`, all sharing one `device` block so entities group under the single rack device. ## 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: -- **Pre-amp pump** — runs semi-independently at ~170 mA, lifts the seed to - ~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`). -- **"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. +- **Pre-amp pump** — runs semi-independently at ~170 mA, lifts the seed to ~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`). +- **"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 -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. +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. ### Pump-1 vs output characterisation (2026-06-17) -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. +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. | Pump-1 | Output-1 (internal PD) | note | |-------:|------------------------|------| @@ -66,32 +51,15 @@ spec) and a direct output-to-power-meter connection. | 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) | -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. +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. -> **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.) +> **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.) -> **Safety:** Class 4, >1 W, invisible 1550 nm. The firmware clamps pump-1 to -> `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. +> **Safety:** Class 4, >1 W, invisible 1550 nm. The firmware clamps pump-1 to `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 -- ESP32-D0WD-V3 dev board (WROOM-32 class), CP2102N USB bridge → `/dev/ttyUSB0`, - 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). - Two instrument links on separate hardware UARTs: @@ -103,19 +71,13 @@ gradually** rather than jumping to the setpoint. | GPIO25 (RX1) ← | Agiltron M511 TX (P2 pin 7) | | | GND | both grounds | | - All links are 3V3 logic (CTL200 is 5V-tolerant). UART1 is remapped off its - default flash-clashing pins. + All links are 3V3 logic (CTL200 is 5V-tolerant). UART1 is remapped off its default flash-clashing pins. -> **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. +> **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. ## Toolchain -PlatformIO (Arduino framework), pinned in a project-local `.venv` so nothing -is installed globally. +PlatformIO (Arduino framework), pinned in a project-local `.venv` so nothing is installed globally. ```bash make # show targets @@ -129,28 +91,15 @@ A fresh checkout self-bootstraps the venv on first `make`. ## Configuration -Copy `include/secrets.example.h` → `include/secrets.h` (git-ignored) and fill -in WiFi + MQTT details. Needed from increment 2 onward. +Copy `include/secrets.example.h` → `include/secrets.h` (git-ignored) and fill in WiFi + MQTT details. Needed from increment 2 onward. ## Increment roadmap - [x] **1 — toolchain check.** Blink + serial heartbeat; confirms build/upload/monitor. -- [x] **2 — WiFi + MQTT + HA discovery.** Joins WiFi, connects to broker with an - 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] **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`.) +- [x] **2 — WiFi + MQTT + HA discovery.** Joins WiFi, connects to broker with an 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] **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. Seed TEC setpoint (`rtset`, in ohms) is also settable, with a read-only °C readout derived from the thermistor. (Future: seed `tecon`, amp APC power, Koheron `save`.) ## MQTT interface @@ -164,6 +113,7 @@ State topics retained under `lab/laser/butterfly_edfa_rack/`: | `seed/laser_enabled` | seed laser on (1/0) | | `seed/current_ma`, `seed/voltage_v` | seed drive | | `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/photodiode_ma`, `seed/error` | seed monitor PD, CTL200 error code | | `amp/link` | Agiltron reachable? | @@ -179,5 +129,6 @@ Command topics (publish to `lab/laser/butterfly_edfa_rack/set/`): |--------------|---------|--------| | `seed/laser` | `1` / `0` | Koheron `lason` | | `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/pump1_current_ma` | mA | set ACC mode + pump-1 current (clamped ≤ `PUMP_CURRENT_MAX_MA`) | diff --git a/include/config.h b/include/config.h index 894574f..f6cd667 100644 --- a/include/config.h +++ b/include/config.h @@ -31,6 +31,21 @@ // guardrail against accidental over-drive, not a safety interlock. #define PUMP_CURRENT_MAX_MA 4000 +// 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) ───── // UART0 stays the USB console. The classic ESP32 has 3 hardware UARTs. // diff --git a/src/main.cpp b/src/main.cpp index d67873a..c0eb5ef 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -120,8 +120,8 @@ static void publishDiscovery() { 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_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_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_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); @@ -132,6 +132,10 @@ static void publishDiscovery() { 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", 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) binaryDiscovery("amp_link", "Amp Link", "amp/link", AVAIL_ONLINE, AVAIL_OFFLINE, "connectivity", true); @@ -161,6 +165,14 @@ static void publishDiag() { } // 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; }; static const SeedPoint seedPoints[] = { {"lason", "seed/laser_enabled"}, @@ -177,14 +189,18 @@ static const SeedPoint seedPoints[] = { static void publishSeed() { bool any = false; + float rtActOhm = 0; for (const auto &pt : seedPoints) { String v = koheron.query(pt.cmd); if (v.length()) { mqtt.publish((String(TOPIC_BASE) + "/" + pt.sub).c_str(), v.c_str(), true); + if (strcmp(pt.cmd, "rtact") == 0) rtActOhm = v.toFloat(); any = true; } 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); } @@ -230,6 +246,13 @@ static void onMqtt(char *topic, byte *payload, unsigned int len) { koheron.query((String("ilaser ") + String(ma, 1)).c_str()); 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") { bool on = (p == "1" || p.equalsIgnoreCase("on") || p.equalsIgnoreCase(AVAIL_ONLINE)); bool ok = on ? agiltron.pumpOn() : agiltron.pumpOff();