• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pybricks / pybricks-micropython / 8111159377

01 Mar 2024 12:12PM UTC coverage: 55.498% (+0.003%) from 55.495%
8111159377

push

github

laurensvalk
pybricks.tools.run_task: Add test for is running.

This lets Python users write libraries that can work properly in either async or synchronous mode.

Fixes https://github.com/pybricks/support/issues/1499

2 of 4 new or added lines in 1 file covered. (50.0%)

6 existing lines in 1 file now uncovered.

3669 of 6611 relevant lines covered (55.5%)

20391414.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.74
/pybricks/tools/pb_module_tools.c
1
// SPDX-License-Identifier: MIT
2
// Copyright (c) 2018-2023 The Pybricks Authors
3

4
#include "py/mpconfig.h"
5

6
#if PYBRICKS_PY_TOOLS
7

8
#include "py/builtin.h"
9
#include "py/gc.h"
10
#include "py/mphal.h"
11
#include "py/objmodule.h"
12
#include "py/runtime.h"
13
#include "py/stream.h"
14

15
#include <pbio/int_math.h>
16
#include <pbio/task.h>
17
#include <pbsys/light.h>
18
#include <pbsys/program_stop.h>
19

20
#include <pybricks/parameters.h>
21
#include <pybricks/common.h>
22
#include <pybricks/tools.h>
23
#include <pybricks/tools/pb_type_matrix.h>
24

25
#include <pybricks/util_mp/pb_kwarg_helper.h>
26
#include <pybricks/util_mp/pb_obj_helper.h>
27
#include <pybricks/util_pb/pb_error.h>
28

29

30
// Global state of the run loop for async user programs. Gets set when run_task
31
// is called and cleared when it completes
32
STATIC bool run_loop_is_active;
33

34
bool pb_module_tools_run_loop_is_active(void) {
288✔
35
    return run_loop_is_active;
288✔
36
}
37

38
void pb_module_tools_assert_blocking(void) {
9✔
39
    if (run_loop_is_active) {
9✔
40
        mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("This can only be called before multitasking starts."));
×
41
    }
42
}
9✔
43

44
// The awaitables for the wait() function have no object associated with
45
// it (unlike e.g. a motor), so we make a starting point here. These never
46
// have to cancel each other so shouldn't need to be in a list, but this lets
47
// us share the same code with other awaitables. It also minimizes allocation.
48
MP_REGISTER_ROOT_POINTER(mp_obj_t wait_awaitables);
49

50
STATIC bool pb_module_tools_wait_test_completion(mp_obj_t obj, uint32_t end_time) {
402✔
51
    return mp_hal_ticks_ms() - end_time < UINT32_MAX / 2;
402✔
52
}
53

54
STATIC mp_obj_t pb_module_tools_wait(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
235✔
55
    PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args,
235✔
56
        PB_ARG_REQUIRED(time));
235✔
57

58
    mp_int_t time = pb_obj_get_int(time_in);
235✔
59

60
    // outside run loop, do blocking wait. This would be handled below as well,
61
    // but for this very simple call we'd rather avoid the overhead.
62
    if (!pb_module_tools_run_loop_is_active()) {
235✔
63
        if (time > 0) {
233✔
64
            mp_hal_delay_ms(time);
233✔
65
        }
66
        return mp_const_none;
233✔
67
    }
68

69
    // Require that duration is nonnegative small int. This makes it cheaper to
70
    // test completion state in iteration loop.
71
    time = pbio_int_math_bind(time, 0, INT32_MAX >> 2);
2✔
72

73
    return pb_type_awaitable_await_or_wait(
4✔
74
        NULL, // wait functions are not associated with an object
75
        MP_STATE_PORT(wait_awaitables),
76
        mp_hal_ticks_ms() + (time < 0 ? 0 : time),
2✔
77
        pb_module_tools_wait_test_completion,
78
        pb_type_awaitable_return_none,
79
        pb_type_awaitable_cancel_none,
80
        PB_TYPE_AWAITABLE_OPT_NONE);
81
}
82
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_wait_obj, 0, pb_module_tools_wait);
83

84
/**
85
 * Waits for a task to complete.
86
 *
87
 * If an exception is raised while waiting, then the task is canceled.
88
 *
89
 * @param [in]  task    The task
90
 * @param [in]  timeout The timeout in milliseconds or -1 to wait forever.
91
 */
92
void pb_module_tools_pbio_task_do_blocking(pbio_task_t *task, mp_int_t timeout) {
×
93

94
    pb_module_tools_assert_blocking();
×
95

96
    nlr_buf_t nlr;
×
97

98
    if (nlr_push(&nlr) == 0) {
×
99
        mp_uint_t start = mp_hal_ticks_ms();
×
100

101
        while (timeout < 0 || mp_hal_ticks_ms() - start < (mp_uint_t)timeout) {
×
102
            MICROPY_EVENT_POLL_HOOK
×
103

104
            if (task->status != PBIO_ERROR_AGAIN) {
×
105
                nlr_pop();
×
106
                pb_assert(task->status);
×
107
                return;
×
108
            }
109
        }
110

111
        mp_raise_OSError(MP_ETIMEDOUT);
×
112
        MP_UNREACHABLE
113
    } else {
114
        pbio_task_cancel(task);
×
115

116
        while (task->status == PBIO_ERROR_AGAIN) {
×
117
            MICROPY_VM_HOOK_LOOP
×
118
        }
119

120
        nlr_jump(nlr.ret_val);
×
121
    }
122
}
123

124
// The awaitables associated with pbio tasks can originate from different
125
// objects. At the moment, they are only associated with Bluetooth tasks, and
126
// they cannot run at the same time. So we keep a single list of awaitables
127
// here instead of with each Bluetooth-related MicroPython object.
128
MP_REGISTER_ROOT_POINTER(mp_obj_t pbio_task_awaitables);
129

130
STATIC bool pb_module_tools_pbio_task_test_completion(mp_obj_t obj, uint32_t end_time) {
×
131
    pbio_task_t *task = MP_OBJ_TO_PTR(obj);
×
132

133
    // Keep going if not done yet.
134
    if (task->status == PBIO_ERROR_AGAIN) {
×
135
        return false;
136
    }
137

138
    // If done, make sure it was successful.
139
    pb_assert(task->status);
×
140
    return true;
×
141
}
142

143
mp_obj_t pb_module_tools_pbio_task_wait_or_await(pbio_task_t *task) {
×
144
    return pb_type_awaitable_await_or_wait(
×
145
        MP_OBJ_FROM_PTR(task),
146
        MP_STATE_PORT(pbio_task_awaitables),
147
        pb_type_awaitable_end_time_none,
148
        pb_module_tools_pbio_task_test_completion,
149
        pb_type_awaitable_return_none,
150
        pb_type_awaitable_cancel_none,
151
        PB_TYPE_AWAITABLE_OPT_RAISE_ON_BUSY);
152
}
153

154
/**
155
 * Reads one byte from stdin without blocking.
156
 *
157
 * @returns The integer value of the byte read or @c None if no data is available.
158
 */
159
STATIC mp_obj_t pb_module_tools_read_input_byte(void) {
×
160
    if (!(mp_hal_stdio_poll(MP_STREAM_POLL_RD) & MP_STREAM_POLL_RD)) {
×
161
        // No bytes available.
162
        return mp_const_none;
163
    }
164

165
    // REVISIT: In theory, this should not block if mp_hal_stdio_poll() and
166
    // mp_hal_stdin_rx_chr() are implemented correctly and nothing happens
167
    // in a thread/interrupt/kernel that changes the state.
168
    return MP_OBJ_NEW_SMALL_INT(mp_hal_stdin_rx_chr());
×
169
}
170
STATIC MP_DEFINE_CONST_FUN_OBJ_0(pb_module_tools_read_input_byte_obj, pb_module_tools_read_input_byte);
171

172
STATIC mp_obj_t pb_module_tools_run_task(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
2✔
173
    PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args,
2✔
174
        PB_ARG_REQUIRED(task),
175
        PB_ARG_DEFAULT_INT(loop_time, 10));
2✔
176

177
    // Without args, this function is used to test if the run loop is active.
178
    if (n_args == 0) {
2✔
NEW
179
        return mp_obj_new_bool(run_loop_is_active);
×
180
    }
181

182
    // Can only run one loop at a time.
183
    if (run_loop_is_active) {
2✔
NEW
184
        mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Run loop already active."));
×
185
    }
186

187
    run_loop_is_active = true;
2✔
188

189
    uint32_t start_time = mp_hal_ticks_ms();
2✔
190
    uint32_t loop_time = pb_obj_get_positive_int(loop_time_in);
2✔
191

192
    mp_obj_iter_buf_t iter_buf;
2✔
193
    mp_obj_t iterable = mp_getiter(task_in, &iter_buf);
2✔
194

195
    nlr_buf_t nlr;
2✔
196
    if (nlr_push(&nlr) == 0) {
2✔
197

198
        while (mp_iternext(iterable) != MP_OBJ_STOP_ITERATION) {
1,051✔
199

200
            gc_collect();
1,049✔
201

202
            if (loop_time == 0) {
1,049✔
203
                continue;
5✔
204
            }
205

206
            uint32_t elapsed = mp_hal_ticks_ms() - start_time;
1,044✔
207
            if (elapsed < loop_time) {
1,044✔
208
                mp_hal_delay_ms(loop_time - elapsed);
1,044✔
209
            }
210
            start_time += loop_time;
1,044✔
211
        }
212

213
        nlr_pop();
2✔
214
        run_loop_is_active = false;
2✔
215
    } else {
216
        run_loop_is_active = false;
×
217
        nlr_jump(nlr.ret_val);
×
218
    }
219
    return mp_const_none;
2✔
220
}
221
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_run_task_obj, 1, pb_module_tools_run_task);
222

223
// Reset global awaitable state when user program starts.
224
void pb_module_tools_init(void) {
22✔
225
    MP_STATE_PORT(wait_awaitables) = mp_obj_new_list(0, NULL);
22✔
226
    MP_STATE_PORT(pbio_task_awaitables) = mp_obj_new_list(0, NULL);
22✔
227
    run_loop_is_active = false;
22✔
228
}
22✔
229

230
#if PYBRICKS_PY_TOOLS_HUB_MENU
231

232
STATIC void pb_module_tools_hub_menu_display_symbol(mp_obj_t symbol) {
233
    if (mp_obj_is_str(symbol)) {
234
        pb_type_LightMatrix_display_char(pbsys_hub_light_matrix, symbol);
235
    } else {
236
        pb_type_LightMatrix_display_number(pbsys_hub_light_matrix, symbol);
237
    }
238
}
239

240
/**
241
 * Waits for a button press or release.
242
 *
243
 * @param [in]  press   Choose @c true to wait for press or @c false to wait for release.
244
 * @returns             When waiting for pressed, it returns the button that was pressed, otherwise returns 0.
245
 */
246
STATIC pbio_button_flags_t pb_module_tools_hub_menu_wait_for_press(bool press) {
247

248
    // This function should only be used in a blocking context.
249
    pb_module_tools_assert_blocking();
250

251
    pbio_error_t err;
252
    pbio_button_flags_t btn;
253
    while ((err = pbio_button_is_pressed(&btn)) == PBIO_SUCCESS && ((bool)btn) == !press) {
254
        MICROPY_EVENT_POLL_HOOK;
255
    }
256
    pb_assert(err);
257
    return btn;
258
}
259

260
/**
261
 * Displays a menu on the hub display and allows the user to pick a symbol
262
 * using the buttons.
263
 *
264
 * @param [in]  n_args  The number of args.
265
 * @param [in]  args    The args passed in Python code (the menu entries).
266
 */
267
STATIC mp_obj_t pb_module_tools_hub_menu(size_t n_args, const mp_obj_t *args) {
268

269
    // Validate arguments by displaying all of them, ending with the first.
270
    // This ensures we fail right away instead of midway through the menu. It
271
    // happens so fast that there isn't a time penalty for this.
272
    for (int i = n_args - 1; i >= 0; i--) {
273
        pb_module_tools_hub_menu_display_symbol(args[i]);
274
    }
275

276
    // Disable stop button and cache original setting to restore later.
277
    pbio_button_flags_t stop_button = pbsys_program_stop_get_buttons();
278
    pbsys_program_stop_set_buttons(0);
279

280
    nlr_buf_t nlr;
281
    if (nlr_push(&nlr) == 0) {
282

283
        size_t selection = 0;
284

285
        while (true) {
286
            pb_module_tools_hub_menu_wait_for_press(false);
287
            pbio_button_flags_t btn = pb_module_tools_hub_menu_wait_for_press(true);
288

289
            // Selection made, exit.
290
            if (btn & PBIO_BUTTON_CENTER) {
291
                break;
292
            }
293

294
            // Increment/decrement selection for left/right buttons.
295
            if (btn & PBIO_BUTTON_RIGHT) {
296
                selection = (selection + 1) % n_args;
297
            } else if (btn & PBIO_BUTTON_LEFT) {
298
                selection = selection == 0 ? n_args - 1 : selection - 1;
299
            }
300

301
            // Display current selection.
302
            pb_module_tools_hub_menu_display_symbol(args[selection]);
303
        }
304

305
        // Wait for release before returning, just like starting a normal program.
306
        pb_module_tools_hub_menu_wait_for_press(false);
307

308
        // Restore stop button setting prior to starting menu.
309
        pbsys_program_stop_set_buttons(stop_button);
310

311
        // Complete and return selected object.
312
        nlr_pop();
313
        return args[selection];
314
    } else {
315
        pbsys_program_stop_set_buttons(stop_button);
316
        nlr_jump(nlr.ret_val);
317
        return mp_const_none;
318
    }
319
}
320
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR(pb_module_tools_hub_menu_obj, 2, pb_module_tools_hub_menu);
321

322
#endif // PYBRICKS_PY_TOOLS_HUB_MENU
323

324
STATIC const mp_rom_map_elem_t tools_globals_table[] = {
325
    { MP_ROM_QSTR(MP_QSTR___name__),    MP_ROM_QSTR(MP_QSTR_tools)                    },
326
    { MP_ROM_QSTR(MP_QSTR_wait),        MP_ROM_PTR(&pb_module_tools_wait_obj)         },
327
    { MP_ROM_QSTR(MP_QSTR_read_input_byte), MP_ROM_PTR(&pb_module_tools_read_input_byte_obj) },
328
    #if PYBRICKS_PY_TOOLS_HUB_MENU
329
    { MP_ROM_QSTR(MP_QSTR_hub_menu),    MP_ROM_PTR(&pb_module_tools_hub_menu_obj)     },
330
    #endif // PYBRICKS_PY_TOOLS_HUB_MENU
331
    { MP_ROM_QSTR(MP_QSTR_run_task),    MP_ROM_PTR(&pb_module_tools_run_task_obj)     },
332
    { MP_ROM_QSTR(MP_QSTR_StopWatch),   MP_ROM_PTR(&pb_type_StopWatch)                },
333
    { MP_ROM_QSTR(MP_QSTR_multitask),   MP_ROM_PTR(&pb_type_Task)                     },
334
    #if MICROPY_PY_BUILTINS_FLOAT
335
    { MP_ROM_QSTR(MP_QSTR_Matrix),      MP_ROM_PTR(&pb_type_Matrix)           },
336
    { MP_ROM_QSTR(MP_QSTR_vector),      MP_ROM_PTR(&pb_geometry_vector_obj)   },
337
    { MP_ROM_QSTR(MP_QSTR_cross),       MP_ROM_PTR(&pb_type_matrix_cross_obj) },
338
    // backwards compatibility for pybricks.geometry.Axis
339
    { MP_ROM_QSTR(MP_QSTR_Axis),        MP_ROM_PTR(&pb_enum_type_Axis) },
340
    #endif // MICROPY_PY_BUILTINS_FLOAT
341
};
342
STATIC MP_DEFINE_CONST_DICT(pb_module_tools_globals, tools_globals_table);
343

344
const mp_obj_module_t pb_module_tools = {
345
    .base = { &mp_type_module },
346
    .globals = (mp_obj_dict_t *)&pb_module_tools_globals,
347
};
348

349
#if PYBRICKS_RUNS_ON_EV3DEV
350
// ev3dev extends the C module in Python
351
MP_REGISTER_MODULE(MP_QSTR__tools, pb_module_tools);
352
#else
353
MP_REGISTER_MODULE(MP_QSTR_pybricks_dot_tools, pb_module_tools);
354
#endif
355

356
// backwards compatibility for pybricks.geometry
357
#if MICROPY_PY_BUILTINS_FLOAT
358
MP_REGISTER_MODULE(MP_QSTR_pybricks_dot_geometry, pb_module_tools);
359
#endif // MICROPY_PY_BUILTINS_FLOAT
360

361
#endif // PYBRICKS_PY_TOOLS
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc