Butterfly EDFA Rack bridge: seed + EDFA over WiFi/MQTT to Home Assistant
ESP32 firmware bridging the 1550 nm MOPA (Koheron CTL200 seed + Agiltron M511 two-stage EDFA) to MQTT/HA as a single device. Telemetry and control for both instruments verified end-to-end: MQTT command -> seed lasing -> booster ramp to 27 dBm (~515 mW) at the documented 2500 mA operating point. - PlatformIO/Arduino project, project-local venv, Makefile upload flow - Koheron ASCII driver on UART2; Agiltron binary 0x55AA driver on UART1 - HA discovery (sensors, binary_sensors, switches, numbers); LWT availability - Static IP, firmware current clamps on seed and booster pump
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
#include "agiltron.h"
|
||||
|
||||
uint8_t Agiltron::checksum(const uint8_t *p, size_t n) {
|
||||
uint32_t s = 0;
|
||||
for (size_t i = 0; i < n; i++) s += p[i];
|
||||
return ((0xFF - (s & 0xFF)) + 0x01) & 0xFF;
|
||||
}
|
||||
|
||||
size_t Agiltron::buildPacket(uint8_t *out, uint8_t cmd, const uint8_t *data, uint8_t len) {
|
||||
size_t i = 0;
|
||||
out[i++] = 0x55;
|
||||
out[i++] = 0xAA;
|
||||
for (int k = 0; k < 4; k++) out[i++] = _devId[k];
|
||||
out[i++] = cmd;
|
||||
out[i++] = len;
|
||||
for (uint8_t k = 0; k < len; k++) out[i++] = data[k];
|
||||
// checksum covers everything from dev_id onward (i.e. skip the 0x55 0xAA)
|
||||
out[i] = checksum(out + 2, i - 2);
|
||||
i++;
|
||||
return i;
|
||||
}
|
||||
|
||||
size_t Agiltron::transact(uint8_t cmd, const uint8_t *data, uint8_t len,
|
||||
uint8_t *buf, size_t bufLen, uint32_t timeoutMs) {
|
||||
uint8_t pkt[64];
|
||||
size_t n = buildPacket(pkt, cmd, data, len);
|
||||
|
||||
while (_ser.available()) _ser.read(); // flush stale
|
||||
_ser.write(pkt, n);
|
||||
_ser.flush();
|
||||
|
||||
size_t got = 0;
|
||||
uint32_t start = millis(), lastByte = start;
|
||||
const uint32_t IDLE_GAP = 30; // binary reply is short and prompt
|
||||
while (millis() - start < timeoutMs) {
|
||||
while (_ser.available() && got < bufLen) {
|
||||
buf[got++] = (uint8_t)_ser.read();
|
||||
lastByte = millis();
|
||||
}
|
||||
if (got && (millis() - lastByte) > IDLE_GAP) break;
|
||||
delay(2);
|
||||
}
|
||||
return got;
|
||||
}
|
||||
|
||||
bool Agiltron::validate(const uint8_t *buf, size_t n, uint8_t expectCmd) {
|
||||
if (n < 8) return false;
|
||||
if (buf[0] != 0xAA || buf[1] != 0x55) return false;
|
||||
if (buf[6] != expectCmd) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
AmpStatus Agiltron::getStatus() {
|
||||
AmpStatus st;
|
||||
uint8_t r[64];
|
||||
size_t n = transact(0x2F, nullptr, 0, r, sizeof(r));
|
||||
if (!validate(r, n, 0x2F) || n < 32) { _linkOk = false; return st; }
|
||||
_linkOk = true;
|
||||
|
||||
st.module_temp_c = u16(r[10], r[11]) / 10.0f;
|
||||
st.preamp_temp_c = u16(r[12], r[13]) / 10.0f;
|
||||
st.preamp_current_ma = u16(r[14], r[15]) / 10.0f;
|
||||
st.tec_current_ma = u16(r[16], r[17]) / 10.0f;
|
||||
st.pump1_current_ma = u16(r[18], r[19]);
|
||||
st.pump2_current_ma = u16(r[20], r[21]);
|
||||
st.input_power_dbm = s16(r[22], r[23]) / 100.0f;
|
||||
st.preamp_power_dbm = s16(r[24], r[25]) / 100.0f;
|
||||
st.output1_power_dbm = s16(r[26], r[27]) / 100.0f;
|
||||
st.output2_power_dbm = s16(r[28], r[29]) / 100.0f;
|
||||
st.warn_byte = r[31];
|
||||
st.valid = true;
|
||||
return st;
|
||||
}
|
||||
|
||||
int Agiltron::getPumpEnabled() {
|
||||
uint8_t r[64];
|
||||
size_t n = transact(0x2E, nullptr, 0, r, sizeof(r));
|
||||
if (!validate(r, n, 0x2E) || n < 10) return -1;
|
||||
return (u16(r[8], r[9]) == 0) ? 1 : 0; // edfa.py: ==0 means enabled
|
||||
}
|
||||
|
||||
String Agiltron::getSn() {
|
||||
uint8_t r[64];
|
||||
size_t n = transact(0x1F, nullptr, 0, r, sizeof(r));
|
||||
if (!validate(r, n, 0x1F)) return String();
|
||||
String s;
|
||||
for (size_t i = 8; i + 1 < n; i++) { // r[8 .. n-2], drop checksum
|
||||
char c = (char)r[i];
|
||||
if (c >= 32 && c < 127) s += c;
|
||||
}
|
||||
s.trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
String Agiltron::getPn() {
|
||||
uint8_t r[64];
|
||||
size_t n = transact(0x1E, nullptr, 0, r, sizeof(r));
|
||||
if (!validate(r, n, 0x1E)) return String();
|
||||
String s;
|
||||
for (size_t i = 8; i + 1 < n; i++) {
|
||||
char c = (char)r[i];
|
||||
if (c >= 32 && c < 127) s += c;
|
||||
}
|
||||
s.trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
bool Agiltron::setPump1Current(int ma) {
|
||||
if (ma < 0 || ma > MAX_PUMP_CURRENT_MA) return false;
|
||||
uint8_t d[2] = {(uint8_t)((ma >> 8) & 0xFF), (uint8_t)(ma & 0xFF)};
|
||||
return cmdAck(0x23, d, 2);
|
||||
}
|
||||
|
||||
bool Agiltron::cmdAck(uint8_t cmd, const uint8_t *data, uint8_t len) {
|
||||
uint8_t r[64];
|
||||
size_t n = transact(cmd, data, len, r, sizeof(r));
|
||||
bool ok = validate(r, n, cmd);
|
||||
_linkOk = ok || _linkOk;
|
||||
return ok;
|
||||
}
|
||||
|
||||
String Agiltron::warningsString(uint8_t w) {
|
||||
String out;
|
||||
auto add = [&](const char *m) { if (out.length()) out += ", "; out += m; };
|
||||
// bit set (=1) means warning
|
||||
if (w & (1 << 7)) add("Overall warning");
|
||||
if (w & (1 << 3)) add("Pump current");
|
||||
if (w & (1 << 2)) add("Device temperature");
|
||||
if (w & (1 << 1)) add("Input LOS");
|
||||
if (w & (1 << 0)) add("Output LOS");
|
||||
// bit clear (=0) means warning
|
||||
if (!(w & (1 << 6))) add("Pump is OFF");
|
||||
if (!(w & (1 << 5))) add("TEC current");
|
||||
if (!(w & (1 << 4))) add("Pump temperature");
|
||||
if (out.length() == 0) out = "none";
|
||||
return out;
|
||||
}
|
||||
|
||||
void Agiltron::rawProbe(uint8_t cmd, const uint8_t *data, uint8_t len) {
|
||||
uint8_t r[64];
|
||||
size_t n = transact(cmd, data, len, r, sizeof(r));
|
||||
Serial.printf("[agiltron] probe cmd 0x%02x -> %u bytes:\n", cmd, (unsigned)n);
|
||||
Serial.print(" hex:");
|
||||
for (size_t i = 0; i < n; i++) Serial.printf(" %02x", r[i]);
|
||||
Serial.println();
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Agiltron M511 EYDFA driver --- binary 0x55AA packet protocol.
|
||||
//
|
||||
// Ported from the lab's edfa.py (Agiltron EDFA/edfa.py). Packet layout:
|
||||
// 0x55 0xAA | dev_id[4] | cmd | len | data[len] | checksum
|
||||
// checksum = ((0xFF - (sum(bytes from dev_id..end of data) & 0xFF)) + 1) & 0xFF
|
||||
// Reply starts 0xAA 0x55, with the command id echoed at byte[6]. 115200 8N1.
|
||||
//
|
||||
// Scalings (from edfa.py, corrected against the docs):
|
||||
// optical power = signed int16 / 100 (dBm)
|
||||
// pump currents = raw mA
|
||||
// temps, preamp/TEC currents = / 10
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
struct AmpStatus {
|
||||
bool valid = false;
|
||||
float module_temp_c;
|
||||
float preamp_temp_c;
|
||||
float preamp_current_ma;
|
||||
float tec_current_ma;
|
||||
int pump1_current_ma;
|
||||
int pump2_current_ma;
|
||||
float input_power_dbm;
|
||||
float preamp_power_dbm;
|
||||
float output1_power_dbm;
|
||||
float output2_power_dbm;
|
||||
uint8_t warn_byte;
|
||||
};
|
||||
|
||||
class Agiltron {
|
||||
public:
|
||||
static const uint8_t ACC = 0x01; // constant current
|
||||
static const uint8_t APC = 0x00; // constant power
|
||||
static const int MAX_PUMP_CURRENT_MA = 8000;
|
||||
|
||||
Agiltron(HardwareSerial &serial, int8_t rxPin, int8_t txPin, uint32_t baud,
|
||||
const uint8_t devId[4])
|
||||
: _ser(serial), _rx(rxPin), _tx(txPin), _baud(baud) {
|
||||
memcpy(_devId, devId, 4);
|
||||
}
|
||||
|
||||
void begin() { _ser.begin(_baud, SERIAL_8N1, _rx, _tx); }
|
||||
|
||||
bool linkOk() const { return _linkOk; }
|
||||
|
||||
// Queries
|
||||
AmpStatus getStatus();
|
||||
String getSn();
|
||||
String getPn();
|
||||
// Authoritative pump-enable from get_settings (0x2E): byte[8:9]==0 => enabled.
|
||||
// Returns -1 on no/invalid reply, 0 = disabled, 1 = enabled.
|
||||
int getPumpEnabled();
|
||||
|
||||
// Control (return true if the device ACKed)
|
||||
bool pumpOn() { return cmdAck(0x20, (const uint8_t[]){0x00,0x00}, 2); }
|
||||
bool pumpOff() { return cmdAck(0x20, (const uint8_t[]){0x00,0x01}, 2); }
|
||||
bool setPump1Mode(uint8_t mode) { uint8_t d[2]={0x00,mode}; return cmdAck(0x21,d,2); }
|
||||
bool setPump1Current(int ma);
|
||||
|
||||
// Decode the warning byte into a comma-separated list (matches edfa.py).
|
||||
static String warningsString(uint8_t warnByte);
|
||||
|
||||
// Dump raw reply bytes for a command to the USB console (first-wiring aid).
|
||||
void rawProbe(uint8_t cmd, const uint8_t *data, uint8_t len);
|
||||
|
||||
private:
|
||||
HardwareSerial &_ser;
|
||||
int8_t _rx, _tx;
|
||||
uint32_t _baud;
|
||||
uint8_t _devId[4];
|
||||
bool _linkOk = false;
|
||||
|
||||
uint8_t checksum(const uint8_t *p, size_t n);
|
||||
size_t buildPacket(uint8_t *out, uint8_t cmd, const uint8_t *data, uint8_t len);
|
||||
// Send cmd, read reply into buf (max bufLen). Returns bytes read.
|
||||
size_t transact(uint8_t cmd, const uint8_t *data, uint8_t len,
|
||||
uint8_t *buf, size_t bufLen, uint32_t timeoutMs = 300);
|
||||
bool validate(const uint8_t *buf, size_t n, uint8_t expectCmd);
|
||||
bool cmdAck(uint8_t cmd, const uint8_t *data, uint8_t len);
|
||||
|
||||
static uint16_t u16(uint8_t msb, uint8_t lsb) { return (uint16_t)msb * 256 + lsb; }
|
||||
static int16_t s16(uint8_t msb, uint8_t lsb) { return (int16_t)((msb << 8) | lsb); }
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "koheron.h"
|
||||
|
||||
// Read everything the device sends until there's an ~80 ms idle gap (end of
|
||||
// reply) or the overall timeout elapses.
|
||||
String Koheron::readRaw(uint32_t timeoutMs) {
|
||||
String buf;
|
||||
uint32_t start = millis();
|
||||
uint32_t lastByte = start;
|
||||
const uint32_t IDLE_GAP = 80;
|
||||
while (millis() - start < timeoutMs) {
|
||||
while (_ser.available()) {
|
||||
buf += (char)_ser.read();
|
||||
lastByte = millis();
|
||||
}
|
||||
if (buf.length() && (millis() - lastByte) > IDLE_GAP) break;
|
||||
delay(2);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
String Koheron::query(const char *cmd, uint32_t timeoutMs) {
|
||||
while (_ser.available()) _ser.read(); // flush stale input
|
||||
_ser.print(cmd);
|
||||
_ser.print("\r\n");
|
||||
|
||||
String raw = readRaw(timeoutMs);
|
||||
if (raw.length() == 0) {
|
||||
_linkOk = false;
|
||||
return String();
|
||||
}
|
||||
_linkOk = true;
|
||||
|
||||
// Split into lines; drop ">>" prompts, the echoed command, and blanks.
|
||||
// Return the last surviving line (the value).
|
||||
String value;
|
||||
int start = 0;
|
||||
while (start < (int)raw.length()) {
|
||||
int nl = raw.indexOf('\n', start);
|
||||
String line = (nl < 0) ? raw.substring(start) : raw.substring(start, nl);
|
||||
line.replace(">", ""); // strip prompt chars
|
||||
line.trim(); // drop \r and surrounding whitespace
|
||||
if (line.length() && line != cmd) value = line;
|
||||
if (nl < 0) break;
|
||||
start = nl + 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void Koheron::rawProbe(const char *cmd, uint32_t timeoutMs) {
|
||||
while (_ser.available()) _ser.read();
|
||||
_ser.print(cmd);
|
||||
_ser.print("\r\n");
|
||||
String raw = readRaw(timeoutMs);
|
||||
Serial.printf("[koheron] probe \"%s\" -> %u bytes:\n", cmd, raw.length());
|
||||
Serial.print(" hex:");
|
||||
for (size_t i = 0; i < raw.length(); i++) Serial.printf(" %02x", (uint8_t)raw[i]);
|
||||
Serial.println();
|
||||
Serial.print(" asc: ");
|
||||
for (size_t i = 0; i < raw.length(); i++) {
|
||||
char c = raw[i];
|
||||
Serial.print((c >= 32 && c < 127) ? c : '.');
|
||||
}
|
||||
Serial.println();
|
||||
Serial.printf(" parsed value: \"%s\"\n", query(cmd, timeoutMs).c_str());
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Koheron CTL200 driver --- ASCII serial protocol.
|
||||
//
|
||||
// The CTL200 is a line-oriented terminal: send "<cmd>\r\n", it replies with
|
||||
// the value (and may echo the command and a ">>" prompt). We strip the echo
|
||||
// and prompt and return the value. 115200 8N1.
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
class Koheron {
|
||||
public:
|
||||
Koheron(HardwareSerial &serial, int8_t rxPin, int8_t txPin, uint32_t baud)
|
||||
: _ser(serial), _rx(rxPin), _tx(txPin), _baud(baud) {}
|
||||
|
||||
void begin() { _ser.begin(_baud, SERIAL_8N1, _rx, _tx); }
|
||||
|
||||
// Send a command, return the cleaned value line (echo + ">>" removed).
|
||||
// Empty string means no/garbled response.
|
||||
String query(const char *cmd, uint32_t timeoutMs = 250);
|
||||
|
||||
// Dump the raw bytes of a reply (hex + ascii) to the USB console, so the
|
||||
// exact framing (echo? prompt placement?) can be confirmed on first wiring.
|
||||
void rawProbe(const char *cmd, uint32_t timeoutMs = 500);
|
||||
|
||||
bool linkOk() const { return _linkOk; }
|
||||
|
||||
private:
|
||||
HardwareSerial &_ser;
|
||||
int8_t _rx, _tx;
|
||||
uint32_t _baud;
|
||||
bool _linkOk = false;
|
||||
|
||||
String readRaw(uint32_t timeoutMs); // read until idle gap or timeout
|
||||
};
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
// Koheron CTL200 + Agiltron M511 -> WiFi/MQTT bridge --- ESP32 firmware.
|
||||
//
|
||||
// INCREMENT 3: seed (Koheron CTL200) read-only telemetry on UART2, published
|
||||
// under lab/laser/butterfly_edfa_rack/seed/... with HA discovery. Builds on
|
||||
// increment 2 (WiFi + MQTT + diagnostics). Amp (Agiltron) and control come
|
||||
// in increments 4-5.
|
||||
//
|
||||
// On boot the firmware raw-probes the Koheron `version` command and dumps the
|
||||
// reply (hex+ascii) to the USB console, so the exact framing can be confirmed
|
||||
// the first time the CTL200 is wired to GPIO16/17. See include/config.h.
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include "config.h"
|
||||
#include "koheron.h"
|
||||
#include "agiltron.h"
|
||||
|
||||
static WiFiClient net;
|
||||
static PubSubClient mqtt(net);
|
||||
static Koheron koheron(Serial2, KOHERON_RX_PIN, KOHERON_TX_PIN, KOHERON_BAUD);
|
||||
static const uint8_t kAmpDevId[4] = {AGILTRON_DEV_ID};
|
||||
static Agiltron agiltron(Serial1, AGILTRON_RX_PIN, AGILTRON_TX_PIN, AGILTRON_BAUD, kAmpDevId);
|
||||
|
||||
// Probe each instrument link once per boot to reveal its framing.
|
||||
#define KOHERON_RAW_PROBE 1
|
||||
#define AGILTRON_PROBE 1
|
||||
|
||||
// ── HA discovery helpers ────────────────────────────────────────────
|
||||
|
||||
static String deviceBlock() {
|
||||
return String("\"device\":{")
|
||||
+ "\"identifiers\":[\"" DEVICE_NAME "\"],"
|
||||
+ "\"name\":\"" DEVICE_FRIENDLY "\","
|
||||
+ "\"manufacturer\":\"Koheron + Agiltron\","
|
||||
+ "\"model\":\"CTL200 seed + M511 EYDFA\","
|
||||
+ "\"sw_version\":\"" __DATE__ " " __TIME__ "\"}";
|
||||
}
|
||||
|
||||
// Common availability + device tail shared by every discovery payload.
|
||||
static String commonTail(const char *objectId) {
|
||||
return String("\"availability_topic\":\"" TOPIC_AVAIL "\",")
|
||||
+ "\"payload_available\":\"" AVAIL_ONLINE "\","
|
||||
+ "\"payload_not_available\":\"" AVAIL_OFFLINE "\","
|
||||
+ "\"unique_id\":\"" DEVICE_NAME "_" + objectId + "\","
|
||||
+ deviceBlock() + "}";
|
||||
}
|
||||
|
||||
static void publishConfig(const char *component, const char *objectId, const String &payload) {
|
||||
String topic = String(DISCOVERY_PREFIX) + "/" + component + "/" + DEVICE_NAME + "/" + objectId + "/config";
|
||||
if (!mqtt.publish(topic.c_str(), (const uint8_t *)payload.c_str(), payload.length(), true))
|
||||
Serial.printf(" ! discovery publish failed: %s (len %u)\n", objectId, payload.length());
|
||||
}
|
||||
|
||||
// A numeric/text sensor. stateSub is the topic suffix under TOPIC_BASE.
|
||||
static void sensorDiscovery(const char *objectId, const char *name, const char *stateSub,
|
||||
const char *unit, const char *devClass, const char *icon,
|
||||
bool diagnostic) {
|
||||
String p = String("{\"name\":\"") + name + "\","
|
||||
+ "\"state_topic\":\"" TOPIC_BASE "/" + stateSub + "\",";
|
||||
if (unit && *unit) p += String("\"unit_of_measurement\":\"") + unit + "\",";
|
||||
if (devClass && *devClass) p += String("\"device_class\":\"") + devClass + "\",";
|
||||
if (icon && *icon) p += String("\"icon\":\"") + icon + "\",";
|
||||
if (diagnostic) p += "\"entity_category\":\"diagnostic\",";
|
||||
p += commonTail(objectId);
|
||||
publishConfig("sensor", objectId, p);
|
||||
}
|
||||
|
||||
static void binaryDiscovery(const char *objectId, const char *name, const char *stateSub,
|
||||
const char *payloadOn, const char *payloadOff,
|
||||
const char *devClass, bool diagnostic) {
|
||||
String p = String("{\"name\":\"") + name + "\","
|
||||
+ "\"state_topic\":\"" TOPIC_BASE "/" + stateSub + "\","
|
||||
+ "\"payload_on\":\"" + payloadOn + "\","
|
||||
+ "\"payload_off\":\"" + payloadOff + "\",";
|
||||
if (devClass && *devClass) p += String("\"device_class\":\"") + devClass + "\",";
|
||||
if (diagnostic) p += "\"entity_category\":\"diagnostic\",";
|
||||
p += commonTail(objectId);
|
||||
publishConfig("binary_sensor", objectId, p);
|
||||
}
|
||||
|
||||
// A switch with state + command topics.
|
||||
static void switchDiscovery(const char *objectId, const char *name, const char *stateSub,
|
||||
const char *cmdSub, const char *payloadOn, const char *payloadOff,
|
||||
const char *icon) {
|
||||
String p = String("{\"name\":\"") + name + "\","
|
||||
+ "\"state_topic\":\"" TOPIC_BASE "/" + stateSub + "\","
|
||||
+ "\"command_topic\":\"" TOPIC_CMD "/" + cmdSub + "\","
|
||||
+ "\"payload_on\":\"" + payloadOn + "\",\"payload_off\":\"" + payloadOff + "\","
|
||||
+ "\"state_on\":\"" + payloadOn + "\",\"state_off\":\"" + payloadOff + "\",";
|
||||
if (icon && *icon) p += String("\"icon\":\"") + icon + "\",";
|
||||
p += commonTail(objectId);
|
||||
publishConfig("switch", objectId, p);
|
||||
}
|
||||
|
||||
// A number (slider) with state + command topics.
|
||||
static void numberDiscovery(const char *objectId, const char *name, const char *stateSub,
|
||||
const char *cmdSub, float vmin, float vmax, float step,
|
||||
const char *unit, const char *icon) {
|
||||
String p = String("{\"name\":\"") + name + "\","
|
||||
+ "\"state_topic\":\"" TOPIC_BASE "/" + stateSub + "\","
|
||||
+ "\"command_topic\":\"" TOPIC_CMD "/" + cmdSub + "\","
|
||||
+ "\"min\":" + String(vmin, 0) + ",\"max\":" + String(vmax, 0)
|
||||
+ ",\"step\":" + String(step, 0) + ",\"mode\":\"box\",";
|
||||
if (unit && *unit) p += String("\"unit_of_measurement\":\"") + unit + "\",";
|
||||
if (icon && *icon) p += String("\"icon\":\"") + icon + "\",";
|
||||
p += commonTail(objectId);
|
||||
publishConfig("number", objectId, p);
|
||||
}
|
||||
|
||||
static void publishDiscovery() {
|
||||
Serial.println("Publishing HA discovery...");
|
||||
// Diagnostics (ESP / network)
|
||||
sensorDiscovery("uptime", "Uptime", "diag/uptime", "s", "duration", nullptr, true);
|
||||
sensorDiscovery("rssi", "WiFi Signal", "diag/rssi", "dBm", "signal_strength", nullptr, true);
|
||||
sensorDiscovery("ip", "IP Address", "diag/ip", nullptr, nullptr, "mdi:ip-network", true);
|
||||
|
||||
// Seed (Koheron CTL200)
|
||||
binaryDiscovery("seed_link", "Seed Link", "seed/link", AVAIL_ONLINE, AVAIL_OFFLINE, "connectivity", true);
|
||||
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_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);
|
||||
sensorDiscovery("seed_photodiode", "Seed Photodiode Current", "seed/photodiode_ma", "mA", nullptr, "mdi:current-dc", false);
|
||||
sensorDiscovery("seed_error", "Seed Error Code", "seed/error", nullptr, nullptr, "mdi:alert-circle", true);
|
||||
|
||||
// Seed controls
|
||||
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");
|
||||
|
||||
// Amp (Agiltron M511)
|
||||
binaryDiscovery("amp_link", "Amp Link", "amp/link", AVAIL_ONLINE, AVAIL_OFFLINE, "connectivity", true);
|
||||
sensorDiscovery("amp_module_temp", "Amp Module Temperature", "amp/module_temp_c", "°C", "temperature", nullptr, false);
|
||||
sensorDiscovery("amp_preamp_temp", "Amp Pre-amp Temperature", "amp/preamp_temp_c", "°C", "temperature", nullptr, true);
|
||||
sensorDiscovery("amp_preamp_current", "Amp Pre-amp Current", "amp/preamp_current_ma", "mA", "current", nullptr, true);
|
||||
sensorDiscovery("amp_tec_current", "Amp TEC Current", "amp/tec_current_ma", "mA", "current", nullptr, true);
|
||||
sensorDiscovery("amp_pump1_current", "Amp Pump-1 Current", "amp/pump1_current_ma", "mA", "current", "mdi:current-dc", false);
|
||||
sensorDiscovery("amp_input_power", "Amp Input Power", "amp/input_power_dbm", "dBm", nullptr, "mdi:arrow-right-bold", false);
|
||||
sensorDiscovery("amp_preamp_power", "Amp Pre-amp Power", "amp/preamp_power_dbm", "dBm", nullptr, "mdi:arrow-right-bold", true);
|
||||
sensorDiscovery("amp_output_power", "Output Power", "amp/output1_power_dbm", "dBm", nullptr, "mdi:arrow-left-bold", false);
|
||||
sensorDiscovery("amp_warnings", "Amp Warnings", "amp/warnings", nullptr, nullptr, "mdi:alert", true);
|
||||
|
||||
// Amp controls
|
||||
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",
|
||||
0, PUMP_CURRENT_MAX_MA, 10, "mA", "mdi:current-dc");
|
||||
}
|
||||
|
||||
// ── State publishing ────────────────────────────────────────────────
|
||||
|
||||
static void publishDiag() {
|
||||
String base = String(TOPIC_BASE) + "/diag/";
|
||||
mqtt.publish((base + "uptime").c_str(), String(millis() / 1000).c_str(), true);
|
||||
mqtt.publish((base + "rssi").c_str(), String(WiFi.RSSI()).c_str(), true);
|
||||
mqtt.publish((base + "ip").c_str(), WiFi.localIP().toString().c_str(), true);
|
||||
}
|
||||
|
||||
// Each seed value is the raw string the CTL200 returns (already numeric text).
|
||||
struct SeedPoint { const char *cmd; const char *sub; };
|
||||
static const SeedPoint seedPoints[] = {
|
||||
{"lason", "seed/laser_enabled"},
|
||||
{"ilaser", "seed/current_ma"},
|
||||
{"vlaser", "seed/voltage_v"},
|
||||
{"rtset", "seed/rt_setpoint_ohm"},
|
||||
{"rtact", "seed/rt_actual_ohm"},
|
||||
{"itec", "seed/tec_current_a"},
|
||||
{"vtec", "seed/tec_voltage_v"},
|
||||
{"tboard", "seed/board_temp_c"},
|
||||
{"iphd", "seed/photodiode_ma"},
|
||||
{"err", "seed/error"},
|
||||
};
|
||||
|
||||
static void publishSeed() {
|
||||
bool any = false;
|
||||
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);
|
||||
any = true;
|
||||
}
|
||||
mqtt.loop(); // keep MQTT serviced between (potentially slow) UART round-trips
|
||||
}
|
||||
mqtt.publish(TOPIC_BASE "/seed/link", any ? AVAIL_ONLINE : AVAIL_OFFLINE, true);
|
||||
}
|
||||
|
||||
static void pub(const char *sub, const String &v) {
|
||||
mqtt.publish((String(TOPIC_BASE) + "/" + sub).c_str(), v.c_str(), true);
|
||||
}
|
||||
|
||||
static void publishAmp() {
|
||||
AmpStatus s = agiltron.getStatus();
|
||||
mqtt.publish(TOPIC_BASE "/amp/link", s.valid ? AVAIL_ONLINE : AVAIL_OFFLINE, true);
|
||||
if (!s.valid) return;
|
||||
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_current_ma", String(s.preamp_current_ma, 1));
|
||||
pub("amp/tec_current_ma", String(s.tec_current_ma, 1));
|
||||
pub("amp/pump1_current_ma", String(s.pump1_current_ma));
|
||||
pub("amp/input_power_dbm", String(s.input_power_dbm, 2));
|
||||
pub("amp/preamp_power_dbm", String(s.preamp_power_dbm, 2));
|
||||
pub("amp/output1_power_dbm", String(s.output1_power_dbm, 2));
|
||||
pub("amp/output2_power_dbm", String(s.output2_power_dbm, 2));
|
||||
pub("amp/warnings", Agiltron::warningsString(s.warn_byte));
|
||||
int pe = agiltron.getPumpEnabled(); // authoritative, from get_settings
|
||||
if (pe >= 0) pub("amp/pump_enabled", pe ? "1" : "0");
|
||||
}
|
||||
|
||||
// ── Command handling ────────────────────────────────────────────────
|
||||
|
||||
static void onMqtt(char *topic, byte *payload, unsigned int len) {
|
||||
String t(topic), p;
|
||||
for (unsigned int i = 0; i < len; i++) p += (char)payload[i];
|
||||
p.trim();
|
||||
Serial.printf("[cmd] %s = %s\n", topic, p.c_str());
|
||||
|
||||
if (t == TOPIC_CMD "/seed/laser") {
|
||||
bool on = (p == "1" || p.equalsIgnoreCase("on") || p.equalsIgnoreCase(AVAIL_ONLINE));
|
||||
koheron.query(on ? "lason 1" : "lason 0");
|
||||
pub("seed/laser_enabled", on ? "1" : "0");
|
||||
|
||||
} else if (t == TOPIC_CMD "/seed/current_ma") {
|
||||
float ma = p.toFloat();
|
||||
if (ma < 0) ma = 0;
|
||||
if (ma > SEED_CURRENT_MAX_MA) ma = SEED_CURRENT_MAX_MA; // firmware clamp
|
||||
koheron.query((String("ilaser ") + String(ma, 1)).c_str());
|
||||
pub("seed/current_ma", String(ma, 3));
|
||||
|
||||
} 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();
|
||||
Serial.printf("[cmd] amp pump %s -> %s\n", on ? "ON" : "OFF", ok ? "ack" : "NO-ACK");
|
||||
pub("amp/pump_enabled", on ? "1" : "0");
|
||||
|
||||
} else if (t == TOPIC_CMD "/amp/pump1_current_ma") {
|
||||
int ma = (int)p.toFloat();
|
||||
if (ma < 0) ma = 0;
|
||||
if (ma > PUMP_CURRENT_MAX_MA) ma = PUMP_CURRENT_MAX_MA; // firmware clamp
|
||||
agiltron.setPump1Mode(Agiltron::ACC); // current-control mode (lab note: avoid APC steps)
|
||||
bool ok = agiltron.setPump1Current(ma);
|
||||
Serial.printf("[cmd] amp pump1 = %d mA -> %s\n", ma, ok ? "ack" : "NO-ACK");
|
||||
pub("amp/pump1_set_ma", String(ma));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Connectivity ────────────────────────────────────────────────────
|
||||
|
||||
static void connectWiFi() {
|
||||
Serial.printf("WiFi: connecting to \"%s\"", WIFI_SSID);
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setHostname(DEVICE_NAME);
|
||||
#ifdef USE_STATIC_IP
|
||||
IPAddress ip(STATIC_IP_OCTETS), gw(GATEWAY_OCTETS), sn(SUBNET_OCTETS), dns(DNS_OCTETS);
|
||||
if (!WiFi.config(ip, gw, sn, dns)) Serial.print(" [static cfg failed]");
|
||||
#endif
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); }
|
||||
Serial.printf(" ok, IP %s, RSSI %d dBm\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
||||
}
|
||||
|
||||
static bool connectMQTT() {
|
||||
Serial.printf("MQTT: connecting to %s:%d ... ", MQTT_BROKER, MQTT_PORT);
|
||||
bool ok = mqtt.connect(DEVICE_NAME, MQTT_USER, MQTT_PASSWORD,
|
||||
TOPIC_AVAIL, 0, true, AVAIL_OFFLINE);
|
||||
if (ok) {
|
||||
Serial.println("ok");
|
||||
mqtt.publish(TOPIC_AVAIL, AVAIL_ONLINE, true);
|
||||
mqtt.subscribe(TOPIC_CMD "/#");
|
||||
publishDiscovery();
|
||||
publishDiag();
|
||||
} else {
|
||||
Serial.printf("failed, rc=%d\n", mqtt.state());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ── Arduino entry points ────────────────────────────────────────────
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(200);
|
||||
Serial.println("\n=== Butterfly EDFA Rack bridge — increment 3 (seed telemetry) ===");
|
||||
Serial.printf("Device: %s (%s)\n", DEVICE_FRIENDLY, DEVICE_NAME);
|
||||
|
||||
koheron.begin();
|
||||
agiltron.begin();
|
||||
#if KOHERON_RAW_PROBE
|
||||
Serial.printf("Seed probe on UART%d (RX=%d, TX=%d):\n",
|
||||
KOHERON_UART_NUM, KOHERON_RX_PIN, KOHERON_TX_PIN);
|
||||
koheron.rawProbe("version");
|
||||
if (!koheron.linkOk())
|
||||
Serial.println(" (no reply — Koheron not wired yet? RX/TX swapped? GND?)");
|
||||
#endif
|
||||
#if AGILTRON_PROBE
|
||||
Serial.printf("Amp probe on UART%d (RX=%d, TX=%d):\n",
|
||||
AGILTRON_UART_NUM, AGILTRON_RX_PIN, AGILTRON_TX_PIN);
|
||||
String sn = agiltron.getSn(), pn = agiltron.getPn();
|
||||
Serial.printf(" SN=\"%s\" PN=\"%s\"\n", sn.c_str(), pn.c_str());
|
||||
if (sn.length() == 0)
|
||||
Serial.println(" (no reply — Agiltron not wired? RX/TX swapped? wrong dev ID? GND?)");
|
||||
#endif
|
||||
|
||||
connectWiFi();
|
||||
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
|
||||
mqtt.setBufferSize(MQTT_BUFFER_SIZE);
|
||||
mqtt.setCallback(onMqtt);
|
||||
connectMQTT();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (WiFi.status() != WL_CONNECTED) connectWiFi();
|
||||
|
||||
if (!mqtt.connected()) {
|
||||
static uint32_t lastTry = 0;
|
||||
if (millis() - lastTry >= MQTT_RECONNECT_MS) { lastTry = millis(); connectMQTT(); }
|
||||
} else {
|
||||
mqtt.loop();
|
||||
static uint32_t lastPub = 0;
|
||||
if (millis() - lastPub >= PUBLISH_INTERVAL_MS) {
|
||||
lastPub = millis();
|
||||
publishDiag();
|
||||
publishSeed();
|
||||
publishAmp();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user