Public Access
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:
@@ -0,0 +1,28 @@
|
||||
#include "meshtastic_header.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace roro::meshtastic {
|
||||
|
||||
static uint32_t le32(const uint8_t* p) { return p[0] | p[1] << 8 | p[2] << 16 | static_cast<uint32_t>(p[3]) << 24; }
|
||||
|
||||
bool parseHeader(const uint8_t* data, size_t len, PacketHeader& out) {
|
||||
if (!data || len < kHeaderSize) return false;
|
||||
out.to = le32(data);
|
||||
out.from = le32(data + 4);
|
||||
out.id = le32(data + 8);
|
||||
out.flags = data[12];
|
||||
out.channelHash = data[13];
|
||||
out.nextHop = data[14];
|
||||
out.relayNode = data[15];
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string nodeId(uint32_t node) {
|
||||
if (node == kBroadcast) return "all";
|
||||
char s[10];
|
||||
std::snprintf(s, sizeof s, "!%08x", static_cast<unsigned>(node));
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace roro::meshtastic
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace roro::meshtastic {
|
||||
|
||||
constexpr uint32_t kBroadcast = 0xFFFFFFFF;
|
||||
|
||||
// The 16 bytes every Meshtastic packet starts with, sent in clear (little-endian). The payload
|
||||
// after it is encrypted with the Channel's key.
|
||||
struct PacketHeader {
|
||||
uint32_t to = 0, from = 0, id = 0;
|
||||
uint8_t flags = 0;
|
||||
uint8_t channelHash = 0; // the Channel's name and key folded into a byte (see channelHash())
|
||||
uint8_t nextHop = 0; // last byte of the Node meant to relay it next; 0 when flooding
|
||||
uint8_t relayNode = 0; // last byte of the Node that relayed it to us
|
||||
|
||||
uint8_t hopLimit() const { return flags & 0x07; }
|
||||
bool wantAck() const { return flags & 0x08; }
|
||||
bool viaMqtt() const { return flags & 0x10; }
|
||||
uint8_t hopStart() const { return flags >> 5; }
|
||||
bool broadcast() const { return to == kBroadcast; }
|
||||
// How many times it was relayed before we heard it; -1 when the sender didn't say (hop start 0).
|
||||
int hopsAway() const { return hopStart() == 0 ? -1 : hopStart() - hopLimit(); }
|
||||
};
|
||||
|
||||
constexpr size_t kHeaderSize = 16;
|
||||
|
||||
bool parseHeader(const uint8_t* data, size_t len, PacketHeader& out);
|
||||
|
||||
// "!12345678", as Meshtastic writes node numbers; "all" for broadcast.
|
||||
std::string nodeId(uint32_t node);
|
||||
|
||||
} // namespace roro::meshtastic
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "meshtastic_presets.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace roro::meshtastic {
|
||||
|
||||
const uint8_t kDefaultKey[16] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59,
|
||||
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01};
|
||||
|
||||
// firmware src/mesh/MeshRadio.h (modemPresetToParams) and RadioInterface.cpp (PRESETS_EU_868).
|
||||
const Preset kEu868Presets[] = {
|
||||
{"LongFast", 250, 11, 5}, {"LongSlow", 125, 12, 8}, {"MediumSlow", 250, 10, 5}, {"MediumFast", 250, 9, 5},
|
||||
{"ShortSlow", 250, 8, 5}, {"ShortFast", 250, 7, 5}, {"LongMod", 125, 11, 8},
|
||||
};
|
||||
const size_t kEu868PresetCount = sizeof kEu868Presets / sizeof kEu868Presets[0];
|
||||
|
||||
const Preset* findPreset(const char* name) {
|
||||
for (const Preset& p : kEu868Presets)
|
||||
if (std::strcmp(p.name, name) == 0) return &p;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t djb2(const char* s) {
|
||||
uint32_t h = 5381;
|
||||
for (; *s; ++s) h = (h << 5) + h + static_cast<unsigned char>(*s);
|
||||
return h;
|
||||
}
|
||||
|
||||
uint8_t channelHash(const char* name, const uint8_t* key, size_t keyLen) {
|
||||
uint8_t h = 0;
|
||||
for (; *name; ++name) h ^= static_cast<uint8_t>(*name);
|
||||
for (size_t i = 0; i < keyLen; ++i) h ^= key[i];
|
||||
return h;
|
||||
}
|
||||
|
||||
uint32_t eu868FrequencyHz(const Preset& preset, const char* channelName) {
|
||||
constexpr double kStartMHz = 869.4, kEndMHz = 869.65;
|
||||
double slotMHz = preset.bwKHz / 1000.0;
|
||||
uint32_t slots = static_cast<uint32_t>(std::lround((kEndMHz - kStartMHz) / slotMHz));
|
||||
const char* name = channelName && *channelName ? channelName : preset.name;
|
||||
uint32_t slot = slots ? djb2(name) % slots : 0;
|
||||
double mhz = kStartMHz + slotMHz / 2 + slot * slotMHz;
|
||||
return static_cast<uint32_t>(std::lround(mhz * 1e6));
|
||||
}
|
||||
|
||||
} // namespace roro::meshtastic
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace roro::meshtastic {
|
||||
|
||||
// Radio settings shared by every Meshtastic preset (firmware src/mesh/RadioInterface.h).
|
||||
constexpr uint8_t kSyncWord = 0x2B;
|
||||
constexpr uint16_t kPreambleLength = 16;
|
||||
|
||||
// The default Channel key ("AQ==", expanded): public, so the default Channel is readable by anyone.
|
||||
extern const uint8_t kDefaultKey[16];
|
||||
|
||||
// A modem preset: bandwidth, spreading factor and coding rate (4/cr). Named as Meshtastic shows them.
|
||||
struct Preset {
|
||||
const char* name;
|
||||
float bwKHz;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
};
|
||||
|
||||
// The presets Meshtastic allows in EU_868, its order, LongFast (the default) first.
|
||||
extern const Preset kEu868Presets[];
|
||||
extern const size_t kEu868PresetCount;
|
||||
const Preset* findPreset(const char* name); // nullptr when EU_868 doesn't allow it
|
||||
|
||||
// djb2, as Meshtastic hashes a Channel's name to pick a frequency slot.
|
||||
uint32_t djb2(const char* s);
|
||||
|
||||
// The byte in every packet header naming its Channel: the name's bytes XORed with the key's.
|
||||
uint8_t channelHash(const char* name, const uint8_t* key, size_t keyLen);
|
||||
|
||||
// EU_868 is 869.4 to 869.65 MHz: the slot comes from the Channel's name (the preset's name for
|
||||
// an unnamed Channel, which is the default).
|
||||
uint32_t eu868FrequencyHz(const Preset& preset, const char* channelName = nullptr);
|
||||
|
||||
} // namespace roro::meshtastic
|
||||
Reference in New Issue
Block a user