From b1b544a8a2d4177abbab5206a0eaf454589ef227 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 26 Jan 2026 09:44:15 +0100 Subject: [PATCH 1/2] pbio/port_dcm_pup: Detect and use switch inputs. Co-authored-by: Laurens Valk --- CHANGELOG.md | 4 ++ lib/pbio/src/port_dcm_pup.c | 19 ++++-- .../iodevices/pb_type_iodevices_pupdevice.c | 68 +++++++++++++++---- 3 files changed, 73 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58a698cea..96147841a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and `XboxController` classes, along with a `connect()` method to optionally connect later ([support#1800]). - Added `timeout` and `name` parameters to the `XboxController`. +- Added support for Powered Up touch sensors that are supported according to + the specification, but were never released. Users can make their own switch + inputs ([pybricks-micropython#454]). ### Changed - Changed the default `XboxController` connection timeout from indefinite @@ -18,6 +21,7 @@ [support#1382]: https://github.com/pybricks/support/issues/1382 [support#1800]: https://github.com/pybricks/support/issues/1800 +[pybricks-micropython#454]: https://github.com/pybricks/pybricks-micropython/pull/454 ## [4.0.0b5] - 2026-01-30 diff --git a/lib/pbio/src/port_dcm_pup.c b/lib/pbio/src/port_dcm_pup.c index 767c8aa6c..182d63edf 100644 --- a/lib/pbio/src/port_dcm_pup.c +++ b/lib/pbio/src/port_dcm_pup.c @@ -43,6 +43,8 @@ struct _pbio_port_dcm_t { dev_id_group_t dev_id1_group; uint8_t gpio_value; uint8_t prev_gpio_value; + /** Touch sensor data. */ + uint32_t sensor_data; }; pbio_port_dcm_t dcm_state[PBIO_CONFIG_PORT_DCM_NUM_DEV]; @@ -127,8 +129,7 @@ pbio_error_t pbio_port_dcm_thread(pbio_os_state_t *state, pbio_os_timer_t *timer PBIO_OS_AWAIT_MS(state, timer, DCM_AWAIT_MS); // ID1 is inverse of touch sensor value - // TODO: save this value to sensor dcm - // sensor_data = !pbdrv_gpio_input(&pins->p5); + dcm->sensor_data = !pbdrv_gpio_input(&pins->p5); } // if ID2 changed from low to high else if (dcm->prev_gpio_value == 0 && dcm->gpio_value == 1) { @@ -320,13 +321,21 @@ pbio_error_t pbio_port_dcm_assert_type_id(pbio_port_dcm_t *dcm, lego_device_type case LEGO_DEVICE_TYPE_ID_LPF2_MMOTOR: case LEGO_DEVICE_TYPE_ID_LPF2_TRAIN: case LEGO_DEVICE_TYPE_ID_LPF2_LIGHT: - // On Powered Up, the only known existing passive devices are DC + // On Powered Up, the only known official passive devices are DC // devices. Pass if requesting a specific match or any DC device. if (*type_id == LEGO_DEVICE_TYPE_ID_ANY_DC_MOTOR || *type_id == dcm->prev_type_id) { *type_id = dcm->prev_type_id; return PBIO_SUCCESS; } return PBIO_ERROR_NO_DEV; + case LEGO_DEVICE_TYPE_ID_LPF2_TOUCH: + // The Powered Up protocol appears to have been designed to support + // basic switches as touch sensors. None of such sensors were ever + // released, but we can still detect matching custom devices here. + if (*type_id == LEGO_DEVICE_TYPE_ID_LPF2_TOUCH) { + return PBIO_SUCCESS; + } + return PBIO_ERROR_NO_DEV; case LEGO_DEVICE_TYPE_ID_LPF2_UNKNOWN_UART: if (*type_id == LEGO_DEVICE_TYPE_ID_ANY_LUMP_UART) { return PBIO_SUCCESS; @@ -338,7 +347,9 @@ pbio_error_t pbio_port_dcm_assert_type_id(pbio_port_dcm_t *dcm, lego_device_type } uint32_t pbio_port_dcm_get_analog_value(pbio_port_dcm_t *dcm, const pbdrv_ioport_pins_t *pins, bool active) { - return 0; + // This platform does not have any analog sensors, but we can use this to + // return the state of a passive touch sensor. + return dcm->sensor_data; } pbio_error_t pbio_port_dcm_get_analog_rgba(pbio_port_dcm_t *dcm, pbio_port_dcm_analog_rgba_t *rgba) { diff --git a/pybricks/iodevices/pb_type_iodevices_pupdevice.c b/pybricks/iodevices/pb_type_iodevices_pupdevice.c index f0da7c452..2d00d2e1e 100644 --- a/pybricks/iodevices/pb_type_iodevices_pupdevice.c +++ b/pybricks/iodevices/pb_type_iodevices_pupdevice.c @@ -32,35 +32,36 @@ typedef struct _iodevices_PUPDevice_obj_t { uint8_t last_mode; // ID of a passive device, if any. lego_device_type_id_t passive_id; + // Device port. + pbio_port_t *port; } iodevices_PUPDevice_obj_t; /** * Tests if the given device is a passive device and stores ID. * * @param [in] self The PUP device. - * @param [in] port_in The port. * @return True if passive device, false otherwise. */ -static bool init_passive_pup_device(iodevices_PUPDevice_obj_t *self, mp_obj_t port_in) { - pb_module_tools_assert_blocking(); - - pbio_port_id_t port_id = pb_type_enum_get_value(port_in, &pb_enum_type_Port); +static bool init_passive_pup_device(iodevices_PUPDevice_obj_t *self) { - // Get the port instance. - pbio_port_t *port; - pb_assert(pbio_port_get_port(port_id, &port)); + // Check for custom devices that follow the Powered Up spec for simple + // switches as touch sensors. + uint32_t value; + pbio_error_t err = pbio_port_get_analog_value(self->port, LEGO_DEVICE_TYPE_ID_LPF2_TOUCH, false, &value); + if (err == PBIO_SUCCESS) { + self->passive_id = LEGO_DEVICE_TYPE_ID_LPF2_TOUCH; + return true; + } + // Check for DC motor or light. lego_device_type_id_t type_id = LEGO_DEVICE_TYPE_ID_ANY_DC_MOTOR; pbio_dcmotor_t *dcmotor; - pbio_error_t err = pbio_port_get_dcmotor(port, &type_id, &dcmotor); - + err = pbio_port_get_dcmotor(self->port, &type_id, &dcmotor); if (err == PBIO_SUCCESS) { self->passive_id = type_id; return true; } return false; - - self->passive_id = type_id; } // pybricks.iodevices.PUPDevice.__init__ @@ -70,8 +71,14 @@ static mp_obj_t iodevices_PUPDevice_make_new(const mp_obj_type_t *type, size_t n iodevices_PUPDevice_obj_t *self = mp_obj_malloc(iodevices_PUPDevice_obj_t, type); + pb_module_tools_assert_blocking(); + + // Get the port instance. + pbio_port_id_t port_id = pb_type_enum_get_value(port_in, &pb_enum_type_Port); + pb_assert(pbio_port_get_port(port_id, &self->port)); + // For backwards compatibility, allow class to be used with passive devices. - if (init_passive_pup_device(self, port_in)) { + if (init_passive_pup_device(self)) { return MP_OBJ_FROM_PTR(self); } @@ -158,13 +165,46 @@ static mp_obj_t get_pup_data_tuple(mp_obj_t self_in) { return mp_obj_new_tuple(mode_info[current_mode].num_values, values); } +static mp_obj_t iodevices_PUPDevice_touch_sensor_true(mp_obj_t self_in) { + return mp_const_true; +} + +static mp_obj_t iodevices_PUPDevice_touch_sensor_false(mp_obj_t self_in) { + return mp_const_false; +} + +static pbio_error_t iodevices_PUPDevice_touch_sensor_iter_once(pbio_os_state_t *state, mp_obj_t self_in) { + return PBIO_SUCCESS; +} + // pybricks.iodevices.PUPDevice.read static mp_obj_t iodevices_PUPDevice_read(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { PB_PARSE_ARGS_METHOD(n_args, pos_args, kw_args, iodevices_PUPDevice_obj_t, self, PB_ARG_REQUIRED(mode)); - // Passive devices don't support reading. + // Allow reading from passive touch sensors as per the Powered Up spec. + // These do not have modes. For this special case, alwyas return a bool, + // even in async mode when other reads would return awaitables. + if (self->passive_id == LEGO_DEVICE_TYPE_ID_LPF2_TOUCH) { + uint32_t value; + pb_assert(pbio_port_get_analog_value(self->port, self->passive_id, false, &value)); + + if (!pb_module_tools_run_loop_is_active()) { + return mp_obj_new_bool(value); + } + + // REVISIT: we could probably make something more efficient here. + pb_type_async_t config = { + .parent_obj = MP_OBJ_FROM_PTR(self), + .iter_once = iodevices_PUPDevice_touch_sensor_iter_once, + .return_map = value ? iodevices_PUPDevice_touch_sensor_true : iodevices_PUPDevice_touch_sensor_false, + }; + + return pb_type_async_wait_or_await(&config, &self->device_base.last_awaitable, false); + } + + // Other passive devices don't support reading. if (self->passive_id != LEGO_DEVICE_TYPE_ID_LPF2_UNKNOWN_UART) { pb_assert(PBIO_ERROR_INVALID_OP); } From c31d07747431454b0597460238d1aa3f51f4cef3 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 2 Feb 2026 09:36:57 +0100 Subject: [PATCH 2/2] pybricks.tools: Add helper for awaitable constants. We have a few functions with this structure: async def some_func(): if simple_case: # This commits adds a helper to do this: return some_constant return await some_awaitable_operation() Also apply this to the touch sensor value of the Powered Up device. --- .../iodevices/pb_type_iodevices_pupdevice.c | 26 +-------- pybricks/tools/pb_type_async.c | 56 ++++++++++++++++++- pybricks/tools/pb_type_async.h | 2 + 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/pybricks/iodevices/pb_type_iodevices_pupdevice.c b/pybricks/iodevices/pb_type_iodevices_pupdevice.c index 2d00d2e1e..1f472e742 100644 --- a/pybricks/iodevices/pb_type_iodevices_pupdevice.c +++ b/pybricks/iodevices/pb_type_iodevices_pupdevice.c @@ -165,18 +165,6 @@ static mp_obj_t get_pup_data_tuple(mp_obj_t self_in) { return mp_obj_new_tuple(mode_info[current_mode].num_values, values); } -static mp_obj_t iodevices_PUPDevice_touch_sensor_true(mp_obj_t self_in) { - return mp_const_true; -} - -static mp_obj_t iodevices_PUPDevice_touch_sensor_false(mp_obj_t self_in) { - return mp_const_false; -} - -static pbio_error_t iodevices_PUPDevice_touch_sensor_iter_once(pbio_os_state_t *state, mp_obj_t self_in) { - return PBIO_SUCCESS; -} - // pybricks.iodevices.PUPDevice.read static mp_obj_t iodevices_PUPDevice_read(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { PB_PARSE_ARGS_METHOD(n_args, pos_args, kw_args, @@ -189,19 +177,7 @@ static mp_obj_t iodevices_PUPDevice_read(size_t n_args, const mp_obj_t *pos_args if (self->passive_id == LEGO_DEVICE_TYPE_ID_LPF2_TOUCH) { uint32_t value; pb_assert(pbio_port_get_analog_value(self->port, self->passive_id, false, &value)); - - if (!pb_module_tools_run_loop_is_active()) { - return mp_obj_new_bool(value); - } - - // REVISIT: we could probably make something more efficient here. - pb_type_async_t config = { - .parent_obj = MP_OBJ_FROM_PTR(self), - .iter_once = iodevices_PUPDevice_touch_sensor_iter_once, - .return_map = value ? iodevices_PUPDevice_touch_sensor_true : iodevices_PUPDevice_touch_sensor_false, - }; - - return pb_type_async_wait_or_await(&config, &self->device_base.last_awaitable, false); + return pb_type_async_return_result(mp_obj_new_bool(value), &self->device_base.last_awaitable); } // Other passive devices don't support reading. diff --git a/pybricks/tools/pb_type_async.c b/pybricks/tools/pb_type_async.c index a85b920a3..8ea914de5 100644 --- a/pybricks/tools/pb_type_async.c +++ b/pybricks/tools/pb_type_async.c @@ -103,7 +103,7 @@ MP_DEFINE_CONST_OBJ_TYPE(pb_type_async, * Returns an awaitable operation if the runloop is active, or awaits the * operation here and now. * - * @param [in] config Configuration of the operation + * @param [in] config Configuration of the operation. NB: State will not be reset. * @param [in, out] prev Candidate iterable object that might be re-used, otherwise assigned newly allocated object. * @param [in] stop_prev Whether to stop ongoing awaitable if it is active. * @returns An awaitable if the runloop is active, otherwise the mapped return value. @@ -147,3 +147,57 @@ mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config, pb_type_async_t ** pb_assert(err); return config->return_map ? config->return_map(config->parent_obj) : mp_const_none; } + +/** + * Iteration for a constant awaitable that yields once before returning. + * + * This is different from omitting iter_once to achieve a single yield, since + * that special case cannot have a return value. + * + * @param [in] state Protothread state. + * @param [in] parent_obj The constant. + */ +static pbio_error_t pb_type_async_constant_iter_once(pbio_os_state_t *state, mp_obj_t parent_obj) { + PBIO_OS_ASYNC_BEGIN(state); + PBIO_OS_AWAIT_ONCE(state); + PBIO_OS_ASYNC_END(PBIO_SUCCESS); +} + +/** + * Return map for a constant awaitable. + * + * @param [in] parent_obj The constant. + * @returns The same constant. + */ +static mp_obj_t pb_type_async_constant_return_map(mp_obj_t parent_obj) { + return parent_obj; +} + +/** + * Returns an awaitable operation that yields once and then returns a constant + * result. If the runloop is not active, this just returns the given value. + * + * Can be used to return constants from functions that still need to be + * awaitable for other reasons. + * + * @param [in] result_obj Return result. + * @param [in, out] prev Candidate iterable object that might be + * re-used, otherwise assigned newly allocated object. + * @returns An awaitable if the runloop is active, otherwise the constant return value. + */ +mp_obj_t pb_type_async_return_result(mp_obj_t result_obj, pb_type_async_t **prev) { + + // In synchronous mode, return right away. + if (!pb_module_tools_run_loop_is_active()) { + return result_obj; + } + + // Async case returns soon. + pb_type_async_t config = { + .parent_obj = result_obj, + .iter_once = pb_type_async_constant_iter_once, + .return_map = pb_type_async_constant_return_map, + .state = 0, + }; + return pb_type_async_wait_or_await(&config, prev, false); +} diff --git a/pybricks/tools/pb_type_async.h b/pybricks/tools/pb_type_async.h index 1ec26ca2c..e106f9ca1 100644 --- a/pybricks/tools/pb_type_async.h +++ b/pybricks/tools/pb_type_async.h @@ -75,6 +75,8 @@ typedef struct { mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config, pb_type_async_t **prev, bool stop_prev); +mp_obj_t pb_type_async_return_result(mp_obj_t result_obj, pb_type_async_t **prev); + void pb_type_async_schedule_stop_iteration(pb_type_async_t *iter); #endif // PYBRICKS_INCLUDED_ASYNC_H