From c0bc2afc66e3d05f4d6047283bc14917c0b104a2 Mon Sep 17 00:00:00 2001 From: Patrick Winnertz Date: Tue, 21 Apr 2026 08:43:34 +0200 Subject: [PATCH] suspend the radio before blocking io --- examples/simple_repeater/MyMesh.cpp | 17 +++++++++++++++-- examples/simple_repeater/MyMesh.h | 1 + examples/simple_room_server/MyMesh.cpp | 16 ++++++++++++++-- examples/simple_room_server/MyMesh.h | 1 + examples/simple_sensor/SensorMesh.cpp | 16 ++++++++++++++-- examples/simple_sensor/SensorMesh.h | 1 + src/Dispatcher.h | 11 +++++++++++ src/helpers/radiolib/RadioLibWrappers.h | 1 + 8 files changed, 58 insertions(+), 6 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 666f79fc5c..ecf0fd7341 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1291,8 +1291,21 @@ void MyMesh::loop() { // is pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { - acl.save(_fs); - dirty_contacts_expiry = 0; + if (_mgr->getOutboundTotal() > 0 || isRadioBusy()) { + // Radio is active on SPI — defer to avoid flash/SPI conflict on nRF52 + dirty_contacts_expiry = futureMillis(200); + if (++dirty_contacts_defer_count > 50) { // cap at ~10s to guarantee save + _radio->suspendRadio(); + acl.save(_fs); + dirty_contacts_expiry = 0; + dirty_contacts_defer_count = 0; + } + } else { + _radio->suspendRadio(); + acl.save(_fs); + dirty_contacts_expiry = 0; + dirty_contacts_defer_count = 0; + } } // update uptime diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8ed0317e69..0b8cea687d 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -103,6 +103,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { unsigned long pending_discover_until; bool region_load_active; unsigned long dirty_contacts_expiry; + uint8_t dirty_contacts_defer_count; #if MAX_NEIGHBOURS NeighbourInfo neighbours[MAX_NEIGHBOURS]; #endif diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 145fb0fd9f..50bb19e6cd 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1011,8 +1011,20 @@ void MyMesh::loop() { // is pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { - acl.save(_fs, MyMesh::saveFilter); - dirty_contacts_expiry = 0; + if (_mgr->getOutboundTotal() > 0 || isRadioBusy()) { + dirty_contacts_expiry = futureMillis(200); + if (++dirty_contacts_defer_count > 50) { + _radio->suspendRadio(); + acl.save(_fs, MyMesh::saveFilter); + dirty_contacts_expiry = 0; + dirty_contacts_defer_count = 0; + } + } else { + _radio->suspendRadio(); + acl.save(_fs, MyMesh::saveFilter); + dirty_contacts_expiry = 0; + dirty_contacts_defer_count = 0; + } } // TODO: periodically check for OLD/inactive entries in known_clients[], and evict diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 1b35ae95a1..de5e9e4532 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -101,6 +101,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ClientACL acl; CommonCLI _cli; unsigned long dirty_contacts_expiry; + uint8_t dirty_contacts_defer_count; uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long next_push; uint16_t _num_posted, _num_post_pushes; diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index b8fe1e579c..1f2f19ad72 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -968,7 +968,19 @@ void SensorMesh::loop() { // is there are pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { - acl.save(_fs); - dirty_contacts_expiry = 0; + if (_mgr->getOutboundTotal() > 0 || isRadioBusy()) { + dirty_contacts_expiry = futureMillis(200); + if (++dirty_contacts_defer_count > 50) { + _radio->suspendRadio(); + acl.save(_fs); + dirty_contacts_expiry = 0; + dirty_contacts_defer_count = 0; + } + } else { + _radio->suspendRadio(); + acl.save(_fs); + dirty_contacts_expiry = 0; + dirty_contacts_defer_count = 0; + } } } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index ee5d5e025a..d9d349b7a8 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -138,6 +138,7 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { CommonCLI _cli; uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long dirty_contacts_expiry; + uint8_t dirty_contacts_defer_count; CayenneLPP telemetry; TransportKeyStore key_store; RegionMap region_map; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 2a99d0682b..8a612847a6 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -69,6 +69,14 @@ class Radio { virtual bool isInRecvMode() const = 0; + /** + * \brief Put radio into standby. Next recvRaw() call will retsart receive. + * Used to quiesce the SPI bus before flash I/O on nRF52. + * + * Introduced as fix for #2283 + */ + virtual void suspendRadio() { } + /** * \returns true if the radio is currently mid-receive of a packet. */ @@ -193,6 +201,9 @@ class Dispatcher { bool millisHasNowPassed(unsigned long timestamp) const; unsigned long futureMillis(int millis_from_now) const; + /// True when a packet is mid-transmit (dequeued, SPI active). + bool isRadioBusy() const { return outbound != NULL; } + private: bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len); void checkRecv(); diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index a42e060a4c..1e22bc7ab5 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -29,6 +29,7 @@ class RadioLibWrapper : public mesh::Radio { bool isSendComplete() override; void onSendFinished() override; bool isInRecvMode() const override; + void suspendRadio() override { idle(); } bool isChannelActive(); bool isReceiving() override {