M3 step 5: Sweep, the band's signal strength as bars and a waterfall

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
This commit is contained in:
2026-10-05 20:28:46 +02:00
co-authored by Claude Opus 5.5
parent 2fce79aebd
commit b1de0f8804
12 changed files with 425 additions and 12 deletions
+26 -1
View File
@@ -30,6 +30,16 @@ struct RadioPacket {
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
@@ -76,6 +86,15 @@ class RadioService : public Service {
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);
@@ -100,6 +119,8 @@ class RadioService : public Service {
void store(RadioPacket& p);
void sampleNoise();
void runProbe();
void sweepPass();
void endSweep();
void wake(Notify why);
std::unique_ptr<ArduinoHal> hal_;
@@ -117,8 +138,12 @@ class RadioService : public Service {
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;
uint32_t lastNoiseMs_ = 0, echoed_ = 0, sweepEchoMs_ = 0;
bool sweepEcho_ = false;
Print* probeOut_ = nullptr;
static RadioService* instance_;
};