From c4e53ef0db69209386679938b6faaef431f3f745 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Thu, 4 Sep 2025 14:56:45 +0200 Subject: [PATCH 01/17] pybricks.tools: Introduce simplified async type. Can be used to await protothreads, ultimately simplifying most of our awaitable pbio operations. --- bricks/_common/sources.mk | 1 + pybricks/tools/pb_type_async.c | 111 +++++++++++++++++++++++++++++++++ pybricks/tools/pb_type_async.h | 77 +++++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 pybricks/tools/pb_type_async.c create mode 100644 pybricks/tools/pb_type_async.h diff --git a/bricks/_common/sources.mk b/bricks/_common/sources.mk index b8c9b86f4..191724bdc 100644 --- a/bricks/_common/sources.mk +++ b/bricks/_common/sources.mk @@ -99,6 +99,7 @@ PYBRICKS_PYBRICKS_SRC_C = $(addprefix pybricks/,\ tools/pb_module_tools.c \ tools/pb_type_app_data.c \ tools/pb_type_awaitable.c \ + tools/pb_type_async.c \ tools/pb_type_matrix.c \ tools/pb_type_stopwatch.c \ tools/pb_type_task.c \ diff --git a/pybricks/tools/pb_type_async.c b/pybricks/tools/pb_type_async.c new file mode 100644 index 000000000..ff3238cb8 --- /dev/null +++ b/pybricks/tools/pb_type_async.c @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022-2025 The Pybricks Authors + + +#include "py/mpconfig.h" +#include "py/obj.h" +#include "py/runtime.h" + +#include "pb_type_async.h" + +#include +#include + +/** + * Cancels the iterable to it will stop awaiting. + * + * This will not call close(). Safe to call even if iter is NULL. + * + * @param [in] iter The awaitable object. + */ +void pb_type_async_cancel(pb_type_async_t *iter) { + if (iter) { + iter->parent_obj = MP_OBJ_NULL; + } +} + +mp_obj_t pb_type_async_close(mp_obj_t iter_in) { + pb_type_async_t *iter = MP_OBJ_TO_PTR(iter_in); + if (iter->close && iter->parent_obj != MP_OBJ_NULL) { + iter->close(iter->parent_obj); + } + pb_type_async_cancel(iter); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_async_close_obj, pb_type_async_close); + +static mp_obj_t pb_type_async_iternext(mp_obj_t iter_in) { + pb_type_async_t *iter = MP_OBJ_TO_PTR(iter_in); + + // On special case of sentinel, yield once and complete next time. + if (iter->parent_obj == MP_OBJ_SENTINEL) { + iter->parent_obj = MP_OBJ_NULL; + return mp_const_none; + } + + // No object, so this has already ended. + if (iter->parent_obj == MP_OBJ_NULL) { + return MP_OBJ_STOP_ITERATION; + } + + // Run one iteration of the protothread. + pbio_error_t err = iter->iter_once(&iter->state, iter->parent_obj); + + // Yielded, keep going. + if (err == PBIO_ERROR_AGAIN) { + return mp_const_none; + } + + // Raises on other errors, Proceeds on successful completion. + pb_assert(err); + + // For no return map, return basic stop iteration, which results in None. + if (!iter->return_map) { + pb_type_async_cancel(iter); + return MP_OBJ_STOP_ITERATION; + } + + mp_obj_t return_obj = iter->return_map(iter->parent_obj); + pb_type_async_cancel(iter); + + // Set return value via stop iteration. + return mp_make_stop_iteration(return_obj); +} + +static const mp_rom_map_elem_t pb_type_async_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&pb_type_async_close_obj) }, +}; +MP_DEFINE_CONST_DICT(pb_type_async_locals_dict, pb_type_async_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE(pb_type_async, + MP_QSTR_Async, + MP_TYPE_FLAG_ITER_IS_ITERNEXT, + iter, pb_type_async_iternext, + locals_dict, &pb_type_async_locals_dict); + +/** + * Returns an awaitable operation if the runloop is active, or awaits the + * operation here and now. + * + * @param [in] config Configuration of the operation + * @returns An awaitable if the runloop is active, otherwise the mapped return value. + */ +mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config) { + + config->base.type = &pb_type_async; + + // Return allocated awaitable if runloop active. + if (pb_module_tools_run_loop_is_active()) { + pb_type_async_t *iter = (pb_type_async_t *)m_malloc(sizeof(pb_type_async_t)); + *iter = *config; + return MP_OBJ_FROM_PTR(iter); + } + + // Otherwise wait for completion here without allocating the iterable. + pbio_error_t err; + while ((err = config->iter_once(&config->state, config->parent_obj)) == PBIO_ERROR_AGAIN) { + MICROPY_EVENT_POLL_HOOK; + } + pb_assert(err); + return config->return_map ? config->return_map(config->parent_obj) : mp_const_none; +} diff --git a/pybricks/tools/pb_type_async.h b/pybricks/tools/pb_type_async.h new file mode 100644 index 000000000..0f5326e51 --- /dev/null +++ b/pybricks/tools/pb_type_async.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +#ifndef PYBRICKS_INCLUDED_ASYNC_H +#define PYBRICKS_INCLUDED_ASYNC_H + +#include "py/mpconfig.h" + +#include "py/obj.h" + +#include + +/** + * Called on cancel/close. Used to stop hardware operation in unhandled + * conditions. + * + * @param [in] parent_obj The parent object associated with this iterable. + * @return Usually mp_const_none, for compatibility with typical close functions. + */ +typedef mp_obj_t (*pb_type_async_close_t)(mp_obj_t parent_obj); + +/** + * Function that computes the return object at the end of the operation. + * + * If NULL, the awaitable will return None at the end. + * + * @param [in] parent_obj The parent object associated with this iterable. + * @return Value to return at the end of the iteration. + */ +typedef mp_obj_t (*pb_type_async_return_map_t)(mp_obj_t parent_obj); + +/** + * Run one iteration of the protothread associated with this iterable. + * + * @param [in] state State of the operation, usually the protothread state. + * @param [in] parent_obj The parent object associated with this iterable. + * @return ::PBIO_ERROR_AGAIN while ongoing + * ::PBIO_SUCCESS on completion. + * Other error values will be raised. + */ +typedef pbio_error_t (*pb_type_async_iterate_once_t)(pbio_os_state_t *state, mp_obj_t parent_obj); + +// Object representing the iterable that is returned by an awaitable operation. +typedef struct { + mp_obj_base_t base; + /** + * The object instance whose method made us. Usually a class instance whose + * methods returned us. + * + * Special values: + * MP_OBJ_NULL: This iterable has completed or has been closed. + * MP_OBJ_SENTINEL: This iterable will yield once and complete next time. + */ + mp_obj_t parent_obj; + /** + * The iterable function associated with this operation. Usually a protothread. + */ + pb_type_async_iterate_once_t iter_once; + /** + * Close function to call when this iterable is closed. + */ + pb_type_async_close_t close; + /** + * Function that computes the return object at the end of the operation. + */ + pb_type_async_return_map_t return_map; + /** + * State of the protothread used by the iterable. + */ + pbio_os_state_t state; +} pb_type_async_t; + +mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config); + +void pb_type_async_cancel(pb_type_async_t *iter); + +#endif // PYBRICKS_INCLUDED_ASYNC_H From 2145c9b2f8367d449a3faf350bb9c41a60d2549c Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Thu, 4 Sep 2025 20:37:21 +0200 Subject: [PATCH 02/17] pybricks.common.Motor: Use new async type. --- pybricks/common.h | 4 +- pybricks/common/pb_type_motor.c | 85 ++++++++++++++++----------------- pybricks/robotics/pb_type_car.c | 1 - 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/pybricks/common.h b/pybricks/common.h index 46c4d580d..0b13d8199 100644 --- a/pybricks/common.h +++ b/pybricks/common.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -104,10 +105,11 @@ mp_obj_t common_Logger_obj_make_new(pbio_log_t *log, uint8_t num_values); // pybricks.common.DCMotor and pybricks.common.Motor typedef struct { - pb_type_device_obj_base_t device_base; + mp_obj_base_t base; pbio_servo_t *srv; pbio_dcmotor_t *dcmotor; pbio_port_id_t port_id; + pb_type_async_t *last_awaitable; #if PYBRICKS_PY_COMMON_MOTOR_MODEL mp_obj_t model; #endif diff --git a/pybricks/common/pb_type_motor.c b/pybricks/common/pb_type_motor.c index d0e59f5f2..935f7c898 100644 --- a/pybricks/common/pb_type_motor.c +++ b/pybricks/common/pb_type_motor.c @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include @@ -148,7 +148,7 @@ static mp_obj_t pb_type_Motor_make_new(const mp_obj_type_t *type, size_t n_args, self->logger = common_Logger_obj_make_new(&self->srv->log, PBIO_SERVO_LOGGER_NUM_COLS); #endif - self->device_base.awaitables = mp_obj_new_list(0, NULL); + self->last_awaitable = NULL; return MP_OBJ_FROM_PTR(self); } @@ -157,7 +157,7 @@ static mp_obj_t pb_type_Motor_make_new(const mp_obj_type_t *type, size_t n_args, static void pb_type_Motor_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { pb_type_Motor_obj_t *self = MP_OBJ_TO_PTR(self_in); mp_printf(print, "%q(Port.%c, %q.%q)", - self->device_base.base.type->name, self->port_id, MP_QSTR_Direction, + self->base.type->name, self->port_id, MP_QSTR_Direction, self->dcmotor->direction == PBIO_DIRECTION_CLOCKWISE ? MP_QSTR_CLOCKWISE : MP_QSTR_COUNTERCLOCKWISE); } @@ -243,7 +243,7 @@ static mp_obj_t pb_type_Motor_reset_angle(size_t n_args, const mp_obj_t *pos_arg // Set the new angle pb_assert(pbio_servo_reset_angle(self->srv, reset_angle, reset_to_abs)); - pb_type_awaitable_update_all(self->device_base.awaitables, PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + pb_type_async_cancel(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_reset_angle_obj, 1, pb_type_Motor_reset_angle); @@ -268,7 +268,7 @@ static mp_obj_t pb_type_Motor_run(size_t n_args, const mp_obj_t *pos_args, mp_ma mp_int_t speed = pb_obj_get_int(speed_in); pb_assert(pbio_servo_run_forever(self->srv, speed)); - pb_type_awaitable_update_all(self->device_base.awaitables, PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + pb_type_async_cancel(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_obj, 1, pb_type_Motor_run); @@ -277,36 +277,51 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_obj, 1, pb_type_Motor_run); static mp_obj_t pb_type_Motor_hold(mp_obj_t self_in) { pb_type_Motor_obj_t *self = MP_OBJ_TO_PTR(self_in); pb_assert(pbio_servo_stop(self->srv, PBIO_CONTROL_ON_COMPLETION_HOLD)); - pb_type_awaitable_update_all(self->device_base.awaitables, PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + pb_type_async_cancel(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_Motor_hold_obj, pb_type_Motor_hold); -static bool pb_type_Motor_test_completion(mp_obj_t self_in, uint32_t end_time) { - pb_type_Motor_obj_t *self = MP_OBJ_TO_PTR(self_in); +static pbio_error_t pb_type_motor_run_iterate_once(pbio_os_state_t *state, mp_obj_t parent_obj) { + pb_type_Motor_obj_t *self = MP_OBJ_TO_PTR(parent_obj); + // Handle I/O exceptions like port unplugged. if (!pbio_servo_update_loop_is_running(self->srv)) { - pb_assert(PBIO_ERROR_NO_DEV); + return PBIO_ERROR_NO_DEV; } // Get completion state. - return pbio_control_is_done(&self->srv->control); + return pbio_control_is_done(&self->srv->control) ? PBIO_SUCCESS : PBIO_ERROR_AGAIN; } -static void pb_type_Motor_cancel(mp_obj_t self_in) { - pb_type_Motor_stop(self_in); +static mp_obj_t pb_type_motor_get_final_angle(mp_obj_t parent_obj) { + pb_type_Motor_obj_t *self = MP_OBJ_TO_PTR(parent_obj); + + // Return the angle upon completion of the stall maneuver. + int32_t stall_angle, stall_speed; + pb_assert(pbio_servo_get_state_user(self->srv, &stall_angle, &stall_speed)); + + return mp_obj_new_int(stall_angle); } // Common awaitable used for most motor methods. -static mp_obj_t await_or_wait(pb_type_Motor_obj_t *self) { - return pb_type_awaitable_await_or_wait( - MP_OBJ_FROM_PTR(self), - self->device_base.awaitables, - pb_type_awaitable_end_time_none, - pb_type_Motor_test_completion, - pb_type_awaitable_return_none, - pb_type_Motor_cancel, - PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); +static mp_obj_t pb_type_motor_wait_or_await(pb_type_Motor_obj_t *self, bool return_final_angle) { + + pb_type_async_t config = { + .parent_obj = MP_OBJ_FROM_PTR(self), + .iter_once = pb_type_motor_run_iterate_once, + .close = pb_type_Motor_stop, + .return_map = return_final_angle ? pb_type_motor_get_final_angle : NULL, + }; + mp_obj_t result = pb_type_async_wait_or_await(&config); + + // New operation always wins; ongoing awaitable motor motion is cancelled. + if (pb_module_tools_run_loop_is_active()) { + pb_type_async_cancel(self->last_awaitable); + self->last_awaitable = result; + } + + return result; } // pybricks.common.Motor.run_time @@ -331,21 +346,10 @@ static mp_obj_t pb_type_Motor_run_time(size_t n_args, const mp_obj_t *pos_args, return mp_const_none; } // Handle completion by awaiting or blocking. - return await_or_wait(self); + return pb_type_motor_wait_or_await(self, false); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_time_obj, 1, pb_type_Motor_run_time); -static mp_obj_t pb_type_Motor_stall_return_value(mp_obj_t self_in) { - - pb_type_Motor_obj_t *self = MP_OBJ_TO_PTR(self_in); - - // Return the angle upon completion of the stall maneuver. - int32_t stall_angle, stall_speed; - pb_assert(pbio_servo_get_state_user(self->srv, &stall_angle, &stall_speed)); - - return mp_obj_new_int(stall_angle); -} - // pybricks.common.Motor.run_until_stalled static mp_obj_t pb_type_Motor_run_until_stalled(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, @@ -370,14 +374,7 @@ static mp_obj_t pb_type_Motor_run_until_stalled(size_t n_args, const mp_obj_t *p pb_assert(pbio_servo_run_until_stalled(self->srv, speed, torque_limit, then)); // Handle completion by awaiting or blocking. - return pb_type_awaitable_await_or_wait( - MP_OBJ_FROM_PTR(self), - self->device_base.awaitables, - pb_type_awaitable_end_time_none, - pb_type_Motor_test_completion, - pb_type_Motor_stall_return_value, - pb_type_Motor_cancel, - PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + return pb_type_motor_wait_or_await(self, true); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_until_stalled_obj, 1, pb_type_Motor_run_until_stalled); @@ -402,7 +399,7 @@ static mp_obj_t pb_type_Motor_run_angle(size_t n_args, const mp_obj_t *pos_args, return mp_const_none; } // Handle completion by awaiting or blocking. - return await_or_wait(self); + return pb_type_motor_wait_or_await(self, false); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_angle_obj, 1, pb_type_Motor_run_angle); @@ -427,7 +424,7 @@ static mp_obj_t pb_type_Motor_run_target(size_t n_args, const mp_obj_t *pos_args return mp_const_none; } // Handle completion by awaiting or blocking. - return await_or_wait(self); + return pb_type_motor_wait_or_await(self, false); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_target_obj, 1, pb_type_Motor_run_target); @@ -439,7 +436,7 @@ static mp_obj_t pb_type_Motor_track_target(size_t n_args, const mp_obj_t *pos_ar mp_int_t target_angle = pb_obj_get_int(target_angle_in); pb_assert(pbio_servo_track_target(self->srv, target_angle)); - pb_type_awaitable_update_all(self->device_base.awaitables, PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + pb_type_async_cancel(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_track_target_obj, 1, pb_type_Motor_track_target); diff --git a/pybricks/robotics/pb_type_car.c b/pybricks/robotics/pb_type_car.c index 32a7360d9..759527714 100644 --- a/pybricks/robotics/pb_type_car.c +++ b/pybricks/robotics/pb_type_car.c @@ -18,7 +18,6 @@ #include #include #include -#include #include #include From f96d593d6b42592cbf08961863181340d137302a Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Fri, 5 Sep 2025 11:29:50 +0200 Subject: [PATCH 03/17] pybricks.tools: Allow exhausted awaitable to be re-used. --- pybricks/common/pb_type_motor.c | 21 ++++-------- pybricks/tools/pb_type_async.c | 58 +++++++++++++++++++++------------ pybricks/tools/pb_type_async.h | 8 +++-- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/pybricks/common/pb_type_motor.c b/pybricks/common/pb_type_motor.c index 935f7c898..6adcc259f 100644 --- a/pybricks/common/pb_type_motor.c +++ b/pybricks/common/pb_type_motor.c @@ -243,7 +243,7 @@ static mp_obj_t pb_type_Motor_reset_angle(size_t n_args, const mp_obj_t *pos_arg // Set the new angle pb_assert(pbio_servo_reset_angle(self->srv, reset_angle, reset_to_abs)); - pb_type_async_cancel(self->last_awaitable); + pb_type_async_schedule_cancel(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_reset_angle_obj, 1, pb_type_Motor_reset_angle); @@ -268,7 +268,7 @@ static mp_obj_t pb_type_Motor_run(size_t n_args, const mp_obj_t *pos_args, mp_ma mp_int_t speed = pb_obj_get_int(speed_in); pb_assert(pbio_servo_run_forever(self->srv, speed)); - pb_type_async_cancel(self->last_awaitable); + pb_type_async_schedule_cancel(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_obj, 1, pb_type_Motor_run); @@ -277,7 +277,7 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_obj, 1, pb_type_Motor_run); static mp_obj_t pb_type_Motor_hold(mp_obj_t self_in) { pb_type_Motor_obj_t *self = MP_OBJ_TO_PTR(self_in); pb_assert(pbio_servo_stop(self->srv, PBIO_CONTROL_ON_COMPLETION_HOLD)); - pb_type_async_cancel(self->last_awaitable); + pb_type_async_schedule_cancel(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_Motor_hold_obj, pb_type_Motor_hold); @@ -304,24 +304,17 @@ static mp_obj_t pb_type_motor_get_final_angle(mp_obj_t parent_obj) { return mp_obj_new_int(stall_angle); } -// Common awaitable used for most motor methods. +// Common awaitable used for all motor methods that take some time to complete. static mp_obj_t pb_type_motor_wait_or_await(pb_type_Motor_obj_t *self, bool return_final_angle) { - pb_type_async_t config = { .parent_obj = MP_OBJ_FROM_PTR(self), .iter_once = pb_type_motor_run_iterate_once, .close = pb_type_Motor_stop, .return_map = return_final_angle ? pb_type_motor_get_final_angle : NULL, }; - mp_obj_t result = pb_type_async_wait_or_await(&config); - // New operation always wins; ongoing awaitable motor motion is cancelled. - if (pb_module_tools_run_loop_is_active()) { - pb_type_async_cancel(self->last_awaitable); - self->last_awaitable = result; - } - - return result; + pb_type_async_schedule_cancel(self->last_awaitable); + return pb_type_async_wait_or_await(&config, &self->last_awaitable); } // pybricks.common.Motor.run_time @@ -436,7 +429,7 @@ static mp_obj_t pb_type_Motor_track_target(size_t n_args, const mp_obj_t *pos_ar mp_int_t target_angle = pb_obj_get_int(target_angle_in); pb_assert(pbio_servo_track_target(self->srv, target_angle)); - pb_type_async_cancel(self->last_awaitable); + pb_type_async_schedule_cancel(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_track_target_obj, 1, pb_type_Motor_track_target); diff --git a/pybricks/tools/pb_type_async.c b/pybricks/tools/pb_type_async.c index ff3238cb8..388db652d 100644 --- a/pybricks/tools/pb_type_async.c +++ b/pybricks/tools/pb_type_async.c @@ -12,16 +12,19 @@ #include /** - * Cancels the iterable to it will stop awaiting. + * Cancels the iterable so it will stop awaiting. * * This will not call close(). Safe to call even if iter is NULL. * * @param [in] iter The awaitable object. */ -void pb_type_async_cancel(pb_type_async_t *iter) { - if (iter) { - iter->parent_obj = MP_OBJ_NULL; +void pb_type_async_schedule_cancel(pb_type_async_t *iter) { + if (!iter) { + return; } + // Don't set it to MP_OBJ_NULL right away, or the calling code wouldn't + // know it was exhausted, and it would await on the renewed operation. + iter->parent_obj = MP_OBJ_STOP_ITERATION; } mp_obj_t pb_type_async_close(mp_obj_t iter_in) { @@ -29,7 +32,10 @@ mp_obj_t pb_type_async_close(mp_obj_t iter_in) { if (iter->close && iter->parent_obj != MP_OBJ_NULL) { iter->close(iter->parent_obj); } - pb_type_async_cancel(iter); + // Closing is stronger than cancellation. In case of close, we expect that + // the awaitable is no longer to be iterated afterwards, so it would not + // reach exhaustion on its own and could never be re-used, so do it here. + iter->parent_obj = MP_OBJ_NULL; return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_async_close_obj, pb_type_async_close); @@ -37,14 +43,20 @@ static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_async_close_obj, pb_type_async_close); static mp_obj_t pb_type_async_iternext(mp_obj_t iter_in) { pb_type_async_t *iter = MP_OBJ_TO_PTR(iter_in); - // On special case of sentinel, yield once and complete next time. + // On special case of sentinel, yield once now and complete next time. if (iter->parent_obj == MP_OBJ_SENTINEL) { - iter->parent_obj = MP_OBJ_NULL; + pb_type_async_schedule_cancel(iter); return mp_const_none; } - // No object, so this has already ended. - if (iter->parent_obj == MP_OBJ_NULL) { + // It was scheduled for cancellation externally (or exhausted normally + // previously). We are hereby letting the calling code know we are + // exhausted, so now we can set parent_obj to MP_OBJ_NULL to indicate it is + // ready to be used again. This assumes user did not keep a reference to it + // and does not next() or await it again. It is safe if they do, but the + // user code would be awaiting whatever it is re-used for. + if (iter->parent_obj == MP_OBJ_STOP_ITERATION || iter->parent_obj == MP_OBJ_NULL || !iter->iter_once) { + iter->parent_obj = MP_OBJ_NULL; return MP_OBJ_STOP_ITERATION; } @@ -59,17 +71,14 @@ static mp_obj_t pb_type_async_iternext(mp_obj_t iter_in) { // Raises on other errors, Proceeds on successful completion. pb_assert(err); - // For no return map, return basic stop iteration, which results in None. - if (!iter->return_map) { - pb_type_async_cancel(iter); - return MP_OBJ_STOP_ITERATION; + // This causes the stop iteration to provide the return value. + if (iter->return_map) { + mp_make_stop_iteration(iter->return_map(iter->parent_obj)); } - mp_obj_t return_obj = iter->return_map(iter->parent_obj); - pb_type_async_cancel(iter); - - // Set return value via stop iteration. - return mp_make_stop_iteration(return_obj); + // As above, notify caller of exhaustion so this iterable can be re-used. + iter->parent_obj = MP_OBJ_NULL; + return MP_OBJ_STOP_ITERATION; } static const mp_rom_map_elem_t pb_type_async_locals_dict_table[] = { @@ -88,16 +97,25 @@ MP_DEFINE_CONST_OBJ_TYPE(pb_type_async, * operation here and now. * * @param [in] config Configuration of the operation + * @param [in] prev Candidate iterable object that might be re-used. * @returns An awaitable if the runloop is active, otherwise the mapped return value. */ -mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config) { +mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config, pb_type_async_t **prev) { config->base.type = &pb_type_async; // Return allocated awaitable if runloop active. if (pb_module_tools_run_loop_is_active()) { - pb_type_async_t *iter = (pb_type_async_t *)m_malloc(sizeof(pb_type_async_t)); + // Re-use existing awaitable if exists and is free, otherwise allocate + // another one. This allows many resources with one concurrent physical + // operation like a motor to operate without re-allocation. + pb_type_async_t *iter = (prev && *prev && (*prev)->parent_obj == MP_OBJ_NULL) ? + *prev : (pb_type_async_t *)m_malloc(sizeof(pb_type_async_t)); *iter = *config; + if (prev) { + *prev = iter; + } + return MP_OBJ_FROM_PTR(iter); } diff --git a/pybricks/tools/pb_type_async.h b/pybricks/tools/pb_type_async.h index 0f5326e51..cde15531d 100644 --- a/pybricks/tools/pb_type_async.h +++ b/pybricks/tools/pb_type_async.h @@ -48,8 +48,10 @@ typedef struct { * methods returned us. * * Special values: - * MP_OBJ_NULL: This iterable has completed or has been closed. + * MP_OBJ_NULL: This iterable has been fully exhausted and can be reused. * MP_OBJ_SENTINEL: This iterable will yield once and complete next time. + * MP_OBJ_STOP_ITERATION: This iterable is cancelled and will exhaust + * when it is iterated again. */ mp_obj_t parent_obj; /** @@ -70,8 +72,8 @@ typedef struct { pbio_os_state_t state; } pb_type_async_t; -mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config); +mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config, pb_type_async_t **prev); -void pb_type_async_cancel(pb_type_async_t *iter); +void pb_type_async_schedule_cancel(pb_type_async_t *iter); #endif // PYBRICKS_INCLUDED_ASYNC_H From bb9b743301abb2ee0c8c26fe563fc11541c19182 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Sun, 7 Sep 2025 15:00:33 +0200 Subject: [PATCH 04/17] pybricks.tools: Fix awaitable cancellation. It turns out that MP_OBJ_STOP_ITERATION is the same as MP_OBJ_NULL, so it can't be used as an intermediate stage. We'll use MP_OBJ_SENTINEL instead and make yield-once functions work another way. --- pybricks/tools/pb_type_async.c | 16 ++++++++-------- pybricks/tools/pb_type_async.h | 6 ++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pybricks/tools/pb_type_async.c b/pybricks/tools/pb_type_async.c index 388db652d..6bd994ffa 100644 --- a/pybricks/tools/pb_type_async.c +++ b/pybricks/tools/pb_type_async.c @@ -24,7 +24,7 @@ void pb_type_async_schedule_cancel(pb_type_async_t *iter) { } // Don't set it to MP_OBJ_NULL right away, or the calling code wouldn't // know it was exhausted, and it would await on the renewed operation. - iter->parent_obj = MP_OBJ_STOP_ITERATION; + iter->parent_obj = MP_OBJ_SENTINEL; } mp_obj_t pb_type_async_close(mp_obj_t iter_in) { @@ -43,23 +43,23 @@ static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_async_close_obj, pb_type_async_close); static mp_obj_t pb_type_async_iternext(mp_obj_t iter_in) { pb_type_async_t *iter = MP_OBJ_TO_PTR(iter_in); - // On special case of sentinel, yield once now and complete next time. - if (iter->parent_obj == MP_OBJ_SENTINEL) { - pb_type_async_schedule_cancel(iter); - return mp_const_none; - } - // It was scheduled for cancellation externally (or exhausted normally // previously). We are hereby letting the calling code know we are // exhausted, so now we can set parent_obj to MP_OBJ_NULL to indicate it is // ready to be used again. This assumes user did not keep a reference to it // and does not next() or await it again. It is safe if they do, but the // user code would be awaiting whatever it is re-used for. - if (iter->parent_obj == MP_OBJ_STOP_ITERATION || iter->parent_obj == MP_OBJ_NULL || !iter->iter_once) { + if (iter->parent_obj == MP_OBJ_SENTINEL || iter->parent_obj == MP_OBJ_NULL) { iter->parent_obj = MP_OBJ_NULL; return MP_OBJ_STOP_ITERATION; } + // Special case without iterator means yield exactly once and then complete. + if (!iter->iter_once) { + pb_type_async_schedule_cancel(iter); + return mp_const_none; + } + // Run one iteration of the protothread. pbio_error_t err = iter->iter_once(&iter->state, iter->parent_obj); diff --git a/pybricks/tools/pb_type_async.h b/pybricks/tools/pb_type_async.h index cde15531d..9effa1745 100644 --- a/pybricks/tools/pb_type_async.h +++ b/pybricks/tools/pb_type_async.h @@ -49,13 +49,15 @@ typedef struct { * * Special values: * MP_OBJ_NULL: This iterable has been fully exhausted and can be reused. - * MP_OBJ_SENTINEL: This iterable will yield once and complete next time. - * MP_OBJ_STOP_ITERATION: This iterable is cancelled and will exhaust + * MP_OBJ_SENTINEL: This iterable is cancelled and will exhaust * when it is iterated again. */ mp_obj_t parent_obj; /** * The iterable function associated with this operation. Usually a protothread. + * + * Special values: + * NULL: This iterable will yield once and complete next time. */ pb_type_async_iterate_once_t iter_once; /** From 599e099ffaf20289433e52bd5539beea00c95fd5 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Sun, 7 Sep 2025 15:02:40 +0200 Subject: [PATCH 05/17] pybricks.tools: Use new awaitable for wait. No more linked lists to keep track of awaitables. This fixes programs running out of memory if the user forgets await. Still allow avoiding allocation in basic scripts using a fixed number of statically allocated waits. --- pybricks/tools/pb_module_tools.c | 60 +++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/pybricks/tools/pb_module_tools.c b/pybricks/tools/pb_module_tools.c index 87797a687..8049406d5 100644 --- a/pybricks/tools/pb_module_tools.c +++ b/pybricks/tools/pb_module_tools.c @@ -5,6 +5,8 @@ #if PYBRICKS_PY_TOOLS +#include + #include "py/builtin.h" #include "py/gc.h" #include "py/mphal.h" @@ -14,6 +16,7 @@ #include #include +#include #include #include #include @@ -21,6 +24,7 @@ #include #include #include +#include #include #include @@ -42,14 +46,20 @@ void pb_module_tools_assert_blocking(void) { } } -// The awaitables for the wait() function have no object associated with -// it (unlike e.g. a motor), so we make a starting point here. These never -// have to cancel each other so shouldn't need to be in a list, but this lets -// us share the same code with other awaitables. It also minimizes allocation. -MP_REGISTER_ROOT_POINTER(mp_obj_t wait_awaitables); +/** + * Statically allocated wait objects that can be re-used without allocation + * once exhausted. Should be sufficient for trivial applications. + * + * More are allocated as needed. If a user has more than this many parallel + * waits, the user can probably afford to allocate anyway. + * + * This is set to zero each time MicroPython starts. + */ +static pb_type_async_t waits[6]; -static bool pb_module_tools_wait_test_completion(mp_obj_t obj, uint32_t end_time) { - return mp_hal_ticks_ms() - end_time < UINT32_MAX / 2; +static pbio_error_t pb_module_tools_wait_iter_once(pbio_os_state_t *state, mp_obj_t parent_obj) { + // Not a protothread, but using the state variable to store final time. + return pbio_util_time_has_passed(pbdrv_clock_get_ms(), (uint32_t)*state) ? PBIO_SUCCESS: PBIO_ERROR_AGAIN; } static mp_obj_t pb_module_tools_wait(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { @@ -58,8 +68,7 @@ static mp_obj_t pb_module_tools_wait(size_t n_args, const mp_obj_t *pos_args, mp mp_int_t time = pb_obj_get_int(time_in); - // outside run loop, do blocking wait. This would be handled below as well, - // but for this very simple call we'd rather avoid the overhead. + // Outside run loop, do blocking wait to avoid async overhead. if (!pb_module_tools_run_loop_is_active()) { if (time > 0) { mp_hal_delay_ms(time); @@ -67,18 +76,27 @@ static mp_obj_t pb_module_tools_wait(size_t n_args, const mp_obj_t *pos_args, mp return mp_const_none; } - // Require that duration is nonnegative small int. This makes it cheaper to - // test completion state in iteration loop. - time = pbio_int_math_bind(time, 0, INT32_MAX >> 2); + // Find statically allocated candidate that can be re-used again because + // it was never used or used and exhausted. If it stays at NULL then a new + // awaitable is allocated. + pb_type_async_t *reuse = NULL; + for (uint32_t i = 0; i < MP_ARRAY_SIZE(waits); i++) { + if (waits[i].parent_obj == MP_OBJ_NULL) { + reuse = &waits[i]; + break; + } + } + + pb_type_async_t config = { + // Not associated with any parent object. + .parent_obj = mp_const_none, + // Yield once for duration 0 to avoid blocking loops. + .iter_once = time == 0 ? NULL : pb_module_tools_wait_iter_once, + // No protothread here; use it to encode end time. + .state = pbdrv_clock_get_ms() + (uint32_t)time, + }; - return pb_type_awaitable_await_or_wait( - NULL, // wait functions are not associated with an object - MP_STATE_PORT(wait_awaitables), - mp_hal_ticks_ms() + time, - time > 0 ? pb_module_tools_wait_test_completion : pb_type_awaitable_test_completion_yield_once, - pb_type_awaitable_return_none, - pb_type_awaitable_cancel_none, - PB_TYPE_AWAITABLE_OPT_NONE); + return pb_type_async_wait_or_await(&config, &reuse); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_wait_obj, 0, pb_module_tools_wait); @@ -249,7 +267,7 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_run_task_obj, 0, pb_module_too // Reset global awaitable state when user program starts. void pb_module_tools_init(void) { - MP_STATE_PORT(wait_awaitables) = mp_obj_new_list(0, NULL); + memset(waits, 0, sizeof(waits)); MP_STATE_PORT(pbio_task_awaitables) = mp_obj_new_list(0, NULL); run_loop_is_active = false; } From 54a72faa8fa7f935561d0d636c88c9a978e4aa61 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 8 Sep 2025 11:36:51 +0200 Subject: [PATCH 06/17] pybricks.pupdevices: Use new awaitable. --- pybricks/common/pb_type_device.c | 43 +++++++++++--------------------- pybricks/common/pb_type_device.h | 6 ++--- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/pybricks/common/pb_type_device.c b/pybricks/common/pb_type_device.c index 6fdb108a1..b9b1f536f 100644 --- a/pybricks/common/pb_type_device.c +++ b/pybricks/common/pb_type_device.c @@ -65,19 +65,11 @@ void *pb_type_device_get_data_blocking(mp_obj_t self_in, uint8_t mode) { * data has been written to the device, including the neccessary delays for * discarding stale data or the time needed to externally process written data. * - * @param [in] self_in The sensor object instance. - * @param [in] end_time Not used. - * @return True if operation is complete (device ready), - * false otherwise. + * See ::pbio_port_lump_is_ready for details. */ -static bool pb_pup_device_test_completion(mp_obj_t self_in, uint32_t end_time) { +static pbio_error_t pb_pup_device_iter_once(pbio_os_state_t *state, mp_obj_t self_in) { pb_type_device_obj_base_t *sensor = MP_OBJ_TO_PTR(self_in); - pbio_error_t err = pbio_port_lump_is_ready(sensor->lump_dev); - if (err == PBIO_ERROR_AGAIN) { - return false; - } - pb_assert(err); - return true; + return pbio_port_lump_is_ready(sensor->lump_dev); } /** @@ -100,14 +92,12 @@ mp_obj_t pb_type_device_method_call(mp_obj_t self_in, size_t n_args, size_t n_kw pb_type_device_obj_base_t *sensor = MP_OBJ_TO_PTR(sensor_in); pb_assert(pbio_port_lump_set_mode(sensor->lump_dev, method->mode)); - return pb_type_awaitable_await_or_wait( - sensor_in, - sensor->awaitables, - pb_type_awaitable_end_time_none, - pb_pup_device_test_completion, - method->get_values, - pb_type_awaitable_cancel_none, - PB_TYPE_AWAITABLE_OPT_NONE); + pb_type_async_t config = { + .iter_once = pb_pup_device_iter_once, + .parent_obj = sensor_in, + .return_map = method->get_values, + }; + return pb_type_async_wait_or_await(&config, &sensor->last_awaitable); } /** @@ -132,14 +122,11 @@ MP_DEFINE_CONST_OBJ_TYPE( */ mp_obj_t pb_type_device_set_data(pb_type_device_obj_base_t *sensor, uint8_t mode, const void *data, uint8_t size) { pb_assert(pbio_port_lump_set_mode_with_data(sensor->lump_dev, mode, data, size)); - return pb_type_awaitable_await_or_wait( - MP_OBJ_FROM_PTR(sensor), - sensor->awaitables, - pb_type_awaitable_end_time_none, - pb_pup_device_test_completion, - pb_type_awaitable_return_none, - pb_type_awaitable_cancel_none, - PB_TYPE_AWAITABLE_OPT_RAISE_ON_BUSY); + pb_type_async_t config = { + .iter_once = pb_pup_device_iter_once, + .parent_obj = MP_OBJ_FROM_PTR(sensor), + }; + return pb_type_async_wait_or_await(&config, &sensor->last_awaitable); } void pb_device_set_lego_mode(pbio_port_t *port) { @@ -174,7 +161,7 @@ lego_device_type_id_t pb_type_device_init_class(pb_type_device_obj_base_t *self, mp_hal_delay_ms(50); } pb_assert(err); - self->awaitables = mp_obj_new_list(0, NULL); + self->last_awaitable = NULL; return actual_id; } diff --git a/pybricks/common/pb_type_device.h b/pybricks/common/pb_type_device.h index 35f04dedb..cf9e9888b 100644 --- a/pybricks/common/pb_type_device.h +++ b/pybricks/common/pb_type_device.h @@ -10,7 +10,7 @@ #include -#include +#include /** * Used in place of mp_obj_base_t in all pupdevices. This lets us share @@ -19,7 +19,7 @@ typedef struct _pb_type_device_obj_base_t { mp_obj_base_t base; pbio_port_lump_dev_t *lump_dev; - mp_obj_t awaitables; + pb_type_async_t *last_awaitable; } pb_type_device_obj_base_t; #if PYBRICKS_PY_DEVICES @@ -33,7 +33,7 @@ typedef struct _pb_type_device_obj_base_t { */ typedef struct { mp_obj_base_t base; - pb_type_awaitable_return_t get_values; + pb_type_async_return_map_t get_values; uint8_t mode; } pb_type_device_method_obj_t; From b7d770397e3048441000808a30725e9e49b03bfe Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 8 Sep 2025 11:53:51 +0200 Subject: [PATCH 07/17] pybricks.robotics.DriveBase: Use new awaitable. --- pybricks/robotics/pb_type_drivebase.c | 62 ++++++++++++--------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/pybricks/robotics/pb_type_drivebase.c b/pybricks/robotics/pb_type_drivebase.c index 4b4a91dd9..7076cee84 100644 --- a/pybricks/robotics/pb_type_drivebase.c +++ b/pybricks/robotics/pb_type_drivebase.c @@ -17,7 +17,7 @@ #include #include #include -#include +#include #include #include @@ -33,7 +33,7 @@ struct _pb_type_DriveBase_obj_t { mp_obj_t heading_control; mp_obj_t distance_control; #endif - mp_obj_t awaitables; + pb_type_async_t *last_awaitable; }; // pybricks.robotics.DriveBase.reset @@ -78,16 +78,13 @@ static mp_obj_t pb_type_DriveBase_make_new(const mp_obj_type_t *type, size_t n_a self->distance_control = pb_type_Control_obj_make_new(&self->db->control_distance); #endif - // List of awaitables associated with this drivebase. By keeping track, - // we can cancel them as needed when a new movement is started. - self->awaitables = mp_obj_new_list(0, NULL); + self->last_awaitable = NULL; return MP_OBJ_FROM_PTR(self); } -static bool pb_type_DriveBase_test_completion(mp_obj_t self_in, uint32_t end_time) { - - pb_type_DriveBase_obj_t *self = MP_OBJ_TO_PTR(self_in); +static pbio_error_t pb_type_drivebase_iterate_once(pbio_os_state_t *state, mp_obj_t parent_obj) { + pb_type_DriveBase_obj_t *self = MP_OBJ_TO_PTR(parent_obj); // Handle I/O exceptions like port unplugged. if (!pbio_drivebase_update_loop_is_running(self->db)) { @@ -95,24 +92,35 @@ static bool pb_type_DriveBase_test_completion(mp_obj_t self_in, uint32_t end_tim } // Get completion state. - return pbio_drivebase_is_done(self->db); + return pbio_drivebase_is_done(self->db) ? PBIO_SUCCESS : PBIO_ERROR_AGAIN; } -static void pb_type_DriveBase_cancel(mp_obj_t self_in) { +// pybricks.robotics.DriveBase.stop +static mp_obj_t pb_type_DriveBase_stop(mp_obj_t self_in) { + + // Cancel awaitables. pb_type_DriveBase_obj_t *self = MP_OBJ_TO_PTR(self_in); + pb_type_async_schedule_cancel(self->last_awaitable); + + // Stop hardware. pb_assert(pbio_drivebase_stop(self->db, PBIO_CONTROL_ON_COMPLETION_COAST)); + + return mp_const_none; } +MP_DEFINE_CONST_FUN_OBJ_1(pb_type_DriveBase_stop_obj, pb_type_DriveBase_stop); + // All drive base methods use the same kind of completion awaitable. static mp_obj_t await_or_wait(pb_type_DriveBase_obj_t *self) { - return pb_type_awaitable_await_or_wait( - MP_OBJ_FROM_PTR(self), - self->awaitables, - pb_type_awaitable_end_time_none, - pb_type_DriveBase_test_completion, - pb_type_awaitable_return_none, - pb_type_DriveBase_cancel, - PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + + pb_type_async_t config = { + .parent_obj = MP_OBJ_FROM_PTR(self), + .iter_once = pb_type_drivebase_iterate_once, + .close = pb_type_DriveBase_stop, + }; + // New operation always wins; ongoing awaitable motion is cancelled. + pb_type_async_schedule_cancel(self->last_awaitable); + return pb_type_async_wait_or_await(&config, &self->last_awaitable); } // pybricks.robotics.DriveBase.straight @@ -229,33 +237,19 @@ static mp_obj_t pb_type_DriveBase_drive(size_t n_args, const mp_obj_t *pos_args, mp_int_t turn_rate = pb_obj_get_int(turn_rate_in); // Cancel awaitables but not hardware. Drive forever will handle this. - pb_type_awaitable_update_all(self->awaitables, PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + pb_type_async_schedule_cancel(self->last_awaitable); pb_assert(pbio_drivebase_drive_forever(self->db, speed, turn_rate)); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_DriveBase_drive_obj, 1, pb_type_DriveBase_drive); -// pybricks.robotics.DriveBase.stop -static mp_obj_t pb_type_DriveBase_stop(mp_obj_t self_in) { - - // Cancel awaitables. - pb_type_DriveBase_obj_t *self = MP_OBJ_TO_PTR(self_in); - pb_type_awaitable_update_all(self->awaitables, PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); - - // Stop hardware. - pb_type_DriveBase_cancel(self_in); - - return mp_const_none; -} -MP_DEFINE_CONST_FUN_OBJ_1(pb_type_DriveBase_stop_obj, pb_type_DriveBase_stop); - // pybricks.robotics.DriveBase.brake static mp_obj_t pb_type_DriveBase_brake(mp_obj_t self_in) { // Cancel awaitables. pb_type_DriveBase_obj_t *self = MP_OBJ_TO_PTR(self_in); - pb_type_awaitable_update_all(self->awaitables, PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + pb_type_async_schedule_cancel(self->last_awaitable); // Stop hardware. pb_assert(pbio_drivebase_stop(self->db, PBIO_CONTROL_ON_COMPLETION_BRAKE)); From 11064fe0637bfe8fca050ad2b419649aa1329e72 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 8 Sep 2025 11:54:55 +0200 Subject: [PATCH 08/17] pybricks.tools: Use new awaitable for pbio tasks. This new awaitable is introduced so we can eventually drop queued tasks and safely await protothreads. For now, just keep the pbio tasks compiling. --- pybricks/tools/pb_module_tools.c | 37 ++++++++++---------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/pybricks/tools/pb_module_tools.c b/pybricks/tools/pb_module_tools.c index 8049406d5..a6cad31a1 100644 --- a/pybricks/tools/pb_module_tools.c +++ b/pybricks/tools/pb_module_tools.c @@ -145,34 +145,20 @@ void pb_module_tools_pbio_task_do_blocking(pbio_task_t *task, mp_int_t timeout) } } -// The awaitables associated with pbio tasks can originate from different -// objects. At the moment, they are only associated with Bluetooth tasks, and -// they cannot run at the same time. So we keep a single list of awaitables -// here instead of with each Bluetooth-related MicroPython object. -MP_REGISTER_ROOT_POINTER(mp_obj_t pbio_task_awaitables); - -static bool pb_module_tools_pbio_task_test_completion(mp_obj_t obj, uint32_t end_time) { - pbio_task_t *task = MP_OBJ_TO_PTR(obj); - - // Keep going if not done yet. - if (task->status == PBIO_ERROR_AGAIN) { - return false; - } - - // If done, make sure it was successful. - pb_assert(task->status); - return true; +static pbio_error_t pb_module_tools_pbio_task_iterate_once(pbio_os_state_t *state, mp_obj_t parent_obj) { + pbio_task_t *task = MP_OBJ_TO_PTR(parent_obj); + return task->status; } mp_obj_t pb_module_tools_pbio_task_wait_or_await(pbio_task_t *task) { - return pb_type_awaitable_await_or_wait( - MP_OBJ_FROM_PTR(task), - MP_STATE_PORT(pbio_task_awaitables), - pb_type_awaitable_end_time_none, - pb_module_tools_pbio_task_test_completion, - pb_type_awaitable_return_none, - pb_type_awaitable_cancel_none, - PB_TYPE_AWAITABLE_OPT_RAISE_ON_BUSY); + pb_type_async_t config = { + .parent_obj = MP_OBJ_FROM_PTR(task), + .iter_once = pb_module_tools_pbio_task_iterate_once, + }; + + // REVISIT: pbio tasks will be deprecated. Instead, protothreads can now + // be safely awaited. + return pb_type_async_wait_or_await(&config, NULL); } /** @@ -268,7 +254,6 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_run_task_obj, 0, pb_module_too // Reset global awaitable state when user program starts. void pb_module_tools_init(void) { memset(waits, 0, sizeof(waits)); - MP_STATE_PORT(pbio_task_awaitables) = mp_obj_new_list(0, NULL); run_loop_is_active = false; } From bd43a858bfc3c0fd1c498c0302f320bf861ee55f Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Mon, 8 Sep 2025 15:22:04 +0200 Subject: [PATCH 09/17] pybricks.hubs.PrimeHub: Use new awaitable for text animation. Now that we can have protothreads as the iterable function, we can trivially do these in C instead of importing Python code from C. Now they can correctly be cancelled, too. A new text animation will take over and stop any ongoing ones. --- bricks/primehub/manifest.py | 1 - bricks/primehub/modules/_light_matrix.py | 9 --- pybricks/common/pb_type_lightmatrix.c | 99 ++++++++++++++---------- 3 files changed, 59 insertions(+), 50 deletions(-) delete mode 100644 bricks/primehub/modules/_light_matrix.py diff --git a/bricks/primehub/manifest.py b/bricks/primehub/manifest.py index dc7d2cdb2..72c135720 100644 --- a/bricks/primehub/manifest.py +++ b/bricks/primehub/manifest.py @@ -2,4 +2,3 @@ include("../_common/manifest.py") freeze_as_mpy("../primehub/modules", "_imu_calibrate.py") -freeze_as_mpy("../primehub/modules", "_light_matrix.py") diff --git a/bricks/primehub/modules/_light_matrix.py b/bricks/primehub/modules/_light_matrix.py deleted file mode 100644 index 1ac7ed5c9..000000000 --- a/bricks/primehub/modules/_light_matrix.py +++ /dev/null @@ -1,9 +0,0 @@ -from pybricks.tools import wait - - -def light_matrix_text_async(display, text, on, off): - for char in text: - display.char(char) - yield from wait(on) - display.off() - yield from wait(off) diff --git a/pybricks/common/pb_type_lightmatrix.c b/pybricks/common/pb_type_lightmatrix.c index 73f5ad97c..5caffe853 100644 --- a/pybricks/common/pb_type_lightmatrix.c +++ b/pybricks/common/pb_type_lightmatrix.c @@ -12,6 +12,7 @@ #include "py/objstr.h" #include +#include #include #include @@ -20,6 +21,14 @@ #include #include +typedef struct { + const char *data; + size_t len; + pbio_os_timer_t timer; + uint32_t idx; + uint32_t on_time; + uint32_t off_time; +} text_animation_state_t; // pybricks._common.LightMatrix class object typedef struct _common_LightMatrix_obj_t { @@ -27,8 +36,8 @@ typedef struct _common_LightMatrix_obj_t { pbio_light_matrix_t *light_matrix; uint8_t *data; uint8_t frames; - // Frozen Python implementation of the async text() method. - mp_obj_t async_text_method; + pb_type_async_t *text_iter; + text_animation_state_t text; } common_LightMatrix_obj_t; // Renews memory for a given number of frames @@ -273,53 +282,63 @@ static mp_obj_t common_LightMatrix_pixel(size_t n_args, const mp_obj_t *pos_args } static MP_DEFINE_CONST_FUN_OBJ_KW(common_LightMatrix_pixel_obj, 1, common_LightMatrix_pixel); -// pybricks._common.LightMatrix.text -static mp_obj_t common_LightMatrix_text(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, - common_LightMatrix_obj_t, self, - PB_ARG_REQUIRED(text), - PB_ARG_DEFAULT_INT(on, 500), - PB_ARG_DEFAULT_INT(off, 50)); +static pbio_error_t pb_type_lightmatrix_text_iterate_once(pbio_os_state_t *state, mp_obj_t parent_obj) { - if (pb_module_tools_run_loop_is_active()) { - if (self->async_text_method == MP_OBJ_NULL) { - self->async_text_method = pb_function_import_helper(MP_QSTR__light_matrix, MP_QSTR_light_matrix_text_async); + common_LightMatrix_obj_t *self = MP_OBJ_TO_PTR(parent_obj); + text_animation_state_t *text = &self->text; + pbio_error_t err; + + PBIO_OS_ASYNC_BEGIN(state); + + for (text->idx = 0; text->idx < text->len; text->idx++) { + + // Raise on invalid character. + if (text->data[text->idx] < 32 || text->data[text->idx] > 126) { + return PBIO_ERROR_INVALID_ARG; } - mp_obj_t args[] = { - MP_OBJ_FROM_PTR(self), - text_in, - on_in, - off_in, - }; - return mp_call_function_n_kw(self->async_text_method, MP_ARRAY_SIZE(args), 0, args); - } - // Assert that the input is a single text - GET_STR_DATA_LEN(text_in, text, text_len); + // On time. + err = pbio_light_matrix_set_rows(self->light_matrix, pb_font_5x5[text->data[text->idx] - 32]); + if (err != PBIO_SUCCESS) { + return err; + } + PBIO_OS_AWAIT_MS(state, &text->timer, text->on_time); - // Make sure all characters are valid - for (size_t i = 0; i < text_len; i++) { - if (text[0] < 32 || text[0] > 126) { - pb_assert(PBIO_ERROR_INVALID_ARG); + // Off time so we can see multiple of the same characters. + if (text->off_time > 0 || text->idx == text->len - 1) { + err = pbio_light_matrix_clear(self->light_matrix); + if (err != PBIO_SUCCESS) { + return err; + } + PBIO_OS_AWAIT_MS(state, &text->timer, text->off_time); } } - mp_int_t on = pb_obj_get_int(on_in); - mp_int_t off = pb_obj_get_int(off_in); + PBIO_OS_ASYNC_END(PBIO_SUCCESS); +} - // Display all characters one by one - for (size_t i = 0; i < text_len; i++) { - pb_assert(pbio_light_matrix_set_rows(self->light_matrix, pb_font_5x5[text[i] - 32])); - mp_hal_delay_ms(on); +// pybricks._common.LightMatrix.text +static mp_obj_t common_LightMatrix_text(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, + common_LightMatrix_obj_t, self, + PB_ARG_REQUIRED(text), + PB_ARG_DEFAULT_INT(on, 500), + PB_ARG_DEFAULT_INT(off, 50)); - // Some off time so we can see multiple of the same characters - if (off > 0 || i == text_len - 1) { - pb_assert(pbio_light_matrix_clear(self->light_matrix)); - mp_hal_delay_ms(off); - } - } + text_animation_state_t *text = &self->text; - return mp_const_none; + text->on_time = pb_obj_get_int(on_in); + text->off_time = pb_obj_get_int(off_in); + text->idx = 0; + text->data = mp_obj_str_get_data(text_in, &text->len); + + pb_type_async_t config = { + .parent_obj = MP_OBJ_FROM_PTR(self), + .iter_once = pb_type_lightmatrix_text_iterate_once, + }; + // New operation always wins; ongoing animation is cancelled. + pb_type_async_schedule_cancel(self->text_iter); + return pb_type_async_wait_or_await(&config, &self->text_iter); } static MP_DEFINE_CONST_FUN_OBJ_KW(common_LightMatrix_text_obj, 1, common_LightMatrix_text); @@ -348,7 +367,7 @@ mp_obj_t pb_type_LightMatrix_obj_new(pbio_light_matrix_t *light_matrix) { common_LightMatrix_obj_t *self = mp_obj_malloc(common_LightMatrix_obj_t, &pb_type_LightMatrix); self->light_matrix = light_matrix; pbio_light_matrix_set_orientation(light_matrix, PBIO_GEOMETRY_SIDE_TOP); - self->async_text_method = MP_OBJ_NULL; + self->text_iter = NULL; return MP_OBJ_FROM_PTR(self); } From 3d4c5a33651684e14b81692da6b7e23f8891982b Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 9 Sep 2025 09:36:19 +0200 Subject: [PATCH 10/17] pybricks.common.Speaker: Use new awaitable. --- pybricks/common/pb_type_speaker.c | 140 +++++++++++++----------------- 1 file changed, 61 insertions(+), 79 deletions(-) diff --git a/pybricks/common/pb_type_speaker.c b/pybricks/common/pb_type_speaker.c index fcefea41f..7f5645373 100644 --- a/pybricks/common/pb_type_speaker.c +++ b/pybricks/common/pb_type_speaker.c @@ -20,7 +20,7 @@ #include #include -#include +#include #include #include #include @@ -29,11 +29,11 @@ typedef struct { mp_obj_base_t base; // State of awaitable sound + pb_type_async_t *iter; + pbio_os_timer_t timer; mp_obj_t notes_generator; uint32_t note_duration; - uint32_t beep_end_time; - uint32_t release_end_time; - mp_obj_t awaitables; + uint32_t scaled_duration; // volume in 0..100 range uint8_t volume; @@ -61,45 +61,33 @@ static mp_obj_t pb_type_Speaker_volume(size_t n_args, const mp_obj_t *pos_args, } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Speaker_volume_obj, 1, pb_type_Speaker_volume); -static void pb_type_Speaker_start_beep(uint32_t frequency, uint16_t sample_attenuator) { - pbdrv_beep_start(frequency, sample_attenuator); -} - -static void pb_type_Speaker_stop_beep(void) { - pbdrv_sound_stop(); -} - static mp_obj_t pb_type_Speaker_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { pb_type_Speaker_obj_t *self = mp_obj_malloc(pb_type_Speaker_obj_t, type); - // List of awaitables associated with speaker. By keeping track, - // we can cancel them as needed when a new sound is started. - self->awaitables = mp_obj_new_list(0, NULL); - // REVISIT: If a user creates two Speaker instances, this will reset the volume settings for both. // If done only once per singleton, however, altered volume settings would be persisted between program runs. self->volume = PBDRV_CONFIG_SOUND_DEFAULT_VOLUME; self->sample_attenuator = INT16_MAX; + self->iter = NULL; + return MP_OBJ_FROM_PTR(self); } -static bool pb_type_Speaker_beep_test_completion(mp_obj_t self_in, uint32_t end_time) { - pb_type_Speaker_obj_t *self = MP_OBJ_TO_PTR(self_in); - if (mp_hal_ticks_ms() - self->beep_end_time < (uint32_t)INT32_MAX) { - pb_type_Speaker_stop_beep(); - return true; - } - return false; +static mp_obj_t pb_type_Speaker_close(mp_obj_t self_in) { + pbdrv_sound_stop(); + return mp_const_none; } -static void pb_type_Speaker_cancel(mp_obj_t self_in) { - pb_type_Speaker_stop_beep(); - pb_type_Speaker_obj_t *self = MP_OBJ_TO_PTR(self_in); - self->beep_end_time = mp_hal_ticks_ms(); - self->release_end_time = self->beep_end_time; - self->notes_generator = MP_OBJ_NULL; +static pbio_error_t pb_type_Speaker_beep_iterate_once(pbio_os_state_t *state, mp_obj_t parent_obj) { + pb_type_Speaker_obj_t *self = MP_OBJ_TO_PTR(parent_obj); + // The beep has already been started. We just need to await the duration + // and then stop. + PBIO_OS_ASYNC_BEGIN(state); + PBIO_OS_AWAIT_UNTIL(state, pbio_os_timer_is_expired(&self->timer)); + pbdrv_sound_stop(); + PBIO_OS_ASYNC_END(PBIO_SUCCESS); } static mp_obj_t pb_type_Speaker_beep(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { @@ -111,28 +99,27 @@ static mp_obj_t pb_type_Speaker_beep(size_t n_args, const mp_obj_t *pos_args, mp mp_int_t frequency = pb_obj_get_int(frequency_in); mp_int_t duration = pb_obj_get_int(duration_in); - pb_type_Speaker_start_beep(frequency, self->sample_attenuator); + pbdrv_beep_start(frequency, self->sample_attenuator); if (duration < 0) { return mp_const_none; } - self->beep_end_time = mp_hal_ticks_ms() + (uint32_t)duration; - self->release_end_time = self->beep_end_time; - self->notes_generator = MP_OBJ_NULL; - - return pb_type_awaitable_await_or_wait( - MP_OBJ_FROM_PTR(self), - self->awaitables, - pb_type_awaitable_end_time_none, - pb_type_Speaker_beep_test_completion, - pb_type_awaitable_return_none, - pb_type_Speaker_cancel, - PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + pbio_os_timer_set(&self->timer, pb_obj_get_int(duration_in)); + + pb_type_async_t config = { + .parent_obj = MP_OBJ_FROM_PTR(self), + .iter_once = pb_type_Speaker_beep_iterate_once, + .close = pb_type_Speaker_close, + }; + // New operation always wins; ongoing sound awaitable is cancelled. + pb_type_async_schedule_cancel(self->iter); + return pb_type_async_wait_or_await(&config, &self->iter); + } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Speaker_beep_obj, 1, pb_type_Speaker_beep); -static void pb_type_Speaker_play_note(pb_type_Speaker_obj_t *self, mp_obj_t obj, int duration) { +static void pb_type_Speaker_get_note(mp_obj_t obj, uint32_t note_ms, uint32_t *frequency, uint32_t *total_ms, uint32_t *on_ms) { const char *note = mp_obj_str_get_str(obj); int pos = 0; mp_float_t freq; @@ -274,13 +261,13 @@ static void pb_type_Speaker_play_note(pb_type_Speaker_obj_t *self, mp_obj_t obj, fraction = fraction * 10 + fraction2; } - duration /= fraction; + *total_ms = note_ms / fraction; // optional decorations if (note[pos++] == '.') { // dotted note has length extended by 1/2 - duration = 3 * duration / 2; + *total_ms = 3 * *total_ms / 2; } else { pos--; } @@ -292,39 +279,35 @@ static void pb_type_Speaker_play_note(pb_type_Speaker_obj_t *self, mp_obj_t obj, pos--; } - pb_type_Speaker_start_beep((uint32_t)freq, self->sample_attenuator); - - uint32_t time_now = mp_hal_ticks_ms(); - self->release_end_time = time_now + duration; - self->beep_end_time = release ? time_now + 7 * duration / 8 : time_now + duration; + *frequency = (uint32_t)freq; + *on_ms = release ? 7 * (*total_ms) / 8 : *total_ms; } -static bool pb_type_Speaker_notes_test_completion(mp_obj_t self_in, uint32_t end_time) { - pb_type_Speaker_obj_t *self = MP_OBJ_TO_PTR(self_in); +static pbio_error_t pb_type_Speaker_play_notes_iterate_once(pbio_os_state_t *state, mp_obj_t parent_obj) { + pb_type_Speaker_obj_t *self = MP_OBJ_TO_PTR(parent_obj); + mp_obj_t item; - bool release_done = mp_hal_ticks_ms() - self->release_end_time < (uint32_t)INT32_MAX; - bool beep_done = mp_hal_ticks_ms() - self->beep_end_time < (uint32_t)INT32_MAX; + PBIO_OS_ASYNC_BEGIN(state); - if (self->notes_generator != MP_OBJ_NULL && release_done && beep_done) { - // Full note done, so get next note. - mp_obj_t item = mp_iternext(self->notes_generator); + while ((item = mp_iternext(self->notes_generator)) != MP_OBJ_STOP_ITERATION) { - // If there is no next note, generator is done. - if (item == MP_OBJ_STOP_ITERATION) { - return true; - } + // Parse next note. + uint32_t frequency; + uint32_t beep_time; + pb_type_Speaker_get_note(item, self->note_duration, &frequency, &self->scaled_duration, &beep_time); - // Start the note. - pb_type_Speaker_play_note(self, item, self->note_duration); - return false; - } + // On portion of the note. + pbdrv_beep_start(frequency, self->sample_attenuator); + pbio_os_timer_set(&self->timer, beep_time); + PBIO_OS_AWAIT_UNTIL(state, pbio_os_timer_is_expired(&self->timer)); - if (beep_done) { - // Time to release. - pb_type_Speaker_stop_beep(); + // Off portion of the note. + pbdrv_sound_stop(); + self->timer.duration = self->scaled_duration; + PBIO_OS_AWAIT_UNTIL(state, pbio_os_timer_is_expired(&self->timer)); } - return false; + PBIO_OS_ASYNC_END(PBIO_SUCCESS); } static mp_obj_t pb_type_Speaker_play_notes(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { @@ -335,16 +318,15 @@ static mp_obj_t pb_type_Speaker_play_notes(size_t n_args, const mp_obj_t *pos_ar self->notes_generator = mp_getiter(notes_in, NULL); self->note_duration = 4 * 60 * 1000 / pb_obj_get_int(tempo_in); - self->beep_end_time = mp_hal_ticks_ms(); - self->release_end_time = self->beep_end_time; - return pb_type_awaitable_await_or_wait( - MP_OBJ_FROM_PTR(self), - self->awaitables, - pb_type_awaitable_end_time_none, - pb_type_Speaker_notes_test_completion, - pb_type_awaitable_return_none, - pb_type_Speaker_cancel, - PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + + pb_type_async_t config = { + .parent_obj = MP_OBJ_FROM_PTR(self), + .iter_once = pb_type_Speaker_play_notes_iterate_once, + .close = pb_type_Speaker_close, + }; + // New operation always wins; ongoing sound awaitable is cancelled. + pb_type_async_schedule_cancel(self->iter); + return pb_type_async_wait_or_await(&config, &self->iter); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Speaker_play_notes_obj, 1, pb_type_Speaker_play_notes); From 9deac01665634c7e9f60ed8921ddc4cb0d145804 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 9 Sep 2025 13:05:06 +0200 Subject: [PATCH 11/17] pybricks.iodevices.I2CDevices: Use new awaitable. Also generalize return map to pass in the sensor object. This allows the return map to make use of the sensor state. --- pybricks/iodevices/iodevices.h | 8 +- pybricks/iodevices/pb_type_i2c_device.c | 154 +++++++++--------- .../pb_type_nxtdevices_temperaturesensor.c | 6 +- .../pb_type_nxtdevices_ultrasonicsensor.c | 6 +- pybricks/tools/pb_type_app_data.c | 1 - 5 files changed, 89 insertions(+), 86 deletions(-) diff --git a/pybricks/iodevices/iodevices.h b/pybricks/iodevices/iodevices.h index 5d65e4b7c..e0fa12b03 100644 --- a/pybricks/iodevices/iodevices.h +++ b/pybricks/iodevices/iodevices.h @@ -10,6 +10,8 @@ #include "py/obj.h" +#include + extern const mp_obj_type_t pb_type_iodevices_PUPDevice; extern const mp_obj_type_t pb_type_uart_device; @@ -21,12 +23,14 @@ extern const mp_obj_type_t pb_type_i2c_device; * of a desired form. For example, it could map two bytes to a single floating * point value representing temperature. * + * @param [in] sensor_obj Instance of sensor that owns this device or MP_OBJ_NULL for the standalone I2CDevice class. * @param [in] data The data read. * @param [in] len The data length. + * @return Resulting object to return to user. */ -typedef mp_obj_t (*pb_type_i2c_device_return_map_t)(const uint8_t *data, size_t len); +typedef mp_obj_t (*pb_type_i2c_device_return_map_t)(mp_obj_t sensor_obj, const uint8_t *data, size_t len); -mp_obj_t pb_type_i2c_device_make_new(mp_obj_t port_in, uint8_t address, bool custom, bool powered, bool nxt_quirk); +mp_obj_t pb_type_i2c_device_make_new(mp_obj_t sensor_obj, mp_obj_t port_in, uint8_t address, bool custom, bool powered, bool nxt_quirk); mp_obj_t pb_type_i2c_device_start_operation(mp_obj_t i2c_device_obj, const uint8_t *write_data, size_t write_len, size_t read_len, pb_type_i2c_device_return_map_t return_map); void pb_type_i2c_device_assert_string_at_register(mp_obj_t i2c_device_obj, uint8_t reg, const char *string); diff --git a/pybricks/iodevices/pb_type_i2c_device.c b/pybricks/iodevices/pb_type_i2c_device.c index 40a178d27..fb81252be 100644 --- a/pybricks/iodevices/pb_type_i2c_device.c +++ b/pybricks/iodevices/pb_type_i2c_device.c @@ -15,14 +15,29 @@ #include #include #include +#include #include #include #include -// Object representing a pybricks.iodevices.I2CDevice instance. +/** + * Object representing a pybricks.iodevices.I2CDevice instance. + * + * Also used by sensor classes for I2C Devices. + */ typedef struct { mp_obj_base_t base; + /** + * Object that owns this I2C device, such as an Ultrasonic Sensor instance. + * Gets passed to all return mappings. + * Equals MP_OBJ_NULL when this is the standalone I2CDevice class instance. + */ + mp_obj_t sensor_obj; + /** + * Generic reusable awaitable operation. + */ + pb_type_async_t *iter; /** * The following are buffered parameters for one ongoing I2C operation, See * ::pbdrv_i2c_write_then_read for details on each parameter. We need to @@ -31,17 +46,19 @@ typedef struct { * immediately copied to the driver on the first call to the protothread. */ pbdrv_i2c_dev_t *i2c_dev; - pbio_os_state_t state; uint8_t address; bool nxt_quirk; - pb_type_i2c_device_return_map_t return_map; size_t write_len; size_t read_len; uint8_t *read_buf; + /** + * Maps bytes read to the user return object. + */ + pb_type_i2c_device_return_map_t return_map; } device_obj_t; // pybricks.iodevices.I2CDevice.__init__ -mp_obj_t pb_type_i2c_device_make_new(mp_obj_t port_in, uint8_t address, bool custom, bool powered, bool nxt_quirk) { +mp_obj_t pb_type_i2c_device_make_new(mp_obj_t sensor_obj, mp_obj_t port_in, uint8_t address, bool custom, bool powered, bool nxt_quirk) { pb_module_tools_assert_blocking(); @@ -68,6 +85,8 @@ mp_obj_t pb_type_i2c_device_make_new(mp_obj_t port_in, uint8_t address, bool cus device->i2c_dev = i2c_dev; device->address = address; device->nxt_quirk = nxt_quirk; + device->sensor_obj = sensor_obj; + device->iter = NULL; if (powered) { pbio_port_p1p2_set_power(port, PBIO_PORT_POWER_REQUIREMENTS_BATTERY_VOLTAGE_P1_POS); } @@ -85,29 +104,25 @@ static mp_obj_t make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, PB_ARG_DEFAULT_FALSE(nxt_quirk) ); - return pb_type_i2c_device_make_new(port_in, mp_obj_get_int(address_in), mp_obj_is_true(custom_in), mp_obj_is_true(powered_in), mp_obj_is_true(nxt_quirk_in)); + return pb_type_i2c_device_make_new( + MP_OBJ_NULL, + port_in, + mp_obj_get_int(address_in), + mp_obj_is_true(custom_in), + mp_obj_is_true(powered_in), + mp_obj_is_true(nxt_quirk_in) + ); } -// Object representing the iterable that is returned when calling an I2C -// method. This object can then be awaited (iterated). It has a reference to -// the device from which it was created. Only one operation can be active at -// one time. -typedef struct { - mp_obj_base_t base; - mp_obj_t device_obj; -} operation_obj_t; - -static mp_obj_t operation_close(mp_obj_t op_in) { - // Close is not implemented but needs to exist. - operation_obj_t *op = MP_OBJ_TO_PTR(op_in); - (void)op; - return mp_const_none; -} -static MP_DEFINE_CONST_FUN_OBJ_1(operation_close_obj, operation_close); +/** + * This keeps calling the I2C protothread with cached parameters until completion. + */ +static pbio_error_t pb_type_i2c_device_iterate_once(pbio_os_state_t *state, mp_obj_t i2c_device_obj) { + + device_obj_t *device = MP_OBJ_TO_PTR(i2c_device_obj); -static pbio_error_t operation_iterate_once(device_obj_t *device) { return pbdrv_i2c_write_then_read( - &device->state, device->i2c_dev, + state, device->i2c_dev, device->address, NULL, // Already memcpy'd on initial iteration. No need to provide here. device->write_len, @@ -117,40 +132,23 @@ static pbio_error_t operation_iterate_once(device_obj_t *device) { ); } -static mp_obj_t operation_iternext(mp_obj_t op_in) { - operation_obj_t *op = MP_OBJ_TO_PTR(op_in); - device_obj_t *device = MP_OBJ_TO_PTR(op->device_obj); - - pbio_error_t err = operation_iterate_once(device); - - // Yielded, keep going. - if (err == PBIO_ERROR_AGAIN) { - return mp_const_none; - } - - // Raises on Timeout and other I/O errors. Proceeds on success. - pb_assert(err); +/** + * This is the callable form required by the shared awaitable code. + * + * For classes that have an I2C class instance such as the Ultrasonic Sensor, + * the I2C object is not of interest, but rather the sensor object. So this + * wrapper essentially passes the containing object to the return map. + */ +static mp_obj_t pb_type_i2c_device_return_generic(mp_obj_t i2c_device_obj) { + device_obj_t *device = MP_OBJ_TO_PTR(i2c_device_obj); - // For no return map, return basic stop iteration, which results None. if (!device->return_map) { - return MP_OBJ_STOP_ITERATION; + return mp_const_none; } - // Set return value via stop iteration. - return mp_make_stop_iteration(device->return_map(device->read_buf, device->read_len)); + return device->return_map(device->sensor_obj, device->read_buf, device->read_len); } -static const mp_rom_map_elem_t operation_locals_dict_table[] = { - { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&operation_close_obj) }, -}; -MP_DEFINE_CONST_DICT(operation_locals_dict, operation_locals_dict_table); - -MP_DEFINE_CONST_OBJ_TYPE(operation_type, - MP_QSTR_I2COperation, - MP_TYPE_FLAG_ITER_IS_ITERNEXT, - iter, operation_iternext, - locals_dict, &operation_locals_dict); - mp_obj_t pb_type_i2c_device_start_operation(mp_obj_t i2c_device_obj, const uint8_t *write_data, size_t write_len, size_t read_len, pb_type_i2c_device_return_map_t return_map) { pb_assert_type(i2c_device_obj, &pb_type_i2c_device); @@ -173,32 +171,23 @@ mp_obj_t pb_type_i2c_device_start_operation(mp_obj_t i2c_device_obj, const uint8 } // The initial operation above can fail if an I2C transaction is already in - // progress. If so, we don't want to reset it state or allow the return + // progress. If so, we don't want to reset its state or allow the return // result to be garbage collected. Now that the first iteration succeeded, - // save the state and assign the new result buffer. + // save the state. device->read_len = read_len; device->write_len = write_len; - device->state = state; device->read_buf = NULL; device->return_map = return_map; - // If runloop active, return an awaitable object. - if (pb_module_tools_run_loop_is_active()) { - operation_obj_t *operation = mp_obj_malloc(operation_obj_t, &operation_type); - operation->device_obj = MP_OBJ_FROM_PTR(device); - return MP_OBJ_FROM_PTR(operation); - } - - // Otherwise block and wait for the result here. - while ((err = operation_iterate_once(device)) == PBIO_ERROR_AGAIN) { - MICROPY_EVENT_POLL_HOOK; - } - pb_assert(err); - - if (!device->return_map) { - return mp_const_none; - } - return device->return_map(device->read_buf, device->read_len); + pb_type_async_t config = { + .parent_obj = i2c_device_obj, + .iter_once = pb_type_i2c_device_iterate_once, + .state = state, + .return_map = return_map ? pb_type_i2c_device_return_generic : NULL, + }; + // New operation always wins; ongoing sound awaitable is cancelled. + pb_type_async_schedule_cancel(device->iter); + return pb_type_async_wait_or_await(&config, &device->iter); } /** @@ -207,19 +196,24 @@ mp_obj_t pb_type_i2c_device_start_operation(mp_obj_t i2c_device_obj, const uint8 * string is not found. */ void pb_type_i2c_device_assert_string_at_register(mp_obj_t i2c_device_obj, uint8_t reg, const char *string) { + + device_obj_t *device = MP_OBJ_TO_PTR(i2c_device_obj); + pb_module_tools_assert_blocking(); + size_t read_len = strlen(string); const uint8_t write_data[] = { reg }; - mp_obj_t result = pb_type_i2c_device_start_operation(i2c_device_obj, write_data, MP_ARRAY_SIZE(write_data), strlen(string) - 1, mp_obj_new_bytes); - - size_t result_len; - const char *result_data = mp_obj_str_get_data(result, &result_len); + pb_type_i2c_device_start_operation(i2c_device_obj, write_data, MP_ARRAY_SIZE(write_data), read_len, NULL); - if (memcmp(string, result_data, strlen(string) - 1)) { + if (memcmp(string, device->read_buf, read_len)) { pb_assert(PBIO_ERROR_NO_DEV); } } +static mp_obj_t pb_type_i2c_device_return_bytes(mp_obj_t self_in, const uint8_t *data, size_t len) { + return mp_obj_new_bytes(data, len); +} + // pybricks.iodevices.I2CDevice.read static mp_obj_t 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, @@ -235,7 +229,13 @@ static mp_obj_t read(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) &(uint8_t) { mp_obj_get_int(reg_in) }; size_t write_len = reg_in == mp_const_none ? 0 : 1; - return pb_type_i2c_device_start_operation(MP_OBJ_FROM_PTR(device), write_data, write_len, pb_obj_get_positive_int(length_in), mp_obj_new_bytes); + return pb_type_i2c_device_start_operation( + MP_OBJ_FROM_PTR(device), + write_data, + write_len, + pb_obj_get_positive_int(length_in), + pb_type_i2c_device_return_bytes + ); } static MP_DEFINE_CONST_FUN_OBJ_KW(read_obj, 0, read); diff --git a/pybricks/nxtdevices/pb_type_nxtdevices_temperaturesensor.c b/pybricks/nxtdevices/pb_type_nxtdevices_temperaturesensor.c index 711d93c1a..447eb11d2 100644 --- a/pybricks/nxtdevices/pb_type_nxtdevices_temperaturesensor.c +++ b/pybricks/nxtdevices/pb_type_nxtdevices_temperaturesensor.c @@ -21,7 +21,7 @@ // pybricks.nxtdevices.TemperatureSensor class object typedef struct _nxtdevices_TemperatureSensor_obj_t { mp_obj_base_t base; - mp_obj_t *i2c_device_obj; + mp_obj_t i2c_device_obj; } nxtdevices_TemperatureSensor_obj_t; // pybricks.nxtdevices.TemperatureSensor.__init__ @@ -30,7 +30,7 @@ static mp_obj_t nxtdevices_TemperatureSensor_make_new(const mp_obj_type_t *type, PB_ARG_REQUIRED(port)); nxtdevices_TemperatureSensor_obj_t *self = mp_obj_malloc(nxtdevices_TemperatureSensor_obj_t, type); - self->i2c_device_obj = pb_type_i2c_device_make_new(port_in, 0x4C, true, false, false); + self->i2c_device_obj = pb_type_i2c_device_make_new(MP_OBJ_FROM_PTR(self), port_in, 0x4C, true, false, false); // Set resolution to 0.125 degrees as a fair balance between speed and accuracy. const uint8_t write_data[] = { 0x01, (1 << 6) | (0 << 5) }; @@ -39,7 +39,7 @@ static mp_obj_t nxtdevices_TemperatureSensor_make_new(const mp_obj_type_t *type, return MP_OBJ_FROM_PTR(self); } -static mp_obj_t map_temperature(const uint8_t *data, size_t len) { +static mp_obj_t map_temperature(mp_obj_t self_in, const uint8_t *data, size_t len) { int16_t combined = ((uint16_t)data[0] << 8) | data[1]; return mp_obj_new_float_from_f((combined >> 4) / 16.0f); } diff --git a/pybricks/nxtdevices/pb_type_nxtdevices_ultrasonicsensor.c b/pybricks/nxtdevices/pb_type_nxtdevices_ultrasonicsensor.c index 467a45dec..0e63b9136 100644 --- a/pybricks/nxtdevices/pb_type_nxtdevices_ultrasonicsensor.c +++ b/pybricks/nxtdevices/pb_type_nxtdevices_ultrasonicsensor.c @@ -21,7 +21,7 @@ // pybricks.nxtdevices.UltrasonicSensor class object typedef struct _nxtdevices_UltrasonicSensor_obj_t { mp_obj_base_t base; - mp_obj_t *i2c_device_obj; + mp_obj_t i2c_device_obj; } nxtdevices_UltrasonicSensor_obj_t; // pybricks.nxtdevices.UltrasonicSensor.__init__ @@ -30,7 +30,7 @@ static mp_obj_t nxtdevices_UltrasonicSensor_make_new(const mp_obj_type_t *type, PB_ARG_REQUIRED(port)); nxtdevices_UltrasonicSensor_obj_t *self = mp_obj_malloc(nxtdevices_UltrasonicSensor_obj_t, type); - self->i2c_device_obj = pb_type_i2c_device_make_new(port_in, 0x01, false, true, true); + self->i2c_device_obj = pb_type_i2c_device_make_new(MP_OBJ_FROM_PTR(self), port_in, 0x01, false, true, true); // NXT Ultrasonic Sensor appears to need some time after initializing I2C pins before it can receive data. mp_hal_delay_ms(100); @@ -41,7 +41,7 @@ static mp_obj_t nxtdevices_UltrasonicSensor_make_new(const mp_obj_type_t *type, return MP_OBJ_FROM_PTR(self); } -static mp_obj_t map_distance(const uint8_t *data, size_t len) { +static mp_obj_t map_distance(mp_obj_t self_in, const uint8_t *data, size_t len) { return mp_obj_new_int(data[0] * 10); } diff --git a/pybricks/tools/pb_type_app_data.c b/pybricks/tools/pb_type_app_data.c index 038a99732..9766316df 100644 --- a/pybricks/tools/pb_type_app_data.c +++ b/pybricks/tools/pb_type_app_data.c @@ -14,7 +14,6 @@ #include "py/objstr.h" #include -#include #include #include From 5e489ff451a5e672be235f27d6130f72b591f64d Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 9 Sep 2025 14:08:32 +0200 Subject: [PATCH 12/17] pybricks.iodevices.I2CDevices: Allow user callback. Writing both the def and async def for each user method is cumbersome. This allows the user to make a read call and specify how to map the bytes to a return value. This method may then be called as async or sync without further wrappers. --- pybricks/iodevices/pb_type_i2c_device.c | 29 +++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/pybricks/iodevices/pb_type_i2c_device.c b/pybricks/iodevices/pb_type_i2c_device.c index fb81252be..44b6d4ee6 100644 --- a/pybricks/iodevices/pb_type_i2c_device.c +++ b/pybricks/iodevices/pb_type_i2c_device.c @@ -31,7 +31,9 @@ typedef struct { /** * Object that owns this I2C device, such as an Ultrasonic Sensor instance. * Gets passed to all return mappings. - * Equals MP_OBJ_NULL when this is the standalone I2CDevice class instance. + * + * In case of the standalone I2CDevice class instance, this value is instead + * used to store an optional user callable to map bytes to a return object. */ mp_obj_t sensor_obj; /** @@ -105,7 +107,7 @@ static mp_obj_t make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, ); return pb_type_i2c_device_make_new( - MP_OBJ_NULL, + MP_OBJ_NULL, // Not associated with any particular sensor instance. port_in, mp_obj_get_int(address_in), mp_obj_is_true(custom_in), @@ -210,16 +212,29 @@ void pb_type_i2c_device_assert_string_at_register(mp_obj_t i2c_device_obj, uint8 } } +/** + * I2C result mapping that just returns a bytes object. + */ static mp_obj_t pb_type_i2c_device_return_bytes(mp_obj_t self_in, const uint8_t *data, size_t len) { return mp_obj_new_bytes(data, len); } +/** + * I2C result mapping that calls user provided callback with self and bytes as argument. + */ +static mp_obj_t pb_type_i2c_device_return_user_map(mp_obj_t callable_obj, const uint8_t *data, size_t len) { + // If user provides bound method, MicroPython takes care of providing + // self as the first argument. We just need to pass in data arg. + return mp_call_function_1(callable_obj, mp_obj_new_bytes(data, len)); +} + // pybricks.iodevices.I2CDevice.read static mp_obj_t 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, device_obj_t, device, PB_ARG_DEFAULT_NONE(reg), - PB_ARG_DEFAULT_INT(length, 1) + PB_ARG_DEFAULT_INT(length, 1), + PB_ARG_DEFAULT_NONE(map) ); // Write payload is one byte representing the register we want to read, @@ -229,12 +244,18 @@ static mp_obj_t read(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) &(uint8_t) { mp_obj_get_int(reg_in) }; size_t write_len = reg_in == mp_const_none ? 0 : 1; + // Optional user provided callback method of the form def my_method(self, data) + // We can use sensor_obj for this since it isn't used by I2CDevice instances, + // and we are already passing this to the mapping anyway, so we can conviently + // use it to pass the callable object in this case. + device->sensor_obj = mp_obj_is_callable(map_in) ? map_in : MP_OBJ_NULL; + return pb_type_i2c_device_start_operation( MP_OBJ_FROM_PTR(device), write_data, write_len, pb_obj_get_positive_int(length_in), - pb_type_i2c_device_return_bytes + mp_obj_is_callable(map_in) ? pb_type_i2c_device_return_user_map : pb_type_i2c_device_return_bytes ); } static MP_DEFINE_CONST_FUN_OBJ_KW(read_obj, 0, read); From fbe975277a6fc0c5f5a0d8e8adb1ab6704a64d9a Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 9 Sep 2025 17:20:43 +0200 Subject: [PATCH 13/17] pybricks.iodevices.UARTDevice: Use new awaitable. Also flush UART when opening, in case program stopped during prior transmission. --- pybricks/iodevices/pb_type_uart_device.c | 93 +++++++++--------------- 1 file changed, 34 insertions(+), 59 deletions(-) diff --git a/pybricks/iodevices/pb_type_uart_device.c b/pybricks/iodevices/pb_type_uart_device.c index 0ea7bee5b..b2a1af3c7 100644 --- a/pybricks/iodevices/pb_type_uart_device.c +++ b/pybricks/iodevices/pb_type_uart_device.c @@ -16,10 +16,10 @@ #include #include +#include #include #include -#include #include // pybricks.iodevices.uart_device class object @@ -28,12 +28,10 @@ typedef struct _pb_type_uart_device_obj_t { pbio_port_t *port; pbdrv_uart_dev_t *uart_dev; uint32_t timeout; - pbio_os_state_t write_pt; + pb_type_async_t *write_iter; mp_obj_t write_obj; - mp_obj_t write_awaitables; - pbio_os_state_t read_pt; + pb_type_async_t *read_iter; mp_obj_t read_obj; - mp_obj_t read_awaitables; } pb_type_uart_device_obj_t; // pybricks.iodevices.UARTDevice.__init__ @@ -62,31 +60,27 @@ static mp_obj_t pb_type_uart_device_make_new(const mp_obj_type_t *type, size_t n pb_assert(pbio_port_get_port(port_id, &self->port)); pbio_port_set_mode(self->port, PBIO_PORT_MODE_UART); pb_assert(pbio_port_get_uart_dev(self->port, &self->uart_dev)); + pbdrv_uart_flush(self->uart_dev); - // List of awaitables associated with reading and writing. - self->write_awaitables = mp_obj_new_list(0, NULL); - self->read_awaitables = mp_obj_new_list(0, NULL); + // Awaitables associated with reading and writing. + self->write_iter = NULL; + self->read_iter = NULL; return MP_OBJ_FROM_PTR(self); } -static bool pb_type_uart_device_write_test_completion(mp_obj_t self_in, uint32_t end_time) { +static pbio_error_t pb_type_uart_device_write_iter_once(pbio_os_state_t *state, mp_obj_t self_in) { pb_type_uart_device_obj_t *self = MP_OBJ_TO_PTR(self_in); GET_STR_DATA_LEN(self->write_obj, data, data_len); + return pbdrv_uart_write(state, self->uart_dev, (uint8_t *)data, data_len, self->timeout); +} - // Runs one iteration of the write protothread. - pbio_error_t err = pbdrv_uart_write(&self->read_pt, self->uart_dev, (uint8_t *)data, data_len, self->timeout); - if (err == PBIO_ERROR_AGAIN) { - // Not done yet, so return false. - return false; - } - - // Complete or stopped, so allow written object to be garbage collected. - self->write_obj = mp_const_none; - - // Either completed or timed out, so assert it. - pb_assert(err); - return true; +static mp_obj_t pb_type_uart_device_write_return_map(mp_obj_t self_in) { + pb_type_uart_device_obj_t *self = MP_OBJ_TO_PTR(self_in); + // Write always returns none, but this is effectively a completion callback. + // So we can use it to disconnect the write object so it can be garbage collected. + self->write_obj = MP_OBJ_NULL; + return mp_const_none; } // pybricks.iodevices.UARTDevice.write @@ -101,20 +95,16 @@ static mp_obj_t pb_type_uart_device_write(size_t n_args, const mp_obj_t *pos_arg pb_assert(PBIO_ERROR_INVALID_ARG); } - // Reset protothread state. - self->write_pt = 0; - // Prevents this object from being garbage collected while the write is in progress. self->write_obj = data_in; - return pb_type_awaitable_await_or_wait( - MP_OBJ_FROM_PTR(self), - self->write_awaitables, - pb_type_awaitable_end_time_none, - pb_type_uart_device_write_test_completion, - pb_type_awaitable_return_none, - pb_type_awaitable_cancel_none, - PB_TYPE_AWAITABLE_OPT_RAISE_ON_BUSY); + pb_type_async_t config = { + .iter_once = pb_type_uart_device_write_iter_once, + .parent_obj = MP_OBJ_FROM_PTR(self), + .return_map = pb_type_uart_device_write_return_map, + }; + pb_type_async_schedule_cancel(self->write_iter); + return pb_type_async_wait_or_await(&config, &self->write_iter); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_uart_device_write_obj, 1, pb_type_uart_device_write); @@ -127,27 +117,16 @@ static mp_obj_t pb_type_uart_device_in_waiting(mp_obj_t self_in) { } static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_uart_device_in_waiting_obj, pb_type_uart_device_in_waiting); -static bool pb_type_uart_device_read_test_completion(mp_obj_t self_in, uint32_t end_time) { +static pbio_error_t pb_type_uart_device_read_iter_once(pbio_os_state_t *state, mp_obj_t self_in) { pb_type_uart_device_obj_t *self = MP_OBJ_TO_PTR(self_in); - mp_obj_str_t *str = MP_OBJ_TO_PTR(self->read_obj); - - // Runs one iteration of the read protothread. - pbio_error_t err = pbdrv_uart_read(&self->read_pt, self->uart_dev, (uint8_t *)str->data, str->len, self->timeout); - if (err == PBIO_ERROR_AGAIN) { - // Not done yet, so return false. - return false; - } - - // Either completed or timed out, so assert it. - pb_assert(err); - return true; + return pbdrv_uart_read(state, self->uart_dev, (uint8_t *)str->data, str->len, self->timeout); } -static mp_obj_t pb_type_uart_device_read_return_value(mp_obj_t self_in) { +static mp_obj_t pb_type_uart_device_read_return_map(mp_obj_t self_in) { pb_type_uart_device_obj_t *self = MP_OBJ_TO_PTR(self_in); mp_obj_t ret = self->read_obj; - self->read_obj = mp_const_none; + self->read_obj = MP_OBJ_NULL; return ret; } @@ -162,17 +141,13 @@ static mp_obj_t pb_type_uart_device_read(size_t n_args, const mp_obj_t *pos_args mp_obj_t args[] = { length_in }; self->read_obj = MP_OBJ_TYPE_GET_SLOT(&mp_type_bytes, make_new)((mp_obj_t)&mp_type_bytes, MP_ARRAY_SIZE(args), 0, args); - // Reset protothread state. - self->read_pt = 0; - - return pb_type_awaitable_await_or_wait( - MP_OBJ_FROM_PTR(self), - self->read_awaitables, - pb_type_awaitable_end_time_none, - pb_type_uart_device_read_test_completion, - pb_type_uart_device_read_return_value, - pb_type_awaitable_cancel_none, - PB_TYPE_AWAITABLE_OPT_RAISE_ON_BUSY); + pb_type_async_t config = { + .iter_once = pb_type_uart_device_read_iter_once, + .parent_obj = MP_OBJ_FROM_PTR(self), + .return_map = pb_type_uart_device_read_return_map, + }; + pb_type_async_schedule_cancel(self->read_iter); + return pb_type_async_wait_or_await(&config, &self->read_iter); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_uart_device_read_obj, 1, pb_type_uart_device_read); From f676f6264258ad53697c6f10d741ab0c38ba515c Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Tue, 9 Sep 2025 17:23:09 +0200 Subject: [PATCH 14/17] pybricks.tools: Drop legacy awaitable code. All modules have been upgraded, so we can drop this. --- bricks/_common/sources.mk | 1 - pybricks/common.h | 1 - pybricks/tools/pb_type_awaitable.c | 249 ----------------------------- pybricks/tools/pb_type_awaitable.h | 100 ------------ 4 files changed, 351 deletions(-) delete mode 100644 pybricks/tools/pb_type_awaitable.c delete mode 100644 pybricks/tools/pb_type_awaitable.h diff --git a/bricks/_common/sources.mk b/bricks/_common/sources.mk index 191724bdc..cd6bf5d32 100644 --- a/bricks/_common/sources.mk +++ b/bricks/_common/sources.mk @@ -98,7 +98,6 @@ PYBRICKS_PYBRICKS_SRC_C = $(addprefix pybricks/,\ robotics/pb_type_spikebase.c \ tools/pb_module_tools.c \ tools/pb_type_app_data.c \ - tools/pb_type_awaitable.c \ tools/pb_type_async.c \ tools/pb_type_matrix.c \ tools/pb_type_stopwatch.c \ diff --git a/pybricks/common.h b/pybricks/common.h index 0b13d8199..59d7b177a 100644 --- a/pybricks/common.h +++ b/pybricks/common.h @@ -23,7 +23,6 @@ #include #include #include -#include #include void pb_package_pybricks_init(bool import_all); diff --git a/pybricks/tools/pb_type_awaitable.c b/pybricks/tools/pb_type_awaitable.c deleted file mode 100644 index 420915b65..000000000 --- a/pybricks/tools/pb_type_awaitable.c +++ /dev/null @@ -1,249 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2022-2023 The Pybricks Authors - -#include "py/mpconfig.h" - -#if PYBRICKS_PY_TOOLS - -#include "py/mphal.h" -#include "py/mpstate.h" -#include "py/obj.h" -#include "py/runtime.h" - -#include -#include - -// The awaitable object is free to be reused. -#define AWAITABLE_FREE (NULL) - -struct _pb_type_awaitable_obj_t { - mp_obj_base_t base; - /** - * Object associated with this awaitable, such as the motor we wait on. - */ - mp_obj_t obj; - /** - * End time. Gets passed to completion test to allow for graceful timeout - * or raise timeout errors if desired. - */ - uint32_t end_time; - /** - * Tests if operation is complete. Gets reset to AWAITABLE_FREE - * on completion, which means that it can be used again. - */ - pb_type_awaitable_test_completion_t test_completion; - /** - * Gets the return value of the awaitable. - */ - pb_type_awaitable_return_t return_value; - /** - * Called on cancellation. - */ - pb_type_awaitable_cancel_t cancel; -}; - -// close() cancels the awaitable. -static mp_obj_t pb_type_awaitable_close(mp_obj_t self_in) { - pb_type_awaitable_obj_t *self = MP_OBJ_TO_PTR(self_in); - self->test_completion = AWAITABLE_FREE; - // Handle optional clean up/cancelling of hardware operation. - if (self->cancel) { - self->cancel(self->obj); - } - return mp_const_none; -} -static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_awaitable_close_obj, pb_type_awaitable_close); - -/** - * Completion checker that is always true. - * - * Linked awaitables are gracefully cancelled by setting this as the completion - * checker. This allows MicroPython to handle completion during the next call - * to iternext. - */ -static bool pb_type_awaitable_test_completion_completed(mp_obj_t self_in, uint32_t start_time) { - return true; -} - -/** - * Special completion test for awaitable that should yield exactly once. - * - * It will return false to indicate that it is not done. Then the iternext will - * replace this test with one that is always done, thus completing next time. - */ -bool pb_type_awaitable_test_completion_yield_once(mp_obj_t obj, uint32_t end_time) { - return false; -} - -static mp_obj_t pb_type_awaitable_iternext(mp_obj_t self_in) { - pb_type_awaitable_obj_t *self = MP_OBJ_TO_PTR(self_in); - - // If completed callback was unset, then we completed previously. - if (self->test_completion == AWAITABLE_FREE) { - return MP_OBJ_STOP_ITERATION; - } - - bool complete = self->test_completion(self->obj, self->end_time); - - // If this was a special awaitable that was supposed to yield exactly once, - // it will now be yielding by being not complete, but complete the next time. - if (self->test_completion == pb_type_awaitable_test_completion_yield_once) { - self->test_completion = pb_type_awaitable_test_completion_completed; - } - - // Keep going if not completed by returning None. - if (!complete) { - return mp_const_none; - } - - // Complete, so unset callback. - self->test_completion = AWAITABLE_FREE; - - // For no return value, return basic stop iteration. - if (!self->return_value) { - return MP_OBJ_STOP_ITERATION; - } - - // Otherwise, set return value via stop iteration. - return mp_make_stop_iteration(self->return_value(self->obj)); -} - -static const mp_rom_map_elem_t pb_type_awaitable_locals_dict_table[] = { - { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&pb_type_awaitable_close_obj) }, -}; -MP_DEFINE_CONST_DICT(pb_type_awaitable_locals_dict, pb_type_awaitable_locals_dict_table); - -// This is a partial implementation of the Python generator type. It is missing -// send(value) and throw(type[, value[, traceback]]) -MP_DEFINE_CONST_OBJ_TYPE(pb_type_awaitable, - MP_QSTR_wait, - MP_TYPE_FLAG_ITER_IS_ITERNEXT, - iter, pb_type_awaitable_iternext, - locals_dict, &pb_type_awaitable_locals_dict); - -/** - * Gets an awaitable object that is not in use, or makes a new one. - * - * @param [in] awaitables_in List of awaitables associated with @p obj. - */ -static pb_type_awaitable_obj_t *pb_type_awaitable_get(mp_obj_t awaitables_in) { - - mp_obj_list_t *awaitables = MP_OBJ_TO_PTR(awaitables_in); - - for (size_t i = 0; i < awaitables->len; i++) { - pb_type_awaitable_obj_t *awaitable = MP_OBJ_TO_PTR(awaitables->items[i]); - - // Return awaitable if it is not in use. - if (awaitable->test_completion == AWAITABLE_FREE) { - return awaitable; - } - } - - // Otherwise allocate a new one. - pb_type_awaitable_obj_t *awaitable = mp_obj_malloc(pb_type_awaitable_obj_t, &pb_type_awaitable); - awaitable->test_completion = AWAITABLE_FREE; - - // Add to list of awaitables. - mp_obj_list_append(awaitables_in, MP_OBJ_FROM_PTR(awaitable)); - - return awaitable; -} - -/** - * Checks and updates all awaitables associated with an object. - * - * @param [in] awaitables_in List of awaitables associated with @p obj. - * @param [in] options Controls update behavior. - */ -void pb_type_awaitable_update_all(mp_obj_t awaitables_in, pb_type_awaitable_opt_t options) { - - // Exit if nothing to do. - if (!pb_module_tools_run_loop_is_active() || options == PB_TYPE_AWAITABLE_OPT_NONE) { - return; - } - - mp_obj_list_t *awaitables = MP_OBJ_TO_PTR(awaitables_in); - - for (size_t i = 0; i < awaitables->len; i++) { - pb_type_awaitable_obj_t *awaitable = MP_OBJ_TO_PTR(awaitables->items[i]); - - // Skip awaitables that are not in use. - if (!awaitable->test_completion) { - continue; - } - - // Raise EBUSY if requested. - if (options & PB_TYPE_AWAITABLE_OPT_RAISE_ON_BUSY) { - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("This resource cannot be used in two tasks at once.")); - } - - // Cancel hardware operation if requested and available. - if (options & PB_TYPE_AWAITABLE_OPT_CANCEL_HARDWARE && awaitable->cancel) { - awaitable->cancel(awaitable->obj); - } - // Set awaitable to done so it gets cancelled it gracefully on the - // next iteration. - if (options & PB_TYPE_AWAITABLE_OPT_CANCEL_ALL) { - awaitable->test_completion = pb_type_awaitable_test_completion_completed; - } - - } -} - -/** - * Get a new awaitable in async mode or block and wait for it to complete in sync mode. - * - * Automatically cancels any previous awaitables associated with the object if requested. - * - * @param [in] obj The object whose method we want to wait for completion. - * @param [in] awaitables_in List of awaitables associated with @p obj. - * @param [in] end_time Wall time in milliseconds when the operation should end. - * May be arbitrary if completion function does not need it. - * @param [in] test_completion_func Function to test if the operation is complete. - * @param [in] return_value_func Function that gets the return value for the awaitable. - * @param [in] cancel_func Function to cancel the hardware operation. - * @param [in] options Controls awaitable behavior. - */ -mp_obj_t pb_type_awaitable_await_or_wait( - mp_obj_t obj, - mp_obj_t awaitables_in, - uint32_t end_time, - pb_type_awaitable_test_completion_t test_completion_func, - pb_type_awaitable_return_t return_value_func, - pb_type_awaitable_cancel_t cancel_func, - pb_type_awaitable_opt_t options) { - - // Within run loop, return the generator that user program will iterate. - if (pb_module_tools_run_loop_is_active()) { - - // Some operations are not allowed in async mode. - if (options & PB_TYPE_AWAITABLE_OPT_FORCE_BLOCK) { - pb_module_tools_assert_blocking(); - } - - // First cancel linked awaitables if requested. - pb_type_awaitable_update_all(awaitables_in, options); - - // Gets free existing awaitable or creates a new one. - pb_type_awaitable_obj_t *awaitable = pb_type_awaitable_get(awaitables_in); - - // Initialize awaitable. - awaitable->obj = obj; - awaitable->test_completion = test_completion_func; - awaitable->return_value = return_value_func; - awaitable->cancel = cancel_func; - awaitable->end_time = end_time; - return MP_OBJ_FROM_PTR(awaitable); - } - - // Outside run loop, block until the operation is complete. - while (test_completion_func && !test_completion_func(obj, end_time)) { - mp_hal_delay_ms(1); - } - if (!return_value_func) { - return mp_const_none; - } - return return_value_func(obj); -} - -#endif // PYBRICKS_PY_TOOLS diff --git a/pybricks/tools/pb_type_awaitable.h b/pybricks/tools/pb_type_awaitable.h deleted file mode 100644 index b63f434b8..000000000 --- a/pybricks/tools/pb_type_awaitable.h +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2023 The Pybricks Authors - -#ifndef PYBRICKS_INCLUDED_PYBRICKS_TOOLS_AWAITABLE_H -#define PYBRICKS_INCLUDED_PYBRICKS_TOOLS_AWAITABLE_H - -#include "py/mpconfig.h" - -#if PYBRICKS_PY_TOOLS - -#include "py/obj.h" - -/** - * Options for canceling an awaitable. - */ -typedef enum _pb_type_awaitable_opt_t { - /** No options. */ - PB_TYPE_AWAITABLE_OPT_NONE = 0, - /** - * Forces the awaitable to block until completion. Raises RuntimeError if - * called inside the run loop. This can be used to wait for operations like - * initializing a sensor or connecting to a remote. - */ - PB_TYPE_AWAITABLE_OPT_FORCE_BLOCK = 1 << 1, - /** - * Makes all linked awaitables end gracefully. Can be used if awaitables - * running in parallel are using the same resources. This way, the newly - * started operation "wins" and everything else is cancelled. - */ - PB_TYPE_AWAITABLE_OPT_CANCEL_ALL = 1 << 2, - /** - * On cancelling the linked awaitables, also call their cancel function - * to stop hardware. Only used to close hardware resources that aren't - * already cleaned up by lower level drivers (so not needed for motors). - */ - PB_TYPE_AWAITABLE_OPT_CANCEL_HARDWARE = 1 << 3, - /** - * Raises EBUSY if the resource is already in use. Used for resources that - * do not support graceful cancellation. - */ - PB_TYPE_AWAITABLE_OPT_RAISE_ON_BUSY = 1 << 4, -} pb_type_awaitable_opt_t; - -/** - * A generator-like type for waiting on some operation to complete. - */ -typedef struct _pb_type_awaitable_obj_t pb_type_awaitable_obj_t; - -/** - * Tests if awaitable operation is complete. - * - * On completion, this function is expected to close/stop hardware - * operations as needed (hold a motor, etc.). This is not the same as cancel - * below, which always stops the relevant hardware (i.e. always coast). - * - * @param [in] obj The object associated with this awaitable. - * @param [in] start_time The time when the awaitable was created. - * @return True if operation is complete, False otherwise. - */ -typedef bool (*pb_type_awaitable_test_completion_t)(mp_obj_t obj, uint32_t end_time); - -/** - * Gets the return value of the awaitable. If it always returns None, providing - * this function is not necessary. - * - * @param [in] obj The object associated with this awaitable. - * @return The return value of the awaitable. - */ -typedef mp_obj_t (*pb_type_awaitable_return_t)(mp_obj_t obj); - -/** - * Called on cancel/close. Used to stop hardware operation in unhandled - * conditions. - * - * @param [in] obj The object associated with this awaitable. - */ -typedef void (*pb_type_awaitable_cancel_t)(mp_obj_t obj); - -#define pb_type_awaitable_end_time_none (0) - -#define pb_type_awaitable_return_none (NULL) - -#define pb_type_awaitable_cancel_none (NULL) - -bool pb_type_awaitable_test_completion_yield_once(mp_obj_t obj, uint32_t end_time); - -void pb_type_awaitable_update_all(mp_obj_t awaitables_in, pb_type_awaitable_opt_t options); - -mp_obj_t pb_type_awaitable_await_or_wait( - mp_obj_t obj, - mp_obj_t awaitables_in, - uint32_t end_time, - pb_type_awaitable_test_completion_t test_completion_func, - pb_type_awaitable_return_t return_value_func, - pb_type_awaitable_cancel_t cancel_func, - pb_type_awaitable_opt_t options); - -#endif // PYBRICKS_PY_TOOLS - -#endif // PYBRICKS_INCLUDED_PYBRICKS_TOOLS_AWAITABLE_H From 95228092929937799e109908c431ab9b07c4f82e Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Thu, 11 Sep 2025 14:06:50 +0200 Subject: [PATCH 15/17] pybricks.tools: Fix cancellation when not active. This would shedule cancellation even when already completed. This flagged the awaitable not ready for recycling, so would cause allocation when this isn't needed. --- pybricks/tools/pb_type_async.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pybricks/tools/pb_type_async.c b/pybricks/tools/pb_type_async.c index 6bd994ffa..24005950f 100644 --- a/pybricks/tools/pb_type_async.c +++ b/pybricks/tools/pb_type_async.c @@ -14,12 +14,14 @@ /** * Cancels the iterable so it will stop awaiting. * - * This will not call close(). Safe to call even if iter is NULL. + * This will not call close(). Safe to call even if iter is NULL or if it is + * already complete. * * @param [in] iter The awaitable object. */ void pb_type_async_schedule_cancel(pb_type_async_t *iter) { - if (!iter) { + if (!iter || iter->parent_obj == MP_OBJ_NULL) { + // Don't schedule if already complete. return; } // Don't set it to MP_OBJ_NULL right away, or the calling code wouldn't From 0e5b020d3050c49e227872f7efd9ee76b39c243c Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Wed, 17 Sep 2025 19:38:58 +0200 Subject: [PATCH 16/17] pybricks.tools: Include stop option when making object. Almost everywhere we use pb_type_async_wait_or_await we stop the ongoing iteration first, so include that functionality as a bool. It is still also available separately for methods that want to stop ongoing awaitables without spawning another one. Also rename pb_type_async_schedule_cancel to pb_type_async_schedule_stop_iteration since cancel isn't quite the right word and this makes it more explicit what it does. --- pybricks/common/pb_type_device.c | 4 ++-- pybricks/common/pb_type_lightmatrix.c | 3 +-- pybricks/common/pb_type_motor.c | 11 ++++----- pybricks/common/pb_type_speaker.c | 6 ++--- pybricks/iodevices/pb_type_i2c_device.c | 3 +-- pybricks/iodevices/pb_type_uart_device.c | 6 ++--- pybricks/robotics/pb_type_drivebase.c | 9 ++++--- pybricks/tools/pb_module_tools.c | 4 ++-- pybricks/tools/pb_type_async.c | 30 +++++++++++++++++++----- pybricks/tools/pb_type_async.h | 9 ++++--- 10 files changed, 47 insertions(+), 38 deletions(-) diff --git a/pybricks/common/pb_type_device.c b/pybricks/common/pb_type_device.c index b9b1f536f..05d2de468 100644 --- a/pybricks/common/pb_type_device.c +++ b/pybricks/common/pb_type_device.c @@ -97,7 +97,7 @@ mp_obj_t pb_type_device_method_call(mp_obj_t self_in, size_t n_args, size_t n_kw .parent_obj = sensor_in, .return_map = method->get_values, }; - return pb_type_async_wait_or_await(&config, &sensor->last_awaitable); + return pb_type_async_wait_or_await(&config, &sensor->last_awaitable, false); } /** @@ -126,7 +126,7 @@ mp_obj_t pb_type_device_set_data(pb_type_device_obj_base_t *sensor, uint8_t mode .iter_once = pb_pup_device_iter_once, .parent_obj = MP_OBJ_FROM_PTR(sensor), }; - return pb_type_async_wait_or_await(&config, &sensor->last_awaitable); + return pb_type_async_wait_or_await(&config, &sensor->last_awaitable, false); } void pb_device_set_lego_mode(pbio_port_t *port) { diff --git a/pybricks/common/pb_type_lightmatrix.c b/pybricks/common/pb_type_lightmatrix.c index 5caffe853..e46df968f 100644 --- a/pybricks/common/pb_type_lightmatrix.c +++ b/pybricks/common/pb_type_lightmatrix.c @@ -337,8 +337,7 @@ static mp_obj_t common_LightMatrix_text(size_t n_args, const mp_obj_t *pos_args, .iter_once = pb_type_lightmatrix_text_iterate_once, }; // New operation always wins; ongoing animation is cancelled. - pb_type_async_schedule_cancel(self->text_iter); - return pb_type_async_wait_or_await(&config, &self->text_iter); + return pb_type_async_wait_or_await(&config, &self->text_iter, true); } static MP_DEFINE_CONST_FUN_OBJ_KW(common_LightMatrix_text_obj, 1, common_LightMatrix_text); diff --git a/pybricks/common/pb_type_motor.c b/pybricks/common/pb_type_motor.c index 6adcc259f..84817778e 100644 --- a/pybricks/common/pb_type_motor.c +++ b/pybricks/common/pb_type_motor.c @@ -243,7 +243,7 @@ static mp_obj_t pb_type_Motor_reset_angle(size_t n_args, const mp_obj_t *pos_arg // Set the new angle pb_assert(pbio_servo_reset_angle(self->srv, reset_angle, reset_to_abs)); - pb_type_async_schedule_cancel(self->last_awaitable); + pb_type_async_schedule_stop_iteration(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_reset_angle_obj, 1, pb_type_Motor_reset_angle); @@ -268,7 +268,7 @@ static mp_obj_t pb_type_Motor_run(size_t n_args, const mp_obj_t *pos_args, mp_ma mp_int_t speed = pb_obj_get_int(speed_in); pb_assert(pbio_servo_run_forever(self->srv, speed)); - pb_type_async_schedule_cancel(self->last_awaitable); + pb_type_async_schedule_stop_iteration(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_obj, 1, pb_type_Motor_run); @@ -277,7 +277,7 @@ static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_run_obj, 1, pb_type_Motor_run); static mp_obj_t pb_type_Motor_hold(mp_obj_t self_in) { pb_type_Motor_obj_t *self = MP_OBJ_TO_PTR(self_in); pb_assert(pbio_servo_stop(self->srv, PBIO_CONTROL_ON_COMPLETION_HOLD)); - pb_type_async_schedule_cancel(self->last_awaitable); + pb_type_async_schedule_stop_iteration(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_1(pb_type_Motor_hold_obj, pb_type_Motor_hold); @@ -313,8 +313,7 @@ static mp_obj_t pb_type_motor_wait_or_await(pb_type_Motor_obj_t *self, bool retu .return_map = return_final_angle ? pb_type_motor_get_final_angle : NULL, }; // New operation always wins; ongoing awaitable motor motion is cancelled. - pb_type_async_schedule_cancel(self->last_awaitable); - return pb_type_async_wait_or_await(&config, &self->last_awaitable); + return pb_type_async_wait_or_await(&config, &self->last_awaitable, true); } // pybricks.common.Motor.run_time @@ -429,7 +428,7 @@ static mp_obj_t pb_type_Motor_track_target(size_t n_args, const mp_obj_t *pos_ar mp_int_t target_angle = pb_obj_get_int(target_angle_in); pb_assert(pbio_servo_track_target(self->srv, target_angle)); - pb_type_async_schedule_cancel(self->last_awaitable); + pb_type_async_schedule_stop_iteration(self->last_awaitable); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Motor_track_target_obj, 1, pb_type_Motor_track_target); diff --git a/pybricks/common/pb_type_speaker.c b/pybricks/common/pb_type_speaker.c index 7f5645373..650d4c53b 100644 --- a/pybricks/common/pb_type_speaker.c +++ b/pybricks/common/pb_type_speaker.c @@ -113,8 +113,7 @@ static mp_obj_t pb_type_Speaker_beep(size_t n_args, const mp_obj_t *pos_args, mp .close = pb_type_Speaker_close, }; // New operation always wins; ongoing sound awaitable is cancelled. - pb_type_async_schedule_cancel(self->iter); - return pb_type_async_wait_or_await(&config, &self->iter); + return pb_type_async_wait_or_await(&config, &self->iter, true); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Speaker_beep_obj, 1, pb_type_Speaker_beep); @@ -325,8 +324,7 @@ static mp_obj_t pb_type_Speaker_play_notes(size_t n_args, const mp_obj_t *pos_ar .close = pb_type_Speaker_close, }; // New operation always wins; ongoing sound awaitable is cancelled. - pb_type_async_schedule_cancel(self->iter); - return pb_type_async_wait_or_await(&config, &self->iter); + return pb_type_async_wait_or_await(&config, &self->iter, true); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_Speaker_play_notes_obj, 1, pb_type_Speaker_play_notes); diff --git a/pybricks/iodevices/pb_type_i2c_device.c b/pybricks/iodevices/pb_type_i2c_device.c index 44b6d4ee6..f3cf49093 100644 --- a/pybricks/iodevices/pb_type_i2c_device.c +++ b/pybricks/iodevices/pb_type_i2c_device.c @@ -188,8 +188,7 @@ mp_obj_t pb_type_i2c_device_start_operation(mp_obj_t i2c_device_obj, const uint8 .return_map = return_map ? pb_type_i2c_device_return_generic : NULL, }; // New operation always wins; ongoing sound awaitable is cancelled. - pb_type_async_schedule_cancel(device->iter); - return pb_type_async_wait_or_await(&config, &device->iter); + return pb_type_async_wait_or_await(&config, &device->iter, true); } /** diff --git a/pybricks/iodevices/pb_type_uart_device.c b/pybricks/iodevices/pb_type_uart_device.c index b2a1af3c7..7329c5c0a 100644 --- a/pybricks/iodevices/pb_type_uart_device.c +++ b/pybricks/iodevices/pb_type_uart_device.c @@ -103,8 +103,7 @@ static mp_obj_t pb_type_uart_device_write(size_t n_args, const mp_obj_t *pos_arg .parent_obj = MP_OBJ_FROM_PTR(self), .return_map = pb_type_uart_device_write_return_map, }; - pb_type_async_schedule_cancel(self->write_iter); - return pb_type_async_wait_or_await(&config, &self->write_iter); + return pb_type_async_wait_or_await(&config, &self->write_iter, true); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_uart_device_write_obj, 1, pb_type_uart_device_write); @@ -146,8 +145,7 @@ static mp_obj_t pb_type_uart_device_read(size_t n_args, const mp_obj_t *pos_args .parent_obj = MP_OBJ_FROM_PTR(self), .return_map = pb_type_uart_device_read_return_map, }; - pb_type_async_schedule_cancel(self->read_iter); - return pb_type_async_wait_or_await(&config, &self->read_iter); + return pb_type_async_wait_or_await(&config, &self->read_iter, true); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_type_uart_device_read_obj, 1, pb_type_uart_device_read); diff --git a/pybricks/robotics/pb_type_drivebase.c b/pybricks/robotics/pb_type_drivebase.c index 7076cee84..783837e7f 100644 --- a/pybricks/robotics/pb_type_drivebase.c +++ b/pybricks/robotics/pb_type_drivebase.c @@ -100,7 +100,7 @@ static mp_obj_t pb_type_DriveBase_stop(mp_obj_t self_in) { // Cancel awaitables. pb_type_DriveBase_obj_t *self = MP_OBJ_TO_PTR(self_in); - pb_type_async_schedule_cancel(self->last_awaitable); + pb_type_async_schedule_stop_iteration(self->last_awaitable); // Stop hardware. pb_assert(pbio_drivebase_stop(self->db, PBIO_CONTROL_ON_COMPLETION_COAST)); @@ -119,8 +119,7 @@ static mp_obj_t await_or_wait(pb_type_DriveBase_obj_t *self) { .close = pb_type_DriveBase_stop, }; // New operation always wins; ongoing awaitable motion is cancelled. - pb_type_async_schedule_cancel(self->last_awaitable); - return pb_type_async_wait_or_await(&config, &self->last_awaitable); + return pb_type_async_wait_or_await(&config, &self->last_awaitable, true); } // pybricks.robotics.DriveBase.straight @@ -237,7 +236,7 @@ static mp_obj_t pb_type_DriveBase_drive(size_t n_args, const mp_obj_t *pos_args, mp_int_t turn_rate = pb_obj_get_int(turn_rate_in); // Cancel awaitables but not hardware. Drive forever will handle this. - pb_type_async_schedule_cancel(self->last_awaitable); + pb_type_async_schedule_stop_iteration(self->last_awaitable); pb_assert(pbio_drivebase_drive_forever(self->db, speed, turn_rate)); return mp_const_none; @@ -249,7 +248,7 @@ static mp_obj_t pb_type_DriveBase_brake(mp_obj_t self_in) { // Cancel awaitables. pb_type_DriveBase_obj_t *self = MP_OBJ_TO_PTR(self_in); - pb_type_async_schedule_cancel(self->last_awaitable); + pb_type_async_schedule_stop_iteration(self->last_awaitable); // Stop hardware. pb_assert(pbio_drivebase_stop(self->db, PBIO_CONTROL_ON_COMPLETION_BRAKE)); diff --git a/pybricks/tools/pb_module_tools.c b/pybricks/tools/pb_module_tools.c index a6cad31a1..bc7d53313 100644 --- a/pybricks/tools/pb_module_tools.c +++ b/pybricks/tools/pb_module_tools.c @@ -96,7 +96,7 @@ static mp_obj_t pb_module_tools_wait(size_t n_args, const mp_obj_t *pos_args, mp .state = pbdrv_clock_get_ms() + (uint32_t)time, }; - return pb_type_async_wait_or_await(&config, &reuse); + return pb_type_async_wait_or_await(&config, &reuse, false); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_wait_obj, 0, pb_module_tools_wait); @@ -158,7 +158,7 @@ mp_obj_t pb_module_tools_pbio_task_wait_or_await(pbio_task_t *task) { // REVISIT: pbio tasks will be deprecated. Instead, protothreads can now // be safely awaited. - return pb_type_async_wait_or_await(&config, NULL); + return pb_type_async_wait_or_await(&config, NULL, false); } /** diff --git a/pybricks/tools/pb_type_async.c b/pybricks/tools/pb_type_async.c index 24005950f..087d0efb2 100644 --- a/pybricks/tools/pb_type_async.c +++ b/pybricks/tools/pb_type_async.c @@ -12,14 +12,19 @@ #include /** - * Cancels the iterable so it will stop awaiting. + * Makes the iterable exhaust the next time it is iterated. * * This will not call close(). Safe to call even if iter is NULL or if it is * already complete. * + * This is useful when all we need is for the ongoing awaitable to stop, with + * the newly created iterable taking care of the hardware. For example, if the + * new operation takes over the speaker, the old one only has to stop iterating, + * not stop the speaker as it would do with close(). + * * @param [in] iter The awaitable object. */ -void pb_type_async_schedule_cancel(pb_type_async_t *iter) { +void pb_type_async_schedule_stop_iteration(pb_type_async_t *iter) { if (!iter || iter->parent_obj == MP_OBJ_NULL) { // Don't schedule if already complete. return; @@ -58,7 +63,7 @@ static mp_obj_t pb_type_async_iternext(mp_obj_t iter_in) { // Special case without iterator means yield exactly once and then complete. if (!iter->iter_once) { - pb_type_async_schedule_cancel(iter); + pb_type_async_schedule_stop_iteration(iter); return mp_const_none; } @@ -98,22 +103,35 @@ 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] prev Candidate iterable object that might be re-used. + * @param [in] config Configuration of the operation + * @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. */ -mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config, pb_type_async_t **prev) { +mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config, pb_type_async_t **prev, bool stop_prev) { config->base.type = &pb_type_async; // Return allocated awaitable if runloop active. if (pb_module_tools_run_loop_is_active()) { + + // Optionally schedule ongoing awaitable to stop (next time) if busy. + if (prev && stop_prev) { + pb_type_async_schedule_stop_iteration(*prev); + } + // Re-use existing awaitable if exists and is free, otherwise allocate // another one. This allows many resources with one concurrent physical // operation like a motor to operate without re-allocation. pb_type_async_t *iter = (prev && *prev && (*prev)->parent_obj == MP_OBJ_NULL) ? *prev : (pb_type_async_t *)m_malloc(sizeof(pb_type_async_t)); + + // Copy the confuration to the object on heap so it lives on. *iter = *config; + + // Attaches newly defined awaitable (or no-op if reused) to the parent + // object. The object that was here before is detached, so we no longer + // prevent it from being garbage collected. if (prev) { *prev = iter; } diff --git a/pybricks/tools/pb_type_async.h b/pybricks/tools/pb_type_async.h index 9effa1745..1ec26ca2c 100644 --- a/pybricks/tools/pb_type_async.h +++ b/pybricks/tools/pb_type_async.h @@ -40,7 +40,7 @@ typedef mp_obj_t (*pb_type_async_return_map_t)(mp_obj_t parent_obj); */ typedef pbio_error_t (*pb_type_async_iterate_once_t)(pbio_os_state_t *state, mp_obj_t parent_obj); -// Object representing the iterable that is returned by an awaitable operation. +/** Object representing the iterable that is returned by an awaitable operation. */ typedef struct { mp_obj_base_t base; /** @@ -49,8 +49,7 @@ typedef struct { * * Special values: * MP_OBJ_NULL: This iterable has been fully exhausted and can be reused. - * MP_OBJ_SENTINEL: This iterable is cancelled and will exhaust - * when it is iterated again. + * MP_OBJ_SENTINEL: This iterable is will raise StopIteration when it is iterated again. */ mp_obj_t parent_obj; /** @@ -74,8 +73,8 @@ typedef struct { pbio_os_state_t state; } pb_type_async_t; -mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config, pb_type_async_t **prev); +mp_obj_t pb_type_async_wait_or_await(pb_type_async_t *config, pb_type_async_t **prev, bool stop_prev); -void pb_type_async_schedule_cancel(pb_type_async_t *iter); +void pb_type_async_schedule_stop_iteration(pb_type_async_t *iter); #endif // PYBRICKS_INCLUDED_ASYNC_H From f14f5ab6bc4372d6c8744540768957a15348c236 Mon Sep 17 00:00:00 2001 From: Laurens Valk Date: Wed, 17 Sep 2025 20:48:25 +0200 Subject: [PATCH 17/17] tests/virtualhub/multitasking: Add tests for new async type. --- tests/virtualhub/multitasking/motor_cancel.py | 112 ++++++++++++++++++ .../multitasking/motor_cancel.py.exp | 3 + tests/virtualhub/multitasking/wait.py | 53 +++++++++ tests/virtualhub/multitasking/wait.py.exp | 7 ++ 4 files changed, 175 insertions(+) create mode 100644 tests/virtualhub/multitasking/motor_cancel.py create mode 100644 tests/virtualhub/multitasking/motor_cancel.py.exp create mode 100644 tests/virtualhub/multitasking/wait.py create mode 100644 tests/virtualhub/multitasking/wait.py.exp diff --git a/tests/virtualhub/multitasking/motor_cancel.py b/tests/virtualhub/multitasking/motor_cancel.py new file mode 100644 index 000000000..43af3578c --- /dev/null +++ b/tests/virtualhub/multitasking/motor_cancel.py @@ -0,0 +1,112 @@ +from pybricks.pupdevices import Motor +from pybricks.parameters import Port, Direction +from pybricks.robotics import DriveBase +from pybricks.tools import wait, multitask, run_task + +ENDPOINT = 142 +SPEED = 500 + + +def is_close(motor, target): + return abs(motor.angle() - target) < 5 + + +# Spins freely. +motor = Motor(Port.A) + +# Physically blocked in two directions. +lever = Motor(Port.C) + + +def reset(): + for m in (motor, lever): + m.run_target(SPEED, 0) + assert is_close(m, 0) + + +reset() + +# Should block until endpoint. +assert lever.run_until_stalled(SPEED) == ENDPOINT +assert lever.angle() == ENDPOINT + +# Should block until close to target +lever.run_target(SPEED, 0) +assert is_close(lever, 0) + + +async def stall(): + # Should return None for most movements. + ret = await lever.run_target(SPEED, -90) + assert is_close(lever, -90) + assert ret is None + + # Should return value at end of stall awaitable. + stall_angle = await lever.run_until_stalled(SPEED) + assert stall_angle == ENDPOINT + + +run_task(stall()) + + +def is_coasting(motor): + return motor.load() == 0 + + +# Confirm that stop() coasts the motor. +motor.run_angle(SPEED, 360) +assert not is_coasting(motor) +motor.stop() +assert is_coasting(motor) +reset() + + +async def par1(expect_interruption=False): + for i in range(4): + await motor.run_angle(SPEED, 90) + if expect_interruption: + raise RuntimeError("Expected interruption, so shold never see this.") + + +async def par2(): + await wait(100) + + +# Let the motor run on its own. +reset() +run_task(par1()) +assert not is_coasting(motor) +assert is_close(motor, 360) + +# Let the motor run in parallel to a task that does not affect it. +reset() +run_task(multitask(par1(), par2())) +assert not is_coasting(motor) +assert is_close(motor, 360) + +# Let the motor run in parallel to a short task as a race. This should cancel +# the motor task early and coast it. +reset() +run_task(multitask(par1(True), par2(), race=True)) +assert is_coasting(motor) +assert not is_close(motor, 360) + + +reset() + + +async def par3(): + await motor.run_target(SPEED, 36000) + # We should never make it, but stop waiting and proceed instead. + assert not is_close(motor, 36000) + print("motor movement awaiting was cancelled") + + +async def par4(): + await wait(500) + print("Going to take over the motor.") + await motor.run_target(SPEED, 90) + print("Finished turning after take over.") + + +run_task(multitask(par3(), par4())) diff --git a/tests/virtualhub/multitasking/motor_cancel.py.exp b/tests/virtualhub/multitasking/motor_cancel.py.exp new file mode 100644 index 000000000..2dcdcbf02 --- /dev/null +++ b/tests/virtualhub/multitasking/motor_cancel.py.exp @@ -0,0 +1,3 @@ +Going to take over the motor. +motor movement awaiting was cancelled +Finished turning after take over. diff --git a/tests/virtualhub/multitasking/wait.py b/tests/virtualhub/multitasking/wait.py new file mode 100644 index 000000000..0a14eeb95 --- /dev/null +++ b/tests/virtualhub/multitasking/wait.py @@ -0,0 +1,53 @@ +from pybricks.tools import multitask, run_task, wait, StopWatch + +watch = StopWatch() +DELAY = 100 + +# Should block +watch.reset() +wait(DELAY) +assert watch.time() == DELAY + + +async def one_wait(): + # Forgot await, so should not wait. + watch.reset() + wait(DELAY) + assert watch.time() == 0 + + # Should await. + watch.reset() + await wait(DELAY) + assert watch.time() == DELAY + + # Await object + watch.reset() + it = wait(DELAY) + await it + assert watch.time() == DELAY + + +run_task(one_wait()) + + +async def main1(): + print("started main1") + await wait(DELAY) + print("completed main1") + + +async def main2(): + print("started main2") + await wait(DELAY * 2) + print("completed main2") + + +# Should get all outputs +watch.reset() +run_task(multitask(main1(), main2())) +assert watch.time() == DELAY * 2 + +# Only one task completes. +watch.reset() +run_task(multitask(main1(), main2(), race=True)) +assert watch.time() == DELAY diff --git a/tests/virtualhub/multitasking/wait.py.exp b/tests/virtualhub/multitasking/wait.py.exp new file mode 100644 index 000000000..47248690c --- /dev/null +++ b/tests/virtualhub/multitasking/wait.py.exp @@ -0,0 +1,7 @@ +started main1 +started main2 +completed main1 +completed main2 +started main1 +started main2 +completed main1