@@ -49,7 +49,7 @@ NimBLECharacteristic *logRadioCharacteristic;
4949NimBLEServer *bleServer;
5050
5151static bool passkeyShowing;
52- static std::atomic<int32_t > nimbleBluetoothConnHandle{- 1 }; // actual handles are uint16_t, so -1 means "no connection"
52+ static std::atomic<uint16_t > nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE }; // BLE_HS_CONN_HANDLE_NONE means "no connection"
5353
5454class BluetoothPhoneAPI : public PhoneAPI , public concurrency ::OSThread
5555{
@@ -144,21 +144,28 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
144144 protected:
145145 virtual int32_t runOnce () override
146146 {
147- bool shouldBreakAndRetryLater = false ;
148-
149147 while (runOnceHasWorkToDo ()) {
150- // Important that we service onRead first, because the onRead callback blocks NimBLE until we clear
151- // onReadCallbackIsWaitingForData.
152- shouldBreakAndRetryLater = runOnceHandleToPhoneQueue (); // push data from getFromRadio to onRead
153- runOnceHandleFromPhoneQueue (); // pull data from onWrite to handleToRadio
148+ /*
149+ PROCESS fromPhoneQueue BEFORE toPhoneQueue:
154150
155- if (shouldBreakAndRetryLater) {
156- // onRead still wants data, but it's not available yet. Return so we can try again when a packet may be ready.
157- #ifdef DEBUG_NIMBLE_ON_READ_TIMING
158- LOG_INFO (" BLE runOnce breaking to retry later (leaving onRead waiting)" );
159- #endif
160- return 100 ; // try again in 100ms
161- }
151+ In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the same
152+ time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally ok to
153+ service either the reads or writes first.
154+
155+ However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and they
156+ expect the read will respond to the write. (This also happens when a client goes from STATE_SEND_PACKETS back to
157+ another wantConfig, like the iOS client does when requesting the nodedb after requesting the main config only.)
158+
159+ So it's safest to always service writes (fromPhoneQueue) before reads (toPhoneQueue), so that any "synchronous"
160+ write-then-read sequences from the client work as expected, even if this means we block onRead for a while: this is
161+ what the client wants!
162+ */
163+
164+ // PHONE -> RADIO:
165+ runOnceHandleFromPhoneQueue (); // pull data from onWrite to handleToRadio
166+
167+ // RADIO -> PHONE:
168+ runOnceHandleToPhoneQueue (); // push data from getFromRadio to onRead
162169 }
163170
164171 // the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback
@@ -171,9 +178,9 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
171178
172179 // Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds)
173180 if (bleServer && isConnected ()) {
174- int32_t conn_handle = nimbleBluetoothConnHandle.load ();
175- if (conn_handle != - 1 ) {
176- requestHighThroughputConnection (static_cast < uint16_t >( conn_handle) );
181+ uint16_t conn_handle = nimbleBluetoothConnHandle.load ();
182+ if (conn_handle != BLE_HS_CONN_HANDLE_NONE ) {
183+ requestHighThroughputConnection (conn_handle);
177184 }
178185 }
179186 }
@@ -184,9 +191,9 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
184191
185192 // Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete
186193 if (bleServer && isConnected ()) {
187- int32_t conn_handle = nimbleBluetoothConnHandle.load ();
188- if (conn_handle != - 1 ) {
189- requestLowerPowerConnection (static_cast < uint16_t >( conn_handle) );
194+ uint16_t conn_handle = nimbleBluetoothConnHandle.load ();
195+ if (conn_handle != BLE_HS_CONN_HANDLE_NONE ) {
196+ requestLowerPowerConnection (conn_handle);
190197 }
191198 }
192199 }
@@ -220,12 +227,8 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
220227 }
221228 }
222229
223- bool runOnceHandleToPhoneQueue ()
230+ void runOnceHandleToPhoneQueue ()
224231 {
225- // Returns false normally.
226- // Returns true if we should break out of runOnce and retry later, such as setup states where getFromRadio returns 0
227- // bytes.
228-
229232 // Stack buffer for getFromRadio packet
230233 uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0 };
231234 size_t numBytes = 0 ;
@@ -234,28 +237,15 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
234237 numBytes = getFromRadio (fromRadioBytes);
235238
236239 if (numBytes == 0 ) {
237- // Client expected a read, but we have nothing to send.
238- // Returning a 0-byte packet breaks clients during the config phase, so we have to block onRead until there's a
239- // packet ready.
240- if (isSendingPackets ()) {
241- // In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond
242- // notifies regularly, to make sure they have nothing else to read.
243- #ifdef DEBUG_NIMBLE_ON_READ_TIMING
244- LOG_DEBUG (" BLE getFromRadio returned numBytes=0, but in STATE_SEND_PACKETS, so clearing "
245- " onReadCallbackIsWaitingForData flag" );
246- #endif
247- } else {
248- // In other states, this breaks clients.
249- // Return early, leaving onReadCallbackIsWaitingForData==true so onRead knows to try again.
250- // This gives runOnce a chance to handleToRadio and produce a response.
251- #ifdef DEBUG_NIMBLE_ON_READ_TIMING
252- LOG_DEBUG (" BLE getFromRadio returned numBytes=0. Blocking onRead until we have data" );
253- #endif
240+ /*
241+ Client expected a read, but we have nothing to send.
254242
255- // Return true to tell runOnce to shouldBreakAndRetryLater, so we don't busy-loop in runOnce even though
256- // onRead is still waiting!
257- return true ;
258- }
243+ In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond
244+ notifies regularly, to make sure they have nothing else to read.
245+
246+ In other states, this is fine **so long as we've already processed pending onWrites first**, because the client
247+ may requesting wantConfig and immediately doing a read.
248+ */
259249 } else {
260250 // Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible.
261251 if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) {
@@ -282,8 +272,6 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
282272 // Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed.
283273 onReadCallbackIsWaitingForData = false ; // only clear this flag AFTER the push
284274 }
285-
286- return false ;
287275 }
288276
289277 bool runOnceHasWorkFromPhone () { return fromPhoneQueueSize > 0 ; }
@@ -466,10 +454,6 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
466454 virtual void onRead (NimBLECharacteristic *pCharacteristic)
467455#endif
468456 {
469- // In some cases, it seems a new connection starts with a read.
470- // The API has no bytes to send, leading to a timeout. This short-circuits this problem.
471- if (!bluetoothPhoneAPI->isConnected ())
472- return ;
473457 // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
474458
475459 int currentReadCount = bluetoothPhoneAPI->readCount .fetch_add (1 );
@@ -726,7 +710,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
726710 // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection
727711 memset (lastToRadio, 0 , sizeof (lastToRadio));
728712
729- nimbleBluetoothConnHandle = - 1 ; // -1 means "no connection"
713+ nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE ; // BLE_HS_CONN_HANDLE_NONE means "no connection"
730714
731715#ifdef NIMBLE_TWO
732716 // Restart Advertising
0 commit comments