Skip to content

Commit e34e303

Browse files
committed
pybricks.iodevices.LWP3Device: Buffer incoming notifications.
Allows to read without blocking and allows subscribing to multiple sensor values without losing data.
1 parent 8dea17f commit e34e303

File tree

2 files changed

+72
-40
lines changed

2 files changed

+72
-40
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
### Changed
1212
- Extensive overhaul of UART and port drivers on all hubs. This affects all
1313
official LEGO sensors on all hubs.
14+
- LWP3Device.read() now gives buffered notification values instead of blocking
15+
until a value arrives.
1416

1517
### Fixed
1618
- Reduced hanging when broadcasting and observing at the same time with Technic

pybricks/iodevices/pb_type_iodevices_lwp3device.c

Lines changed: 70 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
// MTU is assumed to be 23, not the actual negotiated MTU.
3636
// A overhead of 3 yields a max message size of 20 (=23-3)
37-
#define LWP3_MAX_MESSAGE_SIZE 20
37+
#define LWP3_MAX_MESSAGE_SIZE (23 - LWP3_HEADER_SIZE)
3838

3939
enum {
4040
REMOTE_PORT_LEFT_BUTTONS = 0,
@@ -88,8 +88,22 @@ static pbdrv_bluetooth_peripheral_char_t pb_lwp3device_char = {
8888
typedef struct {
8989
pbio_task_t task;
9090
#if PYBRICKS_PY_IODEVICES
91-
uint8_t buffer[LWP3_MAX_MESSAGE_SIZE];
92-
bool notification_received;
91+
/**
92+
* Buffer of incoming notifications.
93+
*/
94+
uint8_t *noti_data;
95+
/**
96+
* Maximum number of stored notifications.
97+
*/
98+
uint8_t noti_num;
99+
/**
100+
* Index to read (the oldest data)
101+
*/
102+
uint8_t noti_idx_read;
103+
/**
104+
* Index to write (next free slot)
105+
*/
106+
uint8_t noti_idx_write;
93107
#endif // PYBRICKS_PY_IODEVICES
94108
uint8_t left[3];
95109
uint8_t right[3];
@@ -102,25 +116,13 @@ typedef struct {
102116
} pb_lwp3device_t;
103117

104118
static pb_lwp3device_t pb_lwp3device_singleton;
119+
void *pb_lwp3device_root_pointer = &pb_lwp3device_singleton;
120+
MP_REGISTER_ROOT_POINTER(void *pb_lwp3device_root_pointer);
105121

106-
// Handles LEGO Wireless protocol messages from the LWP3 Device.
107-
static pbio_pybricks_error_t handle_notification(pbdrv_bluetooth_connection_t connection, const uint8_t *value, uint32_t size) {
122+
// Handles LEGO Wireless protocol messages from the Powered Up Remote.
123+
static pbio_pybricks_error_t handle_remote_notification(pbdrv_bluetooth_connection_t connection, const uint8_t *value, uint32_t size) {
108124
pb_lwp3device_t *lwp3device = &pb_lwp3device_singleton;
109125

110-
#if PYBRICKS_PY_IODEVICES
111-
// Each message overwrites the previous received messages
112-
// Messages will be lost if they are not read fast enough
113-
memcpy(lwp3device->buffer, &value[0], (size < LWP3_MAX_MESSAGE_SIZE) ? size : LWP3_MAX_MESSAGE_SIZE);
114-
lwp3device->notification_received = true;
115-
116-
if (lwp3device->hub_kind != LWP3_HUB_KIND_HANDSET) {
117-
// This is not a handset, so we don't care about button state.
118-
return PBIO_PYBRICKS_ERROR_OK;
119-
}
120-
#endif // PYBRICKS_PY_IODEVICES
121-
122-
// The LWP3 class is mostly just used for the remote, so do the work
123-
// to parse the button state here.
124126
if (value[0] == 5 && value[2] == LWP3_MSG_TYPE_HW_NET_CMDS && value[3] == LWP3_HW_NET_CMD_CONNECTION_REQ) {
125127
// This message is meant for something else, but contains the center button state
126128
lwp3device->center = value[4];
@@ -186,7 +188,7 @@ static void pb_lwp3device_assert_connected(void) {
186188
}
187189
}
188190

189-
static void pb_lwp3device_connect(const char *name, mp_int_t timeout, lwp3_hub_kind_t hub_kind, bool pair) {
191+
static void pb_lwp3device_connect(const char *name, mp_int_t timeout, lwp3_hub_kind_t hub_kind, pbdrv_bluetooth_receive_handler_t notification_handler, bool pair) {
190192
pb_lwp3device_t *lwp3device = &pb_lwp3device_singleton;
191193

192194
// REVISIT: for now, we only allow a single connection to a LWP3 device.
@@ -201,7 +203,9 @@ static void pb_lwp3device_connect(const char *name, mp_int_t timeout, lwp3_hub_k
201203

202204
// needed to ensure that no buttons are "pressed" after reconnecting since
203205
// we are using static memory
204-
memset(lwp3device, 0, sizeof(*lwp3device));
206+
memset(&lwp3device->left, 0, sizeof(lwp3device->left));
207+
memset(&lwp3device->right, 0, sizeof(lwp3device->left));
208+
lwp3device->center = 0;
205209

206210
// Hub kind and name are set to filter advertisements and responses.
207211
lwp3device->hub_kind = hub_kind;
@@ -218,7 +222,7 @@ static void pb_lwp3device_connect(const char *name, mp_int_t timeout, lwp3_hub_k
218222
pbdrv_bluetooth_peripheral_scan_and_connect(&lwp3device->task,
219223
lwp3_advertisement_matches,
220224
lwp3_advertisement_response_matches,
221-
handle_notification,
225+
notification_handler,
222226
options);
223227
pb_module_tools_pbio_task_do_blocking(&lwp3device->task, timeout);
224228

@@ -396,7 +400,7 @@ static mp_obj_t pb_type_pupdevices_Remote_make_new(const mp_obj_type_t *type, si
396400

397401
const char *name = name_in == mp_const_none ? NULL : mp_obj_str_get_str(name_in);
398402
mp_int_t timeout = timeout_in == mp_const_none ? -1 : pb_obj_get_positive_int(timeout_in);
399-
pb_lwp3device_connect(name, timeout, LWP3_HUB_KIND_HANDSET, false);
403+
pb_lwp3device_connect(name, timeout, LWP3_HUB_KIND_HANDSET, handle_remote_notification, false);
400404
pb_lwp3device_configure_remote();
401405

402406
self->buttons = pb_type_Keypad_obj_new(pb_type_remote_button_pressed);
@@ -480,22 +484,44 @@ MP_DEFINE_CONST_OBJ_TYPE(pb_type_pupdevices_Remote,
480484

481485
#if PYBRICKS_PY_IODEVICES
482486

487+
static pbio_pybricks_error_t handle_lwp3_generic_notification(pbdrv_bluetooth_connection_t connection, const uint8_t *value, uint32_t size) {
488+
pb_lwp3device_t *self = &pb_lwp3device_singleton;
489+
490+
if (!self->noti_data || !self->noti_num) {
491+
// Allocated data not ready, but no error.
492+
return PBIO_PYBRICKS_ERROR_OK;
493+
}
494+
495+
memcpy(&self->noti_data[self->noti_idx_write * LWP3_MAX_MESSAGE_SIZE], &value[0], (size < LWP3_MAX_MESSAGE_SIZE) ? size : LWP3_MAX_MESSAGE_SIZE);
496+
self->noti_idx_write = (self->noti_idx_write + 1) % self->noti_num;
497+
return PBIO_PYBRICKS_ERROR_OK;
498+
}
499+
483500
static mp_obj_t pb_type_iodevices_LWP3Device_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
484501
PB_PARSE_ARGS_CLASS(n_args, n_kw, args,
485502
PB_ARG_REQUIRED(hub_kind),
486503
PB_ARG_DEFAULT_NONE(name),
487504
PB_ARG_DEFAULT_INT(timeout, 10000),
488-
PB_ARG_DEFAULT_FALSE(pair));
505+
PB_ARG_DEFAULT_FALSE(pair),
506+
PB_ARG_DEFAULT_INT(num_notifications, 8));
489507

490-
pb_type_pupdevices_Remote_obj_t *self = mp_obj_malloc(pb_type_pupdevices_Remote_obj_t, type);
508+
mp_obj_base_t *obj = mp_obj_malloc(mp_obj_base_t, type);
509+
pb_lwp3device_t *self = &pb_lwp3device_singleton;
491510

492511
const char *name = name_in == mp_const_none ? NULL : mp_obj_str_get_str(name_in);
493512
mp_int_t timeout = timeout_in == mp_const_none ? -1 : pb_obj_get_positive_int(timeout_in);
494513
uint8_t hub_kind = pb_obj_get_positive_int(hub_kind_in);
495514
bool pair = mp_obj_is_true(pair_in);
496-
pb_lwp3device_connect(name, timeout, hub_kind, pair);
497515

498-
return MP_OBJ_FROM_PTR(self);
516+
self->noti_num = mp_obj_get_int(num_notifications_in);
517+
self->noti_num = self->noti_num ? self->noti_num : 1;
518+
self->noti_data = m_malloc0(LWP3_MAX_MESSAGE_SIZE * self->noti_num);
519+
self->noti_idx_read = 0;
520+
self->noti_idx_write = 0;
521+
522+
pb_lwp3device_connect(name, timeout, hub_kind, handle_lwp3_generic_notification, pair);
523+
524+
return MP_OBJ_FROM_PTR(obj);
499525
}
500526

501527
static mp_obj_t lwp3device_write(mp_obj_t self_in, mp_obj_t buf_in) {
@@ -528,27 +554,31 @@ static mp_obj_t lwp3device_write(mp_obj_t self_in, mp_obj_t buf_in) {
528554
static MP_DEFINE_CONST_FUN_OBJ_2(lwp3device_write_obj, lwp3device_write);
529555

530556
static mp_obj_t lwp3device_read(mp_obj_t self_in) {
531-
pb_lwp3device_t *lwp3device = &pb_lwp3device_singleton;
557+
pb_lwp3device_t *self = &pb_lwp3device_singleton;
532558

533-
// wait until a notification is received
534-
for (;;) {
535-
pb_lwp3device_assert_connected();
536-
537-
if (lwp3device->notification_received) {
538-
lwp3device->notification_received = false;
539-
break;
540-
}
559+
pb_lwp3device_assert_connected();
541560

542-
MICROPY_EVENT_POLL_HOOK
561+
if (self->noti_idx_read == self->noti_idx_write || !self->noti_num || !self->noti_data) {
562+
return mp_const_none;
543563
}
544564

545-
size_t len = lwp3device->buffer[0];
565+
// Update index before returning, else bad values would not be cleared.
566+
uint8_t index = self->noti_idx_read;
567+
self->noti_idx_read = (self->noti_idx_read + 1) % self->noti_num;
546568

569+
// First byte is the size.
570+
uint8_t len = self->noti_data[index * LWP3_MAX_MESSAGE_SIZE];
547571
if (len < LWP3_HEADER_SIZE || len > LWP3_MAX_MESSAGE_SIZE) {
548-
mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("bad data"));
572+
// This is rare but it can happen sometimes. It is better to just
573+
// ignore it rather than raise and crash the user application.
574+
return mp_const_none;
549575
}
550576

551-
return mp_obj_new_bytes(lwp3device->buffer, len);
577+
// Allocation of the return object may drive the runloop and process
578+
// new incoming messages, so copy data atomically before that happens.
579+
uint8_t message[LWP3_MAX_MESSAGE_SIZE];
580+
memcpy(message, &self->noti_data[index * LWP3_MAX_MESSAGE_SIZE], len);
581+
return mp_obj_new_bytes(message, len);
552582
}
553583
static MP_DEFINE_CONST_FUN_OBJ_1(lwp3device_read_obj, lwp3device_read);
554584

0 commit comments

Comments
 (0)