Skip to content

Commit 871986d

Browse files
authored
Merge pull request #8404 from compumike/compumike/fix-nimble-bluetooth-process-fromPhone-before-toPhone
Fix NimbleBluetooth: process fromPhoneQueue (phone->radio) before toPhoneQueue (radio->phone)
2 parents c4656da + 821d8aa commit 871986d

File tree

1 file changed

+37
-53
lines changed

1 file changed

+37
-53
lines changed

src/nimble/NimbleBluetooth.cpp

Lines changed: 37 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ NimBLECharacteristic *logRadioCharacteristic;
4949
NimBLEServer *bleServer;
5050

5151
static 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

5454
class 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

Comments
 (0)