// Appended to the real bridge source by test_bridge_recovery.code(). Only // SDK APIs, clocks, Packet allocation and the bounded debug sink are mocked. namespace diag = mesh::bridge::diag; static void startBackend(Backend& backend) { backend.wifi = true; mesh::wireless::control().begin(backend); sdk_initialized = sdk_started = true; sdk_mode = WIFI_STA; } static void deliver(ESPNowBridge& bridge, const uint8_t* bytes, int len) { const uint8_t mac[6] = {1, 2, 3, 4, 5, 6}; mesh::in_sdk_callback = true; #if ESP_IDF_VERSION_MAJOR >= 5 const esp_now_recv_info_t info{mac}; ESPNowBridge::recv_cb(&info, bytes, len); #else ESPNowBridge::recv_cb(mac, bytes, len); #endif mesh::in_sdk_callback = false; (void)bridge; } static void deliverCompletion(ESPNowBridge& bridge, int status) { mesh::in_sdk_callback = true; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) const uint8_t mac[6] = {255, 255, 255, 255, 255, 255}; const esp_now_send_info_t info{mac}; ESPNowBridge::send_cb(&info, static_cast(status)); #else ESPNowBridge::send_cb(nullptr, static_cast(status)); #endif mesh::in_sdk_callback = false; (void)bridge; } static void receiveProbes() { Backend backend; startBackend(backend); NodePrefs prefs; mesh::PacketManager mgr; mesh::RTCClock rtc; ESPNowBridge bridge(&prefs, &mgr, &rtc); uint8_t bytes[16]; assert(diag::encodeProbe(bytes, sizeof(bytes), {0x12345678U, 0, 6})); mgr.exhausted = true; for (uint8_t format : {uint8_t(0), uint8_t(1)}) { prefs.bridge_format = format; bridge.begin(); assert(bridge._initialized); const auto allocations = mgr.allocs; const auto received = bridge._stats.probe.rx_ok; deliver(bridge, bytes, sizeof(bytes)); assert(bridge._stats.probe.rx_ok == received); // callback only copies bridge.loop(); assert(bridge._stats.probe.rx_ok == received + 1); assert(bridge._stats.probe.last_rx_nonce == 0x12345678U); assert(bridge._rx_count == 0 && mgr.allocs == allocations); assert(mgr.received.empty() && bridge._seen_packets.ids.empty()); assert(send_calls == 0 && sent.empty()); // no echo or LoRa admission bytes[15] ^= 1; deliver(bridge, bytes, sizeof(bytes)); bridge.loop(); bytes[15] ^= 1; assert(bridge._stats.probe.rx_bad == uint32_t(format + 1)); assert(bridge._rx_count == 0 && mgr.allocs == allocations); bridge.end(); } assert(bridge._stats.probe.rx_ok == 2 && bridge._stats.probe.rx_bad == 2); } static void probeOneShot() { Backend backend; startBackend(backend); NodePrefs prefs; mesh::PacketManager mgr; mesh::RTCClock rtc; ESPNowBridge bridge(&prefs, &mgr, &rtc); assert(!bridge.sendTest(1) && send_calls == 0); bridge.begin(); assert(bridge.sendTest(0x01020304U)); assert(send_calls == 0 && mgr.allocs == 0); bridge.loop(); assert(send_calls == 1 && sent.size() == 1); diag::Probe parsed; assert(diag::decodeProbe(sent[0].data(), sent[0].size(), parsed)); assert(parsed.nonce == 0x01020304U && parsed.channel == 6 && parsed.format == 1); deliverCompletion(bridge, ESP_NOW_SEND_FAIL); bridge.loop(); assert(bridge._tx_count == 0 && bridge._stats.probe.tx_fail == 1); for (unsigned i = 0; i < 4; ++i) { now_ms += 20; bridge.loop(); } assert(send_calls == 1); // callback failure never retries a probe send_result = ESP_ERR_ESPNOW_NO_MEM; assert(bridge.sendTest(2)); bridge.loop(); assert(send_calls == 2 && bridge._tx_count == 0); assert(bridge._stats.probe.tx_fail == 2); for (unsigned i = 0; i < 4; ++i) { now_ms += 20; bridge.loop(); } assert(send_calls == 2); // ordinary data retry policy does not apply send_result = ESP_OK; assert(bridge.sendTest(3)); bridge.loop(); assert(send_calls == 3 && bridge._tx_waiting); now_ms += 1000; bridge.loop(); assert(bridge._initialized && bridge._tx_count == 0); assert(send_calls == 3 && bridge._stats.probe.tx_timeout == 1); assert(bridge._stats.tx.callback_timeout == 0); // probes have separate counters deliverCompletion(bridge, ESP_NOW_SEND_SUCCESS); bridge.loop(); assert(send_calls == 3 && bridge._stats.probe.tx_ok == 0); assert(bridge._seen_packets.ids.empty() && mgr.allocs == 0); assert(bridge.sendTest(3)); // a new explicit command may reuse its nonce bridge.loop(); deliverCompletion(bridge, ESP_NOW_SEND_SUCCESS); bridge.loop(); assert(send_calls == 4 && bridge._stats.probe.tx_ok == 1); bridge.end(); } static void queueOwnership() { Backend backend; startBackend(backend); NodePrefs prefs; mesh::PacketManager mgr; mesh::RTCClock rtc; ESPNowBridge bridge(&prefs, &mgr, &rtc); bridge.begin(); auto normal = packet(1); bridge.sendPacket(&normal); assert(bridge.sendTest(0xABCDEF01U)); bridge.loop(); assert(send_calls == 1 && sent[0][0] == normal.data[0]); bridge.loop(); assert(send_calls == 1); deliverCompletion(bridge, ESP_NOW_SEND_SUCCESS); bridge.loop(); assert(send_calls == 2 && diag::isProbeCandidate(sent[1].data(), sent[1].size())); deliverCompletion(bridge, ESP_NOW_SEND_SUCCESS); bridge.loop(); assert(bridge._tx_count == 0 && bridge._stats.probe.tx_ok == 1); assert(bridge._seen_packets.ids == std::vector{1}); assert(mgr.allocs == 0 && mgr.received.empty()); auto zero = packet(0); bridge._seen_packets.markSeen(&zero); assert(bridge.sendTest(9)); bridge.end(); assert(bridge._seen_packets.wasSeen(&zero)); // no fake-packet dedup clear bridge.begin(); for (uint8_t id = 10; id < 16; ++id) { auto p = packet(id); bridge.sendPacket(&p); } assert(bridge._tx_count == 6); const auto seen = bridge._seen_packets.ids; assert(!bridge.sendTest(10)); assert(bridge._tx_count == 6 && bridge._seen_packets.ids == seen); bridge.end(); } static void admissionCounters() { Backend backend; startBackend(backend); NodePrefs prefs; mesh::PacketManager mgr; mesh::RTCClock rtc; ESPNowBridge bridge(&prefs, &mgr, &rtc); bridge.begin(); auto good = packet(1), filtered = packet(2), invalid = packet(0xEE); deliver(bridge, good.data, good.length); bridge.loop(); deliver(bridge, good.data, good.length); bridge.loop(); bridge.allowed = false; deliver(bridge, filtered.data, filtered.length); bridge.loop(); bridge.allowed = true; deliver(bridge, invalid.data, invalid.length); bridge.loop(); assert(bridge._stats.rx.delivered == 1 && bridge._stats.rx.duplicate == 1); assert(bridge._stats.rx.filtered == 1 && bridge._stats.rx.packet_bad == 1); assert(bridge._stats.rx.packet_ok == 3); assert(mgr.received == std::vector{1} && mgr.allocs == mgr.frees); bridge.resetStats(); auto tx = packet(3), oversize = packet(4, 256); bridge.sendPacket(&tx); bridge.sendPacket(&tx); bridge.allowed = false; bridge.sendPacket(&filtered); bridge.allowed = true; bridge.sendPacket(&oversize); assert(bridge._stats.tx.requested == 4 && bridge._stats.tx.queued == 1); assert(bridge._stats.tx.duplicate == 1 && bridge._stats.tx.filtered == 1); assert(bridge._stats.tx.oversize == 1); bridge.loop(); deliverCompletion(bridge, ESP_NOW_SEND_SUCCESS); bridge.loop(); assert(bridge._stats.tx.start_ok == 1 && bridge._stats.tx.callback_ok == 1); bridge.end(); } static void overflowAndReset() { Backend backend; startBackend(backend); NodePrefs prefs; mesh::PacketManager mgr; mesh::RTCClock rtc; ESPNowBridge bridge(&prefs, &mgr, &rtc); bridge.begin(); uint8_t probe[16]; assert(diag::encodeProbe(probe, sizeof(probe), {7, 1, 6})); for (unsigned i = 0; i < 5; ++i) deliver(bridge, probe, sizeof(probe)); assert(bridge._rx_count == 4 && bridge._stats.rx.queue_drop == 1); bridge.loop(); assert(bridge._stats.probe.rx_ok == 4 && bridge._rx_count == 0); bridge.end(); bridge.begin(); assert(bridge._stats.probe.rx_ok == 4 && bridge._stats.rx.queue_drop == 1); assert(bridge.sendTest(8)); bridge.loop(); assert(bridge._tx_count == 1 && bridge._tx_waiting); const int writes = send_calls; bridge.resetStats(); assert(bridge._initialized && bridge._tx_count == 1 && bridge._tx_waiting); assert(send_calls == writes && prefs.bridge_channel == 6 && prefs.bridge_format == 1); assert(bridge._stats.rx.frames == 0 && bridge._stats.probe.rx_ok == 0); deliverCompletion(bridge, ESP_NOW_SEND_SUCCESS); bridge.loop(); assert(bridge._tx_count == 0 && bridge._stats.probe.tx_ok == 1); bridge.end(); } static void startupErrors() { Backend backend; startBackend(backend); NodePrefs prefs; mesh::PacketManager mgr; mesh::RTCClock rtc; ESPNowBridge bridge(&prefs, &mgr, &rtc); struct Expected { int failure; diag::Stage stage; }; for (const auto& row : { Expected{2, diag::Stage::WiFiProtocol}, {4, diag::Stage::EspNowInit}, {8, diag::Stage::RecvCallback}, {7, diag::Stage::SendCallback}, {9, diag::Stage::PeerAdd}, {10, diag::Stage::PeerRate}}) { failure = row.failure; bridge.begin(); assert(!bridge._initialized && bridge._stats.error.stage == row.stage); assert(bridge._stats.error.code == -1 && !espnow_active); } failure = 0; wifi_channel = 3; bridge.begin(); assert(!bridge._initialized && bridge._stats.error.stage == diag::Stage::ChannelConflict); assert(sdk_started); // failed bridge setup cannot shut down shared WiFi wifi_channel = 6; bridge.begin(); assert(bridge._initialized); bridge.end(); } static void boundedReplies() { Backend backend; startBackend(backend); NodePrefs prefs; mesh::PacketManager mgr; mesh::RTCClock rtc; ESPNowBridge bridge(&prefs, &mgr, &rtc); bridge.begin(); memset(&bridge._stats.rx, 0xFF, sizeof(bridge._stats.rx)); memset(&bridge._stats.tx, 0xFF, sizeof(bridge._stats.tx)); memset(&bridge._stats.probe, 0xFF, sizeof(bridge._stats.probe)); bridge._stats.error = {diag::Stage::ChannelConflict, INT32_MIN, UINT32_MAX}; for (const char* page : {"rx", "tx", "drops", "test"}) { char reply[160]; memset(reply, 0xA5, sizeof(reply)); bridge.formatStats(page, reply, 157); assert(memchr(reply, 0, 157) != nullptr); assert(static_cast(reply[157]) == 0xA5); assert(strlen(reply) < 157); printf("%s: %s\n", page, reply); } char status[160]; memset(status, 0xA5, sizeof(status)); bridge.formatStatus(status, 157); assert(memchr(status, 0, 157) != nullptr && strlen(status) < 157); assert(static_cast(status[157]) == 0xA5); printf("status: %s\n", status); for (wifi_mode_t mode : {WIFI_AP, WIFI_OFF}) { sdk_mode = mode; bridge.formatStatus(status, 157); assert(strstr(status, "> driver-error ") == status); assert(strstr(status, "ch=6/6") != nullptr); // matching channel is insufficient } sdk_mode = WIFI_AP_STA; bridge.formatStatus(status, 157); assert(strstr(status, "> ready ") == status); sdk_mode = WIFI_STA; for (size_t capacity : {size_t(0), size_t(1), size_t(2), size_t(10)}) { char reply[160]; memset(reply, 0xA5, sizeof(reply)); bridge.formatStats("tx", reply, capacity); assert(static_cast(reply[capacity]) == 0xA5); if (capacity) assert(memchr(reply, 0, capacity) != nullptr); } bridge.end(); } static void ownerOnlyLogging() { Backend backend; startBackend(backend); NodePrefs prefs; mesh::PacketManager mgr; mesh::RTCClock rtc; ESPNowBridge bridge(&prefs, &mgr, &rtc); bridge.begin(); uint8_t probe[16]; assert(diag::encodeProbe(probe, sizeof(probe), {1, 1, 6})); mesh::master_logging = true; mesh::verbose_logging = false; deliver(bridge, probe, sizeof(probe)); bridge.loop(); assert(mesh::debug_output.empty()); mesh::master_logging = false; mesh::verbose_logging = true; deliver(bridge, probe, sizeof(probe)); bridge.loop(); assert(mesh::debug_output.empty()); mesh::master_logging = mesh::verbose_logging = true; const auto before = mesh::debug_output.size(); #if !MESH_ESP32_USB_CONSOLE_COOPERATIVE mesh::debug_port.capacity = 0; deliver(bridge, probe, sizeof(probe)); now_ms += 2000; bridge.loop(); assert(mesh::debug_output.size() == before); // full UART never blocks/writes mesh::debug_port.capacity = 1024; #endif deliver(bridge, probe, sizeof(probe)); assert(mesh::debug_output.size() == before); now_ms += 2000; bridge.loop(); assert(mesh::debug_output.size() > before); assert(bridge.sendTest(2)); bridge.loop(); const auto after_start = mesh::debug_output.size(); deliverCompletion(bridge, ESP_NOW_SEND_SUCCESS); assert(mesh::debug_output.size() == after_start); now_ms += 2000; bridge.loop(); assert(mesh::debug_output.size() > after_start); bridge.end(); } int main(int argc, char** argv) { assert(argc == 2); if (!strcmp(argv[1], "rx-probes")) receiveProbes(); else if (!strcmp(argv[1], "one-shot")) probeOneShot(); else if (!strcmp(argv[1], "ownership")) queueOwnership(); else if (!strcmp(argv[1], "admission")) admissionCounters(); else if (!strcmp(argv[1], "overflow-reset")) overflowAndReset(); else if (!strcmp(argv[1], "startup")) startupErrors(); else if (!strcmp(argv[1], "replies")) boundedReplies(); else if (!strcmp(argv[1], "logging")) ownerOnlyLogging(); else assert(false); }