Working on better version

This commit is contained in:
2026-06-16 13:11:48 +10:00
parent 4cc1de9c46
commit a8a09850dd
5 changed files with 715 additions and 284 deletions
+510 -284
View File
@@ -1,18 +1,23 @@
/**
* ESP32 Debug Dongle
*
* Web-based and Bluetooth serial terminal with multi-port support.
* Remote serial debug bridge for the trough/solartrack sensors (and anything
* with a UART). Bridges a target device's UART to:
* - Telnet (port 23) -- primary remote path; nc/minicom or an agent
* - Web terminal -- xterm.js over WebSocket
* - Bluetooth SPP -- original ESP32 only (no Classic BT on ESP32-S3)
* Plus: GPIO control to reset/wake the target, NTP-dated SD logging, and an
* OLED status page (LilyGo T3-S3).
*
* Features:
* - Web terminal using xterm.js over WebSocket
* - Bluetooth Classic SPP serial bridge
* - Switch between internal (virtual), Serial, and Serial1
* - Configurable baud rates
* Telnet protocol: a line is forwarded verbatim to the target UART UNLESS it
* starts with '~', in which case it's a dongle command:
* ~help ~status ~reset [ms] ~wake [ms] ~baud <n> ~port <int|usb|ext>
* ~log on|off ~gpio <pin> <0|1>
* REST mirrors these: /api/status /api/reset /api/wake /api/baud?baud=
* /api/port?port= /api/log?on= /api/gpio?pin=&val= /api/send?data=
*
* Hardware connections for Serial1 (external device):
* GPIO16 = RX1 (connect to external TX)
* GPIO17 = TX1 (connect to external RX)
* GND = Common ground
* Boards (platformio.ini): [env:esp32dev] generic ESP32 (+ BT); [env:t3s3]
* LilyGo T3-S3 (OLED + SD + LoRa). Pins are build flags -- see platformio.ini.
*/
#include <Arduino.h>
@@ -21,20 +26,29 @@
#include <AsyncTCP.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "BluetoothSerial.h"
#include "LoopbackStream.h"
#if HAS_BT_CLASSIC
#include "BluetoothSerial.h"
#endif
#if BOARD_T3S3
#include <time.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#endif
// ============================================================================
// Configuration
// ============================================================================
// WiFi Mode: Set to true to connect to existing network, false for AP mode
#define WIFI_STATION_MODE true
#define ST(A) #A
#define STR(A) ST(A)
// WiFi Station mode settings (connect to existing network)
#ifndef STA_SSID
#define STA_SSID "ExampleSSID"
#endif
@@ -42,60 +56,94 @@
#define STA_PASSWORD "ThePassword"
#endif
// WiFi Access Point fallback settings (if station fails or AP mode selected)
const char* AP_SSID = "ESP32-DebugDongle";
const char* AP_PASSWORD = "debug1234"; // Min 8 characters
// Station mode connection timeout (milliseconds)
const char* AP_PASSWORD = "debug1234";
#define WIFI_CONNECT_TIMEOUT 15000
// Bluetooth device name
const char* BT_NAME = "ESP32-Debug";
// Serial1 pins (external device connection)
#define SERIAL1_RX_PIN 16
#define SERIAL1_TX_PIN 17
// Target UART pins (override per board in platformio.ini).
#ifndef TARGET_RX_PIN
#define TARGET_RX_PIN 16
#endif
#ifndef TARGET_TX_PIN
#define TARGET_TX_PIN 17
#endif
// GPIO control lines to the target. -1 disables. Active-low by default on T3-S3.
#ifndef GPIO_RESET_PIN
#define GPIO_RESET_PIN -1
#endif
#ifndef GPIO_WAKE_PIN
#define GPIO_WAKE_PIN -1
#endif
#ifndef GPIO_CTRL_ACTIVE_LOW
#define GPIO_CTRL_ACTIVE_LOW 0
#endif
// Default baud rates
#define DEFAULT_BAUD_SERIAL 115200
#define DEFAULT_BAUD_SERIAL1 115200
// Internal serial buffer size
#define INTERNAL_BUFFER_SIZE 2048
#define TELNET_PORT 23
#define MAX_TELNET 4
#define TELNET_LINE_MAX 256
#define CTRL_PREFIX '~'
#if BOARD_T3S3
#ifndef NTP_TZ
#define NTP_TZ "UTC0"
#endif
#endif
// ============================================================================
// Global Objects
// ============================================================================
// Web server on port 80
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
// Bluetooth Serial
#if HAS_BT_CLASSIC
BluetoothSerial SerialBT;
#endif
// Hardware serial for external device
// HardwareSerial Serial1(1);
// Virtual/loopback serial for internal debug output
LoopbackStream internalSerial(INTERNAL_BUFFER_SIZE);
// Active serial port pointer
Stream* activePort = &internalSerial;
// Port identifiers
enum SerialPortId {
PORT_INTERNAL = 0, // Virtual loopback (ESP32 debug output)
PORT_USB = 1, // Serial (USB) - shares with programming
PORT_EXTERNAL = 2 // Serial1 (GPIO pins) - external device
};
SerialPortId currentPort = PORT_INTERNAL;
// Baud rates (can be changed at runtime)
enum SerialPortId { PORT_INTERNAL = 0, PORT_USB = 1, PORT_EXTERNAL = 2 };
SerialPortId currentPort = PORT_EXTERNAL; // default: watch the wired target
uint32_t baudSerial1 = DEFAULT_BAUD_SERIAL1;
// Telnet
WiFiServer telnetServer(TELNET_PORT);
WiFiClient telnetClients[MAX_TELNET];
char telnetLine[MAX_TELNET][TELNET_LINE_MAX];
size_t telnetLineLen[MAX_TELNET] = {0};
// Counters
volatile uint32_t rxBytes = 0; // from target
volatile uint32_t txBytes = 0; // to target
// SD logging (T3-S3)
bool sdAvailable = false;
bool logEnabled = true; // ON by default once we have a date
bool ntpSynced = false;
#if BOARD_T3S3
SPIClass sdSPI(HSPI);
File logFile;
bool logAtLineStart = true;
char logPath[40] = {0};
Adafruit_SSD1306 oled(128, 64, &Wire, T3S3_OLED_RST);
bool oledOk = false;
#endif
// ============================================================================
// Forward decls
// ============================================================================
void broadcastFromTarget(const uint8_t* data, size_t len);
void forwardToTarget(const uint8_t* data, size_t len);
void handleCommand(const char* line, Stream* reply);
// ============================================================================
// Serial Port Management
// ============================================================================
@@ -103,27 +151,216 @@ uint32_t baudSerial1 = DEFAULT_BAUD_SERIAL1;
void setActivePort(SerialPortId port) {
currentPort = port;
switch (port) {
case PORT_INTERNAL:
activePort = &internalSerial;
Serial.println("[System] Switched to Internal serial");
break;
case PORT_USB:
activePort = &Serial;
Serial.println("[System] Switched to USB serial");
break;
case PORT_EXTERNAL:
activePort = &Serial1;
Serial.println("[System] Switched to External serial (Serial1)");
break;
case PORT_INTERNAL: activePort = &internalSerial; break;
case PORT_USB: activePort = &Serial; break;
case PORT_EXTERNAL: activePort = &Serial1; break;
}
}
void setBaudRate(SerialPortId port, uint32_t baud) {
if (port == PORT_EXTERNAL) {
baudSerial1 = baud;
Serial1.end();
Serial1.begin(baud, SERIAL_8N1, SERIAL1_RX_PIN, SERIAL1_TX_PIN);
Serial.printf("[System] Serial1 baud rate set to %lu\n", baud);
void setBaudRate(uint32_t baud) {
baudSerial1 = baud;
Serial1.end();
Serial1.begin(baud, SERIAL_8N1, TARGET_RX_PIN, TARGET_TX_PIN);
}
// ============================================================================
// GPIO control (reset / wake)
// ============================================================================
static void gpioInit(int pin) {
if (pin < 0) return;
// Idle = inactive level.
pinMode(pin, OUTPUT);
digitalWrite(pin, GPIO_CTRL_ACTIVE_LOW ? HIGH : LOW);
}
static void gpioPulse(int pin, uint32_t ms, Stream* reply, const char* name) {
if (pin < 0) { if (reply) reply->printf("[ctrl] %s pin not configured\r\n", name); return; }
if (ms == 0) ms = 200;
int active = GPIO_CTRL_ACTIVE_LOW ? LOW : HIGH;
int inactive = GPIO_CTRL_ACTIVE_LOW ? HIGH : LOW;
pinMode(pin, OUTPUT);
digitalWrite(pin, active);
delay(ms);
digitalWrite(pin, inactive);
if (reply) reply->printf("[ctrl] %s pulsed pin %d for %lu ms\r\n", name, pin, (unsigned long)ms);
}
// ============================================================================
// SD logging (T3-S3)
// ============================================================================
#if BOARD_T3S3
static void openLogIfReady() {
if (!sdAvailable || logFile) return;
time_t now = time(nullptr);
if (now < 1700000000) return; // wait for NTP (well past 2023)
struct tm tmv; localtime_r(&now, &tmv);
SD.mkdir("/logs");
strftime(logPath, sizeof(logPath), "/logs/%Y%m%d-%H%M%S.log", &tmv);
logFile = SD.open(logPath, FILE_WRITE);
if (logFile) {
logFile.printf("# debug-dongle log opened %s\n", logPath);
logFile.flush();
logAtLineStart = true;
}
}
static void logBytes(const uint8_t* data, size_t len) {
if (!logEnabled || !logFile) return;
for (size_t i = 0; i < len; i++) {
if (logAtLineStart) {
time_t now = time(nullptr);
struct tm tmv; localtime_r(&now, &tmv);
char ts[16];
strftime(ts, sizeof(ts), "[%H:%M:%S] ", &tmv);
logFile.print(ts);
logAtLineStart = false;
}
logFile.write(data[i]);
if (data[i] == '\n') logAtLineStart = true;
}
logFile.flush(); // power-cut safe (it's a debug log)
}
#else
static void openLogIfReady() {}
static void logBytes(const uint8_t*, size_t) {}
#endif
// ============================================================================
// Fan-out
// ============================================================================
// Target UART -> every sink (web, telnet, BT, SD).
void broadcastFromTarget(const uint8_t* data, size_t len) {
rxBytes += len;
ws.binaryAll((uint8_t*)data, len);
for (int i = 0; i < MAX_TELNET; i++)
if (telnetClients[i] && telnetClients[i].connected())
telnetClients[i].write(data, len);
#if HAS_BT_CLASSIC
if (SerialBT.connected()) SerialBT.write(data, len);
#endif
logBytes(data, len);
}
// Anything (web/telnet/BT) -> target UART.
void forwardToTarget(const uint8_t* data, size_t len) {
txBytes += len;
activePort->write(data, len);
}
// ============================================================================
// Command handling (telnet ~cmds and REST share this)
// ============================================================================
static const char* portName(SerialPortId p) {
return p == PORT_INTERNAL ? "internal" : p == PORT_USB ? "usb" : "external";
}
void printStatus(Stream* o) {
o->printf("[status] port=%s baud=%lu rx=%lu tx=%lu telnet=%d log=%s ntp=%s heap=%u\r\n",
portName(currentPort), (unsigned long)baudSerial1,
(unsigned long)rxBytes, (unsigned long)txBytes,
[]{ int n=0; for (int i=0;i<MAX_TELNET;i++) if (telnetClients[i]&&telnetClients[i].connected()) n++; return n; }(),
logEnabled ? "on" : "off", ntpSynced ? "yes" : "no",
(unsigned)ESP.getFreeHeap());
}
// `line` is the text AFTER the CTRL_PREFIX.
void handleCommand(const char* line, Stream* reply) {
char verb[16] = {0};
int n = 0;
while (line[n] && line[n] != ' ' && n < 15) { verb[n] = line[n]; n++; }
verb[n] = '\0';
const char* args = line[n] ? line + n + 1 : line + n;
if (!strcmp(verb, "help")) {
reply->print("[help] ~status ~reset [ms] ~wake [ms] ~baud <n> "
"~port <int|usb|ext> ~log on|off ~gpio <pin> <0|1>\r\n"
" (any non-~ line is sent to the target UART)\r\n");
} else if (!strcmp(verb, "status")) {
printStatus(reply);
} else if (!strcmp(verb, "reset")) {
gpioPulse(GPIO_RESET_PIN, (uint32_t)strtoul(args, nullptr, 10), reply, "reset");
} else if (!strcmp(verb, "wake")) {
gpioPulse(GPIO_WAKE_PIN, (uint32_t)strtoul(args, nullptr, 10), reply, "wake");
} else if (!strcmp(verb, "baud")) {
uint32_t b = strtoul(args, nullptr, 10);
if (b) { setBaudRate(b); reply->printf("[ctrl] baud=%lu\r\n", (unsigned long)b); }
else reply->print("[ctrl] usage: ~baud <n>\r\n");
} else if (!strcmp(verb, "port")) {
if (!strncmp(args, "int", 3)) setActivePort(PORT_INTERNAL);
else if (!strncmp(args, "usb", 3)) setActivePort(PORT_USB);
else if (!strncmp(args, "ext", 3)) setActivePort(PORT_EXTERNAL);
reply->printf("[ctrl] port=%s\r\n", portName(currentPort));
} else if (!strcmp(verb, "log")) {
#if BOARD_T3S3
if (!strncmp(args, "on", 2)) logEnabled = true;
else if (!strncmp(args, "off", 3)) logEnabled = false;
if (logEnabled) openLogIfReady();
reply->printf("[ctrl] log=%s file=%s\r\n", logEnabled ? "on" : "off",
logFile ? logPath : "(none)");
#else
reply->print("[ctrl] no SD on this build\r\n");
#endif
} else if (!strcmp(verb, "gpio")) {
int pin = -1, val = 0;
if (sscanf(args, "%d %d", &pin, &val) == 2 && pin >= 0) {
pinMode(pin, OUTPUT);
digitalWrite(pin, val ? HIGH : LOW);
reply->printf("[ctrl] gpio %d = %d\r\n", pin, val ? 1 : 0);
} else reply->print("[ctrl] usage: ~gpio <pin> <0|1>\r\n");
} else {
reply->printf("[ctrl] unknown '~%s' (try ~help)\r\n", verb);
}
}
// ============================================================================
// Telnet
// ============================================================================
void setupTelnet() { telnetServer.begin(); telnetServer.setNoDelay(true); }
void serviceTelnet() {
// Accept new clients into a free slot.
if (telnetServer.hasClient()) {
int slot = -1;
for (int i = 0; i < MAX_TELNET; i++)
if (!telnetClients[i] || !telnetClients[i].connected()) { slot = i; break; }
WiFiClient c = telnetServer.available();
if (slot >= 0) {
telnetClients[slot] = c;
telnetLineLen[slot] = 0;
telnetClients[slot].printf("[dongle] connected -- ~help for commands, "
"other lines go to the target UART\r\n");
} else {
c.println("[dongle] too many clients");
c.stop();
}
}
// Read each client line-by-line.
for (int i = 0; i < MAX_TELNET; i++) {
WiFiClient& cl = telnetClients[i];
if (!cl || !cl.connected()) continue;
while (cl.available()) {
char ch = (char)cl.read();
if (ch == '\r') continue;
if (ch == '\n') {
telnetLine[i][telnetLineLen[i]] = '\0';
if (telnetLineLen[i] > 0 && telnetLine[i][0] == CTRL_PREFIX) {
handleCommand(telnetLine[i] + 1, &cl);
} else {
// Forward the line (with newline) to the target.
forwardToTarget((const uint8_t*)telnetLine[i], telnetLineLen[i]);
const uint8_t nl = '\n';
forwardToTarget(&nl, 1);
}
telnetLineLen[i] = 0;
} else if (telnetLineLen[i] < TELNET_LINE_MAX - 1) {
telnetLine[i][telnetLineLen[i]++] = ch;
}
}
}
}
@@ -132,310 +369,299 @@ void setBaudRate(SerialPortId port, uint32_t baud) {
// ============================================================================
void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t len) {
// Check for command prefix (starts with 0x00)
if (len > 0 && data[0] == 0x00) {
// Command message - parse JSON
String cmdStr = String((char*)(data + 1)).substring(0, len - 1);
JsonDocument doc;
DeserializationError err = deserializeJson(doc, cmdStr);
if (!err) {
const char* cmd = doc["cmd"];
if (strcmp(cmd, "setPort") == 0) {
int port = doc["port"];
setActivePort((SerialPortId)port);
// Send confirmation
String response;
JsonDocument respDoc;
respDoc["type"] = "portChanged";
respDoc["port"] = port;
serializeJson(respDoc, response);
client->text(String((char)0x00) + response);
}
else if (strcmp(cmd, "setBaud") == 0) {
int port = doc["port"];
uint32_t baud = doc["baud"];
setBaudRate((SerialPortId)port, baud);
}
else if (strcmp(cmd, "getStatus") == 0) {
String response;
JsonDocument respDoc;
respDoc["type"] = "status";
respDoc["currentPort"] = currentPort;
respDoc["baudSerial1"] = baudSerial1;
respDoc["btConnected"] = SerialBT.connected();
respDoc["freeHeap"] = ESP.getFreeHeap();
respDoc["wifiMode"] = (WiFi.getMode() == WIFI_STA) ? "station" : "ap";
respDoc["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
if (WiFi.getMode() == WIFI_STA) {
respDoc["rssi"] = WiFi.RSSI();
}
serializeJson(respDoc, response);
client->text(String((char)0x00) + response);
}
if (deserializeJson(doc, cmdStr)) return;
const char* cmd = doc["cmd"];
if (!cmd) return;
if (!strcmp(cmd, "setPort")) {
setActivePort((SerialPortId)(int)doc["port"]);
} else if (!strcmp(cmd, "setBaud")) {
setBaudRate((uint32_t)doc["baud"]);
} else if (!strcmp(cmd, "getStatus")) {
String response; JsonDocument r;
r["type"] = "status";
r["currentPort"] = currentPort;
r["baudSerial1"] = baudSerial1;
r["rx"] = rxBytes; r["tx"] = txBytes;
r["log"] = logEnabled; r["ntp"] = ntpSynced; r["sd"] = sdAvailable;
#if BOARD_T3S3
r["logfile"] = logFile ? logPath : "";
#endif
r["freeHeap"] = ESP.getFreeHeap();
r["wifiMode"] = (WiFi.getMode() == WIFI_STA) ? "station" : "ap";
r["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
serializeJson(r, response);
client->text(String((char)0x00) + response);
}
} else {
// Regular serial data - send to active port
activePort->write(data, len);
forwardToTarget(data, len);
}
}
void onWsEvent(AsyncWebSocket* server, AsyncWebSocketClient* client,
void onWsEvent(AsyncWebSocket*, AsyncWebSocketClient* client,
AwsEventType type, void* arg, uint8_t* data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("[WS] Client #%u connected from %s\n",
client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("[WS] Client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA: {
AwsFrameInfo* info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len) {
handleWebSocketMessage(client, data, len);
}
break;
}
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
if (type == WS_EVT_DATA) {
AwsFrameInfo* info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len)
handleWebSocketMessage(client, data, len);
}
}
// ============================================================================
// Web Server Setup
// REST API
// ============================================================================
static String statusJson() {
JsonDocument doc;
doc["currentPort"] = currentPort;
doc["baudSerial1"] = baudSerial1;
doc["rx"] = rxBytes; doc["tx"] = txBytes;
doc["log"] = logEnabled; doc["ntp"] = ntpSynced; doc["sd"] = sdAvailable;
#if BOARD_T3S3
doc["logfile"] = logFile ? logPath : "";
#endif
doc["freeHeap"] = ESP.getFreeHeap();
doc["wifiMode"] = (WiFi.getMode() == WIFI_STA) ? "station" : "ap";
doc["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
doc["ssid"] = (WiFi.getMode() == WIFI_STA) ? WiFi.SSID() : String(AP_SSID);
if (WiFi.getMode() == WIFI_STA) doc["rssi"] = WiFi.RSSI();
String s; serializeJson(doc, s); return s;
}
void setupWebServer() {
// Serve static files from LittleFS
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
// WebSocket handler
ws.onEvent(onWsEvent);
server.addHandler(&ws);
// API endpoint for status (can be used without WebSocket)
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest* request) {
String response;
JsonDocument doc;
doc["currentPort"] = currentPort;
doc["baudSerial1"] = baudSerial1;
doc["btConnected"] = SerialBT.connected();
doc["freeHeap"] = ESP.getFreeHeap();
doc["wifiMode"] = (WiFi.getMode() == WIFI_STA) ? "station" : "ap";
doc["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
doc["ssid"] = (WiFi.getMode() == WIFI_STA) ? WiFi.SSID() : AP_SSID;
if (WiFi.getMode() == WIFI_STA) {
doc["rssi"] = WiFi.RSSI();
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest* req) {
req->send(200, "application/json", statusJson());
});
server.on("/api/reset", HTTP_GET, [](AsyncWebServerRequest* req) {
uint32_t ms = req->hasParam("ms") ? req->getParam("ms")->value().toInt() : 0;
gpioPulse(GPIO_RESET_PIN, ms, nullptr, "reset");
req->send(200, "text/plain", "reset pulsed\n");
});
server.on("/api/wake", HTTP_GET, [](AsyncWebServerRequest* req) {
uint32_t ms = req->hasParam("ms") ? req->getParam("ms")->value().toInt() : 0;
gpioPulse(GPIO_WAKE_PIN, ms, nullptr, "wake");
req->send(200, "text/plain", "wake pulsed\n");
});
server.on("/api/baud", HTTP_GET, [](AsyncWebServerRequest* req) {
if (req->hasParam("baud")) setBaudRate(req->getParam("baud")->value().toInt());
req->send(200, "application/json", statusJson());
});
server.on("/api/port", HTTP_GET, [](AsyncWebServerRequest* req) {
if (req->hasParam("port")) {
String p = req->getParam("port")->value();
if (p.startsWith("int")) setActivePort(PORT_INTERNAL);
else if (p.startsWith("usb")) setActivePort(PORT_USB);
else if (p.startsWith("ext")) setActivePort(PORT_EXTERNAL);
}
serializeJson(doc, response);
request->send(200, "application/json", response);
req->send(200, "application/json", statusJson());
});
server.on("/api/log", HTTP_GET, [](AsyncWebServerRequest* req) {
if (req->hasParam("on")) {
logEnabled = req->getParam("on")->value().toInt() != 0;
if (logEnabled) openLogIfReady();
}
req->send(200, "application/json", statusJson());
});
server.on("/api/gpio", HTTP_GET, [](AsyncWebServerRequest* req) {
if (req->hasParam("pin") && req->hasParam("val")) {
int pin = req->getParam("pin")->value().toInt();
int val = req->getParam("val")->value().toInt();
if (pin >= 0) { pinMode(pin, OUTPUT); digitalWrite(pin, val ? HIGH : LOW); }
}
req->send(200, "text/plain", "ok\n");
});
server.on("/api/send", HTTP_GET, [](AsyncWebServerRequest* req) {
if (req->hasParam("data")) {
String d = req->getParam("data")->value();
forwardToTarget((const uint8_t*)d.c_str(), d.length());
const uint8_t nl = '\n'; forwardToTarget(&nl, 1);
}
req->send(200, "text/plain", "sent\n");
});
// 404 handler
server.onNotFound([](AsyncWebServerRequest* request) {
request->send(404, "text/plain", "Not found");
#if BOARD_T3S3
// List SD log files: JSON [{name,size,active}].
server.on("/api/logs", HTTP_GET, [](AsyncWebServerRequest* req) {
JsonDocument doc; JsonArray arr = doc.to<JsonArray>();
File dir = SD.open("/logs");
if (dir && dir.isDirectory()) {
for (File f = dir.openNextFile(); f; f = dir.openNextFile()) {
if (f.isDirectory()) continue;
const char* n = f.name();
const char* base = strrchr(n, '/'); base = base ? base + 1 : n;
JsonObject o = arr.add<JsonObject>();
o["name"] = base;
o["size"] = (uint32_t)f.size();
o["active"] = (logFile && strcmp(logPath, n) == 0);
}
}
String s; serializeJson(doc, s);
req->send(200, "application/json", s);
});
// Download a log file. ?name=<basename> (no slashes).
server.on("/api/logfile", HTTP_GET, [](AsyncWebServerRequest* req) {
if (!req->hasParam("name")) { req->send(400, "text/plain", "name?\n"); return; }
String name = req->getParam("name")->value();
if (name.indexOf('/') >= 0 || name.indexOf("..") >= 0) { req->send(400, "text/plain", "bad name\n"); return; }
String p = "/logs/" + name;
if (!SD.exists(p)) { req->send(404, "text/plain", "not found\n"); return; }
if (logFile) logFile.flush(); // flush in-flight bytes first
req->send(SD, p, "text/plain", true); // true => download
});
// Delete a log file (refuses the currently-open one).
server.on("/api/logdelete", HTTP_GET, [](AsyncWebServerRequest* req) {
if (!req->hasParam("name")) { req->send(400, "text/plain", "name?\n"); return; }
String name = req->getParam("name")->value();
if (name.indexOf('/') >= 0 || name.indexOf("..") >= 0) { req->send(400, "text/plain", "bad name\n"); return; }
String p = "/logs/" + name;
if (logFile && strcmp(logPath, p.c_str()) == 0) { req->send(409, "text/plain", "active log -- ~log off first\n"); return; }
bool ok = SD.remove(p);
req->send(ok ? 200 : 404, "text/plain", ok ? "deleted\n" : "not found\n");
});
#endif
server.onNotFound([](AsyncWebServerRequest* req) { req->send(404, "text/plain", "Not found"); });
server.begin();
Serial.println("[Web] Server started");
}
// ============================================================================
// WiFi Setup
// WiFi
// ============================================================================
bool connectToWiFi() {
Serial.printf("[WiFi] Connecting to %s", STR(STA_SSID));
WiFi.mode(WIFI_STA);
WiFi.begin(STR(STA_SSID), STR(STA_PASSWORD));
unsigned long startTime = millis();
unsigned long t0 = millis();
while (WiFi.status() != WL_CONNECTED) {
if (millis() - startTime > WIFI_CONNECT_TIMEOUT) {
Serial.println(" TIMEOUT");
return false;
}
delay(500);
Serial.print(".");
if (millis() - t0 > WIFI_CONNECT_TIMEOUT) return false;
delay(500); Serial.print(".");
}
Serial.println(" OK");
Serial.printf("[WiFi] Connected to: %s\n", STR(STA_SSID));
Serial.printf("[WiFi] IP Address: %s\n", WiFi.localIP().toString().c_str());
Serial.printf("[WiFi] Signal strength: %d dBm\n", WiFi.RSSI());
Serial.printf("\n[WiFi] %s IP %s %d dBm\n", STR(STA_SSID),
WiFi.localIP().toString().c_str(), WiFi.RSSI());
return true;
}
void startAccessPoint() {
WiFi.mode(WIFI_AP);
WiFi.softAP(AP_SSID, AP_PASSWORD);
IPAddress ip = WiFi.softAPIP();
Serial.println("[WiFi] Access Point started");
Serial.printf("[WiFi] SSID: %s\n", AP_SSID);
Serial.printf("[WiFi] Password: %s\n", AP_PASSWORD);
Serial.printf("[WiFi] IP Address: %s\n", ip.toString().c_str());
Serial.printf("[WiFi] AP %s IP %s\n", AP_SSID, WiFi.softAPIP().toString().c_str());
}
void setupWiFi() {
WiFi.disconnect(true); // Clear any previous connection
delay(100);
if (WIFI_STATION_MODE) {
// Try to connect to existing network
if (!connectToWiFi()) {
Serial.println("[WiFi] Station mode failed, falling back to AP mode");
startAccessPoint();
}
} else {
// Start as Access Point directly
startAccessPoint();
}
WiFi.disconnect(true); delay(100);
if (!WIFI_STATION_MODE || !connectToWiFi()) startAccessPoint();
}
// ============================================================================
// Bluetooth Setup
// T3-S3 peripherals: NTP, SD, OLED
// ============================================================================
void setupBluetooth() {
if (!SerialBT.begin(BT_NAME)) {
Serial.println("[BT] Failed to initialize Bluetooth");
return;
}
Serial.printf("[BT] Bluetooth started as '%s'\n", BT_NAME);
Serial.println("[BT] Ready for pairing");
#if BOARD_T3S3
void setupSD() {
sdSPI.begin(T3S3_SD_SCK, T3S3_SD_MISO, T3S3_SD_MOSI, T3S3_SD_CS);
sdAvailable = SD.begin(T3S3_SD_CS, sdSPI);
Serial.printf("[SD] %s (CS=%d)\n", sdAvailable ? "mounted" : "NOT found -- check T3S3_SD_* pins",
T3S3_SD_CS);
}
// ============================================================================
// Debug Output Helper
// ============================================================================
void setupNTP() { configTzTime(NTP_TZ, "pool.ntp.org", "time.google.com"); }
// Use this function in your code to write to the internal virtual serial
// These messages will be visible when PORT_INTERNAL is selected
void debugPrint(const char* format, ...) {
char buffer[256];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
internalSerial.print(buffer);
void setupOLED() {
Wire.begin(T3S3_OLED_SDA, T3S3_OLED_SCL);
oledOk = oled.begin(SSD1306_SWITCHCAPVCC, T3S3_OLED_ADDR);
if (oledOk) { oled.clearDisplay(); oled.setTextColor(SSD1306_WHITE); oled.setTextSize(1); oled.display(); }
Serial.printf("[OLED] %s\n", oledOk ? "ok" : "NOT found");
}
void debugPrintln(const char* format, ...) {
char buffer[256];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
internalSerial.println(buffer);
void renderOLED() {
if (!oledOk) return;
int clients = 0;
for (int i = 0; i < MAX_TELNET; i++) if (telnetClients[i] && telnetClients[i].connected()) clients++;
String ip = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
oled.clearDisplay();
oled.setCursor(0, 0); oled.println(ip);
oled.setCursor(0, 10); oled.printf("%s %ddBm\n",
(WiFi.getMode()==WIFI_STA)?WiFi.SSID().c_str():AP_SSID,
(WiFi.getMode()==WIFI_STA)?WiFi.RSSI():0);
oled.setCursor(0, 20); oled.printf("uart %d/%d %lu\n", TARGET_RX_PIN, TARGET_TX_PIN, (unsigned long)baudSerial1);
oled.setCursor(0, 30); oled.printf("rx %lu tx %lu\n", (unsigned long)rxBytes, (unsigned long)txBytes);
oled.setCursor(0, 40); oled.printf("telnet %d log %s\n", clients, (logEnabled && logFile) ? "ON" : "off");
oled.setCursor(0, 50); oled.printf("ntp %s sd %s\n", ntpSynced ? "ok" : "--", sdAvailable ? "ok" : "--");
oled.display();
}
#endif
// ============================================================================
// Main Setup
// Setup / Loop
// ============================================================================
void setup() {
// Initialize USB serial (for local debugging)
Serial.begin(DEFAULT_BAUD_SERIAL);
delay(1000);
delay(500);
Serial.println("\n=== ESP32 Debug Dongle ===");
Serial.println("\n\n========================================");
Serial.println(" ESP32 Debug Dongle");
Serial.println("========================================\n");
Serial1.begin(baudSerial1, SERIAL_8N1, TARGET_RX_PIN, TARGET_TX_PIN);
Serial.printf("[Serial1] %lu baud RX=%d TX=%d\n", baudSerial1, TARGET_RX_PIN, TARGET_TX_PIN);
// Initialize Serial1 for external device
Serial1.begin(baudSerial1, SERIAL_8N1, SERIAL1_RX_PIN, SERIAL1_TX_PIN);
Serial.printf("[Serial1] Initialized at %lu baud (RX=%d, TX=%d)\n",
baudSerial1, SERIAL1_RX_PIN, SERIAL1_TX_PIN);
gpioInit(GPIO_RESET_PIN);
gpioInit(GPIO_WAKE_PIN);
setActivePort(currentPort);
// Initialize LittleFS
if (!LittleFS.begin(true)) {
Serial.println("[FS] LittleFS mount failed!");
} else {
Serial.println("[FS] LittleFS mounted");
}
if (!LittleFS.begin(true)) Serial.println("[FS] LittleFS mount failed");
#if BOARD_T3S3
setupOLED();
setupSD();
#endif
// Setup WiFi Access Point
setupWiFi();
// Setup Bluetooth
setupBluetooth();
#if BOARD_T3S3
if (WiFi.getMode() == WIFI_STA) setupNTP();
#endif
// Setup Web Server
#if HAS_BT_CLASSIC
if (SerialBT.begin(BT_NAME)) Serial.printf("[BT] '%s' ready\n", BT_NAME);
#endif
setupTelnet();
setupWebServer();
Serial.println("\n[System] Ready!");
String ip = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
Serial.printf("[System] Open http://%s in browser\n", ip.c_str());
Serial.println("[System] Or connect via Bluetooth\n");
// Send initial message to internal serial
debugPrintln("ESP32 Debug Dongle initialized");
debugPrintln("Free heap: %d bytes", ESP.getFreeHeap());
Serial.printf("[Ready] http://%s telnet %s 23\n", ip.c_str(), ip.c_str());
}
// ============================================================================
// Main Loop
// ============================================================================
// Buffer for efficient serial reading
static uint8_t serialBuffer[256];
void loop() {
// Read from active serial port and send to WebSocket + Bluetooth
size_t available = activePort->available();
if (available > 0) {
size_t toRead = min(available, sizeof(serialBuffer));
size_t bytesRead = 0;
// Read bytes into buffer
for (size_t i = 0; i < toRead; i++) {
int c = activePort->read();
if (c >= 0) {
serialBuffer[bytesRead++] = (uint8_t)c;
}
}
if (bytesRead > 0) {
// Send to all WebSocket clients
ws.binaryAll(serialBuffer, bytesRead);
// Send to Bluetooth if connected
if (SerialBT.connected()) {
SerialBT.write(serialBuffer, bytesRead);
}
}
// Target UART -> sinks
size_t avail = activePort->available();
if (avail > 0) {
size_t toRead = min(avail, sizeof(serialBuffer));
size_t got = 0;
for (size_t i = 0; i < toRead; i++) { int c = activePort->read(); if (c >= 0) serialBuffer[got++] = (uint8_t)c; }
if (got > 0) broadcastFromTarget(serialBuffer, got);
}
// Read from Bluetooth and send to active serial port
while (SerialBT.available()) {
int c = SerialBT.read();
if (c >= 0) {
activePort->write((uint8_t)c);
}
}
#if HAS_BT_CLASSIC
while (SerialBT.available()) { int c = SerialBT.read(); if (c >= 0) { uint8_t b = (uint8_t)c; forwardToTarget(&b, 1); } }
#endif
// Cleanup disconnected WebSocket clients
serviceTelnet();
ws.cleanupClients();
// Periodic internal debug messages (example)
static unsigned long lastDebug = 0;
if (millis() - lastDebug > 30000) { // Every 30 seconds
lastDebug = millis();
debugPrintln("[%lu] Heartbeat - Heap: %d, WS clients: %d",
millis() / 1000, ESP.getFreeHeap(), ws.count());
}
#if BOARD_T3S3
// Once NTP lands, mark synced + open today's log.
if (!ntpSynced && time(nullptr) > 1700000000) { ntpSynced = true; if (logEnabled) openLogIfReady(); }
static unsigned long lastOled = 0;
if (millis() - lastOled > 500) { lastOled = millis(); renderOLED(); }
#endif
// Small delay to prevent WDT issues
delay(1);
}