mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-27 08:46:23 -04:00
The TX queue was arrival-ordered, and extended advertising is set-and-repeat: a frame that leaves first holds the radio for a whole burst. So a position update queued ahead of an ack went out first and the ack waited behind it, which is exactly backwards for the one a sender is timing out waiting on. It is now a bag rather than a ring. runOnce picks the highest-priority slot and closes the gap; equal priorities still leave oldest-first, because priority is meant to order the queue, not to reorder within itself. A full queue makes room only for something strictly more important, the same trade MeshPacketQueue::replaceLowerPriorityPacket makes for LoRa, and the slot it displaces is the newest of the least important, so a frame that has already waited its turn is not the one thrown away. Refusals are counted in txDroppedQueueFull, kept separate from txDroppedTooLarge: one says the bearer cannot carry this packet at all, the other that it could not carry it right now. The slot carries the priority because the air copy no longer does - strippedForAir drops it, being local scheduling the receiver overwrites.
652 lines
24 KiB
C++
652 lines
24 KiB
C++
#include "DebugConfiguration.h"
|
|
#include "TestUtil.h"
|
|
#include <unity.h>
|
|
|
|
#if defined(ARCH_PORTDUINO) && HAS_BLE_MESH
|
|
|
|
#include "mesh/BLEMeshHandler.h"
|
|
#include "mesh/NodeDB.h"
|
|
#include "mesh/Router.h"
|
|
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
|
|
/**
|
|
* A BLEMeshHandler with the radio replaced by a record of what it was asked to send.
|
|
*
|
|
* Everything worth testing here is platform-independent - the advertisement the transport builds
|
|
* and the guards it applies to what it receives - so the platform hooks only need to be observable,
|
|
* not real.
|
|
*/
|
|
class FakeBLEMesh : public BLEMeshHandler
|
|
{
|
|
public:
|
|
std::vector<std::vector<uint8_t>> sent;
|
|
std::vector<meshtastic_MeshPacket> received;
|
|
bool ready = true;
|
|
bool advertising = false;
|
|
|
|
void start() override { isRunning = true; }
|
|
void stop() override { isRunning = false; }
|
|
|
|
// Exposed so tests can drive ingress without a BLE stack.
|
|
void feed(const uint8_t *data, size_t len, int8_t rssi) { deliverToRouter(data, len, rssi); }
|
|
bool cancel(meshtastic_MeshPacket_TransportMechanism medium, NodeNum from, PacketId id)
|
|
{
|
|
return onCancelSending(medium, from, id);
|
|
}
|
|
uint8_t build(const meshtastic_MeshPacket *mp, uint8_t *out, size_t cap) { return buildAdvPayload(mp, out, cap); }
|
|
int32_t pump() { return runOnce(); }
|
|
|
|
void enqueueReceived(meshtastic_MeshPacket *p) override
|
|
{
|
|
received.push_back(*p);
|
|
packetPool.release(p);
|
|
}
|
|
|
|
protected:
|
|
bool platformBeginAdvertising(const uint8_t *adv, size_t len) override
|
|
{
|
|
sent.emplace_back(adv, adv + len);
|
|
advertising = true;
|
|
return true;
|
|
}
|
|
bool platformAdvertisingActive() override { return advertising; }
|
|
void platformEndAdvertising() override { advertising = false; }
|
|
bool platformReady() override { return ready; }
|
|
};
|
|
|
|
/// A packet in the state Router::send hands to a transport: encrypted, with a real sender.
|
|
meshtastic_MeshPacket encryptedPacket(uint32_t from = 0x3061b02e, uint32_t id = 0x04050b6e, size_t payload = 32)
|
|
{
|
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
|
p.from = from;
|
|
p.to = NODENUM_BROADCAST;
|
|
p.id = id;
|
|
p.channel = 50;
|
|
p.hop_limit = 3;
|
|
p.hop_start = 3;
|
|
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
|
p.encrypted.size = payload;
|
|
for (size_t i = 0; i < payload; i++)
|
|
p.encrypted.bytes[i] = (uint8_t)i;
|
|
return p;
|
|
}
|
|
|
|
/// A packet at a chosen priority, for the queue-order tests.
|
|
meshtastic_MeshPacket packetAt(meshtastic_MeshPacket_Priority priority, uint32_t id)
|
|
{
|
|
meshtastic_MeshPacket p = encryptedPacket(0x3061b02e, id);
|
|
p.priority = priority;
|
|
return p;
|
|
}
|
|
|
|
/// What Router::send actually hands a transport, as opposed to the minimal fixture above.
|
|
///
|
|
/// fixPriority() runs before encryption and never leaves priority UNSET (Router.cpp:562), and
|
|
/// FloodingRouter::send stamps relay_node on everything we send (FloodingRouter.cpp:22). A relay
|
|
/// carries transport_mechanism and the reception metadata it was received with. Measuring the
|
|
/// ceiling against the minimal fixture measures the fixture, not the bearer.
|
|
meshtastic_MeshPacket productionPacket(size_t payload = 32, bool relayed = false)
|
|
{
|
|
meshtastic_MeshPacket p = encryptedPacket(0x3061b02e, 0x04050b6e, payload);
|
|
p.priority = meshtastic_MeshPacket_Priority_DEFAULT;
|
|
p.relay_node = 0x2e;
|
|
if (relayed) {
|
|
p.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
|
p.rx_rssi = -113;
|
|
p.has_rx_rssi = true;
|
|
p.rx_snr = -7.25f;
|
|
p.rx_time = 1757808000;
|
|
p.has_rx_time = true;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/// Largest ciphertext a MeshPacket can carry, i.e. one guaranteed not to fit an advertisement.
|
|
constexpr size_t MAX_ENCRYPTED_FOR_TEST = sizeof(meshtastic_MeshPacket().encrypted.bytes);
|
|
|
|
/// Encode `mp` the way the transport does, so ingress tests have a real advertisement body.
|
|
size_t encodeForAir(const meshtastic_MeshPacket &mp, uint8_t *out, size_t cap)
|
|
{
|
|
return pb_encode_to_bytes(out, cap, &meshtastic_MeshPacket_msg, &mp);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void test_advertisement_carries_the_packet(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket();
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
uint8_t len = h.build(&p, adv, sizeof(adv));
|
|
|
|
TEST_ASSERT_TRUE_MESSAGE(len > BLE_MESH_ADV_OVERHEAD, "built an advertisement");
|
|
// Flags AD, then manufacturer-specific data with our company ID and protocol version.
|
|
TEST_ASSERT_EQUAL_UINT8(2, adv[0]);
|
|
TEST_ASSERT_EQUAL_UINT8(0x01, adv[1]);
|
|
TEST_ASSERT_EQUAL_UINT8(0xFF, adv[4]);
|
|
TEST_ASSERT_EQUAL_UINT8(BLE_MESH_COMPANY_ID & 0xFF, adv[5]);
|
|
TEST_ASSERT_EQUAL_UINT8((BLE_MESH_COMPANY_ID >> 8) & 0xFF, adv[6]);
|
|
TEST_ASSERT_EQUAL_UINT8(BLE_MESH_PROTOCOL_VERSION, adv[7]);
|
|
// The AD length byte counts everything after itself.
|
|
TEST_ASSERT_EQUAL_UINT8(len - 4, adv[3]);
|
|
}
|
|
|
|
void test_refuses_an_unencrypted_packet(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket();
|
|
p.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
// Router::send encrypts before any transport sees a packet, so plaintext here is a bug
|
|
// upstream - putting it on air would leak the message.
|
|
TEST_ASSERT_EQUAL_UINT8(0, h.build(&p, adv, sizeof(adv)));
|
|
}
|
|
|
|
void test_refuses_a_packet_with_no_sender(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket(0 /* from */);
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
TEST_ASSERT_EQUAL_UINT8(0, h.build(&p, adv, sizeof(adv)));
|
|
}
|
|
|
|
void test_drops_a_packet_too_large_for_one_advertisement(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
// A single unfragmented extended advertisement holds 251 bytes and we never chain, so the
|
|
// largest packets cannot ride BLE. They still go out over LoRa - Router::send has already
|
|
// handed them to the radio by the time we refuse.
|
|
auto p = encryptedPacket(0x3061b02e, 0x04050b6e, MAX_ENCRYPTED_FOR_TEST);
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
TEST_ASSERT_EQUAL_UINT8(0, h.build(&p, adv, sizeof(adv)));
|
|
}
|
|
|
|
/// The largest ciphertext that still fits one advertisement, found rather than assumed.
|
|
///
|
|
/// The budget is BLE_MESH_MAX_PROTO_LEN for the *whole* encoded MeshPacket, so the answer is that
|
|
/// minus whatever envelope the packet happens to carry - which is why it is measured per packet
|
|
/// shape rather than written down once.
|
|
size_t largestCiphertextThatFits(meshtastic_MeshPacket shape)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
|
|
size_t lo = 0, hi = MAX_ENCRYPTED_FOR_TEST;
|
|
while (lo < hi) {
|
|
size_t mid = lo + (hi - lo + 1) / 2;
|
|
shape.encrypted.size = mid;
|
|
if (h.build(&shape, adv, sizeof(adv)) > 0)
|
|
lo = mid;
|
|
else
|
|
hi = mid - 1;
|
|
}
|
|
return lo;
|
|
}
|
|
|
|
void test_the_advertisement_ceiling_is_below_the_lora_ceiling(void)
|
|
{
|
|
const size_t fits = largestCiphertextThatFits(productionPacket());
|
|
|
|
// The bearer is not a full bearer and never has been: one unfragmented extended advertisement
|
|
// cannot hold what one LoRa frame holds, and nothing fragments - the nRF52 SoftDevice caps both
|
|
// the advertising data and the scan buffer at 255, so chaining is not available in either
|
|
// direction. Everything above this rides LoRa only, counted by txDroppedTooLarge.
|
|
// 216, not the 219 the minimal fixture reaches: relay_node costs 3 (field 19, so a two-byte
|
|
// tag). priority costs nothing because strippedForAir drops it, which is also why node-kmp's
|
|
// BleAdvertCeilingTest measures 214 for the same packet - it has no equivalent strip yet.
|
|
TEST_ASSERT_EQUAL_size_t(216, fits);
|
|
TEST_ASSERT_LESS_THAN_size_t_MESSAGE(MAX_RADIO_PAYLOAD_LEN, fits, "BLE carries less than LoRa");
|
|
}
|
|
|
|
void test_relaying_no_longer_costs_budget(void)
|
|
{
|
|
// Before the egress strip a relay reached 21 bytes less far than the originator did, because
|
|
// the packet went out carrying the rx_rssi, rx_snr and rx_time it arrived with. It now reaches
|
|
// exactly as far: the receiver overwrites all three, so they were never worth sending.
|
|
TEST_ASSERT_EQUAL_size_t(largestCiphertextThatFits(productionPacket()),
|
|
largestCiphertextThatFits(productionPacket(32, true)));
|
|
}
|
|
|
|
void test_the_air_copy_drops_everything_the_receiver_overwrites(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = productionPacket(32, true);
|
|
p.via_mqtt = true;
|
|
p.tx_after = 4242;
|
|
p.pki_encrypted = true;
|
|
p.public_key.size = 32;
|
|
for (size_t i = 0; i < 32; i++)
|
|
p.public_key.bytes[i] = (uint8_t)(0xA0 + i);
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t len = h.build(&p, adv, sizeof(adv));
|
|
TEST_ASSERT_TRUE(len > BLE_MESH_ADV_OVERHEAD);
|
|
|
|
meshtastic_MeshPacket air = meshtastic_MeshPacket_init_zero;
|
|
TEST_ASSERT_TRUE(
|
|
pb_decode_from_bytes(adv + BLE_MESH_ADV_OVERHEAD, len - BLE_MESH_ADV_OVERHEAD, &meshtastic_MeshPacket_msg, &air));
|
|
|
|
// Exactly the set deliverToRouter rewrites, plus rx_time which Router::handleReceived stamps.
|
|
// A sender that omits them loses nothing and stops publishing its own link quality; one that
|
|
// sends them is paying for bytes the far side discards.
|
|
TEST_ASSERT_EQUAL(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL, air.transport_mechanism);
|
|
TEST_ASSERT_FALSE(air.via_mqtt);
|
|
TEST_ASSERT_EQUAL_UINT32(0, air.tx_after);
|
|
TEST_ASSERT_EQUAL(meshtastic_MeshPacket_Priority_UNSET, air.priority);
|
|
TEST_ASSERT_FALSE(air.pki_encrypted);
|
|
TEST_ASSERT_EQUAL_UINT16(0, air.public_key.size);
|
|
TEST_ASSERT_FALSE(air.has_rx_rssi);
|
|
TEST_ASSERT_FALSE(air.has_rx_time);
|
|
TEST_ASSERT_EQUAL_FLOAT(0.0f, air.rx_snr);
|
|
|
|
// And nothing routing needs went with them.
|
|
TEST_ASSERT_EQUAL_UINT32(p.from, air.from);
|
|
TEST_ASSERT_EQUAL_UINT32(p.to, air.to);
|
|
TEST_ASSERT_EQUAL_UINT32(p.id, air.id);
|
|
TEST_ASSERT_EQUAL_UINT8(p.hop_limit, air.hop_limit);
|
|
TEST_ASSERT_EQUAL_UINT8(p.hop_start, air.hop_start);
|
|
TEST_ASSERT_EQUAL_UINT8(p.relay_node, air.relay_node);
|
|
TEST_ASSERT_EQUAL_UINT32(p.channel, air.channel);
|
|
TEST_ASSERT_EQUAL_UINT16(p.encrypted.size, air.encrypted.size);
|
|
}
|
|
|
|
void test_an_oversized_packet_is_counted_not_just_logged(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket(0x3061b02e, 0x04050b6e, MAX_ENCRYPTED_FOR_TEST);
|
|
|
|
TEST_ASSERT_FALSE(h.onSend(&p));
|
|
TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, h.txDroppedTooLarge, "the loss is countable");
|
|
}
|
|
|
|
void test_an_urgent_frame_overtakes_one_already_queued(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto background = packetAt(meshtastic_MeshPacket_Priority_BACKGROUND, 0x11111111);
|
|
auto ack = packetAt(meshtastic_MeshPacket_Priority_ACK, 0x22222222);
|
|
TEST_ASSERT_TRUE(h.onSend(&background));
|
|
TEST_ASSERT_TRUE(h.onSend(&ack));
|
|
|
|
// Extended advertising is set-and-repeat, so whatever leaves first holds the radio for a whole
|
|
// burst. Arrival order would put a position update ahead of an ack that a sender is timing out
|
|
// waiting for.
|
|
uint8_t expected[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t len = h.build(&ack, expected, sizeof(expected));
|
|
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_MESSAGE(1, h.sent.size(), "one frame on air");
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, h.sent[0].data(), len);
|
|
}
|
|
|
|
void test_equal_priorities_leave_in_arrival_order(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto first = packetAt(meshtastic_MeshPacket_Priority_DEFAULT, 0x11111111);
|
|
auto second = packetAt(meshtastic_MeshPacket_Priority_DEFAULT, 0x22222222);
|
|
TEST_ASSERT_TRUE(h.onSend(&first));
|
|
TEST_ASSERT_TRUE(h.onSend(&second));
|
|
|
|
uint8_t expected[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t len = h.build(&first, expected, sizeof(expected));
|
|
|
|
// Priority orders the queue; it does not reorder within a priority. Without this a burst of
|
|
// same-priority traffic would leave in an order nothing defines.
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, h.sent[0].data(), len);
|
|
}
|
|
|
|
void test_a_full_queue_makes_room_only_for_something_more_important(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
for (size_t i = 0; i < BLE_MESH_TX_QUEUE_SIZE; i++) {
|
|
auto p = packetAt(meshtastic_MeshPacket_Priority_BACKGROUND, (uint32_t)(0x1000 + i));
|
|
TEST_ASSERT_TRUE(h.onSend(&p));
|
|
}
|
|
|
|
// Full of the least important work there is, so an ack displaces one.
|
|
auto ack = packetAt(meshtastic_MeshPacket_Priority_ACK, 0x22222222);
|
|
TEST_ASSERT_TRUE_MESSAGE(h.onSend(&ack), "an ack gets in");
|
|
TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, h.txDroppedQueueFull, "and the displacement is counted");
|
|
|
|
// Now full again, and a second frame of the same priority as the rest has nothing to displace:
|
|
// shuffling equals would only change which packet is lost.
|
|
auto peer = packetAt(meshtastic_MeshPacket_Priority_BACKGROUND, 0x33333333);
|
|
TEST_ASSERT_FALSE_MESSAGE(h.onSend(&peer), "an equal is refused");
|
|
TEST_ASSERT_EQUAL_UINT32(2, h.txDroppedQueueFull);
|
|
|
|
uint8_t expected[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t len = h.build(&ack, expected, sizeof(expected));
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, h.sent[0].data(), len);
|
|
}
|
|
|
|
void test_the_frame_displaced_is_the_newest_of_the_least_important(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto oldest = packetAt(meshtastic_MeshPacket_Priority_BACKGROUND, 0x11111111);
|
|
TEST_ASSERT_TRUE(h.onSend(&oldest));
|
|
for (size_t i = 1; i < BLE_MESH_TX_QUEUE_SIZE; i++) {
|
|
auto p = packetAt(meshtastic_MeshPacket_Priority_BACKGROUND, (uint32_t)(0x2000 + i));
|
|
TEST_ASSERT_TRUE(h.onSend(&p));
|
|
}
|
|
auto ack = packetAt(meshtastic_MeshPacket_Priority_ACK, 0x33333333);
|
|
TEST_ASSERT_TRUE(h.onSend(&ack));
|
|
|
|
// A frame that has already waited its turn is not the one to throw away, so the displaced slot
|
|
// is the newest of the least important rather than the first one found.
|
|
uint8_t expectedAck[BLE_MESH_ADV_TOTAL_MAX];
|
|
uint8_t expectedOldest[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t ackLen = h.build(&ack, expectedAck, sizeof(expectedAck));
|
|
const uint8_t oldestLen = h.build(&oldest, expectedOldest, sizeof(expectedOldest));
|
|
|
|
h.pump();
|
|
h.advertising = false;
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_MESSAGE(2, h.sent.size(), "two frames went out");
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedAck, h.sent[0].data(), ackLen);
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedOldest, h.sent[1].data(), oldestLen);
|
|
}
|
|
|
|
void test_a_dupe_heard_on_ble_cancels_our_queued_copy(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket(0x3061b02e, 0x04050b6e);
|
|
TEST_ASSERT_TRUE(h.onSend(&p));
|
|
|
|
// A neighbour relayed it before we got to. One advertisement reaches every neighbour at once,
|
|
// so their copy has already done our work.
|
|
TEST_ASSERT_TRUE(h.cancel(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADV, p.from, p.id));
|
|
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, h.sent.size(), "nothing went out");
|
|
}
|
|
|
|
void test_a_dupe_heard_on_lora_leaves_the_ble_queue_alone(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket(0x3061b02e, 0x04050b6e);
|
|
TEST_ASSERT_TRUE(h.onSend(&p));
|
|
|
|
// Hearing a LoRa neighbour relay this says nothing about whether our BLE neighbours have it.
|
|
// Cancelling here would silently thin the BLE flood every time the two meshes overlap.
|
|
TEST_ASSERT_FALSE(h.cancel(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA, p.from, p.id));
|
|
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_MESSAGE(1, h.sent.size(), "still advertised");
|
|
}
|
|
|
|
void test_canceling_keeps_the_other_queued_frames_in_order(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto first = encryptedPacket(0x3061b02e, 0x11111111);
|
|
auto doomed = encryptedPacket(0x3061b02e, 0x22222222);
|
|
auto last = encryptedPacket(0x3061b02e, 0x33333333);
|
|
TEST_ASSERT_TRUE(h.onSend(&first));
|
|
TEST_ASSERT_TRUE(h.onSend(&doomed));
|
|
TEST_ASSERT_TRUE(h.onSend(&last));
|
|
|
|
TEST_ASSERT_TRUE(h.cancel(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADV, doomed.from, doomed.id));
|
|
|
|
// Compacting the ring must not drop or reorder its neighbours - the frames either side are
|
|
// unrelated packets that still have to go out, in the order they were queued.
|
|
uint8_t expectedFirst[BLE_MESH_ADV_TOTAL_MAX];
|
|
uint8_t expectedLast[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t firstLen = h.build(&first, expectedFirst, sizeof(expectedFirst));
|
|
const uint8_t lastLen = h.build(&last, expectedLast, sizeof(expectedLast));
|
|
|
|
h.pump();
|
|
h.advertising = false;
|
|
h.pump();
|
|
h.advertising = false;
|
|
h.pump();
|
|
|
|
TEST_ASSERT_EQUAL_MESSAGE(2, h.sent.size(), "two frames survived");
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedFirst, h.sent[0].data(), firstLen);
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedLast, h.sent[1].data(), lastLen);
|
|
}
|
|
|
|
void test_canceling_cuts_a_burst_already_on_air(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket(0x3061b02e, 0x04050b6e);
|
|
TEST_ASSERT_TRUE(h.onSend(&p));
|
|
h.pump();
|
|
TEST_ASSERT_TRUE_MESSAGE(h.advertising, "on air");
|
|
|
|
// The payload repeats for BLE_MESH_ADV_EVENTS events, so the copies still to come are exactly
|
|
// what the overhear says are unnecessary. runOnce pops before it advertises, so this frame is
|
|
// no longer in the ring and only the live-burst identity can find it.
|
|
TEST_ASSERT_TRUE(h.cancel(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADV, p.from, p.id));
|
|
TEST_ASSERT_FALSE_MESSAGE(h.advertising, "burst ended");
|
|
|
|
// And the state machine is not left half-advanced: the next pump finds an empty ring and idles
|
|
// rather than re-ending a burst that is already over.
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_MESSAGE(1, h.sent.size(), "nothing re-sent");
|
|
}
|
|
|
|
void test_send_queues_rather_than_transmitting(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket();
|
|
|
|
TEST_ASSERT_TRUE(h.onSend(&p));
|
|
// onSend is reached from Router::send on the main task. An implementation that advertised
|
|
// inline would stall the router - and so LoRa timing and the whole main loop - for the length
|
|
// of every burst.
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, h.sent.size(), "nothing on air yet");
|
|
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_MESSAGE(1, h.sent.size(), "the pump transmits it");
|
|
}
|
|
|
|
void test_tx_queue_is_bounded(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
|
|
size_t accepted = 0;
|
|
for (size_t i = 0; i < BLE_MESH_TX_QUEUE_SIZE * 3; i++) {
|
|
auto p = encryptedPacket(0x3061b02e, (uint32_t)(0x1000 + i));
|
|
if (h.onSend(&p))
|
|
accepted++;
|
|
}
|
|
// A full ring drops rather than growing without bound or overwriting an unsent frame.
|
|
TEST_ASSERT_EQUAL(BLE_MESH_TX_QUEUE_SIZE, accepted);
|
|
}
|
|
|
|
void test_a_relayed_packet_is_re_advertised(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket();
|
|
p.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADV;
|
|
|
|
// A packet already marked as BLE-sourced is what a *rebroadcast* looks like:
|
|
// perhapsRebroadcast allocCopy()s the received packet and nothing on the TX path rewrites
|
|
// transport_mechanism. Refusing it caps the mesh at a single hop - two nodes can talk and a
|
|
// three-node chain cannot form.
|
|
TEST_ASSERT_TRUE_MESSAGE(h.onSend(&p), "relay must not be refused");
|
|
}
|
|
|
|
void test_ingress_accepts_a_well_formed_frame(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket();
|
|
|
|
uint8_t body[meshtastic_MeshPacket_size];
|
|
size_t n = encodeForAir(p, body, sizeof(body));
|
|
TEST_ASSERT_TRUE(n > 0);
|
|
|
|
h.feed(body, n, -42);
|
|
|
|
TEST_ASSERT_EQUAL_MESSAGE(1, h.received.size(), "delivered to the router");
|
|
const auto &got = h.received[0];
|
|
TEST_ASSERT_EQUAL_UINT32(0x3061b02e, got.from);
|
|
// Stamped so the router - and anything downstream - can tell how it arrived.
|
|
TEST_ASSERT_EQUAL(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADV, got.transport_mechanism);
|
|
// Unlike UDP there IS a real measurement of this hop, so it is reported rather than cleared.
|
|
TEST_ASSERT_TRUE(got.has_rx_rssi);
|
|
TEST_ASSERT_EQUAL_INT(-42, got.rx_rssi);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, got.rx_snr, "no SNR exists for a BLE arrival");
|
|
}
|
|
|
|
void test_ingress_drops_a_frame_with_no_sender(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket(0 /* from */);
|
|
|
|
uint8_t body[meshtastic_MeshPacket_size];
|
|
size_t n = encodeForAir(p, body, sizeof(body));
|
|
|
|
// Nothing legitimate advertises from=0, and a packet with no sender can reach remote admin
|
|
// without authorisation - the LoRa path refuses it for the same reason.
|
|
h.feed(body, n, -50);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, h.received.size(), "spoofed origin rejected");
|
|
}
|
|
|
|
void test_ingress_drops_an_impossible_hop_count(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket();
|
|
p.hop_limit = HOP_MAX + 1;
|
|
|
|
uint8_t body[meshtastic_MeshPacket_size];
|
|
size_t n = encodeForAir(p, body, sizeof(body));
|
|
|
|
// An out-of-range hop count is not relayable; UdpMulticastHandler drops it identically.
|
|
h.feed(body, n, -50);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, h.received.size(), "invalid hop count rejected");
|
|
}
|
|
|
|
void test_ingress_clears_pki_metadata(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket();
|
|
// A sender must not be able to assert its own packet was PKI-authenticated: that flag is
|
|
// local state the Router sets after a successful decrypt, never something off the wire.
|
|
p.pki_encrypted = true;
|
|
p.public_key.size = 32;
|
|
|
|
uint8_t body[meshtastic_MeshPacket_size];
|
|
size_t n = encodeForAir(p, body, sizeof(body));
|
|
|
|
h.feed(body, n, -50);
|
|
TEST_ASSERT_EQUAL(1, h.received.size());
|
|
TEST_ASSERT_FALSE_MESSAGE(h.received[0].pki_encrypted, "claimed authentication stripped");
|
|
TEST_ASSERT_EQUAL(0, h.received[0].public_key.size);
|
|
}
|
|
|
|
void test_ingress_ignores_our_own_advertisement(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket(nodeDB->getNodeNum());
|
|
|
|
uint8_t body[meshtastic_MeshPacket_size];
|
|
size_t n = encodeForAir(p, body, sizeof(body));
|
|
|
|
// Our own advertisement echoing back into our own scanner would loop.
|
|
h.feed(body, n, -50);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, h.received.size(), "self-echo dropped");
|
|
}
|
|
|
|
void test_pump_waits_for_the_platform(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
h.ready = false;
|
|
auto p = encryptedPacket();
|
|
TEST_ASSERT_TRUE(h.onSend(&p));
|
|
|
|
h.pump();
|
|
// Readiness is polled rather than pushed: the BLE stack comes up before main() constructs the
|
|
// handler about half the time, so a one-shot "ready" callback is a race that loses silently.
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, h.sent.size(), "nothing transmitted before the stack is up");
|
|
|
|
h.ready = true;
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_MESSAGE(1, h.sent.size(), "transmits once ready");
|
|
}
|
|
|
|
void setUp(void) {}
|
|
void tearDown(void) {}
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
// deliverToRouter consults nodeDB to recognise - and drop - our own advertisement echoing back
|
|
// into our own scanner, so the ingress tests need a real one.
|
|
if (!nodeDB)
|
|
nodeDB = new NodeDB();
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_advertisement_carries_the_packet);
|
|
RUN_TEST(test_refuses_an_unencrypted_packet);
|
|
RUN_TEST(test_refuses_a_packet_with_no_sender);
|
|
RUN_TEST(test_drops_a_packet_too_large_for_one_advertisement);
|
|
RUN_TEST(test_the_advertisement_ceiling_is_below_the_lora_ceiling);
|
|
RUN_TEST(test_relaying_no_longer_costs_budget);
|
|
RUN_TEST(test_the_air_copy_drops_everything_the_receiver_overwrites);
|
|
RUN_TEST(test_an_oversized_packet_is_counted_not_just_logged);
|
|
RUN_TEST(test_a_dupe_heard_on_ble_cancels_our_queued_copy);
|
|
RUN_TEST(test_a_dupe_heard_on_lora_leaves_the_ble_queue_alone);
|
|
RUN_TEST(test_canceling_keeps_the_other_queued_frames_in_order);
|
|
RUN_TEST(test_canceling_cuts_a_burst_already_on_air);
|
|
RUN_TEST(test_send_queues_rather_than_transmitting);
|
|
RUN_TEST(test_tx_queue_is_bounded);
|
|
RUN_TEST(test_an_urgent_frame_overtakes_one_already_queued);
|
|
RUN_TEST(test_equal_priorities_leave_in_arrival_order);
|
|
RUN_TEST(test_a_full_queue_makes_room_only_for_something_more_important);
|
|
RUN_TEST(test_the_frame_displaced_is_the_newest_of_the_least_important);
|
|
RUN_TEST(test_a_relayed_packet_is_re_advertised);
|
|
RUN_TEST(test_ingress_accepts_a_well_formed_frame);
|
|
RUN_TEST(test_ingress_drops_a_frame_with_no_sender);
|
|
RUN_TEST(test_ingress_drops_an_impossible_hop_count);
|
|
RUN_TEST(test_ingress_clears_pki_metadata);
|
|
RUN_TEST(test_ingress_ignores_our_own_advertisement);
|
|
RUN_TEST(test_pump_waits_for_the_platform);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
#else
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
LOG_WARN("BLE mesh tests require ARCH_PORTDUINO with HAS_BLE_MESH");
|
|
UNITY_BEGIN();
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
#endif // ARCH_PORTDUINO && HAS_BLE_MESH
|
|
|
|
void loop() {}
|