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,132 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "meshtastic_header.h"
|
||||
#include "meshtastic_presets.h"
|
||||
|
||||
using namespace roro::meshtastic;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
// The 16 bytes every Meshtastic packet starts with, little-endian, never encrypted.
|
||||
static const uint8_t kHeader[] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, // to: broadcast
|
||||
0x78, 0x56, 0x34, 0x12, // from: !12345678
|
||||
0xEF, 0xBE, 0xAD, 0xDE, // id
|
||||
0x6B, // flags: hop start 3, want ack, hop limit 3
|
||||
0x08, // channel hash: LongFast with the default key
|
||||
0x00, // next hop: none
|
||||
0x42, // relay node: last byte of the Node that relayed it
|
||||
};
|
||||
|
||||
void test_header_fields() {
|
||||
PacketHeader h;
|
||||
TEST_ASSERT_TRUE(parseHeader(kHeader, sizeof kHeader, h));
|
||||
TEST_ASSERT_EQUAL_HEX32(0xFFFFFFFF, h.to);
|
||||
TEST_ASSERT_EQUAL_HEX32(0x12345678, h.from);
|
||||
TEST_ASSERT_EQUAL_HEX32(0xDEADBEEF, h.id);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x08, h.channelHash);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x00, h.nextHop);
|
||||
TEST_ASSERT_EQUAL_UINT8(0x42, h.relayNode);
|
||||
TEST_ASSERT_TRUE(h.broadcast());
|
||||
}
|
||||
|
||||
void test_header_flags() {
|
||||
PacketHeader h;
|
||||
parseHeader(kHeader, sizeof kHeader, h);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, h.hopLimit());
|
||||
TEST_ASSERT_EQUAL_UINT8(3, h.hopStart());
|
||||
TEST_ASSERT_TRUE(h.wantAck());
|
||||
TEST_ASSERT_FALSE(h.viaMqtt());
|
||||
TEST_ASSERT_EQUAL_INT(0, h.hopsAway());
|
||||
|
||||
uint8_t relayed[16];
|
||||
std::memcpy(relayed, kHeader, 16);
|
||||
relayed[12] = 0xF1 | 0x10; // hop start 7, via MQTT, hop limit 1
|
||||
parseHeader(relayed, 16, h);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, h.hopLimit());
|
||||
TEST_ASSERT_EQUAL_UINT8(7, h.hopStart());
|
||||
TEST_ASSERT_TRUE(h.viaMqtt());
|
||||
TEST_ASSERT_FALSE(h.wantAck());
|
||||
TEST_ASSERT_EQUAL_INT(6, h.hopsAway());
|
||||
}
|
||||
|
||||
void test_header_from_old_firmware_has_no_hop_start() {
|
||||
uint8_t old[16];
|
||||
std::memcpy(old, kHeader, 16);
|
||||
old[12] = 0x02; // hop limit 2, hop start 0: firmware before 2.3 didn't set it
|
||||
PacketHeader h;
|
||||
parseHeader(old, 16, h);
|
||||
TEST_ASSERT_EQUAL_INT(-1, h.hopsAway()); // unknown, not "2 hops below zero"
|
||||
}
|
||||
|
||||
void test_header_too_short() {
|
||||
PacketHeader h;
|
||||
TEST_ASSERT_FALSE(parseHeader(kHeader, 15, h));
|
||||
TEST_ASSERT_FALSE(parseHeader(nullptr, 0, h));
|
||||
}
|
||||
|
||||
void test_node_ids() {
|
||||
TEST_ASSERT_EQUAL_STRING("!12345678", nodeId(0x12345678).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("!0000abcd", nodeId(0xABCD).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("all", nodeId(kBroadcast).c_str());
|
||||
}
|
||||
|
||||
// Meshtastic shows these hashes for the default key ("AQ==").
|
||||
void test_channel_hash_with_default_key() {
|
||||
TEST_ASSERT_EQUAL_UINT8(8, channelHash("LongFast", kDefaultKey, sizeof kDefaultKey));
|
||||
TEST_ASSERT_EQUAL_UINT8(31, channelHash("MediumFast", kDefaultKey, sizeof kDefaultKey));
|
||||
}
|
||||
|
||||
void test_djb2() {
|
||||
TEST_ASSERT_EQUAL_UINT32(5381, djb2(""));
|
||||
TEST_ASSERT_EQUAL_UINT32(130429955u, djb2("LongFast"));
|
||||
}
|
||||
|
||||
void test_eu868_presets() {
|
||||
TEST_ASSERT_EQUAL_size_t(7, kEu868PresetCount);
|
||||
const Preset& lf = kEu868Presets[0]; // the default comes first
|
||||
TEST_ASSERT_EQUAL_STRING("LongFast", lf.name);
|
||||
TEST_ASSERT_EQUAL_FLOAT(250.0f, lf.bwKHz);
|
||||
TEST_ASSERT_EQUAL_UINT8(11, lf.sf);
|
||||
TEST_ASSERT_EQUAL_UINT8(5, lf.cr);
|
||||
const Preset* ls = findPreset("LongSlow");
|
||||
TEST_ASSERT_NOT_NULL(ls);
|
||||
TEST_ASSERT_EQUAL_FLOAT(125.0f, ls->bwKHz);
|
||||
TEST_ASSERT_EQUAL_UINT8(12, ls->sf);
|
||||
TEST_ASSERT_EQUAL_UINT8(8, ls->cr);
|
||||
TEST_ASSERT_NULL(findPreset("ShortTurbo")); // 500 kHz doesn't fit the 250 kHz sub-band
|
||||
}
|
||||
|
||||
// The frequency slot comes from the channel name's hash; an unnamed channel uses the preset's name.
|
||||
void test_eu868_frequencies() {
|
||||
TEST_ASSERT_EQUAL_UINT32(869525000u, eu868FrequencyHz(*findPreset("LongFast")));
|
||||
TEST_ASSERT_EQUAL_UINT32(869525000u, eu868FrequencyHz(*findPreset("MediumFast")));
|
||||
TEST_ASSERT_EQUAL_UINT32(869462500u, eu868FrequencyHz(*findPreset("LongSlow")));
|
||||
TEST_ASSERT_EQUAL_UINT32(869587500u, eu868FrequencyHz(*findPreset("LongMod")));
|
||||
// A 250 kHz preset has one slot, whatever the channel's name.
|
||||
TEST_ASSERT_EQUAL_UINT32(869525000u, eu868FrequencyHz(*findPreset("LongFast"), "Belgium"));
|
||||
}
|
||||
|
||||
void test_radio_constants() {
|
||||
TEST_ASSERT_EQUAL_HEX8(0x2B, kSyncWord);
|
||||
TEST_ASSERT_EQUAL_UINT16(16, kPreambleLength);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_header_fields);
|
||||
RUN_TEST(test_header_flags);
|
||||
RUN_TEST(test_header_from_old_firmware_has_no_hop_start);
|
||||
RUN_TEST(test_header_too_short);
|
||||
RUN_TEST(test_node_ids);
|
||||
RUN_TEST(test_channel_hash_with_default_key);
|
||||
RUN_TEST(test_djb2);
|
||||
RUN_TEST(test_eu868_presets);
|
||||
RUN_TEST(test_eu868_frequencies);
|
||||
RUN_TEST(test_radio_constants);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user