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:
@@ -1,6 +1,6 @@
|
||||
# M3 — Radio bring-up: the LoRa Scanner
|
||||
|
||||
**Status:** in progress (branch `m3`): step 1 done.
|
||||
**Status:** in progress (branch `m3`): steps 1 and 2 done.
|
||||
|
||||
**Goal:** the LoRa radio on the Cap works, receive only: a Radio Service owns it and shares the SPI bus with the SD card safely, and a LoRa Scanner App shows what's on the air, either packets (Sniffer) or energy across the band (Sweep). Nothing in M3 can transmit. The mesh comes on top of this in M4 (receive) and M5 (transmit).
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- **The expander's P0 connects the antenna; it's required.** At power-on P0 is an input (direction 0x00, high-impedance 0xFF), and the receiver reads a flat **-111.9 dBm** at 869.525 MHz, BW 250 kHz: the chip's own floor, deaf. With P0 driven high, the noise floor is **-87 to -94 dBm**: the antenna hearing the room. So the Radio Service drives P0 high at boot (Q91).
|
||||
- **DIO2 doesn't change reception** (within ±2 dB over three runs, P0 high). It likely selects TX versus RX in the FM8625H; it stays the RF switch, as in Meshtastic.
|
||||
- **The noise floor at the desk is high** (-87 to -94 dBm, varying run to run), about 25 dB above thermal noise for 250 kHz. Something nearby is loud, possibly the Cardputer itself or the PC; Sweep (step 5) should show where it sits.
|
||||
- **Step 2:** presets, frequencies and the channel hash are checked against Meshtastic's source (`MeshRadio.h`, `RadioInterface.cpp`): LongFast and the default key give hash 8, MediumFast 31, as Meshtastic shows. Captures were checked with TShark 4.2.5: every LoRaTap field reads back. Wireshark ignores the spec's quarter-dB packet RSSI below 0 dB SNR, so packet RSSI is plain dBm.
|
||||
- **Cost:** RadioLib 7.8.1 and the probe add 23.6 KB of flash and 656 bytes of static RAM to the release firmware (1,679,843 bytes of 3,342,336).
|
||||
|
||||
## Decisions (design round 2026-10-05)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,93 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "loratap.h"
|
||||
|
||||
using namespace roro::lora;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static uint32_t le32(const std::vector<uint8_t>& b, size_t at) {
|
||||
return b[at] | b[at + 1] << 8 | b[at + 2] << 16 | (uint32_t)b[at + 3] << 24;
|
||||
}
|
||||
|
||||
void test_pcap_global_header() {
|
||||
std::vector<uint8_t> out;
|
||||
appendPcapHeader(out);
|
||||
TEST_ASSERT_EQUAL_size_t(24, out.size());
|
||||
TEST_ASSERT_EQUAL_HEX32(0xA1B2C3D4, le32(out, 0)); // microsecond timestamps, little-endian file
|
||||
TEST_ASSERT_EQUAL_UINT8(2, out[4]); // version 2.4
|
||||
TEST_ASSERT_EQUAL_UINT8(4, out[6]);
|
||||
TEST_ASSERT_EQUAL_UINT32(65535, le32(out, 16)); // snap length
|
||||
TEST_ASSERT_EQUAL_UINT32(270, le32(out, 20)); // LINKTYPE_LORATAP
|
||||
}
|
||||
|
||||
void test_record_with_loratap_v0() {
|
||||
RxInfo rx;
|
||||
rx.frequencyHz = 869525000;
|
||||
rx.bandwidthKHz = 250;
|
||||
rx.spreadingFactor = 11;
|
||||
rx.rssi = -97.0f;
|
||||
rx.snr = 6.25f;
|
||||
rx.noiseFloor = -110.0f;
|
||||
rx.syncWord = 0x2B;
|
||||
const uint8_t payload[] = {1, 2, 3};
|
||||
std::vector<uint8_t> out;
|
||||
appendRecord(out, 1791230400u, 250000u, rx, payload, sizeof payload);
|
||||
|
||||
TEST_ASSERT_EQUAL_size_t(16 + 15 + 3, out.size());
|
||||
TEST_ASSERT_EQUAL_UINT32(1791230400u, le32(out, 0));
|
||||
TEST_ASSERT_EQUAL_UINT32(250000u, le32(out, 4));
|
||||
TEST_ASSERT_EQUAL_UINT32(18, le32(out, 8)); // captured: LoRaTap header + payload
|
||||
TEST_ASSERT_EQUAL_UINT32(18, le32(out, 12));
|
||||
|
||||
const uint8_t* t = out.data() + 16; // LoRaTap v0, big-endian
|
||||
TEST_ASSERT_EQUAL_UINT8(0, t[0]); // version
|
||||
TEST_ASSERT_EQUAL_UINT8(0, t[1]); // padding
|
||||
TEST_ASSERT_EQUAL_UINT16(15, t[2] << 8 | t[3]);
|
||||
TEST_ASSERT_EQUAL_UINT32(869525000u, (uint32_t)t[4] << 24 | t[5] << 16 | t[6] << 8 | t[7]);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, t[8]); // bandwidth in 125 kHz steps
|
||||
TEST_ASSERT_EQUAL_UINT8(11, t[9]);
|
||||
TEST_ASSERT_EQUAL_UINT8(42, t[10]); // packet RSSI: -139 + 42 = -97 dBm (SNR >= 0)
|
||||
TEST_ASSERT_EQUAL_UINT8(42, t[11]); // max RSSI: the packet's
|
||||
TEST_ASSERT_EQUAL_UINT8(29, t[12]); // current RSSI: the noise floor, -139 + 29 = -110 dBm
|
||||
TEST_ASSERT_EQUAL_UINT8(25, t[13]); // SNR in quarter dB
|
||||
TEST_ASSERT_EQUAL_HEX8(0x2B, t[14]);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, t[17]);
|
||||
}
|
||||
|
||||
// Below 0 dB SNR the spec says quarter dB, but Wireshark (checked with tshark 4.2) reads plain dBm.
|
||||
void test_negative_snr() {
|
||||
RxInfo rx;
|
||||
rx.bandwidthKHz = 125;
|
||||
rx.rssi = -120.5f;
|
||||
rx.snr = -7.5f;
|
||||
std::vector<uint8_t> out;
|
||||
appendRecord(out, 0, 0, rx, nullptr, 0);
|
||||
const uint8_t* t = out.data() + 16;
|
||||
TEST_ASSERT_EQUAL_UINT8(1, t[8]);
|
||||
TEST_ASSERT_EQUAL_UINT8(19, t[10]); // -139 + 19 = -120 dBm (rounded half away)
|
||||
TEST_ASSERT_EQUAL_INT8(-30, static_cast<int8_t>(t[13])); // -7.5 dB
|
||||
}
|
||||
|
||||
void test_rssi_clamped() {
|
||||
RxInfo rx;
|
||||
rx.rssi = -150.0f; // below what LoRaTap can say
|
||||
rx.snr = 1.0f;
|
||||
rx.noiseFloor = 20.0f;
|
||||
std::vector<uint8_t> out;
|
||||
appendRecord(out, 0, 0, rx, nullptr, 0);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, out[16 + 10]);
|
||||
TEST_ASSERT_EQUAL_UINT8(159, out[16 + 12]);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_pcap_global_header);
|
||||
RUN_TEST(test_record_with_loratap_v0);
|
||||
RUN_TEST(test_negative_snr);
|
||||
RUN_TEST(test_rssi_clamped);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -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