#include "cli_vcp.h" #include "shell/cli_shell.h" #include #include #include #include #include #define TAG "CliVcp" #define USB_CDC_PKT_LEN CDC_DATA_SZ #define VCP_BUF_SIZE (USB_CDC_PKT_LEN * 3) #define VCP_IF_NUM 0 #define VCP_MESSAGE_Q_LEN 8 #ifdef CLI_VCP_TRACE #define VCP_TRACE(...) FURI_LOG_T(__VA_ARGS__) #else #define VCP_TRACE(...) #endif typedef struct { enum { CliVcpMessageTypeEnable, CliVcpMessageTypeDisable, } type; union {}; } CliVcpMessage; typedef enum { CliVcpInternalEventConnected = (1 << 0), CliVcpInternalEventDisconnected = (1 << 1), CliVcpInternalEventTxDone = (1 << 2), CliVcpInternalEventRx = (1 << 3), } CliVcpInternalEvent; #define CliVcpInternalEventAll \ (CliVcpInternalEventConnected | CliVcpInternalEventDisconnected | CliVcpInternalEventTxDone | \ CliVcpInternalEventRx) struct CliVcp { FuriEventLoop* event_loop; FuriMessageQueue* message_queue; // is_currently_transmitting) return; if(!cli_vcp->own_pipe) return; uint8_t buf[USB_CDC_PKT_LEN]; size_t to_receive_from_pipe = MIN(sizeof(buf), pipe_bytes_available(cli_vcp->own_pipe)); size_t length = pipe_receive(cli_vcp->own_pipe, buf, to_receive_from_pipe); if(length > 0 || cli_vcp->previous_tx_length == USB_CDC_PKT_LEN) { VCP_TRACE(TAG, "cdc_send length=%zu", length); cli_vcp->is_currently_transmitting = true; furi_hal_cdc_send(VCP_IF_NUM, buf, length); } cli_vcp->previous_tx_length = length; } /** * Called in the following cases: * - new data arrived at the endpoint; * - data was read out of the pipe. */ static void cli_vcp_maybe_receive_data(CliVcp* cli_vcp) { if(!cli_vcp->own_pipe) return; if(pipe_spaces_available(cli_vcp->own_pipe) < USB_CDC_PKT_LEN) return; uint8_t buf[USB_CDC_PKT_LEN]; size_t length = furi_hal_cdc_receive(VCP_IF_NUM, buf, sizeof(buf)); VCP_TRACE(TAG, "cdc_receive length=%zu", length); furi_check(pipe_send(cli_vcp->own_pipe, buf, length) == length); } // ============= // CDC callbacks // ============= static void cli_vcp_signal_internal_event(CliVcp* cli_vcp, CliVcpInternalEvent event) { furi_thread_flags_set(cli_vcp->thread_id, event); } static void cli_vcp_cdc_tx_done(void* context) { CliVcp* cli_vcp = context; cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventTxDone); } static void cli_vcp_cdc_rx(void* context) { CliVcp* cli_vcp = context; cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventRx); } static void cli_vcp_cdc_state_callback(void* context, CdcState state) { CliVcp* cli_vcp = context; if(state == CdcStateDisconnected) { cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventDisconnected); } // `Connected` events are generated by DTR going active } static void cli_vcp_cdc_ctrl_line_callback(void* context, CdcCtrlLine ctrl_lines) { CliVcp* cli_vcp = context; if(ctrl_lines & CdcCtrlLineDTR) { cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventConnected); } else { cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventDisconnected); } } static CdcCallbacks cdc_callbacks = { .tx_ep_callback = cli_vcp_cdc_tx_done, .rx_ep_callback = cli_vcp_cdc_rx, .state_callback = cli_vcp_cdc_state_callback, .ctrl_line_callback = cli_vcp_cdc_ctrl_line_callback, .config_callback = NULL, }; // ====================== // Pipe callback handlers // ====================== static void cli_vcp_data_from_shell(PipeSide* pipe, void* context) { UNUSED(pipe); CliVcp* cli_vcp = context; cli_vcp_maybe_send_data(cli_vcp); } static void cli_vcp_shell_ready(PipeSide* pipe, void* context) { UNUSED(pipe); CliVcp* cli_vcp = context; cli_vcp_maybe_receive_data(cli_vcp); } /** * Processes messages arriving from other threads */ static void cli_vcp_message_received(FuriEventLoopObject* object, void* context) { CliVcp* cli_vcp = context; CliVcpMessage message; furi_check(furi_message_queue_get(object, &message, 0) == FuriStatusOk); switch(message.type) { case CliVcpMessageTypeEnable: if(cli_vcp->is_enabled) return; FURI_LOG_D(TAG, "Enabling"); cli_vcp->is_enabled = true; // switch usb mode cli_vcp->previous_interface = furi_hal_usb_get_config(); furi_hal_usb_set_config(&usb_cdc_single, NULL); furi_hal_cdc_set_callbacks(VCP_IF_NUM, &cdc_callbacks, cli_vcp); break; case CliVcpMessageTypeDisable: if(!cli_vcp->is_enabled) return; FURI_LOG_D(TAG, "Disabling"); cli_vcp->is_enabled = false; // restore usb mode furi_hal_cdc_set_callbacks(VCP_IF_NUM, NULL, NULL); furi_hal_usb_set_config(cli_vcp->previous_interface, NULL); break; } } /** * Processes messages arriving from CDC event callbacks */ static void cli_vcp_internal_event_happened(void* context) { CliVcp* cli_vcp = context; CliVcpInternalEvent event = furi_thread_flags_wait(CliVcpInternalEventAll, FuriFlagWaitAny, 0); furi_check(!(event & FuriFlagError)); if(event & CliVcpInternalEventDisconnected) { if(!cli_vcp->is_connected) return; FURI_LOG_D(TAG, "Disconnected"); cli_vcp->is_connected = false; // disconnect our side of the pipe pipe_detach_from_event_loop(cli_vcp->own_pipe); pipe_free(cli_vcp->own_pipe); cli_vcp->own_pipe = NULL; } if(event & CliVcpInternalEventConnected) { if(cli_vcp->is_connected) return; FURI_LOG_D(TAG, "Connected"); cli_vcp->is_connected = true; // wait for previous shell to stop furi_check(!cli_vcp->own_pipe); if(cli_vcp->shell) { furi_thread_join(cli_vcp->shell); furi_thread_free(cli_vcp->shell); } // start shell thread PipeSideBundle bundle = pipe_alloc(VCP_BUF_SIZE, 1); cli_vcp->own_pipe = bundle.alices_side; pipe_attach_to_event_loop(cli_vcp->own_pipe, cli_vcp->event_loop); pipe_set_callback_context(cli_vcp->own_pipe, cli_vcp); pipe_set_data_arrived_callback( cli_vcp->own_pipe, cli_vcp_data_from_shell, FuriEventLoopEventFlagEdge); pipe_set_space_freed_callback( cli_vcp->own_pipe, cli_vcp_shell_ready, FuriEventLoopEventFlagEdge); furi_delay_ms(33); // we are too fast, minicom isn't ready yet cli_vcp->shell = cli_shell_start(bundle.bobs_side); } if(event & CliVcpInternalEventRx) { VCP_TRACE(TAG, "Rx"); cli_vcp_maybe_receive_data(cli_vcp); } if(event & CliVcpInternalEventTxDone) { VCP_TRACE(TAG, "TxDone"); cli_vcp->is_currently_transmitting = false; cli_vcp_maybe_send_data(cli_vcp); } } // ============ // Thread stuff // ============ static CliVcp* cli_vcp_alloc(void) { CliVcp* cli_vcp = malloc(sizeof(CliVcp)); cli_vcp->thread_id = furi_thread_get_current_id(); cli_vcp->event_loop = furi_event_loop_alloc(); cli_vcp->message_queue = furi_message_queue_alloc(VCP_MESSAGE_Q_LEN, sizeof(CliVcpMessage)); furi_event_loop_subscribe_message_queue( cli_vcp->event_loop, cli_vcp->message_queue, FuriEventLoopEventIn, cli_vcp_message_received, cli_vcp); furi_event_loop_subscribe_thread_flags( cli_vcp->event_loop, cli_vcp_internal_event_happened, cli_vcp); return cli_vcp; } int32_t cli_vcp_srv(void* p) { UNUSED(p); if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) { FURI_LOG_W(TAG, "Skipping start in special boot mode"); furi_thread_suspend(furi_thread_get_current_id()); return 0; } CliVcp* cli_vcp = cli_vcp_alloc(); furi_record_create(RECORD_CLI_VCP, cli_vcp); furi_event_loop_run(cli_vcp->event_loop); return 0; } // ========== // Public API // ========== void cli_vcp_enable(CliVcp* cli_vcp) { CliVcpMessage message = { .type = CliVcpMessageTypeEnable, }; furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever); } void cli_vcp_disable(CliVcp* cli_vcp) { CliVcpMessage message = { .type = CliVcpMessageTypeDisable, }; furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever); }