1
mirror of https://github.com/flipperdevices/flipperzero-firmware.git synced 2025-12-12 04:41:26 +04:00

[FL-3954, FL-3955] New CLI architecture (#4111)

* feat: FuriThread stdin

* ci: fix f18

* feat: stdio callback context

* feat: FuriPipe

* POTENTIALLY EXPLOSIVE pipe welding

* fix: non-explosive welding

* Revert welding

* docs: furi_pipe

* feat: pipe event loop integration

* update f18 sdk

* f18

* docs: make doxygen happy

* fix: event loop not triggering when pipe attached to stdio

* fix: partial stdout in pipe

* allow simultaneous in and out subscription in event loop

* feat: vcp i/o

* feat: cli ansi stuffs and history

* feat: more line editing

* working but slow cli rewrite

* restore previous speed after 4 days of debugging 🥲

* fix: cli_app_should_stop

* fix: cli and event_loop memory leaks

* style: remove commented out code

* ci: fix pvs warnings

* fix: unit tests, event_loop crash

* ci: fix build

* ci: silence pvs warning

* feat: cli gpio

* ci: fix formatting

* Fix memory leak during event loop unsubscription

* Event better memory leak fix

* feat: cli completions

* Merge remote-tracking branch 'origin/dev' into portasynthinca3/3928-cli-threads

* merge fixups

* temporarily exclude speaker_debug app

* pvs and unit tests fixups

* feat: commands in fals

* move commands out of flash, code cleanup

* ci: fix errors

* fix: run commands in buffer when stopping session

* speedup cli file transfer

* fix f18

* separate cli_shell into modules

* fix pvs warning

* fix qflipper refusing to connect

* remove temp debug logs

* remove erroneous conclusion

* Fix memory leak during event loop unsubscription

* Event better memory leak fix

* unit test for the fix

* improve thread stdio callback signatures

* pipe stdout timeout

* update api symbols

* fix f18, formatting

* fix pvs warnings

* increase stack size, hope to fix unit tests

* cli: revert flag changes

* cli: fix formatting

* cli, fbt: loopback perf benchmark

* thread, event_loop: subscribing to thread flags

* cli: signal internal events using thread flags, improve performance

* fix f18, formatting

* event_loop: fix crash

* storage_cli: increase write_chunk buffer size again

* cli: explanation for order=0

* thread, event_loop: thread flags callback refactor

* cli: increase stack size

* cli: rename cli_app_should_stop -> cli_is_pipe_broken_or_is_etx_next_char

* cli: use plain array instead of mlib for history

* cli: prepend file name to static fns

* cli: fix formatting

* cli_shell: increase stack size

* cli: fix rpc lockup

* cli: better lockup fix

* cli: fix f18

* fix merge

---------

Co-authored-by: Georgii Surkov <georgii.surkov@outlook.com>
Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
Anna Antonenko
2025-04-02 22:10:10 +04:00
committed by GitHub
parent 5786066512
commit 13333edd30
60 changed files with 2074 additions and 1339 deletions

View File

@@ -1,323 +1,298 @@
#include "cli_i.h" // IWYU pragma: keep
#include "cli_vcp.h"
#include "shell/cli_shell.h"
#include <furi_hal_usb_cdc.h>
#include <furi_hal.h>
#include <furi.h>
#include <stdint.h>
#include <toolbox/pipe.h>
#define TAG "CliVcp"
#define USB_CDC_PKT_LEN CDC_DATA_SZ
#define VCP_RX_BUF_SIZE (USB_CDC_PKT_LEN * 3)
#define VCP_TX_BUF_SIZE (USB_CDC_PKT_LEN * 3)
#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
#define VCP_IF_NUM 0
#ifdef CLI_VCP_DEBUG
#define VCP_DEBUG(...) FURI_LOG_D(TAG, __VA_ARGS__)
#ifdef CLI_VCP_TRACE
#define VCP_TRACE(...) FURI_LOG_T(__VA_ARGS__)
#else
#define VCP_DEBUG(...)
#define VCP_TRACE(...)
#endif
typedef enum {
VcpEvtStop = (1 << 0),
VcpEvtConnect = (1 << 1),
VcpEvtDisconnect = (1 << 2),
VcpEvtStreamRx = (1 << 3),
VcpEvtRx = (1 << 4),
VcpEvtStreamTx = (1 << 5),
VcpEvtTx = (1 << 6),
} WorkerEvtFlags;
#define VCP_THREAD_FLAG_ALL \
(VcpEvtStop | VcpEvtConnect | VcpEvtDisconnect | VcpEvtRx | VcpEvtTx | VcpEvtStreamRx | \
VcpEvtStreamTx)
typedef struct {
FuriThread* thread;
enum {
CliVcpMessageTypeEnable,
CliVcpMessageTypeDisable,
} type;
union {};
} CliVcpMessage;
FuriStreamBuffer* tx_stream;
FuriStreamBuffer* rx_stream;
typedef enum {
CliVcpInternalEventConnected = (1 << 0),
CliVcpInternalEventDisconnected = (1 << 1),
CliVcpInternalEventTxDone = (1 << 2),
CliVcpInternalEventRx = (1 << 3),
} CliVcpInternalEvent;
volatile bool connected;
volatile bool running;
#define CliVcpInternalEventAll \
(CliVcpInternalEventConnected | CliVcpInternalEventDisconnected | CliVcpInternalEventTxDone | \
CliVcpInternalEventRx)
FuriHalUsbInterface* usb_if_prev;
struct CliVcp {
FuriEventLoop* event_loop;
FuriMessageQueue* message_queue; // <! external messages
FuriThreadId thread_id;
uint8_t data_buffer[USB_CDC_PKT_LEN];
} CliVcp;
bool is_enabled, is_connected;
FuriHalUsbInterface* previous_interface;
static int32_t vcp_worker(void* context);
static void vcp_on_cdc_tx_complete(void* context);
static void vcp_on_cdc_rx(void* context);
static void vcp_state_callback(void* context, uint8_t state);
static void vcp_on_cdc_control_line(void* context, uint8_t state);
PipeSide* own_pipe;
bool is_currently_transmitting;
size_t previous_tx_length;
static CdcCallbacks cdc_cb = {
vcp_on_cdc_tx_complete,
vcp_on_cdc_rx,
vcp_state_callback,
vcp_on_cdc_control_line,
NULL,
FuriThread* shell;
};
static CliVcp* vcp = NULL;
// ============
// Data copying
// ============
static const uint8_t ascii_soh = 0x01;
static const uint8_t ascii_eot = 0x04;
/**
* Called in the following cases:
* - previous transfer has finished;
* - new data became available to send.
*/
static void cli_vcp_maybe_send_data(CliVcp* cli_vcp) {
if(cli_vcp->is_currently_transmitting) return;
if(!cli_vcp->own_pipe) return;
static void cli_vcp_init(void) {
if(vcp == NULL) {
vcp = malloc(sizeof(CliVcp));
vcp->tx_stream = furi_stream_buffer_alloc(VCP_TX_BUF_SIZE, 1);
vcp->rx_stream = furi_stream_buffer_alloc(VCP_RX_BUF_SIZE, 1);
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);
}
furi_assert(vcp->thread == NULL);
vcp->connected = false;
vcp->thread = furi_thread_alloc_ex("CliVcpWorker", 1024, vcp_worker, NULL);
furi_thread_start(vcp->thread);
FURI_LOG_I(TAG, "Init OK");
cli_vcp->previous_tx_length = length;
}
static void cli_vcp_deinit(void) {
furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtStop);
furi_thread_join(vcp->thread);
furi_thread_free(vcp->thread);
vcp->thread = NULL;
/**
* 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);
}
static int32_t vcp_worker(void* context) {
UNUSED(context);
bool tx_idle = true;
size_t missed_rx = 0;
uint8_t last_tx_pkt_len = 0;
// =============
// CDC callbacks
// =============
// Switch USB to VCP mode (if it is not set yet)
vcp->usb_if_prev = furi_hal_usb_get_config();
if((vcp->usb_if_prev != &usb_cdc_single) && (vcp->usb_if_prev != &usb_cdc_dual)) {
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;
}
furi_hal_cdc_set_callbacks(VCP_IF_NUM, &cdc_cb, NULL);
FURI_LOG_D(TAG, "Start");
vcp->running = true;
while(1) {
uint32_t flags =
furi_thread_flags_wait(VCP_THREAD_FLAG_ALL, FuriFlagWaitAny, FuriWaitForever);
furi_assert(!(flags & FuriFlagError));
// VCP session opened
if(flags & VcpEvtConnect) {
VCP_DEBUG("Connect");
if(vcp->connected == false) {
vcp->connected = true;
furi_stream_buffer_send(vcp->rx_stream, &ascii_soh, 1, FuriWaitForever);
}
}
// VCP session closed
if(flags & VcpEvtDisconnect) {
VCP_DEBUG("Disconnect");
if(vcp->connected == true) {
vcp->connected = false;
furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0);
furi_stream_buffer_send(vcp->rx_stream, &ascii_eot, 1, FuriWaitForever);
}
}
// Rx buffer was read, maybe there is enough space for new data?
if((flags & VcpEvtStreamRx) && (missed_rx > 0)) {
VCP_DEBUG("StreamRx");
if(furi_stream_buffer_spaces_available(vcp->rx_stream) >= USB_CDC_PKT_LEN) {
flags |= VcpEvtRx;
missed_rx--;
}
}
// New data received
if(flags & VcpEvtRx) {
if(furi_stream_buffer_spaces_available(vcp->rx_stream) >= USB_CDC_PKT_LEN) {
int32_t len = furi_hal_cdc_receive(VCP_IF_NUM, vcp->data_buffer, USB_CDC_PKT_LEN);
VCP_DEBUG("Rx %ld", len);
if(len > 0) {
furi_check(
furi_stream_buffer_send(
vcp->rx_stream, vcp->data_buffer, len, FuriWaitForever) ==
(size_t)len);
}
} else {
VCP_DEBUG("Rx missed");
missed_rx++;
}
}
// New data in Tx buffer
if(flags & VcpEvtStreamTx) {
VCP_DEBUG("StreamTx");
if(tx_idle) {
flags |= VcpEvtTx;
}
}
// CDC write transfer done
if(flags & VcpEvtTx) {
size_t len =
furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0);
VCP_DEBUG("Tx %d", len);
if(len > 0) { // Some data left in Tx buffer. Sending it now
tx_idle = false;
furi_hal_cdc_send(VCP_IF_NUM, vcp->data_buffer, len);
last_tx_pkt_len = len;
} else { // There is nothing to send.
if(last_tx_pkt_len == 64) {
// Send extra zero-length packet if last packet len is 64 to indicate transfer end
furi_hal_cdc_send(VCP_IF_NUM, NULL, 0);
} else {
// Set flag to start next transfer instantly
tx_idle = true;
}
last_tx_pkt_len = 0;
}
}
if(flags & VcpEvtStop) {
vcp->connected = false;
vcp->running = false;
furi_hal_cdc_set_callbacks(VCP_IF_NUM, NULL, NULL);
// Restore previous USB mode (if it was set during init)
if((vcp->usb_if_prev != &usb_cdc_single) && (vcp->usb_if_prev != &usb_cdc_dual)) {
furi_hal_usb_unlock();
furi_hal_usb_set_config(vcp->usb_if_prev, NULL);
}
furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0);
furi_stream_buffer_send(vcp->rx_stream, &ascii_eot, 1, FuriWaitForever);
break;
}
}
FURI_LOG_D(TAG, "End");
return 0;
}
static size_t cli_vcp_rx(uint8_t* buffer, size_t size, uint32_t timeout) {
furi_assert(vcp);
furi_assert(buffer);
/**
* 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(vcp->running == false) {
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;
}
VCP_DEBUG("rx %u start", size);
CliVcp* cli_vcp = cli_vcp_alloc();
furi_record_create(RECORD_CLI_VCP, cli_vcp);
furi_event_loop_run(cli_vcp->event_loop);
size_t rx_cnt = 0;
while(size > 0) {
size_t batch_size = size;
if(batch_size > VCP_RX_BUF_SIZE) batch_size = VCP_RX_BUF_SIZE;
size_t len = furi_stream_buffer_receive(vcp->rx_stream, buffer, batch_size, timeout);
VCP_DEBUG("rx %u ", batch_size);
if(len == 0) break;
if(vcp->running == false) {
// EOT command is received after VCP session close
rx_cnt += len;
break;
}
furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtStreamRx);
size -= len;
buffer += len;
rx_cnt += len;
}
VCP_DEBUG("rx %u end", size);
return rx_cnt;
return 0;
}
static size_t cli_vcp_rx_stdin(uint8_t* data, size_t size, uint32_t timeout, void* context) {
UNUSED(context);
return cli_vcp_rx(data, size, timeout);
// ==========
// Public API
// ==========
void cli_vcp_enable(CliVcp* cli_vcp) {
CliVcpMessage message = {
.type = CliVcpMessageTypeEnable,
};
furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever);
}
static void cli_vcp_tx(const uint8_t* buffer, size_t size) {
furi_assert(vcp);
furi_assert(buffer);
if(vcp->running == false) {
return;
}
VCP_DEBUG("tx %u start", size);
while(size > 0 && vcp->connected) {
size_t batch_size = size;
if(batch_size > USB_CDC_PKT_LEN) batch_size = USB_CDC_PKT_LEN;
furi_stream_buffer_send(vcp->tx_stream, buffer, batch_size, FuriWaitForever);
furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtStreamTx);
VCP_DEBUG("tx %u", batch_size);
size -= batch_size;
buffer += batch_size;
}
VCP_DEBUG("tx %u end", size);
void cli_vcp_disable(CliVcp* cli_vcp) {
CliVcpMessage message = {
.type = CliVcpMessageTypeDisable,
};
furi_message_queue_put(cli_vcp->message_queue, &message, FuriWaitForever);
}
static void cli_vcp_tx_stdout(const char* data, size_t size, void* context) {
UNUSED(context);
cli_vcp_tx((const uint8_t*)data, size);
}
static void vcp_state_callback(void* context, uint8_t state) {
UNUSED(context);
if(state == 0) {
furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtDisconnect);
}
}
static void vcp_on_cdc_control_line(void* context, uint8_t state) {
UNUSED(context);
// bit 0: DTR state, bit 1: RTS state
bool dtr = state & (1 << 0);
if(dtr == true) {
furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtConnect);
} else {
furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtDisconnect);
}
}
static void vcp_on_cdc_rx(void* context) {
UNUSED(context);
uint32_t ret = furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtRx);
furi_check(!(ret & FuriFlagError));
}
static void vcp_on_cdc_tx_complete(void* context) {
UNUSED(context);
furi_thread_flags_set(furi_thread_get_id(vcp->thread), VcpEvtTx);
}
static bool cli_vcp_is_connected(void) {
furi_assert(vcp);
return vcp->connected;
}
const CliSession cli_vcp = {
cli_vcp_init,
cli_vcp_deinit,
cli_vcp_rx,
cli_vcp_rx_stdin,
cli_vcp_tx,
cli_vcp_tx_stdout,
cli_vcp_is_connected,
};