• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pybricks / pybricks-micropython / 4995886015

pending completion
4995886015

push

github

David Lechner
drv/bluetooth: handle remote + observing

0 of 9 new or added lines in 1 file covered. (0.0%)

103 existing lines in 2 files now uncovered.

3153 of 6044 relevant lines covered (52.17%)

32681993.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.92
/lib/pbio/drv/bluetooth/bluetooth_btstack.c
1
// SPDX-License-Identifier: MIT
2
// Copyright (c) 2020-2023 The Pybricks Authors
3

4
// Bluetooth driver using BlueKitchen BTStack.
5

6
#include <pbdrv/config.h>
7

8
#if PBDRV_CONFIG_BLUETOOTH_BTSTACK
9

10
#include <ble/gatt-service/device_information_service_server.h>
11
#include <ble/gatt-service/nordic_spp_service_server.h>
12
#include <btstack.h>
13
#include <contiki.h>
14
#include <contiki-lib.h>
15
#include <lego_lwp3.h>
16

17
#include <pbdrv/bluetooth.h>
18
#include <pbio/protocol.h>
19
#include <pbio/task.h>
20
#include <pbio/version.h>
21

22
#include "bluetooth_btstack_run_loop_contiki.h"
23
#include "bluetooth_btstack.h"
24
#include "genhdr/pybricks_service.h"
25
#include "pybricks_service_server.h"
26

27
#ifdef PBDRV_CONFIG_BLUETOOTH_BTSTACK_HUB_KIND
28
#define HUB_KIND PBDRV_CONFIG_BLUETOOTH_BTSTACK_HUB_KIND
29
#else
30
#error "PBDRV_CONFIG_BLUETOOTH_BTSTACK_HUB_KIND is required"
31
#endif
32

33
// location of product variant in bootloader flash memory of Technic Large hubs
34
#if PBDRV_CONFIG_BLUETOOTH_BTSTACK_HUB_VARIANT_ADDR
35
#define HUB_VARIANT (*(const uint16_t *)PBDRV_CONFIG_BLUETOOTH_BTSTACK_HUB_VARIANT_ADDR)
36
#else
37
#define HUB_VARIANT 0x0000
38
#endif
39

40
// these aren't in btstack for some reason
41
#define ADV_IND                                0x00
42
#define ADV_DIRECT_IND                         0x01
43
#define ADV_SCAN_IND                           0x02
44
#define ADV_NONCONN_IND                        0x03
45
#define SCAN_RSP                               0x04
46

47
typedef enum {
48
    CON_STATE_NONE,
49
    CON_STATE_WAIT_ADV_IND,
50
    CON_STATE_WAIT_SCAN_RSP,
51
    CON_STATE_WAIT_CONNECT,
52
    CON_STATE_WAIT_DISCOVER_SERVICES,
53
    CON_STATE_WAIT_DISCOVER_CHARACTERISTICS,
54
    CON_STATE_WAIT_ENABLE_NOTIFICATIONS,
55
    CON_STATE_CONNECTED,
56
    CON_STATE_WAIT_DISCONNECT,
57
} con_state_t;
58

59
typedef enum {
60
    DISCONNECT_REASON_NONE,
61
    DISCONNECT_REASON_TIMEOUT,
62
    DISCONNECT_REASON_CONNECT_FAILED,
63
    DISCONNECT_REASON_DISCOVER_SERVICE_FAILED,
64
    DISCONNECT_REASON_DISCOVER_CHARACTERISTIC_FAILED,
65
    DISCONNECT_REASON_CONFIGURE_CHARACTERISTIC_FAILED,
66
    DISCONNECT_REASON_SEND_SUBSCRIBE_PORT_0_FAILED,
67
    DISCONNECT_REASON_SEND_SUBSCRIBE_PORT_1_FAILED,
68
} disconnect_reason_t;
69

70
typedef struct {
71
    gatt_client_notification_t notification;
72
    uint16_t con_handle;
73
    con_state_t con_state;
74
    disconnect_reason_t disconnect_reason;
75
    gatt_client_service_t lwp3_service;
76
    gatt_client_characteristic_t lwp3_char;
77
    uint8_t btstack_error;
78
    uint8_t address_type;
79
    bd_addr_t address;
80
    lwp3_hub_kind_t hub_kind;
81
    char name[20];
82
} pup_handset_t;
83

84
// hub name goes in special section so that it can be modified when flashing firmware
85
#if !PBIO_TEST_BUILD
86
__attribute__((section(".name")))
87
#endif
88
char pbdrv_bluetooth_hub_name[16] = "Pybricks Hub";
89

90
LIST(task_queue);
91
static hci_con_handle_t le_con_handle = HCI_CON_HANDLE_INVALID;
92
static hci_con_handle_t pybricks_con_handle = HCI_CON_HANDLE_INVALID;
93
static hci_con_handle_t uart_con_handle = HCI_CON_HANDLE_INVALID;
94
static pbdrv_bluetooth_on_event_t bluetooth_on_event;
95
static pbdrv_bluetooth_receive_handler_t receive_handler;
96
static pbdrv_bluetooth_receive_handler_t notification_handler;
97
static pup_handset_t handset;
98
static uint8_t *event_packet;
99
static const pbdrv_bluetooth_btstack_platform_data_t *pdata = &pbdrv_bluetooth_btstack_platform_data;
100

101
static bool is_broadcasting;
102
static bool is_observing;
103
static pbdrv_bluetooth_start_observing_callback_t observe_callback;
104

105
// note on baud rate: with a 48MHz clock, 3000000 baud is the highest we can
106
// go with LL_USART_OVERSAMPLING_16. With LL_USART_OVERSAMPLING_8 we could go
107
// to 4000000, which is the max rating of the CC2564C.
108

109
static const hci_transport_config_uart_t config = {
110
    .type = HCI_TRANSPORT_CONFIG_UART,
111
    .baudrate_init = 115200,
112
    .baudrate_main = 3000000,
113
    .flowcontrol = 1,
114
    .device_name = NULL,
115
};
116

117
/**
118
 * Converts BTStack error to most appropriate PBIO error.
119
 * @param [in]  status      The BTStack error
120
 * @return                  The PBIO error.
121
 */
UNCOV
122
static pbio_error_t att_error_to_pbio_error(uint8_t status) {
×
123
    switch (status) {
×
124
        case ATT_ERROR_SUCCESS:
×
125
            return PBIO_SUCCESS;
×
126
        case ATT_ERROR_HCI_DISCONNECT_RECEIVED:
×
127
            return PBIO_ERROR_NO_DEV;
×
128
        case ATT_ERROR_TIMEOUT:
×
129
            return PBIO_ERROR_TIMEDOUT;
×
130
        default:
×
131
            return PBIO_ERROR_FAILED;
×
132
    }
133
}
134

135
static void pybricks_can_send(void *context) {
4✔
136
    pbdrv_bluetooth_send_context_t *send = context;
4✔
137

138
    pybricks_service_server_send(pybricks_con_handle, send->data, send->size);
4✔
139
    send->done();
4✔
140
}
4✔
141

142
static pbio_pybricks_error_t pybricks_data_received(hci_con_handle_t tx_con_handle, const uint8_t *data, uint16_t size) {
1✔
143
    if (receive_handler) {
1✔
144
        return receive_handler(PBDRV_BLUETOOTH_CONNECTION_PYBRICKS, data, size);
1✔
145
    }
146

UNCOV
147
    return ATT_ERROR_UNLIKELY_ERROR;
×
148
}
149

150
static void pybricks_configured(hci_con_handle_t tx_con_handle, uint16_t value) {
2✔
151
    pybricks_con_handle = value ? tx_con_handle : HCI_CON_HANDLE_INVALID;
2✔
152
}
2✔
153

UNCOV
154
static void nordic_can_send(void *context) {
×
155
    pbdrv_bluetooth_send_context_t *send = context;
×
156

UNCOV
157
    nordic_spp_service_server_send(uart_con_handle, send->data, send->size);
×
158
    send->done();
×
159
}
×
160

161
/**
162
 * Queues a tasks and runs the first iteration if there is no other task already
163
 * running.
164
 *
165
 * @param [in]  task    An uninitialized task.
166
 * @param [in]  thread  The task thread to attach to the task.
167
 * @param [in]  context The context to attach to the task.
168
 */
UNCOV
169
static void start_task(pbio_task_t *task, pbio_task_thread_t thread, void *context) {
×
170
    pbio_task_init(task, thread, context);
×
171

UNCOV
172
    if (list_head(task_queue) != NULL || !pbio_task_run_once(task)) {
×
173
        list_add(task_queue, task);
×
174
    }
UNCOV
175
}
×
176

177
/**
178
 * Runs tasks that may be waiting for event and notifies external subscriber.
179
 *
180
 * @param [in]  packet  Pointer to the raw packet data.
181
 */
182
static void propagate_event(uint8_t *packet) {
147✔
183
    event_packet = packet;
147✔
184

UNCOV
185
    for (;;) {
×
186
        pbio_task_t *current_task = list_head(task_queue);
147✔
187

188
        if (!current_task) {
147✔
189
            break;
147✔
190
        }
191

UNCOV
192
        if (current_task->status != PBIO_ERROR_AGAIN || pbio_task_run_once(current_task)) {
×
193
            // remove the task from the queue only if the task is complete
UNCOV
194
            list_remove(task_queue, current_task);
×
195
            // then start the next task
UNCOV
196
            continue;
×
197
        }
198

UNCOV
199
        break;
×
200
    }
201

202
    event_packet = NULL;
147✔
203

204
    if (bluetooth_on_event) {
147✔
205
        bluetooth_on_event();
147✔
206
    }
207
}
147✔
208

UNCOV
209
static void nordic_spp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
×
210
    switch (packet_type) {
×
211
        case HCI_EVENT_PACKET:
×
212
            if (hci_event_packet_get_type(packet) != HCI_EVENT_GATTSERVICE_META) {
×
213
                break;
×
214
            }
215

UNCOV
216
            switch (hci_event_gattservice_meta_get_subevent_code(packet)) {
×
217
                case GATTSERVICE_SUBEVENT_SPP_SERVICE_CONNECTED:
×
218
                    uart_con_handle = gattservice_subevent_spp_service_connected_get_con_handle(packet);
×
219
                    break;
×
220
                case GATTSERVICE_SUBEVENT_SPP_SERVICE_DISCONNECTED:
×
221
                    uart_con_handle = HCI_CON_HANDLE_INVALID;
×
222
                    break;
×
223
                default:
×
224
                    break;
×
225
            }
226

UNCOV
227
            break;
×
228
        case RFCOMM_DATA_PACKET:
×
229
            if (receive_handler) {
×
230
                receive_handler(PBDRV_BLUETOOTH_CONNECTION_UART, packet, size);
×
231
            }
UNCOV
232
            break;
×
233
        default:
×
234
            break;
×
235
    }
236

UNCOV
237
    propagate_event(packet);
×
238
}
×
239

240
// REVISIT: does this need to be separate from packet_handler()?
241
// currently, this function just handles the Powered Up handset control.
UNCOV
242
static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
×
243

UNCOV
244
    switch (hci_event_packet_get_type(packet)) {
×
245
        case GATT_EVENT_SERVICE_QUERY_RESULT:
×
246
            gatt_event_service_query_result_get_service(packet, &handset.lwp3_service);
×
247
            break;
×
248

UNCOV
249
        case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT:
×
250
            gatt_event_characteristic_query_result_get_characteristic(packet, &handset.lwp3_char);
×
251
            break;
×
252

UNCOV
253
        case GATT_EVENT_QUERY_COMPLETE:
×
254
            if (handset.con_state == CON_STATE_WAIT_DISCOVER_SERVICES) {
×
255
                // TODO: remove cast on lwp3_characteristic_uuid after
256
                // https://github.com/bluekitchen/btstack/pull/359
UNCOV
257
                handset.btstack_error = gatt_client_discover_characteristics_for_service_by_uuid128(
×
258
                    handle_gatt_client_event, handset.con_handle, &handset.lwp3_service, (uint8_t *)pbio_lwp3_hub_char_uuid);
×
259
                if (handset.btstack_error == ERROR_CODE_SUCCESS) {
×
260
                    handset.con_state = CON_STATE_WAIT_DISCOVER_CHARACTERISTICS;
×
261
                } else {
262
                    // configuration failed for some reason, so disconnect
UNCOV
263
                    gap_disconnect(handset.con_handle);
×
264
                    handset.con_state = CON_STATE_WAIT_DISCONNECT;
×
265
                    handset.disconnect_reason = DISCONNECT_REASON_DISCOVER_CHARACTERISTIC_FAILED;
×
266
                }
UNCOV
267
            } else if (handset.con_state == CON_STATE_WAIT_DISCOVER_CHARACTERISTICS) {
×
268
                handset.btstack_error = gatt_client_write_client_characteristic_configuration(
×
269
                    handle_gatt_client_event, handset.con_handle, &handset.lwp3_char,
×
270
                    GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION);
UNCOV
271
                if (handset.btstack_error == ERROR_CODE_SUCCESS) {
×
272
                    gatt_client_listen_for_characteristic_value_updates(
×
273
                        &handset.notification, handle_gatt_client_event, handset.con_handle, &handset.lwp3_char);
×
274
                    handset.con_state = CON_STATE_WAIT_ENABLE_NOTIFICATIONS;
×
275
                } else {
276
                    // configuration failed for some reason, so disconnect
UNCOV
277
                    gap_disconnect(handset.con_handle);
×
278
                    handset.con_state = CON_STATE_WAIT_DISCONNECT;
×
279
                    handset.disconnect_reason = DISCONNECT_REASON_CONFIGURE_CHARACTERISTIC_FAILED;
×
280
                }
UNCOV
281
            } else if (handset.con_state == CON_STATE_WAIT_ENABLE_NOTIFICATIONS) {
×
282
                handset.con_state = CON_STATE_CONNECTED;
×
283
            }
UNCOV
284
            break;
×
285

UNCOV
286
        case GATT_EVENT_NOTIFICATION: {
×
287
            if (gatt_event_notification_get_handle(packet) != handset.con_handle) {
×
288
                break;
×
289
            }
290

UNCOV
291
            if (notification_handler != NULL) {
×
292
                uint16_t length = gatt_event_notification_get_value_length(packet);
×
293
                const uint8_t *value = gatt_event_notification_get_value(packet);
×
294
                notification_handler(PBDRV_BLUETOOTH_CONNECTION_PERIPHERAL_LWP3, value, length);
×
295
            }
UNCOV
296
            break;
×
297
        }
298

UNCOV
299
        default:
×
300
            break;
×
301
    }
302

UNCOV
303
    propagate_event(packet);
×
304
}
×
305

306
static void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
147✔
307
    UNUSED(channel);
308
    UNUSED(size);
309

310
    if (packet_type != HCI_EVENT_PACKET) {
147✔
UNCOV
311
        return;
×
312
    }
313

314
    switch (hci_event_packet_get_type(packet)) {
147✔
315
        case HCI_EVENT_LE_META:
1✔
316
            if (hci_event_le_meta_get_subevent_code(packet) != HCI_SUBEVENT_LE_CONNECTION_COMPLETE) {
1✔
UNCOV
317
                break;
×
318
            }
319

320
            // HCI_ROLE_SLAVE means the connecting device is the central and the hub is the peripheral
321
            // HCI_ROLE_MASTER means the connecting device is the peripheral and the hub is the central.
322
            if (hci_subevent_le_connection_complete_get_role(packet) == HCI_ROLE_SLAVE) {
1✔
323
                le_con_handle = hci_subevent_le_connection_complete_get_connection_handle(packet);
1✔
324

325
                // don't start advertising again on disconnect
326
                gap_advertisements_enable(false);
1✔
327
            } else {
328
                // If we aren't waiting for a handset connection, this must be a different connection.
UNCOV
329
                if (handset.con_state != CON_STATE_WAIT_CONNECT) {
×
330
                    break;
×
331
                }
332

UNCOV
333
                handset.con_handle = hci_subevent_le_connection_complete_get_connection_handle(packet);
×
334

UNCOV
335
                handset.btstack_error = gatt_client_discover_primary_services_by_uuid128(
×
336
                    handle_gatt_client_event, handset.con_handle, pbio_lwp3_hub_service_uuid);
×
337
                if (handset.btstack_error == ERROR_CODE_SUCCESS) {
×
338
                    handset.con_state = CON_STATE_WAIT_DISCOVER_SERVICES;
×
339
                } else {
340
                    // configuration failed for some reason, so disconnect
UNCOV
341
                    gap_disconnect(handset.con_handle);
×
342
                    handset.con_state = CON_STATE_WAIT_DISCONNECT;
×
343
                    handset.disconnect_reason = DISCONNECT_REASON_DISCOVER_SERVICE_FAILED;
×
344
                }
345
            }
346

347
            break;
1✔
348

UNCOV
349
        case HCI_EVENT_DISCONNECTION_COMPLETE:
×
350
            if (hci_event_disconnection_complete_get_connection_handle(packet) == le_con_handle) {
×
351
                le_con_handle = HCI_CON_HANDLE_INVALID;
×
352
                pybricks_con_handle = HCI_CON_HANDLE_INVALID;
×
353
                uart_con_handle = HCI_CON_HANDLE_INVALID;
×
354
            } else if (hci_event_disconnection_complete_get_connection_handle(packet) == handset.con_handle) {
×
355
                gatt_client_stop_listening_for_characteristic_value_updates(&handset.notification);
×
356
                handset.con_handle = HCI_CON_HANDLE_INVALID;
×
357
                handset.con_state = CON_STATE_NONE;
×
358
            }
359

UNCOV
360
            break;
×
361

UNCOV
362
        case GAP_EVENT_ADVERTISING_REPORT: {
×
363
            uint8_t event_type = gap_event_advertising_report_get_advertising_event_type(packet);
×
364
            uint8_t data_length = gap_event_advertising_report_get_data_length(packet);
×
365
            const uint8_t *data = gap_event_advertising_report_get_data(packet);
×
366
            bd_addr_t address;
367

UNCOV
368
            gap_event_advertising_report_get_address(packet, address);
×
369

UNCOV
370
            if (observe_callback) {
×
371
                int8_t rssi = gap_event_advertising_report_get_rssi(packet);
×
372
                observe_callback(event_type, data, data_length, rssi);
×
373
            }
374

UNCOV
375
            if (handset.con_state == CON_STATE_WAIT_ADV_IND) {
×
376
                // HACK: this is making major assumptions about how the advertising data
377
                // is laid out. So far LEGO devices seem consistent in this.
378
                // It is expected that the advertising data contains 3 values in
379
                // this order:
380
                // - Flags (0x01)
381
                // - Complete List of 128-bit Service Class UUIDs (0x07)
382
                // - Manufacturer Specific Data (0xFF)
383
                //   - LEGO System A/S (0x0397) + 6 bytes
UNCOV
384
                if (event_type == ADV_IND && data_length == 31
×
385
                    && pbio_uuid128_reverse_compare(&data[5], pbio_lwp3_hub_service_uuid)
×
386
                    && data[26] == handset.hub_kind) {
×
387

UNCOV
388
                    if (memcmp(address, handset.address, 6) == 0) {
×
389
                        // This was the same device as last time. If the scan response
390
                        // didn't match before, it probably won't match now and we
391
                        // should try a different device.
UNCOV
392
                        break;
×
393
                    }
394

UNCOV
395
                    memcpy(handset.address, address, sizeof(bd_addr_t));
×
396
                    handset.address_type = gap_event_advertising_report_get_address_type(packet);
×
397
                    handset.con_state = CON_STATE_WAIT_SCAN_RSP;
×
398
                }
UNCOV
399
            } else if (handset.con_state == CON_STATE_WAIT_SCAN_RSP) {
×
400
                // REVISIT: for now it is assumed that the saved Bluetooth address compare
401
                //          is a sufficient check to check that scan response is from LWP3 device
402
                // PREVIOUSLY: extra check: data_length == 30 for HANDSET
403
                //                          data_length == 27 for MARIO
404
                //                          data_length == 20 for SYSTEM_2IO ... etc
UNCOV
405
                if (event_type == SCAN_RSP && bd_addr_cmp(address, handset.address) == 0) {
×
406
                    if (data[1] == BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME) {
×
407
                        // if the name was passed in from the caller, then filter on name
UNCOV
408
                        if (handset.name[0] != '\0' && strncmp(handset.name, (char *)&data[2], sizeof(handset.name)) != 0) {
×
409
                            handset.con_state = CON_STATE_WAIT_ADV_IND;
×
410
                            break;
×
411
                        }
412

UNCOV
413
                        memcpy(handset.name, &data[2], sizeof(handset.name));
×
414
                    }
415

UNCOV
416
                    gap_stop_scan();
×
417
                    handset.btstack_error = gap_connect(handset.address, handset.address_type);
×
418

UNCOV
419
                    if (handset.btstack_error == ERROR_CODE_SUCCESS) {
×
420
                        handset.con_state = CON_STATE_WAIT_CONNECT;
×
421
                    } else {
UNCOV
422
                        handset.con_state = CON_STATE_NONE;
×
423
                    }
424
                }
425
            }
426

UNCOV
427
            break;
×
428
        }
429

430
        default:
146✔
431
            break;
146✔
432
    }
433

434
    propagate_event(packet);
147✔
435
}
436

437
// ATT Client Read Callback for Dynamic Data
438
// - if buffer == NULL, don't copy data, just return size of value
439
// - if buffer != NULL, copy data and return number bytes copied
440
// @param offset defines start of attribute value
UNCOV
441
static uint16_t att_read_callback(hci_con_handle_t con_handle, uint16_t attribute_handle, uint16_t offset, uint8_t *buffer, uint16_t buffer_size) {
×
442
    uint16_t att_value_len;
443

UNCOV
444
    switch (attribute_handle) {
×
445
        case ATT_CHARACTERISTIC_GAP_DEVICE_NAME_01_VALUE_HANDLE:
×
446
            att_value_len = strlen(pbdrv_bluetooth_hub_name);
×
447
            if (buffer) {
×
448
                memcpy(buffer, pbdrv_bluetooth_hub_name, att_value_len);
×
449
            }
UNCOV
450
            return att_value_len;
×
451

UNCOV
452
        default:
×
453
            return 0;
×
454
    }
455
}
456

457
void pbdrv_bluetooth_init(void) {
15✔
458
    static btstack_packet_callback_registration_t hci_event_callback_registration;
459

460
    // don't need to init the whole struct, so doing this here
461
    handset.con_handle = HCI_CON_HANDLE_INVALID;
15✔
462

463
    btstack_memory_init();
15✔
464
    btstack_run_loop_init(pbdrv_bluetooth_btstack_run_loop_contiki_get_instance());
15✔
465

466
    hci_init(hci_transport_h4_instance(pdata->uart_block_instance()), &config);
15✔
467
    hci_set_chipset(pdata->chipset_instance());
15✔
468
    hci_set_control(pdata->control_instance());
15✔
469

470
    // REVISIT: do we need to call btstack_chipset_cc256x_set_power() or btstack_chipset_cc256x_set_power_vector()?
471

472
    hci_event_callback_registration.callback = &packet_handler;
15✔
473
    hci_add_event_handler(&hci_event_callback_registration);
15✔
474

475
    l2cap_init();
15✔
476

477
    // setup LE device DB
478
    le_device_db_init();
15✔
479

480
    // setup security manager
481
    sm_init();
15✔
482
    sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
15✔
483
    sm_set_er((uint8_t *)pdata->er_key);
15✔
484
    sm_set_ir((uint8_t *)pdata->ir_key);
15✔
485

486
    gap_random_address_set_mode(GAP_RANDOM_ADDRESS_NON_RESOLVABLE);
15✔
487
    gap_set_max_number_peripheral_connections(2);
15✔
488

489
    // GATT Client setup
490
    gatt_client_init();
15✔
491

492
    // setup ATT server
493
    att_server_init(profile_data, att_read_callback, NULL);
15✔
494

495
    device_information_service_server_init();
15✔
496
    device_information_service_server_set_firmware_revision(PBIO_VERSION_STR);
15✔
497
    device_information_service_server_set_software_revision(PBIO_PROTOCOL_VERSION_STR);
15✔
498
    device_information_service_server_set_pnp_id(0x01, LWP3_LEGO_COMPANY_ID, HUB_KIND, HUB_VARIANT);
15✔
499

500
    pybricks_service_server_init(pybricks_data_received, pybricks_configured);
15✔
501
    nordic_spp_service_server_init(nordic_spp_packet_handler);
15✔
502
}
15✔
503

504
void pbdrv_bluetooth_power_on(bool on) {
1✔
505
    hci_power_control(on ? HCI_POWER_ON : HCI_POWER_OFF);
1✔
506

507
    // When powering off, cancel all pending tasks.
508
    if (!on) {
1✔
509
        pbio_task_t *task;
510

UNCOV
511
        while ((task = list_pop(task_queue)) != NULL) {
×
512
            if (task->status == PBIO_ERROR_AGAIN) {
×
513
                task->status = PBIO_ERROR_CANCELED;
×
514
            }
515
        }
516
    }
517
}
1✔
518

519
bool pbdrv_bluetooth_is_ready(void) {
1✔
520
    return hci_get_state() != HCI_STATE_OFF;
1✔
521
}
522

UNCOV
523
const char *pbdrv_bluetooth_get_hub_name(void) {
×
524
    return pbdrv_bluetooth_hub_name;
×
525
}
526

UNCOV
527
const char *pbdrv_bluetooth_get_fw_version(void) {
×
528
    // REVISIT: this should be linked to the init script as it can be updated in software
529
    // init script version
UNCOV
530
    return "v1.4";
×
531
}
532

533
static void init_advertising_data(void) {
1✔
534
    bd_addr_t null_addr = { };
1✔
535
    gap_advertisements_set_params(0x30, 0x30, ADV_IND, 0x00, null_addr, 0x07, 0x00);
1✔
536

537
    static const uint8_t adv_data[] = {
538
        // Flags general discoverable, BR/EDR not supported
539
        2, BLUETOOTH_DATA_TYPE_FLAGS, 0x06,
540
        // Pybricks service
541
        17, BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
542
        0xef, 0xae, 0xe4, 0x51, 0x80, 0x6d, 0xf4, 0x89, 0xda, 0x46, 0x80, 0x82, 0x01, 0x00, 0xf5, 0xc5,
543
        // Tx Power
544
        2, BLUETOOTH_DATA_TYPE_TX_POWER_LEVEL, 0,
545
    };
546

547
    _Static_assert(sizeof(adv_data) <= 31, "31 octet max");
548

549
    gap_advertisements_set_data(sizeof(adv_data), (uint8_t *)adv_data);
1✔
550

551
    static uint8_t scan_resp_data[31] = {
552
        10, BLUETOOTH_DATA_TYPE_SERVICE_DATA,
553
        // used to identify which hub - Device Information Service (DIS).
554
        // 0x2A50 - service UUID - PnP ID characteristic UUID
555
        // 0x01 - Vendor ID Source Field - Bluetooth SIG-assigned ID
556
        // 0x0397 - Vendor ID Field - LEGO company identifier
557
        // 0x00XX - Product ID Field - hub kind
558
        // 0x00XX - Product Version Field - product variant
559
        0x50, 0x2a, 0x01, 0x97, 0x03, HUB_KIND, 0x00, 0x00, 0x00,
560
        0, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME,
561
    };
562

563
    scan_resp_data[9] = HUB_VARIANT;
1✔
564

565
    uint8_t hub_name_len = strlen(pbdrv_bluetooth_hub_name);
1✔
566
    scan_resp_data[11] = hub_name_len + 1;
1✔
567
    memcpy(&scan_resp_data[13], pbdrv_bluetooth_hub_name, hub_name_len);
1✔
568
    _Static_assert(13 + sizeof(pbdrv_bluetooth_hub_name) - 1 <= 31, "scan response is 31 octet max");
569

570
    gap_scan_response_set_data(13 + hub_name_len, scan_resp_data);
1✔
571
}
1✔
572

573
void pbdrv_bluetooth_start_advertising(void) {
1✔
574
    init_advertising_data();
1✔
575
    gap_advertisements_enable(true);
1✔
576
}
1✔
577

UNCOV
578
void pbdrv_bluetooth_stop_advertising(void) {
×
579
    gap_advertisements_enable(false);
×
580
}
×
581

582
bool pbdrv_bluetooth_is_connected(pbdrv_bluetooth_connection_t connection) {
93✔
583
    if (connection == PBDRV_BLUETOOTH_CONNECTION_LE && le_con_handle != HCI_CON_HANDLE_INVALID) {
93✔
584
        return true;
10✔
585
    }
586

587
    if (connection == PBDRV_BLUETOOTH_CONNECTION_PYBRICKS && pybricks_con_handle != HCI_CON_HANDLE_INVALID) {
83✔
588
        return true;
10✔
589
    }
590

591
    if (connection == PBDRV_BLUETOOTH_CONNECTION_UART && uart_con_handle != HCI_CON_HANDLE_INVALID) {
73✔
UNCOV
592
        return true;
×
593
    }
594

595
    if (connection == PBDRV_BLUETOOTH_CONNECTION_PERIPHERAL_LWP3 && handset.con_handle != HCI_CON_HANDLE_INVALID) {
73✔
UNCOV
596
        return true;
×
597
    }
598

599
    return false;
73✔
600
}
601

602
void pbdrv_bluetooth_set_on_event(pbdrv_bluetooth_on_event_t on_event) {
1✔
603
    bluetooth_on_event = on_event;
1✔
604
}
1✔
605

606
void pbdrv_bluetooth_send(pbdrv_bluetooth_send_context_t *context) {
4✔
607
    static btstack_context_callback_registration_t send_request;
608

609
    send_request.context = context;
4✔
610

611
    if (context->connection == PBDRV_BLUETOOTH_CONNECTION_PYBRICKS) {
4✔
612
        send_request.callback = &pybricks_can_send;
4✔
613
        pybricks_service_server_request_can_send_now(&send_request, pybricks_con_handle);
4✔
UNCOV
614
    } else if (context->connection == PBDRV_BLUETOOTH_CONNECTION_UART) {
×
615
        send_request.callback = &nordic_can_send;
×
616
        nordic_spp_service_server_request_can_send_now(&send_request, uart_con_handle);
×
617
    }
618
}
4✔
619

620
void pbdrv_bluetooth_set_receive_handler(pbdrv_bluetooth_receive_handler_t handler) {
1✔
621
    receive_handler = handler;
1✔
622
}
1✔
623

UNCOV
624
void pbdrv_bluetooth_set_notification_handler(pbdrv_bluetooth_receive_handler_t handler) {
×
625
    notification_handler = handler;
×
626
}
×
627

NEW
628
static void start_observing(void) {
×
NEW
629
    gap_set_scan_params(0, 0x30, 0x30, 0);
×
NEW
630
    gap_start_scan();
×
NEW
631
}
×
632

633
static PT_THREAD(scan_and_connect_task(struct pt *pt, pbio_task_t *task)) {
×
634
    pbdrv_bluetooth_scan_and_connect_context_t *context = task->context;
×
635

UNCOV
636
    PT_BEGIN(pt);
×
637

UNCOV
638
    memset(&handset, 0, sizeof(handset));
×
639
    handset.con_handle = HCI_CON_HANDLE_INVALID;
×
640
    memcpy(handset.name, context->name, sizeof(handset.name));
×
641
    handset.hub_kind = context->hub_kind;
×
642

643
    // active scanning to get scan response data.
644
    // scan interval: 48 * 0.625ms = 30ms
UNCOV
645
    gap_set_scan_params(1, 0x30, 0x30, 0);
×
646
    gap_start_scan();
×
647
    handset.con_state = CON_STATE_WAIT_ADV_IND;
×
648

UNCOV
649
    PT_WAIT_UNTIL(pt, ({
×
650
        if (task->cancel) {
651
            goto cancel;
652
        }
653

654
        // if there is any failure to connect or error while enumerating
655
        // attributes, con_state will be set to CON_STATE_NONE
656
        if (handset.con_state == CON_STATE_NONE) {
657
            task->status = PBIO_ERROR_FAILED;
658
            PT_EXIT(pt);
659
        }
660

661
        handset.con_state == CON_STATE_CONNECTED;
662
    }));
663

664
    // REVISIT: probably want to make a generic connection handle data structure
665
    // that includes handle, name, address, etc.
UNCOV
666
    memcpy(context->name, handset.name, sizeof(context->name));
×
667

668
    task->status = PBIO_SUCCESS;
×
NEW
669
    goto out;
×
670

671
cancel:
×
672
    if (handset.con_state == CON_STATE_WAIT_ADV_IND || handset.con_state == CON_STATE_WAIT_SCAN_RSP) {
×
673
        gap_stop_scan();
×
UNCOV
674
    } else if (handset.con_state == CON_STATE_WAIT_CONNECT) {
×
675
        gap_connect_cancel();
×
676
    } else if (handset.con_handle != HCI_CON_HANDLE_INVALID) {
×
677
        gap_disconnect(handset.con_handle);
×
678
    }
UNCOV
679
    handset.con_state = CON_STATE_NONE;
×
680
    task->status = PBIO_ERROR_CANCELED;
×
681

NEW
682
out:
×
683
    // restore observing state
NEW
684
    if (is_observing) {
×
NEW
685
        start_observing();
×
686
    }
687

688
    PT_END(pt);
×
689
}
690

691
void pbdrv_bluetooth_scan_and_connect(pbio_task_t *task, pbdrv_bluetooth_scan_and_connect_context_t *context) {
×
692
    start_task(task, scan_and_connect_task, context);
×
UNCOV
693
}
×
694

UNCOV
695
static PT_THREAD(write_remote_task(struct pt *pt, pbio_task_t *task)) {
×
UNCOV
696
    pbdrv_bluetooth_value_t *value = task->context;
×
697

UNCOV
698
    PT_BEGIN(pt);
×
699

700
    // TODO: Make connection handle and value handle part of pbdrv_bluetooth_value_t
701
    // to allow for simultaneous connections.
702

703
    uint8_t err = gatt_client_write_value_of_characteristic(packet_handler,
×
704
        handset.con_handle, handset.lwp3_char.value_handle, value->size, value->data);
×
705

UNCOV
706
    if (err != ERROR_CODE_SUCCESS) {
×
UNCOV
707
        task->status = PBIO_ERROR_FAILED;
×
UNCOV
708
        PT_EXIT(pt);
×
709
    }
710

711
    // NB: Value buffer must remain valid until GATT_EVENT_QUERY_COMPLETE, so
712
    // this wait is not cancelable.
UNCOV
713
    PT_WAIT_UNTIL(pt, ({
×
714
        if (handset.con_handle == HCI_CON_HANDLE_INVALID) {
715
            // disconnected
716
            task->status = PBIO_ERROR_NO_DEV;
717
            PT_EXIT(pt);
718
        }
719
        event_packet &&
720
        hci_event_packet_get_type(event_packet) == GATT_EVENT_QUERY_COMPLETE &&
721
        gatt_event_query_complete_get_handle(event_packet) == handset.con_handle;
722
    }));
723

UNCOV
724
    uint8_t status = gatt_event_query_complete_get_att_status(event_packet);
×
UNCOV
725
    task->status = att_error_to_pbio_error(status);
×
726

727
    PT_END(pt);
×
728
}
729

730
void pbdrv_bluetooth_write_remote(pbio_task_t *task, pbdrv_bluetooth_value_t *value) {
×
731
    start_task(task, write_remote_task, value);
×
732
}
×
733

734
void pbdrv_bluetooth_disconnect_remote(void) {
×
UNCOV
735
    if (handset.con_handle != HCI_CON_HANDLE_INVALID) {
×
736
        gap_disconnect(handset.con_handle);
×
737
    }
738
}
×
739

UNCOV
740
void pbdrv_bluetooth_start_broadcasting(pbio_task_t *task, pbdrv_bluetooth_value_t *value) {
×
UNCOV
741
    if (value->size > LE_ADVERTISING_DATA_SIZE) {
×
742
        task->status = PBIO_ERROR_INVALID_ARG;
×
743
        return;
×
744
    }
745

UNCOV
746
    bd_addr_t null_addr = { };
×
747
    gap_advertisements_set_params(0xA0, 0xA0, ADV_NONCONN_IND, 0, null_addr, 0x7, 0);
×
748

749
    // have to keep copy of data here since BTStack doesn't copy
750
    static uint8_t static_data[LE_ADVERTISING_DATA_SIZE];
751
    memcpy(static_data, value->data, value->size);
×
752

753
    gap_advertisements_set_data(value->size, static_data);
×
754

UNCOV
755
    if (!is_broadcasting) {
×
UNCOV
756
        gap_advertisements_enable(true);
×
757
        is_broadcasting = true;
×
758
    }
759

760
    // REVISIT: use callback to actually wait for start?
761
    task->status = PBIO_SUCCESS;
×
762
}
763

UNCOV
764
void pbdrv_bluetooth_stop_broadcasting(void) {
×
765
    if (is_broadcasting) {
×
UNCOV
766
        gap_advertisements_enable(false);
×
767
        is_broadcasting = false;
×
768
    }
UNCOV
769
}
×
770

771
void pbdrv_bluetooth_start_observing(pbio_task_t *task, pbdrv_bluetooth_start_observing_callback_t callback) {
×
772
    observe_callback = callback;
×
773

UNCOV
774
    if (!is_observing) {
×
NEW
775
        start_observing();
×
UNCOV
776
        is_observing = true;
×
777
    }
778

779
    // REVISIT: use callback to actually wait for start?
780
    task->status = PBIO_SUCCESS;
×
UNCOV
781
}
×
782

783
void pbdrv_bluetooth_stop_observing(void) {
×
784
    observe_callback = NULL;
×
785

786
    if (is_observing) {
×
UNCOV
787
        gap_stop_scan();
×
UNCOV
788
        is_observing = false;
×
789
    }
UNCOV
790
}
×
791

792
#endif // PBDRV_CONFIG_BLUETOOTH_BTSTACK
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc