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

pybricks / pybricks-micropython / 19013858907

02 Nov 2025 02:46PM UTC coverage: 49.083% (-0.02%) from 49.098%
19013858907

push

github

laurensvalk
!ci: skip all but virtualhub tests

3024 of 6161 relevant lines covered (49.08%)

16849.06 hits per line

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

45.99
/bricks/_common/micropython.c
1
// SPDX-License-Identifier: MIT
2
// Copyright (c) 2018-2023 The Pybricks Authors
3

4
// This file provides a MicroPython runtime to run code in MULTI_MPY_V6 format.
5

6
#include <stdint.h>
7
#include <stdio.h>
8
#include <string.h>
9

10
#include <pbdrv/stack.h>
11

12
#include <pbio/button.h>
13
#include <pbio/main.h>
14
#include <pbio/os.h>
15
#include <pbio/util.h>
16
#include <pbio/protocol.h>
17
#include <pbsys/main.h>
18
#include <pbsys/program_stop.h>
19
#include <pbsys/storage.h>
20

21
#include <pybricks/common.h>
22
#include <pybricks/util_mp/pb_obj_helper.h>
23

24
#include "genhdr/mpversion.h"
25
#include "shared/readline/readline.h"
26
#include "shared/runtime/gchelper.h"
27
#include "shared/runtime/interrupt_char.h"
28
#include "shared/runtime/pyexec.h"
29
#include "py/builtin.h"
30
#include "py/compile.h"
31
#include "py/frozenmod.h"
32
#include "py/gc.h"
33
#include "py/mperrno.h"
34
#include "py/mphal.h"
35
#include "py/objmodule.h"
36
#include "py/persistentcode.h"
37
#include "py/reader.h"
38
#include "py/repl.h"
39
#include "py/runtime.h"
40
#include "py/smallint.h"
41
#include "py/stackctrl.h"
42
#include "py/stream.h"
43

44
// Implementation for MICROPY_EVENT_POLL_HOOK
45
void pb_event_poll_hook(void) {
82,620✔
46

47
    while (pbio_os_run_processes_once()) {
163,262✔
48
    }
49

50
    mp_handle_pending(true);
82,620✔
51

52
    pbio_os_run_processes_and_wait_for_event();
82,620✔
53
}
82,620✔
54

55
// callback for when stop button is pressed in IDE or on hub
56
void pbsys_main_stop_program(bool force_stop) {
×
57
    if (force_stop) {
×
58
        mp_sched_vm_abort();
×
59
    } else {
60
        static mp_obj_exception_t system_exit;
61
        system_exit.base.type = &mp_type_SystemExit;
×
62
        system_exit.traceback_alloc = system_exit.traceback_len = 0;
×
63
        system_exit.traceback_data = NULL;
×
64
        system_exit.args = (mp_obj_tuple_t *)&mp_const_empty_tuple_obj;
×
65

66
        mp_sched_exception(MP_OBJ_FROM_PTR(&system_exit));
×
67
    }
68
}
×
69

70
bool pbsys_main_stdin_event(uint8_t c) {
×
71
    if (c == mp_interrupt_char) {
×
72
        mp_sched_keyboard_interrupt();
×
73
        return true;
×
74
    }
75

76
    return false;
×
77
}
78

79
// Prints the exception that ended the program.
80
static void print_final_exception(mp_obj_t exc, int ret) {
1✔
81

82
    // Ensure exception prints on new line.
83
    mp_hal_stdout_tx_flush();
1✔
84

85
    nlr_buf_t nlr;
86
    nlr.ret_val = NULL;
1✔
87

88
    if (nlr_push(&nlr) == 0) {
1✔
89
        // Handle graceful stop with button.
90
        if ((ret & PYEXEC_FORCED_EXIT) &&
1✔
91
            mp_obj_exception_match(exc, MP_OBJ_FROM_PTR(&mp_type_SystemExit))) {
×
92
            mp_printf(&mp_plat_print, "The program was stopped (%q).\n",
×
93
                ((mp_obj_exception_t *)MP_OBJ_TO_PTR(exc))->base.type->name);
×
94
            return;
×
95
        }
96

97
        // REVISIT: flash the light red a few times to indicate an unhandled exception?
98

99
        // Print unhandled exception with traceback.
100
        mp_obj_print_exception(&mp_plat_print, exc);
1✔
101

102
        nlr_pop();
1✔
103
    } else {
104
        // If we couldn't print the exception, just return. There is nothing
105
        // else we can do.
106

107
        // REVISIT: flash the light with a different pattern here?
108
    }
109
}
110

111
#if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL
112
static void run_repl(void) {
×
113
    int ret = 0;
×
114

115
    readline_init0();
×
116

117
    nlr_buf_t nlr;
118
    nlr.ret_val = NULL;
×
119

120
    if (nlr_push(&nlr) == 0) {
×
121
        nlr_set_abort(&nlr);
×
122
        // No need to set interrupt_char here, it is done by pyexec.
123
        #if PYBRICKS_OPT_RAW_REPL
124
        if (pyexec_mode_kind == PYEXEC_MODE_RAW_REPL) {
125
            // Compatibility with mpremote.
126
            mp_printf(&mp_plat_print, "MPY: soft reboot\n");
127
            ret = pyexec_raw_repl();
128
        } else {
129
            ret = pyexec_friendly_repl();
130
        }
131
        #else // PYBRICKS_OPT_RAW_REPL
132
        ret = pyexec_friendly_repl();
×
133
        #endif // PYBRICKS_OPT_RAW_REPL
134
        nlr_pop();
×
135
    } else {
136
        // if vm abort
137
        if (nlr.ret_val == NULL) {
×
138
            // we are shutting down, so don't bother with cleanup
139
            return;
×
140
        }
141

142
        // clear any pending exceptions (and run any callbacks).
143
        mp_handle_pending(false);
×
144
        // Print which exception triggered this.
145
        print_final_exception(MP_OBJ_FROM_PTR(nlr.ret_val), ret);
×
146
    }
147

148
    nlr_set_abort(NULL);
×
149
}
150
#endif // PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL
151

152

153
/** mpy info and data for one script or module. */
154
typedef struct {
155
    /** Size of the mpy program. Not aligned, use pbio_get_uint32_le() to read. */
156
    uint8_t mpy_size[4];
157
    /** Null-terminated name of the script, without file extension. */
158
    char mpy_name[];
159
    /** mpy data follows thereafter. */
160
} mpy_info_t;
161

162
// Program data is a concatenation of multiple mpy files. This sets a reference
163
// to the first script and the total size so we can search for modules.
164
static mpy_info_t *mpy_first;
165
static mpy_info_t *mpy_end;
166
static inline void mpy_data_init(pbsys_main_program_t *program) {
25✔
167
    mpy_first = (mpy_info_t *)program->code_start;
25✔
168
    mpy_end = (mpy_info_t *)program->code_end;
25✔
169
}
25✔
170

171
/**
172
 * Gets a reference to the mpy data of a script.
173
 * @param [in]  info    A pointer to an mpy info header.
174
 * @return              A pointer to the .mpy file.
175
 */
176
static uint8_t *mpy_data_get_buf(mpy_info_t *info) {
51✔
177
    // The header consists of the size and a zero-terminated module name string.
178
    return (uint8_t *)info + sizeof(info->mpy_size) + strlen(info->mpy_name) + 1;
51✔
179
}
180

181
/**
182
 * Finds a MicroPython module in the program data.
183
 * @param [in]  name    The fully qualified name of the module.
184
 * @return              A pointer to the .mpy file in user RAM or NULL if the
185
 *                      module was not found.
186
 */
187
static mpy_info_t *mpy_data_find(qstr name) {
×
188
    const char *name_str = qstr_str(name);
×
189

190
    for (mpy_info_t *info = mpy_first; (uintptr_t)info + sizeof(uint32_t) < (uintptr_t)mpy_end;
×
191
         info = (mpy_info_t *)(mpy_data_get_buf(info) + pbio_get_uint32_le(info->mpy_size))) {
×
192
        if (strcmp(info->mpy_name, name_str) == 0) {
×
193
            return info;
×
194
        }
195
    }
196

197
    return NULL;
×
198
}
199

200
// From micropython/py/builtinimport.c, but copied because it is static.
201
static void do_execute_proto_fun(const mp_module_context_t *context, mp_proto_fun_t proto_fun) {
24✔
202

203
    // execute the module in its context
204
    mp_obj_dict_t *mod_globals = context->module.globals;
24✔
205

206
    // save context
207
    nlr_jump_callback_node_globals_locals_t ctx;
208
    ctx.globals = mp_globals_get();
24✔
209
    ctx.locals = mp_locals_get();
24✔
210

211
    // set new context
212
    mp_globals_set(mod_globals);
24✔
213
    mp_locals_set(mod_globals);
24✔
214

215
    // set exception handler to restore context if an exception is raised
216
    nlr_push_jump_callback(&ctx.callback, mp_globals_locals_set_from_nlr_jump_callback);
24✔
217

218
    // make and execute the function
219
    mp_obj_t module_fun = mp_make_function_from_proto_fun(proto_fun, context, NULL);
24✔
220
    mp_call_function_0(module_fun);
24✔
221

222
    // deregister exception handler and restore context
223
    nlr_pop_jump_callback(true);
24✔
224
}
24✔
225

226
static void execute_rom_mpy_in_context(mp_module_context_t *module_context, mpy_info_t *mpy_info) {
25✔
227
    // Prepare to execute in its own context.
228
    mp_compiled_module_t compiled_module;
229
    compiled_module.context = module_context;
25✔
230

231
    // Execute the MPY file in the new module context.
232
    mp_reader_t reader;
233
    mp_reader_new_mem(&reader, mpy_data_get_buf(mpy_info), pbio_get_uint32_le(mpy_info->mpy_size), MP_READER_IS_ROM);
25✔
234
    mp_raw_code_load(&reader, &compiled_module);
25✔
235
    do_execute_proto_fun(compiled_module.context, compiled_module.rc);
24✔
236
}
24✔
237

238
/**
239
 * Runs the __main__ module from user RAM.
240
 */
241
static void run_user_program(void) {
25✔
242
    int ret = 0;
25✔
243

244
    nlr_buf_t nlr;
245
    nlr.ret_val = NULL;
25✔
246

247
    if (nlr_push(&nlr) == 0) {
25✔
248
        nlr_set_abort(&nlr);
25✔
249

250
        // Run the script while letting CTRL-C interrupt it.
251
        mp_hal_set_interrupt_char(CHAR_CTRL_C);
25✔
252

253
        // Main program runs in __main__ module which is already initialized.
254
        mp_module_context_t main_context = {
25✔
255
            .module = mp_module___main__
256
        };
257
        execute_rom_mpy_in_context(&main_context, mpy_first);
25✔
258

259
        mp_hal_set_interrupt_char(-1);
24✔
260

261
        // Handle any pending exceptions (and any callbacks)
262
        mp_handle_pending(true);
24✔
263

264
        nlr_pop();
24✔
265
    } else {
266
        mp_hal_set_interrupt_char(-1);
1✔
267

268
        // if vm abort
269
        if (nlr.ret_val == NULL) {
1✔
270
            // we are shutting down, so don't bother with cleanup
271
            return;
×
272
        }
273

274
        // Clear any pending exceptions (and run any callbacks).
275
        mp_handle_pending(false);
1✔
276

277
        if (mp_obj_is_subclass_fast(MP_OBJ_FROM_PTR(((mp_obj_base_t *)nlr.ret_val)->type), MP_OBJ_FROM_PTR(&mp_type_SystemExit))) {
1✔
278
            // at the moment, the value of SystemExit is unused
279
            ret = PYEXEC_FORCED_EXIT;
×
280
        }
281

282
        print_final_exception(MP_OBJ_FROM_PTR(nlr.ret_val), ret);
1✔
283

284
        #if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL
285
        // On KeyboardInterrupt, drop to REPL for debugging.
286
        if (mp_obj_exception_match(MP_OBJ_FROM_PTR(nlr.ret_val), MP_OBJ_FROM_PTR(&mp_type_KeyboardInterrupt))) {
1✔
287

288
            // The global scope is preserved to facilitate debugging, but we
289
            // stop active resources like motors and sounds. They are stopped
290
            // but not reset so the user can restart them in the REPL.
291
            pbio_main_soft_stop();
×
292

293
            // Enter REPL.
294
            run_repl();
×
295
        }
296
        #endif // PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL
297
    }
298

299
    nlr_set_abort(NULL);
25✔
300
}
301

302
pbio_error_t pbsys_main_program_validate(pbsys_main_program_t *program) {
26✔
303

304
    // For builtin programs, check requested ID against feature flags.
305
    #if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL
306
    if (program->id == PBIO_PYBRICKS_USER_PROGRAM_ID_REPL) {
26✔
307
        return PBIO_SUCCESS;
×
308
    }
309
    #endif
310
    #if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_PORT_VIEW
311
    if (program->id == PBIO_PYBRICKS_USER_PROGRAM_ID_PORT_VIEW) {
26✔
312
        return PBIO_SUCCESS;
×
313
    }
314
    #endif
315
    #if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_IMU_CALIBRATION
316
    if (program->id == PBIO_PYBRICKS_USER_PROGRAM_ID_IMU_CALIBRATION) {
317
        return PBIO_SUCCESS;
318
    }
319
    #endif
320

321
    // If requesting a user program, ensure that it exists and is valid.
322
    uint32_t program_size = program->code_end - program->code_start;
26✔
323
    if (program_size == 0 || program_size > pbsys_storage_get_maximum_program_size()) {
26✔
324
        return PBIO_ERROR_NOT_SUPPORTED;
×
325
    }
326

327
    // Application-specific name may be used in system UI.
328
    mpy_info_t *mpy_info = (mpy_info_t *)program->code_start;
26✔
329
    program->name = mpy_info->mpy_name;
26✔
330

331
    // This is the same test done when loading the mpy. Do it early so we don't
332
    // start MicroPython and so the system knows this is valid. Revisit: Consider
333
    // making this part of the public API upstream.
334
    uint8_t *header = mpy_data_get_buf(mpy_info);
26✔
335
    uint8_t arch = MPY_FEATURE_DECODE_ARCH(header[2]);
26✔
336
    if (header[0] != 'M'
26✔
337
        || header[1] != MPY_VERSION
26✔
338
        || (arch != MP_NATIVE_ARCH_NONE && MPY_FEATURE_DECODE_SUB_VERSION(header[2]) != MPY_SUB_VERSION)
26✔
339
        || header[3] > MP_SMALL_INT_BITS) {
25✔
340
        return PBIO_ERROR_INVALID_ARG;
1✔
341
    }
342

343
    return PBIO_SUCCESS;
25✔
344
}
345

346
const char *pbsys_main_get_application_version_hash(void) {
26✔
347
    // This is (somewhat confusingly) passed in as the MICROPY_GIT_HASH.
348
    // REVISIT: Make PYBRICKS_GIT_HASH available in a pbio header via a build step.
349
    return MICROPY_GIT_HASH;
26✔
350
}
351

352
// Runs MicroPython with the given program data.
353
void pbsys_main_run_program(pbsys_main_program_t *program) {
25✔
354

355
    #if PBDRV_CONFIG_STACK_EMBEDDED
356
    // Stack limit should be less than real stack size, so we have a chance
357
    // to recover from limit hit.  (Limit is measured in bytes.)
358
    char *stack_start;
359
    char *stack_end;
360
    pbdrv_stack_get_info(&stack_start, &stack_end);
361
    #if PYBRICKS_OPT_USE_STACK_END_AS_TOP
362
    mp_stack_set_top(stack_end);
363
    #else
364
    // Sets the top to current stack pointer.
365
    mp_stack_ctrl_init();
366
    #endif
367
    mp_stack_set_limit(MP_STATE_THREAD(stack_top) - stack_start - 1024);
368
    #else
369
    mp_cstack_init_with_sp_here(1024 * 1024);
25✔
370
    #endif
371

372
    // MicroPython heap is the free RAM after program data.
373
    gc_init(program->user_ram_start, program->user_ram_end);
25✔
374

375
    // Set program data reference to first script. This is used to run main,
376
    // and to set the starting point for finding downloaded modules.
377
    mpy_data_init(program);
25✔
378

379
    // Initialize MicroPython.
380
    mp_init();
25✔
381

382
    // Runs the requested downloaded or builtin user program.
383
    switch (program->id) {
25✔
384

385
        #if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL
386
        case PBIO_PYBRICKS_USER_PROGRAM_ID_REPL:
×
387
            // Run REPL with everything auto-imported.
388
            pb_package_pybricks_init(true);
×
389
            run_repl();
×
390
            break;
×
391
        #endif
392

393
        #if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_PORT_VIEW && MICROPY_MODULE_FROZEN
394
        case PBIO_PYBRICKS_USER_PROGRAM_ID_PORT_VIEW:
×
395
            pb_package_pybricks_init(false);
×
396
            pyexec_frozen_module("_builtin_port_view.py", false);
×
397
            break;
×
398
        #endif
399

400
        #if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_IMU_CALIBRATION
401
        case PBIO_PYBRICKS_USER_PROGRAM_ID_IMU_CALIBRATION:
402
            // Todo
403
            break;
404
        #endif
405

406
        default:
25✔
407
            // Init Pybricks package without auto-import.
408
            pb_package_pybricks_init(false);
25✔
409
            // Run loaded user program (just slot 0 for now).
410
            run_user_program();
25✔
411
            break;
25✔
412
    }
413

414
    // Ensure everything is written before the user application is considered
415
    // done, so that the host does not receive stdout after receiving stop.
416
    mp_hal_stdout_tx_flush();
25✔
417
}
25✔
418

419
void pbsys_main_run_program_cleanup(void) {
25✔
420
    gc_sweep_all();
25✔
421
    mp_deinit();
25✔
422
}
25✔
423

424
void gc_collect(void) {
×
425
    gc_collect_start();
×
426
    gc_helper_collect_regs_and_stack();
×
427
    gc_collect_end();
×
428
}
×
429

430
mp_obj_t mp_builtin_open(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
×
431
    mp_raise_OSError(MP_ENOENT);
×
432
}
433
MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_open_obj, 1, mp_builtin_open);
434

435
// Overrides MicroPython's mp_builtin___import__
436
// IMPORTANT: this needs to be kept in sync with mp_builtin___import___default().
437
mp_obj_t pb_builtin_import(size_t n_args, const mp_obj_t *args) {
×
438
    // Check that it's not a relative import
439
    if (n_args >= 5 && MP_OBJ_SMALL_INT_VALUE(args[4]) != 0) {
×
440
        mp_raise_NotImplementedError(MP_ERROR_TEXT("relative import"));
×
441
    }
442

443
    // Check if the module is already loaded.
444
    mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_loaded_modules_dict).map, args[0], MP_MAP_LOOKUP);
×
445
    if (elem) {
×
446
        return elem->value;
×
447
    }
448

449
    // Try the name directly as a non-extensible built-in (e.g. `micropython`).
450
    qstr module_name_qstr = mp_obj_str_get_qstr(args[0]);
×
451
    mp_obj_t module_obj = mp_module_get_builtin(module_name_qstr, false);
×
452
    if (module_obj != MP_OBJ_NULL) {
×
453
        return module_obj;
×
454
    }
455

456
    // Now try as an extensible built-in (e.g. `struct`/`ustruct`).
457
    module_obj = mp_module_get_builtin(module_name_qstr, true);
×
458
    if (module_obj != MP_OBJ_NULL) {
×
459
        return module_obj;
×
460
    }
461

462
    // Check for presence of user program in user RAM.
463
    mpy_info_t *info = mpy_data_find(module_name_qstr);
×
464

465
    // If a downloaded module was found but not yet loaded, load it.
466
    if (info) {
×
467
        // Create new module.
468
        mp_module_context_t *module_context = mp_obj_new_module(module_name_qstr);
×
469

470
        // Execute the module in that context.
471
        execute_rom_mpy_in_context(module_context, info);
×
472

473
        // Return the newly imported module.
474
        return MP_OBJ_FROM_PTR(module_context);
×
475
    }
476

477
    // Allow importing of frozen modules if any were included in the firmware.
478
    #if MICROPY_MODULE_FROZEN_MPY
479
    void *modref;
480
    int frozen_type;
481
    const char *ext = ".py";
×
482
    char module_path[(1 << (8 * MICROPY_QSTR_BYTES_IN_LEN)) + sizeof(ext)] = { 0 };
×
483
    strcpy(module_path, mp_obj_str_get_str(args[0]));
×
484
    strcpy(module_path + qstr_len(module_name_qstr), ext);
×
485
    if (mp_find_frozen_module(module_path, &frozen_type, &modref) == MP_IMPORT_STAT_FILE && frozen_type == MP_FROZEN_MPY) {
×
486
        // Create new module to be returned.
487
        mp_module_context_t *module_context = mp_obj_new_module(module_name_qstr);
×
488
        mp_obj_t module_obj = MP_OBJ_FROM_PTR(module_context);
×
489

490
        // Execute frozen code in the new module context.
491
        const mp_frozen_module_t *frozen = modref;
×
492
        module_context->constants = frozen->constants;
×
493
        do_execute_proto_fun(module_context, frozen->proto_fun);
×
494
        return module_obj;
×
495
    }
496
    #endif
497

498
    // Nothing found, raise ImportError.
499
    mp_raise_msg_varg(&mp_type_ImportError, MP_ERROR_TEXT("no module named '%q'"), module_name_qstr);
×
500
}
501

502
mp_import_stat_t mp_import_stat(const char *path) {
×
503
    return MP_IMPORT_STAT_NO_EXIST;
×
504
}
505

506
mp_lexer_t *mp_lexer_new_from_file(qstr filename) {
×
507
    mp_raise_OSError(MP_ENOENT);
×
508
}
509

510
void nlr_jump_fail(void *val) {
×
511
    while (1) {
×
512
        ;
513
    }
514
}
515

516
void NORETURN __fatal_error(const char *msg) {
×
517
    while (1) {
×
518
        ;
519
    }
520
}
521

522
#ifndef NDEBUG
523
void MP_WEAK __assert_func(const char *file, int line, const char *func, const char *expr) {
×
524
    printf("Assertion '%s' failed, at file %s:%d\n", expr, file, line);
×
525
    __fatal_error("Assertion failed");
×
526
}
527
#endif
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