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

pybricks / pybricks-micropython / 6675885095

28 Oct 2023 08:38AM UTC coverage: 56.053% (+10.0%) from 46.074%
6675885095

push

github

laurensvalk
pybricks.hubs.MoveHub: Use standard hub init.

The Move Hub cannot have true vector axes, but we can still use this API to initialize the hub using numeric indices.

This ensures we can use the custom orientation not just in tilt, but also in acceleration like we do on other hubs.

3616 of 6451 relevant lines covered (56.05%)

20895680.75 hits per line

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

27.2
/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 "hci_transport_h4.h"
26
#include "pybricks_service_server.h"
27

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

148
    return ATT_ERROR_UNLIKELY_ERROR;
×
149
}
150

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

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

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

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

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

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

186
    for (;;) {
×
187
        pbio_task_t *current_task = list_head(task_queue);
148✔
188

189
        if (!current_task) {
148✔
190
            break;
148✔
191
        }
192

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

200
        break;
×
201
    }
202

203
    event_packet = NULL;
148✔
204

205
    if (bluetooth_on_event) {
148✔
206
        bluetooth_on_event();
148✔
207
    }
208
}
148✔
209

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

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

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

238
    propagate_event(packet);
×
239
}
×
240

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

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

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

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

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

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

298
        default:
×
299
            break;
×
300
    }
301

302
    propagate_event(packet);
×
303
}
×
304

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

309
    if (packet_type != HCI_EVENT_PACKET) {
148✔
310
        return;
×
311
    }
312

313
    switch (hci_event_packet_get_type(packet)) {
148✔
314
        case HCI_EVENT_LE_META:
2✔
315
            if (hci_event_le_meta_get_subevent_code(packet) != HCI_SUBEVENT_LE_CONNECTION_COMPLETE) {
2✔
316
                break;
1✔
317
            }
318

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

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

332
                handset.con_handle = hci_subevent_le_connection_complete_get_connection_handle(packet);
×
333

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

346
            break;
1✔
347

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

359
            break;
×
360

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

367
            gap_event_advertising_report_get_address(packet, address);
×
368

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

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

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

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

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

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

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

426
            break;
×
427
        }
428

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

433
    propagate_event(packet);
148✔
434
}
435

436
// ATT Client Read Callback for Dynamic Data
437
// - if buffer == NULL, don't copy data, just return size of value
438
// - if buffer != NULL, copy data and return number bytes copied
439
// @param offset defines start of attribute value
440
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) {
×
441
    uint16_t att_value_len;
442

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

451
        default:
×
452
            return 0;
×
453
    }
454
}
455

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

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

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

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

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

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

474
    l2cap_init();
15✔
475

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

562
    scan_resp_data[9] = HUB_VARIANT;
1✔
563

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

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

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

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

581
bool pbdrv_bluetooth_is_connected(pbdrv_bluetooth_connection_t connection) {
94✔
582
    if (connection == PBDRV_BLUETOOTH_CONNECTION_LE && le_con_handle != HCI_CON_HANDLE_INVALID) {
94✔
583
        return true;
11✔
584
    }
585

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

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

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

598
    return false;
73✔
599
}
600

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

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

608
    send_request.context = context;
4✔
609

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

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

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

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

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

635
    PT_BEGIN(pt);
×
636

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

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

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

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

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

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

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

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

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

687
    PT_END(pt);
×
688
}
689

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

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

697
    PT_BEGIN(pt);
×
698

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

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

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

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

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

726
    PT_END(pt);
×
727
}
728

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

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

739
static PT_THREAD(start_broadcasting_task(struct pt *pt, pbio_task_t *task)) {
×
740
    pbdrv_bluetooth_value_t *value = task->context;
×
741

742
    static struct timer broadcast_delay;
743

744
    PT_BEGIN(pt);
×
745

746
    if (value->size > LE_ADVERTISING_DATA_SIZE) {
×
747
        task->status = PBIO_ERROR_INVALID_ARG;
×
748
        PT_EXIT(pt);
×
749
    }
750

751
    bd_addr_t null_addr = { };
×
752
    gap_advertisements_set_params(0xA0, 0xA0, ADV_NONCONN_IND, 0, null_addr, 0x7, 0);
×
753

754
    // have to keep copy of data here since BTStack doesn't copy
755
    static uint8_t static_data[LE_ADVERTISING_DATA_SIZE];
756
    memcpy(static_data, value->data, value->size);
×
757

758
    gap_advertisements_set_data(value->size, static_data);
×
759

760
    if (!is_broadcasting) {
×
761
        gap_advertisements_enable(true);
×
762
        is_broadcasting = true;
×
763
    }
764

765
    // Delay to allow for advertising to start. FIXME: This is technically only
766
    // needed if a previous broadcast was started sooner than this. It would be
767
    // better to conditionally await just before the broadcast so we can avoid
768
    // unnecessary delays.
769
    timer_set(&broadcast_delay, 10);
×
770
    PT_WAIT_UNTIL(pt, timer_expired(&broadcast_delay));
×
771

772
    // REVISIT: use callback to actually wait for start?
773
    task->status = PBIO_SUCCESS;
×
774

775
    PT_END(pt);
×
776
}
777

778
void pbdrv_bluetooth_start_broadcasting(pbio_task_t *task, pbdrv_bluetooth_value_t *value) {
×
779
    start_task(task, start_broadcasting_task, value);
×
780
}
×
781

782
void pbdrv_bluetooth_stop_broadcasting(void) {
×
783
    if (is_broadcasting) {
×
784
        gap_advertisements_enable(false);
×
785
        is_broadcasting = false;
×
786
    }
787
}
×
788

789
static PT_THREAD(start_observing_task(struct pt *pt, pbio_task_t *task)) {
×
790
    pbdrv_bluetooth_start_observing_callback_t callback = task->context;
×
791

792
    PT_BEGIN(pt);
×
793

794
    observe_callback = callback;
×
795

796
    if (!is_observing) {
×
797
        start_observing();
×
798
        is_observing = true;
×
799
    }
800

801
    // REVISIT: use callback to actually wait for start?
802
    task->status = PBIO_SUCCESS;
×
803

804
    PT_END(pt);
×
805
}
806

807
void pbdrv_bluetooth_start_observing(pbio_task_t *task, pbdrv_bluetooth_start_observing_callback_t callback) {
×
808
    start_task(task, start_observing_task, callback);
×
809
}
×
810

811
void pbdrv_bluetooth_stop_observing(void) {
×
812
    observe_callback = NULL;
×
813

814
    if (is_observing) {
×
815
        gap_stop_scan();
×
816
        is_observing = false;
×
817
    }
818
}
×
819

820
#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