Public Access
M3 step 3: the Radio Service, receive only
One task owns the SX1262 and does all its SPI behind the card's bus lock; the main loop posts requests and DIO1 only wakes the task. It listens while a client asks (App, Capture, Console) and sleeps otherwise, with a ring of the last 32 packets (9.8 KB, freed when idle). No transmit path. The Cap's antenna switch (expander P0) is set on the main loop, which owns the I2C bus. `lora probe` now runs on the radio task and checks the DIO1 interrupt with a receive timeout; `lora status`, `lora rx on|off`, `lora preset`, and `lora custom` for other LoRa settings. Measured: DIO1 works (timeout after 105 ms); four 1.7 MB uploads and Gemini pages to the card while listening, no radio or card errors; task stack peak 2.0 KB. Twenty minutes on LongFast and LoRaWAN: no packets, and a noise floor of -83 to -94 dBm at the desk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
+27
-3
@@ -20,7 +20,6 @@
|
||||
#include "key_mapper.h"
|
||||
#include "platform/console.h"
|
||||
#include "platform/crash_report.h"
|
||||
#include "platform/lora_probe.h"
|
||||
#include "platform/nvs_store.h"
|
||||
#include "platform/system_info.h"
|
||||
#include "service_manager.h"
|
||||
@@ -32,6 +31,7 @@
|
||||
#include "services/gemini_service.h"
|
||||
#include "services/gnss_service.h"
|
||||
#include "services/power_service.h"
|
||||
#include "services/radio_service.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "services/update_service.h"
|
||||
#include "services/wifi_service.h"
|
||||
@@ -57,6 +57,7 @@ static StorageService* storageService;
|
||||
static PowerService* power;
|
||||
static ClockService* clockService;
|
||||
static GnssService* gnssService;
|
||||
static RadioService* radioService;
|
||||
static GeminiService* geminiService;
|
||||
static SavedNetworks* savedNetworks;
|
||||
static WifiService* wifi;
|
||||
@@ -151,6 +152,7 @@ void setup() {
|
||||
clockService = new ClockService(settings, bus);
|
||||
geminiService = new GeminiService(nvs, *storageService, *clockService, bus);
|
||||
gnssService = new GnssService(settings, *clockService, *storageService, bus);
|
||||
radioService = new RadioService();
|
||||
savedNetworks = new SavedNetworks(nvs);
|
||||
savedNetworks->load();
|
||||
wifi = new WifiService(settings, *savedNetworks, *clockService);
|
||||
@@ -162,6 +164,7 @@ void setup() {
|
||||
services.add(*clockService);
|
||||
services.add(*battery);
|
||||
services.add(*gnssService);
|
||||
services.add(*radioService);
|
||||
services.add(*storageService);
|
||||
services.add(*wifi);
|
||||
services.add(*irc);
|
||||
@@ -348,7 +351,8 @@ static const char* const kHelp =
|
||||
"reboot restart\n"
|
||||
"boot other restart into the other app slot (manual Rollback)\n"
|
||||
"log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\n"
|
||||
"lora probe find the LoRa radio: chip, oscillator, antenna switch, noise floor (receive only)\n"
|
||||
"lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio, receive only\n"
|
||||
"lora custom <MHz> <BW kHz> <SF> <CR 5-8> <sync hex> [preamble] e.g. 868.1 125 7 5 34 8 (LoRaWAN)\n"
|
||||
"gnss status | gnss restart | gnss track start|stop | gnss nmea on|off | gnss send <sentence without $ and checksum>\n"
|
||||
"crash the last crash: firmware, reason, task, backtrace\n"
|
||||
"coredump erase forget the core dump in flash\n"
|
||||
@@ -412,7 +416,27 @@ static void runCommand(String line) {
|
||||
});
|
||||
}
|
||||
if (line.startsWith("install ")) update->installFromSd(line.substring(8).c_str()); // Update from SD
|
||||
if (line == "lora probe") loraProbe(console);
|
||||
if (line == "lora probe" && radioService) radioService->probe(console);
|
||||
if (line == "lora status" && radioService) radioService->printStatus(console);
|
||||
if ((line == "lora rx on" || line == "lora rx off") && radioService) radioService->setEcho(line.endsWith("on"));
|
||||
if (line.startsWith("lora custom ") && radioService) {
|
||||
double mhz = 0, bw = 0;
|
||||
unsigned sf = 0, cr = 0, sync = 0, preamble = 8;
|
||||
int n = sscanf(line.c_str() + 12, "%lf %lf %u %u %x %u", &mhz, &bw, &sf, &cr, &sync, &preamble);
|
||||
// Inside what the Cap's SX1262 tunes (868 to 923 MHz) and LoRa's own limits.
|
||||
if (n < 5 || mhz < 863 || mhz > 928 || bw < 7 || bw > 500 || sf < 5 || sf > 12 || cr < 5 || cr > 8 || sync > 0xFF)
|
||||
console.println("lora custom: usage: lora custom <MHz> <BW kHz> <SF 5-12> <CR 5-8> <sync hex> [preamble]");
|
||||
else {
|
||||
radioService->setConfig({static_cast<uint32_t>(mhz * 1e6 + 0.5), static_cast<float>(bw), static_cast<uint8_t>(sf),
|
||||
static_cast<uint8_t>(cr), static_cast<uint8_t>(sync), static_cast<uint16_t>(preamble), "Custom"});
|
||||
console.printf("lora custom: %.3f MHz, BW %.1f kHz, SF %u, CR 4/%u, sync 0x%02X, preamble %u\n", mhz, bw, sf, cr, sync, preamble);
|
||||
}
|
||||
}
|
||||
if (line.startsWith("lora preset ") && radioService) {
|
||||
const meshtastic::Preset* p = meshtastic::findPreset(line.substring(12).c_str());
|
||||
if (p) radioService->setPreset(*p);
|
||||
console.printf("lora preset: %s\n", p ? p->name : "unknown (LongFast LongSlow MediumSlow MediumFast ShortSlow ShortFast LongMod)");
|
||||
}
|
||||
if (line == "gnss status" && gnssService) gnssService->printStatus(console, millis());
|
||||
if ((line == "gnss nmea on" || line == "gnss nmea off") && gnssService) gnssService->setEcho(line.endsWith("on"));
|
||||
if (line == "gnss restart" && gnssService) gnssService->restart(millis());
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
#include "lora_probe.h"
|
||||
|
||||
#include <M5Cardputer.h>
|
||||
#include <RadioLib.h>
|
||||
|
||||
#include "pins.h"
|
||||
#include "shared_spi.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
|
||||
// The antenna switch enable M5Stack's page describes: P0 of a PI4IOE5V6408 on the internal I2C bus.
|
||||
constexpr uint8_t kExpanderAddresses[] = {0x43, 0x44};
|
||||
constexpr uint32_t kI2cFreq = 400000;
|
||||
enum ExpanderReg : uint8_t { kId = 0x01, kDirection = 0x03, kOutput = 0x05, kHighZ = 0x07, kInput = 0x0F };
|
||||
|
||||
// Meshtastic EU868 LongFast (Q95).
|
||||
constexpr float kFreq = 869.525f, kBw = 250.0f;
|
||||
constexpr uint8_t kSf = 11, kCr = 5, kSyncWord = 0x2B;
|
||||
constexpr uint16_t kPreamble = 16;
|
||||
|
||||
Module& module() {
|
||||
// SPI transactions take the bus lock the SD card uses (Q93); the HAL doesn't re-begin the bus.
|
||||
static ArduinoHal hal(sharedSpi(), SPISettings(8000000, MSBFIRST, SPI_MODE0));
|
||||
static Module m(&hal, pins::kLoraCs, pins::kLoraIrq, pins::kLoraReset, pins::kLoraBusy);
|
||||
return m;
|
||||
}
|
||||
|
||||
SX1262& radio() {
|
||||
static SX1262 sx(&module());
|
||||
return sx;
|
||||
}
|
||||
|
||||
// Mean and spread of the instantaneous RSSI while receiving: the noise floor where nothing transmits.
|
||||
void noiseFloor(SX1262& sx, float& mean, float& low, float& high) {
|
||||
sx.startReceive();
|
||||
delay(20);
|
||||
float sum = 0;
|
||||
low = 0, high = -200;
|
||||
constexpr int kSamples = 64;
|
||||
for (int i = 0; i < kSamples; ++i) {
|
||||
float r = sx.getRSSI(false);
|
||||
sum += r;
|
||||
low = std::min(low, r), high = std::max(high, r);
|
||||
delay(3);
|
||||
}
|
||||
mean = sum / kSamples;
|
||||
sx.standby();
|
||||
}
|
||||
|
||||
int findExpander() {
|
||||
for (uint8_t a : kExpanderAddresses)
|
||||
if (M5.In_I2C.scanID(a, kI2cFreq)) return a;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void setP0High(uint8_t addr) {
|
||||
auto& i2c = M5.In_I2C;
|
||||
i2c.writeRegister8(addr, kOutput, i2c.readRegister8(addr, kOutput, kI2cFreq) | 1, kI2cFreq);
|
||||
i2c.writeRegister8(addr, kHighZ, i2c.readRegister8(addr, kHighZ, kI2cFreq) & ~1, kI2cFreq);
|
||||
i2c.writeRegister8(addr, kDirection, i2c.readRegister8(addr, kDirection, kI2cFreq) | 1, kI2cFreq);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void loraProbe(Print& out) {
|
||||
// The internal I2C bus: what answers, and the expander's registers if there is one.
|
||||
bool found[120] = {};
|
||||
M5.In_I2C.scanID(found, kI2cFreq);
|
||||
out.print("lora probe: i2c (internal bus, 8/9):");
|
||||
for (int a = 8; a < 120; ++a)
|
||||
if (found[a]) out.printf(" 0x%02X", a);
|
||||
out.println();
|
||||
int expander = findExpander();
|
||||
if (expander < 0) {
|
||||
out.println("lora probe: expander none at 0x43/0x44");
|
||||
} else {
|
||||
auto r = [&](uint8_t reg) { return M5.In_I2C.readRegister8(expander, reg, kI2cFreq); };
|
||||
out.printf("lora probe: expander 0x%02X id %02X dir %02X out %02X highz %02X in %02X\n", expander, r(kId),
|
||||
r(kDirection), r(kOutput), r(kHighZ), r(kInput));
|
||||
}
|
||||
|
||||
// The chip, with the TCXO first and the crystal as the fallback (Meshtastic's TCXO_OPTIONAL).
|
||||
SX1262& sx = radio();
|
||||
float tcxo = 1.8f;
|
||||
uint32_t t0 = millis();
|
||||
int16_t state = sx.begin(kFreq, kBw, kSf, kCr, kSyncWord, 0, kPreamble, tcxo, false);
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
out.printf("lora probe: begin with TCXO 1.8 V: error %d, trying the crystal\n", state);
|
||||
tcxo = 0;
|
||||
state = sx.begin(kFreq, kBw, kSf, kCr, kSyncWord, 0, kPreamble, tcxo, false);
|
||||
}
|
||||
if (state != RADIOLIB_ERR_NONE) return (void)out.printf("lora probe: error %d, no SX1262 found\n", state);
|
||||
char version[17] = {};
|
||||
module().SPIreadRegisterBurst(RADIOLIB_SX126X_REG_VERSION_STRING, 16, reinterpret_cast<uint8_t*>(version));
|
||||
out.printf("lora probe: %s, %s, ready in %u ms\n", version, tcxo > 0 ? "TCXO 1.8 V" : "crystal (no TCXO)",
|
||||
(unsigned)(millis() - t0));
|
||||
out.printf("lora probe: %.3f MHz, BW %.0f kHz, SF %u, CR 4/%u, sync 0x%02X (LongFast)\n", kFreq, kBw, kSf, kCr,
|
||||
kSyncWord);
|
||||
|
||||
// The antenna switch, by measuring: a receive path that isn't connected to the antenna is
|
||||
// quieter, so the arrangement with the higher noise floor is the one that hears the antenna.
|
||||
auto measure = [&](const char* what) {
|
||||
float mean, low, high;
|
||||
noiseFloor(sx, mean, low, high);
|
||||
out.printf("lora probe: noise %-24s %7.1f dBm (%.1f to %.1f)\n", what, mean, low, high);
|
||||
};
|
||||
sx.setDio2AsRfSwitch(false);
|
||||
measure("DIO2 off");
|
||||
sx.setDio2AsRfSwitch(true);
|
||||
measure("DIO2 on");
|
||||
if (expander >= 0) {
|
||||
setP0High(expander);
|
||||
measure("DIO2 on, expander P0 high");
|
||||
sx.setDio2AsRfSwitch(false);
|
||||
measure("DIO2 off, expander P0 high");
|
||||
sx.setDio2AsRfSwitch(true);
|
||||
}
|
||||
sx.sleep();
|
||||
out.println("lora probe: done, radio asleep");
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Print.h>
|
||||
|
||||
namespace roro {
|
||||
|
||||
// `lora probe` (M3 step 1, Q91): finds the SX1262 on the Cap and how its oscillator and antenna
|
||||
// switch are wired, by measuring. Receive only; leaves the radio asleep.
|
||||
void loraProbe(Print& out);
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,346 @@
|
||||
#include "services/radio_service.h"
|
||||
|
||||
#include <M5Cardputer.h>
|
||||
#include <RadioLib.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <new>
|
||||
|
||||
#include "meshtastic_header.h"
|
||||
#include "platform/console.h"
|
||||
#include "platform/pins.h"
|
||||
#include "platform/shared_spi.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
|
||||
// The Cap's PI4IOE5V6408: its P0 connects the antenna; at power-on it's an input and the receiver
|
||||
// is deaf (M3 step 1, measured). Only touched from the main loop, which owns the I2C bus.
|
||||
constexpr uint8_t kExpander = 0x43;
|
||||
constexpr uint32_t kI2cFreq = 400000;
|
||||
enum ExpanderReg : uint8_t { kId = 0x01, kDirection = 0x03, kOutput = 0x05, kHighZ = 0x07, kInput = 0x0F };
|
||||
|
||||
bool antennaOn() {
|
||||
auto& i2c = M5.In_I2C;
|
||||
if (!i2c.scanID(kExpander, kI2cFreq)) return false;
|
||||
i2c.writeRegister8(kExpander, kOutput, i2c.readRegister8(kExpander, kOutput, kI2cFreq) | 1, kI2cFreq);
|
||||
i2c.writeRegister8(kExpander, kHighZ, i2c.readRegister8(kExpander, kHighZ, kI2cFreq) & ~1, kI2cFreq);
|
||||
i2c.writeRegister8(kExpander, kDirection, i2c.readRegister8(kExpander, kDirection, kI2cFreq) | 1, kI2cFreq);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Below this the receiver hears only itself: the antenna path is open (step 1: -111.9 dBm).
|
||||
constexpr float kDeafBelow = -108.0f;
|
||||
|
||||
} // namespace
|
||||
|
||||
RadioService* RadioService::instance_ = nullptr;
|
||||
|
||||
RadioService::RadioService() : lock_(xSemaphoreCreateMutex()) {
|
||||
const meshtastic::Preset& p = meshtastic::kEu868Presets[0]; // LongFast (Q95)
|
||||
config_ = {meshtastic::eu868FrequencyHz(p), p.bwKHz, p.sf, p.cr, meshtastic::kSyncWord, meshtastic::kPreambleLength,
|
||||
p.name};
|
||||
}
|
||||
|
||||
void RadioService::start() {
|
||||
instance_ = this;
|
||||
expander_ = antennaOn();
|
||||
// SPI transactions take the bus lock the SD card uses; the HAL doesn't re-begin the bus.
|
||||
hal_.reset(new ArduinoHal(sharedSpi(), SPISettings(8000000, MSBFIRST, SPI_MODE0)));
|
||||
module_.reset(new Module(hal_.get(), pins::kLoraCs, pins::kLoraIrq, pins::kLoraReset, pins::kLoraBusy));
|
||||
radio_.reset(new SX1262(module_.get()));
|
||||
// Above the main loop and the other Services' tasks, so a packet is read before the next.
|
||||
xTaskCreatePinnedToCore(taskEntry, "radio", 4096, this, 2, &task_, 1); // peak 2.0 KB (M3 step 3)
|
||||
}
|
||||
|
||||
void RadioService::taskEntry(void* self) { static_cast<RadioService*>(self)->run(); }
|
||||
|
||||
void IRAM_ATTR RadioService::onDio1() {
|
||||
BaseType_t woken = pdFALSE;
|
||||
if (instance_ && instance_->task_) xTaskNotifyFromISR(instance_->task_, kIrq, eSetBits, &woken);
|
||||
portYIELD_FROM_ISR(woken);
|
||||
}
|
||||
|
||||
void RadioService::wake(Notify why) {
|
||||
if (task_) xTaskNotify(task_, why, eSetBits);
|
||||
}
|
||||
|
||||
bool RadioService::beginRadio(Print* report) {
|
||||
Config c = config();
|
||||
float mhz = c.frequencyHz / 1e6f;
|
||||
int16_t state = radio_->begin(mhz, c.bandwidthKHz, c.spreadingFactor, c.codingRate, c.syncWord, 0, c.preamble,
|
||||
tcxo_, false);
|
||||
if (state != RADIOLIB_ERR_NONE && tcxo_ > 0) { // Meshtastic's TCXO_OPTIONAL: fall back to the crystal
|
||||
if (report) report->printf("lora probe: begin with TCXO %.1f V: error %d, trying the crystal\n", tcxo_, state);
|
||||
tcxo_ = 0;
|
||||
state = radio_->begin(mhz, c.bandwidthKHz, c.spreadingFactor, c.codingRate, c.syncWord, 0, c.preamble, tcxo_,
|
||||
false);
|
||||
}
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
if (report) report->printf("lora probe: error %d, no SX1262 found\n", state);
|
||||
return false;
|
||||
}
|
||||
radio_->setDio2AsRfSwitch(true); // DIO2 selects TX or RX in the switch, as in Meshtastic
|
||||
radio_->setRxBoostedGainMode(true); // about 2 dB more sensitivity for about 2 mA
|
||||
return true;
|
||||
}
|
||||
|
||||
void RadioService::run() {
|
||||
present_ = beginRadio(nullptr);
|
||||
if (present_) radio_->sleep();
|
||||
console.printf("radio: %s\n", present_ ? (tcxo_ > 0 ? "SX1262 ready (TCXO), asleep" : "SX1262 ready (crystal), asleep")
|
||||
: "no radio found");
|
||||
for (;;) {
|
||||
uint32_t bits = 0;
|
||||
xTaskNotifyWait(0, UINT32_MAX, &bits, listening_ ? pdMS_TO_TICKS(kNoiseEveryMs) : portMAX_DELAY);
|
||||
if (probeWanted_.exchange(false)) runProbe();
|
||||
if (!present_) continue;
|
||||
bool want = clients_ != 0;
|
||||
if (want && !listening_) startListening();
|
||||
else if (!want && listening_) stopListening();
|
||||
if (listening_ && configChanged_.exchange(false)) startListening(); // again, with the new settings
|
||||
if (listening_ && (bits & kIrq)) readPacket();
|
||||
if (listening_ && millis() - lastNoiseMs_ >= kNoiseEveryMs) sampleNoise();
|
||||
}
|
||||
}
|
||||
|
||||
void RadioService::startListening() {
|
||||
if (!ring_) {
|
||||
ring_.reset(new (std::nothrow) RadioPacket[kRing]);
|
||||
if (!ring_) {
|
||||
console.println("radio: not enough memory to listen");
|
||||
clients_ = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// A full begin (with a reset) each time: the chip comes back from sleep with nothing assumed.
|
||||
if (!beginRadio(nullptr)) {
|
||||
++radioErrors_;
|
||||
return;
|
||||
}
|
||||
radio_->setPacketReceivedAction(onDio1);
|
||||
int16_t state = receive();
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
++radioErrors_;
|
||||
console.printf("radio: can't receive (error %d)\n", state);
|
||||
return;
|
||||
}
|
||||
if (!listening_) console.printf("radio: listening, %s\n", config().name);
|
||||
listening_ = true;
|
||||
lastNoiseMs_ = 0;
|
||||
}
|
||||
|
||||
void RadioService::stopListening() {
|
||||
radio_->clearPacketReceivedAction();
|
||||
radio_->sleep();
|
||||
listening_ = false;
|
||||
noise_ = 0;
|
||||
xSemaphoreTake(lock_, portMAX_DELAY);
|
||||
ring_.reset(); // listening again starts a fresh list; idle costs nothing (Q100, Q103)
|
||||
xSemaphoreGive(lock_);
|
||||
console.println("radio: asleep");
|
||||
}
|
||||
|
||||
void RadioService::readPacket() {
|
||||
RadioPacket p;
|
||||
size_t len = std::min<size_t>(radio_->getPacketLength(), sizeof p.data);
|
||||
int16_t state = radio_->readData(p.data, len);
|
||||
if (state == RADIOLIB_ERR_CRC_MISMATCH) {
|
||||
p.crcOk = false;
|
||||
++crcErrors_;
|
||||
} else if (state != RADIOLIB_ERR_NONE) { // a header error or a timeout: nothing to keep
|
||||
++radioErrors_;
|
||||
if (receive() != RADIOLIB_ERR_NONE) ++radioErrors_;
|
||||
++restarts_;
|
||||
return;
|
||||
}
|
||||
Config c = config();
|
||||
p.ms = millis();
|
||||
p.len = static_cast<uint8_t>(len);
|
||||
p.rx.frequencyHz = c.frequencyHz;
|
||||
p.rx.bandwidthKHz = c.bandwidthKHz;
|
||||
p.rx.spreadingFactor = c.spreadingFactor;
|
||||
p.rx.syncWord = c.syncWord;
|
||||
p.rx.rssi = radio_->getRSSI();
|
||||
p.rx.snr = radio_->getSNR();
|
||||
p.rx.noiseFloor = noise_;
|
||||
p.frequencyError = radio_->getFrequencyError();
|
||||
if (receive() != RADIOLIB_ERR_NONE) ++radioErrors_;
|
||||
++restarts_;
|
||||
|
||||
xSemaphoreTake(lock_, portMAX_DELAY);
|
||||
p.seq = seq_ + 1;
|
||||
if (ring_) ring_[p.seq % kRing] = p;
|
||||
seq_ = p.seq;
|
||||
xSemaphoreGive(lock_);
|
||||
++packets_;
|
||||
}
|
||||
|
||||
// Continuous receive. Only RX done raises DIO1; preambles and headers are only recorded in the
|
||||
// IRQ status, which sampleNoise() reads.
|
||||
int16_t RadioService::receive() {
|
||||
return radio_->startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF,
|
||||
RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED),
|
||||
RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
|
||||
}
|
||||
|
||||
void RadioService::sampleNoise() {
|
||||
lastNoiseMs_ = millis();
|
||||
noise_ = radio_->getRSSI(false);
|
||||
constexpr uint16_t kSeen =
|
||||
RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR;
|
||||
uint32_t flags = radio_->getIrqFlags();
|
||||
if (flags & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED) ++preambles_;
|
||||
if (flags & RADIOLIB_SX126X_IRQ_HEADER_VALID) ++headers_;
|
||||
if (flags & RADIOLIB_SX126X_IRQ_HEADER_ERR) ++headerErrors_;
|
||||
if (flags & kSeen) radio_->clearIrqFlags(kSeen); // never RX done: that one is the task's
|
||||
}
|
||||
|
||||
bool RadioService::packet(uint32_t seq, RadioPacket& out) const {
|
||||
if (seq == 0 || seq > seq_ || seq_ - seq >= kRing) return false;
|
||||
xSemaphoreTake(lock_, portMAX_DELAY);
|
||||
bool ok = ring_ && ring_[seq % kRing].seq == seq;
|
||||
if (ok) out = ring_[seq % kRing];
|
||||
xSemaphoreGive(lock_);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void RadioService::listen(Client client, bool on) {
|
||||
if (on) clients_ |= client;
|
||||
else clients_ &= ~client;
|
||||
wake(kRequest);
|
||||
}
|
||||
|
||||
RadioService::Config RadioService::config() const {
|
||||
xSemaphoreTake(lock_, portMAX_DELAY);
|
||||
Config c = config_;
|
||||
xSemaphoreGive(lock_);
|
||||
return c;
|
||||
}
|
||||
|
||||
void RadioService::setPreset(const meshtastic::Preset& p) {
|
||||
xSemaphoreTake(lock_, portMAX_DELAY);
|
||||
config_ = {meshtastic::eu868FrequencyHz(p), p.bwKHz, p.sf, p.cr, meshtastic::kSyncWord, meshtastic::kPreambleLength,
|
||||
p.name};
|
||||
xSemaphoreGive(lock_);
|
||||
configChanged_ = true;
|
||||
wake(kRequest);
|
||||
}
|
||||
|
||||
void RadioService::setConfig(const Config& c) {
|
||||
xSemaphoreTake(lock_, portMAX_DELAY);
|
||||
config_ = c;
|
||||
config_.name = "Custom";
|
||||
xSemaphoreGive(lock_);
|
||||
configChanged_ = true;
|
||||
wake(kRequest);
|
||||
}
|
||||
|
||||
void RadioService::setEcho(bool echo) {
|
||||
echo_ = echo;
|
||||
echoed_ = seq_;
|
||||
listen(Console, echo);
|
||||
}
|
||||
|
||||
void RadioService::tick(uint32_t nowMs) {
|
||||
(void)nowMs;
|
||||
if (!echo_) return;
|
||||
for (uint32_t s = std::max(echoed_ + 1, seq_ > kRing ? seq_ - kRing + 1 : 1u); s <= seq_; ++s) {
|
||||
RadioPacket p;
|
||||
if (!packet(s, p)) continue;
|
||||
console.printf("lora: #%lu %u B %.1f dBm SNR %.1f dB%s", (unsigned long)p.seq, p.len, p.rx.rssi, p.rx.snr,
|
||||
p.crcOk ? "" : " (bad CRC)");
|
||||
meshtastic::PacketHeader h;
|
||||
if (meshtastic::parseHeader(p.data, p.len, h))
|
||||
console.printf(" | %s > %s, ch 0x%02X, hops %d/%u%s", meshtastic::nodeId(h.from).c_str(),
|
||||
meshtastic::nodeId(h.to).c_str(), h.channelHash, h.hopsAway(), h.hopStart(),
|
||||
h.viaMqtt() ? ", via MQTT" : "");
|
||||
console.println();
|
||||
}
|
||||
echoed_ = seq_;
|
||||
}
|
||||
|
||||
void RadioService::printStatus(Print& out) const {
|
||||
Config c = config();
|
||||
Counters n = counters();
|
||||
out.printf("radio: %s, %s, antenna switch %s\n", present_ ? "SX1262" : "no radio",
|
||||
tcxo_ > 0 ? "TCXO 1.8 V" : "crystal", expander_ ? "on (expander 0x43 P0)" : "expander missing");
|
||||
out.printf("radio: %s, %s %.3f MHz, BW %.0f kHz, SF %u, CR 4/%u, sync 0x%02X, preamble %u\n",
|
||||
listening_ ? "listening" : "asleep", c.name, c.frequencyHz / 1e6, c.bandwidthKHz, c.spreadingFactor,
|
||||
c.codingRate, c.syncWord, c.preamble);
|
||||
out.printf("radio: clients%s%s%s%s\n", clients_ ? "" : " none", clients_ & App ? " App" : "",
|
||||
clients_ & Capture ? " Capture" : "", clients_ & Console ? " Console" : "");
|
||||
out.printf("radio: %lu packets, %lu bad CRC, %lu radio errors, %lu receive restarts\n", (unsigned long)n.packets,
|
||||
(unsigned long)n.crcErrors, (unsigned long)n.radioErrors, (unsigned long)n.restarts);
|
||||
out.printf("radio: activity (500 ms samples): %lu preambles, %lu headers, %lu bad headers\n",
|
||||
(unsigned long)n.preambles, (unsigned long)n.headers, (unsigned long)n.headerErrors);
|
||||
if (listening_)
|
||||
out.printf("radio: noise floor %.1f dBm%s\n", noise_.load(),
|
||||
noise_ < kDeafBelow ? " (deaf? the antenna path looks open)" : "");
|
||||
if (task_) out.printf("radio: task stack %u B free\n", (unsigned)uxTaskGetStackHighWaterMark(task_));
|
||||
}
|
||||
|
||||
void RadioService::probe(Print& out) {
|
||||
// The I2C part here, on the main loop; the radio part on the radio task.
|
||||
bool found[120] = {};
|
||||
M5.In_I2C.scanID(found, kI2cFreq);
|
||||
out.print("lora probe: i2c (internal bus, 8/9):");
|
||||
for (int a = 8; a < 120; ++a)
|
||||
if (found[a]) out.printf(" 0x%02X", a);
|
||||
out.println();
|
||||
if (found[kExpander]) {
|
||||
auto r = [](uint8_t reg) { return M5.In_I2C.readRegister8(kExpander, reg, kI2cFreq); };
|
||||
out.printf("lora probe: expander 0x43 dir %02X out %02X highz %02X in %02X: antenna %s\n", r(kDirection),
|
||||
r(kOutput), r(kHighZ), r(kInput), r(kOutput) & 1 && r(kDirection) & 1 ? "on" : "OFF");
|
||||
} else {
|
||||
out.println("lora probe: no expander at 0x43: the antenna switch can't be enabled");
|
||||
}
|
||||
probeOut_ = &out;
|
||||
probeWanted_ = true;
|
||||
wake(kRequest);
|
||||
}
|
||||
|
||||
void RadioService::runProbe() {
|
||||
Print& out = *probeOut_;
|
||||
uint32_t t0 = millis();
|
||||
if (!beginRadio(&out)) return;
|
||||
char version[17] = {};
|
||||
module_->SPIreadRegisterBurst(RADIOLIB_SX126X_REG_VERSION_STRING, 16, reinterpret_cast<uint8_t*>(version));
|
||||
out.printf("lora probe: %s, %s, ready in %u ms\n", version, tcxo_ > 0 ? "TCXO 1.8 V" : "crystal (no TCXO)",
|
||||
(unsigned)(millis() - t0));
|
||||
// DIO1 to the task, with no transmitter needed: a 100 ms receive timeout, routed to DIO1.
|
||||
radio_->setPacketReceivedAction(onDio1);
|
||||
uint32_t bits = 0;
|
||||
xTaskNotifyWait(0, UINT32_MAX, &bits, 0); // drop anything pending
|
||||
uint32_t sent = millis();
|
||||
radio_->startReceive(6400, RADIOLIB_IRQ_RX_DEFAULT_FLAGS,
|
||||
(1UL << RADIOLIB_IRQ_RX_DONE) | (1UL << RADIOLIB_IRQ_TIMEOUT), 0); // 6400 x 15.625 us
|
||||
bits = 0;
|
||||
xTaskNotifyWait(0, UINT32_MAX, &bits, pdMS_TO_TICKS(500));
|
||||
uint32_t flags = radio_->getIrqFlags();
|
||||
if (bits & kIrq)
|
||||
out.printf("lora probe: DIO1 interrupt works (receive timeout after %u ms)\n", (unsigned)(millis() - sent));
|
||||
else
|
||||
out.printf("lora probe: DIO1 interrupt never came (IRQ status 0x%04X)\n", (unsigned)flags);
|
||||
radio_->standby();
|
||||
radio_->clearIrqFlags(RADIOLIB_SX126X_IRQ_ALL);
|
||||
if (!listening_) radio_->clearPacketReceivedAction();
|
||||
receive();
|
||||
delay(20);
|
||||
float sum = 0, low = 0, high = -200;
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
float r = radio_->getRSSI(false);
|
||||
sum += r, low = std::min(low, r), high = std::max(high, r);
|
||||
delay(3);
|
||||
}
|
||||
float mean = sum / 64;
|
||||
out.printf("lora probe: noise %.1f dBm (%.1f to %.1f) at %.3f MHz: %s\n", mean, low, high, config().frequencyHz / 1e6,
|
||||
mean < kDeafBelow ? "deaf, the antenna path looks open" : "the antenna is connected");
|
||||
// Back to what it was doing.
|
||||
if (listening_) startListening();
|
||||
else radio_->sleep();
|
||||
out.println("lora probe: done");
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,116 @@
|
||||
#pragma once
|
||||
|
||||
#include <Print.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "loratap.h"
|
||||
#include "meshtastic_presets.h"
|
||||
#include "service.h"
|
||||
|
||||
class SX1262;
|
||||
class Module;
|
||||
class ArduinoHal;
|
||||
|
||||
namespace roro {
|
||||
|
||||
// One received packet, as the Radio Service keeps it.
|
||||
struct RadioPacket {
|
||||
uint32_t seq = 0; // 1, 2, 3… since boot
|
||||
uint32_t ms = 0; // millis() when it was read (the Clock is read on the main loop, not here)
|
||||
lora::RxInfo rx;
|
||||
float frequencyError = 0; // Hz
|
||||
bool crcOk = true;
|
||||
uint8_t len = 0;
|
||||
uint8_t data[255];
|
||||
};
|
||||
|
||||
// The LoRa radio on the Cap (CONTEXT.md: Radio Service; docs/milestones/M3.md). One task owns the
|
||||
// SX1262 and does all its SPI traffic, behind the bus lock the SD card uses (Q93); the main loop
|
||||
// only posts requests, and DIO1's interrupt only wakes the task. **Receive only (Q94): there is
|
||||
// no way to transmit.** The radio listens while someone asks it to (Q100) and sleeps otherwise.
|
||||
class RadioService : public Service {
|
||||
public:
|
||||
enum Client : uint8_t { App = 1, Capture = 2, Console = 4 };
|
||||
static constexpr size_t kRing = 32; // Q103
|
||||
static constexpr uint32_t kNoiseEveryMs = 500; // instantaneous RSSI while listening
|
||||
|
||||
struct Config {
|
||||
uint32_t frequencyHz;
|
||||
float bandwidthKHz;
|
||||
uint8_t spreadingFactor, codingRate, syncWord;
|
||||
uint16_t preamble;
|
||||
const char* name; // the preset's, or "Custom"
|
||||
};
|
||||
struct Counters {
|
||||
uint32_t packets, crcErrors, radioErrors, restarts;
|
||||
// Seen in the IRQ status at each noise sample (every 500 ms). Preamble detections include
|
||||
// false alarms in noise (M3: more on an empty frequency than on LongFast); headers don't.
|
||||
uint32_t preambles, headers, headerErrors;
|
||||
};
|
||||
|
||||
RadioService();
|
||||
const char* name() const override { return "radio"; }
|
||||
uint32_t tickIntervalMs() const override { return 100; }
|
||||
void start() override;
|
||||
void tick(uint32_t nowMs) override;
|
||||
|
||||
bool present() const { return present_; }
|
||||
bool listening() const { return listening_; }
|
||||
void listen(Client client, bool on);
|
||||
Config config() const;
|
||||
void setPreset(const meshtastic::Preset& preset);
|
||||
void setConfig(const Config& config); // custom settings (Q95), named "Custom"
|
||||
|
||||
uint32_t received() const { return seq_; } // seq of the newest packet
|
||||
bool packet(uint32_t seq, RadioPacket& out) const; // false once it has left the ring
|
||||
float noiseFloor() const { return noise_; }
|
||||
Counters counters() const {
|
||||
return {packets_, crcErrors_, radioErrors_, restarts_, preambles_, headers_, headerErrors_};
|
||||
}
|
||||
|
||||
// Debug aids (Q102).
|
||||
void printStatus(Print& out) const;
|
||||
void setEcho(bool echo);
|
||||
void probe(Print& out); // `lora probe`: runs on the radio task
|
||||
|
||||
private:
|
||||
enum Notify : uint32_t { kIrq = 1, kRequest = 2 };
|
||||
static void taskEntry(void* self);
|
||||
static void onDio1();
|
||||
void run();
|
||||
bool beginRadio(Print* report);
|
||||
void applyConfig();
|
||||
void startListening();
|
||||
void stopListening();
|
||||
int16_t receive();
|
||||
void readPacket();
|
||||
void sampleNoise();
|
||||
void runProbe();
|
||||
void wake(Notify why);
|
||||
|
||||
std::unique_ptr<ArduinoHal> hal_;
|
||||
std::unique_ptr<Module> module_;
|
||||
std::unique_ptr<SX1262> radio_;
|
||||
TaskHandle_t task_ = nullptr;
|
||||
mutable SemaphoreHandle_t lock_; // the ring and the config
|
||||
std::unique_ptr<RadioPacket[]> ring_;
|
||||
Config config_;
|
||||
bool present_ = false, expander_ = false;
|
||||
float tcxo_ = 1.8f;
|
||||
std::atomic<uint8_t> clients_{0};
|
||||
std::atomic<bool> listening_{false}, configChanged_{false}, probeWanted_{false}, echo_{false};
|
||||
std::atomic<uint32_t> seq_{0}, packets_{0}, crcErrors_{0}, radioErrors_{0}, restarts_{0};
|
||||
std::atomic<uint32_t> preambles_{0}, headers_{0}, headerErrors_{0};
|
||||
std::atomic<float> noise_{0};
|
||||
uint32_t lastNoiseMs_ = 0, echoed_ = 0;
|
||||
Print* probeOut_ = nullptr;
|
||||
static RadioService* instance_;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user