feat(firmware): MeshCore advert TX + Meshtastic text send
- MeshCore: fix PHY to live narrowband config (869.618/62.5/SF8/CR5),
read off the test node's SELF_INFO — old 869.525/250/SF11 heard nothing
- MeshCore: on-chip Ed25519 identity (rweather/Crypto, NVS-persisted) and
signed ADVERT TX (verified valid + received by a real node) — the bridge
now shows up as a MeshCore contact
- Meshtastic: send text messages (portnum 1) reusing the announce path
- serial TX console with per-network targeting ("T:hi" / bare = all)
- longer dwell (3000ms) to sit through flood-rebroadcast bursts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d731bfd7fa
commit
8e5b4c97d7
@ -17,3 +17,4 @@ build_flags =
|
|||||||
lib_deps =
|
lib_deps =
|
||||||
jgromes/RadioLib@7.7.1
|
jgromes/RadioLib@7.7.1
|
||||||
olikraus/U8g2@2.36.18
|
olikraus/U8g2@2.36.18
|
||||||
|
rweather/Crypto@0.4.0 ; Ed25519 / Curve25519 for MeshCore identity + signing
|
||||||
|
|||||||
@ -26,6 +26,8 @@
|
|||||||
#include "mbedtls/aes.h"
|
#include "mbedtls/aes.h"
|
||||||
#include "esp_mac.h"
|
#include "esp_mac.h"
|
||||||
#include "esp_random.h"
|
#include "esp_random.h"
|
||||||
|
#include <Ed25519.h> // rweather/Crypto — MeshCore identity + advert signing
|
||||||
|
#include <Preferences.h> // NVS-backed persistent Ed25519 key
|
||||||
|
|
||||||
// --- Heltec V3 SX1262 pin map (from the Heltec V3 schematic) ----------------
|
// --- Heltec V3 SX1262 pin map (from the Heltec V3 schematic) ----------------
|
||||||
static const int PIN_LORA_NSS = 8;
|
static const int PIN_LORA_NSS = 8;
|
||||||
@ -64,13 +66,16 @@ struct PhyConfig {
|
|||||||
static const PhyConfig CONFIGS[] = {
|
static const PhyConfig CONFIGS[] = {
|
||||||
// meshTastic EU868 "LongFast" — confirmed correct (received live packets).
|
// meshTastic EU868 "LongFast" — confirmed correct (received live packets).
|
||||||
{ "meshtastic", 'T', 869.525f, 250.0f, 11, 5, 0x2b, 16 },
|
{ "meshtastic", 'T', 869.525f, 250.0f, 11, 5, 0x2b, 16 },
|
||||||
// MeshCore compiled default PHY; sync word 0x12 is a GUESS (open item §1).
|
// MeshCore narrowband — read live from the "HP Pro Desk" node's SELF_INFO
|
||||||
{ "meshcore", 'C', 869.525f, 250.0f, 11, 5, 0x12, 16 },
|
// (companion serial protocol, 2026-07-01): 869.618 MHz / 62.5 kHz / SF8 / CR5.
|
||||||
|
{ "meshcore", 'C', 869.618f, 62.5f, 8, 5, 0x12, 16 },
|
||||||
// Reticulum has no fixed preset; PLACEHOLDER — set to your RNS RNode config.
|
// Reticulum has no fixed preset; PLACEHOLDER — set to your RNS RNode config.
|
||||||
{ "reticulum", 'R', 867.200f, 125.0f, 8, 5, 0x12, 16 },
|
{ "reticulum", 'R', 867.200f, 125.0f, 8, 5, 0x12, 16 },
|
||||||
};
|
};
|
||||||
static const size_t NUM_CONFIGS = sizeof(CONFIGS) / sizeof(CONFIGS[0]);
|
static const size_t NUM_CONFIGS = sizeof(CONFIGS) / sizeof(CONFIGS[0]);
|
||||||
static const uint32_t DWELL_MS = 1200; // faster cycle so all three feel live
|
static const uint32_t DWELL_MS = 3000; // camp long enough to sit through a
|
||||||
|
// flood-rebroadcast burst on each PHY;
|
||||||
|
// tune vs. how "live" C/R need to feel
|
||||||
|
|
||||||
// Device identity — this single name is what the bridge will present on ALL
|
// Device identity — this single name is what the bridge will present on ALL
|
||||||
// three networks once the participation/TX layer lands, so a message addressed
|
// three networks once the participation/TX layer lands, so a message addressed
|
||||||
@ -95,6 +100,12 @@ static uint32_t totalPkts = 0;
|
|||||||
static uint32_t dwellStart = 0;
|
static uint32_t dwellStart = 0;
|
||||||
static uint32_t frame = 0;
|
static uint32_t frame = 0;
|
||||||
|
|
||||||
|
// Outbound message queued from the serial console ("T:hello", or bare "hello"
|
||||||
|
// == all nets). Flushed on the matching PHY window so the radio is tuned right.
|
||||||
|
static char txText[200] = {0};
|
||||||
|
static bool txPending = false;
|
||||||
|
static char txNets[4] = {0}; // subset of "TCR"; empty == all
|
||||||
|
|
||||||
// Last decoded human-readable event, shown along the bottom of the display.
|
// Last decoded human-readable event, shown along the bottom of the display.
|
||||||
static char lastMsg[40] = {0};
|
static char lastMsg[40] = {0};
|
||||||
static char lastMsgNet = ' ';
|
static char lastMsgNet = ' ';
|
||||||
@ -106,6 +117,25 @@ static char nodeId[12] = {0}; // "!xxxxxxxx"
|
|||||||
static uint32_t lastAnnounceMs = 0;
|
static uint32_t lastAnnounceMs = 0;
|
||||||
static const uint32_t ANNOUNCE_INTERVAL_MS = 60000; // re-announce every 60s
|
static const uint32_t ANNOUNCE_INTERVAL_MS = 60000; // re-announce every 60s
|
||||||
|
|
||||||
|
// Message modal: on an inbound text we play an intro animation (upside-down
|
||||||
|
// spinning striped smiley + lightning bolts) then a Meshtastic-style modal
|
||||||
|
// stating the network, sender and text. Network-agnostic — any protocol that
|
||||||
|
// decodes a message drives it.
|
||||||
|
static bool msgModalActive = false;
|
||||||
|
static uint32_t msgModalStart = 0;
|
||||||
|
static char modalNet[14] = {0};
|
||||||
|
static char modalSender[20] = {0};
|
||||||
|
static char modalText[80] = {0};
|
||||||
|
static const uint32_t MODAL_INTRO_MS = 1400;
|
||||||
|
static const uint32_t MODAL_TOTAL_MS = 6500;
|
||||||
|
|
||||||
|
static void triggerMessageModal(const char* net, const char* sender, const char* text) {
|
||||||
|
strncpy(modalNet, net, sizeof(modalNet) - 1); modalNet[sizeof(modalNet) - 1] = 0;
|
||||||
|
strncpy(modalSender, sender, sizeof(modalSender) - 1); modalSender[sizeof(modalSender) - 1] = 0;
|
||||||
|
strncpy(modalText, text, sizeof(modalText) - 1); modalText[sizeof(modalText) - 1] = 0;
|
||||||
|
msgModalActive = true; msgModalStart = millis();
|
||||||
|
}
|
||||||
|
|
||||||
// --- RX interrupt -----------------------------------------------------------
|
// --- RX interrupt -----------------------------------------------------------
|
||||||
static volatile bool rxFlag = false;
|
static volatile bool rxFlag = false;
|
||||||
ICACHE_RAM_ATTR static void onDio1() { rxFlag = true; }
|
ICACHE_RAM_ATTR static void onDio1() { rxFlag = true; }
|
||||||
@ -143,11 +173,14 @@ static uint32_t readVarint(const uint8_t* b, size_t len, size_t& i) {
|
|||||||
|
|
||||||
// Decode a Meshtastic packet into a short human string (text, or node name for
|
// Decode a Meshtastic packet into a short human string (text, or node name for
|
||||||
// NodeInfo). Returns false if not decodable / not an interesting type.
|
// NodeInfo). Returns false if not decodable / not an interesting type.
|
||||||
static bool decodeMeshtastic(const uint8_t* pkt, size_t len, char* out, size_t outsz) {
|
static bool decodeMeshtastic(const uint8_t* pkt, size_t len, char* out, size_t outsz,
|
||||||
|
bool* isText, uint32_t* senderOut) {
|
||||||
if (len <= 16) return false;
|
if (len <= 16) return false;
|
||||||
uint32_t sender, pid;
|
uint32_t sender, pid;
|
||||||
memcpy(&sender, pkt + 4, 4);
|
memcpy(&sender, pkt + 4, 4);
|
||||||
memcpy(&pid, pkt + 8, 4);
|
memcpy(&pid, pkt + 8, 4);
|
||||||
|
if (senderOut) *senderOut = sender;
|
||||||
|
if (isText) *isText = false;
|
||||||
|
|
||||||
// CTR nonce = packetId (8 LE, high 4 zero) + fromNode (4 LE) + extraNonce (4 = 0)
|
// CTR nonce = packetId (8 LE, high 4 zero) + fromNode (4 LE) + extraNonce (4 = 0)
|
||||||
uint8_t nonce[16]; memset(nonce, 0, 16);
|
uint8_t nonce[16]; memset(nonce, 0, 16);
|
||||||
@ -177,6 +210,7 @@ static bool decodeMeshtastic(const uint8_t* pkt, size_t len, char* out, size_t o
|
|||||||
if (port == 1) { // TEXT_MESSAGE_APP
|
if (port == 1) { // TEXT_MESSAGE_APP
|
||||||
size_t n = payLen < outsz - 1 ? payLen : outsz - 1;
|
size_t n = payLen < outsz - 1 ? payLen : outsz - 1;
|
||||||
memcpy(out, payload, n); out[n] = 0;
|
memcpy(out, payload, n); out[n] = 0;
|
||||||
|
if (isText) *isText = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (port == 4) { // NODEINFO_APP -> User protobuf; field 3 = shortName
|
if (port == 4) { // NODEINFO_APP -> User protobuf; field 3 = shortName
|
||||||
@ -240,8 +274,106 @@ static void announceMeshtastic() {
|
|||||||
|
|
||||||
radio.standby();
|
radio.standby();
|
||||||
int st = radio.transmit(pkt, 16 + d);
|
int st = radio.transmit(pkt, 16 + d);
|
||||||
Serial.printf(" [T] announce '%s' (%s) id=0x%08x -> tx=%d\n",
|
Serial.printf(" [T] announce '%s' (%s) id=0x%08x -> tx=%d : ",
|
||||||
DEVICE_NAME, nodeId, pid, st);
|
DEVICE_NAME, nodeId, pid, st);
|
||||||
|
for (size_t i = 0; i < 16 + d; i++) Serial.printf("%02x", pkt[i]); // TX hex for self-verify
|
||||||
|
Serial.println();
|
||||||
|
radio.startReceive();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a text message on the Meshtastic LongFast primary channel. Same wire
|
||||||
|
// format as announceMeshtastic() but portnum=1 (TEXT_MESSAGE_APP), payload =
|
||||||
|
// the raw UTF-8 text. Assumes the radio is already tuned to the 'T' PHY.
|
||||||
|
static void sendMeshtasticText(const char* text) {
|
||||||
|
size_t tl = strlen(text);
|
||||||
|
if (tl > 200) tl = 200;
|
||||||
|
|
||||||
|
// Data protobuf: field1 portnum=1 (varint), field2 payload=text (bytes)
|
||||||
|
uint8_t data[224]; size_t d = 0;
|
||||||
|
data[d++] = 0x08; data[d++] = 0x01; // portnum = 1
|
||||||
|
data[d++] = 0x12; data[d++] = (uint8_t)tl; // payload, len-delimited
|
||||||
|
memcpy(data + d, text, tl); d += tl;
|
||||||
|
|
||||||
|
uint32_t pid = esp_random();
|
||||||
|
uint8_t enc[224];
|
||||||
|
mtCryptCtr(nodeNum, pid, data, enc, d);
|
||||||
|
|
||||||
|
uint8_t pkt[240]; uint32_t dest = 0xffffffff;
|
||||||
|
memcpy(pkt + 0, &dest, 4);
|
||||||
|
memcpy(pkt + 4, &nodeNum, 4);
|
||||||
|
memcpy(pkt + 8, &pid, 4);
|
||||||
|
pkt[12] = 0x63; // flags: hop_limit=3, hop_start=3
|
||||||
|
pkt[13] = 0x08; // channel hash (LongFast primary)
|
||||||
|
pkt[14] = 0x00; // next_hop
|
||||||
|
pkt[15] = 0x00; // relay_node
|
||||||
|
memcpy(pkt + 16, enc, d);
|
||||||
|
|
||||||
|
radio.standby();
|
||||||
|
int st = radio.transmit(pkt, 16 + d);
|
||||||
|
Serial.printf(" [T] TX text id=0x%08x -> tx=%d : \"%s\"\n", pid, st, text);
|
||||||
|
radio.startReceive();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MeshCore identity + advert TX -----------------------------------------
|
||||||
|
// MeshCore nodes prove identity with an Ed25519 keypair. To appear as a contact
|
||||||
|
// in other clients we broadcast a signed ADVERT (PAYLOAD_TYPE_ADVERT = 0x04):
|
||||||
|
// on-air: [header 0x12][path_len 0x00][pub_key 32][timestamp 4 LE][sig 64][app_data]
|
||||||
|
// signed message = pub_key || timestamp || app_data (verified in MeshCore
|
||||||
|
// Mesh.cpp onRecvPacket PAYLOAD_TYPE_ADVERT). app_data = flags(0x81 = chat
|
||||||
|
// node + has-name) followed by the UTF-8 name.
|
||||||
|
static uint8_t mcPrv[32]; // Ed25519 private key (seed)
|
||||||
|
static uint8_t mcPub[32]; // Ed25519 public key = MeshCore identity
|
||||||
|
static uint32_t lastMcAdvertMs = 0;
|
||||||
|
static const uint32_t MC_ADVERT_INTERVAL_MS = 30000;
|
||||||
|
// Build-time epoch base (2026-07-01 UTC) + uptime — advert timestamps just need
|
||||||
|
// to be fresh/monotonic; the node has no RTC.
|
||||||
|
static const uint32_t MC_EPOCH_BASE = 1782864000UL;
|
||||||
|
static uint32_t mcNow() { return MC_EPOCH_BASE + millis() / 1000; }
|
||||||
|
|
||||||
|
static void initMeshCoreIdentity() {
|
||||||
|
Preferences prefs;
|
||||||
|
prefs.begin("meshcore", false);
|
||||||
|
if (prefs.getBytes("prv", mcPrv, 32) != 32) {
|
||||||
|
Ed25519::generatePrivateKey(mcPrv); // uses the Crypto lib CSPRNG
|
||||||
|
prefs.putBytes("prv", mcPrv, 32);
|
||||||
|
Serial.println("MeshCore: generated new Ed25519 identity");
|
||||||
|
}
|
||||||
|
prefs.end();
|
||||||
|
Ed25519::derivePublicKey(mcPub, mcPrv);
|
||||||
|
Serial.print("MeshCore identity pub: ");
|
||||||
|
for (int i = 0; i < 32; i++) Serial.printf("%02x", mcPub[i]);
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sendMeshCoreAdvert() {
|
||||||
|
uint8_t app_data[40]; size_t ad = 0;
|
||||||
|
app_data[ad++] = 0x81; // flags: chat node + has name
|
||||||
|
size_t nl = strlen(DEVICE_NAME); if (nl > 32) nl = 32;
|
||||||
|
memcpy(app_data + ad, DEVICE_NAME, nl); ad += nl;
|
||||||
|
|
||||||
|
uint32_t ts = mcNow();
|
||||||
|
uint8_t message[32 + 4 + 40]; size_t ml = 0;
|
||||||
|
memcpy(message + ml, mcPub, 32); ml += 32;
|
||||||
|
memcpy(message + ml, &ts, 4); ml += 4;
|
||||||
|
memcpy(message + ml, app_data, ad); ml += ad;
|
||||||
|
|
||||||
|
uint8_t sig[64];
|
||||||
|
Ed25519::sign(sig, mcPrv, mcPub, message, ml);
|
||||||
|
|
||||||
|
uint8_t pkt[2 + 32 + 4 + 64 + 40]; size_t p = 0;
|
||||||
|
pkt[p++] = 0x12; // route=flood(2) | type=advert(4)
|
||||||
|
pkt[p++] = 0x00; // path_len = 0 (direct/flood)
|
||||||
|
memcpy(pkt + p, mcPub, 32); p += 32;
|
||||||
|
memcpy(pkt + p, &ts, 4); p += 4;
|
||||||
|
memcpy(pkt + p, sig, 64); p += 64;
|
||||||
|
memcpy(pkt + p, app_data, ad); p += ad;
|
||||||
|
|
||||||
|
radio.standby();
|
||||||
|
int st = radio.transmit(pkt, p);
|
||||||
|
Serial.printf(" [C] advert '%s' ts=%u -> tx=%d (%u bytes) : ",
|
||||||
|
DEVICE_NAME, ts, st, (unsigned)p);
|
||||||
|
for (size_t i = 0; i < p; i++) Serial.printf("%02x", pkt[i]); // TX hex for self-verify
|
||||||
|
Serial.println();
|
||||||
radio.startReceive();
|
radio.startReceive();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,11 +396,15 @@ static void drainPacket() {
|
|||||||
|
|
||||||
// meshTastic packets we can decode on-chip today.
|
// meshTastic packets we can decode on-chip today.
|
||||||
if (CONFIGS[activeIdx].letter == 'T') {
|
if (CONFIGS[activeIdx].letter == 'T') {
|
||||||
char msg[40];
|
char msg[64]; bool isText = false; uint32_t snd = 0;
|
||||||
if (decodeMeshtastic(buf, len, msg, sizeof(msg))) {
|
if (decodeMeshtastic(buf, len, msg, sizeof(msg), &isText, &snd)) {
|
||||||
strncpy(lastMsg, msg, sizeof(lastMsg) - 1); lastMsg[sizeof(lastMsg) - 1] = 0;
|
strncpy(lastMsg, msg, sizeof(lastMsg) - 1); lastMsg[sizeof(lastMsg) - 1] = 0;
|
||||||
lastMsgNet = 'T'; lastMsgMs = millis();
|
lastMsgNet = 'T'; lastMsgMs = millis();
|
||||||
Serial.printf(" decoded[T]: %s\n", msg);
|
Serial.printf(" decoded[T]%s: %s\n", isText ? "(text)" : "(info)", msg);
|
||||||
|
if (isText) {
|
||||||
|
char sid[16]; snprintf(sid, sizeof(sid), "!%08x", snd);
|
||||||
|
triggerMessageModal("meshTastic", sid, msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -345,7 +481,83 @@ static int indexByLetter(char L) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Message-received animation + modal ------------------------------------
|
||||||
|
static void drawBolt(int x, int y, int h) {
|
||||||
|
oled.drawLine(x, y, x - 3, y + h / 2);
|
||||||
|
oled.drawLine(x - 3, y + h / 2, x + 3, y + h / 2);
|
||||||
|
oled.drawLine(x + 3, y + h / 2, x - 1, y + h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greedy word-wrap for a fixed-width font (6px/char here).
|
||||||
|
static void drawWrapped(const char* text, int x, int y0, int cpl, int lineh, int maxlines) {
|
||||||
|
int n = strlen(text), i = 0, line = 0;
|
||||||
|
while (i < n && line < maxlines) {
|
||||||
|
int end = i + cpl;
|
||||||
|
if (end < n) { int b = end; while (b > i && text[b] != ' ') b--; if (b > i) end = b; }
|
||||||
|
else end = n;
|
||||||
|
char buf[40]; int len = end - i; if (len > 39) len = 39;
|
||||||
|
memcpy(buf, text + i, len); buf[len] = 0;
|
||||||
|
oled.drawStr(x, y0 + line * lineh, buf);
|
||||||
|
i = end; while (i < n && text[i] == ' ') i++;
|
||||||
|
line++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intro: the striped smiley spins UPSIDE DOWN (eyes below, mouth above) while
|
||||||
|
// lightning bolts flash — a beat before the modal, à la Meshtastic.
|
||||||
|
static void drawMsgIntro() {
|
||||||
|
oled.clearBuffer();
|
||||||
|
int cx = 64, cy = 30; const int R = 22;
|
||||||
|
oled.setDrawColor(1); oled.drawDisc(cx, cy, R, U8G2_DRAW_ALL);
|
||||||
|
float th = -frame * 0.5f; // reversed spin
|
||||||
|
float c = cosf(th), s = sinf(th);
|
||||||
|
oled.setDrawColor(0);
|
||||||
|
for (int off = -R; off <= R; off += 4) {
|
||||||
|
float px = cx - s * off, py = cy + c * off;
|
||||||
|
oled.drawLine((int)(px - c * R), (int)(py - s * R), (int)(px + c * R), (int)(py + s * R));
|
||||||
|
}
|
||||||
|
oled.setDrawColor(1); oled.drawCircle(cx, cy, R, U8G2_DRAW_ALL);
|
||||||
|
for (int sgn = -1; sgn <= 1; sgn += 2) { // eyes BELOW centre (upside down)
|
||||||
|
int ex = cx + sgn * 8;
|
||||||
|
oled.setDrawColor(1); oled.drawDisc(ex, cy + 8, 4, U8G2_DRAW_ALL);
|
||||||
|
oled.setDrawColor(0); oled.drawDisc(ex, cy + 8, 3, U8G2_DRAW_ALL);
|
||||||
|
oled.setDrawColor(1); oled.drawDisc(ex, cy + 8, 1, U8G2_DRAW_ALL);
|
||||||
|
}
|
||||||
|
oled.setDrawColor(1); oled.drawDisc(cx, cy - 9, 5, U8G2_DRAW_ALL); // mouth ABOVE
|
||||||
|
oled.setDrawColor(0); oled.drawDisc(cx, cy - 9, 3, U8G2_DRAW_ALL);
|
||||||
|
oled.setDrawColor(1);
|
||||||
|
if ((frame / 2) & 1) { drawBolt(22, 5, 22); drawBolt(106, 5, 22); }
|
||||||
|
else { drawBolt(14, 12, 20); drawBolt(114, 10, 22); }
|
||||||
|
oled.setFont(u8g2_font_5x8_tf);
|
||||||
|
const char* t = "INCOMING";
|
||||||
|
oled.drawStr((128 - oled.getStrWidth(t)) / 2, 62, t);
|
||||||
|
oled.sendBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal: network banner + sender + wrapped message text.
|
||||||
|
static void drawMsgModal() {
|
||||||
|
oled.clearBuffer();
|
||||||
|
oled.drawRFrame(1, 1, 126, 62, 3);
|
||||||
|
oled.drawBox(1, 1, 126, 13); // header bar
|
||||||
|
oled.setDrawColor(0);
|
||||||
|
oled.setFont(u8g2_font_6x10_tf);
|
||||||
|
oled.drawStr((128 - oled.getStrWidth(modalNet)) / 2, 11, modalNet);
|
||||||
|
oled.setDrawColor(1);
|
||||||
|
oled.setFont(u8g2_font_5x8_tf);
|
||||||
|
char sl[28]; snprintf(sl, sizeof(sl), "from %s", modalSender);
|
||||||
|
oled.drawStr(6, 24, sl);
|
||||||
|
oled.setFont(u8g2_font_6x10_tf);
|
||||||
|
drawWrapped(modalText, 6, 36, 19, 11, 3);
|
||||||
|
oled.sendBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
static void drawUI() {
|
static void drawUI() {
|
||||||
|
if (msgModalActive) {
|
||||||
|
uint32_t e = millis() - msgModalStart;
|
||||||
|
if (e < MODAL_INTRO_MS) { drawMsgIntro(); return; }
|
||||||
|
if (e < MODAL_TOTAL_MS) { drawMsgModal(); return; }
|
||||||
|
msgModalActive = false;
|
||||||
|
}
|
||||||
oled.clearBuffer();
|
oled.clearBuffer();
|
||||||
|
|
||||||
// Every ~9s the smiley freaks out (spins, stripes, eyes + mouth open) for ~2.5s.
|
// Every ~9s the smiley freaks out (spins, stripes, eyes + mouth open) for ~2.5s.
|
||||||
@ -448,12 +660,63 @@ void setup() {
|
|||||||
snprintf(nodeId, sizeof(nodeId), "!%08x", nodeNum);
|
snprintf(nodeId, sizeof(nodeId), "!%08x", nodeNum);
|
||||||
Serial.printf("Meshtastic node: %s (%s)\n", DEVICE_NAME, nodeId);
|
Serial.printf("Meshtastic node: %s (%s)\n", DEVICE_NAME, nodeId);
|
||||||
|
|
||||||
|
initMeshCoreIdentity();
|
||||||
|
|
||||||
activeIdx = 0;
|
activeIdx = 0;
|
||||||
applyConfig(CONFIGS[0]);
|
applyConfig(CONFIGS[0]);
|
||||||
dwellStart = millis();
|
dwellStart = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read a line from the serial console into the TX queue. Syntax:
|
||||||
|
// "hello" -> send "hello" on all networks
|
||||||
|
// "T:hello" -> send only on Meshtastic (T / C / R prefix, case-insensitive)
|
||||||
|
static void pollSerialTx() {
|
||||||
|
static char buf[210]; static size_t n = 0;
|
||||||
|
while (Serial.available()) {
|
||||||
|
char c = Serial.read();
|
||||||
|
if (c == '\r') continue;
|
||||||
|
if (c == '\n') {
|
||||||
|
buf[n] = 0;
|
||||||
|
if (n > 0) {
|
||||||
|
const char* msg = buf;
|
||||||
|
strcpy(txNets, "TCR"); // default: all networks
|
||||||
|
if (n >= 2 && buf[1] == ':') {
|
||||||
|
char L = toupper(buf[0]);
|
||||||
|
if (L == 'T' || L == 'C' || L == 'R') { txNets[0] = L; txNets[1] = 0; msg = buf + 2; }
|
||||||
|
}
|
||||||
|
strncpy(txText, msg, sizeof(txText) - 1); txText[sizeof(txText) - 1] = 0;
|
||||||
|
txPending = true;
|
||||||
|
Serial.printf(" [tx] queued for %s: \"%s\"\n", txNets, txText);
|
||||||
|
}
|
||||||
|
n = 0;
|
||||||
|
} else if (n < sizeof(buf) - 1) {
|
||||||
|
buf[n++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a message is queued for the currently-tuned network, send it now.
|
||||||
|
static void flushTxOnWindow() {
|
||||||
|
if (!txPending) return;
|
||||||
|
char L = CONFIGS[activeIdx].letter;
|
||||||
|
char* p = strchr(txNets, L);
|
||||||
|
if (!p) return; // not queued for this network
|
||||||
|
|
||||||
|
if (L == 'T') {
|
||||||
|
sendMeshtasticText(txText);
|
||||||
|
} else {
|
||||||
|
// MeshCore / Reticulum TX framing not built yet — acknowledge, don't fake it.
|
||||||
|
Serial.printf(" [%c] TX not implemented yet (msg: \"%s\")\n", L, txText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove this network from the pending set; done when none remain.
|
||||||
|
memmove(p, p + 1, strlen(p)); // includes the NUL terminator
|
||||||
|
if (txNets[0] == 0) txPending = false;
|
||||||
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
pollSerialTx();
|
||||||
|
|
||||||
// Non-blocking scan: rotate the radio when the dwell window elapses, so the
|
// Non-blocking scan: rotate the radio when the dwell window elapses, so the
|
||||||
// display keeps animating smoothly the whole time.
|
// display keeps animating smoothly the whole time.
|
||||||
if (millis() - dwellStart >= DWELL_MS) {
|
if (millis() - dwellStart >= DWELL_MS) {
|
||||||
@ -461,6 +724,7 @@ void loop() {
|
|||||||
applyConfig(CONFIGS[activeIdx]);
|
applyConfig(CONFIGS[activeIdx]);
|
||||||
dwellStart = millis();
|
dwellStart = millis();
|
||||||
}
|
}
|
||||||
|
flushTxOnWindow();
|
||||||
if (rxFlag) {
|
if (rxFlag) {
|
||||||
rxFlag = false;
|
rxFlag = false;
|
||||||
drainPacket();
|
drainPacket();
|
||||||
@ -472,6 +736,13 @@ void loop() {
|
|||||||
announceMeshtastic();
|
announceMeshtastic();
|
||||||
lastAnnounceMs = millis();
|
lastAnnounceMs = millis();
|
||||||
}
|
}
|
||||||
|
// Advertise ourselves as a MeshCore chat node while tuned to that PHY, so we
|
||||||
|
// show up as a contact in other MeshCore clients.
|
||||||
|
if (CONFIGS[activeIdx].letter == 'C' &&
|
||||||
|
(lastMcAdvertMs == 0 || millis() - lastMcAdvertMs > MC_ADVERT_INTERVAL_MS)) {
|
||||||
|
sendMeshCoreAdvert();
|
||||||
|
lastMcAdvertMs = millis();
|
||||||
|
}
|
||||||
drawUI();
|
drawUI();
|
||||||
frame++;
|
frame++;
|
||||||
delay(45); // ~20 fps
|
delay(45); // ~20 fps
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user