diff --git a/bricks/_common/micropython.c b/bricks/_common/micropython.c index bec3db0d7..7253226c0 100644 --- a/bricks/_common/micropython.c +++ b/bricks/_common/micropython.c @@ -105,16 +105,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 diff --git a/bricks/_common_stm32/mphalport.c b/bricks/_common_stm32/mphalport.c index e7b70a074..d75c79c54 100644 --- a/bricks/_common_stm32/mphalport.c +++ b/bricks/_common_stm32/mphalport.c @@ -38,7 +38,7 @@ 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_host_stdin_get_available()) { ret |= MP_STREAM_POLL_RD; } @@ -51,7 +51,7 @@ int mp_hal_stdin_rx_chr(void) { uint8_t c; // wait for rx interrupt - while (size = 1, pbsys_host_rx(&c, &size) != PBIO_SUCCESS) { + while (size = 1, pbsys_host_stdin_read(&c, &size) != PBIO_SUCCESS) { MICROPY_EVENT_POLL_HOOK } @@ -60,10 +60,22 @@ 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) { + size_t remaining = len; + + while (remaining) { + uint32_t size = remaining; + pbio_error_t err = pbsys_host_stdout_write((const uint8_t *)str, &size); + if (err == PBIO_SUCCESS) { + str += size; + remaining -= size; + } else if (err != PBIO_ERROR_AGAIN) { + // Ignoring error for now. This means stdout is lost if Bluetooth/USB + // is disconnected. + return len - remaining; + } + MICROPY_EVENT_POLL_HOOK } - // Not raising the error. This means stdout lost if host is not connected. return len; } diff --git a/lib/pbio/drv/usb/usb_stm32.c b/lib/pbio/drv/usb/usb_stm32.c index 4fbb08bce..45573ce9f 100644 --- a/lib/pbio/drv/usb/usb_stm32.c +++ b/lib/pbio/drv/usb/usb_stm32.c @@ -39,6 +39,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; @@ -145,8 +146,8 @@ void pbdrv_usb_stm32_handle_vbus_irq(bool active) { * @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,12 +157,13 @@ 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; @@ -174,14 +176,26 @@ 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; +} + +uint32_t pbdrv_usb_stdout_tx_available(void) { + if (!pbdrv_usb_stm32_is_events_subscribed) { + return UINT32_MAX; + } + + if (usb_stdout_sz) { + return 0; + } + + return sizeof(usb_stdout_buf) - 2; // 2 bytes for header } /** @@ -192,6 +206,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 +223,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 +235,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; } @@ -307,7 +327,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 +349,7 @@ PROCESS_THREAD(pbdrv_usb_process, ev, data) { PROCESS_BEGIN(); - etimer_set(&timer, 500); + etimer_set(&status_timer, 500); for (;;) { PROCESS_WAIT_EVENT(); @@ -360,6 +381,12 @@ 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); @@ -376,6 +403,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 +419,32 @@ 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]); + + // REVISIT: we really shouldn't need a status timer on USB since + // it's not a lossy transport. We just need to make sure we send + // status updates when they change and send the current status + // immediately after subscribing to events. + 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..5a2f09435 100644 --- a/lib/pbio/include/pbdrv/usb.h +++ b/lib/pbio/include/pbdrv/usb.h @@ -46,6 +46,16 @@ pbdrv_usb_bcd_t pbdrv_usb_get_bcd(void); */ pbio_error_t pbdrv_usb_stdout_tx(const uint8_t *data, uint32_t *size); +/** + * Gets the number of bytes that can be queued for sending stdout via USB. + * + * Returns UINT32_MAX if there is no USB connection or no app is subscribed to + * stdout. + * + * @return The number of bytes that can be queued. + */ +uint32_t pbdrv_usb_stdout_tx_available(void); + /** * Indicates if the USB stdout stream is idle. * @return true if the USB stdout stream is idle. @@ -62,6 +72,10 @@ static inline pbio_error_t pbdrv_usb_stdout_tx(const uint8_t *data, uint32_t *si return PBIO_SUCCESS; } +static inline uint32_t pbdrv_usb_stdout_tx_available(void) { + return UINT32_MAX; +} + static inline bool pbdrv_usb_stdout_tx_is_idle(void) { return true; } diff --git a/lib/pbio/include/pbsys/host.h b/lib/pbio/include/pbsys/host.h index b3c866b47..efa9c1a65 100644 --- a/lib/pbio/include/pbsys/host.h +++ b/lib/pbio/include/pbsys/host.h @@ -26,33 +26,26 @@ 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); +uint32_t pbsys_host_stdin_get_free(void); +void pbsys_host_stdin_write(const uint8_t *data, uint32_t size); +void pbsys_host_stdin_set_callback(pbsys_host_stdin_event_callback_t callback); +void pbsys_host_stdin_flush(void); +uint32_t pbsys_host_stdin_get_available(void); +pbio_error_t pbsys_host_stdin_read(uint8_t *data, uint32_t *size); +pbio_error_t pbsys_host_stdout_write(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; -} +#define pbsys_host_stdin_get_free() 0 +#define pbsys_host_stdin_write(data, size) { (void)(data); (void)(size); } +#define pbsys_host_stdin_set_callback(callback) { (void)(callback); } +#define pbsys_host_stdin_flush() +#define pbsys_host_stdin_get_available() 0 +#define pbsys_host_stdin_read(data, size) PBIO_ERROR_NOT_SUPPORTED +#define pbsys_host_stdout_write(data, size) { (void)(data); (void)(size); PBIO_ERROR_NOT_SUPPORTED; } +#define pbsys_host_tx_is_idle() false #endif // PBSYS_CONFIG_HOST diff --git a/lib/pbio/platform/city_hub/pbsysconfig.h b/lib/pbio/platform/city_hub/pbsysconfig.h index 005b03d80..929d29f34 100644 --- a/lib/pbio/platform/city_hub/pbsysconfig.h +++ b/lib/pbio/platform/city_hub/pbsysconfig.h @@ -13,6 +13,7 @@ #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) #define PBSYS_CONFIG_HOST (1) +#define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (21) #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..8e07f32db 100644 --- a/lib/pbio/platform/essential_hub/pbsysconfig.h +++ b/lib/pbio/platform/essential_hub/pbsysconfig.h @@ -11,6 +11,7 @@ #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) #define PBSYS_CONFIG_HOST (1) +#define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (64) #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..5cb33f9db 100644 --- a/lib/pbio/platform/ev3/pbsysconfig.h +++ b/lib/pbio/platform/ev3/pbsysconfig.h @@ -7,6 +7,7 @@ #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_HOST_STDIN_BUF_SIZE (21) #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..aee4faa96 100644 --- a/lib/pbio/platform/move_hub/pbsysconfig.h +++ b/lib/pbio/platform/move_hub/pbsysconfig.h @@ -13,6 +13,7 @@ #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) #define PBSYS_CONFIG_HOST (1) +#define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (21) #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..65b9060c9 100644 --- a/lib/pbio/platform/nxt/pbsysconfig.h +++ b/lib/pbio/platform/nxt/pbsysconfig.h @@ -7,6 +7,7 @@ #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_HOST_STDIN_BUF_SIZE (64) #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 61c6d5eb6..cece15f76 100644 --- a/lib/pbio/platform/prime_hub/pbdrvconfig.h +++ b/lib/pbio/platform/prime_hub/pbdrvconfig.h @@ -123,7 +123,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..ad54ab504 100644 --- a/lib/pbio/platform/prime_hub/pbsysconfig.h +++ b/lib/pbio/platform/prime_hub/pbsysconfig.h @@ -13,6 +13,7 @@ #define PBSYS_CONFIG_HMI_NUM_SLOTS (5) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (1) #define PBSYS_CONFIG_HOST (1) +#define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (64) #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..929d29f34 100644 --- a/lib/pbio/platform/technic_hub/pbsysconfig.h +++ b/lib/pbio/platform/technic_hub/pbsysconfig.h @@ -13,6 +13,7 @@ #define PBSYS_CONFIG_HMI_NUM_SLOTS (0) #define PBSYS_CONFIG_HUB_LIGHT_MATRIX (0) #define PBSYS_CONFIG_HOST (1) +#define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (21) #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..69b64b529 100644 --- a/lib/pbio/platform/test/pbsysconfig.h +++ b/lib/pbio/platform/test/pbsysconfig.h @@ -8,6 +8,7 @@ #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_HOST_STDIN_BUF_SIZE (64) #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..2a134ab2e 100644 --- a/lib/pbio/sys/bluetooth.c +++ b/lib/pbio/sys/bluetooth.c @@ -28,10 +28,7 @@ // REVISIT: this can be the negotiated MTU - 3 to allow for better throughput #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 lwrb_t stdout_ring_buf; -static lwrb_t stdin_ring_buf; typedef struct { list_t queue; @@ -57,105 +54,21 @@ void pbsys_bluetooth_init(void) { // enough for two packets, one currently being sent and one to be ready // as soon as the previous one completes + 1 byte for ring buf pointer static uint8_t stdout_buf[MAX_CHAR_SIZE * 2 + 1]; - // enough for one packet received + 1 byte for ring buf pointer - static uint8_t stdin_buf[PBDRV_BLUETOOTH_MAX_MTU_SIZE - 3 + 1]; lwrb_init(&stdout_ring_buf, stdout_buf, PBIO_ARRAY_SIZE(stdout_buf)); - lwrb_init(&stdin_ring_buf, stdin_buf, PBIO_ARRAY_SIZE(stdin_buf)); process_start(&pbsys_bluetooth_process); } -/** - * Gets the number of bytes currently free for writing in stdin. - * @return The number of bytes. - */ -uint32_t pbsys_bluetooth_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 - * pbsys_bluetooth_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 pbsys_bluetooth_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); - } -} - // Public API -/** - * 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) { - 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 pbsys_bluetooth_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 pbsys_bluetooth_rx(uint8_t *data, uint32_t *size) { - // make sure we have a Bluetooth connection - if (!pbdrv_bluetooth_is_connected(PBDRV_BLUETOOTH_CONNECTION_PYBRICKS)) { - return PBIO_ERROR_INVALID_OP; - } - - 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 ::pbsys_bluetooth_rx - * can be used to wait for new data. - */ -void pbsys_bluetooth_rx_flush(void) { - lwrb_reset(&stdin_ring_buf); -} - /** * Queues data to be transmitted via Bluetooth serial port. * @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 +76,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 +92,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 +100,22 @@ 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; +} + +/** + * Gets the number of bytes that can be queued for transmission via Bluetooth. + * + * If there is no connection, this will return %UINT32_MAX. + * + * @returns The number of bytes that can be queued. + */ +uint32_t pbsys_bluetooth_tx_available(void) { + if (!pbdrv_bluetooth_is_connected(PBDRV_BLUETOOTH_CONNECTION_PYBRICKS)) { + return UINT32_MAX; + } + + return lwrb_get_free(&stdout_ring_buf); } /** @@ -247,7 +173,6 @@ static void reset_all(void) { send_busy = false; - lwrb_reset(&stdin_ring_buf); lwrb_reset(&stdout_ring_buf); } diff --git a/lib/pbio/sys/bluetooth.h b/lib/pbio/sys/bluetooth.h index 0c62877d3..947ddb716 100644 --- a/lib/pbio/sys/bluetooth.h +++ b/lib/pbio/sys/bluetooth.h @@ -10,33 +10,25 @@ #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); #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_flush(void); -uint32_t pbsys_bluetooth_rx_get_available(void); -pbio_error_t pbsys_bluetooth_rx(uint8_t *data, uint32_t *size); pbio_error_t pbsys_bluetooth_tx(const uint8_t *data, uint32_t *size); +uint32_t pbsys_bluetooth_tx_available(void); bool pbsys_bluetooth_tx_is_idle(void); #else // PBSYS_CONFIG_BLUETOOTH #define pbsys_bluetooth_init() -#define pbsys_bluetooth_rx_set_callback(callback) -#define pbsys_bluetooth_rx_flush() -#define pbsys_bluetooth_rx_get_available() 0 -static inline pbio_error_t pbsys_bluetooth_rx(uint8_t *data, uint32_t *size) { - return PBIO_ERROR_NOT_SUPPORTED; -} static inline pbio_error_t pbsys_bluetooth_tx(const uint8_t *data, uint32_t *size) { return PBIO_ERROR_NOT_SUPPORTED; } +static inline uint32_t pbsys_bluetooth_tx_available(void) { + return UINT32_MAX; +} static inline bool pbsys_bluetooth_tx_is_idle(void) { return false; } diff --git a/lib/pbio/sys/command.c b/lib/pbio/sys/command.c index 621f1f294..1885e21b0 100644 --- a/lib/pbio/sys/command.c +++ b/lib/pbio/sys/command.c @@ -73,10 +73,10 @@ pbio_pybricks_error_t pbsys_command(const uint8_t *data, uint32_t size) { case PBIO_PYBRICKS_COMMAND_WRITE_STDIN: #if PBSYS_CONFIG_HOST - if (pbsys_host_rx_get_free() < size - 1) { + if (pbsys_host_stdin_get_free() < size - 1) { return PBIO_PYBRICKS_ERROR_BUSY; } - pbsys_host_rx_write(&data[1], size - 1); + pbsys_host_stdin_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/host.c b/lib/pbio/sys/host.c index 9fb9ecb6b..61f904269 100644 --- a/lib/pbio/sys/host.c +++ b/lib/pbio/sys/host.c @@ -5,92 +5,173 @@ #if PBSYS_CONFIG_HOST -#include +#include +#include +#include #include #include "bluetooth.h" +static pbsys_host_stdin_event_callback_t pbsys_host_stdin_event_callback; +static lwrb_t pbsys_host_stdin_ring_buf; + void pbsys_host_init(void) { + static uint8_t stdin_buf[PBSYS_CONFIG_HOST_STDIN_BUF_SIZE]; + lwrb_init(&pbsys_host_stdin_ring_buf, stdin_buf, PBIO_ARRAY_SIZE(stdin_buf)); pbsys_bluetooth_init(); } -void pbsys_host_rx_set_callback(pbsys_host_stdin_event_callback_t callback) { - pbsys_bluetooth_rx_set_callback(callback); +// Publisher APIs. Pybricks Profile connections call these to push data to +// a common stdin buffer. + +/** + * Gets the number of bytes currently free for writing in stdin. + * @return The number of bytes. + */ +uint32_t pbsys_host_stdin_get_free(void) { + return lwrb_get_free(&pbsys_host_stdin_ring_buf); } -void pbsys_host_rx_flush(void) { - pbsys_bluetooth_rx_flush(); +/** + * Writes data to the stdin buffer. + * + * This does not currently return the number of bytes written, so first call + * pbsys_bluetooth_stdin_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 pbsys_host_stdin_write(const uint8_t *data, uint32_t size) { + if (pbsys_host_stdin_event_callback) { + // If there is a callback hook, we have to process things one byte at + // a time. This is needed, e.g. by Micropython to handle Ctrl-C. + for (uint32_t i = 0; i < size; i++) { + if (!pbsys_host_stdin_event_callback(data[i])) { + lwrb_write(&pbsys_host_stdin_ring_buf, &data[i], 1); + } + } + } else { + lwrb_write(&pbsys_host_stdin_ring_buf, data, size); + } } -uint32_t pbsys_host_rx_get_available(void) { - return pbsys_bluetooth_rx_get_available(); +// Consumer APIs. User-facing code calls these to read data from stdin. + +/** + * Sets the host stdin callback function. + * @param callback [in] The callback or NULL. + */ +void pbsys_host_stdin_set_callback(pbsys_host_stdin_event_callback_t callback) { + pbsys_host_stdin_event_callback = callback; } -uint32_t pbsys_host_rx_get_free(void) { - return pbsys_bluetooth_rx_get_free(); +/** + * Flushes data from the stdin buffer without reading it. + */ +void pbsys_host_stdin_flush(void) { + lwrb_reset(&pbsys_host_stdin_ring_buf); } -void pbsys_host_rx_write(const uint8_t *data, uint32_t size) { - pbsys_bluetooth_rx_write(data, size); +/** + * Gets the number of bytes currently available to be read from the host stdin buffer. + * @return The number of bytes. + */ +uint32_t pbsys_host_stdin_get_available(void) { + return lwrb_get_full(&pbsys_host_stdin_ring_buf); } -pbio_error_t pbsys_host_rx(uint8_t *data, uint32_t *size) { - return pbsys_bluetooth_rx(data, size); +/** + * 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 pbsys_host_stdin_read(uint8_t *data, uint32_t *size) { + if ((*size = lwrb_read(&pbsys_host_stdin_ring_buf, data, *size)) == 0) { + return PBIO_ERROR_AGAIN; + } + + return PBIO_SUCCESS; } /** - * Transmits data over Bluetooth and USB. + * Transmits data over any connected transport that is subscribed to Pybricks + * protocol events. * - * Should be called in a loop with the same arguments until it no longer - * returns ::PBIO_ERROR_AGAIN. + * This may perform partial writes. Callers should check the number of bytes + * actually written and call again with the remaining data until all data is + * written. * - * @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. + * @param data [in] The data to transmit. + * @param size [inout] The size of the data to transmit. Upon success, this + * contains the number of bytes actually processed. + * @return ::PBIO_ERROR_INVALID_OP if there is no active transport, + * ::PBIO_ERROR_AGAIN if no @p data could be queued, + * ::PBIO_SUCCESS if at least some data was queued. */ -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 pbsys_host_stdout_write(const uint8_t *data, uint32_t *size) { + #if PBSYS_CONFIG_BLUETOOTH && (!PBDRV_CONFIG_USB || PBDRV_CONFIG_USB_CHARGE_ONLY) + return pbsys_bluetooth_tx(data, size); + #elif !PBSYS_CONFIG_BLUETOOTH && PBDRV_CONFIG_USB && !PBDRV_CONFIG_USB_CHARGE_ONLY + return pbdrv_usb_stdout_tx(data, size); + #elif PBSYS_CONFIG_BLUETOOTH && PBDRV_CONFIG_USB && !PBDRV_CONFIG_USB_CHARGE_ONLY + + uint32_t bt_avail = pbsys_bluetooth_tx_available(); + uint32_t usb_avail = pbdrv_usb_stdout_tx_available(); + uint32_t available = MIN(UINT32_MAX, MIN(bt_avail, usb_avail)); + + // If all tx_available() calls returned UINT32_MAX, then there is one listening. + if (available == UINT32_MAX) { + return PBIO_ERROR_INVALID_OP; } - - 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 one or more tx_available() calls returned 0, then we need to wait. + if (available == 0) { + return PBIO_ERROR_AGAIN; } - 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; - } + // Limit size to smallest available space from all transports so that we + // don't do partial writes to one transport and not the other. + *size = MIN(*size, available); - // 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; - } + // Unless something became disconnected in an interrupt handler, these + // functions should always succeed since we already checked tx_available(). + // And if both somehow got disconnected at the same time, it is not a big deal + // if we return PBIO_SUCCESS without actually sending anything. + (void)pbsys_bluetooth_tx(data, size); + (void)pbdrv_usb_stdout_tx(data, size); + + return PBIO_SUCCESS; - // 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; + #else + // stdout goes to /dev/null return PBIO_SUCCESS; + #endif } +/** + * Checks if all data has been transmitted. + * + * This is used to implement, e.g. a flush() function that blocks until all + * data has been sent. + * + * @return true if all data has been transmitted or no one is + * listening, false if there is still data queued to be sent. + */ bool pbsys_host_tx_is_idle(void) { + #if PBDRV_CONFIG_USB && !PBDRV_CONFIG_USB_CHARGE_ONLY + // The USB part is a bit of a hack since it depends on the USB driver not + // buffering more than one packet at a time to actually be accurate. + return pbsys_bluetooth_tx_is_idle() && pbdrv_usb_stdout_tx_available(); + #else return pbsys_bluetooth_tx_is_idle(); + #endif } #endif // PBSYS_CONFIG_HOST diff --git a/lib/pbio/sys/main.c b/lib/pbio/sys/main.c index 416a65c18..53377329b 100644 --- a/lib/pbio/sys/main.c +++ b/lib/pbio/sys/main.c @@ -105,7 +105,7 @@ 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_host_stdin_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 @@ -113,12 +113,15 @@ int main(int argc, char **argv) { while (pbio_os_run_processes_once()) { } + // Make sure we are starting with an empty stdin buffer. + pbsys_host_stdin_flush(); + // Run the main application. pbsys_main_run_program(&program); // Get system back in idle state. pbsys_status_clear(PBIO_PYBRICKS_STATUS_USER_PROGRAM_RUNNING); - pbsys_host_rx_set_callback(NULL); + pbsys_host_stdin_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/test/sys/test_bluetooth.c b/lib/pbio/test/sys/test_bluetooth.c index 5fb0eb515..17f07f15f 100644 --- a/lib/pbio/test/sys/test_bluetooth.c +++ b/lib/pbio/test/sys/test_bluetooth.c @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -20,7 +21,7 @@ static PT_THREAD(test_bluetooth(struct pt *pt)) { PT_BEGIN(pt); - pbsys_bluetooth_init(); + pbsys_host_init(); // power should be initialized to off tt_want_uint_op(pbio_test_bluetooth_get_control_state(), ==, PBIO_TEST_BLUETOOTH_STATE_OFF); @@ -84,7 +85,7 @@ static PT_THREAD(test_bluetooth(struct pt *pt)) { PT_WAIT_UNTIL(pt, ({ pbio_test_clock_tick(1); size = PBIO_ARRAY_SIZE(rx_data); - pbsys_bluetooth_rx(rx_data, &size) == PBIO_SUCCESS; + pbsys_host_stdin_read(rx_data, &size) == PBIO_SUCCESS; })); tt_want_uint_op(size, ==, strlen("test3\n"));