Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
19 changes: 15 additions & 4 deletions lib/pbio/src/port_dcm_pup.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
44 changes: 30 additions & 14 deletions pybricks/iodevices/pb_type_iodevices_pupdevice.c
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
}
Expand Down
56 changes: 55 additions & 1 deletion pybricks/tools/pb_type_async.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.state = 0,

Technically not needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to make it explicit here since state isn't always zeroed in the config. In some, it is set to the protothread state at the first yield.

};
return pb_type_async_wait_or_await(&config, prev, false);
}
2 changes: 2 additions & 0 deletions pybricks/tools/pb_type_async.h
Original file line number Diff line number Diff line change
Expand Up @@ -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