Files
MeshCore/test/fixtures/pico_ble_queues/test.cpp
T

263 lines
14 KiB
C++

#include <helpers/rp2040/SerialBLEInterface.h>
#include <helpers/MultiSerialInterface.h>
#include <cassert>
#include <cstdio>
#include <string>
using fake_pico::Bytes;
@QUERY_PRODUCER@
struct Fixture {
SerialBLEInterface node;
BLECharacteristic *rx = nullptr, *tx = nullptr;
Fixture() {
fake_pico::now = 100;
assert(node.begin("MeshCore-", "PicoW", 123456)); node.enable();
for (auto* characteristic : BLE.server()->services[0]->characteristics) {
if (characteristic->uuid.bytes[3] == 2) rx = characteristic;
if (characteristic->uuid.bytes[3] == 3) tx = characteristic;
}
assert(rx && tx);
fake_pico::disconnect_event = []() { BLE.server()->disconnectEvent(); };
BLE.server()->connect(); tx->setNotify(true); fake_pico::mtu = 247;
assert(node.isConnected());
}
void write(const Bytes& frame) { assert(node.writeFrame(frame.data(), frame.size()) == frame.size()); }
void query() { rx->hostWrite(Bytes{CMD_DEVICE_QUERY, 3}); }
size_t receive(uint8_t* input) { return node.checkRecvFrame(input); }
void dispatch() {
uint8_t input[MAX_FRAME_SIZE]{};
const size_t length = receive(input); assert(length == 2);
ActualDeviceQuery mesh; mesh._serial = &node; mesh.dispatch(input, length);
}
void tick(uint32_t delay = 10) { fake_pico::now += delay; node.loop(); }
void drain() {
for (unsigned i = 0; i < 20; ++i) {
if (!fake_pico::pending.empty()) fake_pico::fireOne();
tick();
}
}
};
struct HoldingUsb : BaseSerialInterface {
bool pending = true;
void enable() override {}
void disable() override {}
bool isEnabled() const override { return true; }
bool isConnected() const override { return true; }
bool isReadBusy() const override { return pending; }
bool isWriteBusy() const override { return false; }
size_t writeFrame(const uint8_t*, size_t length) override { return length; }
size_t checkRecvFrame(uint8_t* out) override {
if (!pending) return 0;
pending = false; out[0] = 4; return 1;
}
};
static void test_stall(const std::string& scenario) {
Fixture f;
fake_pico::now = scenario == "stall-wrap" ? UINT32_MAX - 5000 : 0;
fake_pico::synchronous = scenario == "stall-register" || scenario == "stall-notify" || scenario == "stall-mtu";
if (scenario == "stall-register") fake_pico::registration_error = ERROR_CODE_UNKNOWN_CONNECTION_IDENTIFIER;
if (scenario == "stall-notify") fake_pico::notify_errors = {BTSTACK_ACL_BUFFERS_FULL, BTSTACK_ACL_BUFFERS_FULL};
if (scenario == "stall-mtu") fake_pico::mtu = 23;
fake_pico::disconnect_error = ERROR_CODE_COMMAND_DISALLOWED;
Bytes required(scenario == "stall-mtu" ? 82 : 2, 0x41); required[0] = 0x09;
f.write(required); f.node.loop();
if (scenario == "stall-cccd") f.tx->setNotify(false);
f.tick(9999); assert(fake_pico::disconnects == 0);
f.tick(1);
assert(fake_pico::disconnects == 1 && fake_pico::connection_live);
assert(!f.node.isConnected() && f.node.isWriteBusy() && f.node.hasPendingIO());
uint8_t input[MAX_FRAME_SIZE]{};
f.query(); assert(f.receive(input) == 0);
const uint8_t reply[] = {0x09, 0x42};
assert(f.node.writeFrame(reply, sizeof(reply)) == 0);
f.tx->setNotify(false); f.tick(999); assert(fake_pico::disconnects == 1);
f.tick(1); assert(fake_pico::disconnects == 2 && !f.node.isConnected());
fake_pico::disconnect_error = 0;
f.tick(1000); assert(fake_pico::disconnects == 3 && fake_pico::connection_live);
// Request acceptance and missing CCCD cannot end recovery before closure.
assert(!f.node.isConnected() && f.node.hasPendingIO());
BLE.server()->disconnectEvent(); assert(!f.node.hasPendingIO());
assert(fake_pico::advertising_active);
fake_pico::synchronous = true;
fake_pico::registration_error = 0; fake_pico::notify_errors.clear(); fake_pico::mtu = 247;
BLE.server()->connect(2); f.tx->setNotify(true);
assert(f.node.isConnected()); f.query(); f.dispatch(); f.tick();
assert(fake_pico::notified.size() == 1 && fake_pico::notified[0].size() == 82);
assert(fake_pico::notified[0][0] == 13);
}
int main(int argc, char** argv) {
assert(argc == 2);
const std::string scenario = argv[1];
if (scenario.compare(0, 6, "stall-") == 0) {
test_stall(scenario);
} else {
Fixture f;
if (scenario == "normal") {
Bytes maximum(MAX_FRAME_SIZE);
for (unsigned i = 0; i < maximum.size(); ++i) maximum[i] = uint8_t(i);
f.write(maximum); f.write(Bytes{0x09, 0x41});
assert(fake_pico::notified == (std::vector<Bytes>{maximum, Bytes{0x09, 0x41}}));
assert(!f.node.hasPendingIO() && fake_pico::max_callback_depth == 1);
assert(f.node.writeFrame(nullptr, 1) == 0);
assert(f.node.writeFrame(maximum.data(), 0) == 0);
assert(f.node.writeFrame(maximum.data(), MAX_FRAME_SIZE + 1) == 0);
assert(f.node.checkRecvFrame(nullptr) == 0);
f.rx->hostWrite({}); f.rx->hostWrite(Bytes(MAX_FRAME_SIZE + 1, 1));
uint8_t input[MAX_FRAME_SIZE + 2]{}; input[0] = input[MAX_FRAME_SIZE + 1] = 0xa5;
assert(f.receive(input + 1) == 0);
f.rx->hostWrite(maximum); assert(f.receive(input + 1) == MAX_FRAME_SIZE);
assert(input[0] == 0xa5 && input[MAX_FRAME_SIZE + 1] == 0xa5);
assert(std::equal(maximum.begin(), maximum.end(), input + 1));
} else if (scenario == "admission") {
fake_pico::synchronous = false;
for (unsigned i = 0; i < 8; ++i) f.write(Bytes{0x09, uint8_t(i)});
f.query(); uint8_t input[MAX_FRAME_SIZE]{};
assert(f.receive(input) == 0
&& "a full required TX queue must retain the command until its reply fits");
fake_pico::fireOne(); assert(f.receive(input) == 2);
ActualDeviceQuery mesh; mesh._serial = &f.node; mesh.dispatch(input, 2);
f.drain();
assert(fake_pico::notified.size() == 9 && fake_pico::notified.back().size() == 82);
assert(fake_pico::notified.back()[0] == 13 && !f.node.hasPendingIO());
} else if (scenario == "priority") {
fake_pico::synchronous = false;
const Bytes log{0x88, 0x41};
for (unsigned i = 0; i < 7; ++i) f.write(log);
assert(f.node.writeFrame(log.data(), log.size()) == 0);
f.query(); f.dispatch(); f.drain();
assert(fake_pico::notified.size() == 8 && fake_pico::notified[0].size() == 82);
assert(fake_pico::notified[0][0] == 13);
for (unsigned i = 1; i < 8; ++i) assert(fake_pico::notified[i] == log);
fake_pico::notified.clear();
for (unsigned i = 0; i < 30; ++i) f.write(Bytes{0x83});
f.drain(); assert(fake_pico::notified == std::vector<Bytes>{Bytes{0x83}});
fake_pico::notified.clear();
for (unsigned i = 0; i < 7; ++i) f.write(log);
f.write(Bytes{0x85, 1}); f.write(Bytes{0x09, 2}); f.drain();
assert(fake_pico::notified.size() == 8);
assert(fake_pico::notified[0] == (Bytes{0x09, 2}));
assert(fake_pico::notified[1] == (Bytes{0x85, 1}));
} else if (scenario == "mtu") {
fake_pico::mtu = 23; f.query(); f.dispatch();
assert(f.node.hasPendingIO() && fake_pico::notified.empty()
&& "required DEVICE_QUERY response must survive an initial MTU of 23");
const unsigned attempts = fake_pico::registrations;
f.tick(9); assert(fake_pico::registrations == attempts);
f.tick(1); assert(fake_pico::notified.empty());
fake_pico::mtu = 247; f.tick(); f.tick();
assert(fake_pico::notified.size() == 1 && fake_pico::notified[0].size() == 82);
assert(fake_pico::notified[0][0] == 13 && !f.node.hasPendingIO());
} else if (scenario == "notify") {
Bytes maximum(MAX_FRAME_SIZE, 0x31); maximum[0] = 13;
fake_pico::notify_errors = {BTSTACK_ACL_BUFFERS_FULL, ERROR_CODE_COMMAND_DISALLOWED, 0};
f.write(maximum); assert(fake_pico::notified.empty() && f.node.hasPendingIO());
assert(fake_pico::registrations == 1 && fake_pico::max_callback_depth == 1);
f.tick(9); assert(fake_pico::registrations == 1);
f.tick(1); assert(fake_pico::registrations == 2 && fake_pico::notified.empty());
f.tick(); f.tick();
assert(fake_pico::notified == std::vector<Bytes>{maximum});
assert(fake_pico::notification_attempts == 3 && !f.node.hasPendingIO());
} else if (scenario == "registration" || scenario == "registration-sync-duplicate") {
fake_pico::synchronous = scenario == "registration-sync-duplicate";
fake_pico::duplicate_registration = true;
f.write(Bytes{0x09, 1});
if (!fake_pico::synchronous) {
for (unsigned i = 0; i < 10; ++i) f.tick();
assert(fake_pico::registrations == 1 && fake_pico::pending.size() == 1);
fake_pico::fireOne();
}
assert(fake_pico::notified == (std::vector<Bytes>{Bytes{0x09, 1}}));
assert(!f.node.hasPendingIO());
f.write(Bytes{0x09, 2});
if (!fake_pico::synchronous) fake_pico::fireOne();
assert(fake_pico::notified.size() == 2 && fake_pico::registrations == 2);
} else if (scenario == "registration-error" || scenario == "callback-clock") {
fake_pico::registration_error = ERROR_CODE_UNKNOWN_CONNECTION_IDENTIFIER;
f.write(Bytes{0x09, 1}); assert(fake_pico::registrations == 1);
fake_pico::registration_error = 0;
if (scenario == "callback-clock") fake_pico::now += 9;
else f.tick(9);
assert(fake_pico::registrations == 1);
if (scenario == "callback-clock") {
fake_pico::callback_advance_ms = 1;
fake_pico::notify_errors.push_back(BTSTACK_ACL_BUFFERS_FULL);
}
f.tick(1);
assert(fake_pico::disconnects == 0 && f.node.isConnected()
&& "a synchronous callback must not backdate the stall watchdog");
f.tick();
assert(fake_pico::notified == (std::vector<Bytes>{Bytes{0x09, 1}}));
} else if (scenario == "deadline-progress") {
fake_pico::mtu = 23; f.query(); f.dispatch();
fake_pico::mtu = 247; f.tick(10000);
assert(fake_pico::notified.size() == 1 && fake_pico::disconnects == 0);
} else if (scenario == "best-effort") {
fake_pico::mtu = 23; Bytes log(64, 1); log[0] = 0x88; f.write(log);
f.tick(100000); assert(fake_pico::disconnects == 0 && !f.node.hasPendingIO());
fake_pico::mtu = 247;
fake_pico::notify_errors.push_back(BTSTACK_ACL_BUFFERS_FULL);
f.write(Bytes{0x88, 1}); f.tick(100000);
assert(fake_pico::disconnects == 0 && f.node.isConnected() && !f.node.hasPendingIO());
} else if (scenario == "locked") {
HoldingUsb usb; MultiSerialInterface manager;
assert(manager.addInterface(InterfaceType::USB, &usb));
assert(manager.addInterface(InterfaceType::Bluetooth, &f.node)); manager.enable();
uint8_t input[MAX_FRAME_SIZE]{};
assert(manager.checkRecvFrame(input) == 1); manager.lockReplyRoute();
fake_pico::mtu = 23; Bytes required(82, 1); required[0] = 13; f.write(required);
f.query(); assert(manager.checkRecvFrame(input) == 0);
fake_pico::mtu = 247; fake_pico::now += 10; manager.loop();
assert(fake_pico::notified == std::vector<Bytes>{required});
assert(manager.captureReplyRoute() == &usb && manager.checkRecvFrame(input) == 0);
fake_pico::mtu = 23; f.write(required);
fake_pico::now += 10000; manager.loop();
assert(fake_pico::disconnects == 1 && !manager.isReplyRouteAvailable(&f.node));
assert(manager.captureReplyRoute() == &usb && manager.isConnected());
} else if (scenario == "disabled-reenable" || scenario == "disabled-closed") {
fake_pico::disconnect_error = ERROR_CODE_COMMAND_DISALLOWED;
f.node.disable(); assert(!f.node.isConnected() && !fake_pico::advertising_requested);
if (scenario == "disabled-reenable") f.node.enable();
assert(fake_pico::advertising_starts == 1 && !fake_pico::advertising_active);
f.tick(1000); assert(fake_pico::disconnects == 2 && !f.node.isConnected());
BLE.server()->disconnectEvent();
assert(!fake_pico::advertising_active); f.tick();
const bool enabled = scenario == "disabled-reenable";
assert(fake_pico::advertising_active == enabled);
assert(fake_pico::advertising_starts == unsigned(enabled ? 2 : 1));
assert(!f.node.hasPendingIO());
} else if (scenario == "late-connect") {
BLE.server()->disconnectEvent(); f.node.disable();
BLE.server()->connect(2); // Controller completed connection after stopAdvertising.
assert(!f.node.isConnected() && f.node.hasPendingIO());
f.node.enable(); assert(fake_pico::advertising_starts == 1);
f.tick(); assert(fake_pico::disconnects == 1 && !f.node.isConnected());
BLE.server()->disconnectEvent(); f.tick();
assert(fake_pico::advertising_active && fake_pico::advertising_starts == 2);
} else if (scenario == "synchronous-disconnect") {
fake_pico::synchronous_disconnect = true;
f.node.disconnect();
assert(!fake_pico::connection_live && !f.node.hasPendingIO() && !f.node.isConnected());
assert(fake_pico::disconnects == 1);
f.tick(1000); assert(fake_pico::disconnects == 1);
} else if (scenario == "pending-cccd") {
fake_pico::synchronous = false; f.query(); f.dispatch();
f.tx->setNotify(false); fake_pico::fireOne();
assert(f.node.hasPendingIO() && fake_pico::notified.empty());
f.tx->setNotify(true); f.tick(); fake_pico::fireOne();
assert(fake_pico::notified.size() == 1 && fake_pico::notified[0].size() == 82);
assert(!f.node.hasPendingIO());
} else if (scenario == "cccd") {
f.tx->setNotify(false); f.query(); uint8_t input[MAX_FRAME_SIZE]{};
assert(f.receive(input) == 0);
f.tx->setNotify(true); f.dispatch();
assert(fake_pico::notified.size() == 1 && fake_pico::notified[0].size() == 82);
} else assert(false && "unknown scenario");
}
assert(fake_pico::lock_depth == 0 && fake_pico::callback_depth == 0);
std::printf("PASS: Pico BLE delivery %s\n", scenario.c_str());
}