-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Expand file tree
/
Copy pathNimbleBluetooth.cpp
More file actions
1085 lines (916 loc) · 49 KB
/
Copy pathNimbleBluetooth.cpp
File metadata and controls
1085 lines (916 loc) · 49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "configuration.h"
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
#include "BluetoothCommon.h"
#include "NimbleBluetooth.h"
#include "PowerFSM.h"
#include "StaticPointerQueue.h"
#include "concurrency/OSThread.h"
#include "main.h"
#include "mesh/PhoneAPI.h"
#include "mesh/Throttle.h"
#include "mesh/mesh-pb-constants.h"
#include "sleep.h"
#include <BLE2904.h>
#include <BLEAdvertising.h>
#include <BLEDevice.h>
#include <BLESecurity.h>
#include <BLEUtils.h>
#include <atomic>
#include <mutex>
#include "PowerStatus.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/ble_store.h"
#ifdef ARCH_ESP32
#include <nvs.h>
#include <nvs_flash.h>
#endif
namespace
{
constexpr uint16_t kPreferredBleMtu = 517;
constexpr uint16_t kPreferredBleTxOctets = 251;
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
} // namespace
#ifdef ARCH_ESP32
// Discard NimBLE bonds left in an incompatible on-disk format. The ESP-IDF/NimBLE upgrade changed
// the length of the fixed-size bond records (ble_store_value_sec), so the new host rejects every
// old record on each boot ("NVS data size mismatch for obj_type 1 ...") with no auto-recovery --
// pairing stays broken until a factory reset. Wipe the bond namespace once when a stored record's
// size differs from this build's struct; a same-size store is left untouched, so this never loops.
// Adapted from https://github.com/h2zero/NimBLE-Arduino/issues/740
static void purgeIncompatibleBleBonds()
{
esp_err_t initErr = nvs_flash_init();
if (initErr != ESP_OK) {
LOG_WARN("purgeIncompatibleBleBonds: nvs_flash_init failed, err=%d", (int)initErr);
return; // NVS should already be up; if not, nothing safe to do here
}
nvs_handle_t handle = 0;
esp_err_t err = nvs_open("nimble_bond", NVS_READWRITE, &handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return; // no bonds stored yet
}
if (err != ESP_OK) {
LOG_ERROR("nimble_bond open failed, err=%d", err);
return;
}
// Probe the first record of each fixed-size object type (bonds are written from index 1); a
// stored size differing from this build's struct means the store predates a format change.
size_t sz = 0;
bool mismatch = (nvs_get_blob(handle, "our_sec_1", nullptr, &sz) == ESP_OK && sz != sizeof(struct ble_store_value_sec)) ||
(nvs_get_blob(handle, "peer_sec_1", nullptr, &sz) == ESP_OK && sz != sizeof(struct ble_store_value_sec)) ||
(nvs_get_blob(handle, "cccd_sec_1", nullptr, &sz) == ESP_OK && sz != sizeof(struct ble_store_value_cccd));
bool wiped = false;
if (mismatch) {
LOG_WARN("Wiping incompatible NimBLE bonds (on-disk format changed)");
wiped = nvs_erase_all(handle) == ESP_OK && nvs_commit(handle) == ESP_OK;
if (!wiped) {
LOG_ERROR("Failed to erase nimble_bond namespace");
}
}
nvs_close(handle);
if (wiped) {
LOG_INFO("Restarting after NimBLE bond cleanup");
ESP.restart();
}
}
#endif
// Debugging options: careful, they slow things down quite a bit!
// #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration
// #define DEBUG_NIMBLE_ON_WRITE_TIMING // uncomment to time onWrite duration
// #define DEBUG_NIMBLE_NOTIFY // uncomment to enable notify logging
#define NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE 3
#define NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE 3
BLECharacteristic *fromNumCharacteristic;
BLECharacteristic *BatteryCharacteristic;
static int lastBatteryLevel = -1; // last value written to 0x2A19, to skip redundant writes/notifies
BLECharacteristic *logRadioCharacteristic;
BLEServer *bleServer;
static bool passkeyShowing;
static std::atomic<uint16_t> nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection"
// Set by onDisconnect to defer (re)starting advertising to the main task. A stale-bond reconnect
// triggers a MIC failure + NimBLE host reset; re-entering ble_gap_adv_* from the disconnect
// callback while the host is mid-reset crashes (LoadProhibited), so the main task does it instead.
static std::atomic<bool> pendingStartAdvertising{false};
// Set by deinit() before it disconnects. Makes onRead bail immediately instead of arming the
// up-to-20s wait, so a read arriving mid-teardown can't pin the NimBLE task and stall the disconnect.
static std::atomic<bool> bleDraining{false};
static void clearPairingDisplay()
{
if (!passkeyShowing) {
return;
}
passkeyShowing = false;
#if HAS_SCREEN
if (screen) {
screen->endAlert();
}
#endif
}
class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
{
/*
CAUTION: There's a lot going on here and lots of room to break things.
This NimbleBluetooth.cpp file does some tricky synchronization between the NimBLE FreeRTOS task (which runs the onRead and
onWrite callbacks) and the main task (which runs runOnce and the rest of PhoneAPI).
The main idea is to add a little bit of synchronization here to make it so that the rest of the codebase doesn't have to
know about concurrency and mutexes, and can just run happily ever after as a cooperative multitasking OSThread system, where
locking isn't something that anyone has to worry about too much! :)
We achieve this by having some queues and mutexes in this file only, and ensuring that all calls to getFromRadio and
handleToRadio are only made from the main FreeRTOS task. This way, the rest of the codebase doesn't have to worry about
being run concurrently, which would make everything else much much much more complicated.
PHONE -> RADIO:
- [NimBLE FreeRTOS task:] onWrite callback holds fromPhoneMutex and pushes received packets into fromPhoneQueue.
- [Main task:] runOnceHandleFromPhoneQueue in main task holds fromPhoneMutex, pulls packets from fromPhoneQueue, and calls
handleToRadio **in main task**.
RADIO -> PHONE:
- [NimBLE FreeRTOS task:] onRead callback sets onReadCallbackIsWaitingForData flag and polls in a busy loop. (unless
there's already a packet waiting in toPhoneQueue)
- [Main task:] runOnceHandleToPhoneQueue sees onReadCallbackIsWaitingForData flag, calls getFromRadio **in main task** to
get packets from radio, holds toPhoneMutex, pushes the packet into toPhoneQueue, and clears the
onReadCallbackIsWaitingForData flag.
- [NimBLE FreeRTOS task:] onRead callback sees that the onReadCallbackIsWaitingForData flag cleared, holds toPhoneMutex,
pops the packet from toPhoneQueue, and returns it to NimBLE.
MUTEXES:
- fromPhoneMutex protects fromPhoneQueue and fromPhoneQueueSize
- toPhoneMutex protects toPhoneQueue, toPhoneQueueByteSizes, and toPhoneQueueSize
ATOMICS:
- fromPhoneQueueSize is only increased by onWrite, and only decreased by runOnceHandleFromPhoneQueue (or onDisconnect).
- toPhoneQueueSize is only increased by runOnceHandleToPhoneQueue, and only decreased by onRead (or onDisconnect).
- onReadCallbackIsWaitingForData is a flag. It's only set by onRead, and only cleared by runOnceHandleToPhoneQueue (or
onDisconnect).
PRELOADING: see comments in runOnceToPhoneCanPreloadNextPacket about when it's safe to preload packets from getFromRadio.
BLE CONNECTION PARAMS:
- During config, we request a high-throughput, low-latency BLE connection for speed.
- After config, we switch to a lower-power BLE connection for steady-state use to extend battery life.
MEMORY MANAGEMENT:
- We keep packets on the stack and do not allocate heap.
- We use std::array for fromPhoneQueue and toPhoneQueue to avoid mallocs and frees across FreeRTOS tasks.
- Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management.
NOTIFY IS BROKEN:
- Adding BLECharacteristic::PROPERTY_NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards
compatible.
ZERO-SIZE READS:
- Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we
have data.
- During the STATE_SEND_PACKETS phase, it's totally OK to return zero-size reads, as clients are expected to do reads
until they get a 0-byte response.
CROSS-TASK WAKEUP:
- If you call: bluetoothPhoneAPI->setIntervalFromNow(0); to schedule immediate processing of new data,
- Then you should also call: concurrency::mainDelay.interrupt(); to wake up the main loop if it's sleeping.
- Otherwise, you're going to wait ~100ms or so until the main loop wakes up from some other cause.
*/
public:
BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { api_type = TYPE_BLE; }
/* Packets from phone (BLE onWrite callback) */
std::mutex fromPhoneMutex;
std::atomic<size_t> fromPhoneQueueSize{0};
// We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks.
std::array<BLEValue, NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE> fromPhoneQueue{};
/* Packets to phone (BLE onRead callback) */
std::mutex toPhoneMutex;
std::atomic<size_t> toPhoneQueueSize{0};
// We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks.
std::array<std::array<uint8_t, meshtastic_FromRadio_size>, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueue{};
std::array<size_t, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueueByteSizes{};
// The onReadCallbackIsWaitingForData flag provides synchronization between the NimBLE task's onRead callback and our main
// task's runOnce. It's only set by onRead, and only cleared by runOnce.
std::atomic<bool> onReadCallbackIsWaitingForData{false};
/* Statistics/logging helpers */
std::atomic<int32_t> readCount{0};
std::atomic<int32_t> notifyCount{0};
std::atomic<int32_t> writeCount{0};
protected:
virtual int32_t runOnce() override
{
// Service a deferred advertising restart from onDisconnect, gated on ble_hs_synced() so we
// never re-enter the GAP API while the host is still mid-reset.
if (pendingStartAdvertising) {
if (checkIsConnected()) {
pendingStartAdvertising = false; // a new physical connection beat us to it; nothing to do
} else if (ble_hs_synced()) {
pendingStartAdvertising = false;
if (nimbleBluetooth) {
nimbleBluetooth->startAdvertising();
}
} else {
return 200; // host still re-syncing after a reset; retry shortly
}
}
while (runOnceHasWorkToDo()) {
/*
PROCESS fromPhoneQueue BEFORE toPhoneQueue:
In normal STATE_SEND_PACKETS operation, it's unlikely that we'll have both writes and reads to process at the same
time, because either onWrite or onRead will trigger this runOnce. And in STATE_SEND_PACKETS, it's generally ok to
service either the reads or writes first.
However, during the initial setup wantConfig packet, the clients send a write and immediately send a read, and they
expect the read will respond to the write. (This also happens when a client goes from STATE_SEND_PACKETS back to
another wantConfig, like the iOS client does when requesting the nodedb after requesting the main config only.)
So it's safest to always service writes (fromPhoneQueue) before reads (toPhoneQueue), so that any "synchronous"
write-then-read sequences from the client work as expected, even if this means we block onRead for a while: this is
what the client wants!
*/
// PHONE -> RADIO:
runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio
// RADIO -> PHONE:
runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead
}
// the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback
return INT32_MAX;
}
virtual void onConfigStart() override
{
LOG_INFO("BLE onConfigStart");
// Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds)
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
requestHighThroughputConnection(conn_handle);
}
}
virtual void onConfigComplete() override
{
LOG_INFO("BLE onConfigComplete");
// Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
requestLowerPowerConnection(conn_handle);
}
}
bool runOnceHasWorkToDo() { return runOnceHasWorkToPhone() || runOnceHasWorkFromPhone(); }
bool runOnceHasWorkToPhone() { return onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket(); }
bool runOnceToPhoneCanPreloadNextPacket()
{
/*
* PRELOADING getFromRadio RESPONSES:
*
* It's not safe to preload packets if we're in STATE_SEND_PACKETS, because there may be a while between the time we call
* getFromRadio and when the client actually reads it. If the connection drops in that time, we might lose that packet
* forever. In STATE_SEND_PACKETS, if we wait for onRead before we call getFromRadio, we minimize the time window where
* the client might disconnect before completing the read.
*
* However, if we're in the setup states (sending config, nodeinfo, etc), it's safe and beneficial to preload packets into
* toPhoneQueue because the client will just reconnect after a disconnect, losing nothing.
*/
if (!isConnected()) {
return false;
} else if (isSendingPackets()) {
// If we're in STATE_SEND_PACKETS, we must wait for onRead before calling getFromRadio.
return false;
} else {
// In other states, we can preload as long as there's space in the toPhoneQueue.
return toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE;
}
}
void runOnceHandleToPhoneQueue()
{
// Stack buffer for getFromRadio packet
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
size_t numBytes = 0;
if (onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket()) {
numBytes = getFromRadio(fromRadioBytes);
if (numBytes == 0) {
/*
Client expected a read, but we have nothing to send.
In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond
notifies regularly, to make sure they have nothing else to read.
In other states, this is fine **so long as we've already processed pending onWrites first**, because the client
may requesting wantConfig and immediately doing a read.
*/
} else {
// Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible.
if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) {
// Note: the comparison above is safe without a mutex because we are the only method that *increases*
// toPhoneQueueSize. (It's okay if toPhoneQueueSize *decreases* in the NimBLE task meanwhile.)
{ // scope for toPhoneMutex mutex
std::lock_guard<std::mutex> guard(toPhoneMutex);
size_t storeAtIndex = toPhoneQueueSize.load();
memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes);
toPhoneQueueByteSizes[storeAtIndex] = numBytes;
toPhoneQueueSize++;
}
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
LOG_DEBUG("BLE getFromRadio returned numBytes=%u, pushed toPhoneQueueSize=%u", numBytes,
toPhoneQueueSize.load());
#endif
} else {
// Shouldn't happen because the onRead callback shouldn't be waiting if the queue is full!
LOG_ERROR("Shouldn't happen! Drop FromRadio packet, toPhoneQueue full (%u bytes)", numBytes);
}
}
// Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed.
onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push
}
}
bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; }
void runOnceHandleFromPhoneQueue()
{
// Handle packets we received from onWrite from the phone.
if (fromPhoneQueueSize > 0) {
// Note: the comparison above is safe without a mutex because we are the only method that *decreases*
// fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *increases* in the NimBLE task meanwhile.)
LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load());
// Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop.
BLEValue val;
{ // scope for fromPhoneMutex mutex
std::lock_guard<std::mutex> guard(fromPhoneMutex);
val = fromPhoneQueue[0];
// Shift the rest of the queue down
for (uint8_t i = 1; i < fromPhoneQueueSize; i++) {
fromPhoneQueue[i - 1] = fromPhoneQueue[i];
}
// Safe decrement due to onDisconnect
if (fromPhoneQueueSize > 0)
fromPhoneQueueSize--;
}
handleToRadio(val.getData(), val.getLength());
}
}
/**
* Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies)
*/
virtual void onNowHasData(uint32_t fromRadioNum) override
{
PhoneAPI::onNowHasData(fromRadioNum);
#ifdef DEBUG_NIMBLE_NOTIFY
int currentNotifyCount = notifyCount.fetch_add(1);
uint8_t cc = bleServer->getConnectedCount();
// This logging slows things down when there are lots of packets going to the phone, like initial connection:
LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc);
#endif
uint8_t val[4];
put_le32(val, fromRadioNum);
if (!fromNumCharacteristic) // BLE may have been torn down; never notify a freed characteristic
return;
fromNumCharacteristic->setValue(val, sizeof(val));
fromNumCharacteristic->notify();
}
/// Check the current underlying physical link to see if the client is currently connected
virtual bool checkIsConnected() override { return nimbleBluetoothConnHandle.load() != BLE_HS_CONN_HANDLE_NONE; }
void requestHighThroughputConnection(uint16_t conn_handle)
{
/* Request a lower-latency, higher-throughput BLE connection.
This comes at the cost of higher power consumption, so we may want to only use this for initial setup, and then switch to
a slower mode.
See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS
constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple
recommendations.)
Selected settings:
minInterval (units of 1.25ms): 7.5ms = 6 (lower than the Apple recommended minimum, but allows faster when the client
supports it.)
maxInterval (units of 1.25ms): 15ms = 12
latency: 0 (don't allow peripheral to skip any connection events)
timeout (units of 10ms): 6 seconds = 600 (supervision timeout)
These are intentionally aggressive to prioritize speed over power consumption, but are only used for a few seconds at
setup. Not worth adjusting much.
*/
LOG_INFO("BLE requestHighThroughputConnection");
bleServer->updateConnParams(conn_handle, 6, 12, 0, 600);
}
void requestLowerPowerConnection(uint16_t conn_handle)
{
/* Request a lower power consumption (but higher latency, lower throughput) BLE connection.
This is suitable for steady-state operation after initial setup is complete.
See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS
constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple
recommendations.)
Selected settings:
minInterval (units of 1.25ms): 30ms = 24
maxInterval (units of 1.25ms): 50ms = 40
latency: 2 (allow peripheral to skip up to 2 consecutive connection events to save power)
timeout (units of 10ms): 6 seconds = 600 (supervision timeout)
There's an opportunity for tuning here if anyone wants to do some power measurements, but these should allow 10-20 packets
per second.
*/
LOG_INFO("BLE requestLowerPowerConnection");
bleServer->updateConnParams(conn_handle, 24, 40, 2, 600);
}
};
static BluetoothPhoneAPI *bluetoothPhoneAPI;
/**
* Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies)
*/
// Last ToRadio value received from the phone
static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
class NimbleBluetoothToRadioCallback : public BLECharacteristicCallbacks
{
void onWrite(BLECharacteristic *pCharacteristic) override
{
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
// Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls.
int currentWriteCount = bluetoothPhoneAPI->writeCount.fetch_add(1);
#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING
int startMillis = millis();
LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis);
#endif
// Create a BLEValue and populate it with the received data
BLEValue val;
val.setValue(pCharacteristic->getData(), pCharacteristic->getLength());
if (memcmp(lastToRadio, val.getData(), val.getLength()) != 0) {
if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) {
// Note: the comparison above is safe without a mutex because we are the only method that *increases*
// fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.)
memcpy(lastToRadio, val.getData(), val.getLength());
{ // scope for fromPhoneMutex mutexv, pCharacteristic->getLen
// Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible.
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val;
bluetoothPhoneAPI->fromPhoneQueueSize++;
}
// After releasing the mutex, schedule immediate processing of the new packet.
bluetoothPhoneAPI->setIntervalFromNow(0);
concurrency::mainDelay.interrupt(); // wake up main loop if sleeping
#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING
int finishMillis = millis();
LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount,
finishMillis - startMillis, val.getLength());
#endif
} else {
LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount,
val.getLength());
}
} else {
LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.getLength());
}
}
};
class NimbleBluetoothFromRadioCallback : public BLECharacteristicCallbacks
{
void onRead(BLECharacteristic *pCharacteristic) override
{
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1);
int tries = 0;
int startMillis = millis();
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
LOG_DEBUG("BLE onRead(%d): start millis=%d", currentReadCount, startMillis);
#endif
// Is there a packet ready to go, or do we have to ask the main task to get one for us?
if (bluetoothPhoneAPI->toPhoneQueueSize > 0) {
// Note: the comparison above is safe without a mutex because we are the only method that *decreases*
// toPhoneQueueSize. (It's okay if toPhoneQueueSize *increases* in the main task meanwhile.)
// There's already a packet queued. Great! We don't need to wait for onReadCallbackIsWaitingForData.
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
LOG_DEBUG("BLE onRead(%d): packet already waiting, no need to set onReadCallbackIsWaitingForData", currentReadCount);
#endif
} else if (!bleDraining) {
// (If deinit() is tearing the stack down, skip the wait entirely and just return a 0-size
// response below - arming the wait here could pin this NimBLE task for ~20s and stall teardown.)
// Tell the main task that we'd like a packet.
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true;
// Wait for the main task to produce a packet for us, up to about 20 seconds.
// It normally takes just a few milliseconds, but at initial startup, etc, the main task can get blocked for longer
// doing various setup tasks.
// bleDraining lets deinit() release an in-flight wait immediately.
while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && !bleDraining && tries < 4000) {
// Schedule the main task runOnce to run ASAP.
bluetoothPhoneAPI->setIntervalFromNow(0);
concurrency::mainDelay.interrupt(); // wake up main loop if sleeping
if (!bluetoothPhoneAPI->onReadCallbackIsWaitingForData) {
// we may be able to break even before a delay, if the call to interrupt woke up the main loop and it ran
// already
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
LOG_DEBUG("BLE onRead(%d): broke before delay after %u ms, %d tries", currentReadCount,
millis() - startMillis, tries);
#endif
break;
}
// This delay happens in the NimBLE FreeRTOS task, which really can't do anything until we get a value back.
// No harm in polling pretty frequently.
delay(tries < 20 ? 1 : 5);
tries++;
if (tries == 4000) {
LOG_WARN(
"BLE onRead(%d): timeout waiting for data after %u ms, %d tries, giving up and returning 0-size response",
currentReadCount, millis() - startMillis, tries);
}
}
}
// Pop from toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible.
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; // Stack buffer for getFromRadio packet
size_t numBytes = 0;
{ // scope for toPhoneMutex mutex
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
size_t toPhoneQueueSize = bluetoothPhoneAPI->toPhoneQueueSize.load();
if (toPhoneQueueSize > 0) {
// Copy from the front of the toPhoneQueue
memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[0]);
numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0];
// Shift the rest of the queue down
for (uint8_t i = 1; i < toPhoneQueueSize; i++) {
memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(),
bluetoothPhoneAPI->toPhoneQueueByteSizes[i]);
// The above line is similar to:
// bluetoothPhoneAPI->toPhoneQueue[i - 1] = bluetoothPhoneAPI->toPhoneQueue[i]
// but is usually faster because it doesn't have to copy all the trailing bytes beyond
// toPhoneQueueByteSizes[i].
//
// We deliberately use an array here (and pay the CPU cost of some memcpy) to avoid synchronizing dynamic
// memory allocations and frees across FreeRTOS tasks.
bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i];
}
// Safe decrement due to onDisconnect
if (bluetoothPhoneAPI->toPhoneQueueSize > 0)
bluetoothPhoneAPI->toPhoneQueueSize--;
} else {
// nothing in the toPhoneQueue; that's fine, and we'll just have numBytes=0.
}
}
#ifdef DEBUG_NIMBLE_ON_READ_TIMING
int finishMillis = millis();
LOG_DEBUG("BLE onRead(%d): onReadCallbackIsWaitingForData took %u ms, %d tries. numBytes=%d", currentReadCount,
finishMillis - startMillis, tries, numBytes);
#endif
pCharacteristic->setValue(fromRadioBytes, numBytes);
// If we sent something, wake up the main loop if it's sleeping in case there are more packets ready to enqueue.
if (numBytes != 0) {
bluetoothPhoneAPI->setIntervalFromNow(0);
concurrency::mainDelay.interrupt(); // wake up main loop if sleeping
}
}
};
// One log notify per log line shares the msys_1 mbuf pool with the fromNum doorbell and ATT
// responses, so a logging burst starves them; back off on rejection until the pool refills.
static constexpr uint32_t LOG_NOTIFY_BACKOFF_MS = 250;
static std::atomic<uint32_t> lastLogNotifyFailureMs{0};
class NimbleBluetoothLogRadioCallback : public BLECharacteristicCallbacks
{
void onStatus(BLECharacteristic *, Status s, uint32_t) override
{
// ERROR_GATT is the only status meaning the host refused it; the rest never allocated.
if (s == Status::ERROR_GATT)
lastLogNotifyFailureMs.store(millis());
}
};
class NimbleBluetoothSecurityCallback : public BLESecurityCallbacks
{
void onPassKeyNotify(uint32_t passkey) override
{
LOG_INFO("*** Enter passkey %06u on the peer side ***", passkey);
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
bluetoothStatus->updateStatus(&newStatus);
#if HAS_SCREEN
if (screen) {
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
char btPIN[16] = "888888";
snprintf(btPIN, sizeof(btPIN), "%06u", passkey);
int x_offset = display->width() / 2;
int y_offset = display->height() <= 80 ? 0 : 12;
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_MEDIUM);
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
#if !defined(OLED_TINY)
display->setFont(FONT_SMALL);
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
display->drawString(x_offset + x, y_offset + y, "Enter this code");
#endif
display->setFont(FONT_LARGE);
char pin[8];
snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3);
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5;
display->drawString(x_offset + x, y_offset + y, pin);
display->setFont(FONT_SMALL);
char deviceName[64];
snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName());
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5;
display->drawString(x_offset + x, y_offset + y, deviceName);
});
}
#endif
passkeyShowing = true;
}
void onAuthenticationComplete(ble_gap_conn_desc *desc) override
{
// Called on every BLE_GAP_EVENT_ENC_CHANGE, success or failure. A stale-bond reconnect
// yields a *failed* encryption change here -- don't latch a connected/authenticated state
// on a link that is actually being torn down.
if (desc == nullptr || !desc->sec_state.encrypted) {
LOG_WARN("BLE encryption change without an encrypted link; ignoring");
return;
}
LOG_INFO("BLE authentication complete");
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
bluetoothStatus->updateStatus(&newStatus);
clearPairingDisplay();
nimbleBluetoothConnHandle = desc->conn_handle;
}
};
// Reset per-session PhoneAPI and transport state. Runs from onDisconnect, and again from
// setupService() on BLE re-enable because deinit()'s bounded disconnect wait can expire
// before the disconnect event delivers this cleanup (leaving stale queues/state behind).
static void resetBleSessionState()
{
if (bluetoothPhoneAPI) {
bluetoothPhoneAPI->close();
{ // scope for fromPhoneMutex mutex
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
bluetoothPhoneAPI->fromPhoneQueueSize = 0;
}
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false;
{ // scope for toPhoneMutex mutex
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
bluetoothPhoneAPI->toPhoneQueueSize = 0;
}
bluetoothPhoneAPI->readCount = 0;
bluetoothPhoneAPI->notifyCount = 0;
bluetoothPhoneAPI->writeCount = 0;
}
memset(lastToRadio, 0, sizeof(lastToRadio));
nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE;
}
class NimbleBluetoothServerCallback : public BLEServerCallbacks
{
public:
explicit NimbleBluetoothServerCallback(NimbleBluetooth *ble) : ble(ble) {}
private:
NimbleBluetooth *ble;
void onConnect(BLEServer *pServer, struct ble_gap_conn_desc *desc)
{
BLEAddress peer_addr(desc->peer_id_addr);
LOG_INFO("BLE incoming connection %s", peer_addr.toString().c_str());
const uint16_t connHandle = desc->conn_handle;
// With Google Pixel 8 Android devices, this causes ESP32 device crash
// when phone reconnects. Disable this to make progress on the
// Arduino v3 migration while we investigate the Android compatibility
// issue.
#if 0
int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs);
if (dataLenResult == 0) {
LOG_INFO("BLE conn %u requested data length %u bytes", connHandle, kPreferredBleTxOctets);
} else {
LOG_WARN("Failed to raise data length for conn %u, rc=%d", connHandle, dataLenResult);
}
#endif
LOG_INFO("BLE conn %u peer MTU %u (target %u)", connHandle, pServer->getPeerMTU(connHandle), kPreferredBleMtu);
pServer->updateConnParams(connHandle, 6, 12, 0, 200);
}
void onDisconnect(BLEServer *pServer, struct ble_gap_conn_desc *desc)
{
LOG_INFO("BLE disconnected");
if (ble->isDeInit)
return;
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
bluetoothStatus->updateStatus(&newStatus);
clearPairingDisplay();
resetBleSessionState();
// Defer the advertising restart to runOnce (see pendingStartAdvertising): calling
// startAdvertising() here would crash if this disconnect was a host reset.
pendingStartAdvertising = true;
if (bluetoothPhoneAPI) {
bluetoothPhoneAPI->setIntervalFromNow(0);
}
concurrency::mainDelay.interrupt(); // wake the main loop to service the restart
}
};
void NimbleBluetooth::startAdvertising()
{
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->stop();
pAdvertising->reset();
pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
// if (powerStatus->getHasBattery() == 1) {
// pAdvertising->addServiceUUID(BLEUUID((uint16_t)0x180f));
// }
BLEAdvertisementData scan = BLEAdvertisementData();
scan.setName(getDeviceName());
pAdvertising->setScanResponseData(scan);
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
pAdvertising->setMaxPreferred(0x12);
if (!pAdvertising->start(0)) {
LOG_ERROR("BLE failed to start advertising");
} else {
LOG_DEBUG("BLE Advertising started");
}
}
void NimbleBluetooth::shutdown()
{
#ifndef ARCH_ESP32
LOG_INFO("Disable bluetooth");
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->reset();
pAdvertising->stop();
#endif
}
void NimbleBluetooth::deinit()
{
#ifdef ARCH_ESP32
LOG_INFO("Disable bluetooth until reboot");
// BLEDevice::deinit() deletes the BLEServer before nimble_port_stop(); doing that with a live
// connection dispatches synthesized unsubscribe events into the freed server (LoadProhibited),
// so disconnect cleanly first. deinit() runs on the main task; the waits below are bounded.
// bleDraining must be set before we clear the flag / disconnect, so a read arriving now bails
// instead of re-arming the ~20s wait and re-pinning the NimBLE task through the whole teardown.
bleDraining = true;
if (bluetoothPhoneAPI)
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false; // release any in-flight onRead
// isDeInit must stay false here, else onDisconnect early-returns without clearing the handle.
uint16_t connHandle = nimbleBluetoothConnHandle.load();
if (connHandle != BLE_HS_CONN_HANDLE_NONE && bleServer) {
bleServer->disconnect(connHandle);
uint32_t start = millis();
while (nimbleBluetoothConnHandle.load() != BLE_HS_CONN_HANDLE_NONE && Throttle::isWithinTimespanMs(start, 2000))
delay(10);
delay(50);
}
isDeInit = true;
pendingStartAdvertising = false; // stack is going away; don't let runOnce retry the adv restart
#ifdef BLE_LED
digitalWrite(BLE_LED, LED_STATE_OFF);
#endif
BLEDevice::deinit(true);
bleServer = nullptr; // deleted by deinit(); clear the dangling copy
BatteryCharacteristic = nullptr; // freed by deinit; clear so updateBatteryLevel() won't touch it
fromNumCharacteristic = nullptr; // freed by deinit; a late onNowHasData() must not notify freed memory
logRadioCharacteristic = nullptr;
lastBatteryLevel = -1;
// The bounded disconnect wait above can expire before onDisconnect runs, leaving the PhoneAPI
// observer attached with a live state machine; a later mesh packet would then drive onNowHasData()
// into the now-freed characteristics. Detach the observer and reset session state unconditionally.
resetBleSessionState();
#endif
}
bool NimbleBluetooth::isActive()
{
return bleServer != nullptr;
}
bool NimbleBluetooth::isConnected()
{
return nimbleBluetoothConnHandle.load() != BLE_HS_CONN_HANDLE_NONE;
}
int NimbleBluetooth::getRssi()
{
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
uint16_t conn_handle = nimbleBluetoothConnHandle.load();
if (conn_handle == BLE_HS_CONN_HANDLE_NONE) {
return 0; // No active BLE connection
}
int8_t rssi = 0;
const int rc = ble_gap_conn_rssi(conn_handle, &rssi);
if (rc == 0) {
return rssi;
}
LOG_DEBUG("BLE RSSI read failed, rc=%d", rc);
#endif
return 0;
}
void NimbleBluetooth::setup()
{
// Uncomment for testing
// NimbleBluetooth::clearBonds();
LOG_INFO("Init the NimBLE bluetooth module");
// deinit() latches these teardown guards; clear them so a re-init on the same boot (e.g. an
// admin disable-bluetooth followed by re-enable) doesn't leave onRead stuck draining or
// onDisconnect early-returning without clearing the connection handle.
bleDraining = false;
isDeInit = false;
#ifdef ARCH_ESP32
// Runs before BLEDevice::init() reads the bond store, but logs after the "Init" line above so
// any bond-cleanup output doesn't appear to precede the module init.
purgeIncompatibleBleBonds(); // wipe bonds left in an incompatible on-disk format (post-upgrade)
#endif
BLEDevice::init(getDeviceName());
BLEDevice::setPower(ESP_PWR_LVL_P9);
int mtuResult = BLEDevice::setMTU(kPreferredBleMtu);
if (mtuResult == 0) {
LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu);
} else {
LOG_WARN("Unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult);
}
// BLESecurity only forwards to static NimBLEDevice setters; a stack instance suffices.
BLESecurity security;
security.setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
security.setRespEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
// Set IO capability to DisplayOnly for MITM authentication
security.setCapability(ESP_IO_CAP_OUT);
// Set the passkey
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
LOG_INFO("Use random passkey");
security.setPassKey(false); // generate a random passkey
} else {
LOG_INFO("Use fixed passkey");
security.setPassKey(true, config.bluetooth.fixed_pin);
}
// Enable authorization requirements:
// - bonding: true (for persistent storage of the keys)
// - MITM: true (enables Man-In-The-Middle protection for password prompts)
// - secure connection: true (enables secure connection for encryption)
security.setAuthenticationMode(true, true, true);
} else {
// No IO capability for no PIN mode
security.setCapability(ESP_IO_CAP_NONE);
// No PIN mode: no MITM protection
security.setAuthenticationMode(true, false, false);
}
// Statics: setup() re-runs on BLE re-enable, and the library never frees these
// caller-owned callback objects, so register the same instances every cycle.
static NimbleBluetoothSecurityCallback securityCallbacks;
BLEDevice::setSecurityCallbacks(&securityCallbacks);
bleServer = BLEDevice::createServer();
// BLEDevice::createServer calls ble_svc_gap_init, which resets the device
// name to default, so set it again.
int nameRc = ble_svc_gap_device_name_set(BLEDevice::getDeviceName().c_str());
if (nameRc != 0) {
LOG_ERROR("ble_svc_gap_device_name_set: rc=%d %s", nameRc, BLEUtils::returnCodeToString(nameRc));
}
static NimbleBluetoothServerCallback serverCallbacks(this); // safe: NimbleBluetooth is a never-deleted singleton
bleServer->setCallbacks(&serverCallbacks);
setupService();
startAdvertising();
}
void NimbleBluetooth::setupService()
{
BLEService *bleService = bleServer->createService(MESH_SERVICE_UUID);
BLECharacteristic *ToRadioCharacteristic;
BLECharacteristic *FromRadioCharacteristic;
// Define the characteristics that the app is looking for
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE);
// Allow notifications so phones can stream FromRadio without polling.
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ);
fromNumCharacteristic =
bleService->createCharacteristic(FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ);
logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_READ);
} else {
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_WRITE_AUTHEN |
BLECharacteristic::PROPERTY_WRITE_ENC);
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_READ_AUTHEN |
BLECharacteristic::PROPERTY_READ_ENC);