diff --git a/src/main.cpp b/src/main.cpp index f79b903..0fcd3e2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 the LoRa radio, receive only\n" + "lora custom [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 \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 [preamble]"); + else { + radioService->setConfig({static_cast(mhz * 1e6 + 0.5), static_cast(bw), static_cast(sf), + static_cast(cr), static_cast(sync), static_cast(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()); diff --git a/src/platform/lora_probe.cpp b/src/platform/lora_probe.cpp deleted file mode 100644 index 45ed320..0000000 --- a/src/platform/lora_probe.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "lora_probe.h" - -#include -#include - -#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(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 diff --git a/src/platform/lora_probe.h b/src/platform/lora_probe.h deleted file mode 100644 index c7a323d..0000000 --- a/src/platform/lora_probe.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include - -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 diff --git a/src/services/radio_service.cpp b/src/services/radio_service.cpp new file mode 100644 index 0000000..2f480b0 --- /dev/null +++ b/src/services/radio_service.cpp @@ -0,0 +1,346 @@ +#include "services/radio_service.h" + +#include +#include + +#include +#include + +#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(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(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(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(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 diff --git a/src/services/radio_service.h b/src/services/radio_service.h new file mode 100644 index 0000000..b806039 --- /dev/null +++ b/src/services/radio_service.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#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 hal_; + std::unique_ptr module_; + std::unique_ptr radio_; + TaskHandle_t task_ = nullptr; + mutable SemaphoreHandle_t lock_; // the ring and the config + std::unique_ptr ring_; + Config config_; + bool present_ = false, expander_ = false; + float tcxo_ = 1.8f; + std::atomic clients_{0}; + std::atomic listening_{false}, configChanged_{false}, probeWanted_{false}, echo_{false}; + std::atomic seq_{0}, packets_{0}, crcErrors_{0}, radioErrors_{0}, restarts_{0}; + std::atomic preambles_{0}, headers_{0}, headerErrors_{0}; + std::atomic noise_{0}; + uint32_t lastNoiseMs_ = 0, echoed_ = 0; + Print* probeOut_ = nullptr; + static RadioService* instance_; +}; + +} // namespace roro