// archy-messh Phase 1 — time-multiplexed tri-protocol RX scanner // --------------------------------------------------------------------------- // Target: Heltec WiFi LoRa 32 V3 (ESP32-S3 + single Semtech SX1262). // // This is the FIRST Phase 1 increment. It does not yet bridge, decode, or // transmit anything. Its only job is to answer the project's central open // question (see docs/ARCHITECTURE.md §1-§2): // // Can one SX1262, re-tuned on a schedule, actually hear raw LoRa traffic // from the Meshtastic, MeshCore and Reticulum networks — and do the PHY // configs even work on this board? // // It rotates the radio through each network's PHY config, listens for a fixed // dwell window, and logs every raw packet it demodulates (length, RSSI, SNR, // hex). No framing/crypto/dedup yet — those layers build on top of a working // RX loop, so we prove the RX loop first. // // What this increment proves WITHOUT any other radios nearby: // - correct Heltec V3 SX1262 wiring / TCXO / RF-switch (radio.begin() == OK) // - clean reconfiguration of the single radio between three PHY presets // What it can only prove WITH real traffic on air: // - whether time-multiplexed dwell actually catches other nodes' packets // --------------------------------------------------------------------------- #include #include // --- Heltec WiFi LoRa 32 V3 SX1262 pin map (from Heltec V3 schematic) ------- static const int PIN_LORA_NSS = 8; // SPI chip select static const int PIN_LORA_DIO1 = 14; // IRQ static const int PIN_LORA_NRST = 12; // reset static const int PIN_LORA_BUSY = 13; // busy static const int PIN_LORA_SCK = 9; static const int PIN_LORA_MISO = 11; static const int PIN_LORA_MOSI = 10; // Heltec V3 drives the SX1262 reference oscillator from a 1.8V TCXO and uses // DIO2 as the TX/RX RF switch. Both are board facts, not tunables. static const float TCXO_VOLTAGE = 1.8f; static const int8_t RX_TX_POWER = 10; // unused in RX-only scan; begin() needs a value SX1262 radio = new Module(PIN_LORA_NSS, PIN_LORA_DIO1, PIN_LORA_NRST, PIN_LORA_BUSY); // --- The three PHY presets -------------------------------------------------- // Values from docs/ARCHITECTURE.md §1. Items flagged CONFIRM/PLACEHOLDER are // the project's known open questions — the whole point of this scan is to test // them against reality, so they are deliberately explicit here. struct PhyConfig { const char* name; float freqMHz; float bwKHz; uint8_t sf; uint8_t cr; // coding-rate denominator: 4/cr (so 5 == 4/5) uint8_t syncWord; uint16_t preamble; }; static const PhyConfig CONFIGS[] = { // Meshtastic EU868 "LongFast" — well documented. NOTE: Meshtastic actually // hashes the channel name to a slot within the region plan, so a single // fixed frequency may not match every deployment (ARCHITECTURE.md §1). { "meshtastic", 869.525f, 250.0f, 11, 5, 0x2b, 16 }, // MeshCore compiled default is the same PHY as Meshtastic. Its sync word is // UNCONFIRMED (ARCHITECTURE.md §1 open item) — 0x12 is RadioLib's private // default and only a GUESS here. Community has also moved to narrowband // SF7-8 / BW62.5kHz (Oct 2025), not reflected yet. { "meshcore", 869.525f, 250.0f, 11, 5, 0x12, 16 }, // Reticulum has NO fixed preset — freq/BW/SF/CR are operator-configured per // deployment. This is a PLACEHOLDER; set it to whatever your actual RNS // RNode interface uses or this window will hear nothing. { "reticulum", 867.200f, 125.0f, 8, 5, 0x12, 16 }, }; static const size_t NUM_CONFIGS = sizeof(CONFIGS) / sizeof(CONFIGS[0]); // Dwell per network before rotating to the next. 2s is a starting point; the // coverage/latency tradeoff of this value is exactly what Phase 1 must measure. static const uint32_t DWELL_MS = 2000; // --- RX interrupt plumbing -------------------------------------------------- static volatile bool rxFlag = false; ICACHE_RAM_ATTR static void onDio1() { rxFlag = true; } static void logResult(const char* what, int state) { Serial.printf(" %s -> %d%s\n", what, state, state == RADIOLIB_ERR_NONE ? " (OK)" : " (ERR)"); } // Apply a PHY config to the already-initialised radio and start listening. static bool applyConfig(const PhyConfig& c) { radio.standby(); bool ok = true; ok &= radio.setFrequency(c.freqMHz) == RADIOLIB_ERR_NONE; ok &= radio.setBandwidth(c.bwKHz) == RADIOLIB_ERR_NONE; ok &= radio.setSpreadingFactor(c.sf) == RADIOLIB_ERR_NONE; ok &= radio.setCodingRate(c.cr) == RADIOLIB_ERR_NONE; ok &= radio.setSyncWord(c.syncWord) == RADIOLIB_ERR_NONE; ok &= radio.setPreambleLength(c.preamble) == RADIOLIB_ERR_NONE; if (!ok) return false; return radio.startReceive() == RADIOLIB_ERR_NONE; } static void drainPacket(const PhyConfig& c) { size_t len = radio.getPacketLength(); uint8_t buf[256]; if (len > sizeof(buf)) len = sizeof(buf); int state = radio.readData(buf, len); if (state != RADIOLIB_ERR_NONE) { Serial.printf(" [%s] RX error state=%d\n", c.name, state); return; } Serial.printf(" [%s] PACKET len=%u rssi=%.1fdBm snr=%.1fdB : ", c.name, (unsigned)len, radio.getRSSI(), radio.getSNR()); for (size_t i = 0; i < len; i++) Serial.printf("%02x", buf[i]); Serial.println(); } void setup() { Serial.begin(115200); uint32_t t0 = millis(); while (!Serial && millis() - t0 < 3000) { /* wait briefly for USB CDC */ } Serial.println(); Serial.println("=== archy-messh Phase 1 — tri-protocol RX scanner ==="); SPI.begin(PIN_LORA_SCK, PIN_LORA_MISO, PIN_LORA_MOSI, PIN_LORA_NSS); const PhyConfig& first = CONFIGS[0]; int state = radio.begin(first.freqMHz, first.bwKHz, first.sf, first.cr, first.syncWord, RX_TX_POWER, first.preamble, TCXO_VOLTAGE); logResult("radio.begin", state); if (state != RADIOLIB_ERR_NONE) { Serial.println("!! SX1262 init failed — check wiring/TCXO. Halting."); while (true) delay(1000); } // DIO2 controls the antenna RF switch on the Heltec V3. logResult("setDio2AsRfSwitch", radio.setDio2AsRfSwitch(true)); radio.setDio1Action(onDio1); Serial.printf("Scanning %u configs, %lums dwell each.\n", (unsigned)NUM_CONFIGS, (unsigned long)DWELL_MS); } void loop() { for (size_t i = 0; i < NUM_CONFIGS; i++) { const PhyConfig& c = CONFIGS[i]; Serial.printf("[%s] %.3fMHz BW%.1f SF%u CR4/%u sync0x%02x\n", c.name, c.freqMHz, c.bwKHz, c.sf, c.cr, c.syncWord); if (!applyConfig(c)) { Serial.printf(" [%s] !! failed to apply PHY config — skipping\n", c.name); continue; } uint32_t start = millis(); while (millis() - start < DWELL_MS) { if (rxFlag) { rxFlag = false; drainPacket(c); radio.startReceive(); // re-arm for the rest of the dwell window } delay(2); } } }