Public Access
Tab in the LoRa Scanner sweeps 863-870 MHz in 100 kHz steps (the strongest of three RSSI readings at each, at 125 kHz), shown as bars with peak hold over a waterfall, the Sniffer's frequency marked (Q98). The Sweep pauses the Sniffer and keeps its packets; Tab resumes it (Q99). Status Bar: SW. The floor, top and peaks are host-tested; `lora sweep on|off|dump` prints them on the console. At the desk: about 607 ms a pass, a flat floor at -100 to -102 dBm (15 dB above the chip's own) and a steady carrier at 863.2 MHz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
152 lines
5.9 KiB
C++
152 lines
5.9 KiB
C++
#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];
|
|
};
|
|
|
|
// 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<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<uint32_t> lastPacketMs_{0};
|
|
std::atomic<uint32_t> ringFirst_{1}; // the first seq the current ring can hold
|
|
std::atomic<bool> sweepWanted_{false}, sweeping_{false};
|
|
std::atomic<uint32_t> sweepFrom_{863000000}, sweepTo_{870000000}, sweepStep_{100000}, sweepSeq_{0};
|
|
SweepFrame frame_; // under lock_
|
|
std::atomic<float> noise_{0};
|
|
uint32_t lastNoiseMs_ = 0, echoed_ = 0, sweepEchoMs_ = 0;
|
|
bool sweepEcho_ = false;
|
|
Print* probeOut_ = nullptr;
|
|
static RadioService* instance_;
|
|
};
|
|
|
|
} // namespace roro
|