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

pybricks / pybricks-micropython / 10574114372

27 Aug 2024 08:02AM UTC coverage: 55.967% (-0.06%) from 56.025%
10574114372

Pull #263

github

laurensvalk
pybricks.tools.run_task: Drop run_loop arg.

This wasn't having the desired effect, and would cause loop round trips to take 10 ms for every await wait(1). Passing the argument is still allowed for backwards compatibility.
Pull Request #263: pybricks.tools.wait: Fix awaiting on wait(0).

6 of 12 new or added lines in 2 files covered. (50.0%)

4 existing lines in 1 file now uncovered.

3761 of 6720 relevant lines covered (55.97%)

20651104.03 hits per line

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

48.15
/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
#include <pbsys/status.h>
20

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

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

30

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

35
bool pb_module_tools_run_loop_is_active(void) {
294✔
36
    return run_loop_is_active;
294✔
37
}
38

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

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

51
static bool pb_module_tools_wait_test_completion(mp_obj_t obj, uint32_t end_time) {
11,171✔
52
    return mp_hal_ticks_ms() - end_time < UINT32_MAX / 2;
11,171✔
53
}
54

55
static mp_obj_t pb_module_tools_wait(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
241✔
56
    PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args,
241✔
57
        PB_ARG_REQUIRED(time));
241✔
58

59
    mp_int_t time = pb_obj_get_int(time_in);
241✔
60

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

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

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

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

95
    pb_module_tools_assert_blocking();
×
96

97
    nlr_buf_t nlr;
×
98

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

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

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

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

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

120
            // Stop waiting (and potentially blocking) in case of forced shutdown.
121
            if (pbsys_status_test(PBIO_PYBRICKS_STATUS_SHUTDOWN_REQUEST)) {
×
122
                break;
123
            }
124
        }
125

126
        nlr_jump(nlr.ret_val);
×
127
    }
128
}
129

130
// The awaitables associated with pbio tasks can originate from different
131
// objects. At the moment, they are only associated with Bluetooth tasks, and
132
// they cannot run at the same time. So we keep a single list of awaitables
133
// here instead of with each Bluetooth-related MicroPython object.
134
MP_REGISTER_ROOT_POINTER(mp_obj_t pbio_task_awaitables);
135

136
static bool pb_module_tools_pbio_task_test_completion(mp_obj_t obj, uint32_t end_time) {
×
137
    pbio_task_t *task = MP_OBJ_TO_PTR(obj);
×
138

139
    // Keep going if not done yet.
140
    if (task->status == PBIO_ERROR_AGAIN) {
×
141
        return false;
142
    }
143

144
    // If done, make sure it was successful.
145
    pb_assert(task->status);
×
146
    return true;
×
147
}
148

149
mp_obj_t pb_module_tools_pbio_task_wait_or_await(pbio_task_t *task) {
×
150
    return pb_type_awaitable_await_or_wait(
×
151
        MP_OBJ_FROM_PTR(task),
152
        MP_STATE_PORT(pbio_task_awaitables),
153
        pb_type_awaitable_end_time_none,
154
        pb_module_tools_pbio_task_test_completion,
155
        pb_type_awaitable_return_none,
156
        pb_type_awaitable_cancel_none,
157
        PB_TYPE_AWAITABLE_OPT_RAISE_ON_BUSY);
158
}
159

160
/**
161
 * Reads one byte from stdin without blocking if a byte is available, and
162
 * optionally converts it to character representation.
163
 *
164
 * @param [in]  last    Choose @c True to read until the last byte is read
165
 *                      or @c False to get the first available byte.
166
 * @param [in]  chr     Choose @c False to return the integer value of the byte.
167
 *                      Choose @c True to return a single character string of
168
 *                      the resulting byte if it is printable and otherwise
169
 *                      return @c None .
170
 *
171
 * @returns The resulting byte if there was one, converted as above, otherwise @c None .
172
 *
173
 */
174
static mp_obj_t pb_module_tools_read_input_byte(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
×
175
    PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args,
×
176
        PB_ARG_DEFAULT_FALSE(last),
177
        PB_ARG_DEFAULT_FALSE(chr));
×
178

179
    int chr = -1;
×
180

181
    while ((mp_hal_stdio_poll(MP_STREAM_POLL_RD) & MP_STREAM_POLL_RD)) {
×
182
        // REVISIT: In theory, this should not block if mp_hal_stdio_poll() and
183
        // mp_hal_stdin_rx_chr() are implemented correctly and nothing happens
184
        // in a thread/interrupt/kernel that changes the state.
185
        chr = mp_hal_stdin_rx_chr();
×
186

187
        // For last=False, break to stop at first byte. Otherwise, keep reading.
188
        if (!mp_obj_is_true(last_in)) {
×
189
            break;
190
        }
191
    }
192

193
    // If no data is available, return None.
194
    if (chr < 0) {
×
195
        return mp_const_none;
196
    }
197

198
    // If chr=False, return the integer value of the byte.
199
    if (!mp_obj_is_true(chr_in)) {
×
200
        return MP_OBJ_NEW_SMALL_INT(chr);
×
201
    }
202

203
    // If char requested but not printable, return None.
204
    if (chr < 32 || chr > 126) {
×
205
        return mp_const_none;
206
    }
207

208
    // Return the character as a string.
209
    const char result[] = {chr};
×
210
    return mp_obj_new_str(result, sizeof(result));
×
211
}
212
static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_read_input_byte_obj, 0, pb_module_tools_read_input_byte);
213

214
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✔
215
    PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args,
2✔
216
        PB_ARG_DEFAULT_NONE(task),
217
        PB_ARG_DEFAULT_NONE(loop_time));
2✔
218

219
    // Deprecated, but allow argument for backwards compatibility with <= v3.5.0
220
    (void)loop_time_in;
2✔
221

222
    // Without args, this function is used to test if the run loop is active.
223
    if (n_args == 0) {
2✔
UNCOV
224
        return mp_obj_new_bool(run_loop_is_active);
×
225
    }
226

227
    // Can only run one loop at a time.
228
    if (run_loop_is_active) {
2✔
UNCOV
229
        mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Run loop already active."));
×
230
    }
231

232
    mp_obj_iter_buf_t iter_buf;
2✔
233
    mp_obj_t iterable = mp_getiter(task_in, &iter_buf);
2✔
234

235
    nlr_buf_t nlr;
2✔
236
    if (nlr_push(&nlr) == 0) {
2✔
237
        run_loop_is_active = true;
2✔
238
        while (mp_iternext(iterable) != MP_OBJ_STOP_ITERATION) {
27,559✔
239
            MICROPY_VM_HOOK_LOOP
27,557✔
240
        }
241
        nlr_pop();
2✔
242
        run_loop_is_active = false;
2✔
243
    } else {
UNCOV
244
        run_loop_is_active = false;
×
UNCOV
245
        nlr_jump(nlr.ret_val);
×
246
    }
247
    return mp_const_none;
2✔
248
}
249
static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_run_task_obj, 0, pb_module_tools_run_task);
250

251
// Reset global awaitable state when user program starts.
252
void pb_module_tools_init(void) {
23✔
253
    MP_STATE_PORT(wait_awaitables) = mp_obj_new_list(0, NULL);
23✔
254
    MP_STATE_PORT(pbio_task_awaitables) = mp_obj_new_list(0, NULL);
23✔
255
    run_loop_is_active = false;
23✔
256
}
23✔
257

258
#if PYBRICKS_PY_TOOLS_HUB_MENU
259

260
static void pb_module_tools_hub_menu_display_symbol(mp_obj_t symbol) {
261
    if (mp_obj_is_str(symbol)) {
262
        pb_type_LightMatrix_display_char(pbsys_hub_light_matrix, symbol);
263
    } else {
264
        pb_type_LightMatrix_display_number(pbsys_hub_light_matrix, symbol);
265
    }
266
}
267

268
/**
269
 * Waits for a button press or release.
270
 *
271
 * @param [in]  press   Choose @c true to wait for press or @c false to wait for release.
272
 * @returns             When waiting for pressed, it returns the button that was pressed, otherwise returns 0.
273
 */
274
static pbio_button_flags_t pb_module_tools_hub_menu_wait_for_press(bool press) {
275

276
    // This function should only be used in a blocking context.
277
    pb_module_tools_assert_blocking();
278

279
    pbio_error_t err;
280
    pbio_button_flags_t btn;
281
    while ((err = pbio_button_is_pressed(&btn)) == PBIO_SUCCESS && ((bool)btn) == !press) {
282
        MICROPY_EVENT_POLL_HOOK;
283
    }
284
    pb_assert(err);
285
    return btn;
286
}
287

288
/**
289
 * Displays a menu on the hub display and allows the user to pick a symbol
290
 * using the buttons.
291
 *
292
 * @param [in]  n_args  The number of args.
293
 * @param [in]  args    The args passed in Python code (the menu entries).
294
 */
295
static mp_obj_t pb_module_tools_hub_menu(size_t n_args, const mp_obj_t *args) {
296

297
    // Validate arguments by displaying all of them, ending with the first.
298
    // This ensures we fail right away instead of midway through the menu. It
299
    // happens so fast that there isn't a time penalty for this.
300
    for (int i = n_args - 1; i >= 0; i--) {
301
        pb_module_tools_hub_menu_display_symbol(args[i]);
302
    }
303

304
    // Disable stop button and cache original setting to restore later.
305
    pbio_button_flags_t stop_button = pbsys_program_stop_get_buttons();
306
    pbsys_program_stop_set_buttons(0);
307

308
    nlr_buf_t nlr;
309
    if (nlr_push(&nlr) == 0) {
310

311
        size_t selection = 0;
312

313
        while (true) {
314
            pb_module_tools_hub_menu_wait_for_press(false);
315
            pbio_button_flags_t btn = pb_module_tools_hub_menu_wait_for_press(true);
316

317
            // Selection made, exit.
318
            if (btn & PBIO_BUTTON_CENTER) {
319
                break;
320
            }
321

322
            // Increment/decrement selection for left/right buttons.
323
            if (btn & PBIO_BUTTON_RIGHT) {
324
                selection = (selection + 1) % n_args;
325
            } else if (btn & PBIO_BUTTON_LEFT) {
326
                selection = selection == 0 ? n_args - 1 : selection - 1;
327
            }
328

329
            // Display current selection.
330
            pb_module_tools_hub_menu_display_symbol(args[selection]);
331
        }
332

333
        // Wait for release before returning, just like starting a normal program.
334
        pb_module_tools_hub_menu_wait_for_press(false);
335

336
        // Restore stop button setting prior to starting menu.
337
        pbsys_program_stop_set_buttons(stop_button);
338

339
        // Complete and return selected object.
340
        nlr_pop();
341
        return args[selection];
342
    } else {
343
        pbsys_program_stop_set_buttons(stop_button);
344
        nlr_jump(nlr.ret_val);
345
        return mp_const_none;
346
    }
347
}
348
static MP_DEFINE_CONST_FUN_OBJ_VAR(pb_module_tools_hub_menu_obj, 2, pb_module_tools_hub_menu);
349

350
#endif // PYBRICKS_PY_TOOLS_HUB_MENU
351

352
static const mp_rom_map_elem_t tools_globals_table[] = {
353
    { MP_ROM_QSTR(MP_QSTR___name__),    MP_ROM_QSTR(MP_QSTR_tools)                    },
354
    { MP_ROM_QSTR(MP_QSTR_wait),        MP_ROM_PTR(&pb_module_tools_wait_obj)         },
355
    { MP_ROM_QSTR(MP_QSTR_read_input_byte), MP_ROM_PTR(&pb_module_tools_read_input_byte_obj) },
356
    #if PYBRICKS_PY_TOOLS_APP_DATA
357
    { MP_ROM_QSTR(MP_QSTR_AppData),  MP_ROM_PTR(&pb_type_app_data)               },
358
    #endif // PYBRICKS_PY_TOOLS_APP_DATA
359
    #if PYBRICKS_PY_TOOLS_HUB_MENU
360
    { MP_ROM_QSTR(MP_QSTR_hub_menu),    MP_ROM_PTR(&pb_module_tools_hub_menu_obj)     },
361
    #endif // PYBRICKS_PY_TOOLS_HUB_MENU
362
    { MP_ROM_QSTR(MP_QSTR_run_task),    MP_ROM_PTR(&pb_module_tools_run_task_obj)     },
363
    { MP_ROM_QSTR(MP_QSTR_StopWatch),   MP_ROM_PTR(&pb_type_StopWatch)                },
364
    { MP_ROM_QSTR(MP_QSTR_multitask),   MP_ROM_PTR(&pb_type_Task)                     },
365
    #if MICROPY_PY_BUILTINS_FLOAT
366
    { MP_ROM_QSTR(MP_QSTR_Matrix),      MP_ROM_PTR(&pb_type_Matrix)           },
367
    { MP_ROM_QSTR(MP_QSTR_vector),      MP_ROM_PTR(&pb_geometry_vector_obj)   },
368
    { MP_ROM_QSTR(MP_QSTR_cross),       MP_ROM_PTR(&pb_type_matrix_cross_obj) },
369
    // backwards compatibility for pybricks.geometry.Axis
370
    { MP_ROM_QSTR(MP_QSTR_Axis),        MP_ROM_PTR(&pb_enum_type_Axis) },
371
    #endif // MICROPY_PY_BUILTINS_FLOAT
372
};
373
static MP_DEFINE_CONST_DICT(pb_module_tools_globals, tools_globals_table);
374

375
const mp_obj_module_t pb_module_tools = {
376
    .base = { &mp_type_module },
377
    .globals = (mp_obj_dict_t *)&pb_module_tools_globals,
378
};
379

380
#if PYBRICKS_RUNS_ON_EV3DEV
381
// ev3dev extends the C module in Python
382
MP_REGISTER_MODULE(MP_QSTR__tools, pb_module_tools);
383
#else
384
MP_REGISTER_MODULE(MP_QSTR_pybricks_dot_tools, pb_module_tools);
385
#endif
386

387
// backwards compatibility for pybricks.geometry
388
#if MICROPY_PY_BUILTINS_FLOAT
389
MP_REGISTER_MODULE(MP_QSTR_pybricks_dot_geometry, pb_module_tools);
390
#endif // MICROPY_PY_BUILTINS_FLOAT
391

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