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
|
||||
|
||||
Reference in New Issue
Block a user