#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]; }; // One Sweep pass (M3, Q98): the strongest of three instantaneous RSSI readings at each step. struct SweepFrame { static constexpr size_t kMaxSteps = 160; uint32_t seq = 0; // 1, 2, 3… per pass uint32_t fromHz = 0, stepHz = 0; uint16_t steps = 0; uint16_t passMs = 0; // how long the pass took int8_t dbm[kMaxSteps]; }; // 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 uint32_t lastPacketMs() const { return lastPacketMs_; } // millis(), 0 before the first bool packet(uint32_t seq, RadioPacket& out) const; // false once it has left the ring uint32_t available() const; // packets in the ring: seqs received()-available()+1 to received() float noiseFloor() const { return noise_; } Counters counters() const { return {packets_, crcErrors_, radioErrors_, restarts_, preambles_, headers_, headerErrors_}; } // Sweep (Q98, Q99): takes the radio across a band, pausing the Sniffer (its packets are kept) // until stopped. EU868 by default: 863 to 870 MHz in 100 kHz steps, measured at 125 kHz. void sweep(bool on, uint32_t fromHz = 863000000, uint32_t toHz = 870000000, uint32_t stepHz = 100000); bool sweeping() const { return sweeping_; } bool sweepFrame(SweepFrame& out) const; // the latest pass; false before the first uint32_t sweeps() const { return sweepSeq_; } void setSweepEcho(bool echo) { sweepEcho_ = echo; } // a summary on the console every 2 s void printSweep(Print& out) const; // `lora sweep dump`: the latest pass // Debug aids (Q102). void printStatus(Print& out) const; void setEcho(bool echo); void probe(Print& out); // `lora probe`: runs on the radio task #ifdef RORO_DEBUG // `lora inject`: a packet into the ring as if received, to test the App and Captures with no // transmitter in range. Nothing goes on air. void inject(const uint8_t* data, size_t len, float rssi, float snr); #endif 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 store(RadioPacket& p); void sampleNoise(); void runProbe(); void sweepPass(); void endSweep(); 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 lastPacketMs_{0}; std::atomic ringFirst_{1}; // the first seq the current ring can hold std::atomic sweepWanted_{false}, sweeping_{false}; std::atomic sweepFrom_{863000000}, sweepTo_{870000000}, sweepStep_{100000}, sweepSeq_{0}; SweepFrame frame_; // under lock_ std::atomic noise_{0}; uint32_t lastNoiseMs_ = 0, echoed_ = 0, sweepEchoMs_ = 0; bool sweepEcho_ = false; Print* probeOut_ = nullptr; static RadioService* instance_; }; } // namespace roro