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

pybricks / pybricks-micropython / 6681021856

29 Oct 2023 03:28AM UTC coverage: 56.079% (+0.03%) from 56.053%
6681021856

push

github

dlech
pbio/task: don't run task from cancel function

Since efda5d02 ("drv/bluetooth_stm32_bluenrg: strict ordering of BLE
tasks"), it is no longer safe to call pbio_task_run_once() anywhere but
in the main contiki process loop of Bluetooth drivers.

This could break canceling some Bluetooth operations (causing them to
hang), but this will need to be fixed in a different way. A FIXME
comment with some suggestions is added to remind us to fix this.

3616 of 6448 relevant lines covered (56.08%)

20905520.69 hits per line

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

60.27
/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) {
237✔
35
    return run_loop_is_active;
237✔
36
}
37

38
void pb_module_tools_assert_blocking(void) {
7✔
39
    if (run_loop_is_active) {
7✔
40
        mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("This can only be called before multitasking starts."));
×
41
    }
42
}
7✔
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) {
207✔
55
    PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args,
207✔
56
        PB_ARG_REQUIRED(time));
207✔
57

58
    mp_int_t time = pb_obj_get_int(time_in);
207✔
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()) {
207✔
63
        if (time > 0) {
205✔
64
            mp_hal_delay_ms(time);
205✔
65
        }
66
        return mp_const_none;
205✔
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
    run_loop_is_active = true;
2✔
178

179
    uint32_t start_time = mp_hal_ticks_ms();
2✔
180
    uint32_t loop_time = pb_obj_get_positive_int(loop_time_in);
2✔
181

182
    mp_obj_iter_buf_t iter_buf;
2✔
183
    mp_obj_t iterable = mp_getiter(task_in, &iter_buf);
2✔
184

185
    nlr_buf_t nlr;
2✔
186
    if (nlr_push(&nlr) == 0) {
2✔
187

188
        while (mp_iternext(iterable) != MP_OBJ_STOP_ITERATION) {
1,053✔
189

190
            gc_collect();
1,051✔
191

192
            if (loop_time == 0) {
1,051✔
193
                continue;
5✔
194
            }
195

196
            uint32_t elapsed = mp_hal_ticks_ms() - start_time;
1,046✔
197
            if (elapsed < loop_time) {
1,046✔
198
                mp_hal_delay_ms(loop_time - elapsed);
1,046✔
199
            }
200
            start_time += loop_time;
1,046✔
201
        }
202

203
        nlr_pop();
2✔
204
        run_loop_is_active = false;
2✔
205
    } else {
206
        run_loop_is_active = false;
×
207
        nlr_jump(nlr.ret_val);
×
208
    }
209
    return mp_const_none;
2✔
210
}
211
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_run_task_obj, 1, pb_module_tools_run_task);
212

213
// Reset global awaitable state when user program starts.
214
void pb_module_tools_init(void) {
21✔
215
    MP_STATE_PORT(wait_awaitables) = mp_obj_new_list(0, NULL);
21✔
216
    MP_STATE_PORT(pbio_task_awaitables) = mp_obj_new_list(0, NULL);
21✔
217
    run_loop_is_active = false;
21✔
218
}
21✔
219

220
#if PYBRICKS_PY_TOOLS_HUB_MENU
221

222
STATIC void pb_module_tools_hub_menu_display_symbol(mp_obj_t symbol) {
223
    if (mp_obj_is_str(symbol)) {
224
        pb_type_LightMatrix_display_char(pbsys_hub_light_matrix, symbol);
225
    } else {
226
        pb_type_LightMatrix_display_number(pbsys_hub_light_matrix, symbol);
227
    }
228
}
229

230
/**
231
 * Waits for a button press or release.
232
 *
233
 * @param [in]  press   Choose @c true to wait for press or @c false to wait for release.
234
 * @returns             When waiting for pressed, it returns the button that was pressed, otherwise returns 0.
235
 */
236
STATIC pbio_button_flags_t pb_module_tools_hub_menu_wait_for_press(bool press) {
237

238
    // This function should only be used in a blocking context.
239
    pb_module_tools_assert_blocking();
240

241
    pbio_error_t err;
242
    pbio_button_flags_t btn;
243
    while ((err = pbio_button_is_pressed(&btn)) == PBIO_SUCCESS && ((bool)btn) == !press) {
244
        MICROPY_EVENT_POLL_HOOK;
245
    }
246
    pb_assert(err);
247
    return btn;
248
}
249

250
/**
251
 * Displays a menu on the hub display and allows the user to pick a symbol
252
 * using the buttons.
253
 *
254
 * @param [in]  n_args  The number of args.
255
 * @param [in]  args    The args passed in Python code (the menu entries).
256
 */
257
STATIC mp_obj_t pb_module_tools_hub_menu(size_t n_args, const mp_obj_t *args) {
258

259
    // Validate arguments by displaying all of them, ending with the first.
260
    // This ensures we fail right away instead of midway through the menu. It
261
    // happens so fast that there isn't a time penalty for this.
262
    for (int i = n_args - 1; i >= 0; i--) {
263
        pb_module_tools_hub_menu_display_symbol(args[i]);
264
    }
265

266
    // Disable stop button and cache original setting to restore later.
267
    pbio_button_flags_t stop_button = pbsys_program_stop_get_buttons();
268
    pbsys_program_stop_set_buttons(0);
269

270
    nlr_buf_t nlr;
271
    if (nlr_push(&nlr) == 0) {
272

273
        size_t selection = 0;
274

275
        while (true) {
276
            pb_module_tools_hub_menu_wait_for_press(false);
277
            pbio_button_flags_t btn = pb_module_tools_hub_menu_wait_for_press(true);
278

279
            // Selection made, exit.
280
            if (btn & PBIO_BUTTON_CENTER) {
281
                break;
282
            }
283

284
            // Increment/decrement selection for left/right buttons.
285
            if (btn & PBIO_BUTTON_RIGHT) {
286
                selection = (selection + 1) % n_args;
287
            } else if (btn & PBIO_BUTTON_LEFT) {
288
                selection = selection == 0 ? n_args - 1 : selection - 1;
289
            }
290

291
            // Display current selection.
292
            pb_module_tools_hub_menu_display_symbol(args[selection]);
293
        }
294

295
        // Wait for release before returning, just like starting a normal program.
296
        pb_module_tools_hub_menu_wait_for_press(false);
297

298
        // Restore stop button setting prior to starting menu.
299
        pbsys_program_stop_set_buttons(stop_button);
300

301
        // Complete and return selected object.
302
        nlr_pop();
303
        return args[selection];
304
    } else {
305
        pbsys_program_stop_set_buttons(stop_button);
306
        nlr_jump(nlr.ret_val);
307
        return mp_const_none;
308
    }
309
}
310
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR(pb_module_tools_hub_menu_obj, 2, pb_module_tools_hub_menu);
311

312
#endif // PYBRICKS_PY_TOOLS_HUB_MENU
313

314
STATIC const mp_rom_map_elem_t tools_globals_table[] = {
315
    { MP_ROM_QSTR(MP_QSTR___name__),    MP_ROM_QSTR(MP_QSTR_tools)                    },
316
    { MP_ROM_QSTR(MP_QSTR_wait),        MP_ROM_PTR(&pb_module_tools_wait_obj)         },
317
    { MP_ROM_QSTR(MP_QSTR_read_input_byte), MP_ROM_PTR(&pb_module_tools_read_input_byte_obj) },
318
    #if PYBRICKS_PY_TOOLS_HUB_MENU
319
    { MP_ROM_QSTR(MP_QSTR_hub_menu),    MP_ROM_PTR(&pb_module_tools_hub_menu_obj)     },
320
    #endif // PYBRICKS_PY_TOOLS_HUB_MENU
321
    { MP_ROM_QSTR(MP_QSTR_run_task),    MP_ROM_PTR(&pb_module_tools_run_task_obj)     },
322
    { MP_ROM_QSTR(MP_QSTR_StopWatch),   MP_ROM_PTR(&pb_type_StopWatch)                },
323
    { MP_ROM_QSTR(MP_QSTR_multitask),   MP_ROM_PTR(&pb_type_Task)                     },
324
    #if MICROPY_PY_BUILTINS_FLOAT
325
    { MP_ROM_QSTR(MP_QSTR_Matrix),      MP_ROM_PTR(&pb_type_Matrix)           },
326
    { MP_ROM_QSTR(MP_QSTR_vector),      MP_ROM_PTR(&pb_geometry_vector_obj)   },
327
    { MP_ROM_QSTR(MP_QSTR_cross),       MP_ROM_PTR(&pb_type_matrix_cross_obj) },
328
    // backwards compatibility for pybricks.geometry.Axis
329
    { MP_ROM_QSTR(MP_QSTR_Axis),        MP_ROM_PTR(&pb_enum_type_Axis) },
330
    #endif // MICROPY_PY_BUILTINS_FLOAT
331
};
332
STATIC MP_DEFINE_CONST_DICT(pb_module_tools_globals, tools_globals_table);
333

334
const mp_obj_module_t pb_module_tools = {
335
    .base = { &mp_type_module },
336
    .globals = (mp_obj_dict_t *)&pb_module_tools_globals,
337
};
338

339
#if PYBRICKS_RUNS_ON_EV3DEV
340
// ev3dev extends the C module in Python
341
MP_REGISTER_MODULE(MP_QSTR__tools, pb_module_tools);
342
#else
343
MP_REGISTER_MODULE(MP_QSTR_pybricks_dot_tools, pb_module_tools);
344
#endif
345

346
// backwards compatibility for pybricks.geometry
347
#if MICROPY_PY_BUILTINS_FLOAT
348
MP_REGISTER_MODULE(MP_QSTR_pybricks_dot_geometry, pb_module_tools);
349
#endif // MICROPY_PY_BUILTINS_FLOAT
350

351
#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