meshncore version
This commit is contained in:
+178
-5
@@ -109,16 +109,85 @@
|
|||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-row {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.terminal-container {
|
.terminal-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#terminal {
|
#terminal {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Right-side comms panel (MeshCore today, more later) */
|
||||||
|
.comms-panel {
|
||||||
|
flex: 0 0 340px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #16213e;
|
||||||
|
border-left: 1px solid #0f3460;
|
||||||
|
padding: 10px 12px;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.comms-panel h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #e94560;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.comms-cfg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.comms-cfg input {
|
||||||
|
background: #0f3460;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #1a4a7a;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.comms-cfg .row { display: flex; gap: 6px; }
|
||||||
|
.comms-cfg .row input { flex: 1; }
|
||||||
|
#meshMessages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #0f1a30;
|
||||||
|
border: 1px solid #0f3460;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-family: Menlo, Monaco, "Courier New", monospace;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.4;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
.mc-msg { margin-bottom: 4px; word-break: break-word; }
|
||||||
|
.mc-msg .who { color: #60a5fa; }
|
||||||
|
.mc-msg.tx .who { color: #4ade80; }
|
||||||
|
.mc-msg .meta { color: #666; font-size: 0.85em; }
|
||||||
|
.mc-empty { color: #666; }
|
||||||
|
.comms-send { display: flex; gap: 6px; }
|
||||||
|
.comms-send input { flex: 1; }
|
||||||
|
.mesh-dot {
|
||||||
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
|
background: #666; display: inline-block;
|
||||||
|
}
|
||||||
|
.mesh-dot.up { background: #4ade80; }
|
||||||
|
.mesh-dot.down { background: #ef4444; }
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
@@ -138,6 +207,14 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.main-row { flex-direction: column; }
|
||||||
|
.comms-panel {
|
||||||
|
flex: 0 0 45vh;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid #0f3460;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -194,10 +271,31 @@
|
|||||||
<table id="filesTable" style="width:100%; border-collapse:collapse; font-size:0.85em;"></table>
|
<table id="filesTable" style="width:100%; border-collapse:collapse; font-size:0.85em;"></table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="terminal-container">
|
<div class="main-row">
|
||||||
<div id="terminal"></div>
|
<div class="terminal-container">
|
||||||
|
<div id="terminal"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comms-panel" id="commsPanel">
|
||||||
|
<h2>
|
||||||
|
<span>📡 MeshCore</span>
|
||||||
|
<span><span class="mesh-dot" id="meshDot"></span> <span id="meshState" style="font-size:0.75em;color:#aaa;">…</span></span>
|
||||||
|
</h2>
|
||||||
|
<div class="comms-cfg">
|
||||||
|
<input type="text" id="meshChan" placeholder="Channel name (e.g. SensorsTest)">
|
||||||
|
<div class="row">
|
||||||
|
<input type="text" id="meshPsk" placeholder="PSK (base64, 16 or 32-byte key)">
|
||||||
|
<button onclick="programChannel()">Program</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="meshMessages"><div class="mc-empty">No messages yet.</div></div>
|
||||||
|
<div class="comms-send">
|
||||||
|
<input type="text" id="meshInput" placeholder="Message…" onkeydown="if(event.key==='Enter')sendMesh()">
|
||||||
|
<button onclick="sendMesh()">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<span id="rxCount">RX: 0 bytes</span>
|
<span id="rxCount">RX: 0 bytes</span>
|
||||||
<span id="txCount">TX: 0 bytes</span>
|
<span id="txCount">TX: 0 bytes</span>
|
||||||
@@ -272,6 +370,7 @@
|
|||||||
|
|
||||||
// Request initial status
|
// Request initial status
|
||||||
sendCommand({ cmd: 'getStatus' });
|
sendCommand({ cmd: 'getStatus' });
|
||||||
|
sendCommand({ cmd: 'meshGet' });
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
@@ -293,6 +392,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
|
// Control/mesh events arrive as text frames (0x00-prefixed JSON).
|
||||||
|
if (typeof event.data === 'string') {
|
||||||
|
let s = event.data;
|
||||||
|
if (s.charCodeAt(0) === 0) s = s.slice(1);
|
||||||
|
try { handleResponse(JSON.parse(s)); } catch (e) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.data instanceof ArrayBuffer) {
|
if (event.data instanceof ArrayBuffer) {
|
||||||
const data = new Uint8Array(event.data);
|
const data = new Uint8Array(event.data);
|
||||||
|
|
||||||
@@ -344,8 +450,75 @@
|
|||||||
const portNames = ['Internal', 'USB Serial', 'External'];
|
const portNames = ['Internal', 'USB Serial', 'External'];
|
||||||
term.writeln(`\r\n\x1b[33m[Switched to ${portNames[msg.port]}]\x1b[0m\r\n`);
|
term.writeln(`\r\n\x1b[33m[Switched to ${portNames[msg.port]}]\x1b[0m\r\n`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'mesh':
|
||||||
|
appendMeshMsg(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'meshcfg':
|
||||||
|
updateMeshCfg(msg);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- MeshCore comms panel ----
|
||||||
|
let meshEnabled = false;
|
||||||
|
function updateMeshCfg(c) {
|
||||||
|
meshEnabled = !!c.enabled;
|
||||||
|
const dot = document.getElementById('meshDot');
|
||||||
|
const state = document.getElementById('meshState');
|
||||||
|
const chan = document.getElementById('meshChan');
|
||||||
|
const psk = document.getElementById('meshPsk');
|
||||||
|
if (!meshEnabled) {
|
||||||
|
dot.className = 'mesh-dot';
|
||||||
|
state.textContent = 'not in build';
|
||||||
|
} else if (c.up) {
|
||||||
|
dot.className = 'mesh-dot up';
|
||||||
|
state.textContent = `up · rx ${c.rx} tx ${c.tx}`;
|
||||||
|
} else {
|
||||||
|
dot.className = 'mesh-dot down';
|
||||||
|
state.textContent = 'radio down';
|
||||||
|
}
|
||||||
|
// Don't clobber a field the user is editing.
|
||||||
|
if (document.activeElement !== chan && typeof c.channel !== 'undefined') chan.value = c.channel;
|
||||||
|
if (document.activeElement !== psk && typeof c.psk !== 'undefined') psk.value = c.psk;
|
||||||
|
const disabled = !meshEnabled;
|
||||||
|
document.getElementById('meshInput').disabled = disabled;
|
||||||
|
}
|
||||||
|
function appendMeshMsg(m) {
|
||||||
|
const box = document.getElementById('meshMessages');
|
||||||
|
const empty = box.querySelector('.mc-empty');
|
||||||
|
if (empty) empty.remove();
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'mc-msg' + (m.dir === 'tx' ? ' tx' : '');
|
||||||
|
const t = new Date().toLocaleTimeString();
|
||||||
|
let meta = m.dir === 'tx' ? '↑' : '↓';
|
||||||
|
if (m.channel) meta += ' ' + m.channel;
|
||||||
|
if (typeof m.rssi !== 'undefined' && m.rssi) meta += ` ${m.rssi}dBm`;
|
||||||
|
div.innerHTML = `<span class="meta">[${t}] ${escapeHtml(meta)}</span> ` +
|
||||||
|
`<span class="who">${escapeHtml(m.sender || '?')}:</span> ` +
|
||||||
|
escapeHtml(m.text || '');
|
||||||
|
box.appendChild(div);
|
||||||
|
box.scrollTop = box.scrollHeight;
|
||||||
|
}
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s).replace(/[&<>"']/g, c =>
|
||||||
|
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
|
}
|
||||||
|
function programChannel() {
|
||||||
|
const name = document.getElementById('meshChan').value.trim();
|
||||||
|
const psk = document.getElementById('meshPsk').value.trim();
|
||||||
|
if (!psk) { alert('Enter a PSK (base64 16- or 32-byte key)'); return; }
|
||||||
|
sendCommand({ cmd: 'meshPsk', name: name, psk: psk });
|
||||||
|
setTimeout(() => sendCommand({ cmd: 'meshGet' }), 300);
|
||||||
|
}
|
||||||
|
function sendMesh() {
|
||||||
|
const inp = document.getElementById('meshInput');
|
||||||
|
const text = inp.value;
|
||||||
|
if (!text) return;
|
||||||
|
sendCommand({ cmd: 'meshSend', text: text });
|
||||||
|
inp.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
function sendCommand(cmd) {
|
function sendCommand(cmd) {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
|||||||
@@ -103,3 +103,80 @@ lib_deps =
|
|||||||
adafruit/Adafruit GFX Library
|
adafruit/Adafruit GFX Library
|
||||||
extra_scripts =
|
extra_scripts =
|
||||||
pre:scripts/download_xterm.py
|
pre:scripts/download_xterm.py
|
||||||
|
|
||||||
|
; ============================================================================
|
||||||
|
; LilyGo T3-S3 + MeshCore radio. Extends env:t3s3 (OLED + SD logging) and
|
||||||
|
; adds the SX1276 LoRa link so the web "MeshCore" panel and telnet ~psk/~msg
|
||||||
|
; commands work. Builds on the same hardware; pins match MeshCore's
|
||||||
|
; experiment/lilygot3s3logger (the proven T3-S3 SX1276 reference).
|
||||||
|
;
|
||||||
|
; pio run -e t3s3_mesh -t upload && pio run -e t3s3_mesh -t uploadfs
|
||||||
|
; ============================================================================
|
||||||
|
[env:t3s3_mesh]
|
||||||
|
extends = env:t3s3
|
||||||
|
; ed25519 (MeshCore's bundled identity crypto) lives in MeshCore/lib.
|
||||||
|
lib_extra_dirs =
|
||||||
|
/home/scottp/github/MeshCore/lib
|
||||||
|
lib_ldf_mode = deep+
|
||||||
|
build_unflags =
|
||||||
|
-DBOARD_HAS_PSRAM
|
||||||
|
lib_deps =
|
||||||
|
${env:t3s3.lib_deps}
|
||||||
|
symlink:///home/scottp/github/MeshCore
|
||||||
|
jgromes/RadioLib @ ^7.6.0
|
||||||
|
rweather/Crypto @ ^0.4.0
|
||||||
|
adafruit/RTClib @ ^2.1.3
|
||||||
|
densaugeo/base64 @ ~1.4.0
|
||||||
|
Preferences
|
||||||
|
ed25519
|
||||||
|
build_flags =
|
||||||
|
${env:t3s3.build_flags}
|
||||||
|
-w
|
||||||
|
-DUSE_MESHCORE=1
|
||||||
|
-D ESP32_PLATFORM
|
||||||
|
; --- pin 21 is the SX1276 RX-enable on the T3-S3, so the OLED can't use it
|
||||||
|
; as a reset line here; SSD1306 over I2C runs fine with no reset pin. ---
|
||||||
|
-DT3S3_OLED_RST=-1
|
||||||
|
; --- RadioLib: lock to the SX127x family ---
|
||||||
|
-D RADIOLIB_STATIC_ONLY=1
|
||||||
|
-D RADIOLIB_GODMODE=1
|
||||||
|
-D RADIOLIB_EXCLUDE_CC1101=1
|
||||||
|
-D RADIOLIB_EXCLUDE_RF69=1
|
||||||
|
-D RADIOLIB_EXCLUDE_SX1231=1
|
||||||
|
-D RADIOLIB_EXCLUDE_SI443X=1
|
||||||
|
-D RADIOLIB_EXCLUDE_RFM2X=1
|
||||||
|
-D RADIOLIB_EXCLUDE_SX128X=1
|
||||||
|
-D RADIOLIB_EXCLUDE_AFSK=1
|
||||||
|
-D RADIOLIB_EXCLUDE_AX25=1
|
||||||
|
-D RADIOLIB_EXCLUDE_HELLSCHREIBER=1
|
||||||
|
-D RADIOLIB_EXCLUDE_MORSE=1
|
||||||
|
-D RADIOLIB_EXCLUDE_APRS=1
|
||||||
|
-D RADIOLIB_EXCLUDE_BELL=1
|
||||||
|
-D RADIOLIB_EXCLUDE_RTTY=1
|
||||||
|
-D RADIOLIB_EXCLUDE_SSTV=1
|
||||||
|
; --- T3-S3 v1.2 SX1276 pin map (MeshCore variants/lilygo_t3s3_sx1276) ---
|
||||||
|
-D USE_SX1276
|
||||||
|
-D P_LORA_NSS=7
|
||||||
|
-D P_LORA_RESET=8
|
||||||
|
-D P_LORA_DIO_0=9
|
||||||
|
-D P_LORA_DIO_1=33
|
||||||
|
-D P_LORA_SCLK=5
|
||||||
|
-D P_LORA_MISO=3
|
||||||
|
-D P_LORA_MOSI=6
|
||||||
|
-D P_LORA_TX_LED=37
|
||||||
|
-D SX127X_CURRENT_LIMIT=120
|
||||||
|
-D SX176X_RXEN=21
|
||||||
|
-D SX176X_TXEN=10
|
||||||
|
; --- LoRa PHY: Australia Narrow (match the sensor fleet) ---
|
||||||
|
-D LORA_FREQ=916.575
|
||||||
|
-D LORA_BW=62.5
|
||||||
|
-D LORA_SF=7
|
||||||
|
-D LORA_CR=8
|
||||||
|
-D LORA_TX_POWER=20
|
||||||
|
-D MAX_GROUP_CHANNELS=4
|
||||||
|
-D MAX_CONTACTS=8
|
||||||
|
; --- channel defaults (runtime-overridable + persisted in NVS) ---
|
||||||
|
-D NODE_NAME='"dongle"'
|
||||||
|
-D SENSORS_CHANNEL_NAME='"SensorsTest"'
|
||||||
|
-D SENSORS_CHANNEL_PSK_B64='"PNtgMxiq9R7eQ3IleHoL3g=="'
|
||||||
|
-D PUBLIC_CHANNEL_PSK_B64='"izOH6cXN6mrJ5e26oRXNcg=="'
|
||||||
|
|||||||
+126
-1
@@ -27,6 +27,7 @@
|
|||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include "LoopbackStream.h"
|
#include "LoopbackStream.h"
|
||||||
|
#include "meshcore_link.h"
|
||||||
|
|
||||||
#if HAS_BT_CLASSIC
|
#if HAS_BT_CLASSIC
|
||||||
#include "BluetoothSerial.h"
|
#include "BluetoothSerial.h"
|
||||||
@@ -129,7 +130,9 @@ bool sdAvailable = false;
|
|||||||
bool logEnabled = true; // ON by default once we have a date
|
bool logEnabled = true; // ON by default once we have a date
|
||||||
bool ntpSynced = false;
|
bool ntpSynced = false;
|
||||||
#if BOARD_T3S3
|
#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;
|
File logFile;
|
||||||
bool logAtLineStart = true;
|
bool logAtLineStart = true;
|
||||||
char logPath[40] = {0};
|
char logPath[40] = {0};
|
||||||
@@ -143,6 +146,7 @@ bool oledOk = false;
|
|||||||
void broadcastFromTarget(const uint8_t* data, size_t len);
|
void broadcastFromTarget(const uint8_t* data, size_t len);
|
||||||
void forwardToTarget(const uint8_t* data, size_t len);
|
void forwardToTarget(const uint8_t* data, size_t len);
|
||||||
void handleCommand(const char* line, Stream* reply);
|
void handleCommand(const char* line, Stream* reply);
|
||||||
|
static const char* portName(SerialPortId p);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Serial Port Management
|
// Serial Port Management
|
||||||
@@ -201,6 +205,14 @@ static void openLogIfReady() {
|
|||||||
logFile = SD.open(logPath, FILE_WRITE);
|
logFile = SD.open(logPath, FILE_WRITE);
|
||||||
if (logFile) {
|
if (logFile) {
|
||||||
logFile.printf("# debug-dongle log opened %s\n", logPath);
|
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();
|
logFile.flush();
|
||||||
logAtLineStart = true;
|
logAtLineStart = true;
|
||||||
}
|
}
|
||||||
@@ -250,6 +262,82 @@ void forwardToTarget(const uint8_t* data, size_t len) {
|
|||||||
activePort->write(data, 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)
|
// Command handling (telnet ~cmds and REST share this)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -278,6 +366,7 @@ void handleCommand(const char* line, Stream* reply) {
|
|||||||
if (!strcmp(verb, "help")) {
|
if (!strcmp(verb, "help")) {
|
||||||
reply->print("[help] ~status ~reset [ms] ~wake [ms] ~baud <n> "
|
reply->print("[help] ~status ~reset [ms] ~wake [ms] ~baud <n> "
|
||||||
"~port <int|usb|ext> ~log on|off ~gpio <pin> <0|1>\r\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");
|
" (any non-~ line is sent to the target UART)\r\n");
|
||||||
} else if (!strcmp(verb, "status")) {
|
} else if (!strcmp(verb, "status")) {
|
||||||
printStatus(reply);
|
printStatus(reply);
|
||||||
@@ -311,6 +400,26 @@ void handleCommand(const char* line, Stream* reply) {
|
|||||||
digitalWrite(pin, val ? HIGH : LOW);
|
digitalWrite(pin, val ? HIGH : LOW);
|
||||||
reply->printf("[ctrl] gpio %d = %d\r\n", pin, val ? 1 : 0);
|
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->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 {
|
} else {
|
||||||
reply->printf("[ctrl] unknown '~%s' (try ~help)\r\n", verb);
|
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();
|
r["ip"] = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
||||||
serializeJson(r, response);
|
serializeJson(r, response);
|
||||||
client->text(String((char)0x00) + 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 {
|
} else {
|
||||||
forwardToTarget(data, len);
|
forwardToTarget(data, len);
|
||||||
@@ -633,6 +753,8 @@ void setup() {
|
|||||||
setupTelnet();
|
setupTelnet();
|
||||||
setupWebServer();
|
setupWebServer();
|
||||||
|
|
||||||
|
mc::begin(onMeshRx); // MeshCore radio (no-op unless USE_MESHCORE)
|
||||||
|
|
||||||
String ip = (WiFi.getMode() == WIFI_STA) ? WiFi.localIP().toString() : WiFi.softAPIP().toString();
|
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());
|
Serial.printf("[Ready] http://%s telnet %s 23\n", ip.c_str(), ip.c_str());
|
||||||
}
|
}
|
||||||
@@ -656,6 +778,9 @@ void loop() {
|
|||||||
serviceTelnet();
|
serviceTelnet();
|
||||||
ws.cleanupClients();
|
ws.cleanupClients();
|
||||||
|
|
||||||
|
mc::loop(); // pump radio + drain mesh queue
|
||||||
|
if (mc::consumeCfgChanged()) { meshBroadcastCfg(); meshLogCfg(); }
|
||||||
|
|
||||||
#if BOARD_T3S3
|
#if BOARD_T3S3
|
||||||
// Once NTP lands, mark synced + open today's log.
|
// Once NTP lands, mark synced + open today's log.
|
||||||
if (!ntpSynced && time(nullptr) > 1700000000) { ntpSynced = true; if (logEnabled) openLogIfReady(); }
|
if (!ntpSynced && time(nullptr) > 1700000000) { ntpSynced = true; if (logEnabled) openLogIfReady(); }
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
/**
|
||||||
|
* meshcore_link.cpp -- see meshcore_link.h.
|
||||||
|
*
|
||||||
|
* Pattern mirrors MeshCore/experiment/lilygot3s3logger (same board: LilyGo
|
||||||
|
* T3-S3, ESP32-S3 + SX1276), trimmed to a single reprogrammable channel.
|
||||||
|
*/
|
||||||
|
#include "meshcore_link.h"
|
||||||
|
|
||||||
|
#if USE_MESHCORE
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <SPI.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <mbedtls/base64.h> // ESP-IDF builtin; avoids clashing with MeshCore's base64.hpp
|
||||||
|
|
||||||
|
#include <Mesh.h>
|
||||||
|
#include <helpers/BaseChatMesh.h>
|
||||||
|
#include <helpers/ArduinoHelpers.h>
|
||||||
|
#include <helpers/StaticPoolPacketManager.h>
|
||||||
|
#include <helpers/SimpleMeshTables.h>
|
||||||
|
#include <helpers/IdentityStore.h>
|
||||||
|
#include <helpers/ESP32Board.h>
|
||||||
|
#include <helpers/AutoDiscoverRTCClock.h>
|
||||||
|
#include <helpers/radiolib/CustomSX1276.h>
|
||||||
|
#include <helpers/radiolib/CustomSX1276Wrapper.h>
|
||||||
|
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
|
|
||||||
|
// ---- compile-time defaults (override via build flags) ----------------------
|
||||||
|
#ifndef NODE_NAME
|
||||||
|
#define NODE_NAME "dongle"
|
||||||
|
#endif
|
||||||
|
#ifndef SENSORS_CHANNEL_NAME
|
||||||
|
#define SENSORS_CHANNEL_NAME "SensorsTest"
|
||||||
|
#endif
|
||||||
|
#ifndef SENSORS_CHANNEL_PSK_B64
|
||||||
|
#define SENSORS_CHANNEL_PSK_B64 "PNtgMxiq9R7eQ3IleHoL3g=="
|
||||||
|
#endif
|
||||||
|
// Standard MeshCore "Public" channel (same key the companion examples use).
|
||||||
|
#ifndef PUBLIC_CHANNEL_PSK_B64
|
||||||
|
#define PUBLIC_CHANNEL_PSK_B64 "izOH6cXN6mrJ5e26oRXNcg=="
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ---- radio + mesh support objects (file-scope, mirrors the logger) ---------
|
||||||
|
ESP32Board board;
|
||||||
|
// Radio on FSPI (SPI2). The SD card is on HSPI (SPI3, see main.cpp). They MUST
|
||||||
|
// be different peripherals -- SPIClass() defaults to HSPI, which would collide
|
||||||
|
// with the SD bus and corrupt it the moment the radio re-inits the pins.
|
||||||
|
SPIClass lora_spi(FSPI);
|
||||||
|
CustomSX1276 lora(new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, lora_spi));
|
||||||
|
CustomSX1276Wrapper radio_driver(lora, board);
|
||||||
|
ESP32RTCClock fallback_clock;
|
||||||
|
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||||
|
StdRNG fast_rng;
|
||||||
|
SimpleMeshTables tables;
|
||||||
|
|
||||||
|
mc::RxHandler s_on_rx = nullptr;
|
||||||
|
bool s_up = false;
|
||||||
|
volatile bool s_cfg_dirty = false;
|
||||||
|
QueueHandle_t s_queue = nullptr;
|
||||||
|
|
||||||
|
// User-channel state (index 1; "Public" sits at index 0).
|
||||||
|
const int USER_IDX = 1;
|
||||||
|
char s_channel[32] = SENSORS_CHANNEL_NAME;
|
||||||
|
char s_psk[48] = SENSORS_CHANNEL_PSK_B64;
|
||||||
|
|
||||||
|
// ---- queued action (producer = any task, consumer = loop()) ----------------
|
||||||
|
struct McAction {
|
||||||
|
uint8_t kind; // 0 = send text, 1 = reprogram channel
|
||||||
|
char name[32];
|
||||||
|
char text[176];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split "<sender>: <payload>" in place; returns sender (or "?") + payload.
|
||||||
|
void split_sender(char* buf, const char** sender, const char** payload) {
|
||||||
|
*sender = "?";
|
||||||
|
*payload = buf;
|
||||||
|
char* sep = strstr(buf, ": ");
|
||||||
|
if (sep) { *sep = 0; *sender = buf; *payload = sep + 2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the mesh node ---------------------------------------------------------
|
||||||
|
class DongleMesh : public BaseChatMesh {
|
||||||
|
ChannelDetails* _public = nullptr;
|
||||||
|
ChannelDetails* _user = nullptr;
|
||||||
|
ChannelDetails _user_view; // kept in sync for sending
|
||||||
|
uint32_t _rx = 0, _tx = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Unused contact-protocol hooks -- this node only does channel chat.
|
||||||
|
void onDiscoveredContact(ContactInfo&, bool, uint8_t, const uint8_t*) override {}
|
||||||
|
void onContactPathUpdated(const ContactInfo&) override {}
|
||||||
|
ContactInfo* processAck(const uint8_t*) override { return nullptr; }
|
||||||
|
void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {}
|
||||||
|
void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {}
|
||||||
|
void onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override {}
|
||||||
|
uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*) override { return 0; }
|
||||||
|
void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {}
|
||||||
|
void onSendTimeout() override {}
|
||||||
|
uint32_t calcFloodTimeoutMillisFor(uint32_t air) const override { return 500 + 16 * air; }
|
||||||
|
uint32_t calcDirectTimeoutMillisFor(uint32_t air, uint8_t) const override { return 500 + 16 * air; }
|
||||||
|
// Listener only -- do not re-flood other nodes' traffic.
|
||||||
|
bool allowPacketForward(const mesh::Packet*) override { return false; }
|
||||||
|
|
||||||
|
const char* channelName(const mesh::GroupChannel& ch) {
|
||||||
|
int idx = findChannelIdx(ch);
|
||||||
|
if (idx < 0) return "unknown";
|
||||||
|
ChannelDetails cd;
|
||||||
|
if (!getChannel(idx, cd)) return "unknown";
|
||||||
|
return cd.name[0] ? cd.name : "unnamed";
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt,
|
||||||
|
uint32_t, const char* text) override {
|
||||||
|
_rx++;
|
||||||
|
int rssi = (int)lroundf(radio_driver.getLastRSSI());
|
||||||
|
float snr = radio_driver.getLastSNR();
|
||||||
|
const char* chan = channelName(channel);
|
||||||
|
|
||||||
|
char buf[200];
|
||||||
|
strlcpy(buf, text, sizeof(buf));
|
||||||
|
const char *sender, *payload;
|
||||||
|
split_sender(buf, &sender, &payload);
|
||||||
|
|
||||||
|
if (s_on_rx) s_on_rx(chan, sender, payload, rssi, snr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
DongleMesh()
|
||||||
|
: BaseChatMesh(radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock,
|
||||||
|
*new StaticPoolPacketManager(16), tables) {}
|
||||||
|
|
||||||
|
void begin() {
|
||||||
|
BaseChatMesh::begin();
|
||||||
|
|
||||||
|
IdentityStore store(LittleFS, "/identity");
|
||||||
|
if (!store.load("_main", self_id)) {
|
||||||
|
self_id = mesh::LocalIdentity(getRNG());
|
||||||
|
store.save("_main", self_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_public = addChannel("Public", PUBLIC_CHANNEL_PSK_B64);
|
||||||
|
_user = addChannel(s_channel, s_psk); // -> index USER_IDX
|
||||||
|
if (_user) getChannel(USER_IDX, _user_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the user channel's key (and name) at runtime. Recomputes the
|
||||||
|
// channel hash from the decoded secret via setChannel().
|
||||||
|
bool reprogram(const char* name, const char* psk_b64) {
|
||||||
|
ChannelDetails cd;
|
||||||
|
memset(&cd, 0, sizeof(cd));
|
||||||
|
unsigned char key[32];
|
||||||
|
size_t len = 0;
|
||||||
|
if (mbedtls_base64_decode(key, sizeof(key), &len,
|
||||||
|
(const unsigned char*)psk_b64, strlen(psk_b64)) != 0)
|
||||||
|
return false;
|
||||||
|
if (len != 16 && len != 32) return false; // PSK must be a 16- or 32-byte key
|
||||||
|
memcpy(cd.channel.secret, key, len);
|
||||||
|
strlcpy(cd.name, name, sizeof(cd.name));
|
||||||
|
if (!setChannel(USER_IDX, cd)) return false;
|
||||||
|
getChannel(USER_IDX, _user_view);
|
||||||
|
_user = &_user_view;
|
||||||
|
strlcpy(s_channel, name, sizeof(s_channel));
|
||||||
|
strlcpy(s_psk, psk_b64, sizeof(s_psk));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool send(const char* text) {
|
||||||
|
if (!_user) return false;
|
||||||
|
bool ok = sendGroupMessage(getRTCClock()->getCurrentTime(),
|
||||||
|
_user_view.channel, NODE_NAME, text, strlen(text));
|
||||||
|
if (ok) _tx++;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t rxCount() const { return _rx; }
|
||||||
|
uint32_t txCount() const { return _tx; }
|
||||||
|
};
|
||||||
|
|
||||||
|
DongleMesh the_mesh;
|
||||||
|
|
||||||
|
void persist_cfg() {
|
||||||
|
Preferences p;
|
||||||
|
if (p.begin("mcfg", false)) {
|
||||||
|
p.putString("name", s_channel);
|
||||||
|
p.putString("psk", s_psk);
|
||||||
|
p.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_cfg() {
|
||||||
|
Preferences p;
|
||||||
|
if (p.begin("mcfg", true)) {
|
||||||
|
String n = p.getString("name", "");
|
||||||
|
String k = p.getString("psk", "");
|
||||||
|
if (n.length()) strlcpy(s_channel, n.c_str(), sizeof(s_channel));
|
||||||
|
if (k.length()) strlcpy(s_psk, k.c_str(), sizeof(s_psk));
|
||||||
|
p.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace mc {
|
||||||
|
|
||||||
|
bool enabled() { return true; }
|
||||||
|
|
||||||
|
void begin(RxHandler on_rx) {
|
||||||
|
s_on_rx = on_rx;
|
||||||
|
s_queue = xQueueCreate(8, sizeof(McAction));
|
||||||
|
|
||||||
|
load_cfg(); // NVS overrides of channel/psk, if any
|
||||||
|
|
||||||
|
board.begin();
|
||||||
|
fallback_clock.begin();
|
||||||
|
if (!lora.std_init(&lora_spi)) {
|
||||||
|
Serial.println("[mesh] radio init FAILED -- mesh disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fast_rng.begin(lora.random(0x7FFFFFFF));
|
||||||
|
the_mesh.begin();
|
||||||
|
lora.startReceive();
|
||||||
|
s_up = true;
|
||||||
|
Serial.printf("[mesh] up: freq=%.3f bw=%.1f sf=%d cr=%d channel='%s'\n",
|
||||||
|
(float)LORA_FREQ, (float)LORA_BW, (int)LORA_SF, (int)LORA_CR, s_channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (s_queue) {
|
||||||
|
McAction a;
|
||||||
|
while (xQueueReceive(s_queue, &a, 0) == pdTRUE) {
|
||||||
|
if (a.kind == 0) {
|
||||||
|
the_mesh.send(a.text);
|
||||||
|
} else {
|
||||||
|
if (the_mesh.reprogram(a.name, a.text)) persist_cfg();
|
||||||
|
s_cfg_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s_up) the_mesh.loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestSend(const char* text) {
|
||||||
|
if (!s_queue || !text) return;
|
||||||
|
McAction a; a.kind = 0; a.name[0] = 0;
|
||||||
|
strlcpy(a.text, text, sizeof(a.text));
|
||||||
|
xQueueSend(s_queue, &a, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestPsk(const char* name, const char* psk_b64) {
|
||||||
|
if (!s_queue || !psk_b64) return;
|
||||||
|
McAction a; a.kind = 1;
|
||||||
|
strlcpy(a.name, (name && *name) ? name : s_channel, sizeof(a.name));
|
||||||
|
strlcpy(a.text, psk_b64, sizeof(a.text));
|
||||||
|
xQueueSend(s_queue, &a, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool up() { return s_up; }
|
||||||
|
bool consumeCfgChanged() { bool d = s_cfg_dirty; s_cfg_dirty = false; return d; }
|
||||||
|
const char* nodeName() { return NODE_NAME; }
|
||||||
|
const char* channelName() { return s_channel; }
|
||||||
|
const char* pskB64() { return s_psk; }
|
||||||
|
const char* radioConfig() {
|
||||||
|
static char buf[80];
|
||||||
|
snprintf(buf, sizeof(buf), "freq=%.3f bw=%.1f sf=%d cr=%d tx=%d",
|
||||||
|
(float)LORA_FREQ, (float)LORA_BW, (int)LORA_SF, (int)LORA_CR, (int)LORA_TX_POWER);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
uint32_t rxCount() { return the_mesh.rxCount(); }
|
||||||
|
uint32_t txCount() { return the_mesh.txCount(); }
|
||||||
|
|
||||||
|
} // namespace mc
|
||||||
|
|
||||||
|
#else // ---- USE_MESHCORE not set: cheap no-op build ----------------------
|
||||||
|
|
||||||
|
namespace mc {
|
||||||
|
bool enabled() { return false; }
|
||||||
|
void begin(RxHandler) {}
|
||||||
|
void loop() {}
|
||||||
|
void requestSend(const char*) {}
|
||||||
|
void requestPsk(const char*, const char*) {}
|
||||||
|
bool up() { return false; }
|
||||||
|
bool consumeCfgChanged() { return false; }
|
||||||
|
const char* nodeName() { return "me"; }
|
||||||
|
const char* channelName() { return ""; }
|
||||||
|
const char* pskB64() { return ""; }
|
||||||
|
const char* radioConfig() { return ""; }
|
||||||
|
uint32_t rxCount() { return 0; }
|
||||||
|
uint32_t txCount() { return 0; }
|
||||||
|
} // namespace mc
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* meshcore_link -- thin facade over a MeshCore BaseChatMesh node.
|
||||||
|
*
|
||||||
|
* Lets the debug dongle send/receive text on a single PSK-protected group
|
||||||
|
* channel (plus the well-known "Public" channel), and reprogram that channel's
|
||||||
|
* PSK at runtime. All MeshCore + RadioLib headers stay inside meshcore_link.cpp
|
||||||
|
* so the rest of the firmware compiles identically with or without the radio.
|
||||||
|
*
|
||||||
|
* Enabled by the USE_MESHCORE build flag (see env:t3s3_mesh in platformio.ini).
|
||||||
|
* When the flag is absent every function below is a cheap no-op, so callers
|
||||||
|
* need no #if guards.
|
||||||
|
*
|
||||||
|
* Threading: requestSend()/requestPsk() are safe to call from any task (e.g.
|
||||||
|
* the async web-server callback). They enqueue work that is applied inside
|
||||||
|
* loop(), which must be called from the same task that owns the radio.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
namespace mc {
|
||||||
|
|
||||||
|
// Fired from loop() (radio task context) for each received channel message.
|
||||||
|
typedef void (*RxHandler)(const char* channel, const char* sender,
|
||||||
|
const char* text, int rssi, float snr);
|
||||||
|
|
||||||
|
bool enabled(); // compile-time: USE_MESHCORE set?
|
||||||
|
void begin(RxHandler on_rx); // bring up radio + mesh
|
||||||
|
void loop(); // pump radio, drain action queue
|
||||||
|
|
||||||
|
void requestSend(const char* text); // queue a TX on the user channel
|
||||||
|
void requestPsk(const char* name, const char* psk_b64); // queue a channel reprogram
|
||||||
|
|
||||||
|
bool up(); // radio initialised OK
|
||||||
|
bool consumeCfgChanged(); // true once after a reprogram
|
||||||
|
const char* nodeName(); // our sender name
|
||||||
|
const char* channelName(); // current user-channel name
|
||||||
|
const char* pskB64(); // current user-channel PSK (base64)
|
||||||
|
const char* radioConfig(); // "freq=.. bw=.. sf=.. cr=.. tx=.." ("" if disabled)
|
||||||
|
uint32_t rxCount();
|
||||||
|
uint32_t txCount();
|
||||||
|
|
||||||
|
} // namespace mc
|
||||||
Reference in New Issue
Block a user