169 lines
6.8 KiB
C++
169 lines
6.8 KiB
C++
|
|
// 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 <Arduino.h>
|
||
|
|
#include <RadioLib.h>
|
||
|
|
|
||
|
|
// --- 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|