|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +// Copyright (c) 2021 The Pybricks Authors |
| 3 | + |
| 4 | +#include "py/mpconfig.h" |
| 5 | + |
| 6 | +#if PYBRICKS_PY_IODEVICES && PYBRICKS_PY_PUPDEVICES |
| 7 | + |
| 8 | +#include <stdbool.h> |
| 9 | +#include <stdint.h> |
| 10 | + |
| 11 | +#include <lego_lwp3.h> |
| 12 | + |
| 13 | +#include <pbdrv/bluetooth.h> |
| 14 | +#include <pbio/error.h> |
| 15 | +#include <pbio/task.h> |
| 16 | + |
| 17 | +#include <pybricks/common.h> |
| 18 | +#include <pybricks/parameters.h> |
| 19 | +#include <pybricks/util_mp/pb_kwarg_helper.h> |
| 20 | +#include <pybricks/util_mp/pb_obj_helper.h> |
| 21 | +#include <pybricks/util_pb/pb_error.h> |
| 22 | +#include <pybricks/util_pb/pb_task.h> |
| 23 | + |
| 24 | +#include "py/mphal.h" |
| 25 | +#include "py/runtime.h" |
| 26 | +#include "py/obj.h" |
| 27 | +#include "py/mperrno.h" |
| 28 | + |
| 29 | +// TODO: most of the functions in this class can be shared with the Remote class |
| 30 | + |
| 31 | +#define LWP3_HEADER_SIZE 3 |
| 32 | + |
| 33 | +// MTU is assumed to be 23, not the actual negotiated MTU. |
| 34 | +// A overhead of 3 yields a max message size of 20 (=23-3) |
| 35 | +#define LWP3_MAX_MESSAGE_SIZE 20 |
| 36 | + |
| 37 | +typedef struct { |
| 38 | + pbio_task_t task; |
| 39 | + uint8_t buffer[LWP3_MAX_MESSAGE_SIZE]; |
| 40 | + bool notification_received; |
| 41 | + pbdrv_bluetooth_scan_and_connect_context_t context; |
| 42 | +} pb_lwp3device_t; |
| 43 | + |
| 44 | +STATIC pb_lwp3device_t pb_lwp3device_singleton; |
| 45 | + |
| 46 | +// Handles LEGO Wireless protocol messages from the LWP3 Device |
| 47 | +STATIC void handle_notification(pbdrv_bluetooth_connection_t connection, const uint8_t *value, uint8_t size) { |
| 48 | + pb_lwp3device_t *lwp3device = &pb_lwp3device_singleton; |
| 49 | + |
| 50 | + // Each message overwrites the previous received messages |
| 51 | + // Messages will be lost if they are not read fast enough |
| 52 | + memcpy(lwp3device->buffer, &value[0], (size < LWP3_MAX_MESSAGE_SIZE) ? size : LWP3_MAX_MESSAGE_SIZE); |
| 53 | + |
| 54 | + lwp3device->notification_received = true; |
| 55 | +} |
| 56 | + |
| 57 | +STATIC void lwp3device_connect(const uint8_t hub_kind, const char *name, mp_int_t timeout) { |
| 58 | + pb_lwp3device_t *lwp3device = &pb_lwp3device_singleton; |
| 59 | + |
| 60 | + // REVISIT: for now, we only allow a single connection to a LWP3 device. |
| 61 | + if (pbdrv_bluetooth_is_connected(PBDRV_BLUETOOTH_CONNECTION_PERIPHERAL_LWP3)) { |
| 62 | + pb_assert(PBIO_ERROR_BUSY); |
| 63 | + } |
| 64 | + |
| 65 | + // clear memory after reconnect to empty buffer |
| 66 | + // we are using static memory |
| 67 | + memset(lwp3device, 0, sizeof(*lwp3device)); |
| 68 | + |
| 69 | + lwp3device->context.hub_kind = hub_kind; |
| 70 | + |
| 71 | + if (name) { |
| 72 | + strncpy(lwp3device->context.name, name, sizeof(lwp3device->context.name)); |
| 73 | + } |
| 74 | + |
| 75 | + pbdrv_bluetooth_set_notification_handler(handle_notification); |
| 76 | + pbdrv_bluetooth_scan_and_connect(&lwp3device->task, &lwp3device->context); |
| 77 | + pb_wait_task(&lwp3device->task, timeout); |
| 78 | +} |
| 79 | + |
| 80 | +STATIC void lwp3device_assert_connected(void) { |
| 81 | + if (!pbdrv_bluetooth_is_connected(PBDRV_BLUETOOTH_CONNECTION_PERIPHERAL_LWP3)) { |
| 82 | + mp_raise_OSError(MP_ENODEV); |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +typedef struct _pb_type_iodevices_LWP3Device_obj_t { |
| 87 | + mp_obj_base_t base; |
| 88 | +} pb_type_iodevices_LWP3Device_obj_t; |
| 89 | + |
| 90 | +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) { |
| 91 | + PB_PARSE_ARGS_CLASS(n_args, n_kw, args, |
| 92 | + PB_ARG_REQUIRED(hub_kind), |
| 93 | + PB_ARG_DEFAULT_NONE(name), |
| 94 | + PB_ARG_DEFAULT_INT(timeout, 10000)); |
| 95 | + |
| 96 | + pb_type_iodevices_LWP3Device_obj_t *self = m_new_obj(pb_type_iodevices_LWP3Device_obj_t); |
| 97 | + self->base.type = (mp_obj_type_t *)type; |
| 98 | + |
| 99 | + uint8_t hub_kind = pb_obj_get_positive_int(hub_kind_in); |
| 100 | + |
| 101 | + const char *name = name_in == mp_const_none ? NULL : mp_obj_str_get_str(name_in); |
| 102 | + mp_int_t timeout = timeout_in == mp_const_none ? -1 : pb_obj_get_positive_int(timeout_in); |
| 103 | + lwp3device_connect(hub_kind, name, timeout); |
| 104 | + |
| 105 | + return MP_OBJ_FROM_PTR(self); |
| 106 | +} |
| 107 | + |
| 108 | +STATIC mp_obj_t lwp3device_name(size_t n_args, const mp_obj_t *args) { |
| 109 | + pb_lwp3device_t *lwp3device = &pb_lwp3device_singleton; |
| 110 | + |
| 111 | + lwp3device_assert_connected(); |
| 112 | + |
| 113 | + if (n_args == 2) { |
| 114 | + size_t len; |
| 115 | + const char *name = mp_obj_str_get_data(args[1], &len); |
| 116 | + |
| 117 | + if (len == 0 || len > LWP3_MAX_MESSAGE_SIZE) { |
| 118 | + mp_raise_ValueError(MP_ERROR_TEXT("bad name length")); |
| 119 | + } |
| 120 | + |
| 121 | + struct { |
| 122 | + pbdrv_bluetooth_value_t value; |
| 123 | + uint8_t length; |
| 124 | + uint8_t hub; |
| 125 | + uint8_t type; |
| 126 | + uint8_t property; |
| 127 | + uint8_t operation; |
| 128 | + char payload[LWP3_MAX_HUB_PROPERTY_NAME_SIZE]; |
| 129 | + } __attribute__((packed)) msg; |
| 130 | + |
| 131 | + msg.value.size = msg.length = len + 5; |
| 132 | + msg.hub = 0; |
| 133 | + msg.type = LWP3_MSG_TYPE_HUB_PROPERTIES; |
| 134 | + msg.property = LWP3_HUB_PROPERTY_NAME; |
| 135 | + msg.operation = LWP3_HUB_PROPERTY_OP_SET; |
| 136 | + memcpy(msg.payload, name, len); |
| 137 | + |
| 138 | + // NB: operation is not cancelable, so timeout is not used |
| 139 | + pbdrv_bluetooth_write_remote(&lwp3device->task, &msg.value); |
| 140 | + pb_wait_task(&lwp3device->task, -1); |
| 141 | + |
| 142 | + // assuming write was successful instead of reading back from the handset |
| 143 | + memcpy(lwp3device->context.name, name, len); |
| 144 | + lwp3device->context.name[len] = 0; |
| 145 | + |
| 146 | + return mp_const_none; |
| 147 | + } |
| 148 | + |
| 149 | + return mp_obj_new_str(lwp3device->context.name, strlen(lwp3device->context.name)); |
| 150 | +} |
| 151 | +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(lwp3device_name_obj, 1, 2, lwp3device_name); |
| 152 | + |
| 153 | +STATIC mp_obj_t lwp3device_write(mp_obj_t self_in, mp_obj_t buf_in) { |
| 154 | + pb_lwp3device_t *lwp3device = &pb_lwp3device_singleton; |
| 155 | + |
| 156 | + lwp3device_assert_connected(); |
| 157 | + |
| 158 | + mp_buffer_info_t bufinfo; |
| 159 | + mp_get_buffer_raise(buf_in, &bufinfo, MP_BUFFER_READ); |
| 160 | + |
| 161 | + if (bufinfo.len < LWP3_HEADER_SIZE || bufinfo.len > LWP3_MAX_MESSAGE_SIZE) { |
| 162 | + mp_raise_ValueError(MP_ERROR_TEXT("bad length")); |
| 163 | + } |
| 164 | + if (((uint8_t *)bufinfo.buf)[0] != bufinfo.len) { |
| 165 | + mp_raise_ValueError(MP_ERROR_TEXT("length in header wrong")); |
| 166 | + } |
| 167 | + |
| 168 | + struct { |
| 169 | + pbdrv_bluetooth_value_t value; |
| 170 | + char payload[LWP3_MAX_MESSAGE_SIZE]; |
| 171 | + } __attribute__((packed)) msg = { |
| 172 | + .value.size = bufinfo.len, |
| 173 | + }; |
| 174 | + memcpy(msg.payload, bufinfo.buf, bufinfo.len); |
| 175 | + |
| 176 | + pbdrv_bluetooth_write_remote(&lwp3device->task, &msg.value); |
| 177 | + pb_wait_task(&lwp3device->task, -1); |
| 178 | + |
| 179 | + return mp_const_none; |
| 180 | +} |
| 181 | +STATIC MP_DEFINE_CONST_FUN_OBJ_2(lwp3device_write_obj, lwp3device_write); |
| 182 | + |
| 183 | +STATIC mp_obj_t lwp3device_read(mp_obj_t self_in) { |
| 184 | + pb_lwp3device_t *lwp3device = &pb_lwp3device_singleton; |
| 185 | + |
| 186 | + // wait until a notification is received |
| 187 | + for (;;) { |
| 188 | + lwp3device_assert_connected(); |
| 189 | + |
| 190 | + if (lwp3device->notification_received) { |
| 191 | + lwp3device->notification_received = false; |
| 192 | + break; |
| 193 | + } |
| 194 | + |
| 195 | + MICROPY_EVENT_POLL_HOOK |
| 196 | + } |
| 197 | + |
| 198 | + size_t len = lwp3device->buffer[0]; |
| 199 | + |
| 200 | + if (len < LWP3_HEADER_SIZE || len > LWP3_MAX_MESSAGE_SIZE) { |
| 201 | + mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("bad data")); |
| 202 | + } |
| 203 | + |
| 204 | + return mp_obj_new_bytes(lwp3device->buffer, len); |
| 205 | +} |
| 206 | +STATIC MP_DEFINE_CONST_FUN_OBJ_1(lwp3device_read_obj, lwp3device_read); |
| 207 | + |
| 208 | +STATIC const mp_rom_map_elem_t pb_type_iodevices_LWP3Device_locals_dict_table[] = { |
| 209 | + { MP_ROM_QSTR(MP_QSTR_name), MP_ROM_PTR(&lwp3device_name_obj) }, |
| 210 | + { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&lwp3device_write_obj) }, |
| 211 | + { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&lwp3device_read_obj) }, |
| 212 | +}; |
| 213 | +STATIC MP_DEFINE_CONST_DICT(pb_type_iodevices_LWP3Device_locals_dict, pb_type_iodevices_LWP3Device_locals_dict_table); |
| 214 | + |
| 215 | +const mp_obj_type_t pb_type_iodevices_LWP3Device = { |
| 216 | + { &mp_type_type }, |
| 217 | + .name = MP_QSTR_LWP3Device, |
| 218 | + .make_new = pb_type_iodevices_LWP3Device_make_new, |
| 219 | + .locals_dict = (mp_obj_dict_t *)&pb_type_iodevices_LWP3Device_locals_dict, |
| 220 | +}; |
| 221 | + |
| 222 | +#endif // PYBRICKS_PY_IODEVICES && PYBRICKS_PY_PUPDEVICES |
0 commit comments