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

pybricks / pybricks-micropython / 19016791193

02 Nov 2025 06:40PM UTC coverage: 57.167% (-2.6%) from 59.744%
19016791193

Pull #406

github

laurensvalk
bricks/virtualhub: Replace with embedded simulation.

Instead of using the newly introduced simhub alongside the virtualhub, we'll just replace the old one entirely now that it has reached feature parity. We can keep calling it the virtualhub.
Pull Request #406: New virtual hub for more effective debugging

41 of 48 new or added lines in 7 files covered. (85.42%)

414 existing lines in 53 files now uncovered.

4479 of 7835 relevant lines covered (57.17%)

17178392.75 hits per line

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

66.67
/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 <string.h>
9

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

17
#include <pbdrv/clock.h>
18

19
#include <pbio/int_math.h>
20
#include <pbio/util.h>
21
#include <pbsys/light.h>
22
#include <pbsys/program_stop.h>
23
#include <pbsys/status.h>
24

25
#include <pybricks/parameters.h>
26
#include <pybricks/common.h>
27
#include <pybricks/tools.h>
28
#include <pybricks/tools/pb_type_async.h>
29
#include <pybricks/tools/pb_type_matrix.h>
30

31
#include <pybricks/util_mp/pb_kwarg_helper.h>
32
#include <pybricks/util_mp/pb_obj_helper.h>
33
#include <pybricks/util_pb/pb_error.h>
34

35

36
// Global state of the run loop for async user programs. Gets set when run_task
37
// is called and cleared when it completes
38
static bool run_loop_is_active;
39

40
bool pb_module_tools_run_loop_is_active(void) {
2,165✔
41
    return run_loop_is_active;
2,165✔
42
}
43

44
void pb_module_tools_assert_blocking(void) {
17✔
45
    if (run_loop_is_active) {
17✔
46
        mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("This can only be called before multitasking starts."));
×
47
    }
48
}
17✔
49

50
/**
51
 * Statically allocated wait objects that can be re-used without allocation
52
 * once exhausted. Should be sufficient for trivial applications.
53
 *
54
 * More are allocated as needed. If a user has more than this many parallel
55
 * waits, the user can probably afford to allocate anyway.
56
 *
57
 * This is set to zero each time MicroPython starts.
58
 */
59
static pb_type_async_t waits[6];
60

61
static pbio_error_t pb_module_tools_wait_iter_once(pbio_os_state_t *state, mp_obj_t parent_obj) {
86,281✔
62
    // Not a protothread, but using the state variable to store final time.
63
    return pbio_util_time_has_passed(pbdrv_clock_get_ms(), (uint32_t)*state) ? PBIO_SUCCESS: PBIO_ERROR_AGAIN;
86,281✔
64
}
65

66
static mp_obj_t pb_module_tools_wait(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
2,107✔
67
    PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args,
2,107✔
68
        PB_ARG_REQUIRED(time));
69

70
    mp_int_t time = pb_obj_get_int(time_in);
2,107✔
71

72
    // Outside run loop, do blocking wait to avoid async overhead.
73
    if (!pb_module_tools_run_loop_is_active()) {
2,107✔
74
        if (time > 0) {
2,095✔
75
            mp_hal_delay_ms(time);
2,095✔
76
        }
77
        return mp_const_none;
2,095✔
78
    }
79

80
    // Find statically allocated candidate that can be re-used again because
81
    // it was never used or used and exhausted. If it stays at NULL then a new
82
    // awaitable is allocated.
83
    pb_type_async_t *reuse = NULL;
12✔
84
    for (uint32_t i = 0; i < MP_ARRAY_SIZE(waits); i++) {
20✔
85
        if (waits[i].parent_obj == MP_OBJ_NULL) {
20✔
86
            reuse = &waits[i];
12✔
87
            break;
12✔
88
        }
89
    }
90

91
    pb_type_async_t config = {
36✔
92
        // Not associated with any parent object.
93
        .parent_obj = mp_const_none,
94
        // Yield once for duration 0 to avoid blocking loops.
95
        .iter_once = time == 0 ? NULL : pb_module_tools_wait_iter_once,
12✔
96
        // No protothread here; use it to encode end time.
97
        .state = pbdrv_clock_get_ms() + (uint32_t)time,
12✔
98
    };
99

100
    return pb_type_async_wait_or_await(&config, &reuse, false);
12✔
101
}
102
static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_wait_obj, 0, pb_module_tools_wait);
103

104
/**
105
 * Reads one byte from stdin without blocking if a byte is available, and
106
 * optionally converts it to character representation.
107
 *
108
 * @param [in]  last    Choose @c True to read until the last byte is read
109
 *                      or @c False to get the first available byte.
110
 * @param [in]  chr     Choose @c False to return the integer value of the byte.
111
 *                      Choose @c True to return a single character string of
112
 *                      the resulting byte if it is printable and otherwise
113
 *                      return @c None .
114
 *
115
 * @returns The resulting byte if there was one, converted as above, otherwise @c None .
116
 *
117
 */
118
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) {
×
119
    PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args,
×
120
        PB_ARG_DEFAULT_FALSE(last),
121
        PB_ARG_DEFAULT_FALSE(chr));
122

123
    int chr = -1;
×
124

125
    while ((mp_hal_stdio_poll(MP_STREAM_POLL_RD) & MP_STREAM_POLL_RD)) {
×
126
        // REVISIT: In theory, this should not block if mp_hal_stdio_poll() and
127
        // mp_hal_stdin_rx_chr() are implemented correctly and nothing happens
128
        // in a thread/interrupt/kernel that changes the state.
129
        chr = mp_hal_stdin_rx_chr();
×
130

131
        // For last=False, break to stop at first byte. Otherwise, keep reading.
132
        if (!mp_obj_is_true(last_in)) {
×
UNCOV
133
            break;
×
134
        }
135
    }
136

137
    // If no data is available, return None.
138
    if (chr < 0) {
×
UNCOV
139
        return mp_const_none;
×
140
    }
141

142
    // If chr=False, return the integer value of the byte.
143
    if (!mp_obj_is_true(chr_in)) {
×
144
        return MP_OBJ_NEW_SMALL_INT(chr);
×
145
    }
146

147
    // If char requested but not printable, return None.
148
    if (chr < 32 || chr > 126) {
×
UNCOV
149
        return mp_const_none;
×
150
    }
151

152
    // Return the character as a string.
153
    const char result[] = {chr};
×
154
    return mp_obj_new_str(result, sizeof(result));
×
155
}
156
static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_read_input_byte_obj, 0, pb_module_tools_read_input_byte);
157

158
static mp_obj_t pb_module_tools_run_task(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
10✔
159
    PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args,
10✔
160
        PB_ARG_DEFAULT_NONE(task));
161

162
    // Without args, this function is used to test if the run loop is active.
163
    if (task_in == mp_const_none) {
10✔
164
        return mp_obj_new_bool(run_loop_is_active);
×
165
    }
166

167
    // Can only run one loop at a time.
168
    if (run_loop_is_active) {
10✔
169
        mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Run loop already active."));
×
170
    }
171

172
    mp_obj_iter_buf_t iter_buf;
173
    nlr_buf_t nlr;
174

175
    if (nlr_push(&nlr) == 0) {
10✔
176
        run_loop_is_active = true;
10✔
177
        mp_obj_t iterable = mp_getiter(task_in, &iter_buf);
10✔
178
        while (mp_iternext(iterable) != MP_OBJ_STOP_ITERATION) {
277,479✔
179
            // Keep running system processes.
180
            MICROPY_VM_HOOK_LOOP
277,469✔
181
            // Stop on exception such as SystemExit.
182
            mp_handle_pending(true);
277,469✔
183
        }
184
        nlr_pop();
10✔
185
        run_loop_is_active = false;
10✔
186
    } else {
187
        run_loop_is_active = false;
×
188
        nlr_jump(nlr.ret_val);
×
189
    }
190
    return mp_const_none;
10✔
191
}
192
static MP_DEFINE_CONST_FUN_OBJ_KW(pb_module_tools_run_task_obj, 0, pb_module_tools_run_task);
193

194
// Reset global awaitable state when user program starts.
195
void pb_module_tools_init(void) {
25✔
196
    memset(waits, 0, sizeof(waits));
25✔
197
    run_loop_is_active = false;
25✔
198
}
25✔
199

200
#if PYBRICKS_PY_TOOLS_HUB_MENU
201

202
static void pb_module_tools_hub_menu_display_symbol(mp_obj_t symbol) {
203
    if (mp_obj_is_str(symbol)) {
204
        pb_type_LightMatrix_display_char(pbsys_hub_light_matrix, symbol);
205
    } else {
206
        pb_type_LightMatrix_display_number(pbsys_hub_light_matrix, symbol);
207
    }
208
}
209

210
/**
211
 * Waits for a button press or release.
212
 *
213
 * @param [in]  press   Choose @c true to wait for press or @c false to wait for release.
214
 * @returns             When waiting for pressed, it returns the button that was pressed, otherwise returns 0.
215
 */
216
static pbio_button_flags_t pb_module_tools_hub_menu_wait_for_press(bool press) {
217

218
    // This function should only be used in a blocking context.
219
    pb_module_tools_assert_blocking();
220

221
    pbio_button_flags_t btn;
222
    while ((bool)(btn = pbdrv_button_get_pressed()) == !press) {
223
        MICROPY_EVENT_POLL_HOOK;
224
    }
225
    return btn;
226
}
227

228
/**
229
 * Displays a menu on the hub display and allows the user to pick a symbol
230
 * using the buttons.
231
 *
232
 * @param [in]  n_args  The number of args.
233
 * @param [in]  args    The args passed in Python code (the menu entries).
234
 */
235
static mp_obj_t pb_module_tools_hub_menu(size_t n_args, const mp_obj_t *args) {
236

237
    // Validate arguments by displaying all of them, ending with the first.
238
    // This ensures we fail right away instead of midway through the menu. It
239
    // happens so fast that there isn't a time penalty for this.
240
    for (int i = n_args - 1; i >= 0; i--) {
241
        pb_module_tools_hub_menu_display_symbol(args[i]);
242
    }
243

244
    // Disable stop button and cache original setting to restore later.
245
    pbio_button_flags_t stop_button = pbsys_program_stop_get_buttons();
246

247
    // Disable normal stop behavior since we need the buttons for the menu.
248
    // Except if the Bluetooth button is used for stopping, since we don't need
249
    // it for the menu.
250
    if (stop_button != PBIO_BUTTON_RIGHT_UP) {
251
        pbsys_program_stop_set_buttons(0);
252
    }
253

254
    nlr_buf_t nlr;
255
    if (nlr_push(&nlr) == 0) {
256

257
        size_t selection = 0;
258

259
        while (true) {
260
            pb_module_tools_hub_menu_wait_for_press(false);
261
            pbio_button_flags_t btn = pb_module_tools_hub_menu_wait_for_press(true);
262

263
            // Selection made, exit.
264
            if (btn & PBIO_BUTTON_CENTER) {
265
                break;
266
            }
267

268
            // Increment/decrement selection for left/right buttons.
269
            if (btn & PBIO_BUTTON_RIGHT) {
270
                selection = (selection + 1) % n_args;
271
            } else if (btn & PBIO_BUTTON_LEFT) {
272
                selection = selection == 0 ? n_args - 1 : selection - 1;
273
            }
274

275
            // Display current selection.
276
            pb_module_tools_hub_menu_display_symbol(args[selection]);
277
        }
278

279
        // Wait for release before returning, just like starting a normal program.
280
        pb_module_tools_hub_menu_wait_for_press(false);
281

282
        // Restore stop button setting prior to starting menu.
283
        pbsys_program_stop_set_buttons(stop_button);
284

285
        // Complete and return selected object.
286
        nlr_pop();
287
        return args[selection];
288
    } else {
289
        pbsys_program_stop_set_buttons(stop_button);
290
        nlr_jump(nlr.ret_val);
291
        return mp_const_none;
292
    }
293
}
294
static MP_DEFINE_CONST_FUN_OBJ_VAR(pb_module_tools_hub_menu_obj, 2, pb_module_tools_hub_menu);
295

296
#endif // PYBRICKS_PY_TOOLS_HUB_MENU
297

298
static const mp_rom_map_elem_t tools_globals_table[] = {
299
    { MP_ROM_QSTR(MP_QSTR___name__),    MP_ROM_QSTR(MP_QSTR_tools)                    },
300
    { MP_ROM_QSTR(MP_QSTR_wait),        MP_ROM_PTR(&pb_module_tools_wait_obj)         },
301
    { MP_ROM_QSTR(MP_QSTR_read_input_byte), MP_ROM_PTR(&pb_module_tools_read_input_byte_obj) },
302
    #if PYBRICKS_PY_TOOLS_APP_DATA
303
    { MP_ROM_QSTR(MP_QSTR_AppData),  MP_ROM_PTR(&pb_type_app_data)               },
304
    #endif // PYBRICKS_PY_TOOLS_APP_DATA
305
    #if PYBRICKS_PY_TOOLS_HUB_MENU
306
    { MP_ROM_QSTR(MP_QSTR_hub_menu),    MP_ROM_PTR(&pb_module_tools_hub_menu_obj)     },
307
    #endif // PYBRICKS_PY_TOOLS_HUB_MENU
308
    { MP_ROM_QSTR(MP_QSTR_run_task),    MP_ROM_PTR(&pb_module_tools_run_task_obj)     },
309
    { MP_ROM_QSTR(MP_QSTR_StopWatch),   MP_ROM_PTR(&pb_type_StopWatch)                },
310
    { MP_ROM_QSTR(MP_QSTR_multitask),   MP_ROM_PTR(&pb_type_Task)                     },
311
    #if MICROPY_PY_BUILTINS_FLOAT
312
    { MP_ROM_QSTR(MP_QSTR_Matrix),      MP_ROM_PTR(&pb_type_Matrix)           },
313
    { MP_ROM_QSTR(MP_QSTR_vector),      MP_ROM_PTR(&pb_geometry_vector_obj)   },
314
    { MP_ROM_QSTR(MP_QSTR_cross),       MP_ROM_PTR(&pb_type_matrix_cross_obj) },
315
    // backwards compatibility for pybricks.geometry.Axis
316
    { MP_ROM_QSTR(MP_QSTR_Axis),        MP_ROM_PTR(&pb_enum_type_Axis) },
317
    #endif // MICROPY_PY_BUILTINS_FLOAT
318
};
319
static MP_DEFINE_CONST_DICT(pb_module_tools_globals, tools_globals_table);
320

321
const mp_obj_module_t pb_module_tools = {
322
    .base = { &mp_type_module },
323
    .globals = (mp_obj_dict_t *)&pb_module_tools_globals,
324
};
325

326
#if !MICROPY_MODULE_BUILTIN_SUBPACKAGES
327

328
MP_REGISTER_MODULE(MP_QSTR_pybricks_dot_tools, pb_module_tools);
329

330
// backwards compatibility for pybricks.geometry
331
#if MICROPY_PY_BUILTINS_FLOAT
332
MP_REGISTER_MODULE(MP_QSTR_pybricks_dot_geometry, pb_module_tools);
333
#endif // MICROPY_PY_BUILTINS_FLOAT
334

335
#endif // !MICROPY_MODULE_BUILTIN_SUBPACKAGES
336

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