Public Access
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:
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "meshtastic_presets.h"
|
||||
#include "packet_view.h"
|
||||
#include "sweep_view.h"
|
||||
#include "ui/canvas.h"
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/widgets.h"
|
||||
@@ -30,7 +31,21 @@ void LoraScannerApp::onEnter() {
|
||||
requestRedraw();
|
||||
}
|
||||
|
||||
void LoraScannerApp::onExit() { radio_.listen(RadioService::App, false); }
|
||||
void LoraScannerApp::onExit() {
|
||||
showSweep(false);
|
||||
radio_.listen(RadioService::App, false);
|
||||
}
|
||||
|
||||
void LoraScannerApp::showSweep(bool on) {
|
||||
if (on == (view_ == View::Sweep)) return;
|
||||
radio_.sweep(on);
|
||||
view_ = on ? View::Sweep : View::Packets;
|
||||
peak_.clear();
|
||||
std::vector<int8_t>().swap(waterfall_); // its memory only while shown
|
||||
sweepSteps_ = 0;
|
||||
shownSweep_ = radio_.sweeps();
|
||||
requestRedraw();
|
||||
}
|
||||
|
||||
bool LoraScannerApp::meshtasticSettings() const { return radio_.config().syncWord == meshtastic::kSyncWord; }
|
||||
|
||||
@@ -41,6 +56,17 @@ void LoraScannerApp::say(const std::string& text) {
|
||||
}
|
||||
|
||||
bool LoraScannerApp::onKey(const KeyEvent& e) {
|
||||
if (e.key == Key::Tab && (view_ == View::Packets || view_ == View::Sweep)) {
|
||||
showSweep(view_ == View::Packets);
|
||||
return true;
|
||||
}
|
||||
if (view_ == View::Sweep) {
|
||||
if (e.key == Key::Back) {
|
||||
showSweep(false);
|
||||
return true;
|
||||
}
|
||||
return e.key != Key::Home; // everything else stays here
|
||||
}
|
||||
if (view_ == View::Details) {
|
||||
int last = std::max<int>(0, static_cast<int>(details_.size()) - 8);
|
||||
if (e.key == Key::Up) detailsTop_ = std::max(0, detailsTop_ - 1);
|
||||
@@ -130,6 +156,10 @@ std::string LoraScannerApp::timeOf(const RadioPacket& p) const {
|
||||
}
|
||||
|
||||
void LoraScannerApp::update(uint32_t nowMs) {
|
||||
if (view_ == View::Sweep) {
|
||||
if (radio_.sweeps() != shownSweep_) requestRedraw();
|
||||
return;
|
||||
}
|
||||
if (radio_.received() != shownSeq_ || nowMs - lastDrawMs_ >= 1000 ||
|
||||
(!message_.empty() && nowMs - messageMs_ >= kMessageMs))
|
||||
requestRedraw();
|
||||
@@ -151,6 +181,7 @@ void LoraScannerApp::draw(Canvas& c) {
|
||||
case View::Packets: drawPackets(c); break;
|
||||
case View::Details: drawDetails(c); break;
|
||||
case View::Presets: drawPresets(c); break;
|
||||
case View::Sweep: drawSweep(c); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +234,7 @@ void LoraScannerApp::drawPackets(Canvas& c) {
|
||||
c.setFont(&fonts::small);
|
||||
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
|
||||
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
|
||||
std::string keys = std::string("Enter: details p: preset c: ") + (capture_.capturing() ? "stop capture" : "capture");
|
||||
std::string keys = std::string("Enter: details p: preset Tab: sweep c: ") + (capture_.capturing() ? "stop" : "capture");
|
||||
c.drawString(showMessage ? message_.c_str() : keys.c_str(), 4, area.y + area.h - 9);
|
||||
}
|
||||
|
||||
@@ -229,3 +260,81 @@ void LoraScannerApp::drawPresets(Canvas& c) {
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Sweep (Q98): bars of the latest pass with peak hold, the waterfall underneath (newest at the
|
||||
// top), and where the Sniffer's preset sits in the band.
|
||||
void LoraScannerApp::drawSweep(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
constexpr int kLow = -120, kHigh = -60; // the scale, dBm: about the chip's floor to a near signal
|
||||
SweepFrame f;
|
||||
bool have = radio_.sweepFrame(f) && f.seq != 0;
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("Sweep, the Sniffer is paused", 4, area.y + 2);
|
||||
if (!have) {
|
||||
c.drawString("Measuring...", 4, area.y + 20);
|
||||
return;
|
||||
}
|
||||
// Keep peak hold and the waterfall for this band.
|
||||
if (f.steps != sweepSteps_) {
|
||||
sweepSteps_ = f.steps;
|
||||
peak_.assign(f.steps, -127);
|
||||
waterfall_.assign(static_cast<size_t>(kWaterfallRows) * f.steps, -127);
|
||||
}
|
||||
if (f.seq != shownSweep_) {
|
||||
shownSweep_ = f.seq;
|
||||
for (uint16_t i = 0; i < f.steps; ++i) peak_[i] = std::max(peak_[i], f.dbm[i]);
|
||||
std::copy_backward(waterfall_.begin(), waterfall_.end() - f.steps, waterfall_.end());
|
||||
std::copy(f.dbm, f.dbm + f.steps, waterfall_.begin());
|
||||
}
|
||||
|
||||
// Layout: up to 3 px a step, left-aligned after the scale.
|
||||
int x0 = 26, cell = std::max(1, std::min(3, (area.w - x0 - 2) / std::max<int>(1, f.steps)));
|
||||
int barTop = area.y + 12, barH = 46, barBottom = barTop + barH;
|
||||
auto height = [&](int dbm) { return std::clamp((dbm - kLow) * barH / (kHigh - kLow), 0, barH); };
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("-60", 2, barTop - 1);
|
||||
c.drawString("-120", 0, barBottom - 7);
|
||||
for (uint16_t i = 0; i < f.steps; ++i) {
|
||||
int x = x0 + i * cell, h = height(f.dbm[i]);
|
||||
if (h > 0) c.fillRect(x, barBottom - h, std::max(1, cell - 1), h, theme::kAccent);
|
||||
int ph = height(peak_[i]);
|
||||
if (ph > 0) c.drawFastHLine(x, barBottom - ph, std::max(1, cell - 1), theme::kText);
|
||||
}
|
||||
// The Sniffer's frequency, when it's in the band.
|
||||
uint32_t hz = radio_.config().frequencyHz;
|
||||
if (hz >= f.fromHz && hz <= f.fromHz + (f.steps - 1) * f.stepHz) {
|
||||
int x = x0 + static_cast<int>((hz - f.fromHz) / f.stepHz) * cell + cell / 2;
|
||||
for (int y = barTop; y < barBottom; y += 3) c.drawPixel(x, y, theme::kMessage);
|
||||
}
|
||||
|
||||
// The waterfall.
|
||||
int wfTop = barBottom + 2;
|
||||
for (int r = 0; r < kWaterfallRows; ++r)
|
||||
for (uint16_t i = 0; i < f.steps; ++i) {
|
||||
int8_t v = waterfall_[static_cast<size_t>(r) * f.steps + i];
|
||||
if (v > -127) c.fillRect(x0 + i * cell, wfTop + r, cell, 1, lora::heatColor(lora::heatLevel(v, kLow, kHigh)));
|
||||
}
|
||||
|
||||
// The band's edges, its floor and its strongest signal.
|
||||
lora::SweepStats st = lora::summarize(f.dbm, f.steps, f.fromHz, f.stepHz);
|
||||
char line[64];
|
||||
int y = area.y + area.h - 9;
|
||||
std::snprintf(line, sizeof line, "%.1f", f.fromHz / 1e6);
|
||||
c.drawString(line, x0, wfTop + kWaterfallRows + 1);
|
||||
std::snprintf(line, sizeof line, "%.1f MHz", (f.fromHz + (f.steps - 1) * f.stepHz) / 1e6);
|
||||
c.setTextDatum(top_right);
|
||||
c.drawString(line, x0 + f.steps * cell, wfTop + kWaterfallRows + 1);
|
||||
c.setTextDatum(top_left);
|
||||
if (st.peaks.empty())
|
||||
std::snprintf(line, sizeof line, "floor %d dBm, nothing above it Tab: sniffer", st.floor);
|
||||
else
|
||||
std::snprintf(line, sizeof line, "floor %d, %.1f MHz at %d dBm Tab: sniffer", st.floor, st.peaks[0].hz / 1e6,
|
||||
st.peaks[0].dbm);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString(line, 4, y);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace roro {
|
||||
|
||||
// The LoRa Scanner (docs/milestones/M3.md). Sniffer: the packets the radio hears, newest first,
|
||||
// with the Meshtastic header read (Q96) and a hex dump on Enter; p picks the preset (Q95), c starts
|
||||
// or stops a Capture (Q97). The radio listens while the App is open (Q100).
|
||||
// or stops a Capture (Q97). The radio listens while the App is open (Q100). Tab: Sweep, the band's
|
||||
// RSSI as bars with peak hold and a waterfall (Q98); it pauses the Sniffer while shown (Q99).
|
||||
class LoraScannerApp : public App {
|
||||
public:
|
||||
LoraScannerApp(RadioService& radio, LoraCaptureService& capture, Settings& settings, ClockService& clock)
|
||||
@@ -27,12 +28,14 @@ class LoraScannerApp : public App {
|
||||
void draw(Canvas& c) override;
|
||||
|
||||
private:
|
||||
enum class View { Packets, Details, Presets };
|
||||
enum class View { Packets, Details, Presets, Sweep };
|
||||
static constexpr int kListRows = 7;
|
||||
|
||||
void drawPackets(Canvas& c);
|
||||
void drawDetails(Canvas& c);
|
||||
void drawPresets(Canvas& c);
|
||||
void drawSweep(Canvas& c);
|
||||
void showSweep(bool on);
|
||||
void openDetails();
|
||||
std::string timeOf(const RadioPacket& p) const;
|
||||
bool meshtasticSettings() const;
|
||||
@@ -53,6 +56,12 @@ class LoraScannerApp : public App {
|
||||
uint32_t lastDrawMs_ = 0;
|
||||
std::string message_;
|
||||
uint32_t messageMs_ = 0;
|
||||
// Sweep, only while shown: peak hold per step, and the waterfall, newest row first.
|
||||
static constexpr int kWaterfallRows = 36;
|
||||
uint32_t shownSweep_ = 0;
|
||||
std::vector<int8_t> peak_;
|
||||
std::vector<int8_t> waterfall_; // kWaterfallRows x steps
|
||||
uint16_t sweepSteps_ = 0;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
+15
-1
@@ -109,7 +109,8 @@ static StatusInfo currentStatus() {
|
||||
s.gnssSatellites = g.satellitesUsed;
|
||||
s.tracking = gnssService->tracking();
|
||||
}
|
||||
if (radioService && radioService->listening()) {
|
||||
if (radioService && radioService->sweeping()) s.radio = StatusInfo::Radio::Sweep;
|
||||
else if (radioService && radioService->listening()) {
|
||||
uint32_t last = radioService->lastPacketMs();
|
||||
s.radio = last && millis() - last < 400 ? StatusInfo::Radio::Packet : StatusInfo::Radio::Listening;
|
||||
}
|
||||
@@ -364,6 +365,7 @@ static const char* const kHelp =
|
||||
"log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\n"
|
||||
"lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio, receive only\n"
|
||||
"lora capture start|stop a LoRa Capture to /captures/lora (pcap, LoRaTap)\n"
|
||||
"lora sweep on [from MHz] [to MHz] [step kHz] | off | dump RSSI across a band (863 870 100)\n"
|
||||
"lora custom <MHz> <BW kHz> <SF> <CR 5-8> <sync hex> [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 <sentence without $ and checksum>\n"
|
||||
"crash the last crash: firmware, reason, task, backtrace\n"
|
||||
@@ -432,6 +434,18 @@ static void runCommand(String line) {
|
||||
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 sweep") && radioService) { // on [from MHz] [to MHz] [step kHz] | off | dump
|
||||
if (line == "lora sweep dump") radioService->printSweep(console);
|
||||
else if (line == "lora sweep off") {
|
||||
radioService->setSweepEcho(false);
|
||||
radioService->sweep(false);
|
||||
} else if (line.startsWith("lora sweep on")) {
|
||||
double from = 863, to = 870, step = 100;
|
||||
sscanf(line.c_str() + 13, "%lf %lf %lf", &from, &to, &step);
|
||||
radioService->setSweepEcho(true);
|
||||
radioService->sweep(true, from * 1e6 + 0.5, to * 1e6 + 0.5, step * 1e3 + 0.5);
|
||||
}
|
||||
}
|
||||
if (line == "lora capture start" && loraCapture) {
|
||||
std::string why = loraCapture->start(millis());
|
||||
console.printf("lora capture: %s\n", why.empty() ? loraCapture->path().c_str() : why.c_str());
|
||||
|
||||
@@ -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" : "");
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -53,6 +53,7 @@ void statusBar(Canvas& c, const StatusInfo& info) {
|
||||
switch (info.radio) { // Q101: muted while listening, bright for a moment on each packet
|
||||
case StatusInfo::Radio::Listening: right("L", kMuted); break;
|
||||
case StatusInfo::Radio::Packet: right("L", kAccent); break;
|
||||
case StatusInfo::Radio::Sweep: right("SW", kMuted); break; // the Sniffer is paused
|
||||
case StatusInfo::Radio::None: break;
|
||||
}
|
||||
switch (info.gnss) { // Q61: muted while searching, normal with a Fix, the count with a 3D Fix
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ struct StatusInfo {
|
||||
enum class Gnss { None, Searching, TwoD, ThreeD } gnss = Gnss::None; // None: GNSS off (Q61)
|
||||
int gnssSatellites = 0; // used in the Fix
|
||||
bool tracking = false; // a Track is recording (Q63)
|
||||
enum class Radio { None, Listening, Packet } radio = Radio::None; // M3, Q101: Packet flashes
|
||||
enum class Radio { None, Listening, Packet, Sweep } radio = Radio::None; // M3, Q101: Packet flashes
|
||||
bool capturing = false; // a LoRa Capture is recording (Q97)
|
||||
|
||||
bool operator==(const StatusInfo& o) const {
|
||||
|
||||
Reference in New Issue
Block a user