M3 step 2: Meshtastic header and presets, pcap with LoRaTap (host-tested)

The 16-byte clear header (hops away, channel hash, relay node), the EU_868
presets and their frequency slots, and the channel hash, all checked
against Meshtastic's source. Captures are pcap with LoRaTap v0, read back
with TShark 4.2.5; packet RSSI is plain dBm, as Wireshark reads it.

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 18:59:23 +02:00
co-authored by Claude Opus 5.5
parent fed065a6f9
commit 3242475699
9 changed files with 457 additions and 1 deletions
+52
View File
@@ -0,0 +1,52 @@
#include "loratap.h"
#include <algorithm>
#include <cmath>
namespace roro::lora {
static void le16(std::vector<uint8_t>& o, uint16_t v) { o.insert(o.end(), {uint8_t(v), uint8_t(v >> 8)}); }
static void le32(std::vector<uint8_t>& o, uint32_t v) {
o.insert(o.end(), {uint8_t(v), uint8_t(v >> 8), uint8_t(v >> 16), uint8_t(v >> 24)});
}
// LoRaTap's dBm encoding: -139 dBm plus the byte, clamped to what a byte holds.
static uint8_t dbmByte(float dbm) {
long v = std::lround(dbm + 139);
return static_cast<uint8_t>(std::clamp(v, 0L, 255L));
}
void appendPcapHeader(std::vector<uint8_t>& out) {
le32(out, 0xA1B2C3D4);
le16(out, 2);
le16(out, 4);
le32(out, 0); // time zone
le32(out, 0); // timestamp accuracy
le32(out, 65535); // snap length
le32(out, 270); // LINKTYPE_LORATAP
}
void appendRecord(std::vector<uint8_t>& out, uint32_t seconds, uint32_t micros, const RxInfo& rx, const uint8_t* data,
size_t len) {
uint32_t captured = static_cast<uint32_t>(kLoraTapSize + len);
le32(out, seconds);
le32(out, micros);
le32(out, captured);
le32(out, captured);
uint32_t f = rx.frequencyHz;
// The spec puts packet RSSI in quarter dB below 0 dB SNR (an SX127x formula), but Wireshark
// reads it as -139 dBm plus the byte either way, and the SX1262 already gives dBm.
uint8_t packetRssi = dbmByte(rx.rssi);
long snr = std::clamp(std::lround(rx.snr * 4), -128L, 127L);
out.insert(out.end(), {
0, 0, 0, uint8_t(kLoraTapSize), // version 0, padding, length
uint8_t(f >> 24), uint8_t(f >> 16), uint8_t(f >> 8), uint8_t(f),
uint8_t(std::lround(rx.bandwidthKHz / 125)), rx.spreadingFactor,
packetRssi, dbmByte(rx.rssi), dbmByte(rx.noiseFloor),
static_cast<uint8_t>(static_cast<int8_t>(snr)), rx.syncWord,
});
if (len) out.insert(out.end(), data, data + len);
}
} // namespace roro::lora
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace roro::lora {
// What the radio knew about one received packet.
struct RxInfo {
uint32_t frequencyHz = 0;
float bandwidthKHz = 0;
uint8_t spreadingFactor = 0;
float rssi = 0; // dBm
float snr = 0; // dB
float noiseFloor = 0; // dBm, instantaneous RSSI just before the packet, when known
uint8_t syncWord = 0;
};
// A Capture file (M3, Q97): pcap with LoRaTap v0 headers (LINKTYPE_LORATAP, 270), which Wireshark
// reads. pcap fields are little-endian, LoRaTap fields big-endian.
void appendPcapHeader(std::vector<uint8_t>& out);
void appendRecord(std::vector<uint8_t>& out, uint32_t seconds, uint32_t micros, const RxInfo& rx, const uint8_t* data,
size_t len);
constexpr size_t kLoraTapSize = 15;
constexpr size_t kRecordOverhead = 16 + kLoraTapSize;
} // namespace roro::lora