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
+74
View File
@@ -0,0 +1,74 @@
#include <unity.h>
#include <vector>
#include "sweep_view.h"
using namespace roro::lora;
void setUp() {}
void tearDown() {}
// 863.0 to 870.0 MHz in 100 kHz steps: 71 readings.
static std::vector<int8_t> band(int8_t floor) { return std::vector<int8_t>(71, floor); }
void test_floor_is_the_median() {
auto b = band(-95);
b[10] = -60; // a few strong steps don't move the median
b[11] = -62;
b[50] = -70;
SweepStats s = summarize(b.data(), b.size(), 863000000, 100000);
TEST_ASSERT_EQUAL_INT(-95, s.floor);
TEST_ASSERT_EQUAL_INT(-60, s.top);
}
void test_peaks_strongest_first_and_merged() {
auto b = band(-95);
b[11] = -70; // 864.1 MHz, with shoulders: one peak, not three
b[10] = -75;
b[12] = -78;
b[65] = -55; // 869.5 MHz
b[30] = -90; // only 5 dB above the floor: not a peak
SweepStats s = summarize(b.data(), b.size(), 863000000, 100000);
TEST_ASSERT_EQUAL_size_t(2, s.peaks.size());
TEST_ASSERT_EQUAL_UINT32(869500000u, s.peaks[0].hz);
TEST_ASSERT_EQUAL_INT(-55, s.peaks[0].dbm);
TEST_ASSERT_EQUAL_UINT32(864100000u, s.peaks[1].hz);
}
void test_at_most_three_peaks() {
auto b = band(-100);
for (int i = 5; i < 70; i += 10) b[i] = static_cast<int8_t>(-80 + i / 10);
SweepStats s = summarize(b.data(), b.size(), 863000000, 100000);
TEST_ASSERT_EQUAL_size_t(3, s.peaks.size());
TEST_ASSERT_EQUAL_INT(-74, s.peaks[0].dbm); // the strongest three
}
void test_flat_band_has_no_peaks() {
auto b = band(-88);
SweepStats s = summarize(b.data(), b.size(), 863000000, 100000);
TEST_ASSERT_EQUAL_size_t(0, s.peaks.size());
TEST_ASSERT_EQUAL_INT(-88, s.floor);
}
// The waterfall's scale: dark at the bottom, through blue and green to red at the top.
void test_heat_scale() {
TEST_ASSERT_EQUAL_UINT8(0, heatLevel(-130, -120, -60));
TEST_ASSERT_EQUAL_UINT8(0, heatLevel(-120, -120, -60));
TEST_ASSERT_EQUAL_UINT8(255, heatLevel(-60, -120, -60));
TEST_ASSERT_EQUAL_UINT8(255, heatLevel(-20, -120, -60));
TEST_ASSERT_EQUAL_UINT8(128, heatLevel(-90, -120, -60));
TEST_ASSERT_TRUE(heatColor(0) != heatColor(255));
TEST_ASSERT_EQUAL_HEX16(0x0000, heatColor(0)); // black
TEST_ASSERT_EQUAL_HEX16(0xF800, heatColor(255)); // red
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_floor_is_the_median);
RUN_TEST(test_peaks_strongest_first_and_merged);
RUN_TEST(test_at_most_three_peaks);
RUN_TEST(test_flat_band_has_no_peaks);
RUN_TEST(test_heat_scale);
return UNITY_END();
}