-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtestBLESerial.ino
More file actions
2298 lines (2047 loc) · 90.2 KB
/
testBLESerial.ino
File metadata and controls
2298 lines (2047 loc) · 90.2 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
// *******************************************************
*************************************************************
// Main File: testBLESerial.ino
//
// This program handles data generation and BLE serial communication.
//
// Commands:
//
// interval <value>: Sets the data generation interval to the specified value in micro seconds.
// frequency <value> sets the frequency of the sine, saw tooth or squarewave in Hz
// scenario <value>: Changes the scenario to the specified value (1 to 5).
//
// pause: Pauses the data generation.
// resume: Resumes the data generation if it was paused.
//
// ********************************************************************************************************************
#define VERSION_STRING "NUS Tester 1.0.6"
// *****************************************************************************************************************
#include <inttypes.h> // for PRIu32 in printf
#include <cmath>
#include "RingBuffer.h"
// for ble_gap_set_prefered_le_phy
#include <NimBLEDevice.h>
extern "C" {
#include "host/ble_gap.h" // ble_gap_* (conn params, PHY, DLE)
#include "host/ble_hs_adv.h" // BLE_HS_ADV_F_* flags for adv data
#include "host/ble_hs.h" // BLE_HS_EDONE for notivy backoff
}
// for mac address
#if __has_include(<esp_mac.h>)
#include <esp_mac.h> // IDF 5.x / Arduino core 3.x
#else
#include <esp_system.h> // IDF 4.x / Arduino core 2.x
#endif
// *****************************************************************************************************************
// Optimizations: SPEED - RANGE - LOWPOWER
//
// DEBUG ON/OFF
// Secure connections ON/OFF
//
#define SPEED
//#undef SPEED
//
// min throughput, min power
//
//#define LOWPOWER
#undef LOWPOWER
//
// if not SPEED and if not LOWPOWER
// results in LONGRANGE
//
// DEBUG verbose output on serial and BLE port for debugging
// INFO output on Serial about system changes
// WARNING output on Serial about issues
// ERROR output on Serial about errors
// For any issues select DEBUG
#define NONE 0
#define WANTED 1
#define ERROR 1
#define WARNING 2
#define INFO 3
#define DEBUG 4
//
// Avoid DEBUG for SPEED
#define DEBUG_LEVEL DEBUG
//
// require pairing and encryption
//
// #define BLE_SECURE
#undef BLE_SECURE
// *****************************************************************************************************************
// ===== SERIAL ======
inline constexpr unsigned long BAUDRATE = 2'000'000UL;
// ===== Buffer =====
#if defined(SPEED)
inline constexpr size_t TX_BUFFERSIZE = 4096;
#elif defined(LOWPOWER)
inline constexpr size_t TX_BUFFERSIZE = 1024;
#else
inline constexpr size_t TX_BUFFERSIZE = 2048;
#endif
inline constexpr size_t highWaterMark = TX_BUFFERSIZE*3/4; // When to throttle data generation
uint16_t lowWaterMark = TX_BUFFERSIZE/4; // When to resume data generation
// RX buffer for BLE writes (commands)
#if defined(SPEED)
inline constexpr size_t RX_BUFFERSIZE = 1024;
#elif defined(LOWPOWER)
inline constexpr size_t RX_BUFFERSIZE = 512;
#else
inline constexpr size_t RX_BUFFERSIZE = 1024;
#endif
RingBuffer<char, TX_BUFFERSIZE> txBuffer; // Should be a few times larger than the BLE payload size
RingBuffer<char, RX_BUFFERSIZE> rxBuffer; // Buffer for incoming BLE commands, should be large enough to hold on BLE package
// *****************************************************************************************************************
// ===== GAP / Connection preferences =====
#define DEVICE_NAME "MediBrick"// Name shown when BLE scans for devices
#define BLE_APPEARANCE 0x0540 // Generic Sensor, https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/
// ===== GATT / ATT payload sizing =====
inline constexpr uint16_t BLE_MTU = 517; // Max size in bytes to send at once. MAX ESP 517, Android 512, Nordic 247, Regular size is 23
inline constexpr uint16_t ATT_HDR_BYTES = 3;
inline constexpr uint16_t FRAME_SIZE = BLE_MTU-ATT_HDR_BYTES; // Payload is MTU minus ATT header size
inline constexpr int8_t RSSI_LOW_THRESHOLD = -80; // low power threshold (increase power if in LOWPOWER mode)
inline constexpr int8_t RSSI_FAST_THRESHOLD = -65; // Switch back to 2M/1M
inline constexpr int8_t RSSI_HYSTERESIS = 4; // Prevent oscillation
inline constexpr int8_t RSSI_S8_THRESHOLD = -82; // go S=8 below this
inline constexpr int8_t RSSI_S2_THRESHOLD = -75; // go S=2 below this
inline constexpr uint32_t RSSI_INTERVAL_US = 500000UL; // 0.5s
// ===== LL (Link-Layer) performance knobs =====
// If MTU is larger than LL size the GATT packets need to be fragmented on the link layer
// default LL size is 27
// maximum is 251
// Common BLE 4.2/5.0 DLE targets is 244
inline constexpr uint16_t LL_DEF_TX_OCTETS = 27; // 27..251
inline constexpr uint16_t LL_CONS_TX_OCTETS = 244; // 27..251
inline constexpr uint16_t LL_MAX_TX_OCTETS = 251; // 27..251
#if defined(SPEED)
// max speed
inline constexpr uint16_t LL_TX_OCTETS = LL_MAX_TX_OCTETS;
#elif defined(LOWPOWER)
// low power
inline constexpr uint16_t LL_TX_OCTETS = LL_DEF_TX_OCTETS;
#else
// long range
inline constexpr uint16_t LL_TX_OCTETS = LL_CONS_TX_OCTETS;
#endif
inline constexpr uint16_t LL_TIME_1M_US = 2120; // for 1M PHY
inline constexpr uint16_t LL_TIME_2M_US = 1060; // for 2M PHY
inline constexpr uint16_t LL_TIME_CODED_S2_US = 4240; // for Coded PHY (S2)
inline constexpr uint16_t LL_TIME_CODED_S8_US = 16960; // for Coded PHY (S8)
// ===== Security / Pairing =====
inline constexpr uint32_t BLE_PASSKEY_VALUE =123456; // Generic static Passkey
// ===== UUIDs =====
// Nordic UART Serial (NUS)
inline constexpr const char SERVICE_UUID[] = {"6E400001-B5A3-F393-E0A9-E50E24DCCA9E"};
inline constexpr const char CHARACTERISTIC_UUID_RX[]= {"6E400002-B5A3-F393-E0A9-E50E24DCCA9E"};
inline constexpr const char CHARACTERISTIC_UUID_TX[]= {"6E400003-B5A3-F393-E0A9-E50E24DCCA9E"};
// ===== BLE Optimizations =====
// helpers to convert from human units to BLE units
static constexpr uint16_t itvl_us(uint32_t us) { return (uint16_t)((us * 4) / 5000); } // is in units of 1.25ms
static constexpr uint16_t tout_ms(uint32_t ms) { return (uint16_t)(ms / 10); } // is in units of 10ms
// connection interval, latency and supervision timeout
#if defined(SPEED)
// aggressive speed
#define MIN_BLE_INTERVAL itvl_us( 7500) // Minimum connection interval in microseconds 7.5ms to 4s
#define MAX_BLE_INTERVAL itvl_us(10000) // Maximum connection interval in µs 7.5ms to 4s
#define BLE_SLAVE_LATENCY 0 // Slave latency: number of connection events that can be skipped
#define BLE_SUPERVISION_TIMEOUT tout_ms(4000) // Supervision timeout in milli seconds 100ms to 32s, needs to be larger than 2 * (latency + 1) * (max_interval_ms)
#elif defined(LOWPOWER)
// low power
#define MIN_BLE_INTERVAL itvl_us(60000) // 60ms
#define MAX_BLE_INTERVAL itvl_us(120000) // 120ms
#define BLE_SLAVE_LATENCY 8 // can raise
#define BLE_SUPERVISION_TIMEOUT tout_ms( 6000) // 6s
#else
// long range
#define MIN_BLE_INTERVAL itvl_us(30000) // 30ms
#define MAX_BLE_INTERVAL itvl_us(60000) // 60ms
#define BLE_SLAVE_LATENCY 2 // some dozing
#define BLE_SUPERVISION_TIMEOUT tout_ms(6000) // 6s
#endif
// dBm levels similar to Bluedroid's ESP_PWR_LVL_* names:
#define BLE_TX_DBM_N12 ( -12)
#define BLE_TX_DBM_N9 (-9)
#define BLE_TX_DBM_N6 (-6)
#define BLE_TX_DBM_N3 (-3)
#define BLE_TX_DBM_0 (0)
#define BLE_TX_DBM_P3 (3)
#define BLE_TX_DBM_P6 (6)
#define BLE_TX_DBM_P9 (9) // ~max on many ESP32s
// Scopes roughly matching ESP_BLE_PWR_TYPE_*
#define PWR_ALL NimBLETxPowerType::All
#define PWR_ADV NimBLETxPowerType::Advertising
#define PWR_SCAN NimBLETxPowerType::Scan
#define PWR_CONN NimBLETxPowerType::Connections
// Optional: make the key distribution explicit (same idea as init_key/rsp_key in Bluedroid)
inline constexpr uint8_t KEYDIST_ENC = 0x01; // BLE_SM_PAIR_KEY_DIST_ENC
inline constexpr uint8_t KEYDIST_ID = 0x02; // BLE_SM_PAIR_KEY_DIST_ID
inline constexpr uint8_t KEYDIST_SIGN = 0x04; // BLE_SM_PAIR_KEY_DIST_SIGN
inline constexpr uint8_t KEYDIST_LINK = 0x08; // BLE_SM_PAIR_KEY_DIST_LINK
// ===== BLE globals =====
static NimBLEServer *pServer = nullptr; // BLE Server
static NimBLECharacteristic *pTxCharacteristic = nullptr; // Transmission BLE Characteristic
static NimBLECharacteristic *pRxCharacteristic = nullptr; // Reception BLE Characteristic
static NimBLEAdvertising *pAdvertising = nullptr; // Advertising
volatile bool deviceConnected = false; // Status
volatile bool clientSubscribed = false; // set when subscribed
const uint32_t passkey = BLE_PASSKEY_VALUE; // Define your passkey here
volatile uint16_t txChunkSize = FRAME_SIZE;
volatile uint16_t mtu = txChunkSize+3;
int8_t rssi = 0; // BLE signal strength
int8_t f_rssi = -50; // Filtered BLE signal strength
volatile bool phyIs2M = false;
volatile bool phyIsCODED = false;
static std::string deviceMac;
/*
NOTE: NimBLE-Arduino does not expose an API to read back whether CODED PHY
negotiated S=2 vs S=8 after a generic coded request. We track 'desiredCodedScheme'
(what we asked for) and assume the controller honored it. If the controller
silently falls back (e.g. to S=8), timing estimates may be optimistic.
*/
volatile uint8_t desiredCodedScheme = 8; // what we asked for: 0=none, 2 (S=2), 8 (S=8)
volatile uint8_t codedScheme = 8; // what we got, currently wee can not read it back, so we assume what we asked for
volatile uint16_t llTimeUS = LL_TIME_1M_US;
volatile uint16_t g_connectHandle = BLE_HS_CONN_HANDLE_NONE; // connection handle
volatile uint16_t g_ll_tx_octets = LL_TX_OCTETS;
volatile uint16_t g_ll_rx_octets = LL_TX_OCTETS;
volatile uint16_t g_ll_tx_time_us = LL_TIME_1M_US;
volatile uint16_t g_ll_rx_time_us = LL_TIME_1M_US;
static char pending[FRAME_SIZE]; // temp keep for sent frame
volatile bool generationAllowed = true; // data producer is allowed to generate
volatile uint32_t sendInterval = 200; // start fast
volatile int mtuRetryCount = 0; // number of times we retried to obtain MTU
const int mtuRetryMax = 3; // max number of times we retry to obtain MTU
// ===== Tx backoff/throttle =====
inline constexpr uint16_t PROBE_AFTER_SUCCESSES = 64; // wait this many clean sends before probing faster
inline constexpr uint16_t PROBE_CONFIRM_SUCCESSES = 48; // accept probe only after this many clean sends
inline constexpr uint32_t PROBE_STEP_US = 10; // absolute probe step
inline constexpr uint32_t PROBE_STEP_PCT = 2; // or % of current interval (use the larger of the two)
inline constexpr uint8_t LKG_ESCALATE_AFTER_FAILS = 3; // if LKG last known good fails this many times in a row, relax it
inline constexpr uint32_t LKG_ESCALATE_NUM = 103; // ×1.03
inline constexpr uint32_t LKG_ESCALATE_DEN = 100;
inline constexpr int COOL_SUCCESS_REQUIRED = 64; // successes before probing resumes after a backoff
inline constexpr uint32_t ESCALATE_COOLDOWN_US = 1000000; // 1 s
inline constexpr uint32_t TIMEOUT_BACKOFF_NUM = 6; // ×1.20 on timeout
inline constexpr uint32_t TIMEOUT_BACKOFF_DEN = 5;
volatile uint32_t lkgInterval = 0; // last-known-good interval
volatile bool probing = false; // currently probing lkg
volatile uint16_t probeSuccesses = 0;
volatile uint8_t probeFailures = 0;
volatile uint8_t lkgFailStreak = 0;
volatile unsigned long lastEscalateAt = 0;
volatile uint32_t minSendIntervalUs = 200; // floor in µs
const uint32_t maxSendIntervalUs =
#if defined(LOWPOWER)
// low power:
100000; // 100 ms – 500 ms typical for low power
#elif defined(SPEED)
// max speed
5000; // 5 ms cap for aggressive streams
#else
// long range
30000; // 30 ms balanced
#endif
volatile uint64_t lastSend = 0; // last time data was sent/notify
volatile size_t pendingLen = 0; // length data that we attempted to send
volatile bool txOkFlag = false; // no issues last data was sent
volatile int successStreak = 0; // number of consecutive successful sends
volatile int cooldownSuccess = 0; // successes since last backoff
volatile bool recentlyBackedOff = false; // gate decreases after congestion
volatile uint8_t badDataRetries = 0; // EBADDATA soft fallback attempts
inline constexpr uint8_t badDataMaxRetries = 8; // limit EBADDATA chunk shrink attempts
// *****************************************************************************************************************
// ===== General Globals =====
unsigned long currentTime;
unsigned long interval = 10000; // Default interval at which to generate data
unsigned long blinkInterval = 1000;
unsigned long lastBlink;
static bool userSetInterval = false;
static bool fastMode = false; // true if scenario 11 or 20 (run as fast as possible)
const int ledPin = LED_BUILTIN;
int ledState = LOW;
int samplerate = 1000;
bool paused = true; // Flag to pause the data generation
String receivedCommand = "";
volatile bool commandPending = false; // Flag to indicate if a command is waiting to be processed
char data[1024];
unsigned long lastDataGenerationTime= 0; // Last time data was produced
unsigned long lastRssiPoll = 0;
// ===== Data generation globals =====
int scenario = 6; // stereo sine wave
float frequency = 100.0; // frequency (Hz)
float amplitude = 1024; // amplitude
static float loc = 0;
inline constexpr size_t TABLESIZE = 512; // Number of samples in one full cycle for sine, sawtooth etc
inline constexpr unsigned long SPEEDTEST_DEFAULT_INTERVAL_US = 20; // e.g. for ESP32
int16_t signalTable[TABLESIZE];
// ===== Add timing constraints and helpers (place near other globals) =====
inline constexpr int MIN_SAMPLERATE_HZ = 1;
inline constexpr int MAX_SAMPLERATE_HZ = 200000; // 200kHz, limit for Stereo on Teensy is like 80ksps
inline constexpr unsigned long MIN_INTERVAL_US = 100; // 0.1 ms minimum frame period
inline constexpr unsigned long MAX_INTERVAL_US = 500000; // 500 ms maximum frame period
// ===== BLE Speedtester Globals =====
unsigned long lastBLETime = 0; // Last time data was produced
unsigned long lastCounts = 10000000;
unsigned long currentCounts = 10000000; // Number of lines sent
unsigned long countsPerSecond = 0;
// ===== BLE Mono/Stereo Globals =====
// Fixed-point phase config
constexpr uint32_t ilog2_u32(uint32_t v) {
uint32_t n = 0;
while (v > 1) { v >>= 1; ++n; }
return n;
}
constexpr uint32_t INT_BITS = ilog2_u32((uint32_t)TABLESIZE); // e.g. 9 for 512
constexpr uint32_t FRAC = 32u - INT_BITS; // e.g. 23 for 512
constexpr uint64_t PHASE_MOD = (uint64_t)TABLESIZE << FRAC;
constexpr uint64_t PHASE_MASK = PHASE_MOD - 1ull;
static uint32_t phase = 0;
float stereo_drift_hz = 0.2f; // adjust for faster/slower relative phase sweep
static uint32_t stereo_offset_fp = 0; // fixed‑point phase offset accumulator (8.24)
static inline uint32_t phase_inc_from_hz(float hz, int sr) {
if (hz <= 0.0f || sr <= 0) return 0u;
return (uint32_t)((((uint64_t)TABLESIZE << FRAC) * (double)hz) / (double)sr);
}
static inline uint32_t advance_phase(uint32_t p, uint32_t inc) {
return (p + inc) & (uint32_t)PHASE_MASK;
}
static inline int table_index(uint32_t p) {
return (int)((p >> FRAC) & (TABLESIZE - 1));
}
// ===============================================================================================================================================================
// Helpers for TX sizing & pacing
// ===============================================================================================================================================================
static inline uint16_t compute_txChunkSize(uint16_t mtu_val, uint16_t ll_octets) {
// Base payload is MTU-3 (ATT header excluded). For SPEED profile, allow up to
// 2 LL PDUs to reduce per-notify overhead; otherwise keep within a single LL PDU.
if (mtu_val <= 3) return 20;
const uint16_t att_payload = (uint16_t)(mtu_val - 3);
// Max payload fitting N LL PDUs: N*ll_octets - (L2CAP 4 + ATT 3)
const uint16_t one_pdu_max = (ll_octets > 7) ? (uint16_t)(ll_octets - 7) : 20;
#if defined(SPEED)
//const uint32_t two_pdu_calc = (uint32_t)ll_octets * 2u;
//const uint16_t two_pdu_max = (two_pdu_calc > 7u) ? (uint16_t)(two_pdu_calc - 7u) : one_pdu_max;
//uint16_t llLimit = (two_pdu_max > one_pdu_max) ? two_pdu_max : one_pdu_max;
uint16_t llLimit = one_pdu_max;
#else
uint16_t llLimit = one_pdu_max;
#endif
// Final chunk is limited by both ATT MTU and chosen LL limit
return (att_payload < llLimit) ? att_payload : llLimit;
}
static inline uint32_t compute_minSendIntervalUs(uint16_t chunkSize, uint16_t ll_octets, uint16_t ll_time_us) {
// Estimate number of link-layer PDUs (ceil divide), then multiply by per-PDU time (+10% guard)
uint16_t l2cap_plus_att = (uint16_t)(chunkSize + 4 /*L2CAP hdr*/ + 3 /*ATT hdr*/);
uint16_t num_ll_pd = (uint16_t)((l2cap_plus_att + ll_octets - 1) / ll_octets);
// mode-specific guard
#if defined(SPEED)
const uint32_t guard_num = 103, guard_den = 100; // +3%
#elif defined(LOWPOWER)
const uint32_t guard_num = 110, guard_den = 100; // +10%
#else // LONGRANGE
const uint32_t guard_num = 115, guard_den = 100; // +15%
#endif
return (uint32_t)num_ll_pd * (uint32_t)ll_time_us * guard_num / guard_den;
}
static inline size_t update_lowWaterMark(size_t chunkSize) {
size_t lw = 2 * (size_t)chunkSize; // up to two outbound packets buffered
size_t cap = TX_BUFFERSIZE / 4; // don't let low water exceed 25% of buffer
if (lw > cap) lw = cap;
if (lw < chunkSize) lw = chunkSize; // never below one chunk
return lw;
}
static inline void reset_tx_ramp(bool forceToMin) {
probing = false;
probeSuccesses = 0;
probeFailures = 0;
lkgFailStreak = 0;
recentlyBackedOff = false;
cooldownSuccess = 0;
successStreak = 0;
if (forceToMin || sendInterval == 0 || sendInterval > minSendIntervalUs) {
sendInterval = minSendIntervalUs;
} else if (sendInterval < minSendIntervalUs) {
sendInterval = minSendIntervalUs;
}
lkgInterval = sendInterval;
}
static inline void recompute_tx_timing() {
// update chunk size and send interval floor
txChunkSize = compute_txChunkSize(mtu, g_ll_tx_octets);
minSendIntervalUs = compute_minSendIntervalUs(txChunkSize, g_ll_tx_octets, llTimeUS);
if (sendInterval < minSendIntervalUs) sendInterval = minSendIntervalUs;
lowWaterMark = update_lowWaterMark(txChunkSize);
// Seed/repair last-known-good after timing changes
if (lkgInterval == 0 || lkgInterval < minSendIntervalUs) lkgInterval = sendInterval;
if (!probing && lkgInterval > sendInterval) lkgInterval = sendInterval;
size_t used = txBuffer.available();
if (pendingLen == 0 && used <= lowWaterMark) {
generationAllowed = true;
}
reset_tx_ramp(true);
}
static inline void update_ll_time() {
// update llTimeUS based on current PHY,
// consider 1M, 2M and CODED
// then recompute tx timing
if (phyIsCODED) {
llTimeUS = (codedScheme == 2) ? LL_TIME_CODED_S2_US : LL_TIME_CODED_S8_US;
} else if (phyIs2M) {
llTimeUS = LL_TIME_2M_US;
} else {
llTimeUS = LL_TIME_1M_US;
}
recompute_tx_timing();
}
// ===============================================================================================================================================================
// BLE Service and Characteristic Callbacks
// ===============================================================================================================================================================
// ----------------
// Server Callbacks
// ----------------
class ServerCallbacks : public NimBLEServerCallbacks {
private:
static const char* hciDisconnectReasonStr(uint8_t r) {
switch (r) {
case 0x08: return "Connection Timeout";
case 0x10: return "Remote User Terminated";
case 0x13: return "Remote User Terminated"; // 0x13 (same meaning)
case 0x16: return "Connection Terminated by Local Host";
case 0x3B: return "Unacceptable Connection Parameters";
case 0x3D: return "MIC Failure";
case 0x3E: return "Connection Failed to be Established";
default: return "Unknown";
}
}
public:
void onConnect(NimBLEServer* pServer, NimBLEConnInfo &connInfo) override {
deviceConnected = true;
g_connectHandle = connInfo.getConnHandle();
// Reset EBADDATA fallback budget
badDataRetries = 0;
// We can use the connection handle here to ask for different connection parameters.
pServer->updateConnParams(
g_connectHandle,
MIN_BLE_INTERVAL,
MAX_BLE_INTERVAL,
BLE_SLAVE_LATENCY,
BLE_SUPERVISION_TIMEOUT
);
//PHY and DLE tuning
#if defined(SPEED)
// Max speed
// Ask for 2M (if not supported, rc will be non-zero; that's OK)
(void)ble_gap_set_prefered_le_phy(g_connectHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, 0);
#elif defined (LOWPOWER)
// Low Power
(void)ble_gap_set_prefered_le_phy(g_connectHandle, BLE_GAP_LE_PHY_1M_MASK, BLE_GAP_LE_PHY_1M_MASK, 0);
#else
// Long Range
(void)ble_gap_set_prefered_le_phy(
g_connectHandle,
BLE_GAP_LE_PHY_CODED_MASK,
BLE_GAP_LE_PHY_CODED_MASK,
(desiredCodedScheme == 8) ? BLE_GAP_LE_PHY_CODED_S8 : BLE_GAP_LE_PHY_CODED_S2);
#endif
// Read the PHY actually in use
uint8_t txPhy = 0, rxPhy = 0;
if (ble_gap_read_le_phy(g_connectHandle, &txPhy, &rxPhy) == 0) {
// Pick the correct LL time based on the negotiated PHY
phyIs2M = (txPhy == BLE_HCI_LE_PHY_2M) && (rxPhy == BLE_HCI_LE_PHY_2M);
phyIsCODED = (txPhy == BLE_HCI_LE_PHY_CODED) && (rxPhy == BLE_HCI_LE_PHY_CODED);
if (phyIsCODED) {
// Reading coding scheme is not supported
//
// Coded PHY: check if S=8 or S=2 (default to S=8 if we can't read)
// uint8_t codedPhyOptions = 0;
// if (ble_gap_read_phy_options(g_connectHandle, &codedPhyOptions) == 0) {
// // S=8 is more robust but slower than S=2
// codedScheme = (codedPhyOptions & BLE_GAP_LE_PHY_CODED_S2) ? 2 : 8;
// } else {
// codedScheme = 8; // assume S=8 if we can't read
// }
codedScheme = desiredCodedScheme;
} else {
codedScheme = 0;
}
update_ll_time(); // ll time depends on 1M, 2M and coded
// Apply DLE for this link
(void)ble_gap_set_data_len(g_connectHandle, LL_TX_OCTETS, llTimeUS);
// reset controller
probing = false; probeSuccesses = 0; probeFailures = 0; lkgFailStreak = 0;
recentlyBackedOff = false; cooldownSuccess = 0; successStreak = 0;
lkgInterval = sendInterval;
} else {
// Fallback: assume 1M timings if we couldn't read
(void)ble_gap_set_data_len(g_connectHandle, LL_TX_OCTETS, LL_TIME_1M_US);
}
#if defined(LOWPOWER)
// Adjust power if too low
// If available in your build: IDF/NimBLE has ble_gap_conn_rssi()
int8_t tmp_rssi = 0;
if (ble_gap_conn_rssi(g_connectHandle, &tmp_rssi) == 0) {
rssi = tmp_rssi;
f_rssi = rssi;
if (rssi < RSSI_LOW_THRESHOLD) {
NimBLEDevice::setPower(BLE_TX_DBM_0, PWR_CONN); // boost
} else if (rssi > RSSI_FAST_THRESHOLD) {
NimBLEDevice::setPower(BLE_TX_DBM_N6, PWR_CONN); // trim
}
}
#endif
// Start Pairing
#if defined(BLE_SECURE)
NimBLEDevice::startSecurity(g_connectHandle);
#endif
#if DEBUG_LEVEL >= INFO
Serial.printf("Client [%s] is connected.\r\n", connInfo.getAddress().toString().c_str());
#endif
}
// When a client disconnects
void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo &connInfo, int reason) override {
g_connectHandle = BLE_HS_CONN_HANDLE_NONE;
phyIs2M = false;
phyIsCODED = false;
codedScheme = 0;
update_ll_time(); // back to 1M defaults
deviceConnected = false;
clientSubscribed = false;
generationAllowed = false;
pendingLen = 0; // drop in-flight frame (or keep if you want to resend on next conn)
successStreak = 0;
sendInterval = maxSendIntervalUs; // restart conservatively
NimBLEDevice::startAdvertising(); // Restart advertising immediately
badDataRetries = 0;
#if DEBUG_LEVEL >= INFO
uint8_t hci = (uint8_t)(reason & 0xFF);
Serial.printf("Client [%s] is disconnected (raw=%d, %s). Advertising restarted.\r\n",
connInfo.getAddress().toString().c_str(), reason, hciDisconnectReasonStr(hci));
#endif
}
// MTU updated
void onMTUChange(uint16_t m, NimBLEConnInfo& connInfo) override {
mtu = m;
recompute_tx_timing();
probing = false; probeSuccesses = 0; probeFailures = 0; lkgFailStreak = 0;
recentlyBackedOff = false; cooldownSuccess = 0; successStreak = 0;
lkgInterval = sendInterval;
badDataRetries = 0;
#if DEBUG_LEVEL >= INFO
Serial.printf("MTU updated: %u (conn=%u), tx chunk size=%u, min send interval=%u\r\n",
m, connInfo.getConnHandle(), txChunkSize, minSendIntervalUs);
#endif
}
// Security callbacks
// Passkey display
uint32_t onPassKeyDisplay() override {
#if DEBUG_LEVEL >= WANTED
Serial.printf("Server Passkey Display: %u\r\n", BLE_PASSKEY_VALUE);
#endif
// This should return a random 6 digit number for security
// or make your own static passkey as done here.
return BLE_PASSKEY_VALUE;
}
// Request to confirm a passkey value match
void onConfirmPassKey(NimBLEConnInfo& connInfo, uint32_t pass_key) override {
/** Inject false if passkeys don't match. */
if (pass_key == BLE_PASSKEY_VALUE) {
NimBLEDevice::injectConfirmPasskey(connInfo, true);
#if DEBUG_LEVEL >= INFO
Serial.printf("The passkey: %" PRIu32 " matches\r\n", pass_key);
#endif
} else {
NimBLEDevice::injectConfirmPasskey(connInfo, false);
#if DEBUG_LEVEL >= INFO
Serial.printf("The passkey: %" PRIu32 "does not match\r\n", pass_key);
#endif
}
}
// Authentication complete
void onAuthenticationComplete(NimBLEConnInfo& connInfo) override {
// Check that encryption was successful, if not we disconnect the client
// When security is turned off this will not be called
if (!connInfo.isEncrypted()) {
NimBLEDevice::getServer()->disconnect(connInfo.getConnHandle());
#if DEBUG_LEVEL >= WARNING
Serial.printf("Encrypt connection failed - disconnecting client\r\n");
#endif
return;
}
#if DEBUG_LEVEL >= INFO
Serial.printf("Secured connection to: %s\r\n", connInfo.getAddress().toString().c_str());
#endif
}
} serverCallBacks;
// ----------------
// RX Callbacks
// ----------------
class RxCallback : public NimBLECharacteristicCallbacks {
public:
// A client wrote new data to the RX characteristic
void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override {
const std::string &v = pCharacteristic->getValue();
if (!v.empty()) {
// Push received data into rxBuffer; allow overwrite to avoid stalling on bursts
rxBuffer.push(v.data(), v.size(), true);
}
#if DEBUG_LEVEL >= DEBUG
Serial.printf("%s : onWrite(), value: %s\r\n",
pCharacteristic->getUUID().toString().c_str(),
v.c_str());
#endif
}
} receiverCallBacks;
// ----------------
// TX Callbacks
// ----------------
class TxCallback : public NimBLECharacteristicCallbacks {
public:
// Best-effort normalization for status codes across NimBLE variants
static inline bool isOkOrDone(int code) {
return (code == 0) || (code == 14); // 0=OK, 14=EDONE
}
static inline bool isMsgSize(int code) {
return (code == 4); // EMSGSIZE
}
static inline bool isBadData(int code) {
// EBADDATA observed as 9 and 10 across builds; include both
return (code == BLE_HS_EBADDATA || code == 9 || code == 10);
}
static inline bool isCongestion(int code) {
// ENOMEM(6), ENOMEM_EVT(12/20), EBUSY(15), ETIMEOUT(13)
return (code == 6) || (code == 12) || (code == 20) || (code == 15) || (code == 13);
}
static inline bool isDisconnectedOrEOS(int code) {
// ENOTCONN(7), EOS seen as 11 in this build
return (code == 7) || (code == 11);
}
static const char* codeName(int code) {
switch (code) {
case 0: return "OK";
case 14: return "EDONE";
case 4: return "EMSGSIZE";
case 9: return "EBADDATA(9)";
case 10: return "EBADDATA(10)";
case 6: return "ENOMEM";
case 12: return "ENOMEM_EVT(12)";
case 20: return "ENOMEM_EVT(20)";
case 15: return "EBUSY";
case 13: return "ETIMEOUT";
case 7: return "ENOTCONN";
case 11: return "EOS";
default: return "UNKNOWN";
}
}
// A notification was sent to the client.
void onStatus(NimBLECharacteristic* pCharacteristic, int code) override {
/*
Status codes:
0 → Success (notification queued/sent).
1 (BLE_HS_EUNKNOWN) → Unknown error.
14 (BLE_HS_EDONE) → Success for indication (confirmation received).
6 (BLE_HS_ENOMEM) → Out of buffers / resource exhaustion. You’re sending faster than the stack can drain, or mbufs are tight. Back off or throttle.
15 (BLE_HS_EBUSY) → Another LL/GATT procedure is in progress; try again later.
13 (BLE_HS_ETIMEOUT) → Timed out (e.g., indication not confirmed).
7 (BLE_HS_ENOTCONN) → Connection went away / bad handle.
3/2 (BLE_HS_EINVAL) → Bad arg / state.
4 (BLE_HS_EMSGSIZE) → Payload too big for context. (For notifies you should already be ≤ MTU−3.)
5 (BLE_HS_EALREADY) → Operation already in progress.
8 (BLE_HS_EAPP) → Application error.
** 9 (BLE_HS_EBADDATA) → Malformed data.
** reports as 10
10 (BLE_HS_EOS) → Connection closed, end of stream.
** 12 (BLE_HS_ENOMEM_EVT) → Out of memory for event allocation.
** reports as 20
16 (BLE_HS_EDISABLED) → BLE stack not enabled.
18 (BLE_HS_ENOTSYNCED)→ Host not synced with controller yet.
19 (BLE_HS_EAUTHEN) → Authentication failed.
20 (BLE_HS_EAUTHOR) → Authorization failed.
21 (BLE_HS_EENCRYPT) → Encryption failed.
22 (BLE_HS_EENCRYPT_KEY_SZ) → Insufficient key size.
23 (BLE_HS_ESTORE_CAP) → Storage capacity reached (bonding).
24 (BLE_HS_ESTORE_FAIL) → Persistent storage write failed.
25 (BLE_HS_EHCI) → Low-level HCI failure.
*/
// #if DEBUG_LEVEL >= DEBUG
// Serial.printf("TX onStatus code: %d\r\n", code);
// Serial.printf("Status codes: DONE %d, MSGSIZE %d, BADDATA %d\r\n", BLE_HS_EDONE, BLE_HS_EMSGSIZE, BLE_HS_EBADDATA);
// Serial.printf("Status codes: NOMEM %d, NOMEM_EVT %d, BUSY %d, TIMEOUT %d \r\n", BLE_HS_ENOMEM, BLE_HS_ENOMEM_EVT, BLE_HS_EBUSY, BLE_HS_ETIMEOUT);
// Serial.printf("Status codes: NOTCONN %d, EOS %d\r\n", BLE_HS_ENOTCONN, BLE_HS_EOS);
// #endif
if (isOkOrDone(code))
{
// Success ---------------------------------------------------
txOkFlag = true;
mtuRetryCount = 0;
// cooldown after any backoff/error before allowing new probes for faster rates
if (recentlyBackedOff) {
if (++cooldownSuccess >= COOL_SUCCESS_REQUIRED) {
recentlyBackedOff = false;
cooldownSuccess = 0;
successStreak = 0;
lkgFailStreak = 0; // reset fail streak when we calm down
}
return; // do not probe during cooldown
}
// If we are probing for faster rates, count successes; accept probe once stable
if (probing) {
if (++probeSuccesses >= PROBE_CONFIRM_SUCCESSES) {
lkgInterval = sendInterval; // new stable floor
probing = false;
probeSuccesses= 0;
probeFailures = 0;
lkgFailStreak = 0;
successStreak = 0;
#if DEBUG_LEVEL >= INFO
Serial.printf("Probe accepted. LKG=%u\r\n", lkgInterval);
#endif
}
return;
}
// Not probing: a success at LKG clears fail streak
lkgFailStreak = 0;
// After enough successes, try a small faster probe
if (++successStreak >= PROBE_AFTER_SUCCESSES) {
successStreak = 0;
lkgInterval = sendInterval;
uint32_t stepAbs = PROBE_STEP_US;
uint32_t stepPct = (sendInterval * PROBE_STEP_PCT) / 100;
uint32_t step = (stepPct > stepAbs) ? stepPct : stepAbs;
uint32_t cand = (sendInterval > step) ? (sendInterval - step) : minSendIntervalUs;
if (cand < minSendIntervalUs) cand = (uint32_t)minSendIntervalUs;
if (cand < sendInterval) {
sendInterval = cand;
probing = true;
probeSuccesses = 0;
probeFailures = 0;
#if DEBUG_LEVEL >= INFO
Serial.printf("Probe start: %u -> %u\r\n", lkgInterval, sendInterval);
#endif
}
}
}
else if (isMsgSize(code))
{
// Payload too big for context -----------------------------------
// Recompute chunk size and timing to the current negotiated MTU and restage
if (++mtuRetryCount <= mtuRetryMax)
{
// Try to get the current MTU from the controller
uint16_t currentMtu = NimBLEDevice::getMTU();
if (currentMtu != mtu) {
mtu = currentMtu;
recompute_tx_timing();
#if DEBUG_LEVEL >= INFO
Serial.printf("EMSGSIZE: MTU adjusted, send interval: %u\r\n", sendInterval);
#endif
} else {
uint16_t oldChunk = txChunkSize;
txChunkSize = (uint16_t)max(20, (int)txChunkSize / 2);
lowWaterMark = update_lowWaterMark(txChunkSize);
minSendIntervalUs = compute_minSendIntervalUs(txChunkSize, g_ll_tx_octets, llTimeUS);
if (sendInterval < minSendIntervalUs) sendInterval = minSendIntervalUs;
#if DEBUG_LEVEL >= INFO
Serial.printf("EMSGSIZE: Chunk reduced old=%u new=%u minSendIntervalUs=%u\r\n",
oldChunk, txChunkSize, minSendIntervalUs);
#endif
}
pendingLen = 0; // drop staged copy (ring still has it)
// Keep generationAllowed = false so next loop re-peeks the same data with the new size
}
else
{
// We have issues adjusting chunk size, last try effort before disconnect
#if DEBUG_LEVEL >= WARNING
Serial.println("EMSGSIZE: retries exceeded");
#endif
if (txChunkSize > 20) {
// One last fallback before disconnect: force minimum chunk and retry once
txChunkSize = 20;
lowWaterMark = update_lowWaterMark(txChunkSize);
minSendIntervalUs = compute_minSendIntervalUs(txChunkSize, g_ll_tx_octets, llTimeUS);
if (sendInterval < minSendIntervalUs) sendInterval = minSendIntervalUs;
mtuRetryCount = 0; // give it another chance
} else {
#if DEBUG_LEVEL >= WARNING
Serial.println("EMSGSIZE: retries exceeded, disconnecting");
#endif
pServer->disconnect(g_connectHandle);
mtuRetryCount = 0;
}
pendingLen = 0;
}
}
else if (isBadData(code))
{
// Malformed data (stack-side). Do NOT treat as congestion; try smaller chunks a few times.
static uint32_t lastPrint9Us = 0;
static uint16_t suppressed9 = 0;
const uint64_t now = micros();
if ((now - lastPrint9Us) > 500000ULL) { // rate-limit: print at most ~2 Hz
#if DEBUG_LEVEL >= WARNING
if (suppressed9 > 0) {
Serial.printf("EBADDATA: +%u suppressed\r\n", suppressed9);
}
Serial.printf("EBADDATA: code=%d (%s)\r\n", code, codeName(code));
#endif
lastPrint9Us = (uint32_t)now;
suppressed9 = 0;
} else {
++suppressed9; // quiet path
}
if (badDataRetries < badDataMaxRetries) {
uint16_t oldChunk = txChunkSize;
// shrink ~25% per attempt; floor at 20 bytes
uint16_t newChunk = (uint16_t)max(20, (int)((oldChunk * 9) / 10));
if (newChunk < oldChunk) {
txChunkSize = newChunk;
lowWaterMark = update_lowWaterMark(txChunkSize);
minSendIntervalUs = compute_minSendIntervalUs(txChunkSize, g_ll_tx_octets, llTimeUS);
if (sendInterval < minSendIntervalUs) sendInterval = minSendIntervalUs; // keep floor only
++badDataRetries;
#if DEBUG_LEVEL >= INFO
Serial.printf("EBADDATA: reduced chunk old=%u new=%u minSendIntervalUs=%u (retry %u/%u)\r\n",
oldChunk, txChunkSize, minSendIntervalUs, badDataRetries, badDataMaxRetries);
#endif
}
}
// Restage the same data at the new size; do not change pacing/probe state
pendingLen = 0;
}
else if (isCongestion(code))
{
// Congestion ------------------------------------------------
successStreak = 0;
recentlyBackedOff = true;
cooldownSuccess = 0;
if (probing) {
probing = false;
probeFailures++;
sendInterval = lkgInterval;
lkgFailStreak = 0;
#if DEBUG_LEVEL >= INFO
Serial.printf("Congestion: Probe failed, revert to LKG=%u\r\n", sendInterval);
#endif
} else {
// failure at LKG; if repeated, relax LKG slightly
if (++lkgFailStreak >= LKG_ESCALATE_AFTER_FAILS) {
unsigned long now = micros();
// only escalate if cooldown passed AND buffer shows pressure
size_t used = txBuffer.available();
if ((now - lastEscalateAt) >= ESCALATE_COOLDOWN_US && used >= lowWaterMark) {
lastEscalateAt = now;
lkgFailStreak = 0;
uint32_t next = (lkgInterval * LKG_ESCALATE_NUM) / LKG_ESCALATE_DEN;
if (next < minSendIntervalUs) next = (uint32_t)minSendIntervalUs;
if (next > maxSendIntervalUs) next = maxSendIntervalUs;
lkgInterval = next;
sendInterval = next;
#if DEBUG_LEVEL >= INFO
Serial.printf("Congestion: Escalate LKG to %u\r\n", lkgInterval);
#endif
}
}
}
}
else if (isDisconnectedOrEOS(code))
{
// Connection dropped
successStreak = 0;
recentlyBackedOff = false;
cooldownSuccess = 0;
probing = false;
probeSuccesses = probeFailures = lkgFailStreak = 0;
sendInterval = maxSendIntervalUs;
lkgInterval = sendInterval;
#if DEBUG_LEVEL >= WARNING
Serial.println("Connection dropped or EOS");
#endif
}
else
{
// Unknown/unclassified error: log only; do not change pacing.
if (probing) {
probing = false;
sendInterval = lkgInterval; // drop probe only
lkgFailStreak = 0;
#if DEBUG_LEVEL >= INFO
Serial.printf("Unclassified issue %u (%s) while probing: revert to LKG=%u\r\n", code, codeName(code), sendInterval);
#endif
} else {
#if DEBUG_LEVEL >= WARNING
Serial.printf("Unclassified issue %u (%s) (no interval change)\r\n", code, codeName(code));
#endif
}
}
}
// Peer subscribed to notifications/indications
void onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, uint16_t subValue) override {
clientSubscribed = (bool)(subValue & 0x0001); // enable send data in main loop
#if DEBUG_LEVEL >= INFO
std::string addr = connInfo.getAddress().toString();
std::string uuid = pCharacteristic->getUUID().toString();
if (subValue & 0x0001) {
// Notifications
Serial.printf("BLE GATT client %s subscribed to notifications: %s\r\n", addr.c_str(), uuid.c_str());
}
if (subValue & 0x0002) {
// Indications
Serial.printf("BLE GATT client %s subscribed to indications: %s\r\n", addr.c_str(), uuid.c_str());
}
if (subValue == 0)
// Unsubscribed
Serial.printf("BLE GATT client %s unsubscribed from: %s\r\n", addr.c_str(), uuid.c_str());
#endif
}
} transmitterCallBacks;
// ----------------
// GAP Callbacks