diff --git a/bricks/_common/sources.mk b/bricks/_common/sources.mk index b8c9b86f4..cd6bf5d32 100644 --- a/bricks/_common/sources.mk +++ b/bricks/_common/sources.mk @@ -98,7 +98,7 @@ 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 \ tools/pb_type_task.c \ 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.h b/pybricks/common.h index 46c4d580d..59d7b177a 100644 --- a/pybricks/common.h +++ b/pybricks/common.h @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include void pb_package_pybricks_init(bool import_all); @@ -104,10 +104,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_device.c b/pybricks/common/pb_type_device.c index 6fdb108a1..05d2de468 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, false); } /** @@ -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, false); } 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; diff --git a/pybricks/common/pb_type_lightmatrix.c b/pybricks/common/pb_type_lightmatrix.c index 73f5ad97c..e46df968f 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,62 @@ 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. + 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); @@ -348,7 +366,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); } diff --git a/pybricks/common/pb_type_motor.c b/pybricks/common/pb_type_motor.c index d0e59f5f2..84817778e 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_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_awaitable_update_all(self->device_base.awaitables, PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + 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,36 +277,43 @@ 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_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); -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); +// 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, + }; + // New operation always wins; ongoing awaitable motor motion is cancelled. + return pb_type_async_wait_or_await(&config, &self->last_awaitable, true); } // pybricks.common.Motor.run_time @@ -331,21 +338,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 +366,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 +391,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 +416,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 +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_awaitable_update_all(self->device_base.awaitables, PB_TYPE_AWAITABLE_OPT_CANCEL_ALL); + 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 fcefea41f..650d4c53b 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,26 @@ 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. + 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); -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 +260,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 +278,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 +317,14 @@ 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. + 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/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..f3cf49093 100644 --- a/pybricks/iodevices/pb_type_i2c_device.c +++ b/pybricks/iodevices/pb_type_i2c_device.c @@ -15,14 +15,31 @@ #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. + * + * 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; + /** + * 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 +48,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 +87,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 +106,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, // Not associated with any particular sensor instance. + 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 +134,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 +173,22 @@ 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. + return pb_type_async_wait_or_await(&config, &device->iter, true); } /** @@ -207,25 +197,43 @@ 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); } } +/** + * 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, @@ -235,7 +243,19 @@ 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); + // 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), + 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); diff --git a/pybricks/iodevices/pb_type_uart_device.c b/pybricks/iodevices/pb_type_uart_device.c index 0ea7bee5b..7329c5c0a 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,15 @@ 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, + }; + 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); @@ -127,27 +116,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 +140,12 @@ 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, + }; + 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/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/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 diff --git a/pybricks/robotics/pb_type_drivebase.c b/pybricks/robotics/pb_type_drivebase.c index 4b4a91dd9..783837e7f 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,34 @@ 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_stop_iteration(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. + return pb_type_async_wait_or_await(&config, &self->last_awaitable, true); } // pybricks.robotics.DriveBase.straight @@ -229,33 +236,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_stop_iteration(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_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 87797a687..bc7d53313 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); - - 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); + // 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_async_wait_or_await(&config, &reuse, false); } static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_wait_obj, 0, pb_module_tools_wait); @@ -127,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, false); } /** @@ -249,8 +253,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); - MP_STATE_PORT(pbio_task_awaitables) = mp_obj_new_list(0, NULL); + memset(waits, 0, sizeof(waits)); run_loop_is_active = false; } 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 diff --git a/pybricks/tools/pb_type_async.c b/pybricks/tools/pb_type_async.c new file mode 100644 index 000000000..087d0efb2 --- /dev/null +++ b/pybricks/tools/pb_type_async.c @@ -0,0 +1,149 @@ +// 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 + +/** + * 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_stop_iteration(pb_type_async_t *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 + // know it was exhausted, and it would await on the renewed operation. + iter->parent_obj = MP_OBJ_SENTINEL; +} + +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); + } + // 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); + +static mp_obj_t pb_type_async_iternext(mp_obj_t iter_in) { + pb_type_async_t *iter = MP_OBJ_TO_PTR(iter_in); + + // 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_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_stop_iteration(iter); + return mp_const_none; + } + + // 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); + + // This causes the stop iteration to provide the return value. + if (iter->return_map) { + mp_make_stop_iteration(iter->return_map(iter->parent_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[] = { + { 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 + * @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, 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; + } + + 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..1ec26ca2c --- /dev/null +++ b/pybricks/tools/pb_type_async.h @@ -0,0 +1,80 @@ +// 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 been fully exhausted and can be reused. + * MP_OBJ_SENTINEL: This iterable is will raise StopIteration 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; + /** + * 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, pb_type_async_t **prev, bool stop_prev); + +void pb_type_async_schedule_stop_iteration(pb_type_async_t *iter); + +#endif // PYBRICKS_INCLUDED_ASYNC_H 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 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