From d731bfd7fa5811e932a023c1e6b5679b3c2d42f4 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 1 Jul 2026 16:35:18 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Meshtastic=20TX=20=E2=80=94=20device=20?= =?UTF-8?q?announces=20as=20a=20named=20node=20(messageable)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The device now transmits a NodeInfo (User protobuf: id/long_name/short_name) on the LongFast primary channel every 60s, so it appears in other Meshtastic clients as 'Reticutasticore' / 'RTC'. NodeNum derived from the MAC (!4359d2d0). No public key advertised, so DMs to us fall back to channel encryption — which the existing RX path already decodes and displays. Confirmed on hardware: announce tx=0 (success) on the first T-window. Also spec the planned message-received animation (upside-down spinning smiley + lightning bolts -> Meshtastic-style modal) in the README. Co-Authored-By: Claude Opus 4.8 (1M context) --- firmware/README.md | 7 +++++ firmware/src/main.cpp | 73 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/firmware/README.md b/firmware/README.md index 67e51d1..8470482 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -53,6 +53,13 @@ pio run -t upload --upload-port /dev/cu.usbserial-0001 # flash (adjust port) pio device monitor -b 115200 # watch the scan ``` +## Planned: message-received animation + +When a text message arrives, play a short intro before showing it: the striped +smiley spins **upside-down / rotated**, little lightning bolts flash, then a +Meshtastic-style **message modal** slides in with the sender + text. (Spec'd, +not yet built.) + ## Roadmap from here 1. **RX decode per protocol** — turn logged raw packets into `{sender, text}` diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 8587d7a..37432ed 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -24,6 +24,8 @@ #include #include #include "mbedtls/aes.h" +#include "esp_mac.h" +#include "esp_random.h" // --- Heltec V3 SX1262 pin map (from the Heltec V3 schematic) ---------------- static const int PIN_LORA_NSS = 8; @@ -98,6 +100,12 @@ static char lastMsg[40] = {0}; static char lastMsgNet = ' '; static uint32_t lastMsgMs = 0; +// Meshtastic node identity (this device's presence on the mesh). +static uint32_t nodeNum = 0; +static char nodeId[12] = {0}; // "!xxxxxxxx" +static uint32_t lastAnnounceMs = 0; +static const uint32_t ANNOUNCE_INTERVAL_MS = 60000; // re-announce every 60s + // --- RX interrupt ----------------------------------------------------------- static volatile bool rxFlag = false; ICACHE_RAM_ATTR static void onDio1() { rxFlag = true; } @@ -186,6 +194,57 @@ static bool decodeMeshtastic(const uint8_t* pkt, size_t len, char* out, size_t o return false; } +// AES-128-CTR (symmetric, so same call encrypts) with the Meshtastic default key. +static void mtCryptCtr(uint32_t sender, uint32_t pid, const uint8_t* in, uint8_t* out, size_t n) { + uint8_t nonce[16]; memset(nonce, 0, 16); + memcpy(nonce + 0, &pid, 4); + memcpy(nonce + 8, &sender, 4); + mbedtls_aes_context aes; mbedtls_aes_init(&aes); + mbedtls_aes_setkey_enc(&aes, MT_KEY, 128); + size_t nc_off = 0; uint8_t stream[16]; memset(stream, 0, 16); + mbedtls_aes_crypt_ctr(&aes, n, &nc_off, nonce, stream, in, out); + mbedtls_aes_free(&aes); +} + +// Announce this device as a Meshtastic node named DEVICE_NAME. Broadcasts a +// NodeInfo (User protobuf) on the LongFast primary channel (hash 0x08) so it +// appears in other Meshtastic clients — and, because we advertise no public +// key, DMs to us fall back to channel encryption, which we can already decode. +static void announceMeshtastic() { + // User protobuf: field1 id, field2 long_name, field3 short_name + uint8_t user[64]; size_t u = 0; + size_t idl = strlen(nodeId), lnl = strlen(DEVICE_NAME), snl = strlen(DEVICE_SHORT); + user[u++] = 0x0a; user[u++] = idl; memcpy(user + u, nodeId, idl); u += idl; + user[u++] = 0x12; user[u++] = lnl; memcpy(user + u, DEVICE_NAME, lnl); u += lnl; + user[u++] = 0x1a; user[u++] = snl; memcpy(user + u, DEVICE_SHORT, snl); u += snl; + + // Data protobuf: portnum=4 (NODEINFO_APP), payload=User + uint8_t data[96]; size_t d = 0; + data[d++] = 0x08; data[d++] = 0x04; // portnum = 4 + data[d++] = 0x12; data[d++] = (uint8_t)u; memcpy(data + d, user, u); d += u; + + uint32_t pid = esp_random(); + uint8_t enc[96]; + mtCryptCtr(nodeNum, pid, data, enc, d); + + // 16-byte header + ciphertext + uint8_t pkt[128]; 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] announce '%s' (%s) id=0x%08x -> tx=%d\n", + DEVICE_NAME, nodeId, pid, st); + radio.startReceive(); +} + static void drainPacket() { size_t len = radio.getPacketLength(); uint8_t buf[256]; @@ -380,6 +439,14 @@ void setup() { } radio.setDio2AsRfSwitch(true); radio.setDio1Action(onDio1); + radio.setOutputPower(14); // EU868-safe TX power for announces + + // Derive this device's Meshtastic NodeNum from the last 4 bytes of the MAC. + uint8_t mac[6]; esp_read_mac(mac, ESP_MAC_WIFI_STA); + nodeNum = ((uint32_t)mac[2] << 24) | ((uint32_t)mac[3] << 16) | + ((uint32_t)mac[4] << 8) | mac[5]; + snprintf(nodeId, sizeof(nodeId), "!%08x", nodeNum); + Serial.printf("Meshtastic node: %s (%s)\n", DEVICE_NAME, nodeId); activeIdx = 0; applyConfig(CONFIGS[0]); @@ -399,6 +466,12 @@ void loop() { drainPacket(); radio.startReceive(); } + // Announce ourselves as a Meshtastic node while tuned to that PHY. + if (CONFIGS[activeIdx].letter == 'T' && + (lastAnnounceMs == 0 || millis() - lastAnnounceMs > ANNOUNCE_INTERVAL_MS)) { + announceMeshtastic(); + lastAnnounceMs = millis(); + } drawUI(); frame++; delay(45); // ~20 fps