Initial commit: Sapphire ESP32 Laser Controller with README and .gitignore
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
build/
|
||||||
|
.vscode/
|
||||||
|
*.bin
|
||||||
|
*.elf
|
||||||
|
*.map
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Sapphire ESP32 Laser Controller
|
||||||
|
|
||||||
|
A robust, web-based controller for Coherent Sapphire lasers built on the ESP32 platform. This project provides a real-time telemetry dashboard and control interface for multiple Sapphire lasers via RS232 serial communication.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multi-Port Support**: Control up to 3 Sapphire lasers simultaneously (2 implemented, 1 planned).
|
||||||
|
- **Real-Time Telemetry**: Monitor power, current, temperatures (base, diode, controller), and operational status via WebSockets.
|
||||||
|
- **Interactive Control**: Set laser power levels and send raw serial commands directly from the web interface or USB serial.
|
||||||
|
- **Automatic Discovery**: Smart state machine handles connection, initialization, and fault recovery for each laser port.
|
||||||
|
- **OTA Updates**: Seamless firmware updates over-the-air using [ElegantOTA](https://github.com/ayushsharma82/ElegantOTA).
|
||||||
|
- **Responsive Web Dashboard**: Built-in HTML/CSS interface served directly from the ESP32.
|
||||||
|
|
||||||
|
## Hardware Requirements
|
||||||
|
|
||||||
|
- **ESP32 Development Board**
|
||||||
|
- **RS232 to TTL Converters**: One for each laser port (Sapphire lasers use RS232 levels).
|
||||||
|
- **Coherent Sapphire Laser(s)**
|
||||||
|
|
||||||
|
## Serial Pinout
|
||||||
|
|
||||||
|
The ESP32 communicates with the lasers at **19200 baud (8N1)**.
|
||||||
|
|
||||||
|
| Port | RX Pin | TX Pin | UART Peripheral | Notes |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| **Serial 1** | GPIO 26 | GPIO 27 | UART2 | Primary laser port |
|
||||||
|
| **Serial 2** | GPIO 32 | GPIO 33 | UART1 | Secondary laser port |
|
||||||
|
| **Serial 3** | GPIO 18 | GPIO 19 | UART1 (Remapped) | Optional/Planned |
|
||||||
|
|
||||||
|
*Note: GPIO 1 is used for Debug/USB Serial (115200 baud).*
|
||||||
|
|
||||||
|
## Software Dependencies
|
||||||
|
|
||||||
|
Ensure you have the following libraries installed in your Arduino IDE or PlatformIO environment:
|
||||||
|
|
||||||
|
- `ESPAsyncWebServer`
|
||||||
|
- `AsyncTCP`
|
||||||
|
- `ElegantOTA`
|
||||||
|
- `ArduinoJson`
|
||||||
|
- `WiFi` (Standard ESP32 library)
|
||||||
|
|
||||||
|
## Installation & Setup
|
||||||
|
|
||||||
|
1. **Clone the Repository**:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
```
|
||||||
|
2. **Configure WiFi**:
|
||||||
|
Open `Sapphire_ESP32.ino` and update the WiFi credentials:
|
||||||
|
```cpp
|
||||||
|
const char* ssid = "Your_SSID";
|
||||||
|
const char* password = "Your_Password";
|
||||||
|
```
|
||||||
|
3. **Flash the ESP32**:
|
||||||
|
Upload the sketch using the Arduino IDE or your preferred tool.
|
||||||
|
4. **Connect Hardware**:
|
||||||
|
Connect the RS232 converters to the designated GPIO pins and the Sapphire lasers.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
Once connected to WiFi, the ESP32 will print its IP address to the Serial Monitor. Navigate to this IP in your web browser to access the **Sapphire Controller Dashboard**.
|
||||||
|
|
||||||
|
- **Main Dashboard**: View telemetry and control power.
|
||||||
|
- **Firmware Update**: Access `http://<ip-address>/update` to upload new firmware binaries.
|
||||||
|
|
||||||
|
### Serial Console
|
||||||
|
You can also interact with Port 1 directly via the USB Serial Monitor (115200 baud). Any text entered will be sent as a command to the laser on Port 1.
|
||||||
|
|
||||||
|
## Protocol Support
|
||||||
|
|
||||||
|
The driver implements the Sapphire serial protocol, supporting commands such as:
|
||||||
|
- `?P`: Read output power
|
||||||
|
- `?C`: Read laser current
|
||||||
|
- `?STA`: Read system status
|
||||||
|
- `P=XXX.X`: Set laser power in mW
|
||||||
|
- `?FF`: Read fault flags
|
||||||
|
|
||||||
|
## Firmware Versioning
|
||||||
|
The current firmware version is defined in the source code. Please increment `FIRMWARE_VERSION` in `Sapphire_ESP32.ino` when making changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Developed for high-precision laser control applications.*
|
||||||
@@ -0,0 +1,911 @@
|
|||||||
|
#include <WiFi.h>
|
||||||
|
#include <AsyncTCP.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <ElegantOTA.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
// WiFi Credentials
|
||||||
|
const char* ssid = "Jelly";
|
||||||
|
const char* password = "jellydoodle";
|
||||||
|
|
||||||
|
// Web Server & WebSocket
|
||||||
|
AsyncWebServer server(80);
|
||||||
|
AsyncWebSocket ws("/ws");
|
||||||
|
|
||||||
|
// Firmware Version
|
||||||
|
//NOTE TO AI AGENTS: Increment this version upon any changes to the file
|
||||||
|
const char* FIRMWARE_VERSION = "1.0.26";
|
||||||
|
|
||||||
|
// Serial Pin Definitions
|
||||||
|
// Port 1: Uses UART2 (Serial2) - GPIO 26/27
|
||||||
|
const int SERIAL1_RX = 26;
|
||||||
|
const int SERIAL1_TX = 27;
|
||||||
|
// Port 2: Uses UART1 (Serial1) - GPIO 32/33
|
||||||
|
const int SERIAL2_RX = 32;
|
||||||
|
const int SERIAL2_TX = 33;
|
||||||
|
// Port 3: Uses remapped UART1 or software serial - GPIO 18/19
|
||||||
|
const int SERIAL3_RX = 18;
|
||||||
|
const int SERIAL3_TX = 19;
|
||||||
|
// Note: ESP32 has 3 UARTs. UART0 is debug. We remap UART1 and UART2 for laser ports.
|
||||||
|
// Port 3 uses HardwareSerial with custom pins (can share UART with care or use SoftwareSerial).
|
||||||
|
|
||||||
|
const unsigned long MIN_POLL_INTERVAL_MS = 1000; // Device recommendation: do not poll faster than 1 Hz per query
|
||||||
|
unsigned long pollingRateMs = 1000;
|
||||||
|
|
||||||
|
|
||||||
|
// HTML Index Page
|
||||||
|
const char index_html[] PROGMEM = R"rawliteral(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Sapphire Controller</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; background: #f0f0f0; text-align: center; padding: 50px; }
|
||||||
|
.btn { text-decoration: none; background: #3b82f6; color: #fff; padding: 15px 25px; border-radius: 4px; display: inline-block; margin: 10px; }
|
||||||
|
.btn:hover { background: #2563eb; }
|
||||||
|
.pinout { margin-top: 30px; }
|
||||||
|
table { margin: 0 auto; border-collapse: collapse; background: #fff; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
||||||
|
table, th, td { border: 1px solid #ccc; padding: 12px; }
|
||||||
|
th { background: #e0e0e0; }
|
||||||
|
td:first-child { text-align: left; font-weight: bold; }
|
||||||
|
.version { margin-top: 0.5rem; color: #666; font-size: 0.9rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Sapphire Laser Controller</h1>
|
||||||
|
<p class="version">Firmware v%VERSION%</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/update" class="btn">Firmware Update</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pinout">
|
||||||
|
<h3>Serial Communication Pinout</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>Port</th><th>RX Pin</th><th>TX Pin</th><th>Notes</th></tr>
|
||||||
|
<tr><td>Serial 1</td><td>GPIO 26</td><td>GPIO 27</td><td>Standard UART2</td></tr>
|
||||||
|
<tr><td>Serial 2</td><td>GPIO 32</td><td>GPIO 33</td><td>Safe GPIO (UART1)</td></tr>
|
||||||
|
<tr><td>Serial 3</td><td>GPIO 18</td><td>GPIO 19</td><td>Safe GPIO (SPI pins available if SPI unused)</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawliteral";
|
||||||
|
|
||||||
|
String processor(const String& var) {
|
||||||
|
if(var == "VERSION") return FIRMWARE_VERSION;
|
||||||
|
return String();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper: Broadcast Response ---
|
||||||
|
void broadcastSapphireResponse(int port, String response) {
|
||||||
|
// 1. Print to USB Serial
|
||||||
|
Serial.printf("[%d] < %s\n", port, response.c_str());
|
||||||
|
|
||||||
|
// 2. Send to WebSocket
|
||||||
|
StaticJsonDocument<192> doc;
|
||||||
|
doc["type"] = "serialRx";
|
||||||
|
doc["port"] = port;
|
||||||
|
doc["data"] = response;
|
||||||
|
|
||||||
|
String jsonString;
|
||||||
|
serializeJson(doc, jsonString);
|
||||||
|
ws.textAll(jsonString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sapphire Driver ---
|
||||||
|
class SapphireDriver {
|
||||||
|
public:
|
||||||
|
enum State {
|
||||||
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
READY,
|
||||||
|
FAULT
|
||||||
|
};
|
||||||
|
|
||||||
|
enum CommandType {
|
||||||
|
CMD_NONE,
|
||||||
|
CMD_POWER, // ?P
|
||||||
|
CMD_CURRENT, // ?C
|
||||||
|
CMD_BASE_TEMP, // ?BT
|
||||||
|
CMD_DIODE_TEMP, // ?DT
|
||||||
|
CMD_CTRL_TEMP, // ?PST
|
||||||
|
CMD_STATUS, // ?STA
|
||||||
|
CMD_SET_TEMP, // ?DST
|
||||||
|
CMD_HOURS, // ?HH
|
||||||
|
CMD_CTRL_HOURS, // ?PSH
|
||||||
|
CMD_WAVELENGTH, // ?WAVE
|
||||||
|
CMD_MIN_POWER, // ?MINLP
|
||||||
|
CMD_MAX_POWER, // ?MAXLP
|
||||||
|
CMD_RESERVE, // ?DRC
|
||||||
|
CMD_FAULT_FLAG, // ?FF
|
||||||
|
CMD_SET_POWER, // P=...
|
||||||
|
CMD_PROMPT, // >
|
||||||
|
CMD_CONFIG_ECHO,
|
||||||
|
CMD_CONFIG_PROMPT
|
||||||
|
};
|
||||||
|
|
||||||
|
SapphireDriver(int id, HardwareSerial& serial, int rxPin, int txPin)
|
||||||
|
: _id(id), _serial(serial), _rxPin(rxPin), _txPin(txPin) {}
|
||||||
|
|
||||||
|
void begin() {
|
||||||
|
beginSerial();
|
||||||
|
_state = DISCONNECTED;
|
||||||
|
_lastActionMs = millis();
|
||||||
|
resetInitializationTracking();
|
||||||
|
Serial.printf("[Port %d] Initialized serial on RX=%d, TX=%d at 19200 baud\n", _id, _rxPin, _txPin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// Read incoming data
|
||||||
|
while (_serial.available()) {
|
||||||
|
char c = _serial.read();
|
||||||
|
_lastRxMs = millis();
|
||||||
|
|
||||||
|
// Handle prompt character '>' immediately if it appears alone
|
||||||
|
if (c == '>') {
|
||||||
|
_rxBuffer += c;
|
||||||
|
processResponse(_rxBuffer);
|
||||||
|
_rxBuffer = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early visibility into raw bytes during connection attempts
|
||||||
|
if (_state != READY) {
|
||||||
|
Serial.printf("[Port %d RAW] 0x%02X\n", _id, (uint8_t)c);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTerminator = (c == '\n' || c == '\r');
|
||||||
|
if (isTerminator) {
|
||||||
|
if (_rxBuffer.length() > 0) {
|
||||||
|
processResponse(_rxBuffer);
|
||||||
|
_rxBuffer = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_rxBuffer += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State Machine
|
||||||
|
switch (_state) {
|
||||||
|
case DISCONNECTED:
|
||||||
|
if (millis() - _lastActionMs > 1000) {
|
||||||
|
// Clear any stale data in buffer
|
||||||
|
while (_serial.available()) _serial.read();
|
||||||
|
_rxBuffer = "";
|
||||||
|
|
||||||
|
// Try to connect
|
||||||
|
Serial.printf("[Port %d] Attempting connection... sending ?FF\n", _id);
|
||||||
|
_serial.println(); // Clear any partial command
|
||||||
|
sendCommand("?FF", CMD_FAULT_FLAG); // Query Fault Flag to check presence
|
||||||
|
_state = CONNECTING;
|
||||||
|
_connectionAttempts++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CONNECTING:
|
||||||
|
// Check for timeout
|
||||||
|
if (millis() - _lastActionMs > CMD_TIMEOUT) {
|
||||||
|
if (_rxBuffer.length() > 0) {
|
||||||
|
processResponse(_rxBuffer);
|
||||||
|
_rxBuffer = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_waitingForResponse) {
|
||||||
|
Serial.printf("[Port %d] Connection timeout (attempt %d), retrying...\n", _id, _connectionAttempts);
|
||||||
|
_serial.print("\r\n");
|
||||||
|
_serial.flush();
|
||||||
|
_serial.end();
|
||||||
|
delay(10);
|
||||||
|
beginSerial();
|
||||||
|
_state = DISCONNECTED;
|
||||||
|
resetInitializationTracking();
|
||||||
|
_waitingForResponse = false;
|
||||||
|
_lastActionMs = millis();
|
||||||
|
if (_lastRxMs == 0) {
|
||||||
|
Serial.printf("[Port %d] No RX observed yet from device. Verify RS232 converter path.\n", _id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!_waitingForResponse && millis() - _lastActionMs >= MIN_CMD_INTERVAL) {
|
||||||
|
sendCommand("?FF", CMD_FAULT_FLAG);
|
||||||
|
_connectionAttempts++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case READY:
|
||||||
|
handleReadyState();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FAULT:
|
||||||
|
if (millis() - _lastActionMs > 5000) {
|
||||||
|
Serial.printf("[Port %d] Fault recovery, attempting reconnection...\n", _id);
|
||||||
|
_state = DISCONNECTED;
|
||||||
|
resetInitializationTracking();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPower(float mw) {
|
||||||
|
if (_state == READY) {
|
||||||
|
String cmd = "P=" + String(mw, 1);
|
||||||
|
enqueueCommand(cmd, CMD_SET_POWER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendSerialCommand(String cmd) {
|
||||||
|
// For manual commands, we don't know the type, so use NONE and rely on generic parsing if possible.
|
||||||
|
// If we're not waiting for a response, send immediately to help with debugging even before READY.
|
||||||
|
if (!_waitingForResponse && millis() - _lastResponseMs >= MIN_CMD_INTERVAL) {
|
||||||
|
sendCommand(cmd, CMD_NONE);
|
||||||
|
} else {
|
||||||
|
enqueueCommand(cmd, CMD_NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters for UI
|
||||||
|
bool isConnected() { return _state == READY; }
|
||||||
|
float getPower() { return _data.power; }
|
||||||
|
float getBaseTemp() { return _data.baseTemp; }
|
||||||
|
float getDiodeTemp() { return _data.diodeTemp; }
|
||||||
|
float getCtrlTemp() { return _data.ctrlTemp; }
|
||||||
|
float getCurrent() { return _data.current; }
|
||||||
|
float getReserveCurrent() { return _data.reserveCurrent; }
|
||||||
|
String getStatus() { return _data.status; }
|
||||||
|
String getName() { return _data.name; }
|
||||||
|
float getWavelength() { return _data.wavelength; }
|
||||||
|
float getMinPower() { return _data.minPower; }
|
||||||
|
float getMaxPower() { return _data.maxPower; }
|
||||||
|
float getSetTemp() { return _data.setTemp; }
|
||||||
|
int getHours() { return _data.hours; }
|
||||||
|
int getCtrlHours() { return _data.ctrlHours; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
int _id;
|
||||||
|
HardwareSerial& _serial;
|
||||||
|
int _rxPin, _txPin;
|
||||||
|
State _state = DISCONNECTED;
|
||||||
|
String _rxBuffer = "";
|
||||||
|
unsigned long _lastActionMs = 0;
|
||||||
|
unsigned long _lastResponseMs = 0;
|
||||||
|
int _connectionAttempts = 0;
|
||||||
|
int _retryCount = 0;
|
||||||
|
bool _isInitialized = false;
|
||||||
|
const unsigned long MIN_CMD_INTERVAL = 500;
|
||||||
|
const unsigned long CMD_TIMEOUT = 2000;
|
||||||
|
|
||||||
|
struct Data {
|
||||||
|
String name = "Sapphire";
|
||||||
|
float wavelength = 0;
|
||||||
|
float power = 0;
|
||||||
|
float minPower = 0;
|
||||||
|
float maxPower = 0;
|
||||||
|
float current = 0;
|
||||||
|
float reserveCurrent = 0;
|
||||||
|
float baseTemp = 0;
|
||||||
|
float diodeTemp = 0;
|
||||||
|
float ctrlTemp = 0;
|
||||||
|
float setTemp = 0;
|
||||||
|
int hours = 0;
|
||||||
|
int ctrlHours = 0;
|
||||||
|
String status = "Disconnected";
|
||||||
|
} _data;
|
||||||
|
|
||||||
|
// Polling
|
||||||
|
struct PollCmd {
|
||||||
|
const char* cmd;
|
||||||
|
CommandType type;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr PollCmd _pollCommands[] = {
|
||||||
|
{"?P", CMD_POWER},
|
||||||
|
{"?C", CMD_CURRENT},
|
||||||
|
{"?BT", CMD_BASE_TEMP},
|
||||||
|
{"?DT", CMD_DIODE_TEMP},
|
||||||
|
{"?PST", CMD_CTRL_TEMP},
|
||||||
|
{"?STA", CMD_STATUS},
|
||||||
|
{"?DST", CMD_SET_TEMP}
|
||||||
|
};
|
||||||
|
static constexpr size_t POLL_COMMAND_COUNT = sizeof(_pollCommands) / sizeof(_pollCommands[0]);
|
||||||
|
|
||||||
|
int _pollIndex = 0;
|
||||||
|
unsigned long _lastPollMs = 0;
|
||||||
|
unsigned long _lastStaticRetryMs = 0;
|
||||||
|
unsigned long _initStartMs = 0;
|
||||||
|
static constexpr unsigned long INIT_GRACE_MS = 5000;
|
||||||
|
static constexpr unsigned long STATIC_RETRY_MS = 3000;
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
struct StaticQuery {
|
||||||
|
const char* command;
|
||||||
|
CommandType type;
|
||||||
|
bool* satisfiedFlag;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool _hasHours = false;
|
||||||
|
bool _hasCtrlHours = false;
|
||||||
|
bool _hasWavelength = false;
|
||||||
|
bool _hasSetTemp = false;
|
||||||
|
bool _hasMinPower = false;
|
||||||
|
bool _hasMaxPower = false;
|
||||||
|
bool _hasReserve = false;
|
||||||
|
bool _promptCommandSent = false;
|
||||||
|
bool _echoConfigured = false;
|
||||||
|
bool _promptConfigured = false;
|
||||||
|
|
||||||
|
StaticQuery _initQueries[8] = {
|
||||||
|
{">", CMD_PROMPT, nullptr},
|
||||||
|
{"?HH", CMD_HOURS, &_hasHours},
|
||||||
|
{"?PSH", CMD_CTRL_HOURS, &_hasCtrlHours},
|
||||||
|
{"?WAVE", CMD_WAVELENGTH, &_hasWavelength},
|
||||||
|
{"?DST", CMD_SET_TEMP, &_hasSetTemp},
|
||||||
|
{"?MINLP", CMD_MIN_POWER, &_hasMinPower},
|
||||||
|
{"?MAXLP", CMD_MAX_POWER, &_hasMaxPower},
|
||||||
|
{"?DRC", CMD_RESERVE, &_hasReserve}
|
||||||
|
};
|
||||||
|
size_t _initIndex = 0;
|
||||||
|
|
||||||
|
// Command Queue
|
||||||
|
struct QueueItem {
|
||||||
|
String cmd;
|
||||||
|
CommandType type;
|
||||||
|
};
|
||||||
|
QueueItem _pendingCommand = {"", CMD_NONE};
|
||||||
|
|
||||||
|
bool _waitingForResponse = false;
|
||||||
|
String _lastSentCommandString = "";
|
||||||
|
CommandType _activeCommandType = CMD_NONE;
|
||||||
|
unsigned long _lastRxMs = 0;
|
||||||
|
|
||||||
|
void beginSerial() {
|
||||||
|
_serial.begin(19200, SERIAL_8N1, _rxPin, _txPin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetInitializationTracking() {
|
||||||
|
_isInitialized = false;
|
||||||
|
_hasHours = false;
|
||||||
|
_hasCtrlHours = false;
|
||||||
|
_hasWavelength = false;
|
||||||
|
_hasSetTemp = false;
|
||||||
|
_hasMinPower = false;
|
||||||
|
_hasMaxPower = false;
|
||||||
|
_hasReserve = false;
|
||||||
|
_promptCommandSent = false;
|
||||||
|
_echoConfigured = false;
|
||||||
|
_promptConfigured = false;
|
||||||
|
_initIndex = 0;
|
||||||
|
_initStartMs = millis();
|
||||||
|
_lastStaticRetryMs = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void enqueueCommand(String cmd, CommandType type) {
|
||||||
|
_pendingCommand = {cmd, type};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allStaticFieldsSatisfied() const {
|
||||||
|
return _hasHours && _hasCtrlHours && _hasWavelength && _hasSetTemp && _hasMinPower && _hasMaxPower && _hasReserve;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendNextStaticQuery() {
|
||||||
|
const size_t initCount = sizeof(_initQueries) / sizeof(_initQueries[0]);
|
||||||
|
size_t attempts = 0;
|
||||||
|
|
||||||
|
while (attempts < initCount) {
|
||||||
|
StaticQuery &q = _initQueries[_initIndex];
|
||||||
|
bool satisfied = q.satisfiedFlag ? *(q.satisfiedFlag) : _promptCommandSent;
|
||||||
|
_initIndex = (_initIndex + 1) % initCount;
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
if (satisfied) continue;
|
||||||
|
|
||||||
|
if (!q.satisfiedFlag) {
|
||||||
|
_promptCommandSent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCommand(q.command, q.type);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleReadyState() {
|
||||||
|
if (_waitingForResponse) {
|
||||||
|
if (millis() - _lastActionMs > CMD_TIMEOUT) {
|
||||||
|
_serial.print("\r\n");
|
||||||
|
retryCommand();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis() - _lastResponseMs < MIN_CMD_INTERVAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingCommand.cmd.length() > 0) {
|
||||||
|
sendCommand(_pendingCommand.cmd, _pendingCommand.type);
|
||||||
|
_pendingCommand = {"", CMD_NONE};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply device-side configuration first to keep parsing deterministic
|
||||||
|
if (!_echoConfigured) {
|
||||||
|
sendCommand("E=0", CMD_CONFIG_ECHO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_promptConfigured) {
|
||||||
|
sendCommand(">0", CMD_CONFIG_PROMPT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool staticsComplete = allStaticFieldsSatisfied();
|
||||||
|
|
||||||
|
if (!_isInitialized) {
|
||||||
|
if (sendNextStaticQuery()) return;
|
||||||
|
|
||||||
|
if (staticsComplete) {
|
||||||
|
Serial.printf("[Port %d] Static telemetry confirmed; enabling poll loop.\n", _id);
|
||||||
|
_isInitialized = true;
|
||||||
|
_lastPollMs = 0;
|
||||||
|
_lastStaticRetryMs = millis();
|
||||||
|
} else if (millis() - _initStartMs > INIT_GRACE_MS) {
|
||||||
|
Serial.printf("[Port %d] Static telemetry incomplete. Proceeding to poll.\n", _id);
|
||||||
|
_isInitialized = true;
|
||||||
|
_lastPollMs = 0;
|
||||||
|
_lastStaticRetryMs = millis();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (!staticsComplete && millis() - _lastStaticRetryMs >= STATIC_RETRY_MS) {
|
||||||
|
if (sendNextStaticQuery()) {
|
||||||
|
_lastStaticRetryMs = millis();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned long pollInterval = max(pollingRateMs, MIN_POLL_INTERVAL_MS);
|
||||||
|
if (millis() - _lastPollMs >= pollInterval) {
|
||||||
|
sendCommand(_pollCommands[_pollIndex].cmd, _pollCommands[_pollIndex].type);
|
||||||
|
_pollIndex = (_pollIndex + 1) % POLL_COMMAND_COUNT;
|
||||||
|
_lastPollMs = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendCommand(String cmd, CommandType type) {
|
||||||
|
Serial.printf("[Port %d TX] %s\n", _id, cmd.c_str());
|
||||||
|
_serial.println(cmd);
|
||||||
|
_lastActionMs = millis();
|
||||||
|
_waitingForResponse = true;
|
||||||
|
_lastSentCommandString = cmd;
|
||||||
|
_activeCommandType = type;
|
||||||
|
_retryCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void retryCommand() {
|
||||||
|
if (_retryCount < 3) {
|
||||||
|
_retryCount++;
|
||||||
|
Serial.printf("[Port %d] Retry %d for command %s\n", _id, _retryCount, _lastSentCommandString.c_str());
|
||||||
|
_serial.print("\r\n");
|
||||||
|
_serial.println(_lastSentCommandString);
|
||||||
|
_lastActionMs = millis();
|
||||||
|
_waitingForResponse = true;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[Port %d] Command %s failed after 3 retries\n", _id, _lastSentCommandString.c_str());
|
||||||
|
_waitingForResponse = false;
|
||||||
|
_retryCount = 0;
|
||||||
|
_activeCommandType = CMD_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float parseFloatValue(const String& resp, float fallback = 0) {
|
||||||
|
int start = -1;
|
||||||
|
for (int i = 0; i < resp.length(); i++) {
|
||||||
|
char c = resp[i];
|
||||||
|
if (isDigit(c) || c == '-' || c == '+' || c == '.') {
|
||||||
|
start = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (start >= 0) {
|
||||||
|
float parsed = resp.substring(start).toFloat();
|
||||||
|
if (!isnan(parsed)) return parsed;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parseIntValue(const String& resp, int fallback = 0) {
|
||||||
|
int start = -1;
|
||||||
|
for (int i = 0; i < resp.length(); i++) {
|
||||||
|
char c = resp[i];
|
||||||
|
if (isDigit(c) || c == '-' || c == '+') {
|
||||||
|
start = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (start >= 0) {
|
||||||
|
return resp.substring(start).toInt();
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
String stripPromptPrefix(String resp) {
|
||||||
|
String out = resp;
|
||||||
|
out.trim();
|
||||||
|
|
||||||
|
if (out.startsWith("Sapphire")) {
|
||||||
|
int arrowIndex = out.indexOf("->");
|
||||||
|
if (arrowIndex >= 0) {
|
||||||
|
out = out.substring(arrowIndex + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.trim();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEchoLine(const String& resp) {
|
||||||
|
String normalized = stripPromptPrefix(resp);
|
||||||
|
if (normalized.length() == 0) return true;
|
||||||
|
if (normalized == _lastSentCommandString) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void processResponse(String resp) {
|
||||||
|
resp.trim();
|
||||||
|
if (resp.length() == 0) return;
|
||||||
|
|
||||||
|
broadcastSapphireResponse(_id, resp);
|
||||||
|
|
||||||
|
// 1. Check for Echo or prompt-only traffic
|
||||||
|
if (isEchoLine(resp)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Normalize payload (remove prompt/echo prefix if present)
|
||||||
|
String payload = stripPromptPrefix(resp);
|
||||||
|
if (payload.startsWith(_lastSentCommandString)) {
|
||||||
|
payload = payload.substring(_lastSentCommandString.length());
|
||||||
|
payload.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If payload is empty after stripping echo, it was just an echo line.
|
||||||
|
if (payload.length() == 0) return;
|
||||||
|
|
||||||
|
// 3. Parse Data based on Active Command Context
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
// Special handling for Fault Flag during connection
|
||||||
|
if (_activeCommandType == CMD_FAULT_FLAG && _state == CONNECTING) {
|
||||||
|
long flagValue = parseIntValue(payload, -1);
|
||||||
|
bool warming = false;
|
||||||
|
bool hasFaults = false;
|
||||||
|
|
||||||
|
if (flagValue >= 0) {
|
||||||
|
warming = (flagValue & (1 << 8));
|
||||||
|
// Bit 13 alone (8192) means ready/ok per protocol
|
||||||
|
hasFaults = (flagValue != 0 && flagValue != 8192);
|
||||||
|
} else {
|
||||||
|
hasFaults = payload.indexOf("FAULT") >= 0 && payload.indexOf("NO FAULT") < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warming) {
|
||||||
|
_data.status = "Warming (bit 8 set)";
|
||||||
|
Serial.printf("[Port %d] Controller warming; waiting for ready flag.\n", _id);
|
||||||
|
} else if (hasFaults) {
|
||||||
|
_state = FAULT;
|
||||||
|
_data.status = "Fault: " + payload;
|
||||||
|
} else {
|
||||||
|
_state = READY;
|
||||||
|
Serial.printf("[Port %d] Connected. Response: %s\n", _id, payload.c_str());
|
||||||
|
}
|
||||||
|
_waitingForResponse = false;
|
||||||
|
_activeCommandType = CMD_NONE;
|
||||||
|
_lastResponseMs = millis();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_activeCommandType) {
|
||||||
|
case CMD_POWER:
|
||||||
|
_data.power = parseFloatValue(payload, _data.power);
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CURRENT:
|
||||||
|
_data.current = parseFloatValue(payload, _data.current);
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_BASE_TEMP:
|
||||||
|
_data.baseTemp = parseFloatValue(payload, _data.baseTemp);
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_DIODE_TEMP:
|
||||||
|
_data.diodeTemp = parseFloatValue(payload, _data.diodeTemp);
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CTRL_TEMP:
|
||||||
|
_data.ctrlTemp = parseFloatValue(payload, _data.ctrlTemp);
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_HOURS:
|
||||||
|
_data.hours = parseIntValue(payload, _data.hours);
|
||||||
|
_hasHours = true;
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CTRL_HOURS:
|
||||||
|
_data.ctrlHours = parseIntValue(payload, _data.ctrlHours);
|
||||||
|
_hasCtrlHours = true;
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_WAVELENGTH:
|
||||||
|
_data.wavelength = parseFloatValue(payload, _data.wavelength);
|
||||||
|
if (_data.wavelength > 0) {
|
||||||
|
_data.name = "Sapphire " + String((int)_data.wavelength);
|
||||||
|
_hasWavelength = true;
|
||||||
|
}
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_MIN_POWER:
|
||||||
|
_data.minPower = parseFloatValue(payload, _data.minPower);
|
||||||
|
_hasMinPower = true;
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_MAX_POWER:
|
||||||
|
_data.maxPower = parseFloatValue(payload, _data.maxPower);
|
||||||
|
_hasMaxPower = true;
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_RESERVE:
|
||||||
|
_data.reserveCurrent = parseFloatValue(payload, _data.reserveCurrent);
|
||||||
|
_hasReserve = true;
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_SET_TEMP:
|
||||||
|
_data.setTemp = parseFloatValue(payload, _data.setTemp);
|
||||||
|
_hasSetTemp = true;
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_STATUS:
|
||||||
|
{
|
||||||
|
int status = parseIntValue(payload, -1);
|
||||||
|
if (status >= 0) {
|
||||||
|
switch(status) {
|
||||||
|
case 1: _data.status = "Start up"; break;
|
||||||
|
case 2: _data.status = "Warmup"; break;
|
||||||
|
case 3: _data.status = "Standby"; break;
|
||||||
|
case 4: _data.status = "Laser On"; break;
|
||||||
|
case 5: _data.status = "Laser Ready"; break;
|
||||||
|
case 6: _data.status = "Error"; break;
|
||||||
|
default: _data.status = "Unknown"; break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_data.status = payload;
|
||||||
|
}
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_SET_POWER:
|
||||||
|
// Response to P=... might be OK or new power or nothing
|
||||||
|
if (payload.indexOf("OK") >= 0 || payload.length() > 0) {
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_PROMPT:
|
||||||
|
if (payload.indexOf(">") >= 0) success = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CONFIG_ECHO:
|
||||||
|
_echoConfigured = true;
|
||||||
|
success = true;
|
||||||
|
Serial.printf("[Port %d] Echo disabled for clean responses.\n", _id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_CONFIG_PROMPT:
|
||||||
|
_promptConfigured = true;
|
||||||
|
_promptCommandSent = true;
|
||||||
|
success = true;
|
||||||
|
Serial.printf("[Port %d] Prompt disabled for clean responses.\n", _id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// CMD_NONE or unknown
|
||||||
|
// Try to guess or just log
|
||||||
|
Serial.printf("[Port %d] Unsolicited or unknown data: %s\n", _id, payload.c_str());
|
||||||
|
success = true; // Treat as handled so we don't get stuck?
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
_waitingForResponse = false;
|
||||||
|
_activeCommandType = CMD_NONE;
|
||||||
|
_lastResponseMs = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Drivers
|
||||||
|
SapphireDriver driver1(1, Serial2, SERIAL1_RX, SERIAL1_TX);
|
||||||
|
SapphireDriver driver2(2, Serial1, SERIAL2_RX, SERIAL2_TX);
|
||||||
|
|
||||||
|
// State
|
||||||
|
unsigned long lastUpdateMs = 0;
|
||||||
|
// pollingRateMs moved to top
|
||||||
|
|
||||||
|
// JSON Buffer
|
||||||
|
StaticJsonDocument<2048> doc;
|
||||||
|
|
||||||
|
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
|
||||||
|
if (type == WS_EVT_CONNECT) {
|
||||||
|
Serial.printf("WS Client connected: %u\n", client->id());
|
||||||
|
// Send immediate update on connect
|
||||||
|
lastUpdateMs = 0;
|
||||||
|
} else if (type == WS_EVT_DISCONNECT) {
|
||||||
|
Serial.printf("WS Client disconnected: %u\n", client->id());
|
||||||
|
} else if (type == WS_EVT_DATA) {
|
||||||
|
AwsFrameInfo *info = (AwsFrameInfo*)arg;
|
||||||
|
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
|
||||||
|
data[len] = 0;
|
||||||
|
String msg = (char*)data;
|
||||||
|
|
||||||
|
JsonDocument inputDoc;
|
||||||
|
DeserializationError error = deserializeJson(inputDoc, msg);
|
||||||
|
if (!error) {
|
||||||
|
if (inputDoc.containsKey("pollingRate")) {
|
||||||
|
pollingRateMs = max((unsigned long)inputDoc["pollingRate"], MIN_POLL_INTERVAL_MS);
|
||||||
|
Serial.printf("Polling rate set to: %lu ms (clamped to device-safe minimum of %lu ms)\n", pollingRateMs, MIN_POLL_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputDoc["command"] == "setPower") {
|
||||||
|
int port = inputDoc["port"];
|
||||||
|
float value = inputDoc["value"];
|
||||||
|
Serial.printf("Set Power Port %d: %.1f mW\n", port, value);
|
||||||
|
|
||||||
|
if (port == 1) driver1.setPower(value);
|
||||||
|
else if (port == 2) driver2.setPower(value);
|
||||||
|
}
|
||||||
|
else if (inputDoc["command"] == "raw") {
|
||||||
|
String cmd = inputDoc["data"];
|
||||||
|
int port = inputDoc["port"] | 1; // Default to port 1
|
||||||
|
Serial.printf("[%d] > %s\n", port, cmd.c_str());
|
||||||
|
|
||||||
|
if (port == 1) driver1.sendSerialCommand(cmd);
|
||||||
|
else if (port == 2) driver2.sendSerialCommand(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
// Init Drivers
|
||||||
|
driver1.begin();
|
||||||
|
driver2.begin();
|
||||||
|
|
||||||
|
// Connect to WiFi
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
Serial.println("");
|
||||||
|
|
||||||
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("");
|
||||||
|
Serial.print("Connected to ");
|
||||||
|
Serial.println(ssid);
|
||||||
|
Serial.print("IP address: ");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
|
||||||
|
// Setup WebSocket
|
||||||
|
ws.onEvent(onWsEvent);
|
||||||
|
server.addHandler(&ws);
|
||||||
|
|
||||||
|
// Setup ElegantOTA
|
||||||
|
ElegantOTA.begin(&server);
|
||||||
|
|
||||||
|
// Serve Index
|
||||||
|
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
|
request->send_P(200, "text/html", index_html, processor);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start Server
|
||||||
|
server.begin();
|
||||||
|
Serial.println("HTTP server started");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
ElegantOTA.loop();
|
||||||
|
ws.cleanupClients();
|
||||||
|
|
||||||
|
// Run Drivers
|
||||||
|
driver1.loop();
|
||||||
|
driver2.loop();
|
||||||
|
|
||||||
|
// USB Serial Input
|
||||||
|
if (Serial.available()) {
|
||||||
|
String input = Serial.readStringUntil('\n');
|
||||||
|
input.trim();
|
||||||
|
if (input.length() > 0) {
|
||||||
|
Serial.printf("[1] > %s\n", input.c_str());
|
||||||
|
driver1.sendSerialCommand(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodic Updates
|
||||||
|
if (millis() - lastUpdateMs >= pollingRateMs) {
|
||||||
|
lastUpdateMs = millis();
|
||||||
|
|
||||||
|
// Build JSON from real driver data
|
||||||
|
doc.clear();
|
||||||
|
|
||||||
|
// Port 1
|
||||||
|
JsonObject p1 = doc.createNestedObject("1");
|
||||||
|
if (driver1.isConnected()) {
|
||||||
|
p1["name"] = driver1.getName();
|
||||||
|
p1["wavelength"] = driver1.getWavelength();
|
||||||
|
p1["power"] = driver1.getPower();
|
||||||
|
p1["minPower"] = driver1.getMinPower();
|
||||||
|
p1["maxPower"] = driver1.getMaxPower();
|
||||||
|
p1["current"] = driver1.getCurrent();
|
||||||
|
p1["reserveCurrent"] = driver1.getReserveCurrent();
|
||||||
|
p1["baseTemp"] = driver1.getBaseTemp();
|
||||||
|
p1["diodeTemp"] = driver1.getDiodeTemp();
|
||||||
|
p1["ctrlTemp"] = driver1.getCtrlTemp();
|
||||||
|
p1["setTemp"] = driver1.getSetTemp();
|
||||||
|
p1["hours"] = driver1.getHours();
|
||||||
|
p1["ctrlHours"] = driver1.getCtrlHours();
|
||||||
|
p1["status"] = driver1.getStatus();
|
||||||
|
} else {
|
||||||
|
p1["status"] = "Disconnected";
|
||||||
|
p1["name"] = "Port 1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port 2
|
||||||
|
JsonObject p2 = doc.createNestedObject("2");
|
||||||
|
if (driver2.isConnected()) {
|
||||||
|
p2["name"] = driver2.getName();
|
||||||
|
p2["wavelength"] = driver2.getWavelength();
|
||||||
|
p2["power"] = driver2.getPower();
|
||||||
|
p2["minPower"] = driver2.getMinPower();
|
||||||
|
p2["maxPower"] = driver2.getMaxPower();
|
||||||
|
p2["current"] = driver2.getCurrent();
|
||||||
|
p2["reserveCurrent"] = driver2.getReserveCurrent();
|
||||||
|
p2["baseTemp"] = driver2.getBaseTemp();
|
||||||
|
p2["diodeTemp"] = driver2.getDiodeTemp();
|
||||||
|
p2["ctrlTemp"] = driver2.getCtrlTemp();
|
||||||
|
p2["setTemp"] = driver2.getSetTemp();
|
||||||
|
p2["hours"] = driver2.getHours();
|
||||||
|
p2["ctrlHours"] = driver2.getCtrlHours();
|
||||||
|
p2["status"] = driver2.getStatus();
|
||||||
|
} else {
|
||||||
|
p2["status"] = "Disconnected";
|
||||||
|
p2["name"] = "Port 2";
|
||||||
|
}
|
||||||
|
|
||||||
|
String jsonString;
|
||||||
|
serializeJson(doc, jsonString);
|
||||||
|
|
||||||
|
ws.textAll(jsonString);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user