diff --git a/bricks/_common/micropython.c b/bricks/_common/micropython.c index bec3db0d7..2b26a3a5b 100644 --- a/bricks/_common/micropython.c +++ b/bricks/_common/micropython.c @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -19,6 +20,7 @@ #include #include +#include #include #include "genhdr/mpversion.h" @@ -105,16 +107,30 @@ static void mp_vfs_map_minimal_new_reader(mp_reader_t *reader, mp_vfs_map_minima // Prints the exception that ended the program. static void print_final_exception(mp_obj_t exc, int ret) { - // Handle graceful stop with button. - if ((ret & PYEXEC_FORCED_EXIT) && - mp_obj_exception_match(exc, MP_OBJ_FROM_PTR(&mp_type_SystemExit))) { - mp_printf(&mp_plat_print, "The program was stopped (%q).\n", - ((mp_obj_exception_t *)MP_OBJ_TO_PTR(exc))->base.type->name); - return; - } + nlr_buf_t nlr; + nlr.ret_val = NULL; + + if (nlr_push(&nlr) == 0) { + // Handle graceful stop with button. + if ((ret & PYEXEC_FORCED_EXIT) && + mp_obj_exception_match(exc, MP_OBJ_FROM_PTR(&mp_type_SystemExit))) { + mp_printf(&mp_plat_print, "The program was stopped (%q).\n", + ((mp_obj_exception_t *)MP_OBJ_TO_PTR(exc))->base.type->name); + return; + } - // Print unhandled exception with traceback. - mp_obj_print_exception(&mp_plat_print, exc); + // REVISIT: flash the light red a few times to indicate an unhandled exception? + + // Print unhandled exception with traceback. + mp_obj_print_exception(&mp_plat_print, exc); + + nlr_pop(); + } else { + // If we couldn't print the exception, just return. There is nothing + // else we can do. + + // REVISIT: flash the light with a different pattern here? + } } #if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL @@ -375,6 +391,38 @@ void pbsys_main_run_program(pbsys_main_program_t *program) { // Initialize MicroPython. mp_init(); + #if MICROPY_PY_SYS_MUTABLE_STDIO + + bool use_bluetooth; + + // If the program was started remotely, use the same transport for stdio. + switch (program->start_request_type) { + case PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_BLUETOOTH: + use_bluetooth = true; + break; + case PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_USB: + use_bluetooth = false; + break; + default: + // Use Bluetooth if available, otherwise USB. + // REVISIT: might want to keep track of last used transport and use + // that instead. + use_bluetooth = pbdrv_bluetooth_is_connected(PBDRV_BLUETOOTH_CONNECTION_PYBRICKS); + break; + } + + if (use_bluetooth) { + MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_STDIN]) = MP_OBJ_FROM_PTR(&pb_bluetooth_stdio_wrapper_obj); + MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_STDOUT]) = MP_OBJ_FROM_PTR(&pb_bluetooth_stdio_wrapper_obj); + MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_STDERR]) = MP_OBJ_FROM_PTR(&pb_bluetooth_stdio_wrapper_obj); + } else { + MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_STDIN]) = MP_OBJ_FROM_PTR(&pb_usb_stdio_wrapper_obj); + MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_STDOUT]) = MP_OBJ_FROM_PTR(&pb_usb_stdio_wrapper_obj); + MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_STDERR]) = MP_OBJ_FROM_PTR(&pb_usb_stdio_wrapper_obj); + } + + #endif // MICROPY_PY_SYS_MUTABLE_STDIO + // Runs the requested downloaded or builtin user program. switch (program->id) { diff --git a/bricks/_common/mpconfigport.h b/bricks/_common/mpconfigport.h index e2013c9ba..b12f733a1 100644 --- a/bricks/_common/mpconfigport.h +++ b/bricks/_common/mpconfigport.h @@ -94,9 +94,10 @@ #define MICROPY_PY_REVERSE_SPECIAL_METHODS (PYBRICKS_OPT_EXTRA_LEVEL1) #define MICROPY_PY_SYS_EXIT (0) #define MICROPY_PY_SYS_MODULES (0) -#define MICROPY_PY_SYS_STDFILES (PYBRICKS_OPT_EXTRA_LEVEL1) -#define MICROPY_PY_SYS_STDIO_BUFFER (PYBRICKS_OPT_EXTRA_LEVEL1) -#define MICROPY_PY_SYS_STDIO_FLUSH (PYBRICKS_OPT_EXTRA_LEVEL1) +#define MICROPY_PY_SYS_STDFILES (!PYBRICKS_PY_STDIO && PYBRICKS_OPT_EXTRA_LEVEL1) +#define MICROPY_PY_SYS_STDIO_BUFFER (!PYBRICKS_PY_STDIO && PYBRICKS_OPT_EXTRA_LEVEL1) +#define MICROPY_PY_SYS_STDIO_FLUSH (!PYBRICKS_PY_STDIO && PYBRICKS_OPT_EXTRA_LEVEL1) +#define MICROPY_PY_SYS_MUTABLE_STDIO (PYBRICKS_PY_STDIO) #define MICROPY_PY_RANDOM_EXTRA_FUNCS (PYBRICKS_OPT_EXTRA_LEVEL1) #define MICROPY_PY_RANDOM_SEED_INIT_FUNC ({ extern uint32_t pbdrv_clock_get_us(void); pbdrv_clock_get_us(); }) #define MICROPY_MODULE_BUILTIN_INIT (1) diff --git a/bricks/_common/sources.mk b/bricks/_common/sources.mk index 8ae728a3d..0091bceae 100644 --- a/bricks/_common/sources.mk +++ b/bricks/_common/sources.mk @@ -24,6 +24,7 @@ CONTIKI_SRC_C = $(addprefix lib/contiki-core/,\ PYBRICKS_PYBRICKS_SRC_C = $(addprefix pybricks/,\ common/pb_type_ble.c \ common/pb_type_battery.c \ + common/pb_type_bluetooth_stdio.c \ common/pb_type_charger.c \ common/pb_type_colorlight_external.c \ common/pb_type_colorlight_internal.c \ @@ -39,6 +40,8 @@ PYBRICKS_PYBRICKS_SRC_C = $(addprefix pybricks/,\ common/pb_type_motor.c \ common/pb_type_speaker.c \ common/pb_type_system.c \ + common/pb_type_usb.c \ + common/pb_type_usb_stdio.c \ ev3devices/pb_module_ev3devices.c \ ev3devices/pb_type_ev3devices_colorsensor.c \ ev3devices/pb_type_ev3devices_gyrosensor.c \ @@ -91,6 +94,7 @@ PYBRICKS_PYBRICKS_SRC_C = $(addprefix pybricks/,\ pupdevices/pb_type_pupdevices_tiltsensor.c \ pupdevices/pb_type_pupdevices_ultrasonicsensor.c \ pybricks.c \ + stdio.c \ robotics/pb_module_robotics.c \ robotics/pb_type_car.c \ robotics/pb_type_drivebase.c \ @@ -227,7 +231,6 @@ PBIO_SRC_C = $(addprefix lib/pbio/,\ sys/command.c \ sys/core.c \ sys/hmi.c \ - sys/host.c \ sys/light_matrix.c \ sys/light.c \ sys/main.c \ diff --git a/bricks/_common_stm32/mphalport.c b/bricks/_common_stm32/mphalport.c index 7b94c6bd4..0fc28a065 100644 --- a/bricks/_common_stm32/mphalport.c +++ b/bricks/_common_stm32/mphalport.c @@ -11,7 +11,7 @@ #include #include #include -#include +#include #include "py/runtime.h" #include "py/mphal.h" @@ -43,20 +43,109 @@ void mp_hal_delay_ms(mp_uint_t Delay) { uintptr_t mp_hal_stdio_poll(uintptr_t poll_flags) { uintptr_t ret = 0; - if ((poll_flags & MP_STREAM_POLL_RD) && pbsys_host_rx_get_available()) { + if ((poll_flags & MP_STREAM_POLL_RD) && pbsys_bluetooth_rx_get_available()) { ret |= MP_STREAM_POLL_RD; } return ret; } +#if MICROPY_PY_SYS_MUTABLE_STDIO + +// When MICROPY_PY_SYS_MUTABLE_STDIO is enabled, the relationship between +// sys.stdin/stdout/stderr and mp_hal_stdin/stdout is inverted. In this case, +// calls to mp_hal_stdin/stdout are implemented by calling the currently set +// sys.stdin/stdout objects. + +#include +#include + +// Receive single character +int mp_hal_stdin_rx_chr(void) { + mp_obj_t stdin_obj = MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_STDIN]); + + const mp_stream_p_t *stream_p = mp_get_stream_raise(stdin_obj, MP_STREAM_OP_READ); + + if (stream_p->is_text) { + mp_obj_t buffer_obj; + mp_load_method(stdin_obj, MP_QSTR_buffer, &buffer_obj); + stream_p = mp_get_stream_raise(buffer_obj, MP_STREAM_OP_READ); + } + + if (!stream_p->read) { + pb_assert(PBIO_ERROR_NOT_IMPLEMENTED); + } + + uint8_t c; + int errcode; + mp_uint_t out_sz = stream_p->read(MP_OBJ_FROM_PTR(stream_p), &c, sizeof(c), &errcode); + + if (out_sz == MP_STREAM_ERROR) { + mp_raise_OSError(errcode); + } + + return c; +} + +// Send string of given length +mp_uint_t mp_hal_stdout_tx_strn(const char *str, size_t len) { + mp_obj_t stdout_obj = MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_STDOUT]); + + const mp_stream_p_t *stream_p = mp_get_stream_raise(stdout_obj, MP_STREAM_OP_WRITE); + + if (stream_p->is_text) { + mp_obj_t buffer_obj; + mp_load_method(stdout_obj, MP_QSTR_buffer, &buffer_obj); + stream_p = mp_get_stream_raise(buffer_obj, MP_STREAM_OP_WRITE); + } + + if (!stream_p->write) { + pb_assert(PBIO_ERROR_NOT_IMPLEMENTED); + } + + int errcode; + mp_uint_t out_sz = stream_p->write(MP_OBJ_FROM_PTR(stream_p), str, len, &errcode); + if (out_sz == MP_STREAM_ERROR) { + mp_raise_OSError(errcode); + } + + return out_sz; +} + +void mp_hal_stdout_tx_flush(void) { + mp_obj_t stdout_obj = MP_STATE_VM(sys_mutable[MP_SYS_MUTABLE_STDOUT]); + + const mp_stream_p_t *stream_p = mp_get_stream_raise(stdout_obj, MP_STREAM_OP_IOCTL); + + if (stream_p->is_text) { + mp_obj_t buffer_obj; + mp_load_method(stdout_obj, MP_QSTR_buffer, &buffer_obj); + stream_p = mp_get_stream_raise(buffer_obj, MP_STREAM_OP_IOCTL); + } + + if (!stream_p->ioctl) { + pb_assert(PBIO_ERROR_NOT_IMPLEMENTED); + } + + int errcode; + mp_uint_t ret = stream_p->ioctl(MP_OBJ_FROM_PTR(stream_p), MP_STREAM_FLUSH, 0, &errcode); + if (ret == MP_STREAM_ERROR) { + mp_raise_OSError(errcode); + } +} + +#else // MICROPY_PY_SYS_MUTABLE_STDIO + +// When there is only Bluetooth, we will use the mp_hal to directly connect +// stdin/stdout to Bluetooth to keep the code size small. + // Receive single character int mp_hal_stdin_rx_chr(void) { uint32_t size; uint8_t c; // wait for rx interrupt - while (size = 1, pbsys_host_rx(&c, &size) != PBIO_SUCCESS) { + while (size = 1, pbsys_bluetooth_rx(&c, &size) != PBIO_SUCCESS) { MICROPY_EVENT_POLL_HOOK } @@ -65,16 +154,33 @@ int mp_hal_stdin_rx_chr(void) { // Send string of given length mp_uint_t mp_hal_stdout_tx_strn(const char *str, size_t len) { - while (pbsys_host_tx((const uint8_t *)str, len) == PBIO_ERROR_AGAIN) { + uint32_t remaining = len; + + while (remaining) { + uint32_t size = remaining; + + pbio_error_t err = pbsys_bluetooth_tx((const uint8_t *)str, &size); + if (err == PBIO_SUCCESS) { + str += size; + remaining -= size; + } + MICROPY_EVENT_POLL_HOOK + + if (err != PBIO_ERROR_AGAIN) { + // Ignoring error for now. This means stdout lost if Bluetooth is + // disconnected. + break; + } } - // Not raising the error. This means stdout lost if host is not connected. - return len; + return len - remaining; } void mp_hal_stdout_tx_flush(void) { - while (!pbsys_host_tx_is_idle()) { + while (!pbsys_bluetooth_tx_is_idle()) { MICROPY_EVENT_POLL_HOOK } } + +#endif // MICROPY_PY_SYS_MUTABLE_STDIO diff --git a/bricks/cityhub/mpconfigport.h b/bricks/cityhub/mpconfigport.h index 99f69af52..ba893a385 100644 --- a/bricks/cityhub/mpconfigport.h +++ b/bricks/cityhub/mpconfigport.h @@ -26,6 +26,7 @@ #define PYBRICKS_PY_COMMON_MOTORS (1) #define PYBRICKS_PY_COMMON_SPEAKER (0) #define PYBRICKS_PY_COMMON_SYSTEM (1) +#define PYBRICKS_PY_COMMON_USB (0) #define PYBRICKS_PY_EV3DEVICES (0) #define PYBRICKS_PY_EXPERIMENTAL (1) #define PYBRICKS_PY_HUBS (1) @@ -42,6 +43,7 @@ #define PYBRICKS_PY_ROBOTICS (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_GYRO (0) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (0) +#define PYBRICKS_PY_STDIO (0) #define PYBRICKS_PY_TOOLS (1) #define PYBRICKS_PY_TOOLS_HUB_MENU (0) #define PYBRICKS_PY_TOOLS_APP_DATA (1) diff --git a/bricks/essentialhub/mpconfigport.h b/bricks/essentialhub/mpconfigport.h index 7463b95db..663fa57aa 100644 --- a/bricks/essentialhub/mpconfigport.h +++ b/bricks/essentialhub/mpconfigport.h @@ -27,6 +27,7 @@ #define PYBRICKS_PY_COMMON_MOTORS (1) #define PYBRICKS_PY_COMMON_SPEAKER (0) #define PYBRICKS_PY_COMMON_SYSTEM (1) +#define PYBRICKS_PY_COMMON_USB (1) #define PYBRICKS_PY_EV3DEVICES (0) #define PYBRICKS_PY_EXPERIMENTAL (1) #define PYBRICKS_PY_HUBS (1) @@ -43,6 +44,7 @@ #define PYBRICKS_PY_ROBOTICS (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_GYRO (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (1) +#define PYBRICKS_PY_STDIO (1) #define PYBRICKS_PY_TOOLS (1) #define PYBRICKS_PY_TOOLS_HUB_MENU (0) #define PYBRICKS_PY_TOOLS_APP_DATA (1) diff --git a/bricks/ev3/mpconfigport.h b/bricks/ev3/mpconfigport.h index 5767d7857..a4721eabc 100644 --- a/bricks/ev3/mpconfigport.h +++ b/bricks/ev3/mpconfigport.h @@ -25,6 +25,7 @@ #define PYBRICKS_PY_COMMON_MOTORS (1) #define PYBRICKS_PY_COMMON_SPEAKER (1) #define PYBRICKS_PY_COMMON_SYSTEM (1) +#define PYBRICKS_PY_COMMON_USB (0) #define PYBRICKS_PY_EV3DEVICES (1) #define PYBRICKS_PY_EXPERIMENTAL (1) #define PYBRICKS_PY_HUBS (1) @@ -41,6 +42,7 @@ #define PYBRICKS_PY_ROBOTICS (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_GYRO (0) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (0) +#define PYBRICKS_PY_STDIO (0) #define PYBRICKS_PY_TOOLS (1) #define PYBRICKS_PY_TOOLS_HUB_MENU (0) #define PYBRICKS_PY_TOOLS_APP_DATA (1) diff --git a/bricks/movehub/mpconfigport.h b/bricks/movehub/mpconfigport.h index 580e79d19..b9511966e 100644 --- a/bricks/movehub/mpconfigport.h +++ b/bricks/movehub/mpconfigport.h @@ -25,6 +25,7 @@ #define PYBRICKS_PY_COMMON_MOTOR_MODEL (0) #define PYBRICKS_PY_COMMON_MOTORS (1) #define PYBRICKS_PY_COMMON_SYSTEM (1) +#define PYBRICKS_PY_COMMON_USB (0) #define PYBRICKS_PY_EXPERIMENTAL (0) #define PYBRICKS_PY_HUBS (1) #define PYBRICKS_PY_IODEVICES (0) @@ -39,6 +40,7 @@ #define PYBRICKS_PY_ROBOTICS (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_GYRO (0) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (0) +#define PYBRICKS_PY_STDIO (0) #define PYBRICKS_PY_TOOLS (1) #define PYBRICKS_PY_TOOLS_HUB_MENU (0) #define PYBRICKS_PY_TOOLS_APP_DATA (0) diff --git a/bricks/nxt/mpconfigport.h b/bricks/nxt/mpconfigport.h index 6837749bb..29c4fd4dc 100644 --- a/bricks/nxt/mpconfigport.h +++ b/bricks/nxt/mpconfigport.h @@ -28,6 +28,7 @@ #define PYBRICKS_PY_COMMON_MOTORS (1) #define PYBRICKS_PY_COMMON_SPEAKER (1) #define PYBRICKS_PY_COMMON_SYSTEM (1) +#define PYBRICKS_PY_COMMON_USB (0) #define PYBRICKS_PY_EV3DEVICES (0) #define PYBRICKS_PY_EXPERIMENTAL (0) #define PYBRICKS_PY_HUBS (1) @@ -41,6 +42,7 @@ #define PYBRICKS_PY_ROBOTICS (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_GYRO (0) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (0) +#define PYBRICKS_PY_STDIO (0) #define PYBRICKS_PY_TOOLS (1) #define PYBRICKS_PY_TOOLS_HUB_MENU (0) #define PYBRICKS_PY_TOOLS_APP_DATA (0) diff --git a/bricks/primehub/mpconfigport.h b/bricks/primehub/mpconfigport.h index 42e930da6..c6690a5f9 100644 --- a/bricks/primehub/mpconfigport.h +++ b/bricks/primehub/mpconfigport.h @@ -28,6 +28,7 @@ #define PYBRICKS_PY_COMMON_MOTORS (1) #define PYBRICKS_PY_COMMON_SPEAKER (1) #define PYBRICKS_PY_COMMON_SYSTEM (1) +#define PYBRICKS_PY_COMMON_USB (1) #define PYBRICKS_PY_EV3DEVICES (0) #define PYBRICKS_PY_EXPERIMENTAL (1) #define PYBRICKS_PY_HUBS (1) @@ -44,6 +45,7 @@ #define PYBRICKS_PY_ROBOTICS (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_GYRO (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (1) +#define PYBRICKS_PY_STDIO (1) #define PYBRICKS_PY_TOOLS (1) #define PYBRICKS_PY_TOOLS_HUB_MENU (1) #define PYBRICKS_PY_TOOLS_APP_DATA (1) diff --git a/bricks/technichub/mpconfigport.h b/bricks/technichub/mpconfigport.h index c802e7add..5f8f0d226 100644 --- a/bricks/technichub/mpconfigport.h +++ b/bricks/technichub/mpconfigport.h @@ -26,6 +26,7 @@ #define PYBRICKS_PY_COMMON_MOTORS (1) #define PYBRICKS_PY_COMMON_SPEAKER (0) #define PYBRICKS_PY_COMMON_SYSTEM (1) +#define PYBRICKS_PY_COMMON_USB (0) #define PYBRICKS_PY_EV3DEVICES (0) #define PYBRICKS_PY_EXPERIMENTAL (1) #define PYBRICKS_PY_HUBS (1) @@ -42,6 +43,7 @@ #define PYBRICKS_PY_ROBOTICS (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_GYRO (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (0) +#define PYBRICKS_PY_STDIO (0) #define PYBRICKS_PY_TOOLS (1) #define PYBRICKS_PY_TOOLS_HUB_MENU (0) #define PYBRICKS_PY_TOOLS_APP_DATA (1) diff --git a/bricks/virtualhub/mpconfigvariant.h b/bricks/virtualhub/mpconfigvariant.h index ab4e2cd1a..d7d7dd5c2 100644 --- a/bricks/virtualhub/mpconfigvariant.h +++ b/bricks/virtualhub/mpconfigvariant.h @@ -22,6 +22,7 @@ #define PYBRICKS_PY_COMMON_MOTORS (1) #define PYBRICKS_PY_COMMON_SPEAKER (0) #define PYBRICKS_PY_COMMON_SYSTEM (1) +#define PYBRICKS_PY_COMMON_USB (0) #define PYBRICKS_PY_EV3DEVICES (0) #define PYBRICKS_PY_EXPERIMENTAL (1) #define PYBRICKS_PY_HUBS (1) @@ -36,6 +37,7 @@ #define PYBRICKS_PY_DEVICES (1) #define PYBRICKS_PY_ROBOTICS (1) #define PYBRICKS_PY_ROBOTICS_DRIVEBASE_SPIKE (0) +#define PYBRICKS_PY_STDIO (0) #define PYBRICKS_PY_TOOLS (1) #define PYBRICKS_PY_TOOLS_HUB_MENU (0) #define PYBRICKS_PY_TOOLS_APP_DATA (1) diff --git a/lib/pbio/drv/usb/usb_ev3.c b/lib/pbio/drv/usb/usb_ev3.c index 9ea612417..b2100df3f 100644 --- a/lib/pbio/drv/usb/usb_ev3.c +++ b/lib/pbio/drv/usb/usb_ev3.c @@ -522,6 +522,10 @@ uint32_t pbdrv_usb_rx_data_available(void) { return USBBufferDataAvailable((tUSBBuffer *)&g_sRxBuffer); } +void pbdrv_usb_rx_set_callback(pbdrv_usb_rx_callback_t callback) { + // TODO +} + int32_t pbdrv_usb_get_char(void) { uint8_t c; if (USBBufferRead((tUSBBuffer *)&g_sRxBuffer, &c, 1) > 0) { diff --git a/lib/pbio/drv/usb/usb_stm32.c b/lib/pbio/drv/usb/usb_stm32.c index 4fbb08bce..e8e9fbf27 100644 --- a/lib/pbio/drv/usb/usb_stm32.c +++ b/lib/pbio/drv/usb/usb_stm32.c @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -39,6 +40,7 @@ static volatile uint32_t usb_response_sz; static volatile uint32_t usb_status_sz; static volatile uint32_t usb_stdout_sz; static volatile bool transmitting; +static volatile bool pbdrv_usb_stm32_is_events_subscribed; static USBD_HandleTypeDef husbd; static PCD_HandleTypeDef hpcd; @@ -140,13 +142,93 @@ void pbdrv_usb_stm32_handle_vbus_irq(bool active) { process_poll(&pbdrv_usb_process); } +// REVISIT: this needs to be moved to a common place where it can be shared with Bluetooth +static pbdrv_usb_rx_callback_t stdin_event_callback; +static lwrb_t stdin_ring_buf; + +/** + * Gets the number of bytes currently free for writing in stdin. + * @return The number of bytes. + */ +uint32_t pbdrv_usb_rx_get_free(void) { + return lwrb_get_free(&stdin_ring_buf); +} + +/** + * Writes data to the stdin buffer. + * + * This does not currently return the number of bytes written, so first call + * pbdrv_usb_rx_get_free() to ensure enough free space. + * + * @param [in] data The data to write to the stdin buffer. + * @param [in] size The size of @p data in bytes. + */ +void pbdrv_usb_rx_write(const uint8_t *data, uint32_t size) { + if (stdin_event_callback) { + // If there is a callback hook, we have to process things one byte at + // a time. + for (uint32_t i = 0; i < size; i++) { + if (!stdin_event_callback(data[i])) { + lwrb_write(&stdin_ring_buf, &data[i], 1); + } + } + } else { + lwrb_write(&stdin_ring_buf, data, size); + } +} + +/** + * Sets the UART Rx callback function. + * @param callback [in] The callback or NULL. + */ +void pbdrv_usb_rx_set_callback(pbdrv_usb_rx_callback_t callback) { + stdin_event_callback = callback; +} + +/** + * Gets the number of bytes currently available to be read from the UART Rx + * characteristic. + * @return The number of bytes. + */ +uint32_t pbdrv_usb_rx_get_available(void) { + return lwrb_get_full(&stdin_ring_buf); +} + +/** + * Reads data from the stdin buffer. + * @param data [in] A buffer to receive a copy of the data. + * @param size [in, out] The number of bytes to read (@p data must be at least + * this big). After return @p size contains the number + * of bytes actually read. + * @return ::PBIO_SUCCESS if @p data was read, ::PBIO_ERROR_AGAIN + * if @p data could not be read at this time (i.e. buffer + * is empty), ::PBIO_ERROR_INVALID_OP if there is not an + * active Bluetooth connection or ::PBIO_ERROR_NOT_SUPPORTED + * if this platform does not support Bluetooth. + */ +pbio_error_t pbdrv_usb_rx(uint8_t *data, uint32_t *size) { + if ((*size = lwrb_read(&stdin_ring_buf, data, *size)) == 0) { + return PBIO_ERROR_AGAIN; + } + + return PBIO_SUCCESS; +} + +/** + * Flushes data from the UART Rx characteristic so that ::pbdrv_usb_rx + * can be used to wait for new data. + */ +void pbdrv_usb_rx_flush(void) { + lwrb_reset(&stdin_ring_buf); +} + /** * Queues data to be transmitted via USB. * @param data [in] The data to be sent. * @param size [in, out] The size of @p data in bytes. After return, @p size * contains the number of bytes actually written. - * @return ::PBIO_SUCCESS if all @p data was queued, ::PBIO_ERROR_AGAIN - * if not all @p data could not be queued at this time (e.g. buffer + * @return ::PBIO_SUCCESS if some @p data was queued, ::PBIO_ERROR_AGAIN + * if no @p data could not be queued at this time (e.g. buffer * is full), ::PBIO_ERROR_INVALID_OP if there is not an * active USB connection or ::PBIO_ERROR_NOT_SUPPORTED * if this platform does not support USB. @@ -156,15 +238,15 @@ pbio_error_t pbdrv_usb_stdout_tx(const uint8_t *data, uint32_t *size) { return PBIO_ERROR_NOT_IMPLEMENTED; #endif + if (!pbdrv_usb_stm32_is_events_subscribed) { + // If the app hasn't subscribed to events, we can't send stdout. + return PBIO_ERROR_INVALID_OP; + } + uint8_t *ptr = usb_stdout_buf; uint32_t ptr_len = sizeof(usb_stdout_buf); - uint32_t full_size = *size; - - // TODO: return PBIO_ERROR_INVALID_OP if app flag is not set. Also need a - // timeout in case the app crashes and doesn't clear the flag on exit. if (usb_stdout_sz) { - *size = 0; return PBIO_ERROR_AGAIN; } @@ -174,14 +256,14 @@ pbio_error_t pbdrv_usb_stdout_tx(const uint8_t *data, uint32_t *size) { *ptr++ = PBIO_PYBRICKS_EVENT_WRITE_STDOUT; ptr_len--; - *size = MIN(full_size, ptr_len); + *size = MIN(*size, ptr_len); memcpy(ptr, data, *size); usb_stdout_sz = 1 + 1 + *size; process_poll(&pbdrv_usb_process); - return *size == full_size ? PBIO_SUCCESS : PBIO_ERROR_AGAIN; + return PBIO_SUCCESS; } /** @@ -192,6 +274,14 @@ bool pbdrv_usb_stdout_tx_is_idle(void) { return usb_stdout_sz == 0; } +static void pbdrv_usb_stm32_reset_tx_state(void) { + usb_response_sz = 0; + usb_status_sz = 0; + usb_stdout_sz = 0; + transmitting = false; + pbdrv_usb_stm32_is_events_subscribed = false; +} + /** * @brief Pybricks_Itf_Init * Initializes the Pybricks media low layer @@ -201,10 +291,7 @@ bool pbdrv_usb_stdout_tx_is_idle(void) { static USBD_StatusTypeDef Pybricks_Itf_Init(void) { USBD_Pybricks_SetRxBuffer(&husbd, usb_in_buf); usb_in_sz = 0; - usb_response_sz = 0; - usb_status_sz = 0; - usb_stdout_sz = 0; - transmitting = false; + pbdrv_usb_stm32_reset_tx_state(); return USBD_OK; } @@ -216,6 +303,7 @@ static USBD_StatusTypeDef Pybricks_Itf_Init(void) { * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ static USBD_StatusTypeDef Pybricks_Itf_DeInit(void) { + pbdrv_usb_stm32_reset_tx_state(); return USBD_OK; } @@ -288,6 +376,9 @@ void pbdrv_usb_init(void) { USBD_Start(&husbd); #endif + static uint8_t stdin_buf[USBD_PYBRICKS_MAX_PACKET_SIZE * 2]; + lwrb_init(&stdin_ring_buf, stdin_buf, PBIO_ARRAY_SIZE(stdin_buf)); + process_start(&pbdrv_usb_process); // VBUS may already be active @@ -307,7 +398,8 @@ PROCESS_THREAD(pbdrv_usb_process, ev, data) { static PBIO_ONESHOT(pwrdn_oneshot); static bool bcd_busy; static pbio_pybricks_error_t result; - static struct etimer timer; + static struct etimer status_timer; + static struct etimer transmit_timer; static uint32_t prev_status_flags = ~0; static uint32_t new_status_flags; @@ -328,7 +420,7 @@ PROCESS_THREAD(pbdrv_usb_process, ev, data) { PROCESS_BEGIN(); - etimer_set(&timer, 500); + etimer_set(&status_timer, 500); for (;;) { PROCESS_WAIT_EVENT(); @@ -360,9 +452,15 @@ PROCESS_THREAD(pbdrv_usb_process, ev, data) { if (usb_in_sz) { switch (usb_in_buf[0]) { + case USBD_PYBRICKS_OUT_EP_MSG_SUBSCRIBE: + pbdrv_usb_stm32_is_events_subscribed = usb_in_buf[1]; + usb_response_buf[0] = USBD_PYBRICKS_IN_EP_MSG_RESPONSE; + pbio_set_uint32_le(&usb_response_buf[1], PBIO_PYBRICKS_ERROR_OK); + usb_response_sz = sizeof(usb_response_buf); + break; case USBD_PYBRICKS_OUT_EP_MSG_COMMAND: if (usb_response_sz == 0) { - result = pbsys_command(usb_in_buf + 1, usb_in_sz - 1); + result = pbsys_command(usb_in_buf + 1, usb_in_sz - 1, PBSYS_COMMAND_TRANSPORT_USB); usb_response_buf[0] = USBD_PYBRICKS_IN_EP_MSG_RESPONSE; pbio_set_uint32_le(&usb_response_buf[1], result); usb_response_sz = sizeof(usb_response_buf); @@ -376,6 +474,13 @@ PROCESS_THREAD(pbdrv_usb_process, ev, data) { } if (transmitting) { + if (etimer_expired(&transmit_timer)) { + // Transmission has taken too long, so reset the state to allow + // new transmissions. This can happen if the host stops reading + // data for some reason. + pbdrv_usb_stm32_reset_tx_state(); + } + continue; } @@ -385,20 +490,28 @@ PROCESS_THREAD(pbdrv_usb_process, ev, data) { if (usb_response_sz) { transmitting = true; USBD_Pybricks_TransmitPacket(&husbd, usb_response_buf, usb_response_sz); - } else if ((new_status_flags != prev_status_flags) || etimer_expired(&timer)) { - usb_status_buf[0] = USBD_PYBRICKS_IN_EP_MSG_EVENT; - _Static_assert(sizeof(usb_status_buf) + 1 >= PBIO_PYBRICKS_EVENT_STATUS_REPORT_SIZE, - "size of status report does not match size of event"); - usb_status_sz = 1 + pbsys_status_get_status_report(&usb_status_buf[1]); - - etimer_restart(&timer); - prev_status_flags = new_status_flags; + } else if (pbdrv_usb_stm32_is_events_subscribed) { + if ((new_status_flags != prev_status_flags) || etimer_expired(&status_timer)) { + usb_status_buf[0] = USBD_PYBRICKS_IN_EP_MSG_EVENT; + _Static_assert(sizeof(usb_status_buf) + 1 >= PBIO_PYBRICKS_EVENT_STATUS_REPORT_SIZE, + "size of status report does not match size of event"); + usb_status_sz = 1 + pbsys_status_get_status_report(&usb_status_buf[1]); + + etimer_restart(&status_timer); + prev_status_flags = new_status_flags; + + transmitting = true; + USBD_Pybricks_TransmitPacket(&husbd, usb_status_buf, usb_status_sz); + } else if (usb_stdout_sz) { + transmitting = true; + USBD_Pybricks_TransmitPacket(&husbd, usb_stdout_buf, usb_stdout_sz); + } + } - transmitting = true; - USBD_Pybricks_TransmitPacket(&husbd, usb_status_buf, usb_status_sz); - } else if (usb_stdout_sz) { - transmitting = true; - USBD_Pybricks_TransmitPacket(&husbd, usb_stdout_buf, usb_stdout_sz); + if (transmitting) { + // If the FIFO isn't emptied quickly, then there probably isn't an + // app anymore. This timer is used to detect such a condition. + etimer_set(&transmit_timer, 5); } } diff --git a/lib/pbio/include/pbdrv/usb.h b/lib/pbio/include/pbdrv/usb.h index 3a306be7e..2274c953c 100644 --- a/lib/pbio/include/pbdrv/usb.h +++ b/lib/pbio/include/pbdrv/usb.h @@ -10,6 +10,7 @@ #define _PBDRV_USB_H_ #include +#include #include #include @@ -34,6 +35,15 @@ typedef enum { #if PBDRV_CONFIG_USB +// producer +uint32_t pbdrv_usb_rx_get_free(void); +void pbdrv_usb_rx_write(const uint8_t *data, uint32_t size); +// consumer +typedef bool (*pbdrv_usb_rx_callback_t)(uint8_t c); +void pbdrv_usb_rx_set_callback(pbdrv_usb_rx_callback_t callback); +uint32_t pbdrv_usb_rx_get_available(void); +pbio_error_t pbdrv_usb_rx(uint8_t *data, uint32_t *size); + /** * Gets the result of the USB battery charger detection. * @return The result. @@ -54,17 +64,14 @@ bool pbdrv_usb_stdout_tx_is_idle(void); #else // PBDRV_CONFIG_USB -static inline pbdrv_usb_bcd_t pbdrv_usb_get_bcd(void) { - return PBDRV_USB_BCD_NONE; -} - -static inline pbio_error_t pbdrv_usb_stdout_tx(const uint8_t *data, uint32_t *size) { - return PBIO_SUCCESS; -} - -static inline bool pbdrv_usb_stdout_tx_is_idle(void) { - return true; -} +#define pbdrv_usb_rx_get_free() 0 +#define pbdrv_usb_rx_write(data, size) +#define pbdrv_usb_rx_set_callback(callback) +#define pbdrv_usb_rx_get_available() 0 +#define pbdrv_usb_rx(data, size) PBIO_SUCCESS +#define pbdrv_usb_get_bcd() PBDRV_USB_BCD_NONE +#define pbdrv_usb_stdout_tx(data, size) PBIO_SUCCESS +#define pbdrv_usb_stdout_tx_is_idle() true #endif // PBDRV_CONFIG_USB diff --git a/lib/pbio/sys/bluetooth.h b/lib/pbio/include/pbsys/bluetooth.h similarity index 82% rename from lib/pbio/sys/bluetooth.h rename to lib/pbio/include/pbsys/bluetooth.h index 0c62877d3..bbad5b2a0 100644 --- a/lib/pbio/sys/bluetooth.h +++ b/lib/pbio/include/pbsys/bluetooth.h @@ -1,23 +1,25 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2023 The Pybricks Authors -#ifndef _PBSYS_SYS_BLUETOOTH_H_ -#define _PBSYS_SYS_BLUETOOTH_H_ +#ifndef _PBSYS_BLUETOOTH_H_ +#define _PBSYS_BLUETOOTH_H_ #include #include #include -#include +#include uint32_t pbsys_bluetooth_rx_get_free(void); void pbsys_bluetooth_rx_write(const uint8_t *data, uint32_t size); void pbsys_bluetooth_process_poll(void); +typedef bool (*pbsys_bluetooth_rx_callback_t)(uint8_t c); + #if PBSYS_CONFIG_BLUETOOTH void pbsys_bluetooth_init(void); -void pbsys_bluetooth_rx_set_callback(pbsys_host_stdin_event_callback_t callback); +void pbsys_bluetooth_rx_set_callback(pbsys_bluetooth_rx_callback_t callback); void pbsys_bluetooth_rx_flush(void); uint32_t pbsys_bluetooth_rx_get_available(void); pbio_error_t pbsys_bluetooth_rx(uint8_t *data, uint32_t *size); @@ -43,4 +45,4 @@ static inline bool pbsys_bluetooth_tx_is_idle(void) { #endif // PBSYS_CONFIG_BLUETOOTH -#endif // _PBSYS_SYS_BLUETOOTH_H_ +#endif // _PBSYS_BLUETOOTH_H_ diff --git a/lib/pbio/include/pbsys/command.h b/lib/pbio/include/pbsys/command.h index a193c7d3d..cc451832c 100644 --- a/lib/pbio/include/pbsys/command.h +++ b/lib/pbio/include/pbsys/command.h @@ -16,8 +16,16 @@ #include #include +#include -pbio_pybricks_error_t pbsys_command(const uint8_t *data, uint32_t size); +typedef enum { + // NB: these values allow passing pbsys_command_transport_t directly as + // pbsys_main_program_start_request_type_t without a lookup table. + PBSYS_COMMAND_TRANSPORT_BLE = PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_BLUETOOTH, + PBSYS_COMMAND_TRANSPORT_USB = PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_USB, +} pbsys_command_transport_t; + +pbio_pybricks_error_t pbsys_command(const uint8_t *data, uint32_t size, pbsys_command_transport_t transport); /** * Callback called when the write app data command is given by the host. diff --git a/lib/pbio/include/pbsys/host.h b/lib/pbio/include/pbsys/host.h deleted file mode 100644 index b3c866b47..000000000 --- a/lib/pbio/include/pbsys/host.h +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2025 The Pybricks Authors - -/** - * @addtogroup SystemHost System: Host - * @{ - */ - -#ifndef _PBSYS_HOST_H_ -#define _PBSYS_HOST_H_ - -#include -#include - -#include -#include - -/** - * Callback function to handle stdin events. - * @param [in] c the character received - * @return *true* if the character was handled and should not be placed - * in the stdin buffer, otherwise *false*. - */ -typedef bool (*pbsys_host_stdin_event_callback_t)(uint8_t c); - -#if PBSYS_CONFIG_HOST - -void pbsys_host_init(void); -void pbsys_host_rx_set_callback(pbsys_host_stdin_event_callback_t callback); -void pbsys_host_rx_flush(void); -uint32_t pbsys_host_rx_get_available(void); -uint32_t pbsys_host_rx_get_free(void); -void pbsys_host_rx_write(const uint8_t *data, uint32_t size); -pbio_error_t pbsys_host_rx(uint8_t *data, uint32_t *size); -pbio_error_t pbsys_host_tx(const uint8_t *data, uint32_t size); -bool pbsys_host_tx_is_idle(void); - -#else // PBSYS_CONFIG_HOST - -#define pbsys_host_init() -#define pbsys_host_rx_set_callback(callback) -#define pbsys_host_rx_flush() -#define pbsys_host_rx_get_available() 0 -#define pbsys_host_rx_get_free() 0 -#define pbsys_host_rx_write(data, size) - -static inline pbio_error_t pbsys_host_rx(uint8_t *data, uint32_t *size) { - return PBIO_ERROR_NOT_SUPPORTED; -} -static inline pbio_error_t pbsys_host_tx(const uint8_t *data, uint32_t size) { - return PBIO_ERROR_NOT_SUPPORTED; -} -static inline bool pbsys_host_tx_is_idle(void) { - return false; -} - -#endif // PBSYS_CONFIG_HOST - -#endif // _PBSYS_HOST_H_ - -/** @} */ diff --git a/lib/pbio/include/pbsys/main.h b/lib/pbio/include/pbsys/main.h index 9acefc643..31754be5c 100644 --- a/lib/pbio/include/pbsys/main.h +++ b/lib/pbio/include/pbsys/main.h @@ -36,8 +36,18 @@ typedef enum { PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_HUB_UI = 2, /** * The program was requested to start remotely, such as with an IDE. + * + * @deprecated Use transport-specific items instead. */ PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_REMOTE = 3, + /** + * The program was requested to start using Bluetooth. + */ + PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_BLUETOOTH = 4, + /** + * The program was requested to start using USB. + */ + PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_USB = 5, } pbsys_main_program_start_request_type_t; /** diff --git a/lib/pbio/platform/city_hub/pbsysconfig.h b/lib/pbio/platform/city_hub/pbsysconfig.h index 005b03d80..6e878eda8 100644 --- a/lib/pbio/platform/city_hub/pbsysconfig.h +++ b/lib/pbio/platform/city_hub/pbsysconfig.h @@ -12,7 +12,6 @@ #define PBSYS_CONFIG_BLUETOOTH (1) #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) -#define PBSYS_CONFIG_HOST (1) #define PBSYS_CONFIG_MAIN (1) #define PBSYS_CONFIG_STORAGE (1) #define PBSYS_CONFIG_STORAGE_NUM_SLOTS (1) diff --git a/lib/pbio/platform/essential_hub/pbdrvconfig.h b/lib/pbio/platform/essential_hub/pbdrvconfig.h index 4fc41e004..3930106ed 100644 --- a/lib/pbio/platform/essential_hub/pbdrvconfig.h +++ b/lib/pbio/platform/essential_hub/pbdrvconfig.h @@ -103,7 +103,7 @@ #define PBDRV_CONFIG_USB_MFG_STR LEGO_USB_MFG_STR #define PBDRV_CONFIG_USB_PROD_STR LEGO_USB_PROD_STR_TECHNIC_SMALL_HUB " + Pybricks" #define PBDRV_CONFIG_USB_STM32F4 (1) -#define PBDRV_CONFIG_USB_CHARGE_ONLY (1) +#define PBDRV_CONFIG_USB_CHARGE_ONLY (0) #define PBDRV_CONFIG_STACK (1) #define PBDRV_CONFIG_STACK_EMBEDDED (1) diff --git a/lib/pbio/platform/essential_hub/pbsysconfig.h b/lib/pbio/platform/essential_hub/pbsysconfig.h index 3dd060606..4cbb58350 100644 --- a/lib/pbio/platform/essential_hub/pbsysconfig.h +++ b/lib/pbio/platform/essential_hub/pbsysconfig.h @@ -10,7 +10,6 @@ #define PBSYS_CONFIG_BLUETOOTH (1) #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) -#define PBSYS_CONFIG_HOST (1) #define PBSYS_CONFIG_MAIN (1) #define PBSYS_CONFIG_STORAGE (1) #define PBSYS_CONFIG_STORAGE_NUM_SLOTS (1) diff --git a/lib/pbio/platform/ev3/pbsysconfig.h b/lib/pbio/platform/ev3/pbsysconfig.h index 83a1c4660..5aaf55738 100644 --- a/lib/pbio/platform/ev3/pbsysconfig.h +++ b/lib/pbio/platform/ev3/pbsysconfig.h @@ -6,7 +6,6 @@ #define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_IMU_CALIBRATION (0) #define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6 (1) #define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6_3_NATIVE (0) -#define PBSYS_CONFIG_HOST (1) #define PBSYS_CONFIG_MAIN (1) #define PBSYS_CONFIG_STORAGE (1) #define PBSYS_CONFIG_STORAGE_NUM_SLOTS (1) diff --git a/lib/pbio/platform/move_hub/pbsysconfig.h b/lib/pbio/platform/move_hub/pbsysconfig.h index 9a29265dc..010f462e6 100644 --- a/lib/pbio/platform/move_hub/pbsysconfig.h +++ b/lib/pbio/platform/move_hub/pbsysconfig.h @@ -12,7 +12,6 @@ #define PBSYS_CONFIG_BLUETOOTH (1) #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) -#define PBSYS_CONFIG_HOST (1) #define PBSYS_CONFIG_MAIN (1) #define PBSYS_CONFIG_STORAGE (1) #define PBSYS_CONFIG_STORAGE_NUM_SLOTS (1) diff --git a/lib/pbio/platform/nxt/pbsysconfig.h b/lib/pbio/platform/nxt/pbsysconfig.h index d30fa1802..6a7c86598 100644 --- a/lib/pbio/platform/nxt/pbsysconfig.h +++ b/lib/pbio/platform/nxt/pbsysconfig.h @@ -6,7 +6,6 @@ #define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_IMU_CALIBRATION (0) #define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6 (1) #define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6_3_NATIVE (0) -#define PBSYS_CONFIG_HOST (1) #define PBSYS_CONFIG_MAIN (1) #define PBSYS_CONFIG_STORAGE (1) #define PBSYS_CONFIG_STORAGE_NUM_SLOTS (1) diff --git a/lib/pbio/platform/prime_hub/pbdrvconfig.h b/lib/pbio/platform/prime_hub/pbdrvconfig.h index c6717d4d4..86d0e925c 100644 --- a/lib/pbio/platform/prime_hub/pbdrvconfig.h +++ b/lib/pbio/platform/prime_hub/pbdrvconfig.h @@ -120,7 +120,7 @@ #define PBDRV_CONFIG_USB_PROD_STR LEGO_USB_PROD_STR_TECHNIC_LARGE_HUB " + Pybricks" #define PBDRV_CONFIG_USB_STM32F4 (1) #define PBDRV_CONFIG_USB_STM32F4_HUB_VARIANT_ADDR 0x08007d80 -#define PBDRV_CONFIG_USB_CHARGE_ONLY (1) +#define PBDRV_CONFIG_USB_CHARGE_ONLY (0) #define PBDRV_CONFIG_STACK (1) #define PBDRV_CONFIG_STACK_EMBEDDED (1) diff --git a/lib/pbio/platform/prime_hub/pbsysconfig.h b/lib/pbio/platform/prime_hub/pbsysconfig.h index a1b6c1f88..8c468714e 100644 --- a/lib/pbio/platform/prime_hub/pbsysconfig.h +++ b/lib/pbio/platform/prime_hub/pbsysconfig.h @@ -12,7 +12,6 @@ #define PBSYS_CONFIG_BLUETOOTH_TOGGLE_BUTTON (512) // PBIO_BUTTON_RIGHT_UP, but enum value cannot be used here. #define PBSYS_CONFIG_HMI_NUM_SLOTS (5) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (1) -#define PBSYS_CONFIG_HOST (1) #define PBSYS_CONFIG_MAIN (1) #define PBSYS_CONFIG_STORAGE (1) #define PBSYS_CONFIG_STORAGE_NUM_SLOTS (5) diff --git a/lib/pbio/platform/technic_hub/pbsysconfig.h b/lib/pbio/platform/technic_hub/pbsysconfig.h index 005b03d80..6e878eda8 100644 --- a/lib/pbio/platform/technic_hub/pbsysconfig.h +++ b/lib/pbio/platform/technic_hub/pbsysconfig.h @@ -12,7 +12,6 @@ #define PBSYS_CONFIG_BLUETOOTH (1) #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) -#define PBSYS_CONFIG_HOST (1) #define PBSYS_CONFIG_MAIN (1) #define PBSYS_CONFIG_STORAGE (1) #define PBSYS_CONFIG_STORAGE_NUM_SLOTS (1) diff --git a/lib/pbio/platform/test/pbsysconfig.h b/lib/pbio/platform/test/pbsysconfig.h index 7e347f057..bbbc323e0 100644 --- a/lib/pbio/platform/test/pbsysconfig.h +++ b/lib/pbio/platform/test/pbsysconfig.h @@ -7,7 +7,6 @@ #define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6 (0) #define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6_3_NATIVE (0) #define PBSYS_CONFIG_BLUETOOTH (1) -#define PBSYS_CONFIG_HOST (1) #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) #define PBSYS_CONFIG_MAIN (0) diff --git a/lib/pbio/sys/bluetooth.c b/lib/pbio/sys/bluetooth.c index d1599841c..825d40af1 100644 --- a/lib/pbio/sys/bluetooth.c +++ b/lib/pbio/sys/bluetooth.c @@ -17,7 +17,7 @@ #include #include #include -#include +#include #include #include #include @@ -29,7 +29,7 @@ #define MAX_CHAR_SIZE 20 // REVISIT: this needs to be moved to a common place where it can be shared with USB -static pbsys_host_stdin_event_callback_t stdin_event_callback; +static pbsys_bluetooth_rx_callback_t stdin_event_callback; static lwrb_t stdout_ring_buf; static lwrb_t stdin_ring_buf; @@ -103,7 +103,7 @@ void pbsys_bluetooth_rx_write(const uint8_t *data, uint32_t size) { * Sets the UART Rx callback function. * @param callback [in] The callback or NULL. */ -void pbsys_bluetooth_rx_set_callback(pbsys_host_stdin_event_callback_t callback) { +void pbsys_bluetooth_rx_set_callback(pbsys_bluetooth_rx_callback_t callback) { stdin_event_callback = callback; } @@ -154,8 +154,8 @@ void pbsys_bluetooth_rx_flush(void) { * @param data [in] The data to be sent. * @param size [in, out] The size of @p data in bytes. After return, @p size * contains the number of bytes actually written. - * @return ::PBIO_SUCCESS if all @p data was queued, - * ::PBIO_ERROR_AGAIN if not all @p data could not be + * @return ::PBIO_SUCCESS if some @p data was queued, + * ::PBIO_ERROR_AGAIN if no @p data could be * queued at this time (e.g. buffer is full), * ::PBIO_ERROR_INVALID_OP if there is not an * active Bluetooth connection or ::PBIO_ERROR_NOT_SUPPORTED @@ -163,8 +163,6 @@ void pbsys_bluetooth_rx_flush(void) { */ pbio_error_t pbsys_bluetooth_tx(const uint8_t *data, uint32_t *size) { - uint32_t full_size = *size; - // make sure we have a Bluetooth connection if (!pbdrv_bluetooth_is_connected(PBDRV_BLUETOOTH_CONNECTION_PYBRICKS)) { return PBIO_ERROR_INVALID_OP; @@ -181,7 +179,7 @@ pbio_error_t pbsys_bluetooth_tx(const uint8_t *data, uint32_t *size) { stdout_msg.is_queued = true; } - if ((*size = lwrb_write(&stdout_ring_buf, data, full_size)) == 0) { + if ((*size = lwrb_write(&stdout_ring_buf, data, *size)) == 0) { return PBIO_ERROR_AGAIN; } @@ -189,7 +187,7 @@ pbio_error_t pbsys_bluetooth_tx(const uint8_t *data, uint32_t *size) { // MAX_CHAR_SIZE bytes before actually transmitting pbsys_bluetooth_process_poll(); - return *size == full_size ? PBIO_SUCCESS : PBIO_ERROR_AGAIN; + return PBIO_SUCCESS; } /** @@ -211,7 +209,7 @@ bool pbsys_bluetooth_tx_is_idle(void) { static pbio_pybricks_error_t handle_receive(pbdrv_bluetooth_connection_t connection, const uint8_t *data, uint32_t size) { if (connection == PBDRV_BLUETOOTH_CONNECTION_PYBRICKS) { - return pbsys_command(data, size); + return pbsys_command(data, size, PBSYS_COMMAND_TRANSPORT_BLE); } if (connection == PBDRV_BLUETOOTH_CONNECTION_UART) { diff --git a/lib/pbio/sys/command.c b/lib/pbio/sys/command.c index 621f1f294..722ac7426 100644 --- a/lib/pbio/sys/command.c +++ b/lib/pbio/sys/command.c @@ -5,10 +5,11 @@ #include #include +#include #include +#include #include #include -#include #include #include "./hmi.h" @@ -30,8 +31,9 @@ void pbsys_command_set_write_app_data_callback(pbsys_command_write_app_data_call * Parses binary data for command and dispatches handler for command. * @param [in] data The raw command data. * @param [in] size The size of @p data in bytes. + * @param [in] transport The transport over which the command was received. */ -pbio_pybricks_error_t pbsys_command(const uint8_t *data, uint32_t size) { +pbio_pybricks_error_t pbsys_command(const uint8_t *data, uint32_t size, pbsys_command_transport_t transport) { assert(data); assert(size); @@ -49,14 +51,14 @@ pbio_pybricks_error_t pbsys_command(const uint8_t *data, uint32_t size) { } // Use payload as program ID, otherwise use active user slot. return pbio_pybricks_error_from_pbio_error( - pbsys_main_program_request_start((size == 2 ? data[1] : pbsys_hmi_get_selected_program_slot()), PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_REMOTE)); + pbsys_main_program_request_start((size == 2 ? data[1] : pbsys_hmi_get_selected_program_slot()), (pbsys_main_program_start_request_type_t)transport)); } #if PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL case PBIO_PYBRICKS_COMMAND_START_REPL: // Deprecated. For backwards compatibility with Pybricks // Profile < v1.4.0, make it work anyway. return pbio_pybricks_error_from_pbio_error( - pbsys_main_program_request_start(PBIO_PYBRICKS_USER_PROGRAM_ID_REPL, PBSYS_MAIN_PROGRAM_START_REQUEST_TYPE_REMOTE)); + pbsys_main_program_request_start(PBIO_PYBRICKS_USER_PROGRAM_ID_REPL, (pbsys_main_program_start_request_type_t)transport)); #endif // PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_REPL case PBIO_PYBRICKS_COMMAND_WRITE_USER_PROGRAM_META: @@ -72,12 +74,26 @@ pbio_pybricks_error_t pbsys_command(const uint8_t *data, uint32_t size) { return PBIO_PYBRICKS_ERROR_OK; case PBIO_PYBRICKS_COMMAND_WRITE_STDIN: - #if PBSYS_CONFIG_HOST - if (pbsys_host_rx_get_free() < size - 1) { - return PBIO_PYBRICKS_ERROR_BUSY; + switch (transport) { + #if PBSYS_CONFIG_BLUETOOTH + case PBSYS_COMMAND_TRANSPORT_BLE: + if (pbsys_bluetooth_rx_get_free() < size - 1) { + return PBIO_PYBRICKS_ERROR_BUSY; + } + pbsys_bluetooth_rx_write(&data[1], size - 1); + break; + #endif + #if PBDRV_CONFIG_USB + case PBSYS_COMMAND_TRANSPORT_USB: + if (pbdrv_usb_rx_get_free() < size - 1) { + return PBIO_PYBRICKS_ERROR_BUSY; + } + pbdrv_usb_rx_write(&data[1], size - 1); + break; + #endif + default: + break; } - pbsys_host_rx_write(&data[1], size - 1); - #endif // If no consumers are configured, goes to "/dev/null" without error return PBIO_PYBRICKS_ERROR_OK; diff --git a/lib/pbio/sys/core.c b/lib/pbio/sys/core.c index 26d29a02d..30ff78e8e 100644 --- a/lib/pbio/sys/core.c +++ b/lib/pbio/sys/core.c @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include "core.h" @@ -50,7 +50,7 @@ void pbsys_init(void) { pbsys_storage_init(); pbsys_battery_init(); - pbsys_host_init(); + pbsys_bluetooth_init(); pbsys_hmi_init(); process_start(&pbsys_system_process); diff --git a/lib/pbio/sys/host.c b/lib/pbio/sys/host.c deleted file mode 100644 index 9fb9ecb6b..000000000 --- a/lib/pbio/sys/host.c +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2020-2023 The Pybricks Authors - -#include - -#if PBSYS_CONFIG_HOST - -#include - -#include - -#include "bluetooth.h" - -void pbsys_host_init(void) { - pbsys_bluetooth_init(); -} - -void pbsys_host_rx_set_callback(pbsys_host_stdin_event_callback_t callback) { - pbsys_bluetooth_rx_set_callback(callback); -} - -void pbsys_host_rx_flush(void) { - pbsys_bluetooth_rx_flush(); -} - -uint32_t pbsys_host_rx_get_available(void) { - return pbsys_bluetooth_rx_get_available(); -} - -uint32_t pbsys_host_rx_get_free(void) { - return pbsys_bluetooth_rx_get_free(); -} - -void pbsys_host_rx_write(const uint8_t *data, uint32_t size) { - pbsys_bluetooth_rx_write(data, size); -} - -pbio_error_t pbsys_host_rx(uint8_t *data, uint32_t *size) { - return pbsys_bluetooth_rx(data, size); -} - -/** - * Transmits data over Bluetooth and USB. - * - * Should be called in a loop with the same arguments until it no longer - * returns ::PBIO_ERROR_AGAIN. - * - * @param data [in] The data to transmit. - * @param size [in] The size of the data to transmit. - * @return ::PBIO_ERROR_AGAIN if the data is still being transmitted - * ::PBIO_SUCCESS if complete or failed. - */ -pbio_error_t pbsys_host_tx(const uint8_t *data, uint32_t size) { - - static bool transmitting = false; - static uint32_t tx_done_ble; - static uint32_t tx_done_usb; - - if (!transmitting) { - tx_done_ble = 0; - tx_done_usb = 0; - transmitting = true; - } - - pbio_error_t err_ble = PBIO_SUCCESS; - pbio_error_t err_usb = PBIO_SUCCESS; - uint32_t size_now; - - if (tx_done_ble < size) { - size_now = size - tx_done_ble; - err_ble = pbsys_bluetooth_tx(data + tx_done_ble, &size_now); - tx_done_ble += size_now; - } - - if (tx_done_usb < size) { - size_now = size - tx_done_usb; - err_usb = pbdrv_usb_stdout_tx(data + tx_done_usb, &size_now); - tx_done_usb += size_now; - } - - // Keep waiting as long as at least has not completed or errored. - if (err_ble == PBIO_ERROR_AGAIN || err_usb == PBIO_ERROR_AGAIN) { - return PBIO_ERROR_AGAIN; - } - - // Both of them are either complete or failed. The caller of this function - // does not currently raise errors, so we just return success. - transmitting = false; - return PBIO_SUCCESS; -} - -bool pbsys_host_tx_is_idle(void) { - return pbsys_bluetooth_tx_is_idle(); -} - -#endif // PBSYS_CONFIG_HOST diff --git a/lib/pbio/sys/main.c b/lib/pbio/sys/main.c index 416a65c18..b31f70d8b 100644 --- a/lib/pbio/sys/main.c +++ b/lib/pbio/sys/main.c @@ -20,8 +20,8 @@ #include "light_matrix.h" #include "program_stop.h" #include "storage.h" +#include #include -#include // Singleton with information about the currently (or soon) active program. static pbsys_main_program_t program; @@ -105,7 +105,8 @@ int main(int argc, char **argv) { // Prepare pbsys for running the program. pbsys_status_set_program_id(program.id); pbsys_status_set(PBIO_PYBRICKS_STATUS_USER_PROGRAM_RUNNING); - pbsys_host_rx_set_callback(pbsys_main_stdin_event); + pbsys_bluetooth_rx_set_callback(pbsys_main_stdin_event); + pbdrv_usb_rx_set_callback(pbsys_main_stdin_event); pbsys_hub_light_matrix_handle_user_program_start(true); // Handle pending events triggered by the status change, such as @@ -118,7 +119,8 @@ int main(int argc, char **argv) { // Get system back in idle state. pbsys_status_clear(PBIO_PYBRICKS_STATUS_USER_PROGRAM_RUNNING); - pbsys_host_rx_set_callback(NULL); + pbdrv_usb_rx_set_callback(NULL); + pbsys_bluetooth_rx_set_callback(NULL); pbsys_program_stop_set_buttons(PBIO_BUTTON_CENTER); pbsys_hub_light_matrix_handle_user_program_start(false); pbio_stop_all(true); diff --git a/lib/pbio/sys/storage_settings.c b/lib/pbio/sys/storage_settings.c index c63c0cbf1..ff758c516 100644 --- a/lib/pbio/sys/storage_settings.c +++ b/lib/pbio/sys/storage_settings.c @@ -10,14 +10,13 @@ #include #include - #include #include +#include #include #include #include -#include "bluetooth.h" #include "storage.h" /** diff --git a/lib/pbio/test/sys/test_bluetooth.c b/lib/pbio/test/sys/test_bluetooth.c index 5fb0eb515..277f968d8 100644 --- a/lib/pbio/test/sys/test_bluetooth.c +++ b/lib/pbio/test/sys/test_bluetooth.c @@ -10,12 +10,12 @@ #include #include +#include #include #include #include #include "../drv/clock/clock_test.h" -#include "../../sys/bluetooth.h" static PT_THREAD(test_bluetooth(struct pt *pt)) { PT_BEGIN(pt); diff --git a/micropython b/micropython index f1ab67781..8aabb78ff 160000 --- a/micropython +++ b/micropython @@ -1 +1 @@ -Subproject commit f1ab67781818ed9c9cf8e1c9650619d56a22b6c0 +Subproject commit 8aabb78ffdd9c57c3cb299239acdf481feb500c3 diff --git a/pybricks/common.h b/pybricks/common.h index 46c4d580d..367f419f8 100644 --- a/pybricks/common.h +++ b/pybricks/common.h @@ -145,6 +145,10 @@ extern const mp_obj_module_t pb_type_System; #endif // PYBRICKS_PY_COMMON_SYSTEM +#if PYBRICKS_PY_COMMON_USB +mp_obj_t pb_type_USB_new(void); +#endif // PYBRICKS_PY_COMMON_USB + #endif // PYBRICKS_PY_COMMON #endif // PYBRICKS_INCLUDED_PYBRICKS_COMMON_H diff --git a/pybricks/common/pb_type_ble.c b/pybricks/common/pb_type_ble.c index 348a58de4..02f68a54e 100644 --- a/pybricks/common/pb_type_ble.c +++ b/pybricks/common/pb_type_ble.c @@ -20,6 +20,7 @@ #include "py/runtime.h" #include +#include #include #include #include @@ -527,6 +528,9 @@ static const mp_rom_map_elem_t common_BLE_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_observe_enable), MP_ROM_PTR(&pb_module_ble_observe_enable_obj) }, { MP_ROM_QSTR(MP_QSTR_signal_strength), MP_ROM_PTR(&pb_module_ble_signal_strength_obj) }, { MP_ROM_QSTR(MP_QSTR_version), MP_ROM_PTR(&pb_module_ble_version_obj) }, + #if PYBRICKS_PY_STDIO + { MP_ROM_QSTR(MP_QSTR_io), MP_ROM_PTR(&pb_bluetooth_stdio_wrapper_obj) }, + #endif }; static MP_DEFINE_CONST_DICT(common_BLE_locals_dict, common_BLE_locals_dict_table); diff --git a/pybricks/common/pb_type_bluetooth_stdio.c b/pybricks/common/pb_type_bluetooth_stdio.c new file mode 100644 index 000000000..60fb4aa33 --- /dev/null +++ b/pybricks/common/pb_type_bluetooth_stdio.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors +// +// Python stream object that uses Pybricks Profile stdio over Bluetooth. + +#include +#include + +#include +#include + +#include "py/mpconfig.h" +#include "py/runtime.h" +#include "py/stream.h" + +#if PYBRICKS_PY_STDIO && PBSYS_CONFIG_BLUETOOTH + +typedef struct { + mp_obj_base_t base; +} pb_stdio_obj_t; + +static mp_uint_t pb_bluetooth_stdio_read(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode) { + uint32_t remaining = size; + + for (;;) { + uint32_t chunk = remaining; + + pbio_error_t err = pbsys_bluetooth_rx(buf, &chunk); + if (err == PBIO_SUCCESS) { + buf += chunk; + remaining -= chunk; + + if (!remaining) { + return size; + } + } else if (err == PBIO_ERROR_INVALID_OP) { + // For backwards compatibility, don't raise error if not connected, + // just block forever. + // REVISIT, could add an attribute to control this behavior. + } else if (err != PBIO_ERROR_AGAIN) { + *errcode = pb_errcode_from_pbio_error(err); + return MP_STREAM_ERROR; + } + + MICROPY_EVENT_POLL_HOOK + } +} + +static mp_uint_t pb_bluetooth_stdio_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) { + uint32_t remaining = size; + + while (remaining) { + uint32_t chunk = remaining; + + pbio_error_t err = pbsys_bluetooth_tx(buf, &chunk); + if (err == PBIO_SUCCESS) { + buf += chunk; + remaining -= chunk; + } else if (err == PBIO_ERROR_INVALID_OP) { + // For backwards compatibility, if not connected, send to /dev/null. + // REVISIT, could add an attribute to control this behavior. + return size; + } else if (err != PBIO_ERROR_AGAIN) { + *errcode = pb_errcode_from_pbio_error(err); + return MP_STREAM_ERROR; + } + + MICROPY_EVENT_POLL_HOOK + + } + + return size - remaining; +} + +static mp_uint_t pb_bluetooth_stdio_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) { + switch (request) { + case MP_STREAM_POLL: { + mp_uint_t ret = 0; + + if ((arg & MP_STREAM_POLL_RD) && pbsys_bluetooth_rx_get_available()) { + ret |= MP_STREAM_POLL_RD; + } + + if ((arg & MP_STREAM_POLL_WR) && pbsys_bluetooth_tx_is_idle()) { + ret |= MP_STREAM_POLL_WR; + } + + return ret; + } + case MP_STREAM_CLOSE: + return 0; + case MP_STREAM_FLUSH: + while (!pbsys_bluetooth_tx_is_idle()) { + MICROPY_EVENT_POLL_HOOK + } + return 0; + default: + break; + } + + *errcode = MP_EINVAL; + return MP_STREAM_ERROR; +} + +static const mp_stream_p_t pb_bluetooth_stdio_obj_stream_p = { + .read = pb_bluetooth_stdio_read, + .write = pb_bluetooth_stdio_write, + .ioctl = pb_bluetooth_stdio_ioctl, + .is_text = false, +}; + +static MP_DEFINE_CONST_OBJ_TYPE( + pb_bluetooth_stdio_obj_type, + MP_QSTR_BluetoothStdio, + MP_TYPE_FLAG_ITER_IS_STREAM, + protocol, &pb_bluetooth_stdio_obj_stream_p, + locals_dict, &pb_stdio_locals_dict); + +static const pb_stdio_obj_t pb_bluetooth_stdio_obj = { + .base = { .type = &pb_bluetooth_stdio_obj_type } +}; + +const pb_text_io_wrapper_t pb_bluetooth_stdio_wrapper_obj = { + .base = { .type = &pb_text_io_wrapper_obj_type }, + .binary_stream_obj = MP_OBJ_FROM_PTR(&pb_bluetooth_stdio_obj), + .binary_stream_p = &pb_bluetooth_stdio_obj_stream_p, +}; + +#endif // PYBRICKS_PY_STDIO && PBSYS_CONFIG_BLUETOOTH diff --git a/pybricks/common/pb_type_usb.c b/pybricks/common/pb_type_usb.c new file mode 100644 index 000000000..c55780a66 --- /dev/null +++ b/pybricks/common/pb_type_usb.c @@ -0,0 +1,43 @@ + +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +#include "py/mpconfig.h" + +#if PYBRICKS_PY_COMMON_USB + +#include "py/obj.h" +#include "py/runtime.h" + +#include + +typedef struct { + mp_obj_base_t base; +} pb_obj_USB_t; + +static const mp_rom_map_elem_t common_USB_locals_dict_table[] = { + #if PYBRICKS_PY_STDIO + { MP_ROM_QSTR(MP_QSTR_io), MP_ROM_PTR(&pb_usb_stdio_wrapper_obj) }, + #endif +}; +static MP_DEFINE_CONST_DICT(common_USB_locals_dict, common_USB_locals_dict_table); + +static MP_DEFINE_CONST_OBJ_TYPE(pb_type_USB, + MP_QSTR_USB, + MP_TYPE_FLAG_NONE, + locals_dict, &common_USB_locals_dict); + +static const pb_obj_USB_t pb_type_USB_instance = { + .base = { .type = &pb_type_USB }, +}; + +/** + * Creates a new instance of the USB class. + * + * @returns A signle instance of the USB class. + */ +mp_obj_t pb_type_USB_new(void) { + return MP_OBJ_FROM_PTR(&pb_type_USB_instance); +} + +#endif // PYBRICKS_PY_COMMON_USB diff --git a/pybricks/common/pb_type_usb_stdio.c b/pybricks/common/pb_type_usb_stdio.c new file mode 100644 index 000000000..a3e4e8118 --- /dev/null +++ b/pybricks/common/pb_type_usb_stdio.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors +// +// Python stream object that uses Pybricks Profile stdio over USB. + +#include +#include + +#include +#include + +#include "py/mpconfig.h" +#include "py/runtime.h" +#include "py/stream.h" + +#if PYBRICKS_PY_STDIO && PBDRV_CONFIG_USB + +typedef struct { + mp_obj_base_t base; +} pb_stdio_obj_t; + +static mp_uint_t pb_usb_stdio_read(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode) { + uint32_t remaining = size; + + for (;;) { + uint32_t chunk = remaining; + + pbio_error_t err = pbdrv_usb_rx(buf, &chunk); + if (err == PBIO_SUCCESS) { + buf += chunk; + remaining -= chunk; + + if (!remaining) { + return size; + } + } else if (err == PBIO_ERROR_INVALID_OP) { + // For backwards compatibility, don't raise error if not connected, + // just block forever. + // REVISIT, could add an attribute to control this behavior. + } else if (err != PBIO_ERROR_AGAIN) { + *errcode = pb_errcode_from_pbio_error(err); + return MP_STREAM_ERROR; + } + + MICROPY_EVENT_POLL_HOOK + } +} + +static mp_uint_t pb_usb_stdio_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) { + uint32_t remaining = size; + + while (remaining) { + uint32_t chunk = remaining; + + pbio_error_t err = pbdrv_usb_stdout_tx(buf, &chunk); + if (err == PBIO_SUCCESS) { + buf += chunk; + remaining -= chunk; + } else if (err == PBIO_ERROR_INVALID_OP) { + // For backwards compatibility, if not connected, send to /dev/null. + // REVISIT, could add an attribute to control this behavior. + return size; + } else if (err != PBIO_ERROR_AGAIN) { + *errcode = pb_errcode_from_pbio_error(err); + return MP_STREAM_ERROR; + } + + MICROPY_EVENT_POLL_HOOK + + } + + return size - remaining; +} + +static mp_uint_t pb_usb_stdio_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) { + switch (request) { + case MP_STREAM_POLL: { + mp_uint_t ret = 0; + + if ((arg & MP_STREAM_POLL_RD) && pbdrv_usb_rx_get_available()) { + ret |= MP_STREAM_POLL_RD; + } + + if ((arg & MP_STREAM_POLL_WR) && pbdrv_usb_stdout_tx_is_idle()) { + ret |= MP_STREAM_POLL_WR; + } + + return ret; + } + case MP_STREAM_CLOSE: + return 0; + case MP_STREAM_FLUSH: + while (!pbdrv_usb_stdout_tx_is_idle()) { + MICROPY_EVENT_POLL_HOOK + } + return 0; + default: + break; + } + + *errcode = MP_EINVAL; + return MP_STREAM_ERROR; +} + +static const mp_stream_p_t pb_usb_stdio_obj_stream_p = { + .read = pb_usb_stdio_read, + .write = pb_usb_stdio_write, + .ioctl = pb_usb_stdio_ioctl, + .is_text = false, +}; + +static MP_DEFINE_CONST_OBJ_TYPE( + pb_usb_stdio_obj_type, + MP_QSTR_USBStdio, + MP_TYPE_FLAG_ITER_IS_STREAM, + protocol, &pb_usb_stdio_obj_stream_p, + locals_dict, &pb_stdio_locals_dict); + +static const pb_stdio_obj_t pb_usb_stdio_obj = { + .base = { .type = &pb_usb_stdio_obj_type } +}; + +const pb_text_io_wrapper_t pb_usb_stdio_wrapper_obj = { + .base = { .type = &pb_text_io_wrapper_obj_type }, + .binary_stream_obj = MP_OBJ_FROM_PTR(&pb_usb_stdio_obj), + .binary_stream_p = &pb_usb_stdio_obj_stream_p, +}; + +#endif // PYBRICKS_PY_STDIO && PBSYS_CONFIG_USB diff --git a/pybricks/hubs/pb_type_essentialhub.c b/pybricks/hubs/pb_type_essentialhub.c index 610307df5..ad1236217 100644 --- a/pybricks/hubs/pb_type_essentialhub.c +++ b/pybricks/hubs/pb_type_essentialhub.c @@ -30,6 +30,9 @@ typedef struct _hubs_EssentialHub_obj_t { #if PYBRICKS_PY_COMMON_BLE mp_obj_t ble; #endif + #if PYBRICKS_PY_COMMON_USB + mp_obj_t usb; + #endif mp_obj_t buttons; mp_obj_t charger; mp_obj_t imu; @@ -52,6 +55,9 @@ static mp_obj_t hubs_EssentialHub_make_new(const mp_obj_type_t *type, size_t n_a #if PYBRICKS_PY_COMMON_BLE self->ble = pb_type_BLE_new(broadcast_channel_in, observe_channels_in); #endif + #if PYBRICKS_PY_COMMON_USB + self->usb = pb_type_USB_new(); + #endif self->buttons = pb_type_Keypad_obj_new(pb_type_button_pressed_hub_single_button); self->charger = pb_type_Charger_obj_new(); self->imu = pb_type_IMU_obj_new(MP_OBJ_FROM_PTR(self), top_side_in, front_side_in); @@ -65,6 +71,9 @@ static const pb_attr_dict_entry_t hubs_EssentialHub_attr_dict[] = { #if PYBRICKS_PY_COMMON_BLE PB_DEFINE_CONST_ATTR_RO(MP_QSTR_ble, hubs_EssentialHub_obj_t, ble), #endif + #if PYBRICKS_PY_COMMON_USB + PB_DEFINE_CONST_ATTR_RO(MP_QSTR_usb, hubs_EssentialHub_obj_t, usb), + #endif PB_DEFINE_CONST_ATTR_RO(MP_QSTR_buttons, hubs_EssentialHub_obj_t, buttons), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_button, hubs_EssentialHub_obj_t, buttons), // backwards compatibility PB_DEFINE_CONST_ATTR_RO(MP_QSTR_charger, hubs_EssentialHub_obj_t, charger), diff --git a/pybricks/hubs/pb_type_primehub.c b/pybricks/hubs/pb_type_primehub.c index 2032a5544..2b23c421b 100644 --- a/pybricks/hubs/pb_type_primehub.c +++ b/pybricks/hubs/pb_type_primehub.c @@ -32,6 +32,9 @@ typedef struct _hubs_PrimeHub_obj_t { #if PYBRICKS_PY_COMMON_BLE mp_obj_t ble; #endif + #if PYBRICKS_PY_COMMON_USB + mp_obj_t usb; + #endif mp_obj_t buttons; mp_obj_t charger; mp_obj_t display; @@ -76,6 +79,9 @@ static mp_obj_t hubs_PrimeHub_make_new(const mp_obj_type_t *type, size_t n_args, #if PYBRICKS_PY_COMMON_BLE self->ble = pb_type_BLE_new(broadcast_channel_in, observe_channels_in); #endif + #if PYBRICKS_PY_COMMON_USB + self->usb = pb_type_USB_new(); + #endif self->buttons = pb_type_Keypad_obj_new(pb_type_primehub_button_pressed); self->charger = pb_type_Charger_obj_new(); self->display = pb_type_LightMatrix_obj_new(pbsys_hub_light_matrix); @@ -91,6 +97,9 @@ static const pb_attr_dict_entry_t hubs_PrimeHub_attr_dict[] = { #if PYBRICKS_PY_COMMON_BLE PB_DEFINE_CONST_ATTR_RO(MP_QSTR_ble, hubs_PrimeHub_obj_t, ble), #endif + #if PYBRICKS_PY_COMMON_USB + PB_DEFINE_CONST_ATTR_RO(MP_QSTR_usb, hubs_PrimeHub_obj_t, usb), + #endif PB_DEFINE_CONST_ATTR_RO(MP_QSTR_buttons, hubs_PrimeHub_obj_t, buttons), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_charger, hubs_PrimeHub_obj_t, charger), PB_DEFINE_CONST_ATTR_RO(MP_QSTR_display, hubs_PrimeHub_obj_t, display), diff --git a/pybricks/pybricks.c b/pybricks/pybricks.c index a4c2e0055..e72149b42 100644 --- a/pybricks/pybricks.c +++ b/pybricks/pybricks.c @@ -12,7 +12,7 @@ #include #include -#include +#include #include #include @@ -207,7 +207,7 @@ void pb_package_pybricks_deinit(void) { // tasks and deinit tasks queued before it have completed. static pbio_task_t noop_task; pbdrv_bluetooth_queue_noop(&noop_task); - while (noop_task.status == PBIO_ERROR_AGAIN || !pbsys_host_tx_is_idle()) { + while (noop_task.status == PBIO_ERROR_AGAIN || !pbsys_bluetooth_tx_is_idle()) { MICROPY_VM_HOOK_LOOP // Stop waiting (and potentially blocking) in case of forced shutdown. diff --git a/pybricks/stdio.c b/pybricks/stdio.c new file mode 100644 index 000000000..6620b1305 --- /dev/null +++ b/pybricks/stdio.c @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors +// +// Pybricks custom implementation of replaceable stdio streams for MicroPython. +// This is used for sys.stdin, sys.stdout, and sys.stderr. + +#include "py/mpconfig.h" +#include "py/runtime.h" +#include "py/stream.h" + +#include + +#if PYBRICKS_PY_STDIO + +// This text wrapper class wraps a binary stream and handles line endings so +// that it interacts properly when connected to a terminal program. + +// REVIST: Would be nice to implement proper unicode support. + +static void pb_text_io_wrapper_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + pb_text_io_wrapper_t *self = MP_OBJ_TO_PTR(self_in); + + if (dest[0] == MP_OBJ_NULL) { + if (attr == MP_QSTR_buffer) { + dest[0] = self->binary_stream_obj; + } else { + // Continue lookup in locals_dict. + dest[1] = MP_OBJ_SENTINEL; + } + } +} + +static mp_uint_t pb_text_io_wrapper_read(mp_obj_t self_in, void *buf, mp_uint_t size, int *errcode) { + pb_text_io_wrapper_t *self = MP_OBJ_TO_PTR(self_in); + + for (uint i = 0; i < size; i++) { + byte c; + mp_uint_t ret = self->binary_stream_p->read(self->binary_stream_obj, &c, 1, errcode); + if (ret == MP_STREAM_ERROR) { + return MP_STREAM_ERROR; + } + if (ret == 0) { + return i; + } + if (c == '\r') { + c = '\n'; + } + ((byte *)buf)[i] = c; + } + + return size; +} + +static mp_uint_t pb_text_io_wrapper_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) { + pb_text_io_wrapper_t *self = MP_OBJ_TO_PTR(self_in); + + const char *last = buf; + const char *str = buf; + mp_uint_t len = size; + mp_uint_t ret; + + while (len--) { + if (*str == '\n') { + if (str > last) { + ret = self->binary_stream_p->write(self->binary_stream_obj, last, str - last, errcode); + if (ret == MP_STREAM_ERROR) { + return MP_STREAM_ERROR; + } + if (ret == 0) { + return size - len; + } + } + + ret = self->binary_stream_p->write(self->binary_stream_obj, "\r\n", 2, errcode); + if (ret == MP_STREAM_ERROR) { + return MP_STREAM_ERROR; + } + if (ret == 0) { + return size - len; + } + + str++; + last = str; + } else { + str++; + } + } + + if (str > last) { + ret = self->binary_stream_p->write(self->binary_stream_obj, last, str - last, errcode); + if (ret == MP_STREAM_ERROR) { + return MP_STREAM_ERROR; + } + if (ret == 0) { + return size - len; + } + } + + return size; +} + +static mp_uint_t pb_text_io_wrapper_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) { + pb_text_io_wrapper_t *self = MP_OBJ_TO_PTR(self_in); + + // IOCTLS can just be passed through to the underlying binary stream. + return self->binary_stream_p->ioctl(self->binary_stream_obj, request, arg, errcode); +} + +static const mp_stream_p_t pb_text_io_wrapper_stream_p = { + .read = pb_text_io_wrapper_read, + .write = pb_text_io_wrapper_write, + .ioctl = pb_text_io_wrapper_ioctl, + .is_text = true, +}; + +// This dict is shared with all Pybricks stdio-like objects. +static const mp_rom_map_elem_t pb_stdio_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_stream_read_obj) }, + { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&mp_stream_readinto_obj) }, + { MP_ROM_QSTR(MP_QSTR_readline), MP_ROM_PTR(&mp_stream_unbuffered_readline_obj)}, + { MP_ROM_QSTR(MP_QSTR_readlines), MP_ROM_PTR(&mp_stream_unbuffered_readlines_obj)}, + { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) }, + { MP_ROM_QSTR(MP_QSTR_flush), MP_ROM_PTR(&mp_stream_flush_obj) }, + { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&mp_identity_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&mp_identity_obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&mp_stream___exit___obj) }, +}; +MP_DEFINE_CONST_DICT(pb_stdio_locals_dict, pb_stdio_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + pb_text_io_wrapper_obj_type, + MP_QSTR_TextIOWrapper, + MP_TYPE_FLAG_ITER_IS_STREAM, + attr, pb_text_io_wrapper_attr, + protocol, &pb_text_io_wrapper_stream_p, + locals_dict, &pb_stdio_locals_dict); + +#endif // PYBRICKS_PY_STDIO diff --git a/pybricks/stdio.h b/pybricks/stdio.h new file mode 100644 index 000000000..20f671364 --- /dev/null +++ b/pybricks/stdio.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 The Pybricks Authors + +#ifndef _PYBRICKS_STDIO_H_ +#define _PYBRICKS_STDIO_H_ + +#include "py/mpconfig.h" + +#if PYBRICKS_PY_STDIO + +#include "py/obj.h" +#include "py/stream.h" + +typedef struct { + mp_obj_base_t base; + mp_obj_t binary_stream_obj; + const mp_stream_p_t *binary_stream_p; +} pb_text_io_wrapper_t; + +extern const mp_obj_type_t pb_text_io_wrapper_obj_type; +extern const mp_obj_dict_t pb_stdio_locals_dict; + +extern const pb_text_io_wrapper_t pb_bluetooth_stdio_wrapper_obj; +extern const pb_text_io_wrapper_t pb_usb_stdio_wrapper_obj; + +#endif // PYBRICKS_PY_STDIO + +#endif // _PYBRICKS_STDIO_H_ diff --git a/pybricks/util_pb/pb_error.c b/pybricks/util_pb/pb_error.c index 23340eea0..3ca06489d 100644 --- a/pybricks/util_pb/pb_error.c +++ b/pybricks/util_pb/pb_error.c @@ -9,6 +9,37 @@ #include +int pb_errcode_from_pbio_error(pbio_error_t error) { + switch (error) { + case PBIO_SUCCESS: + return 0; + case PBIO_ERROR_FAILED: + return MP_EFAULT; + case PBIO_ERROR_INVALID_ARG: + return MP_EINVAL; + case PBIO_ERROR_NOT_IMPLEMENTED: + return MP_ENOENT; + case PBIO_ERROR_IO: + return MP_EIO; + case PBIO_ERROR_BUSY: + return MP_EBUSY; + case PBIO_ERROR_NO_DEV: + return MP_ENODEV; + case PBIO_ERROR_NOT_SUPPORTED: + return MP_EOPNOTSUPP; + case PBIO_ERROR_AGAIN: + return MP_EAGAIN; + case PBIO_ERROR_INVALID_OP: + return MP_EPERM; + case PBIO_ERROR_TIMEDOUT: + return MP_ETIMEDOUT; + case PBIO_ERROR_CANCELED: + return MP_ECANCELED; + } + // This should never happen, but if it does, return a generic error. + return MP_EFAULT; +} + /** * Raise an exception if @p error is not ::PBIO_SUCCESS. Most errors translate * to an OSError with the appropriate error code. There are a few special diff --git a/pybricks/util_pb/pb_error.h b/pybricks/util_pb/pb_error.h index 471d2f7f1..1672672ff 100644 --- a/pybricks/util_pb/pb_error.h +++ b/pybricks/util_pb/pb_error.h @@ -6,6 +6,7 @@ #include +int pb_errcode_from_pbio_error(pbio_error_t error); void pb_assert(pbio_error_t error); #endif // _PYBRICKS_EXTMOD_PB_ERROR_H_