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
+107 -3
View File
@@ -7,6 +7,7 @@
#include <new>
#include "meshtastic_header.h"
#include "sweep_view.h"
#include "platform/console.h"
#include "platform/pins.h"
#include "platform/shared_spi.h"
@@ -93,9 +94,15 @@ void RadioService::run() {
: "no radio found");
for (;;) {
uint32_t bits = 0;
xTaskNotifyWait(0, UINT32_MAX, &bits, listening_ ? pdMS_TO_TICKS(kNoiseEveryMs) : portMAX_DELAY);
xTaskNotifyWait(0, UINT32_MAX, &bits,
sweepWanted_ ? pdMS_TO_TICKS(20) : listening_ ? pdMS_TO_TICKS(kNoiseEveryMs) : portMAX_DELAY);
if (probeWanted_.exchange(false)) runProbe();
if (!present_) continue;
if (sweepWanted_) { // a Sweep has the radio: the Sniffer waits, its packets kept (Q99)
sweepPass();
continue;
}
if (sweeping_) endSweep();
bool want = clients_ != 0;
if (want && !listening_) startListening();
else if (!want && listening_) stopListening();
@@ -226,6 +233,84 @@ bool RadioService::packet(uint32_t seq, RadioPacket& out) const {
return ok;
}
void RadioService::sweep(bool on, uint32_t fromHz, uint32_t toHz, uint32_t stepHz) {
if (on) {
stepHz = std::max<uint32_t>(stepHz, 10000);
toHz = std::max(toHz, fromHz);
toHz = std::min<uint32_t>(toHz, fromHz + stepHz * (SweepFrame::kMaxSteps - 1));
sweepFrom_ = fromHz, sweepTo_ = toHz, sweepStep_ = stepHz;
}
sweepWanted_ = on;
wake(kRequest);
}
bool RadioService::sweepFrame(SweepFrame& out) const {
if (!sweepSeq_) return false;
xSemaphoreTake(lock_, portMAX_DELAY);
out = frame_;
xSemaphoreGive(lock_);
return true;
}
// One pass across the band. The first one sets the radio up: the Sniffer is paused, not stopped.
void RadioService::sweepPass() {
if (!sweeping_) {
if (listening_) {
radio_->clearPacketReceivedAction();
listening_ = false; // paused; startListening() resumes on the same ring
}
if (!beginRadio(nullptr)) {
++radioErrors_;
sweepWanted_ = false;
return;
}
radio_->setBandwidth(125);
radio_->setSpreadingFactor(7);
sweeping_ = true;
console.println("radio: sweeping, the Sniffer is paused");
}
uint32_t from = sweepFrom_, to = sweepTo_, step = sweepStep_;
uint16_t steps = static_cast<uint16_t>((to - from) / step + 1);
int8_t dbm[SweepFrame::kMaxSteps];
uint32_t t0 = millis();
for (uint16_t i = 0; i < steps && sweepWanted_; ++i) {
radio_->standby();
// Image calibration is per band and slow; one calibration covers EU868, so skip it per step.
radio_->setFrequency((from + i * step) / 1e6f, i != 0);
radio_->startReceive();
delayMicroseconds(1500); // RSSI settles
float best = -200;
for (int k = 0; k < 3; ++k) {
best = std::max(best, radio_->getRSSI(false));
delayMicroseconds(300);
}
dbm[i] = static_cast<int8_t>(std::clamp(best, -127.0f, 0.0f));
}
radio_->standby();
if (!sweepWanted_) return;
xSemaphoreTake(lock_, portMAX_DELAY);
frame_.seq = sweepSeq_ + 1;
frame_.fromHz = from;
frame_.stepHz = step;
frame_.steps = steps;
frame_.passMs = static_cast<uint16_t>(millis() - t0);
std::copy(dbm, dbm + steps, frame_.dbm);
xSemaphoreGive(lock_);
sweepSeq_ = frame_.seq;
}
void RadioService::endSweep() {
sweeping_ = false;
console.println("radio: sweep stopped");
if (clients_) startListening(); // the Sniffer again, with its settings and its packets
else {
radio_->sleep();
xSemaphoreTake(lock_, portMAX_DELAY);
ring_.reset();
xSemaphoreGive(lock_);
}
}
uint32_t RadioService::available() const {
uint32_t newest = seq_, first = ringFirst_;
if (!listening_ || newest < first) return 0;
@@ -270,7 +355,14 @@ void RadioService::setEcho(bool echo) {
}
void RadioService::tick(uint32_t nowMs) {
(void)nowMs;
SweepFrame f;
if (sweepEcho_ && sweeping_ && nowMs - sweepEchoMs_ >= 2000 && sweepFrame(f)) {
sweepEchoMs_ = nowMs;
lora::SweepStats st = lora::summarize(f.dbm, f.steps, f.fromHz, f.stepHz);
console.printf("sweep: #%lu floor %d, top %d dBm", (unsigned long)f.seq, st.floor, st.top);
for (auto& p : st.peaks) console.printf(", %.1f MHz %d", p.hz / 1e6, p.dbm);
console.printf(" (%u ms a pass)\n", f.passMs);
}
if (!echo_) return;
for (uint32_t s = std::max(echoed_ + 1, seq_ > kRing ? seq_ - kRing + 1 : 1u); s <= seq_; ++s) {
RadioPacket p;
@@ -287,13 +379,25 @@ void RadioService::tick(uint32_t nowMs) {
echoed_ = seq_;
}
void RadioService::printSweep(Print& out) const {
SweepFrame f;
if (!sweepFrame(f)) return (void)out.println("sweep: none yet (lora sweep on)");
out.printf("sweep: #%lu, %u steps of %lu kHz from %.3f MHz, %u ms\n", (unsigned long)f.seq, f.steps,
(unsigned long)(f.stepHz / 1000), f.fromHz / 1e6, f.passMs);
for (uint16_t i = 0; i < f.steps; i += 8) {
out.printf("%8.3f", (f.fromHz + i * f.stepHz) / 1e6);
for (uint16_t k = i; k < f.steps && k < i + 8; ++k) out.printf(" %4d", f.dbm[k]);
out.println();
}
}
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,
sweeping_ ? "sweeping" : 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" : "");
+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_;
};