nRF52 dial: probe a quiet dialled link, dial only the overflow bit, time the connect out, decline encryption

Four things the bench showed a dialled link needs before it can share a room
with a dual-role phone:

- A liveness probe. The central keeps the ACL alive by itself, so a peer whose
  app died looks exactly like a quiet one and the single central slot is lost
  until reboot. A dialled link quiet for 30 s is sent the greeting as a write
  request (the client treats a repeated HELLO as a no-op); one unanswered
  request is retried after 2 s, two in a row shed the link. The request is
  issued directly and its response read through the mesh's event tap, because
  Bluefruit's write_resp() gives the peer 100 ms and a backgrounded iPhone on a
  long connection interval answers every time but later than that. The quiet
  window runs from the link being listed, not from the epoch, so a fresh link
  is not probed while its MTU exchange still holds the GATTC procedure.
- Only the overflow bit is dialled. An advertiser showing the mesh-peer UUID
  alone is Android or another radio, and both reach this node by themselves;
  dialling one spent the slot on the phone that was about to connect anyway,
  then redialled its rotated address every 60 s, each attempt stopping the
  mesh scan. The UUID dial stays behind BLE_GATT_MESH_DIAL_UUID for the bench.
  With that, every dial failure is a race with a link the peer is still
  tearing down, so the cooldown is one length again.
- The connect times out at 4 s. Bluefruit dials with its scanner's parameters,
  whose timeout is 0: a peer that never accepts left the dial pending forever.
- A security request on a dialled link is declined. A phone bonded to this
  node's phone API asks for encryption the moment it is dialled; Bluefruit
  answers nothing on a central link, the phone's SMP timer runs out at 30 s
  and it drops the link with 0x05. The mesh-peer link is open by design.

Bench, 2026-09-19, RAK4631: a backgrounded, peripheral-only iPad dialled
within 12 s of boot and held through probe after probe; killing its app shed
nothing, because iOS kept the service and restored the app on the next write.
A peripheral-only Pixel dialled the same way was shed 10 s after its app was
force-stopped. The dual-role Pixel advertising the UUID beside them was never
dialled. Native suite: 36 pass, five of them the probe. The declined security
request is untested - no bonded phone was on the bench.
This commit is contained in:
James Rich committed 2026-09-19 17:04:12 -05:00
1 parent b6e1370715
commit 86bddc0bd1
6 files changed
+314 -13

No files matched your search

+50 -5
View File
@@ -104,8 +104,10 @@ int32_t BLEGattMeshHandler::runOnce()
if (!isRunning || !platformReady())
return 500;
pumpRx(Time::getMillis());
pumpGreet();
const uint32_t now = Time::getMillis();
pumpRx(now);
pumpGreet(now);
pumpLiveness(now);
return pumpTx() ? 10 : 100;
}
@@ -159,26 +161,67 @@ BLEGattMeshHandler::Greeting *BLEGattMeshHandler::greeting(BLEGattPeerId peer, b
g.peer = peer;
g.sent = false;
g.heard = false;
g.heardMs = 0;
g.probedMs = 0;
g.probeMisses = 0;
return &g;
}
return nullptr;
}
void BLEGattMeshHandler::pumpGreet()
void BLEGattMeshHandler::pumpGreet(uint32_t nowMs)
{
std::array<BLEGattMeshPeer, BLE_GATT_MESH_MAX_PEERS> all{};
const size_t n = platformPeers(all.data(), all.size());
uint8_t hello[BLE_GATT_MESH_HELLO_SIZE];
const size_t len = buildHello(hello, sizeof(hello));
for (size_t i = 0; i < n; i++) {
Greeting *g = greeting(all[i].id, true);
if (!g || g->sent)
Greeting *g = greeting(all[i].id, false);
if (!g) {
// First sight of this link: the quiet window runs from here, not from the epoch.
g = greeting(all[i].id, true);
if (!g)
continue;
g->heardMs = nowMs;
}
if (g->sent)
continue;
if (platformNotify(all[i].id, hello, len))
g->sent = true; // a refusal is the stack busy; the next pump offers it again
}
}
void BLEGattMeshHandler::pumpLiveness(uint32_t nowMs)
{
std::array<BLEGattMeshPeer, BLE_GATT_MESH_MAX_PEERS> all{};
const size_t n = platformPeers(all.data(), all.size());
for (size_t i = 0; i < n; i++) {
if (!all[i].outbound)
continue;
Greeting *g = greeting(all[i].id, false); // pumpGreet listed it first, with the clock started
if (!g)
continue;
// Quiet for less than the idle window: nothing to ask. Asked already: wait the window, or the
// short retry after a miss.
if (nowMs - g->heardMs < BLE_GATT_MESH_PROBE_IDLE_MS)
continue;
const uint32_t wait = g->probeMisses ? BLE_GATT_MESH_PROBE_RETRY_MS : BLE_GATT_MESH_PROBE_IDLE_MS;
if (g->probedMs != 0 && nowMs - g->probedMs < wait)
continue;
g->probedMs = nowMs;
if (platformProbe(all[i].id)) {
g->probeMisses = 0;
continue;
}
if (++g->probeMisses < 2)
continue;
// The disconnect arrives later as a zero-length chunk and forgets the peer then; until it does
// the link is still listed, and probedMs keeps this from asking it again every pump.
LOG_WARN("BLE GATT mesh: dialled conn %u answers nothing; shedding it", all[i].id);
platformShedOutbound(all[i].id);
}
}
void BLEGattMeshHandler::handleHello(BLEGattPeerId peer, const uint8_t *id)
{
Greeting *g = greeting(peer, true);
@@ -280,6 +323,8 @@ void BLEGattMeshHandler::handleChunk(BLEGattPeerId peer, const uint8_t *chunk, s
forgetPeer(peer);
return;
}
if (Greeting *g = greeting(peer, true))
g->heardMs = nowMs;
uint8_t id[BLE_GATT_MESH_LINK_ID_SIZE];
if (parseHello(chunk, len, id)) {
+32 -4
View File
@@ -48,6 +48,18 @@
// pump's 10 ms tick, bounded so a departed peer fails the send instead of stalling the ring.
#ifndef BLE_GATT_MESH_TX_ATTEMPTS
#define BLE_GATT_MESH_TX_ATTEMPTS 50
// A dialled link that has carried nothing for this long is probed with a write the peer must answer.
// The central keeps the ACL alive by itself, so a peer whose app died looks exactly like a quiet one
// until something is asked of it - and the one central slot is lost until reboot otherwise.
#ifndef BLE_GATT_MESH_PROBE_IDLE_MS
#define BLE_GATT_MESH_PROBE_IDLE_MS 30000
#endif
// One unanswered probe may be the stack refusing a second GATTC procedure (an MTU exchange in flight,
// a write queue full); the retry after this long is what tells that apart from a peer that is gone.
#ifndef BLE_GATT_MESH_PROBE_RETRY_MS
#define BLE_GATT_MESH_PROBE_RETRY_MS 2000
#endif
#endif
// (from, id) -> arrival peer, so a relay is never written back to the peer that delivered it.
@@ -67,7 +79,8 @@ typedef uint16_t BLEGattPeerId;
struct BLEGattMeshPeer {
BLEGattPeerId id;
uint16_t chunk; // negotiated ATT MTU - 3
uint16_t chunk; // negotiated ATT MTU - 3
bool outbound = false; // this node dialled it, so nothing but this node will ever notice it die
};
/**
@@ -134,6 +147,21 @@ class BLEGattMeshHandler : private concurrency::OSThread, public MeshTransportBa
/// One node named itself on two links and the election went against this node: drop `peer` if it is
/// a link this node dialled. An inbound link is the peer's to shed, so the default keeps everything.
virtual void platformShedOutbound(BLEGattPeerId peer) { (void)peer; }
/// This node's link id, drawn on first use, and the greeting that carries it - the platform's probe
/// sends the same frame, so both live here rather than below.
void ensureLinkId();
size_t buildHello(uint8_t *out, size_t cap);
/// Greet every listed link once, and start its quiet window; then probe the dialled ones that have
/// gone quiet. A test drives both with its own clock.
void pumpGreet(uint32_t nowMs);
void pumpLiveness(uint32_t nowMs);
/// Ask `peer` for proof of life - a write it must acknowledge. False means it is gone. A platform
/// that cannot ask says true, and never sheds a link for silence.
virtual bool platformProbe(BLEGattPeerId peer)
{
(void)peer;
return true;
}
int32_t runOnce() override;
@@ -195,12 +223,12 @@ class BLEGattMeshHandler : private concurrency::OSThread, public MeshTransportBa
bool sent;
bool heard;
uint8_t id[BLE_GATT_MESH_LINK_ID_SIZE];
uint32_t heardMs; // last chunk of any kind from this peer, or when the link was first listed
uint32_t probedMs; // last proof-of-life asked of it
uint8_t probeMisses; // unanswered probes in a row; two is gone, one may be a busy stack
};
std::array<Greeting, BLE_GATT_MESH_MAX_PEERS * 2> greetings{};
Greeting *greeting(BLEGattPeerId peer, bool create);
void ensureLinkId();
size_t buildHello(uint8_t *out, size_t cap);
void pumpGreet();
void handleHello(BLEGattPeerId peer, const uint8_t *id);
BLEGattPeerId arrivalPeer(NodeNum from, PacketId id) const;
// The peer a relay of this packet must skip, which is none when the arrival came straight from
+110 -4
View File
@@ -155,14 +155,33 @@ BLEClientCharacteristic meshClientCharacteristic = BLEClientCharacteristic(BLEUu
bool dialing = false;
ble_gap_addr_t dialAddr{};
// A peer that dropped, refused or never answered is not redialled for a while: the scanner reports it
// again within 100 ms.
// again within 100 ms. One length for every outcome: a dial that fails is a race with a link the peer
// is still tearing down (a reflash, a relaunch), and a minute outlives that. The failure that would
// have earned a longer wait - a phone that already holds a link to this node redialled under the
// address it rotated to - cannot happen now that only the overflow bit is dialled, because the one
// central slot is that link.
#ifndef BLE_GATT_MESH_DIAL_COOLDOWN_MS
#define BLE_GATT_MESH_DIAL_COOLDOWN_MS 60000
#endif
// How long a dial may wait for the peer to accept the connection. Bluefruit's own default is forever.
#ifndef BLE_GATT_MESH_DIAL_TIMEOUT_MS
#define BLE_GATT_MESH_DIAL_TIMEOUT_MS 4000
#endif
bool cooldownArmed = false;
ble_gap_addr_t cooldownAddr{};
uint32_t cooldownSinceMs = 0;
// The proof-of-life write request in flight on the dialled link, and how it ended. Bluefruit's own
// write_resp() gives the peer 100 ms, which a backgrounded iPhone on a long connection interval
// misses while still answering every time; this waits a few intervals and reads the response event
// through the mesh's event tap instead.
#ifndef BLE_GATT_MESH_PROBE_WAIT_MS
#define BLE_GATT_MESH_PROBE_WAIT_MS 1500
#endif
enum ProbeState : uint8_t { PROBE_IDLE, PROBE_PENDING, PROBE_ANSWERED, PROBE_REFUSED };
volatile uint16_t probeConn = BLE_CONN_HANDLE_INVALID;
volatile uint8_t probeState = PROBE_IDLE;
void armCooldown()
{
cooldownAddr = dialAddr;
@@ -261,6 +280,9 @@ void NRF52BLEGattMesh::setupService()
meshClientCharacteristic.setNotifyCallback(onNotify);
Bluefruit.Central.setConnectCallback(onCentralConnect);
Bluefruit.Central.setDisconnectCallback(onCentralDisconnect);
// Bluefruit dials with its scanner's parameters, whose timeout is 0: a peer that never accepts
// leaves the dial pending forever and this node never dials again.
Bluefruit.Scanner.getParams()->timeout = BLE_GATT_MESH_DIAL_TIMEOUT_MS / 10;
}
#endif
{
@@ -344,10 +366,19 @@ void NRF52BLEGattMesh::onScanReport(const ble_gap_evt_adv_report_t *report)
if (cooldownArmed && memcmp(&cooldownAddr, &report->peer_addr, sizeof(cooldownAddr)) == 0 &&
Throttle::isWithinTimespanMs(cooldownSinceMs, BLE_GATT_MESH_DIAL_COOLDOWN_MS))
return;
// Only the primary advertisement is seen: the mesh scan is passive, so a node that puts the UUID in
// its scan response (this firmware on nRF52) is never dialled. Phones put it in the advertisement.
// Only the primary advertisement is seen: the mesh scan is passive, so a node that puts the UUID in
// its scan response (this firmware on nRF52) is never dialled. Phones put it in the advertisement.
// The dial is for the peer that cannot dial: an iOS app in the background, which always carries the
// overflow bit (in the foreground too). An advertiser showing the UUID alone is Android or another
// radio, and both reach this node by themselves - dialling one of those spent the single central
// slot on the phone that was about to connect anyway, then redialled its rotated address for ever.
#if BLE_GATT_MESH_DIAL_UUID
if (!Bluefruit.Scanner.checkReportForUuid(report, BLEUuid(serviceUuid)) && !reportHasIosOverflowBit(report))
return;
#else
if (!reportHasIosOverflowBit(report))
return;
#endif
dialing = true;
dialAddr = report->peer_addr;
if (!Bluefruit.Central.connect(report)) {
@@ -434,6 +465,7 @@ size_t NRF52BLEGattMesh::platformPeers(BLEGattMeshPeer *out, size_t cap)
break;
out[n].id = l.conn;
out[n].chunk = chunkFor(l.conn);
out[n].outbound = l.outbound;
n++;
}
return n;
@@ -463,6 +495,80 @@ bool NRF52BLEGattMesh::platformNotify(BLEGattPeerId peer, const uint8_t *data, s
return meshPeerCharacteristic.notify(peer, data, (uint16_t)len);
}
bool NRF52BLEGattMesh::platformProbe(BLEGattPeerId peer)
{
#if BLE_GATT_MESH_DIAL
bool outbound = false;
{
concurrency::LockGuard guard(&lock);
Link *l = findLink(peer);
outbound = l && l->outbound;
}
if (!outbound)
return true;
if (!Bluefruit.Central.connected(peer))
return false;
// A write with response: the peer's ATT layer must answer, and a peer whose app died has no
// handle left to answer for. The greeting is idempotent on the client, so it is the probe.
uint8_t hello[BLE_GATT_MESH_HELLO_SIZE];
const size_t len = buildHello(hello, sizeof(hello));
ble_gattc_write_params_t params = {
.write_op = BLE_GATT_OP_WRITE_REQ,
.flags = 0,
.handle = meshClientCharacteristic.valueHandle(),
.offset = 0,
.len = (uint16_t)len,
.p_value = hello,
};
probeConn = peer;
probeState = PROBE_PENDING;
if (sd_ble_gattc_write(peer, &params) != NRF_SUCCESS) {
probeState = PROBE_IDLE; // the stack is busy with another procedure; the retry will ask again
return false;
}
// This runs on the main task; the response lands on the BLE task, and delay() yields to it.
const uint32_t started = millis();
while (probeState == PROBE_PENDING && Bluefruit.Central.connected(peer) &&
Throttle::isWithinTimespanMs(started, BLE_GATT_MESH_PROBE_WAIT_MS))
delay(10);
const bool answered = probeState == PROBE_ANSWERED;
probeState = PROBE_IDLE;
probeConn = BLE_CONN_HANDLE_INVALID;
return answered;
#else
(void)peer;
return true;
#endif
}
void NRF52BLEGattMesh::onWriteResponse(uint16_t conn, uint16_t status)
{
#if BLE_GATT_MESH_DIAL
if (conn == probeConn && probeState == PROBE_PENDING)
probeState = status == BLE_GATT_STATUS_SUCCESS ? PROBE_ANSWERED : PROBE_REFUSED;
#else
(void)conn;
(void)status;
#endif
}
void NRF52BLEGattMesh::onSecurityRequest(uint16_t conn)
{
#if BLE_GATT_MESH_DIAL
// A phone bonded to this node's phone API asks the link be encrypted the moment it is dialled.
// Bluefruit answers nothing on a central link, the phone's SMP timer runs out at 30 s and it drops
// the link (0x05). The mesh-peer link is open by design - the channel key is the security - so
// decline, which the SoftDevice does for a NULL parameter set.
BLEConnection *c = Bluefruit.Connection(conn);
if (!c || c->getRole() != BLE_GAP_ROLE_CENTRAL)
return;
const uint32_t err = sd_ble_gap_authenticate(conn, NULL);
LOG_INFO("BLE GATT mesh: declined the security request on dialled conn %u (0x%x)", conn, (unsigned)err);
#else
(void)conn;
#endif
}
void NRF52BLEGattMesh::platformShedOutbound(BLEGattPeerId peer)
{
#if BLE_GATT_MESH_DIAL
@@ -474,7 +580,7 @@ void NRF52BLEGattMesh::platformShedOutbound(BLEGattPeerId peer)
}
if (!outbound)
return;
LOG_INFO("BLE GATT mesh: shedding dialled conn %u, the peer reaches us already", peer);
LOG_INFO("BLE GATT mesh: shedding dialled conn %u", peer);
Bluefruit.disconnect(peer); // onCentralDisconnect arms the cooldown
#else
(void)peer;
+5
View File
@@ -39,6 +39,10 @@ class NRF52BLEGattMesh : public BLEGattMeshHandler
static void onScanReport(const ble_gap_evt_adv_report_t *report);
/// The dial never completed (BLE_GAP_EVT_TIMEOUT, source CONN).
static void onDialTimeout();
/// A peer on a dialled link asked for encryption. Declined - see the definition.
static void onSecurityRequest(uint16_t conn);
/// A write request on some link was answered, or refused with an ATT error.
static void onWriteResponse(uint16_t conn, uint16_t status);
protected:
bool platformReady() override;
@@ -46,6 +50,7 @@ class NRF52BLEGattMesh : public BLEGattMeshHandler
bool platformNotify(BLEGattPeerId peer, const uint8_t *data, size_t len) override;
bool platformPollInbound(BLEGattPeerId &peer, uint8_t *buf, size_t cap, size_t &len) override;
void platformShedOutbound(BLEGattPeerId peer) override;
bool platformProbe(BLEGattPeerId peer) override;
};
#endif // HAS_BLE_GATT_MESH && ARCH_NRF52
+10
View File
@@ -233,6 +233,16 @@ void NRF52BLEMesh::onBleEvent(ble_evt_t *event)
// A dial stops the scan; scanning and a central link coexist once it is up, or gone.
instance->startScanning();
break;
case BLE_GATTC_EVT_WRITE_RSP:
#if HAS_BLE_GATT_MESH
NRF52BLEGattMesh::onWriteResponse(event->evt.gattc_evt.conn_handle, event->evt.gattc_evt.gatt_status);
#endif
break;
case BLE_GAP_EVT_SEC_REQUEST:
#if HAS_BLE_GATT_MESH
NRF52BLEGattMesh::onSecurityRequest(event->evt.gap_evt.conn_handle);
#endif
break;
case BLE_GAP_EVT_TIMEOUT:
if (event->evt.gap_evt.params.timeout.src == BLE_GAP_TIMEOUT_SRC_SCAN) {
instance->startScanning();
+107
View File
@@ -8,6 +8,7 @@
#include "mesh/NodeDB.h"
#include "mesh/Router.h"
#include <algorithm>
#include <deque>
#include <map>
#include <vector>
@@ -29,6 +30,8 @@ class FakeGattMesh : public BLEGattMeshHandler
std::map<BLEGattPeerId, std::vector<std::vector<uint8_t>>> notified;
std::map<BLEGattPeerId, std::vector<std::vector<uint8_t>>> greeted;
std::vector<BLEGattPeerId> shed;
std::map<BLEGattPeerId, int> probed;
std::vector<BLEGattPeerId> dead;
std::deque<std::pair<BLEGattPeerId, std::vector<uint8_t>>> inbound;
std::vector<meshtastic_MeshPacket> received;
bool ready = true;
@@ -38,6 +41,11 @@ class FakeGattMesh : public BLEGattMeshHandler
void start() override { isRunning = true; }
void stop() override { isRunning = false; }
void liveness(uint32_t nowMs)
{
pumpGreet(nowMs);
pumpLiveness(nowMs);
}
void feed(BLEGattPeerId peer, const std::vector<uint8_t> &chunk, uint32_t nowMs = 0)
{
handleChunk(peer, chunk.data(), chunk.size(), nowMs);
@@ -77,6 +85,11 @@ class FakeGattMesh : public BLEGattMeshHandler
return true;
}
void platformShedOutbound(BLEGattPeerId peer) override { shed.push_back(peer); }
bool platformProbe(BLEGattPeerId peer) override
{
probed[peer]++;
return std::find(dead.begin(), dead.end(), peer) == dead.end();
}
bool platformPollInbound(BLEGattPeerId &peer, uint8_t *buf, size_t cap, size_t &len) override
{
if (inbound.empty())
@@ -611,6 +624,95 @@ void test_the_loser_sheds_a_node_reached_both_ways(void)
TEST_ASSERT_EQUAL_MESSAGE(2, h.shed.size(), "the name on the new link is remembered");
}
void test_a_quiet_dialled_link_is_probed_and_kept(void)
{
FakeGattMesh h;
h.start();
h.peers = {{1, 244, true}};
h.liveness(500); // first listed here: the window runs from now, not from the epoch
h.feed(1, helloWith(0x11), 1000);
h.liveness(20000);
TEST_ASSERT_EQUAL_MESSAGE(0, h.probed[1], "heard 19 s ago: not yet");
h.liveness(31001);
TEST_ASSERT_EQUAL_MESSAGE(1, h.probed[1], "quiet for the idle window: asked once");
h.liveness(45000);
TEST_ASSERT_EQUAL_MESSAGE(1, h.probed[1], "asked 14 s ago: not again yet");
TEST_ASSERT_EQUAL_MESSAGE(0, h.shed.size(), "it answered, so it stays");
h.feed(1, split(encode(encryptedPacket()), 1, 244)[0], 50000);
h.liveness(70000);
TEST_ASSERT_EQUAL_MESSAGE(1, h.probed[1], "traffic 20 s ago resets the window");
}
void test_a_dead_dialled_link_is_shed(void)
{
FakeGattMesh h;
h.start();
h.peers = {{1, 244, true}};
h.dead = {1};
h.liveness(0);
h.feed(1, helloWith(0x11), 0);
h.liveness(30000);
TEST_ASSERT_EQUAL(1, h.probed[1]);
TEST_ASSERT_EQUAL_MESSAGE(0, h.shed.size(), "one miss may be a busy stack");
h.liveness(31000);
TEST_ASSERT_EQUAL_MESSAGE(1, h.probed[1], "the retry waits its two seconds");
h.liveness(32000);
TEST_ASSERT_EQUAL(2, h.probed[1]);
TEST_ASSERT_EQUAL_MESSAGE(1, h.shed.size(), "two misses: shed");
TEST_ASSERT_EQUAL(1, h.shed[0]);
// Still listed until the platform reports the disconnect, and not asked again meanwhile.
h.liveness(32010);
TEST_ASSERT_EQUAL_MESSAGE(2, h.probed[1], "asked nothing more while it drains");
TEST_ASSERT_EQUAL(1, h.shed.size());
// The disconnect forgets the name it gave: a fresh HELLO on a new link is a new peer, not a match.
h.lost(1);
h.feed(2, helloWith(0x11), 32020);
TEST_ASSERT_EQUAL_MESSAGE(1, h.shed.size(), "nothing left to be one node with");
}
void test_a_fresh_dialled_link_gets_its_full_window(void)
{
FakeGattMesh h;
h.start();
h.peers = {{1, 244, true}};
// Up at t=100 s of uptime, never heard: the MTU exchange is still running, and a probe now would
// be refused by the stack and read as death.
h.liveness(100000);
TEST_ASSERT_EQUAL_MESSAGE(0, h.probed[1], "listed just now: not asked");
h.liveness(129000);
TEST_ASSERT_EQUAL(0, h.probed[1]);
h.liveness(130000);
TEST_ASSERT_EQUAL_MESSAGE(1, h.probed[1], "asked once the window has run from link-up");
}
void test_one_missed_probe_is_forgiven_when_the_retry_answers(void)
{
FakeGattMesh h;
h.start();
h.peers = {{1, 244, true}};
h.liveness(0);
h.dead = {1};
h.liveness(30000);
TEST_ASSERT_EQUAL(1, h.probed[1]);
h.dead.clear();
h.liveness(32000);
TEST_ASSERT_EQUAL(2, h.probed[1]);
TEST_ASSERT_EQUAL_MESSAGE(0, h.shed.size(), "the retry answered: kept");
h.liveness(40000);
TEST_ASSERT_EQUAL_MESSAGE(2, h.probed[1], "back to the long window");
}
void test_an_inbound_link_is_never_probed(void)
{
FakeGattMesh h;
h.start();
h.peers = {{1, 244, false}};
h.dead = {1};
h.liveness(100000);
TEST_ASSERT_EQUAL_MESSAGE(0, h.probed[1], "the peer dialled us; its stack tells us when it goes");
TEST_ASSERT_EQUAL(0, h.shed.size());
}
void test_inbound_chunks_are_drained_by_the_pump(void)
{
FakeGattMesh h;
@@ -664,6 +766,11 @@ void setup()
RUN_TEST(test_a_refused_greeting_is_offered_again);
RUN_TEST(test_a_hello_in_is_never_a_fragment);
RUN_TEST(test_the_loser_sheds_a_node_reached_both_ways);
RUN_TEST(test_a_quiet_dialled_link_is_probed_and_kept);
RUN_TEST(test_a_dead_dialled_link_is_shed);
RUN_TEST(test_a_fresh_dialled_link_gets_its_full_window);
RUN_TEST(test_one_missed_probe_is_forgiven_when_the_retry_answers);
RUN_TEST(test_an_inbound_link_is_never_probed);
exit(UNITY_END());
}