meshncore version

This commit is contained in:
2026-06-16 17:42:45 +10:00
parent 172c903f09
commit 1706186727
5 changed files with 720 additions and 6 deletions
+126 -1
View File
@@ -27,6 +27,7 @@
#include <LittleFS.h>
#include <ArduinoJson.h>
#include "LoopbackStream.h"
#include "meshcore_link.h"
#if HAS_BT_CLASSIC
#include "BluetoothSerial.h"
@@ -129,7 +130,9 @@ bool sdAvailable = false;
bool logEnabled = true; // ON by default once we have a date
bool ntpSynced = false;
#if BOARD_T3S3
SPIClass sdSPI(FSPI);
// SD on HSPI (SPI3). The MeshCore LoRa radio (env:t3s3_mesh) is pinned to FSPI
// (SPI2) in meshcore_link.cpp, so the two SPI peripherals never collide.
SPIClass sdSPI(HSPI);
File logFile;
bool logAtLineStart = true;
char logPath[40] = {0};
@@ -143,6 +146,7 @@ bool oledOk = false;
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);
static const char* portName(SerialPortId p);
// ============================================================================
// Serial Port Management
@@ -201,6 +205,14 @@ static void openLogIfReady() {
logFile = SD.open(logPath, FILE_WRITE);
if (logFile) {
logFile.printf("# debug-dongle log opened %s\n", logPath);
logFile.printf("# uart: port=%s baud=%lu rx=%d tx=%d\n",
portName(currentPort), (unsigned long)baudSerial1,
TARGET_RX_PIN, TARGET_TX_PIN);
if (mc::enabled()) {
logFile.printf("# mesh: %s node=%s channel=%s psk=%s %s\n",
mc::up() ? "up" : "down", mc::nodeName(),
mc::channelName(), mc::pskB64(), mc::radioConfig());
}
logFile.flush();
logAtLineStart = true;
}
@@ -250,6 +262,82 @@ void forwardToTarget(const uint8_t* data, size_t len) {
activePort->write(data, len);
}
// ============================================================================
// MeshCore comms panel plumbing (mc:: is no-ops when USE_MESHCORE is unset, so
// none of this needs #if guards -- the UI/telnet just report "not in build").
// ============================================================================
bool meshEchoTelnet = true; // mirror received/sent mesh lines to telnet+USB
static void wsBroadcastJson(JsonDocument& doc) {
String s; serializeJson(doc, s);
ws.textAll(String((char)0x00) + s); // 0x00-prefixed JSON text frame
}
// Build one mesh line and fan it out: SD log (always, timestamped per line via
// logBytes) + telnet/USB mirror (only when ~mesh echo is on).
static void meshEchoLine(const char* dir, const char* channel, const char* who,
const char* text, int rssi, float snr) {
char line[300];
int n;
if (rssi != 0)
n = snprintf(line, sizeof(line), "[mesh %s %s] %s: %s (rssi %d snr %.1f)\r\n",
dir, channel, who, text, rssi, snr);
else
n = snprintf(line, sizeof(line), "[mesh %s %s] %s: %s\r\n", dir, channel, who, text);
if (n < 0) return;
if (n >= (int)sizeof(line)) n = sizeof(line) - 1;
logBytes((const uint8_t*)line, n); // -> SD (no-op when off / not T3-S3)
if (meshEchoTelnet) {
for (int i = 0; i < MAX_TELNET; i++)
if (telnetClients[i] && telnetClients[i].connected()) telnetClients[i].print(line);
Serial.print(line);
}
}
// Received channel message -- called from mc::loop() in the main task.
void onMeshRx(const char* channel, const char* sender, const char* text, int rssi, float snr) {
JsonDocument d;
d["type"] = "mesh"; d["dir"] = "rx";
d["channel"] = channel; d["sender"] = sender; d["text"] = text;
d["rssi"] = rssi; d["snr"] = snr;
wsBroadcastJson(d);
meshEchoLine("rx", channel, sender, text, rssi, snr);
}
// Optimistically echo a locally-sent message to the web panel + telnet.
static void meshEchoTx(const char* text) {
JsonDocument d;
d["type"] = "mesh"; d["dir"] = "tx";
d["channel"] = mc::channelName(); d["sender"] = mc::nodeName(); d["text"] = text;
wsBroadcastJson(d);
meshEchoLine("tx", mc::channelName(), mc::nodeName(), text, 0, 0.0f);
}
static void meshBuildCfg(JsonDocument& d) {
d["type"] = "meshcfg";
d["enabled"] = mc::enabled();
d["up"] = mc::up();
d["channel"] = mc::channelName();
d["psk"] = mc::pskB64();
d["rx"] = mc::rxCount();
d["tx"] = mc::txCount();
}
static void meshBroadcastCfg() { JsonDocument d; meshBuildCfg(d); wsBroadcastJson(d); }
// Note a (re)programmed channel/PSK in the SD log so the capture is self-describing.
static void meshLogCfg() {
char cfg[200];
int n = snprintf(cfg, sizeof(cfg), "[mesh cfg] channel=%s psk=%s %s\r\n",
mc::channelName(), mc::pskB64(), mc::radioConfig());
if (n < 0) return;
if (n >= (int)sizeof(cfg)) n = sizeof(cfg) - 1;
logBytes((const uint8_t*)cfg, n);
}
// ============================================================================
// Command handling (telnet ~cmds and REST share this)
// ============================================================================
@@ -278,6 +366,7 @@ void handleCommand(const char* line, Stream* reply) {
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"
" ~mesh [on|off] ~psk <base64key> ~chan <name> <base64key> ~msg <text>\r\n"
" (any non-~ line is sent to the target UART)\r\n");
} else if (!strcmp(verb, "status")) {
printStatus(reply);
@@ -311,6 +400,26 @@ void handleCommand(const char* line, Stream* reply) {
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 if (!strcmp(verb, "mesh")) {
if (!strncmp(args, "on", 2)) { meshEchoTelnet = true; reply->print("[mesh] echo on\r\n"); }
else if (!strncmp(args, "off", 3)) { meshEchoTelnet = false; reply->print("[mesh] echo off\r\n"); }
else reply->printf("[mesh] %s up=%s channel=%s psk=%s rx=%lu tx=%lu echo=%s\r\n",
mc::enabled() ? "enabled" : "(not in build)", mc::up() ? "yes" : "no",
mc::channelName(), mc::pskB64(),
(unsigned long)mc::rxCount(), (unsigned long)mc::txCount(),
meshEchoTelnet ? "on" : "off");
} else if (!strcmp(verb, "psk")) {
if (*args) { mc::requestPsk("", args); reply->printf("[mesh] psk queued: %s\r\n", args); }
else reply->print("[mesh] usage: ~psk <base64 16- or 32-byte key>\r\n");
} else if (!strcmp(verb, "chan")) {
char nm[32] = {0}, pk[48] = {0};
if (sscanf(args, "%31s %47s", nm, pk) == 2) {
mc::requestPsk(nm, pk);
reply->printf("[mesh] channel '%s' queued\r\n", nm);
} else reply->print("[mesh] usage: ~chan <name> <base64key>\r\n");
} else if (!strcmp(verb, "msg")) {
if (*args) { mc::requestSend(args); meshEchoTx(args); }
else reply->print("[mesh] usage: ~msg <text>\r\n");
} else {
reply->printf("[ctrl] unknown '~%s' (try ~help)\r\n", verb);
}
@@ -394,6 +503,17 @@ void handleWebSocketMessage(AsyncWebSocketClient* client, uint8_t* data, size_t
r["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
serializeJson(r, response);
client->text(String((char)0x00) + response);
} else if (!strcmp(cmd, "meshSend")) {
const char* t = doc["text"];
if (t && *t) { mc::requestSend(t); meshEchoTx(t); }
} else if (!strcmp(cmd, "meshPsk")) {
const char* nm = doc["name"];
const char* pk = doc["psk"];
if (pk && *pk) mc::requestPsk(nm ? nm : "", pk);
} else if (!strcmp(cmd, "meshGet")) {
JsonDocument r; meshBuildCfg(r);
String resp; serializeJson(r, resp);
client->text(String((char)0x00) + resp);
}
} else {
forwardToTarget(data, len);
@@ -633,6 +753,8 @@ void setup() {
setupTelnet();
setupWebServer();
mc::begin(onMeshRx); // MeshCore radio (no-op unless USE_MESHCORE)
String ip = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
Serial.printf("[Ready] http://%s telnet %s 23\n", ip.c_str(), ip.c_str());
}
@@ -656,6 +778,9 @@ void loop() {
serviceTelnet();
ws.cleanupClients();
mc::loop(); // pump radio + drain mesh queue
if (mc::consumeCfgChanged()) { meshBroadcastCfg(); meshLogCfg(); }
#if BOARD_T3S3
// Once NTP lands, mark synced + open today's log.
if (!ntpSynced && time(nullptr) > 1700000000) { ntpSynced = true; if (logEnabled) openLogIfReady(); }