LoRa Scanner: a MeshCore preset, and its packets read

- MeshCore joins the presets (EU/UK Narrow: 869.618 MHz, 62.5 kHz, SF8,
  CR 4/8, sync 0x12), after the Meshtastic ones so stored choices keep
  their number; `lora preset MeshCore` on the console
- lib/meshcore reads what MeshCore sends in clear: header, path, and a
  node's advert (name, kind, key, position)
- the list row, the details, a Capture's viewer and the console echo read
  a packet by the sync word it was received on
- the bandwidth is printed as 62.5, not 62

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-09 20:30:29 +02:00
co-authored by Claude Opus 5.5
parent 861a60b73d
commit 8ba41f72b6
23 changed files with 622 additions and 62 deletions
+3 -4
View File
@@ -264,10 +264,9 @@ void FileViewer::openPacket(int index) {
if (tap) {
std::snprintf(line, sizeof line, "%.0f dBm, SNR %.1f dB, noise %.0f", rx.rssi, rx.snr, rx.noiseFloor);
details_.push_back(line);
std::snprintf(line, sizeof line, "%.4f MHz, SF%u, %.0f kHz", rx.frequencyHz / 1e6, rx.spreadingFactor, rx.bandwidthKHz);
std::snprintf(line, sizeof line, "%.4f MHz, SF%u, %g kHz", rx.frequencyHz / 1e6, rx.spreadingFactor, rx.bandwidthKHz);
details_.push_back(line);
if (rx.syncWord == meshtastic::kSyncWord)
for (auto& l : lora::headerLines(body + skip, have - skip)) details_.push_back(l);
for (auto& l : lora::headerLines(body + skip, have - skip, lora::protocolOf(rx.syncWord))) details_.push_back(l);
}
for (auto& l : lora::hexDump(body + skip, have - skip)) details_.push_back(l);
if (have < r.length) details_.push_back("(the first bytes only)");
@@ -391,7 +390,7 @@ void FileViewer::refresh() {
size_t have = std::min<size_t>(r.length, n - files::kPcapRecordSize);
if (tap && files::parseLoraTap(body, have, rx)) {
shown_.push_back(lora::row({body + lora::kLoraTapSize, have - lora::kLoraTapSize, rx.rssi, rx.snr, true,
rx.syncWord == meshtastic::kSyncWord},
lora::protocolOf(rx.syncWord)},
clockTime(r.seconds)));
} else {
shown_.push_back(clockTime(r.seconds) + " " + std::to_string(r.length) + " bytes");
+17 -15
View File
@@ -7,7 +7,7 @@
#include <cstdio>
#include <ctime>
#include "meshtastic_presets.h"
#include "scan_presets.h"
#include "packet_view.h"
#include "sweep_view.h"
#include "ui/canvas.h"
@@ -21,8 +21,8 @@ constexpr uint32_t kMessageMs = 4000;
}
void LoraScannerApp::onEnter() {
int i = std::clamp<int>(settings_.getInt(Setting::LoraPreset), 0, meshtastic::kEu868PresetCount - 1);
const meshtastic::Preset& p = meshtastic::kEu868Presets[i];
int i = std::clamp<int>(settings_.getInt(Setting::LoraPreset), 0, lora::scanPresetCount() - 1);
lora::ScanPreset p = lora::scanPreset(i);
if (std::string(radio_.config().name) != p.name) radio_.setPreset(p);
radio_.listen(RadioService::App, true);
view_ = View::Packets;
@@ -48,7 +48,7 @@ void LoraScannerApp::showSweep(bool on) {
requestRedraw();
}
bool LoraScannerApp::meshtasticSettings() const { return radio_.config().syncWord == meshtastic::kSyncWord; }
lora::Protocol LoraScannerApp::protocol() const { return lora::protocolOf(radio_.config().syncWord); }
void LoraScannerApp::say(const std::string& text) {
message_ = text;
@@ -83,8 +83,8 @@ bool LoraScannerApp::onKey(const KeyEvent& e) {
else if (e.key == Key::Select) {
int i = presets_.selected();
settings_.setInt(Setting::LoraPreset, i);
radio_.setPreset(meshtastic::kEu868Presets[i]);
say(std::string("Listening on ") + meshtastic::kEu868Presets[i].name);
radio_.setPreset(lora::scanPreset(i));
say(std::string("Listening on ") + lora::scanPreset(i).name);
view_ = View::Packets;
} else if (e.key == Key::Back) view_ = View::Packets;
else return true;
@@ -104,7 +104,7 @@ bool LoraScannerApp::onKey(const KeyEvent& e) {
return true;
}
if (e.key == Key::Char && (e.ch == 'p' || e.ch == 'P')) {
presets_.setCount(meshtastic::kEu868PresetCount);
presets_.setCount(lora::scanPresetCount());
presets_.select(settings_.getInt(Setting::LoraPreset));
view_ = View::Presets;
requestRedraw();
@@ -133,8 +133,8 @@ void LoraScannerApp::openDetails() {
details_.push_back(line);
std::snprintf(line, sizeof line, "%.4f MHz, off by %.0f Hz", p.rx.frequencyHz / 1e6, p.frequencyError);
details_.push_back(line);
if (meshtasticSettings())
for (auto& l : lora::headerLines(p.data, p.len)) details_.push_back(l);
if (p.crcOk)
for (auto& l : lora::headerLines(p.data, p.len, lora::protocolOf(p.rx.syncWord))) details_.push_back(l);
for (auto& l : lora::hexDump(p.data, p.len)) details_.push_back(l);
detailsTop_ = 0;
view_ = View::Details;
@@ -238,10 +238,12 @@ void LoraScannerApp::drawPackets(Canvas& c) {
c.setFont(&fonts::body);
c.setTextColor(theme::kMuted);
c.drawString("No packets yet.", 4, list.y + 4);
c.drawString(meshtasticSettings() ? "Meshtastic nodes in range show here." : "Custom settings.", 4,
lora::Protocol heard = protocol();
c.drawString(heard == lora::Protocol::Meshtastic ? "Meshtastic nodes in range show here."
: heard == lora::Protocol::MeshCore ? "MeshCore nodes in range show here." : "Custom settings.", 4,
list.y + 4 + theme::kLineHeight);
} else {
bool mesh = meshtasticSettings();
lora::Protocol mesh = protocol();
widgets::list(c, packets_, list, [&](int i) {
RadioPacket p;
if (!radio_.packet(newest - i, p)) return std::string("(gone)");
@@ -265,14 +267,14 @@ void LoraScannerApp::drawPresets(Canvas& c) {
const auto& area = theme::kContent;
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
c.drawString("Meshtastic presets allowed in EU868", 4, area.y + 2);
c.drawString("Meshtastic in EU868, and MeshCore", 4, area.y + 2);
widgets::list(
c, presets_, {area.x, area.y + 12, area.w, (kListRows + 1) * theme::kLineHeight},
[](int i) { return std::string(meshtastic::kEu868Presets[i].name); },
[](int i) { return std::string(lora::scanPreset(i).name); },
[](int i) {
char v[24];
const auto& p = meshtastic::kEu868Presets[i];
std::snprintf(v, sizeof v, "SF%u %.0fk %.3f", p.sf, p.bwKHz, meshtastic::eu868FrequencyHz(p) / 1e6);
lora::ScanPreset p = lora::scanPreset(i);
std::snprintf(v, sizeof v, "SF%u %gk %.3f", p.sf, p.bwKHz, p.frequencyHz / 1e6);
return std::string(v);
});
}
+2 -2
View File
@@ -14,7 +14,7 @@
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
// with the Meshtastic or MeshCore 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). 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 {
@@ -40,7 +40,7 @@ class LoraScannerApp : public App {
void showSweep(bool on);
void openDetails();
std::string timeOf(const RadioPacket& p) const;
bool meshtasticSettings() const;
lora::Protocol protocol() const; // whose packets the radio is set for
void say(const std::string& text); // a line in the footer for a few seconds
RadioService& radio_;
+5 -4
View File
@@ -676,7 +676,7 @@ static const char* const kHelp =
"log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\n"
"ls [folder] | du <path> | mkdir <path> | rm [-r] [-f] <path> | cp [-f] <from> <to> | mv [-f] <from> <to> | cancel the SD card, with the Storage App's rules (rm -r for a folder; -f: the Shell doesn't ask; * and ? in a name: /notes/*.txt)\n"
"screenshot [seconds] the screen as a PNG in /screenshots on the card, now or after a pause\n"
"lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio, receive only\n"
"lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio, receive only; a Meshtastic preset (LongFast...) or MeshCore\n"
"lora capture start|stop a LoRa Capture to /captures/lora (pcap, LoRaTap)\n"
"lora sweep on [from MHz] [to MHz] [step kHz] | lora sweep off | lora sweep 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"
@@ -1133,9 +1133,10 @@ static void runCommand(String line, bool fromSerial = false) {
}
}
if (line.startsWith("lora preset ") && radioService) {
const meshtastic::Preset* p = meshtastic::findPreset(line.substring(12).c_str());
if (p) radioService->setPreset(*p);
console.printf("lora preset: %s\n", p ? p->name : "unknown (LongFast LongSlow MediumSlow MediumFast ShortSlow ShortFast LongMod)");
lora::ScanPreset p;
bool found = lora::findScanPreset(line.substring(12).c_str(), p);
if (found) radioService->setPreset(p);
console.printf("lora preset: %s\n", found ? p.name : "unknown (LongFast LongSlow MediumSlow MediumFast ShortSlow ShortFast LongMod MeshCore)");
}
if (line == "gnss quiet on" || line == "gnss quiet off") { // Settings > Pause GNSS for LoRa (issue #20)
settings.setBool(Setting::GnssQuietForLora, line.endsWith("on"));
+13 -8
View File
@@ -39,9 +39,8 @@ constexpr float kDeafBelow = -108.0f;
RadioService* RadioService::instance_ = nullptr;
RadioService::RadioService() : lock_(xSemaphoreCreateMutex()) {
const meshtastic::Preset& p = meshtastic::kEu868Presets[0]; // LongFast (Q95)
config_ = {meshtastic::eu868FrequencyHz(p), p.bwKHz, p.sf, p.cr, meshtastic::kSyncWord, meshtastic::kPreambleLength,
p.name};
lora::ScanPreset p = lora::scanPreset(0); // LongFast (Q95)
config_ = {p.frequencyHz, p.bwKHz, p.sf, p.cr, p.syncWord, p.preamble, p.name};
}
void RadioService::start() {
@@ -334,10 +333,9 @@ RadioService::Config RadioService::config() const {
return c;
}
void RadioService::setPreset(const meshtastic::Preset& p) {
void RadioService::setPreset(const lora::ScanPreset& p) {
xSemaphoreTake(lock_, portMAX_DELAY);
config_ = {meshtastic::eu868FrequencyHz(p), p.bwKHz, p.sf, p.cr, meshtastic::kSyncWord, meshtastic::kPreambleLength,
p.name};
config_ = {p.frequencyHz, p.bwKHz, p.sf, p.cr, p.syncWord, p.preamble, p.name};
xSemaphoreGive(lock_);
configChanged_ = true;
wake(kRequest);
@@ -374,7 +372,14 @@ void RadioService::tick(uint32_t nowMs) {
console.printf("lora: #%lu %u B %.1f dBm SNR %.1f dB%s", (unsigned long)p.seq, p.len, p.rx.rssi, p.rx.snr,
p.crcOk ? "" : " (bad CRC)");
meshtastic::PacketHeader h;
if (meshtastic::parseHeader(p.data, p.len, h))
lora::Protocol protocol = lora::protocolOf(p.rx.syncWord);
if (protocol == lora::Protocol::MeshCore && p.crcOk) { // what the Scanner's details show, on one line
const char* sep = " | ";
for (auto& l : lora::headerLines(p.data, p.len, protocol)) {
console.printf("%s%s", sep, l.c_str());
sep = "; ";
}
} else if (protocol == lora::Protocol::Meshtastic && meshtastic::parseHeader(p.data, p.len, h))
console.printf(" | %s > %s, ch 0x%02X, hops %d/%u%s", meshtastic::nodeId(h.from).c_str(),
meshtastic::nodeId(h.to).c_str(), h.channelHash, h.hopsAway(), h.hopStart(),
h.viaMqtt() ? ", via MQTT" : "");
@@ -400,7 +405,7 @@ void RadioService::printStatus(Print& out) const {
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",
out.printf("radio: %s, %s %.3f MHz, BW %g kHz, SF %u, CR 4/%u, sync 0x%02X, preamble %u\n",
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" : "",
+2 -2
View File
@@ -10,7 +10,7 @@
#include <memory>
#include "loratap.h"
#include "meshtastic_presets.h"
#include "scan_presets.h"
#include "service.h"
class SX1262;
@@ -74,7 +74,7 @@ class RadioService : public Service {
bool listening() const { return listening_; }
void listen(Client client, bool on);
Config config() const;
void setPreset(const meshtastic::Preset& preset);
void setPreset(const lora::ScanPreset& preset);
void setConfig(const Config& config); // custom settings (Q95), named "Custom"
uint32_t received() const { return seq_; } // seq of the newest packet