diff --git a/CHANGELOG.md b/CHANGELOG.md index 67011bc14..34b9c9033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/bricks/_common/micropython.c b/bricks/_common/micropython.c index dd01f63cc..1e55ec5a5 100644 --- a/bricks/_common/micropython.c +++ b/bricks/_common/micropython.c @@ -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)); } @@ -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) { @@ -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(); @@ -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 @@ -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; @@ -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); @@ -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) { @@ -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 @@ -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 @@ -404,15 +451,15 @@ 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 @@ -420,13 +467,17 @@ void pbsys_main_run_program(pbsys_main_program_t *program) { // 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) { diff --git a/bricks/_common/mpconfigport.h b/bricks/_common/mpconfigport.h index 8b77e2826..801bdf9d9 100644 --- a/bricks/_common/mpconfigport.h +++ b/bricks/_common/mpconfigport.h @@ -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) diff --git a/lib/pbio/include/pbio/protocol.h b/lib/pbio/include/pbio/protocol.h index b11865a2b..e1cfe4eb0 100644 --- a/lib/pbio/include/pbio/protocol.h +++ b/lib/pbio/include/pbio/protocol.h @@ -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, @@ -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. diff --git a/lib/pbio/include/pbsys/main.h b/lib/pbio/include/pbsys/main.h index 967b3fab9..065f1918e 100644 --- a/lib/pbio/include/pbsys/main.h +++ b/lib/pbio/include/pbsys/main.h @@ -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 @@ -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) { diff --git a/lib/pbio/include/pbsys/status.h b/lib/pbio/include/pbsys/status.h index 2c6f7b155..0b035715a 100644 --- a/lib/pbio/include/pbsys/status.h +++ b/lib/pbio/include/pbsys/status.h @@ -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); diff --git a/lib/pbio/src/protocol/pybricks.c b/lib/pbio/src/protocol/pybricks.c index 196fe6e10..baf036fb9 100644 --- a/lib/pbio/src/protocol/pybricks.c +++ b/lib/pbio/src/protocol/pybricks.c @@ -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; } diff --git a/lib/pbio/sys/main.c b/lib/pbio/sys/main.c index 11897feb9..a2cc7fdb2 100644 --- a/lib/pbio/sys/main.c +++ b/lib/pbio/sys/main.c @@ -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; diff --git a/lib/pbio/sys/status.c b/lib/pbio/sys/status.c index 8c0551fdb..2b2d65cf9 100644 --- a/lib/pbio/sys/status.c +++ b/lib/pbio/sys/status.c @@ -27,6 +27,8 @@ static struct { pbio_pybricks_user_program_id_t program_id; /** Currently selected program slot */ pbio_pybricks_user_program_id_t slot; + /** Exit code of the program that ran most recently. */ + uint8_t program_exit_code; } pbsys_status; /** @@ -35,7 +37,7 @@ static struct { void pbsys_status_update_emit(void) { uint8_t buf[PBIO_PYBRICKS_EVENT_STATUS_REPORT_SIZE]; - pbio_pybricks_event_status_report(buf, pbsys_status.flags, pbsys_status.program_id, pbsys_status.slot); + pbio_pybricks_event_status_report(buf, pbsys_status.flags, pbsys_status.program_id, pbsys_status.slot, pbsys_status.program_exit_code); pbsys_host_schedule_status_update(buf); // Other processes may be awaiting status changes, so poll. @@ -145,6 +147,21 @@ void pbsys_status_clear(pbio_pybricks_status_flags_t status) { pbsys_status_update_flag(status, false); } +/** + * Clears the user program running status indication and records the exit code + * of the program that just ended. + * + * The exit code is set before the flag is cleared so that hosts receive both + * in the same status update, meaning that the exit code is always up to date + * by the time they see that the program is no longer running. + * + * @param [in] exit_code The exit code of the program that just ended. + */ +void pbsys_status_clear_program_running(uint8_t exit_code) { + pbsys_status.program_exit_code = exit_code; + pbsys_status_update_flag(PBIO_PYBRICKS_STATUS_USER_PROGRAM_RUNNING, false); +} + /** * Tests if status indication is set. * @param [in] status The status indication to to test.