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..1f472e742 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(); +static bool init_passive_pup_device(iodevices_PUPDevice_obj_t *self) { - pbio_port_id_t port_id = pb_type_enum_get_value(port_in, &pb_enum_type_Port); - - // 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); } @@ -164,7 +171,16 @@ static mp_obj_t iodevices_PUPDevice_read(size_t n_args, const mp_obj_t *pos_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)); + return pb_type_async_return_result(mp_obj_new_bool(value), &self->device_base.last_awaitable); + } + + // Other passive devices don't support reading. if (self->passive_id != LEGO_DEVICE_TYPE_ID_LPF2_UNKNOWN_UART) { pb_assert(PBIO_ERROR_INVALID_OP); } 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