feat: RTC rebrand + on-chip Meshtastic decode + acid display
- Rebrand to RTC (Reticulum / meshTastic / meshCore); Meshtastic row = T. - On-chip Meshtastic decode (mbedtls AES-128-CTR + protobuf) surfacing text and node short-names, mirroring firmware/tools/meshtastic_decode.py. - Device identity 'Reticutasticore' / 'RTC' (DEVICE_NAME) baked in for all three networks; boot splash shows it. - Trippy OLED: acid smiley with a periodic spinning-striped freak-out (googly eyes + gaping mouth), punk 'bubble' RTC header, three live network rows (counts + RX blips), last-decoded line. - Hardware-confirmed: Meshtastic + MeshCore both received (0x12 MeshCore sync word guess validated); Reticulum still placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
de4995468d
commit
a284c91848
@ -5,20 +5,30 @@ Embedded firmware for the single-device **3-in-1**: one Heltec WiFi LoRa 32 V3
|
|||||||
Meshtastic / MeshCore / Reticulum PHY configs. See `../docs/ARCHITECTURE.md`
|
Meshtastic / MeshCore / Reticulum PHY configs. See `../docs/ARCHITECTURE.md`
|
||||||
§2-§3 for why Phase 1 is a time-multiplexed single radio (Option A).
|
§2-§3 for why Phase 1 is a time-multiplexed single radio (Option A).
|
||||||
|
|
||||||
## Current increment: tri-protocol RX scanner
|
## Current increment: RTC scanner + acid display + Meshtastic decode
|
||||||
|
|
||||||
`src/main.cpp` is the **first** Phase 1 step. It does not bridge, decode, or
|
`src/main.cpp` rotates the single SX1262 through each network's PHY preset
|
||||||
transmit yet — it rotates the SX1262 through each network's PHY preset, listens
|
(**R**eticulum / mesh**T**astic / mesh**C**ore = **RTC**), listens per dwell
|
||||||
for a fixed dwell window, and logs every raw LoRa packet it demodulates
|
window, logs every raw packet, and **decodes Meshtastic on-chip** (AES-128-CTR
|
||||||
(length, RSSI, SNR, hex). The point is to answer the project's central open
|
via mbedtls + protobuf → text / node names). The 128×64 OLED shows an animated
|
||||||
question before building any protocol stacks: *can one re-tuned radio actually
|
acid-house UI: a smiley (that periodically freaks out — spins, stripes, eyes +
|
||||||
hear all three networks?*
|
mouth open), a punk `RTC` header, three live network rows (packet counts + RX
|
||||||
|
blips), and the last decoded event.
|
||||||
|
|
||||||
**Validated on hardware (2026-07-01):** flashed to a Heltec V3, the scanner
|
Device identity `Reticutasticore` / `RTC` is baked in (`DEVICE_NAME`) as the
|
||||||
`radio.begin()`'d cleanly, cycled all three presets, and received **live
|
name this device will present on all three networks once the TX/participation
|
||||||
Meshtastic broadcast packets** (payloads prefixed `ffffffff`) at −36 to
|
layer lands — so a message addressed to it will be routable.
|
||||||
−41 dBm during the Meshtastic dwell. The single-radio time-multiplex premise
|
|
||||||
holds. MeshCore/Reticulum windows were silent — expected, see open items below.
|
**Validated on hardware (2026-07-01):**
|
||||||
|
- radio inits clean, all three presets cycle, display renders.
|
||||||
|
- **Meshtastic**: live broadcast packets at −34 to −44 dBm; on-chip decode
|
||||||
|
matches the offline reference (`tools/meshtastic_decode.py`, verified against
|
||||||
|
real captures — decoded node "Arch Optiplex"/"ARCH").
|
||||||
|
- **MeshCore**: received real packets too — so the `0x12` sync-word guess is
|
||||||
|
**correct** (was an open item).
|
||||||
|
- **Reticulum**: silent — placeholder PHY config + likely no RNode nodes near.
|
||||||
|
|
||||||
|
Still RX-only: no bridging or transmit yet.
|
||||||
|
|
||||||
## Open items baked into the config table
|
## Open items baked into the config table
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,11 @@
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Target: Heltec WiFi LoRa 32 V3 (ESP32-S3 + single SX1262 + 128x64 SSD1306).
|
// Target: Heltec WiFi LoRa 32 V3 (ESP32-S3 + single SX1262 + 128x64 SSD1306).
|
||||||
//
|
//
|
||||||
// The radio rotates through the Meshtastic / MeshCore / Reticulum PHY presets
|
// The radio rotates through the Reticulum / meshTastic / meshCore PHY presets
|
||||||
// (RMC), listening in each dwell window and logging every raw packet it
|
// (RTC), listening in each dwell window and logging every raw packet it
|
||||||
// demodulates. The OLED shows all three networks live — R M C — behind an
|
// demodulates. The OLED shows all three networks live — R T C — behind an
|
||||||
// animated acid-house smiley, with per-network activity blips.
|
// animated acid-house smiley, with per-network activity blips. Received
|
||||||
|
// Meshtastic packets are decoded on-chip (AES-128-CTR + protobuf).
|
||||||
//
|
//
|
||||||
// HONEST HARDWARE NOTE: the single onboard SX1262 can only be tuned to ONE
|
// HONEST HARDWARE NOTE: the single onboard SX1262 can only be tuned to ONE
|
||||||
// PHY at a time (ARCHITECTURE.md §2), so "all three active" is fast time-
|
// PHY at a time (ARCHITECTURE.md §2), so "all three active" is fast time-
|
||||||
@ -22,6 +23,7 @@
|
|||||||
#include <U8g2lib.h>
|
#include <U8g2lib.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
#include "mbedtls/aes.h"
|
||||||
|
|
||||||
// --- 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;
|
||||||
@ -43,12 +45,12 @@ static const int PIN_VEXT = 36; // drives Vext rail; LOW = ON (powers OLED)
|
|||||||
SX1262 radio = new Module(PIN_LORA_NSS, PIN_LORA_DIO1, PIN_LORA_NRST, PIN_LORA_BUSY);
|
SX1262 radio = new Module(PIN_LORA_NSS, PIN_LORA_DIO1, PIN_LORA_NRST, PIN_LORA_BUSY);
|
||||||
U8G2_SSD1306_128X64_NONAME_F_HW_I2C oled(U8G2_R0, PIN_OLED_RST, PIN_OLED_SCL, PIN_OLED_SDA);
|
U8G2_SSD1306_128X64_NONAME_F_HW_I2C oled(U8G2_R0, PIN_OLED_RST, PIN_OLED_SCL, PIN_OLED_SDA);
|
||||||
|
|
||||||
// --- The three PHY presets (RMC) -------------------------------------------
|
// --- The three PHY presets (RTC) -------------------------------------------
|
||||||
// Values from docs/ARCHITECTURE.md §1. Items flagged are open questions the
|
// Values from docs/ARCHITECTURE.md §1. Items flagged are open questions the
|
||||||
// scan is meant to test against reality.
|
// scan is meant to test against reality.
|
||||||
struct PhyConfig {
|
struct PhyConfig {
|
||||||
const char* name;
|
const char* name;
|
||||||
char letter; // R / M / C for the display
|
char letter; // R / T / C for the display
|
||||||
float freqMHz;
|
float freqMHz;
|
||||||
float bwKHz;
|
float bwKHz;
|
||||||
uint8_t sf;
|
uint8_t sf;
|
||||||
@ -58,8 +60,8 @@ 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", 'M', 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 compiled default PHY; sync word 0x12 is a GUESS (open item §1).
|
||||||
{ "meshcore", 'C', 869.525f, 250.0f, 11, 5, 0x12, 16 },
|
{ "meshcore", 'C', 869.525f, 250.0f, 11, 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.
|
||||||
@ -68,6 +70,16 @@ static const PhyConfig CONFIGS[] = {
|
|||||||
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 = 1200; // faster cycle so all three feel live
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// to "Reticutasticore" on Meshtastic, MeshCore or Reticulum reaches this device:
|
||||||
|
// - Meshtastic: User.long_name = DEVICE_NAME, short_name = DEVICE_SHORT (<=4)
|
||||||
|
// - MeshCore: advertised contact name = DEVICE_NAME
|
||||||
|
// - Reticulum: LXMF delivery display_name = DEVICE_NAME
|
||||||
|
// (RX-only today; the constants exist now so TX inherits them with no churn.)
|
||||||
|
static const char* DEVICE_NAME = "Reticutasticore";
|
||||||
|
static const char* DEVICE_SHORT = "RTC";
|
||||||
|
|
||||||
// --- Runtime stats ----------------------------------------------------------
|
// --- Runtime stats ----------------------------------------------------------
|
||||||
struct NetStat {
|
struct NetStat {
|
||||||
uint32_t pkts = 0;
|
uint32_t pkts = 0;
|
||||||
@ -81,6 +93,11 @@ 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;
|
||||||
|
|
||||||
|
// Last decoded human-readable event, shown along the bottom of the display.
|
||||||
|
static char lastMsg[40] = {0};
|
||||||
|
static char lastMsgNet = ' ';
|
||||||
|
static uint32_t lastMsgMs = 0;
|
||||||
|
|
||||||
// --- 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; }
|
||||||
@ -98,6 +115,77 @@ static bool applyConfig(const PhyConfig& c) {
|
|||||||
return radio.startReceive() == RADIOLIB_ERR_NONE;
|
return radio.startReceive() == RADIOLIB_ERR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- On-chip Meshtastic decode ---------------------------------------------
|
||||||
|
// Default-channel ("LongFast") packets use AES-128-CTR with a PUBLIC key.
|
||||||
|
// Validated offline against real captured packets (firmware/tools/
|
||||||
|
// meshtastic_decode.py). Wire format: 16-byte header + encrypted Data protobuf.
|
||||||
|
static const uint8_t MT_KEY[16] = {
|
||||||
|
0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59,
|
||||||
|
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01};
|
||||||
|
|
||||||
|
static uint32_t readVarint(const uint8_t* b, size_t len, size_t& i) {
|
||||||
|
uint32_t v = 0; int shift = 0;
|
||||||
|
while (i < len) {
|
||||||
|
uint8_t c = b[i++];
|
||||||
|
v |= (uint32_t)(c & 0x7f) << shift; shift += 7;
|
||||||
|
if (!(c & 0x80)) break;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode a Meshtastic packet into a short human string (text, or node name for
|
||||||
|
// 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) {
|
||||||
|
if (len <= 16) return false;
|
||||||
|
uint32_t sender, pid;
|
||||||
|
memcpy(&sender, pkt + 4, 4);
|
||||||
|
memcpy(&pid, pkt + 8, 4);
|
||||||
|
|
||||||
|
// CTR nonce = packetId (8 LE, high 4 zero) + fromNode (4 LE) + extraNonce (4 = 0)
|
||||||
|
uint8_t nonce[16]; memset(nonce, 0, 16);
|
||||||
|
memcpy(nonce + 0, &pid, 4);
|
||||||
|
memcpy(nonce + 8, &sender, 4);
|
||||||
|
|
||||||
|
size_t plen = len - 16;
|
||||||
|
if (plen > 240) return false;
|
||||||
|
uint8_t plain[240];
|
||||||
|
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, plen, &nc_off, nonce, stream, pkt + 16, plain);
|
||||||
|
mbedtls_aes_free(&aes);
|
||||||
|
|
||||||
|
// Data protobuf: field 1 = portnum (varint), field 2 = payload (bytes)
|
||||||
|
size_t i = 0; uint8_t port = 0; const uint8_t* payload = nullptr; size_t payLen = 0;
|
||||||
|
while (i < plen) {
|
||||||
|
uint8_t tag = plain[i++]; uint8_t field = tag >> 3, wt = tag & 7;
|
||||||
|
if (wt == 0) { uint32_t v = readVarint(plain, plen, i); if (field == 1) port = (uint8_t)v; }
|
||||||
|
else if (wt == 2) { uint32_t l = readVarint(plain, plen, i);
|
||||||
|
if (field == 2) { payload = plain + i; payLen = l; } i += l; }
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (!payload) return false;
|
||||||
|
|
||||||
|
if (port == 1) { // TEXT_MESSAGE_APP
|
||||||
|
size_t n = payLen < outsz - 1 ? payLen : outsz - 1;
|
||||||
|
memcpy(out, payload, n); out[n] = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (port == 4) { // NODEINFO_APP -> User protobuf; field 3 = shortName
|
||||||
|
size_t j = 0; const uint8_t* sn = nullptr; size_t snl = 0;
|
||||||
|
while (j < payLen) {
|
||||||
|
uint8_t tag = payload[j++]; uint8_t f = tag >> 3, w = tag & 7;
|
||||||
|
if (w == 2) { uint32_t l = readVarint(payload, payLen, j);
|
||||||
|
if (f == 3) { sn = payload + j; snl = l; } j += l; }
|
||||||
|
else if (w == 0) { readVarint(payload, payLen, j); }
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (sn) { size_t n = snl < outsz - 1 ? snl : outsz - 1;
|
||||||
|
memcpy(out, sn, n); out[n] = 0; return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static void drainPacket() {
|
static void drainPacket() {
|
||||||
size_t len = radio.getPacketLength();
|
size_t len = radio.getPacketLength();
|
||||||
uint8_t buf[256];
|
uint8_t buf[256];
|
||||||
@ -114,6 +202,16 @@ static void drainPacket() {
|
|||||||
CONFIGS[activeIdx].name, (unsigned)len, s.lastRssi, radio.getSNR());
|
CONFIGS[activeIdx].name, (unsigned)len, s.lastRssi, radio.getSNR());
|
||||||
for (size_t i = 0; i < len; i++) Serial.printf("%02x", buf[i]);
|
for (size_t i = 0; i < len; i++) Serial.printf("%02x", buf[i]);
|
||||||
Serial.println();
|
Serial.println();
|
||||||
|
|
||||||
|
// meshTastic packets we can decode on-chip today.
|
||||||
|
if (CONFIGS[activeIdx].letter == 'T') {
|
||||||
|
char msg[40];
|
||||||
|
if (decodeMeshtastic(buf, len, msg, sizeof(msg))) {
|
||||||
|
strncpy(lastMsg, msg, sizeof(lastMsg) - 1); lastMsg[sizeof(lastMsg) - 1] = 0;
|
||||||
|
lastMsgNet = 'T'; lastMsgMs = millis();
|
||||||
|
Serial.printf(" decoded[T]: %s\n", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- The trippy acid display ------------------------------------------------
|
// --- The trippy acid display ------------------------------------------------
|
||||||
@ -139,6 +237,37 @@ static void drawAcidSmiley(int cx, int cy) {
|
|||||||
oled.setDrawColor(1);
|
oled.setDrawColor(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Periodic freak-out: the face becomes a spinning striped ball with wide-open
|
||||||
|
// googly eyes and a gaping mouth.
|
||||||
|
static void drawAcidSmileyFreaky(int cx, int cy) {
|
||||||
|
const int R = 18;
|
||||||
|
oled.setDrawColor(1);
|
||||||
|
oled.drawDisc(cx, cy, R, U8G2_DRAW_ALL);
|
||||||
|
// rotating stripes carved into the ball (the spin)
|
||||||
|
float th = frame * 0.35f;
|
||||||
|
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); // crisp rim
|
||||||
|
// wide-open googly eyes: white halo -> dark iris -> white pupil highlight,
|
||||||
|
// so they read against the striped ball (a plain white disc would vanish)
|
||||||
|
for (int sgn = -1; sgn <= 1; sgn += 2) {
|
||||||
|
int ex = cx + sgn * 7;
|
||||||
|
oled.setDrawColor(1); oled.drawDisc(ex, cy - 6, 4, U8G2_DRAW_ALL); // halo
|
||||||
|
oled.setDrawColor(0); oled.drawDisc(ex, cy - 6, 3, U8G2_DRAW_ALL); // iris
|
||||||
|
oled.setDrawColor(1); oled.drawDisc(ex, cy - 6, 1, U8G2_DRAW_ALL); // pupil
|
||||||
|
}
|
||||||
|
// gaping mouth: white ring around a dark O
|
||||||
|
oled.setDrawColor(1); oled.drawDisc(cx, cy + 7, 5, U8G2_DRAW_ALL);
|
||||||
|
oled.setDrawColor(0); oled.drawDisc(cx, cy + 7, 3, U8G2_DRAW_ALL);
|
||||||
|
oled.setDrawColor(1);
|
||||||
|
}
|
||||||
|
|
||||||
static void drawMarchingBorder() {
|
static void drawMarchingBorder() {
|
||||||
for (int x = 0; x < 128; x++) {
|
for (int x = 0; x < 128; x++) {
|
||||||
if (((x + frame) & 3) == 0) { oled.drawPixel(x, 0); oled.drawPixel(x, 63); }
|
if (((x + frame) & 3) == 0) { oled.drawPixel(x, 0); oled.drawPixel(x, 63); }
|
||||||
@ -160,32 +289,35 @@ static int indexByLetter(char L) {
|
|||||||
static void drawUI() {
|
static void drawUI() {
|
||||||
oled.clearBuffer();
|
oled.clearBuffer();
|
||||||
|
|
||||||
drawAcidSmiley(31, 33);
|
// Every ~9s the smiley freaks out (spins, stripes, eyes + mouth open) for ~2.5s.
|
||||||
|
bool freaky = (millis() % 9000) < 2500;
|
||||||
|
if (freaky) drawAcidSmileyFreaky(31, 33);
|
||||||
|
else drawAcidSmiley(31, 33);
|
||||||
drawMarchingBorder();
|
drawMarchingBorder();
|
||||||
|
|
||||||
// "RMC" pulsing header on the right
|
// "RTC" punk header on the right
|
||||||
oled.setFont(u8g2_font_ncenB12_tr);
|
oled.setFont(u8g2_font_bubble_tr);
|
||||||
const char* rmc = "RMC";
|
const char* rtc = "RTC";
|
||||||
int rw = oled.getStrWidth(rmc);
|
int rw = oled.getStrWidth(rtc);
|
||||||
int rx = 64 + (64 - rw) / 2;
|
int rx = 64 + (64 - rw) / 2;
|
||||||
if ((frame / 10) & 1) { // strobe invert
|
if ((frame / 10) & 1) { // strobe invert
|
||||||
oled.drawBox(rx - 2, 1, rw + 4, 15);
|
oled.drawBox(64, 0, 64, 22);
|
||||||
oled.setDrawColor(0); oled.drawStr(rx, 13, rmc); oled.setDrawColor(1);
|
oled.setDrawColor(0); oled.drawStr(rx, 19, rtc); oled.setDrawColor(1);
|
||||||
} else {
|
} else {
|
||||||
oled.drawStr(rx, 13, rmc);
|
oled.drawStr(rx, 19, rtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// three network rows, in R M C order
|
// three network rows, in R T C order
|
||||||
const char order[3] = {'R', 'M', 'C'};
|
const char order[3] = {'R', 'T', 'C'};
|
||||||
oled.setFont(u8g2_font_6x10_tf);
|
oled.setFont(u8g2_font_6x10_tf);
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
for (int r = 0; r < 3; r++) {
|
for (int r = 0; r < 3; r++) {
|
||||||
int idx = indexByLetter(order[r]);
|
int idx = indexByLetter(order[r]);
|
||||||
if (idx < 0) continue;
|
if (idx < 0) continue;
|
||||||
const NetStat& s = stats[idx];
|
const NetStat& s = stats[idx];
|
||||||
int y = 27 + r * 12; // baseline
|
int y = 32 + r * 11; // baseline (below the taller header)
|
||||||
bool active = (idx == activeIdx);
|
bool active = (idx == activeIdx);
|
||||||
if (active) oled.drawFrame(64, y - 9, 64, 12);
|
if (active) oled.drawFrame(64, y - 9, 64, 11);
|
||||||
char line[24];
|
char line[24];
|
||||||
if (s.seen) snprintf(line, sizeof(line), "%c %4lu", order[r], (unsigned long)s.pkts);
|
if (s.seen) snprintf(line, sizeof(line), "%c %4lu", order[r], (unsigned long)s.pkts);
|
||||||
else snprintf(line, sizeof(line), "%c --", order[r]);
|
else snprintf(line, sizeof(line), "%c --", order[r]);
|
||||||
@ -196,6 +328,15 @@ static void drawUI() {
|
|||||||
else oled.drawCircle(121, y - 4, 3, U8G2_DRAW_ALL);
|
else oled.drawCircle(121, y - 4, 3, U8G2_DRAW_ALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// last decoded event along the bottom of the right column
|
||||||
|
if (lastMsg[0]) {
|
||||||
|
oled.setFont(u8g2_font_4x6_tf);
|
||||||
|
char line[24];
|
||||||
|
snprintf(line, sizeof(line), "%c:%s", lastMsgNet, lastMsg);
|
||||||
|
line[15] = 0; // truncate to fit the ~60px column
|
||||||
|
oled.drawStr(66, 62, line);
|
||||||
|
}
|
||||||
|
|
||||||
oled.sendBuffer();
|
oled.sendBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +345,7 @@ void setup() {
|
|||||||
uint32_t t0 = millis();
|
uint32_t t0 = millis();
|
||||||
while (!Serial && millis() - t0 < 2000) { }
|
while (!Serial && millis() - t0 < 2000) { }
|
||||||
|
|
||||||
Serial.println("\n=== archy-messh Phase 1 — RMC scanner + acid display ===");
|
Serial.println("\n=== archy-messh Phase 1 — RTC scanner + acid display ===");
|
||||||
|
|
||||||
// Power the OLED rail, then reset the panel, then bring up U8g2.
|
// Power the OLED rail, then reset the panel, then bring up U8g2.
|
||||||
pinMode(PIN_VEXT, OUTPUT); digitalWrite(PIN_VEXT, LOW); delay(60);
|
pinMode(PIN_VEXT, OUTPUT); digitalWrite(PIN_VEXT, LOW); delay(60);
|
||||||
@ -214,6 +355,20 @@ void setup() {
|
|||||||
oled.begin();
|
oled.begin();
|
||||||
oled.setBusClock(400000);
|
oled.setBusClock(400000);
|
||||||
|
|
||||||
|
// Boot splash: the device identity, shared across all three networks.
|
||||||
|
oled.clearBuffer();
|
||||||
|
drawAcidSmiley(64, 24);
|
||||||
|
oled.setFont(u8g2_font_6x10_tf);
|
||||||
|
int nw = oled.getStrWidth(DEVICE_NAME);
|
||||||
|
oled.drawStr((128 - nw) / 2, 52, DEVICE_NAME);
|
||||||
|
oled.setFont(u8g2_font_5x8_tf);
|
||||||
|
const char* sub = "R T C bridge";
|
||||||
|
int sw = oled.getStrWidth(sub);
|
||||||
|
oled.drawStr((128 - sw) / 2, 62, sub);
|
||||||
|
oled.sendBuffer();
|
||||||
|
Serial.printf("Device identity (all networks): %s / %s\n", DEVICE_NAME, DEVICE_SHORT);
|
||||||
|
delay(2200);
|
||||||
|
|
||||||
SPI.begin(PIN_LORA_SCK, PIN_LORA_MISO, PIN_LORA_MOSI, PIN_LORA_NSS);
|
SPI.begin(PIN_LORA_SCK, PIN_LORA_MISO, PIN_LORA_MOSI, PIN_LORA_NSS);
|
||||||
const PhyConfig& first = CONFIGS[0];
|
const PhyConfig& first = CONFIGS[0];
|
||||||
int state = radio.begin(first.freqMHz, first.bwKHz, first.sf, first.cr,
|
int state = radio.begin(first.freqMHz, first.bwKHz, first.sf, first.cr,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user