Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## [Unreleased]

### Added
- Added the exit code of the most recently ended program to the status report
event. Stopping a program now also gives it a distinct exit code, so hosts
can tell it apart from a program that ran to completion.

### Changed
- Updated MicroPython to v1.29.0.
- On the Move Hub, `bytes()` and `bytes.find()` now truncate out-of-range
Expand Down
91 changes: 71 additions & 20 deletions bricks/_common/micropython.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,18 @@ void pbsys_main_stop_program(bool force_stop) {
if (force_stop) {
mp_sched_vm_abort();
} else {
// The value of SystemExit is the exit code of the program, so give it
// the stop code. This lets the host tell a program that was stopped
// apart from one that ran to completion or called sys.exit().
static const mp_rom_obj_tuple_t args = {
{ &mp_type_tuple }, 1, { MP_ROM_INT(PBIO_PYBRICKS_EXIT_CODE_STOPPED) }
};

static mp_obj_exception_t system_exit;
system_exit.base.type = &mp_type_SystemExit;
system_exit.traceback_alloc = system_exit.traceback_len = 0;
system_exit.traceback_data = NULL;
system_exit.args = (mp_obj_tuple_t *)&mp_const_empty_tuple_obj;
system_exit.args = (mp_obj_tuple_t *)&args;

mp_sched_exception(MP_OBJ_FROM_PTR(&system_exit));
}
Expand All @@ -65,6 +72,40 @@ bool pbsys_main_stdin_event(uint8_t c) {
return false;
}

// The exit codes reported to the host are the same as the ones that pyexec
// returns for the REPL and for programs run with pyexec_frozen_module().
_Static_assert(PYEXEC_NORMAL_EXIT == PBIO_PYBRICKS_EXIT_CODE_OK, "wrong ok code");
_Static_assert(PYEXEC_UNHANDLED_EXCEPTION == PBIO_PYBRICKS_EXIT_CODE_EXCEPTION, "wrong exception code");
_Static_assert(PYEXEC_KEYBOARD_INTERRUPT == PBIO_PYBRICKS_EXIT_CODE_INTERRUPTED, "wrong interrupt code");
_Static_assert(PYEXEC_ABORT == PBIO_PYBRICKS_EXIT_CODE_ABORTED, "wrong abort code");

// Gets the pyexec-style return value for the exception that ended a program.
// The low byte is the exit code as it will be reported to the host.
static int pyexec_ret_from_exception(mp_obj_t exc) {

if (mp_obj_exception_match(exc, MP_OBJ_FROM_PTR(&mp_type_SystemExit))) {
// As in CPython, no value or None means a normal exit, an integer is
// the exit code itself, and any other object is an error. A program
// stopped on request gets the stop code this way.
mp_obj_t value = mp_obj_exception_get_value(exc);
int exit_code = PBIO_PYBRICKS_EXIT_CODE_OK;
if (mp_obj_is_int(value)) {
exit_code = mp_obj_int_get_truncated(value);
} else if (value != mp_const_none) {
exit_code = PBIO_PYBRICKS_EXIT_CODE_EXCEPTION;
}
// The flag tells callers that the program exited instead of running to
// completion, which an exit code of 0 does not distinguish.
return exit_code | PYEXEC_FORCED_EXIT;
}

if (mp_obj_exception_match(exc, MP_OBJ_FROM_PTR(&mp_type_KeyboardInterrupt))) {
return PBIO_PYBRICKS_EXIT_CODE_INTERRUPTED;
}

return PBIO_PYBRICKS_EXIT_CODE_EXCEPTION;
}

// Prints the exception that ended the program.
static void print_final_exception(mp_obj_t exc, int ret) {

Expand Down Expand Up @@ -98,8 +139,8 @@ static void print_final_exception(mp_obj_t exc, int ret) {
}

#if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL
static void run_repl(void) {
int ret = 0;
static int run_repl(void) {
int ret = PBIO_PYBRICKS_EXIT_CODE_OK;

readline_init0();

Expand All @@ -125,16 +166,19 @@ static void run_repl(void) {
// if vm abort
if (nlr.ret_val == NULL) {
// we are shutting down, so don't bother with cleanup
return;
return PBIO_PYBRICKS_EXIT_CODE_ABORTED;
}

// clear any pending exceptions (and run any callbacks).
mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_CLEAR_EXCEPTIONS);
ret = pyexec_ret_from_exception(MP_OBJ_FROM_PTR(nlr.ret_val));
// Print which exception triggered this.
print_final_exception(MP_OBJ_FROM_PTR(nlr.ret_val), ret);
}

nlr_set_abort(NULL);

return ret;
}
#endif // PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL

Expand Down Expand Up @@ -227,8 +271,8 @@ static void execute_rom_mpy_in_context(mp_module_context_t *module_context, mpy_
/**
* Runs the __main__ module from user RAM.
*/
static void run_user_program(void) {
int ret = 0;
static int run_user_program(void) {
int ret = PBIO_PYBRICKS_EXIT_CODE_OK;

nlr_buf_t nlr;
nlr.ret_val = NULL;
Expand Down Expand Up @@ -257,16 +301,13 @@ static void run_user_program(void) {
// if vm abort
if (nlr.ret_val == NULL) {
// we are shutting down, so don't bother with cleanup
return;
return PBIO_PYBRICKS_EXIT_CODE_ABORTED;
}

// Clear any pending exceptions (and run any callbacks).
mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_CLEAR_EXCEPTIONS);

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))) {
// at the moment, the value of SystemExit is unused
ret = PYEXEC_FORCED_EXIT;
}
ret = pyexec_ret_from_exception(MP_OBJ_FROM_PTR(nlr.ret_val));

print_final_exception(MP_OBJ_FROM_PTR(nlr.ret_val), ret);

Expand All @@ -279,13 +320,15 @@ static void run_user_program(void) {
// but not reset so the user can restart them in the REPL.
pbio_main_soft_stop();

// Enter REPL.
run_repl();
// Enter REPL. Its exit code is now the exit code of the program.
ret = run_repl();
}
#endif // PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL
}

nlr_set_abort(NULL);

return ret;
}

pbio_error_t pbsys_main_program_validate(pbsys_main_program_t *program) {
Expand Down Expand Up @@ -348,7 +391,11 @@ const char *pbsys_main_get_application_version_hash(void) {
}

// Runs MicroPython with the given program data.
void pbsys_main_run_program(pbsys_main_program_t *program) {
uint8_t pbsys_main_run_program(pbsys_main_program_t *program) {

// Return value of the program, which holds the ::pbio_pybricks_exit_code_t
// exit code in its low byte.
int ret = PBIO_PYBRICKS_EXIT_CODE_OK;

#if PBDRV_CONFIG_STACK_EMBEDDED
// Stack limit should be less than real stack size, so we have a chance
Expand Down Expand Up @@ -384,14 +431,14 @@ void pbsys_main_run_program(pbsys_main_program_t *program) {
case PBIO_PYBRICKS_USER_PROGRAM_ID_REPL:
// Run REPL with everything auto-imported.
pb_package_pybricks_init(true);
run_repl();
ret = run_repl();
break;
#endif

#if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_PORT_VIEW && MICROPY_MODULE_FROZEN
case PBIO_PYBRICKS_USER_PROGRAM_ID_PORT_VIEW:
pb_package_pybricks_init(false);
pyexec_frozen_module("_builtin_port_view.py", false);
ret = pyexec_frozen_module("_builtin_port_view.py", false);
break;
#endif

Expand All @@ -404,29 +451,33 @@ void pbsys_main_run_program(pbsys_main_program_t *program) {
#if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_EV3_APPS
case PBIO_PYBRICKS_USER_PROGRAM_ID_EV3_MOTOR_BUTTON_CONTROL:
pb_package_pybricks_init(false);
pyexec_frozen_module("_ev3_motor_button_control.py", false);
ret = pyexec_frozen_module("_ev3_motor_button_control.py", false);
break;
case PBIO_PYBRICKS_USER_PROGRAM_ID_EV3_MOTOR_IR_CONTROL:
pb_package_pybricks_init(false);
pyexec_frozen_module("_ev3_motor_ir_control.py", false);
ret = pyexec_frozen_module("_ev3_motor_ir_control.py", false);
break;
case PBIO_PYBRICKS_USER_PROGRAM_ID_EV3_PORT_VIEW:
pb_package_pybricks_init(false);
pyexec_frozen_module("_ev3_port_view.py", false);
ret = pyexec_frozen_module("_ev3_port_view.py", false);
break;
#endif

default:
// Init Pybricks package without auto-import.
pb_package_pybricks_init(false);
// Run loaded user program (just slot 0 for now).
run_user_program();
ret = run_user_program();
break;
}

// Ensure everything is written before the user application is considered
// done, so that the host does not receive stdout after receiving stop.
pb_stdout_flush_to_new_line();

// Only the exit code is of interest to the system, not the internal
// PYEXEC_FORCED_EXIT flag above it.
return ret & 0xff;
}

void pbsys_main_run_program_cleanup(void) {
Expand Down
3 changes: 3 additions & 0 deletions bricks/_common/mpconfigport.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@
#endif
#define MICROPY_KBD_EXCEPTION (1)
#define MICROPY_ENABLE_VM_ABORT (1)
// Makes pyexec return the value passed to sys.exit() so that it can be
// reported to the host as the program exit code.
#define MICROPY_PYEXEC_ENABLE_EXIT_CODE_HANDLING (1)
#define MICROPY_ENABLE_SCHEDULER (0)
#define MICROPY_PY_INSTANCE_ATTRS (1)

Expand Down
41 changes: 36 additions & 5 deletions lib/pbio/include/pbio/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,14 @@ typedef enum {
* Status report event.
*
* The payload is one 32-bit little-endian unsigned integer containing
* ::pbio_pybricks_status_flags_t flags and a one byte program identifier
* representing the currently active program if it is running.
* ::pbio_pybricks_status_flags_t flags, a one byte program identifier
* representing the currently active program if it is running, a one byte
* identifier of the currently selected program slot, and a one byte exit
* code of the program that ran most recently.
*
* @since Pybricks Profile v1.0.0. Program identifier added in Pybricks Profile v1.4.0.
* @since Pybricks Profile v1.0.0. Program identifier added in Pybricks
* Profile v1.4.0. Slot added in Pybricks Profile v1.5.0. Exit code added
* in Pybricks Profile v1.6.0.
*/
PBIO_PYBRICKS_EVENT_STATUS_REPORT = 0,

Expand Down Expand Up @@ -404,10 +408,37 @@ typedef enum {
*/
#define PBIO_PYBRICKS_STATUS_FLAG(status) (1 << status)

/**
* Well-known exit codes of the program that ran most recently, as reported in
* the status report event.
*
* A program can also exit with a value of its own choosing, so hosts must be
* prepared to receive any value. The values here follow the POSIX convention
* of 128 plus the signal number for programs that did not exit of their own
* accord, so that they don't clash with common program exit codes.
*
* @since Pybricks Profile v1.6.0
*/
typedef enum {
/** The program ran to completion or exited without giving a value. */
PBIO_PYBRICKS_EXIT_CODE_OK = 0,
/** The program ended with an unhandled exception. */
PBIO_PYBRICKS_EXIT_CODE_EXCEPTION = 1,
/** The program was interrupted by the user, such as with Ctrl-C. */
PBIO_PYBRICKS_EXIT_CODE_INTERRUPTED = 128 + 2,
/** The program was aborted because the hub is shutting down. */
PBIO_PYBRICKS_EXIT_CODE_ABORTED = 128 + 9,
/**
* The program was asked to stop, either with the stop button on the hub or
* with ::PBIO_PYBRICKS_COMMAND_STOP_USER_PROGRAM.
*/
PBIO_PYBRICKS_EXIT_CODE_STOPPED = 128 + 15,
} pbio_pybricks_exit_code_t;

/** Size of status report event message in bytes. */
#define PBIO_PYBRICKS_EVENT_STATUS_REPORT_SIZE 7
#define PBIO_PYBRICKS_EVENT_STATUS_REPORT_SIZE 8

uint32_t pbio_pybricks_event_status_report(uint8_t *buf, uint32_t flags, pbio_pybricks_user_program_id_t program_id, uint8_t slot);
uint32_t pbio_pybricks_event_status_report(uint8_t *buf, uint32_t flags, pbio_pybricks_user_program_id_t program_id, uint8_t slot, uint8_t exit_code);

/**
* Application-specific feature flag supported by a hub.
Expand Down
8 changes: 6 additions & 2 deletions lib/pbio/include/pbsys/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,11 @@ pbio_error_t pbsys_main_program_validate(pbsys_main_program_t *program);
* This should be provided by the application running on top of pbio.
*
* @param [in] program Program size and data
* @returns The exit code of the program. This is the value it exited
* with, if any, or one of the well-known
* ::pbio_pybricks_exit_code_t values.
*/
void pbsys_main_run_program(pbsys_main_program_t *program);
uint8_t pbsys_main_run_program(pbsys_main_program_t *program);

/**
* Cleans up after running main application program, such as wiping application
Expand Down Expand Up @@ -169,7 +172,8 @@ static inline pbio_error_t pbsys_main_program_validate(pbsys_main_program_t *pro
return PBIO_ERROR_NOT_SUPPORTED;
}

static inline void pbsys_main_run_program(pbsys_main_program_t *program) {
static inline uint8_t pbsys_main_run_program(pbsys_main_program_t *program) {
return 0;
}

static inline void pbsys_main_stop_program(bool force_stop) {
Expand Down
1 change: 1 addition & 0 deletions lib/pbio/include/pbsys/status.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ void pbsys_status_set_program_id(pbio_pybricks_user_program_id_t program_id);
void pbsys_status_update_emit(void);
void pbsys_status_set(pbio_pybricks_status_flags_t status);
void pbsys_status_clear(pbio_pybricks_status_flags_t status);
void pbsys_status_clear_program_running(uint8_t exit_code);
bool pbsys_status_test(pbio_pybricks_status_flags_t status);
bool pbsys_status_test_debounce(pbio_pybricks_status_flags_t status, bool state, uint32_t ms);
uint32_t pbsys_status_get_flags(void);
Expand Down
5 changes: 4 additions & 1 deletion lib/pbio/src/protocol/pybricks.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,21 @@ _Static_assert(NUM_PBIO_PYBRICKS_STATUS <= sizeof(uint32_t) * 8,
*
* Program ID parameter was added in Pybricks Profile v1.4.0.
* Slot parameter was added in Pybricks Profile v1.5.0.
* Exit code parameter was added in Pybricks Profile v1.6.0.
*
* @param [in] buf The buffer to hold the binary data.
* @param [in] flags The status flags.
* @param [in] program_id Program identifier of currently running program.
* @param [in] slot The currently selected program slot.
* @param [in] exit_code Exit code of the program that ran most recently.
* @return The number of bytes written to @p buf.
*/
uint32_t pbio_pybricks_event_status_report(uint8_t *buf, uint32_t flags, pbio_pybricks_user_program_id_t program_id, uint8_t slot) {
uint32_t pbio_pybricks_event_status_report(uint8_t *buf, uint32_t flags, pbio_pybricks_user_program_id_t program_id, uint8_t slot, uint8_t exit_code) {
buf[0] = PBIO_PYBRICKS_EVENT_STATUS_REPORT;
pbio_set_uint32_le(&buf[1], flags);
buf[5] = program_id;
buf[6] = slot;
buf[7] = exit_code;
return PBIO_PYBRICKS_EVENT_STATUS_REPORT_SIZE;
}

Expand Down
7 changes: 4 additions & 3 deletions lib/pbio/sys/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,14 @@ void pbsys_main(void) {

// Run the main application.
pbio_main_start_application_resources();
pbsys_main_run_program(&program);
uint8_t exit_code = pbsys_main_run_program(&program);

// Stop motors, user animations, user bluetooth activity, etc.
err = pbio_main_stop_application_resources();

// Get system back in idle state.
pbsys_status_clear(PBIO_PYBRICKS_STATUS_USER_PROGRAM_RUNNING);
// Get system back in idle state. This also makes the exit code of the
// program that just ended available to connected hosts.
pbsys_status_clear_program_running(exit_code);
pbsys_host_stdin_set_callback(NULL);
pbsys_program_stop_set_buttons(PBSYS_CONFIG_HMI_STOP_BUTTON);
program.start_request_type = PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_NONE;
Expand Down
Loading