From 13333edd308ccb1322cb1ab201d1c8e319e028a4 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 2 Apr 2025 22:10:10 +0400 Subject: [PATCH 1/9] [FL-3954, FL-3955] New CLI architecture (#4111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: あく --- SConstruct | 15 + .../debug/speaker_debug/speaker_debug.c | 5 +- applications/debug/unit_tests/test_runner.c | 9 +- applications/debug/unit_tests/test_runner.h | 8 +- .../debug/unit_tests/tests/pipe/pipe_test.c | 28 +- .../unit_tests/tests/storage/storage_test.c | 5 - applications/debug/unit_tests/unit_tests.c | 6 +- applications/main/application.fam | 1 + applications/main/gpio/usb_uart_bridge.c | 24 +- applications/main/ibutton/ibutton_cli.c | 48 +- applications/main/infrared/infrared_cli.c | 56 +- applications/main/lfrfid/lfrfid_cli.c | 60 +- applications/main/nfc/nfc_cli.c | 10 +- applications/main/onewire/onewire_cli.c | 33 +- .../main/subghz/helpers/subghz_chat.c | 9 +- .../main/subghz/helpers/subghz_chat.h | 3 +- applications/main/subghz/subghz_cli.c | 81 +-- applications/services/application.fam | 1 + applications/services/bt/bt_cli.c | 33 +- applications/services/cli/application.fam | 36 +- applications/services/cli/cli.c | 493 +++------------- applications/services/cli/cli.h | 170 +++--- applications/services/cli/cli_ansi.c | 123 ++++ applications/services/cli/cli_ansi.h | 153 +++++ applications/services/cli/cli_command_gpio.c | 24 +- applications/services/cli/cli_command_gpio.h | 3 +- applications/services/cli/cli_commands.c | 179 +++--- applications/services/cli/cli_commands.h | 31 +- applications/services/cli/cli_i.h | 63 +- applications/services/cli/cli_vcp.c | 539 +++++++++--------- applications/services/cli/cli_vcp.h | 7 +- applications/services/cli/shell/cli_shell.c | 251 ++++++++ applications/services/cli/shell/cli_shell.h | 16 + applications/services/cli/shell/cli_shell_i.h | 32 ++ .../services/cli/shell/cli_shell_line.c | 238 ++++++++ .../services/cli/shell/cli_shell_line.h | 36 ++ applications/services/crypto/crypto_cli.c | 35 +- applications/services/desktop/desktop.c | 12 +- applications/services/input/input.c | 3 +- applications/services/input/input_cli.c | 16 +- applications/services/loader/loader_cli.c | 5 +- applications/services/power/power_cli.c | 33 +- applications/services/rpc/rpc_cli.c | 19 +- applications/services/rpc/rpc_i.h | 3 +- applications/services/storage/storage_cli.c | 106 ++-- applications/system/js_app/js_app.c | 13 +- applications/system/updater/cli/updater_cli.c | 5 +- furi/core/core_defines.h | 10 + furi/core/event_loop.c | 42 ++ furi/core/event_loop.h | 23 + furi/core/event_loop_i.h | 10 +- furi/core/event_loop_thread_flag_interface.h | 10 + furi/core/thread.c | 4 + lib/toolbox/pipe.c | 66 ++- lib/toolbox/pipe.h | 32 +- scripts/serial_cli_perf.py | 59 ++ scripts/update.py | 2 +- targets/f18/api_symbols.csv | 31 +- targets/f7/api_symbols.csv | 31 +- targets/f7/furi_hal/furi_hal_usb_cdc.h | 14 +- 60 files changed, 2074 insertions(+), 1339 deletions(-) create mode 100644 applications/services/cli/cli_ansi.c create mode 100644 applications/services/cli/cli_ansi.h create mode 100644 applications/services/cli/shell/cli_shell.c create mode 100644 applications/services/cli/shell/cli_shell.h create mode 100644 applications/services/cli/shell/cli_shell_i.h create mode 100644 applications/services/cli/shell/cli_shell_line.c create mode 100644 applications/services/cli/shell/cli_shell_line.h create mode 100644 furi/core/event_loop_thread_flag_interface.h create mode 100644 scripts/serial_cli_perf.py diff --git a/SConstruct b/SConstruct index 127967968..44aa3bc17 100644 --- a/SConstruct +++ b/SConstruct @@ -412,6 +412,21 @@ distenv.PhonyTarget( ], ) + +# Measure CLI loopback performance +distenv.PhonyTarget( + "cli_perf", + [ + [ + "${PYTHON3}", + "${FBT_SCRIPT_DIR}/serial_cli_perf.py", + "-p", + "${FLIP_PORT}", + "${ARGS}", + ] + ], +) + # Update WiFi devboard firmware with release channel distenv.PhonyTarget( "devboard_flash", diff --git a/applications/debug/speaker_debug/speaker_debug.c b/applications/debug/speaker_debug/speaker_debug.c index 3f685ab30..d2a40a2a4 100644 --- a/applications/debug/speaker_debug/speaker_debug.c +++ b/applications/debug/speaker_debug/speaker_debug.c @@ -3,6 +3,7 @@ #include #include #include +#include #define TAG "SpeakerDebug" @@ -37,8 +38,8 @@ static void speaker_app_free(SpeakerDebugApp* app) { free(app); } -static void speaker_app_cli(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void speaker_app_cli(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); SpeakerDebugApp* app = (SpeakerDebugApp*)context; SpeakerDebugAppMessage message; diff --git a/applications/debug/unit_tests/test_runner.c b/applications/debug/unit_tests/test_runner.c index de29e91b3..4de051125 100644 --- a/applications/debug/unit_tests/test_runner.c +++ b/applications/debug/unit_tests/test_runner.c @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -25,7 +26,7 @@ struct TestRunner { NotificationApp* notification; // Temporary used things - Cli* cli; + PipeSide* pipe; FuriString* args; // ELF related stuff @@ -38,14 +39,14 @@ struct TestRunner { int minunit_status; }; -TestRunner* test_runner_alloc(Cli* cli, FuriString* args) { +TestRunner* test_runner_alloc(PipeSide* pipe, FuriString* args) { TestRunner* instance = malloc(sizeof(TestRunner)); instance->storage = furi_record_open(RECORD_STORAGE); instance->loader = furi_record_open(RECORD_LOADER); instance->notification = furi_record_open(RECORD_NOTIFICATION); - instance->cli = cli; + instance->pipe = pipe; instance->args = args; instance->composite_resolver = composite_api_resolver_alloc(); @@ -147,7 +148,7 @@ static void test_runner_run_internal(TestRunner* instance) { } while(true) { - if(cli_cmd_interrupt_received(instance->cli)) { + if(cli_is_pipe_broken_or_is_etx_next_char(instance->pipe)) { break; } diff --git a/applications/debug/unit_tests/test_runner.h b/applications/debug/unit_tests/test_runner.h index 43aba8bb1..0e9495263 100644 --- a/applications/debug/unit_tests/test_runner.h +++ b/applications/debug/unit_tests/test_runner.h @@ -1,12 +1,12 @@ #pragma once #include +#include typedef struct TestRunner TestRunner; -typedef struct Cli Cli; -TestRunner* test_runner_alloc(Cli* cli, FuriString* args); +TestRunner* test_runner_alloc(PipeSide* pipe, FuriString* args); -void test_runner_free(TestRunner* isntance); +void test_runner_free(TestRunner* instance); -void test_runner_run(TestRunner* isntance); +void test_runner_run(TestRunner* instance); diff --git a/applications/debug/unit_tests/tests/pipe/pipe_test.c b/applications/debug/unit_tests/tests/pipe/pipe_test.c index d440a04ee..4eae39636 100644 --- a/applications/debug/unit_tests/tests/pipe/pipe_test.c +++ b/applications/debug/unit_tests/tests/pipe/pipe_test.c @@ -25,16 +25,13 @@ MU_TEST(pipe_test_trivial) { mu_assert_int_eq(PIPE_SIZE - i, pipe_spaces_available(alice)); mu_assert_int_eq(i, pipe_bytes_available(bob)); - if(pipe_send(alice, &i, sizeof(uint8_t), 0) != sizeof(uint8_t)) { - break; - } + if(pipe_spaces_available(alice) == 0) break; + furi_check(pipe_send(alice, &i, sizeof(uint8_t)) == sizeof(uint8_t)); mu_assert_int_eq(PIPE_SIZE - i, pipe_spaces_available(bob)); mu_assert_int_eq(i, pipe_bytes_available(alice)); - if(pipe_send(bob, &i, sizeof(uint8_t), 0) != sizeof(uint8_t)) { - break; - } + furi_check(pipe_send(bob, &i, sizeof(uint8_t)) == sizeof(uint8_t)); } pipe_free(alice); @@ -43,10 +40,9 @@ MU_TEST(pipe_test_trivial) { for(uint8_t i = 0;; ++i) { mu_assert_int_eq(PIPE_SIZE - i, pipe_bytes_available(bob)); + if(pipe_bytes_available(bob) == 0) break; uint8_t value; - if(pipe_receive(bob, &value, sizeof(uint8_t), 0) != sizeof(uint8_t)) { - break; - } + furi_check(pipe_receive(bob, &value, sizeof(uint8_t)) == sizeof(uint8_t)); mu_assert_int_eq(i, value); } @@ -68,16 +64,16 @@ typedef struct { static void on_data_arrived(PipeSide* pipe, void* context) { AncillaryThreadContext* ctx = context; ctx->flag |= TestFlagDataArrived; - uint8_t buffer[PIPE_SIZE]; - size_t size = pipe_receive(pipe, buffer, sizeof(buffer), 0); - pipe_send(pipe, buffer, size, 0); + uint8_t input; + size_t size = pipe_receive(pipe, &input, sizeof(input)); + pipe_send(pipe, &input, size); } static void on_space_freed(PipeSide* pipe, void* context) { AncillaryThreadContext* ctx = context; ctx->flag |= TestFlagSpaceFreed; const char* message = "Hi!"; - pipe_send(pipe, message, strlen(message), 0); + pipe_send(pipe, message, strlen(message)); } static void on_became_broken(PipeSide* pipe, void* context) { @@ -117,15 +113,15 @@ MU_TEST(pipe_test_event_loop) { furi_thread_start(thread); const char* message = "Hello!"; - pipe_send(alice, message, strlen(message), FuriWaitForever); + pipe_send(alice, message, strlen(message)); char buffer_1[16]; - size_t size = pipe_receive(alice, buffer_1, sizeof(buffer_1), FuriWaitForever); + size_t size = pipe_receive(alice, buffer_1, strlen(message)); buffer_1[size] = 0; char buffer_2[16]; const char* expected_reply = "Hi!"; - size = pipe_receive(alice, buffer_2, sizeof(buffer_2), FuriWaitForever); + size = pipe_receive(alice, buffer_2, strlen(expected_reply)); buffer_2[size] = 0; pipe_free(alice); diff --git a/applications/debug/unit_tests/tests/storage/storage_test.c b/applications/debug/unit_tests/tests/storage/storage_test.c index 75c52ef9a..9d5c68a44 100644 --- a/applications/debug/unit_tests/tests/storage/storage_test.c +++ b/applications/debug/unit_tests/tests/storage/storage_test.c @@ -521,11 +521,6 @@ MU_TEST(test_storage_data_path) { // check that appsdata folder exists mu_check(storage_dir_exists(storage, APPS_DATA_PATH)); - // check that cli folder exists - mu_check(storage_dir_exists(storage, APPSDATA_APP_PATH("cli"))); - - storage_simply_remove(storage, APPSDATA_APP_PATH("cli")); - furi_record_close(RECORD_STORAGE); } diff --git a/applications/debug/unit_tests/unit_tests.c b/applications/debug/unit_tests/unit_tests.c index 237cb9080..db78d8ced 100644 --- a/applications/debug/unit_tests/unit_tests.c +++ b/applications/debug/unit_tests/unit_tests.c @@ -1,13 +1,13 @@ #include #include +#include #include "test_runner.h" -void unit_tests_cli(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void unit_tests_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); - TestRunner* test_runner = test_runner_alloc(cli, args); + TestRunner* test_runner = test_runner_alloc(pipe, args); test_runner_run(test_runner); test_runner_free(test_runner); } diff --git a/applications/main/application.fam b/applications/main/application.fam index 0a90ee224..4d3162337 100644 --- a/applications/main/application.fam +++ b/applications/main/application.fam @@ -21,6 +21,7 @@ App( name="On start hooks", apptype=FlipperAppType.METAPACKAGE, provides=[ + "cli", "ibutton_start", "onewire_start", "subghz_start", diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index e6b71cb34..15170f0d0 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -106,15 +106,15 @@ static void usb_uart_on_irq_rx_dma_cb( static void usb_uart_vcp_init(UsbUartBridge* usb_uart, uint8_t vcp_ch) { furi_hal_usb_unlock(); if(vcp_ch == 0) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_close(cli); - furi_record_close(RECORD_CLI); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_disable(cli_vcp); + furi_record_close(RECORD_CLI_VCP); furi_check(furi_hal_usb_set_config(&usb_cdc_single, NULL) == true); } else { furi_check(furi_hal_usb_set_config(&usb_cdc_dual, NULL) == true); - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_open(cli, &cli_vcp); - furi_record_close(RECORD_CLI); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_enable(cli_vcp); + furi_record_close(RECORD_CLI_VCP); } furi_hal_cdc_set_callbacks(vcp_ch, (CdcCallbacks*)&cdc_cb, usb_uart); } @@ -123,9 +123,9 @@ static void usb_uart_vcp_deinit(UsbUartBridge* usb_uart, uint8_t vcp_ch) { UNUSED(usb_uart); furi_hal_cdc_set_callbacks(vcp_ch, NULL, NULL); if(vcp_ch != 0) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_close(cli); - furi_record_close(RECORD_CLI); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_disable(cli_vcp); + furi_record_close(RECORD_CLI_VCP); } } @@ -309,9 +309,9 @@ static int32_t usb_uart_worker(void* context) { furi_hal_usb_unlock(); furi_check(furi_hal_usb_set_config(&usb_cdc_single, NULL) == true); - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_open(cli, &cli_vcp); - furi_record_close(RECORD_CLI); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_enable(cli_vcp); + furi_record_close(RECORD_CLI_VCP); return 0; } diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index 2338ca3c3..e11ace1d0 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -1,26 +1,14 @@ #include #include -#include +#include #include +#include #include #include #include -static void ibutton_cli(Cli* cli, FuriString* args, void* context); - -// app cli function -void ibutton_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(ibutton_cli); -#endif -} - static void ibutton_cli_print_usage(void) { printf("Usage:\r\n"); printf("ikey read\r\n"); @@ -92,7 +80,7 @@ static void ibutton_cli_worker_read_cb(void* context) { furi_event_flag_set(event, EVENT_FLAG_IBUTTON_COMPLETE); } -static void ibutton_cli_read(Cli* cli) { +static void ibutton_cli_read(PipeSide* pipe) { iButtonProtocols* protocols = ibutton_protocols_alloc(); iButtonWorker* worker = ibutton_worker_alloc(protocols); iButtonKey* key = ibutton_key_alloc(ibutton_protocols_get_max_data_size(protocols)); @@ -113,7 +101,7 @@ static void ibutton_cli_read(Cli* cli) { break; } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } ibutton_worker_stop(worker); @@ -138,7 +126,7 @@ static void ibutton_cli_worker_write_cb(void* context, iButtonWorkerWriteResult furi_event_flag_set(write_context->event, EVENT_FLAG_IBUTTON_COMPLETE); } -void ibutton_cli_write(Cli* cli, FuriString* args) { +void ibutton_cli_write(PipeSide* pipe, FuriString* args) { iButtonProtocols* protocols = ibutton_protocols_alloc(); iButtonWorker* worker = ibutton_worker_alloc(protocols); iButtonKey* key = ibutton_key_alloc(ibutton_protocols_get_max_data_size(protocols)); @@ -181,7 +169,7 @@ void ibutton_cli_write(Cli* cli, FuriString* args) { } } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } } while(false); @@ -195,7 +183,7 @@ void ibutton_cli_write(Cli* cli, FuriString* args) { furi_event_flag_free(write_context.event); } -void ibutton_cli_emulate(Cli* cli, FuriString* args) { +void ibutton_cli_emulate(PipeSide* pipe, FuriString* args) { iButtonProtocols* protocols = ibutton_protocols_alloc(); iButtonWorker* worker = ibutton_worker_alloc(protocols); iButtonKey* key = ibutton_key_alloc(ibutton_protocols_get_max_data_size(protocols)); @@ -214,7 +202,7 @@ void ibutton_cli_emulate(Cli* cli, FuriString* args) { ibutton_worker_emulate_start(worker, key); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(100); }; @@ -228,8 +216,8 @@ void ibutton_cli_emulate(Cli* cli, FuriString* args) { ibutton_protocols_free(protocols); } -void ibutton_cli(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void ibutton_cli(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -241,14 +229,24 @@ void ibutton_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "read") == 0) { - ibutton_cli_read(cli); + ibutton_cli_read(pipe); } else if(furi_string_cmp_str(cmd, "write") == 0) { - ibutton_cli_write(cli, args); + ibutton_cli_write(pipe, args); } else if(furi_string_cmp_str(cmd, "emulate") == 0) { - ibutton_cli_emulate(cli, args); + ibutton_cli_emulate(pipe, args); } else { ibutton_cli_print_usage(); } furi_string_free(cmd); } + +void ibutton_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli, cli); + furi_record_close(RECORD_CLI); +#else + UNUSED(ibutton_cli); +#endif +} diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index 85ae95658..e62da5fd2 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -1,11 +1,11 @@ -#include -#include +#include #include #include #include #include #include #include +#include #include #include "infrared_signal.h" @@ -19,14 +19,14 @@ DICT_DEF2(dict_signals, FuriString*, FURI_STRING_OPLIST, int, M_DEFAULT_OPLIST) -static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args); -static void infrared_cli_start_ir_tx(Cli* cli, FuriString* args); -static void infrared_cli_process_decode(Cli* cli, FuriString* args); -static void infrared_cli_process_universal(Cli* cli, FuriString* args); +static void infrared_cli_start_ir_rx(PipeSide* pipe, FuriString* args); +static void infrared_cli_start_ir_tx(PipeSide* pipe, FuriString* args); +static void infrared_cli_process_decode(PipeSide* pipe, FuriString* args); +static void infrared_cli_process_universal(PipeSide* pipe, FuriString* args); static const struct { const char* cmd; - void (*process_function)(Cli* cli, FuriString* args); + void (*process_function)(PipeSide* pipe, FuriString* args); } infrared_cli_commands[] = { {.cmd = "rx", .process_function = infrared_cli_start_ir_rx}, {.cmd = "tx", .process_function = infrared_cli_start_ir_tx}, @@ -38,7 +38,7 @@ static void signal_received_callback(void* context, InfraredWorkerSignal* receiv furi_assert(received_signal); char buf[100]; size_t buf_cnt; - Cli* cli = (Cli*)context; + PipeSide* pipe = (PipeSide*)context; if(infrared_worker_signal_is_decoded(received_signal)) { const InfraredMessage* message = infrared_worker_get_decoded_signal(received_signal); @@ -52,20 +52,20 @@ static void signal_received_callback(void* context, InfraredWorkerSignal* receiv ROUND_UP_TO(infrared_get_protocol_command_length(message->protocol), 4), message->command, message->repeat ? " R" : ""); - cli_write(cli, (uint8_t*)buf, buf_cnt); + pipe_send(pipe, buf, buf_cnt); } else { const uint32_t* timings; size_t timings_cnt; infrared_worker_get_raw_signal(received_signal, &timings, &timings_cnt); buf_cnt = snprintf(buf, sizeof(buf), "RAW, %zu samples:\r\n", timings_cnt); - cli_write(cli, (uint8_t*)buf, buf_cnt); + pipe_send(pipe, buf, buf_cnt); for(size_t i = 0; i < timings_cnt; ++i) { buf_cnt = snprintf(buf, sizeof(buf), "%lu ", timings[i]); - cli_write(cli, (uint8_t*)buf, buf_cnt); + pipe_send(pipe, buf, buf_cnt); } buf_cnt = snprintf(buf, sizeof(buf), "\r\n"); - cli_write(cli, (uint8_t*)buf, buf_cnt); + pipe_send(pipe, buf, buf_cnt); } } @@ -124,9 +124,7 @@ static void infrared_cli_print_usage(void) { infrared_cli_print_universal_remotes(); } -static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args) { - UNUSED(cli); - +static void infrared_cli_start_ir_rx(PipeSide* pipe, FuriString* args) { bool enable_decoding = true; if(!furi_string_empty(args)) { @@ -142,10 +140,10 @@ static void infrared_cli_start_ir_rx(Cli* cli, FuriString* args) { InfraredWorker* worker = infrared_worker_alloc(); infrared_worker_rx_enable_signal_decoding(worker, enable_decoding); infrared_worker_rx_start(worker); - infrared_worker_rx_set_received_signal_callback(worker, signal_received_callback, cli); + infrared_worker_rx_set_received_signal_callback(worker, signal_received_callback, pipe); printf("Receiving %s INFRARED...\r\nPress Ctrl+C to abort\r\n", enable_decoding ? "" : "RAW"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(50); } @@ -214,8 +212,8 @@ static bool infrared_cli_parse_raw(const char* str, InfraredSignal* signal) { return infrared_signal_is_valid(signal); } -static void infrared_cli_start_ir_tx(Cli* cli, FuriString* args) { - UNUSED(cli); +static void infrared_cli_start_ir_tx(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); const char* str = furi_string_get_cstr(args); InfraredSignal* signal = infrared_signal_alloc(); @@ -335,8 +333,8 @@ static bool infrared_cli_decode_file(FlipperFormat* input_file, FlipperFormat* o return ret; } -static void infrared_cli_process_decode(Cli* cli, FuriString* args) { - UNUSED(cli); +static void infrared_cli_process_decode(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); Storage* storage = furi_record_open(RECORD_STORAGE); FlipperFormat* input_file = flipper_format_buffered_file_alloc(storage); FlipperFormat* output_file = NULL; @@ -455,8 +453,10 @@ static void infrared_cli_list_remote_signals(FuriString* remote_name) { furi_record_close(RECORD_STORAGE); } -static void - infrared_cli_brute_force_signals(Cli* cli, FuriString* remote_name, FuriString* signal_name) { +static void infrared_cli_brute_force_signals( + PipeSide* pipe, + FuriString* remote_name, + FuriString* signal_name) { InfraredBruteForce* brute_force = infrared_brute_force_alloc(); FuriString* remote_path = furi_string_alloc_printf( "%s/%s.ir", INFRARED_ASSETS_FOLDER, furi_string_get_cstr(remote_name)); @@ -490,7 +490,7 @@ static void while(running) { running = infrared_brute_force_send(brute_force, current_signal); - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; printf("\r%d%% complete.", (int)((float)current_signal++ / (float)signal_count * 100)); fflush(stdout); @@ -504,7 +504,7 @@ static void infrared_brute_force_free(brute_force); } -static void infrared_cli_process_universal(Cli* cli, FuriString* args) { +static void infrared_cli_process_universal(PipeSide* pipe, FuriString* args) { FuriString* arg1 = furi_string_alloc(); FuriString* arg2 = furi_string_alloc(); @@ -519,14 +519,14 @@ static void infrared_cli_process_universal(Cli* cli, FuriString* args) { } else if(furi_string_equal_str(arg1, "list")) { infrared_cli_list_remote_signals(arg2); } else { - infrared_cli_brute_force_signals(cli, arg1, arg2); + infrared_cli_brute_force_signals(pipe, arg1, arg2); } furi_string_free(arg1); furi_string_free(arg2); } -static void infrared_cli_start_ir(Cli* cli, FuriString* args, void* context) { +static void infrared_cli_start_ir(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); if(furi_hal_infrared_is_busy()) { printf("INFRARED is busy. Exiting."); @@ -546,7 +546,7 @@ static void infrared_cli_start_ir(Cli* cli, FuriString* args, void* context) { } if(i < COUNT_OF(infrared_cli_commands)) { - infrared_cli_commands[i].process_function(cli, args); + infrared_cli_commands[i].process_function(pipe, args); } else { infrared_cli_print_usage(); } diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index a25032d6a..fa74906c0 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -1,11 +1,12 @@ #include #include #include -#include +#include #include #include #include #include +#include #include @@ -14,15 +15,6 @@ #include #include -static void lfrfid_cli(Cli* cli, FuriString* args, void* context); - -// app cli function -void lfrfid_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli, NULL); - furi_record_close(RECORD_CLI); -} - static void lfrfid_cli_print_usage(void) { printf("Usage:\r\n"); printf("rfid read - read in ASK/PSK mode\r\n"); @@ -49,7 +41,7 @@ static void lfrfid_cli_read_callback(LFRFIDWorkerReadResult result, ProtocolId p furi_event_flag_set(context->event, 1 << result); } -static void lfrfid_cli_read(Cli* cli, FuriString* args) { +static void lfrfid_cli_read(PipeSide* pipe, FuriString* args) { FuriString* type_string; type_string = furi_string_alloc(); LFRFIDWorkerReadType type = LFRFIDWorkerReadTypeAuto; @@ -96,7 +88,7 @@ static void lfrfid_cli_read(Cli* cli, FuriString* args) { } } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } lfrfid_worker_stop(worker); @@ -192,7 +184,7 @@ static void lfrfid_cli_write_callback(LFRFIDWorkerWriteResult result, void* ctx) furi_event_flag_set(events, 1 << result); } -static void lfrfid_cli_write(Cli* cli, FuriString* args) { +static void lfrfid_cli_write(PipeSide* pipe, FuriString* args) { ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); ProtocolId protocol; @@ -212,7 +204,7 @@ static void lfrfid_cli_write(Cli* cli, FuriString* args) { (1 << LFRFIDWorkerWriteProtocolCannotBeWritten) | (1 << LFRFIDWorkerWriteFobCannotBeWritten); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { uint32_t flags = furi_event_flag_wait(event, available_flags, FuriFlagWaitAny, 100); if(flags != (unsigned)FuriFlagErrorTimeout) { if(FURI_BIT(flags, LFRFIDWorkerWriteOK)) { @@ -239,7 +231,7 @@ static void lfrfid_cli_write(Cli* cli, FuriString* args) { furi_event_flag_free(event); } -static void lfrfid_cli_emulate(Cli* cli, FuriString* args) { +static void lfrfid_cli_emulate(PipeSide* pipe, FuriString* args) { ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); ProtocolId protocol; @@ -254,7 +246,7 @@ static void lfrfid_cli_emulate(Cli* cli, FuriString* args) { lfrfid_worker_emulate_start(worker, protocol); printf("Emulating RFID...\r\nPress Ctrl+C to abort\r\n"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(100); } printf("Emulation stopped\r\n"); @@ -265,8 +257,8 @@ static void lfrfid_cli_emulate(Cli* cli, FuriString* args) { protocol_dict_free(dict); } -static void lfrfid_cli_raw_analyze(Cli* cli, FuriString* args) { - UNUSED(cli); +static void lfrfid_cli_raw_analyze(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); FuriString *filepath, *info_string; filepath = furi_string_alloc(); info_string = furi_string_alloc(); @@ -392,9 +384,7 @@ static void lfrfid_cli_raw_read_callback(LFRFIDWorkerReadRawResult result, void* furi_event_flag_set(event, 1 << result); } -static void lfrfid_cli_raw_read(Cli* cli, FuriString* args) { - UNUSED(cli); - +static void lfrfid_cli_raw_read(PipeSide* pipe, FuriString* args) { FuriString *filepath, *type_string; filepath = furi_string_alloc(); type_string = furi_string_alloc(); @@ -452,7 +442,7 @@ static void lfrfid_cli_raw_read(Cli* cli, FuriString* args) { } } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } if(overrun) { @@ -479,9 +469,7 @@ static void lfrfid_cli_raw_emulate_callback(LFRFIDWorkerEmulateRawResult result, furi_event_flag_set(event, 1 << result); } -static void lfrfid_cli_raw_emulate(Cli* cli, FuriString* args) { - UNUSED(cli); - +static void lfrfid_cli_raw_emulate(PipeSide* pipe, FuriString* args) { FuriString* filepath; filepath = furi_string_alloc(); Storage* storage = furi_record_open(RECORD_STORAGE); @@ -527,7 +515,7 @@ static void lfrfid_cli_raw_emulate(Cli* cli, FuriString* args) { } } - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } if(overrun) { @@ -548,7 +536,7 @@ static void lfrfid_cli_raw_emulate(Cli* cli, FuriString* args) { furi_string_free(filepath); } -static void lfrfid_cli(Cli* cli, FuriString* args, void* context) { +static void lfrfid_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -560,20 +548,26 @@ static void lfrfid_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "read") == 0) { - lfrfid_cli_read(cli, args); + lfrfid_cli_read(pipe, args); } else if(furi_string_cmp_str(cmd, "write") == 0) { - lfrfid_cli_write(cli, args); + lfrfid_cli_write(pipe, args); } else if(furi_string_cmp_str(cmd, "emulate") == 0) { - lfrfid_cli_emulate(cli, args); + lfrfid_cli_emulate(pipe, args); } else if(furi_string_cmp_str(cmd, "raw_read") == 0) { - lfrfid_cli_raw_read(cli, args); + lfrfid_cli_raw_read(pipe, args); } else if(furi_string_cmp_str(cmd, "raw_emulate") == 0) { - lfrfid_cli_raw_emulate(cli, args); + lfrfid_cli_raw_emulate(pipe, args); } else if(furi_string_cmp_str(cmd, "raw_analyze") == 0) { - lfrfid_cli_raw_analyze(cli, args); + lfrfid_cli_raw_analyze(pipe, args); } else { lfrfid_cli_print_usage(); } furi_string_free(cmd); } + +void lfrfid_on_system_start(void) { + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli, NULL); + furi_record_close(RECORD_CLI); +} diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index 90ac26d7c..8a9b1fec4 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -1,8 +1,10 @@ #include #include #include +#include #include #include +#include #include @@ -17,7 +19,7 @@ static void nfc_cli_print_usage(void) { } } -static void nfc_cli_field(Cli* cli, FuriString* args) { +static void nfc_cli_field(PipeSide* pipe, FuriString* args) { UNUSED(args); // Check if nfc worker is not busy if(furi_hal_nfc_is_hal_ready() != FuriHalNfcErrorNone) { @@ -32,7 +34,7 @@ static void nfc_cli_field(Cli* cli, FuriString* args) { printf("Field is on. Don't leave device in this mode for too long.\r\n"); printf("Press Ctrl+C to abort\r\n"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(50); } @@ -40,7 +42,7 @@ static void nfc_cli_field(Cli* cli, FuriString* args) { furi_hal_nfc_release(); } -static void nfc_cli(Cli* cli, FuriString* args, void* context) { +static void nfc_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -52,7 +54,7 @@ static void nfc_cli(Cli* cli, FuriString* args, void* context) { } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { if(furi_string_cmp_str(cmd, "field") == 0) { - nfc_cli_field(cli, args); + nfc_cli_field(pipe, args); break; } } diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 74ca6bc1f..63e3d696f 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -2,31 +2,18 @@ #include #include - -#include +#include #include #include -static void onewire_cli(Cli* cli, FuriString* args, void* context); - -void onewire_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(onewire_cli); -#endif -} - static void onewire_cli_print_usage(void) { printf("Usage:\r\n"); printf("onewire search\r\n"); } -static void onewire_cli_search(Cli* cli) { - UNUSED(cli); +static void onewire_cli_search(PipeSide* pipe) { + UNUSED(pipe); OneWireHost* onewire = onewire_host_alloc(&gpio_ibutton); Power* power = furi_record_open(RECORD_POWER); uint8_t address[8]; @@ -58,7 +45,7 @@ static void onewire_cli_search(Cli* cli) { furi_record_close(RECORD_POWER); } -void onewire_cli(Cli* cli, FuriString* args, void* context) { +static void onewire_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -70,8 +57,18 @@ void onewire_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "search") == 0) { - onewire_cli_search(cli); + onewire_cli_search(pipe); } furi_string_free(cmd); } + +void onewire_on_system_start(void) { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli, cli); + furi_record_close(RECORD_CLI); +#else + UNUSED(onewire_cli); +#endif +} diff --git a/applications/main/subghz/helpers/subghz_chat.c b/applications/main/subghz/helpers/subghz_chat.c index 9945b69c8..5c55aedeb 100644 --- a/applications/main/subghz/helpers/subghz_chat.c +++ b/applications/main/subghz/helpers/subghz_chat.c @@ -1,5 +1,6 @@ #include "subghz_chat.h" #include +#include #define TAG "SubGhzChat" @@ -14,7 +15,7 @@ struct SubGhzChatWorker { FuriMessageQueue* event_queue; uint32_t last_time_rx_data; - Cli* cli; + PipeSide* pipe; }; /** Worker thread @@ -30,7 +31,7 @@ static int32_t subghz_chat_worker_thread(void* context) { event.event = SubGhzChatEventUserEntrance; furi_message_queue_put(instance->event_queue, &event, 0); while(instance->worker_running) { - if(cli_read_timeout(instance->cli, (uint8_t*)&c, 1, 1000) == 1) { + if(pipe_receive(instance->pipe, (uint8_t*)&c, 1) == 1) { event.event = SubGhzChatEventInputData; event.c = c; furi_message_queue_put(instance->event_queue, &event, FuriWaitForever); @@ -55,10 +56,10 @@ static void subghz_chat_worker_update_rx_event_chat(void* context) { furi_message_queue_put(instance->event_queue, &event, FuriWaitForever); } -SubGhzChatWorker* subghz_chat_worker_alloc(Cli* cli) { +SubGhzChatWorker* subghz_chat_worker_alloc(PipeSide* pipe) { SubGhzChatWorker* instance = malloc(sizeof(SubGhzChatWorker)); - instance->cli = cli; + instance->pipe = pipe; instance->thread = furi_thread_alloc_ex("SubGhzChat", 2048, subghz_chat_worker_thread, instance); diff --git a/applications/main/subghz/helpers/subghz_chat.h b/applications/main/subghz/helpers/subghz_chat.h index 2c454b75d..0d1497506 100644 --- a/applications/main/subghz/helpers/subghz_chat.h +++ b/applications/main/subghz/helpers/subghz_chat.h @@ -2,6 +2,7 @@ #include "../subghz_i.h" #include #include +#include typedef struct SubGhzChatWorker SubGhzChatWorker; @@ -19,7 +20,7 @@ typedef struct { char c; } SubGhzChatEvent; -SubGhzChatWorker* subghz_chat_worker_alloc(Cli* cli); +SubGhzChatWorker* subghz_chat_worker_alloc(PipeSide* pipe); void subghz_chat_worker_free(SubGhzChatWorker* instance); bool subghz_chat_worker_start( SubGhzChatWorker* instance, diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 54e02196d..a07ea5a7e 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -16,6 +17,7 @@ #include #include +#include #include "helpers/subghz_chat.h" @@ -61,7 +63,7 @@ static SubGhzEnvironment* subghz_cli_environment_init(void) { return environment; } -void subghz_cli_command_tx_carrier(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_tx_carrier(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; @@ -91,7 +93,7 @@ void subghz_cli_command_tx_carrier(Cli* cli, FuriString* args, void* context) { if(furi_hal_subghz_tx()) { printf("Transmitting at frequency %lu Hz\r\n", frequency); printf("Press CTRL+C to stop\r\n"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); } } else { @@ -104,7 +106,7 @@ void subghz_cli_command_tx_carrier(Cli* cli, FuriString* args, void* context) { furi_hal_power_suppress_charge_exit(); } -void subghz_cli_command_rx_carrier(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_rx_carrier(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; @@ -132,7 +134,7 @@ void subghz_cli_command_rx_carrier(Cli* cli, FuriString* args, void* context) { furi_hal_subghz_rx(); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); printf("RSSI: %03.1fdbm\r", (double)furi_hal_subghz_get_rssi()); fflush(stdout); @@ -165,7 +167,7 @@ static const SubGhzDevice* subghz_cli_command_get_device(uint32_t* device_ind) { return device; } -void subghz_cli_command_tx(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_tx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; uint32_t key = 0x0074BADE; @@ -235,7 +237,9 @@ void subghz_cli_command_tx(Cli* cli, FuriString* args, void* context) { furi_hal_power_suppress_charge_enter(); if(subghz_devices_start_async_tx(device, subghz_transmitter_yield, transmitter)) { - while(!(subghz_devices_is_async_complete_tx(device) || cli_cmd_interrupt_received(cli))) { + while( + !(subghz_devices_is_async_complete_tx(device) || + cli_is_pipe_broken_or_is_etx_next_char(pipe))) { printf("."); fflush(stdout); furi_delay_ms(333); @@ -292,7 +296,7 @@ static void subghz_cli_command_rx_callback( furi_string_free(text); } -void subghz_cli_command_rx(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_rx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; uint32_t device_ind = 0; // 0 - CC1101_INT, 1 - CC1101_EXT @@ -348,7 +352,7 @@ void subghz_cli_command_rx(Cli* cli, FuriString* args, void* context) { frequency, device_ind); LevelDuration level_duration; - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { int ret = furi_stream_buffer_receive( instance->stream, &level_duration, sizeof(LevelDuration), 10); if(ret == sizeof(LevelDuration)) { @@ -381,7 +385,7 @@ void subghz_cli_command_rx(Cli* cli, FuriString* args, void* context) { free(instance); } -void subghz_cli_command_rx_raw(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_rx_raw(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); uint32_t frequency = 433920000; @@ -419,7 +423,7 @@ void subghz_cli_command_rx_raw(Cli* cli, FuriString* args, void* context) { printf("Listening at %lu. Press CTRL+C to stop\r\n", frequency); LevelDuration level_duration; size_t counter = 0; - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { int ret = furi_stream_buffer_receive( instance->stream, &level_duration, sizeof(LevelDuration), 10); if(ret == 0) { @@ -455,7 +459,7 @@ void subghz_cli_command_rx_raw(Cli* cli, FuriString* args, void* context) { free(instance); } -void subghz_cli_command_decode_raw(Cli* cli, FuriString* args, void* context) { +void subghz_cli_command_decode_raw(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* file_name; file_name = furi_string_alloc(); @@ -525,7 +529,7 @@ void subghz_cli_command_decode_raw(Cli* cli, FuriString* args, void* context) { furi_string_get_cstr(file_name)); LevelDuration level_duration; - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_us(500); //you need to have time to read from the file from the SD card level_duration = subghz_file_encoder_worker_get_level_duration(file_worker_encoder); if(!level_duration_is_reset(level_duration)) { @@ -570,7 +574,7 @@ static FuriHalSubGhzPreset subghz_cli_get_preset_name(const char* preset_name) { return preset; } -void subghz_cli_command_tx_from_file(Cli* cli, FuriString* args, void* context) { // -V524 +void subghz_cli_command_tx_from_file(PipeSide* pipe, FuriString* args, void* context) { // -V524 UNUSED(context); FuriString* file_name; file_name = furi_string_alloc(); @@ -774,7 +778,7 @@ void subghz_cli_command_tx_from_file(Cli* cli, FuriString* args, void* context) if(subghz_devices_start_async_tx(device, subghz_transmitter_yield, transmitter)) { while( !(subghz_devices_is_async_complete_tx(device) || - cli_cmd_interrupt_received(cli))) { + cli_is_pipe_broken_or_is_etx_next_char(pipe))) { printf("."); fflush(stdout); furi_delay_ms(333); @@ -788,11 +792,11 @@ void subghz_cli_command_tx_from_file(Cli* cli, FuriString* args, void* context) if(!strcmp(furi_string_get_cstr(temp_str), "RAW")) { subghz_transmitter_stop(transmitter); repeat--; - if(!cli_cmd_interrupt_received(cli) && repeat) + if(!cli_is_pipe_broken_or_is_etx_next_char(pipe) && repeat) subghz_transmitter_deserialize(transmitter, fff_data_raw); } - } while(!cli_cmd_interrupt_received(cli) && + } while(!cli_is_pipe_broken_or_is_etx_next_char(pipe) && (repeat && !strcmp(furi_string_get_cstr(temp_str), "RAW"))); subghz_devices_sleep(device); @@ -837,8 +841,8 @@ static void subghz_cli_command_print_usage(void) { } } -static void subghz_cli_command_encrypt_keeloq(Cli* cli, FuriString* args) { - UNUSED(cli); +static void subghz_cli_command_encrypt_keeloq(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); uint8_t iv[16]; FuriString* source; @@ -880,8 +884,8 @@ static void subghz_cli_command_encrypt_keeloq(Cli* cli, FuriString* args) { furi_string_free(source); } -static void subghz_cli_command_encrypt_raw(Cli* cli, FuriString* args) { - UNUSED(cli); +static void subghz_cli_command_encrypt_raw(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); uint8_t iv[16]; FuriString* source; @@ -917,7 +921,7 @@ static void subghz_cli_command_encrypt_raw(Cli* cli, FuriString* args) { furi_string_free(source); } -static void subghz_cli_command_chat(Cli* cli, FuriString* args) { +static void subghz_cli_command_chat(PipeSide* pipe, FuriString* args) { uint32_t frequency = 433920000; uint32_t device_ind = 0; // 0 - CC1101_INT, 1 - CC1101_EXT @@ -951,7 +955,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { return; } - SubGhzChatWorker* subghz_chat = subghz_chat_worker_alloc(cli); + SubGhzChatWorker* subghz_chat = subghz_chat_worker_alloc(pipe); if(!subghz_chat_worker_start(subghz_chat, device, frequency)) { printf("Startup error SubGhzChatWorker\r\n"); @@ -992,13 +996,12 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { chat_event = subghz_chat_worker_get_event_chat(subghz_chat); switch(chat_event.event) { case SubGhzChatEventInputData: - if(chat_event.c == CliSymbolAsciiETX) { + if(chat_event.c == CliKeyETX) { printf("\r\n"); chat_event.event = SubGhzChatEventUserExit; subghz_chat_worker_put_event_chat(subghz_chat, &chat_event); break; - } else if( - (chat_event.c == CliSymbolAsciiBackspace) || (chat_event.c == CliSymbolAsciiDel)) { + } else if((chat_event.c == CliKeyBackspace) || (chat_event.c == CliKeyDEL)) { size_t len = furi_string_utf8_length(input); if(len > furi_string_utf8_length(name)) { printf("%s", "\e[D\e[1P"); @@ -1020,7 +1023,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { } furi_string_set(input, sysmsg); } - } else if(chat_event.c == CliSymbolAsciiCR) { + } else if(chat_event.c == CliKeyCR) { printf("\r\n"); furi_string_push_back(input, '\r'); furi_string_push_back(input, '\n'); @@ -1034,7 +1037,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { furi_string_printf(input, "%s", furi_string_get_cstr(name)); printf("%s", furi_string_get_cstr(input)); fflush(stdout); - } else if(chat_event.c == CliSymbolAsciiLF) { + } else if(chat_event.c == CliKeyLF) { //cut out the symbol \n } else { putc(chat_event.c, stdout); @@ -1088,7 +1091,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { break; } } - if(!cli_is_connected(cli)) { + if(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { printf("\r\n"); chat_event.event = SubGhzChatEventUserExit; subghz_chat_worker_put_event_chat(subghz_chat, &chat_event); @@ -1113,7 +1116,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { printf("\r\nExit chat\r\n"); } -static void subghz_cli_command(Cli* cli, FuriString* args, void* context) { +static void subghz_cli_command(PipeSide* pipe, FuriString* args, void* context) { FuriString* cmd; cmd = furi_string_alloc(); @@ -1124,53 +1127,53 @@ static void subghz_cli_command(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "chat") == 0) { - subghz_cli_command_chat(cli, args); + subghz_cli_command_chat(pipe, args); break; } if(furi_string_cmp_str(cmd, "tx") == 0) { - subghz_cli_command_tx(cli, args, context); + subghz_cli_command_tx(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "rx") == 0) { - subghz_cli_command_rx(cli, args, context); + subghz_cli_command_rx(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "rx_raw") == 0) { - subghz_cli_command_rx_raw(cli, args, context); + subghz_cli_command_rx_raw(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "decode_raw") == 0) { - subghz_cli_command_decode_raw(cli, args, context); + subghz_cli_command_decode_raw(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "tx_from_file") == 0) { - subghz_cli_command_tx_from_file(cli, args, context); + subghz_cli_command_tx_from_file(pipe, args, context); break; } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { if(furi_string_cmp_str(cmd, "encrypt_keeloq") == 0) { - subghz_cli_command_encrypt_keeloq(cli, args); + subghz_cli_command_encrypt_keeloq(pipe, args); break; } if(furi_string_cmp_str(cmd, "encrypt_raw") == 0) { - subghz_cli_command_encrypt_raw(cli, args); + subghz_cli_command_encrypt_raw(pipe, args); break; } if(furi_string_cmp_str(cmd, "tx_carrier") == 0) { - subghz_cli_command_tx_carrier(cli, args, context); + subghz_cli_command_tx_carrier(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "rx_carrier") == 0) { - subghz_cli_command_rx_carrier(cli, args, context); + subghz_cli_command_rx_carrier(pipe, args, context); break; } } diff --git a/applications/services/application.fam b/applications/services/application.fam index 9ffb26dd6..a1a0429fa 100644 --- a/applications/services/application.fam +++ b/applications/services/application.fam @@ -3,6 +3,7 @@ App( name="Basic services", apptype=FlipperAppType.METAPACKAGE, provides=[ + "cli_vcp", "crypto_start", "rpc_start", "expansion_start", diff --git a/applications/services/bt/bt_cli.c b/applications/services/bt/bt_cli.c index 7505c424d..9ef3ef8a2 100644 --- a/applications/services/bt/bt_cli.c +++ b/applications/services/bt/bt_cli.c @@ -2,14 +2,15 @@ #include #include #include +#include #include #include "bt_settings.h" #include "bt_service/bt.h" #include -static void bt_cli_command_hci_info(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void bt_cli_command_hci_info(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); FuriString* buffer; @@ -19,7 +20,7 @@ static void bt_cli_command_hci_info(Cli* cli, FuriString* args, void* context) { furi_string_free(buffer); } -static void bt_cli_command_carrier_tx(Cli* cli, FuriString* args, void* context) { +static void bt_cli_command_carrier_tx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int channel = 0; int power = 0; @@ -41,7 +42,7 @@ static void bt_cli_command_carrier_tx(Cli* cli, FuriString* args, void* context) printf("Press CTRL+C to stop\r\n"); furi_hal_bt_start_tone_tx(channel, 0x19 + power); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); } furi_hal_bt_stop_tone_tx(); @@ -51,7 +52,7 @@ static void bt_cli_command_carrier_tx(Cli* cli, FuriString* args, void* context) } while(false); } -static void bt_cli_command_carrier_rx(Cli* cli, FuriString* args, void* context) { +static void bt_cli_command_carrier_rx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int channel = 0; @@ -69,7 +70,7 @@ static void bt_cli_command_carrier_rx(Cli* cli, FuriString* args, void* context) furi_hal_bt_start_packet_rx(channel, 1); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); printf("RSSI: %6.1f dB\r", (double)furi_hal_bt_get_rssi()); fflush(stdout); @@ -82,7 +83,7 @@ static void bt_cli_command_carrier_rx(Cli* cli, FuriString* args, void* context) } while(false); } -static void bt_cli_command_packet_tx(Cli* cli, FuriString* args, void* context) { +static void bt_cli_command_packet_tx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int channel = 0; int pattern = 0; @@ -119,7 +120,7 @@ static void bt_cli_command_packet_tx(Cli* cli, FuriString* args, void* context) printf("Press CTRL+C to stop\r\n"); furi_hal_bt_start_packet_tx(channel, pattern, datarate); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); } furi_hal_bt_stop_packet_test(); @@ -130,7 +131,7 @@ static void bt_cli_command_packet_tx(Cli* cli, FuriString* args, void* context) } while(false); } -static void bt_cli_command_packet_rx(Cli* cli, FuriString* args, void* context) { +static void bt_cli_command_packet_rx(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int channel = 0; int datarate = 1; @@ -152,7 +153,7 @@ static void bt_cli_command_packet_rx(Cli* cli, FuriString* args, void* context) printf("Press CTRL+C to stop\r\n"); furi_hal_bt_start_packet_rx(channel, datarate); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(250); printf("RSSI: %03.1f dB\r", (double)furi_hal_bt_get_rssi()); fflush(stdout); @@ -179,7 +180,7 @@ static void bt_cli_print_usage(void) { } } -static void bt_cli(Cli* cli, FuriString* args, void* context) { +static void bt_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); furi_record_open(RECORD_BT); @@ -194,24 +195,24 @@ static void bt_cli(Cli* cli, FuriString* args, void* context) { break; } if(furi_string_cmp_str(cmd, "hci_info") == 0) { - bt_cli_command_hci_info(cli, args, NULL); + bt_cli_command_hci_info(pipe, args, NULL); break; } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug) && furi_hal_bt_is_testing_supported()) { if(furi_string_cmp_str(cmd, "tx_carrier") == 0) { - bt_cli_command_carrier_tx(cli, args, NULL); + bt_cli_command_carrier_tx(pipe, args, NULL); break; } if(furi_string_cmp_str(cmd, "rx_carrier") == 0) { - bt_cli_command_carrier_rx(cli, args, NULL); + bt_cli_command_carrier_rx(pipe, args, NULL); break; } if(furi_string_cmp_str(cmd, "tx_packet") == 0) { - bt_cli_command_packet_tx(cli, args, NULL); + bt_cli_command_packet_tx(pipe, args, NULL); break; } if(furi_string_cmp_str(cmd, "rx_packet") == 0) { - bt_cli_command_packet_rx(cli, args, NULL); + bt_cli_command_packet_rx(pipe, args, NULL); break; } } diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 7a57bb607..60af336e5 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -1,10 +1,34 @@ App( appid="cli", - name="CliSrv", - apptype=FlipperAppType.SERVICE, - entry_point="cli_srv", + apptype=FlipperAppType.STARTUP, + entry_point="cli_on_system_start", cdefines=["SRV_CLI"], - stack_size=4 * 1024, - order=30, - sdk_headers=["cli.h", "cli_vcp.h"], + sources=[ + "cli.c", + "shell/cli_shell.c", + "shell/cli_shell_line.c", + "cli_commands.c", + "cli_command_gpio.c", + "cli_ansi.c", + ], + sdk_headers=[ + "cli.h", + "cli_ansi.h", + ], + # This STARTUP has to be processed before those that depend on the "cli" record. + # "cli" used to be a SERVICE, but it's been converted into a STARTUP in order to + # reduce RAM usage. The "block until record has been created" mechanism + # unfortunately leads to a deadlock if the STARTUPs are processed sequentially. + order=0, +) + +App( + appid="cli_vcp", + name="CliVcpSrv", + apptype=FlipperAppType.SERVICE, + entry_point="cli_vcp_srv", + stack_size=1024, + order=40, + sdk_headers=["cli_vcp.h"], + sources=["cli_vcp.c"], ) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index c2a0b9cb1..b51715660 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -1,406 +1,57 @@ +#include "cli.h" #include "cli_i.h" #include "cli_commands.h" -#include "cli_vcp.h" -#include -#include +#include "cli_ansi.h" +#include -#define TAG "CliSrv" +#define TAG "cli" -#define CLI_INPUT_LEN_LIMIT 256 +struct Cli { + CliCommandTree_t commands; + FuriMutex* mutex; +}; Cli* cli_alloc(void) { Cli* cli = malloc(sizeof(Cli)); - CliCommandTree_init(cli->commands); - - cli->last_line = furi_string_alloc(); - cli->line = furi_string_alloc(); - - cli->session = NULL; - cli->mutex = furi_mutex_alloc(FuriMutexTypeNormal); - - cli->idle_sem = furi_semaphore_alloc(1, 0); - return cli; } -void cli_putc(Cli* cli, char c) { - furi_check(cli); - if(cli->session != NULL) { - cli->session->tx((uint8_t*)&c, 1); - } -} - -char cli_getc(Cli* cli) { - furi_check(cli); - char c = 0; - if(cli->session != NULL) { - if(cli->session->rx((uint8_t*)&c, 1, FuriWaitForever) == 0) { - cli_reset(cli); - furi_delay_tick(10); - } - } else { - cli_reset(cli); - furi_delay_tick(10); - } - return c; -} - -void cli_write(Cli* cli, const uint8_t* buffer, size_t size) { - furi_check(cli); - if(cli->session != NULL) { - cli->session->tx(buffer, size); - } -} - -size_t cli_read(Cli* cli, uint8_t* buffer, size_t size) { - furi_check(cli); - if(cli->session != NULL) { - return cli->session->rx(buffer, size, FuriWaitForever); - } else { - return 0; - } -} - -size_t cli_read_timeout(Cli* cli, uint8_t* buffer, size_t size, uint32_t timeout) { - furi_check(cli); - if(cli->session != NULL) { - return cli->session->rx(buffer, size, timeout); - } else { - return 0; - } -} - -bool cli_is_connected(Cli* cli) { - furi_check(cli); - if(cli->session != NULL) { - return cli->session->is_connected(); - } - return false; -} - -bool cli_cmd_interrupt_received(Cli* cli) { - furi_check(cli); - char c = '\0'; - if(cli_is_connected(cli)) { - if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) { - return c == CliSymbolAsciiETX; - } - } else { - return true; - } - return false; -} - -void cli_print_usage(const char* cmd, const char* usage, const char* arg) { - furi_check(cmd); - furi_check(arg); - furi_check(usage); - - printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage); -} - -void cli_motd(void) { - printf("\r\n" - " _.-------.._ -,\r\n" - " .-\"```\"--..,,_/ /`-, -, \\ \r\n" - " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" - " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" - " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" - " | | | 0 | | .-' ,/` /\r\n" - " | ,..\\ \\ ,.-\"` ,/` /\r\n" - " ; : `/`\"\"\\` ,/--==,/-----,\r\n" - " | `-...| -.___-Z:_______J...---;\r\n" - " : ` _-'\r\n" - " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" - "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" - "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" - "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" - "\r\n" - "Welcome to Flipper Zero Command Line Interface!\r\n" - "Read the manual: https://docs.flipper.net/development/cli\r\n" - "Run `help` or `?` to list available commands\r\n" - "\r\n"); - - const Version* firmware_version = furi_hal_version_get_firmware_version(); - if(firmware_version) { - printf( - "Firmware version: %s %s (%s%s built on %s)\r\n", - version_get_gitbranch(firmware_version), - version_get_version(firmware_version), - version_get_githash(firmware_version), - version_get_dirty_flag(firmware_version) ? "-dirty" : "", - version_get_builddate(firmware_version)); - } -} - -void cli_nl(Cli* cli) { - UNUSED(cli); - printf("\r\n"); -} - -void cli_prompt(Cli* cli) { - UNUSED(cli); - printf("\r\n>: %s", furi_string_get_cstr(cli->line)); - fflush(stdout); -} - -void cli_reset(Cli* cli) { - // cli->last_line is cleared and cli->line's buffer moved to cli->last_line - furi_string_move(cli->last_line, cli->line); - // Reiniting cli->line - cli->line = furi_string_alloc(); - cli->cursor_position = 0; -} - -static void cli_handle_backspace(Cli* cli) { - if(cli->cursor_position > 0) { - furi_assert(furi_string_size(cli->line) > 0); - // Other side - printf("\e[D\e[1P"); - fflush(stdout); - // Our side - furi_string_replace_at(cli->line, cli->cursor_position - 1, 1, ""); - - cli->cursor_position--; - } else { - cli_putc(cli, CliSymbolAsciiBell); - } -} - -static void cli_normalize_line(Cli* cli) { - furi_string_trim(cli->line); - cli->cursor_position = furi_string_size(cli->line); -} - -static void cli_execute_command(Cli* cli, CliCommand* command, FuriString* args) { - if(!(command->flags & CliCommandFlagInsomniaSafe)) { - furi_hal_power_insomnia_enter(); - } - - // Ensure that we running alone - if(!(command->flags & CliCommandFlagParallelSafe)) { - Loader* loader = furi_record_open(RECORD_LOADER); - bool safety_lock = loader_lock(loader); - if(safety_lock) { - // Execute command - command->callback(cli, args, command->context); - loader_unlock(loader); - } else { - printf("Other application is running, close it first"); - } - furi_record_close(RECORD_LOADER); - } else { - // Execute command - command->callback(cli, args, command->context); - } - - if(!(command->flags & CliCommandFlagInsomniaSafe)) { - furi_hal_power_insomnia_exit(); - } -} - -static void cli_handle_enter(Cli* cli) { - cli_normalize_line(cli); - - if(furi_string_size(cli->line) == 0) { - cli_prompt(cli); - return; - } - - // Command and args container - FuriString* command; - command = furi_string_alloc(); - FuriString* args; - args = furi_string_alloc(); - - // Split command and args - size_t ws = furi_string_search_char(cli->line, ' '); - if(ws == FURI_STRING_FAILURE) { - furi_string_set(command, cli->line); - } else { - furi_string_set_n(command, cli->line, 0, ws); - furi_string_set_n(args, cli->line, ws, furi_string_size(cli->line)); - furi_string_trim(args); - } - - // Search for command - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommand* cli_command_ptr = CliCommandTree_get(cli->commands, command); - - if(cli_command_ptr) { //-V547 - CliCommand cli_command; - memcpy(&cli_command, cli_command_ptr, sizeof(CliCommand)); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - cli_nl(cli); - cli_execute_command(cli, &cli_command, args); - } else { - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - cli_nl(cli); - printf( - "`%s` command not found, use `help` or `?` to list all available commands", - furi_string_get_cstr(command)); - cli_putc(cli, CliSymbolAsciiBell); - } - - cli_reset(cli); - cli_prompt(cli); - - // Cleanup command and args - furi_string_free(command); - furi_string_free(args); -} - -static void cli_handle_autocomplete(Cli* cli) { - cli_normalize_line(cli); - - if(furi_string_size(cli->line) == 0) { - return; - } - - cli_nl(cli); - - // Prepare common base for autocomplete - FuriString* common; - common = furi_string_alloc(); - // Iterate throw commands - for - M_EACH(cli_command, cli->commands, CliCommandTree_t) { - // Process only if starts with line buffer - if(furi_string_start_with(*cli_command->key_ptr, cli->line)) { - // Show autocomplete option - printf("%s\r\n", furi_string_get_cstr(*cli_command->key_ptr)); - // Process common base for autocomplete - if(furi_string_size(common) > 0) { - // Choose shortest string - const size_t key_size = furi_string_size(*cli_command->key_ptr); - const size_t common_size = furi_string_size(common); - const size_t min_size = key_size > common_size ? common_size : key_size; - size_t i = 0; - while(i < min_size) { - // Stop when do not match - if(furi_string_get_char(*cli_command->key_ptr, i) != - furi_string_get_char(common, i)) { - break; - } - i++; - } - // Cut right part if any - furi_string_left(common, i); - } else { - // Start with something - furi_string_set(common, *cli_command->key_ptr); - } - } - } - // Replace line buffer if autocomplete better - if(furi_string_size(common) > furi_string_size(cli->line)) { - furi_string_set(cli->line, common); - cli->cursor_position = furi_string_size(cli->line); - } - // Cleanup - furi_string_free(common); - // Show prompt - cli_prompt(cli); -} - -static void cli_handle_escape(Cli* cli, char c) { - if(c == 'A') { - // Use previous command if line buffer is empty - if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) { - // Set line buffer and cursor position - furi_string_set(cli->line, cli->last_line); - cli->cursor_position = furi_string_size(cli->line); - // Show new line to user - printf("%s", furi_string_get_cstr(cli->line)); - } - } else if(c == 'B') { - } else if(c == 'C') { - if(cli->cursor_position < furi_string_size(cli->line)) { - cli->cursor_position++; - printf("\e[C"); - } - } else if(c == 'D') { - if(cli->cursor_position > 0) { - cli->cursor_position--; - printf("\e[D"); - } - } - fflush(stdout); -} - -void cli_process_input(Cli* cli) { - char in_chr = cli_getc(cli); - size_t rx_len; - - if(in_chr == CliSymbolAsciiTab) { - cli_handle_autocomplete(cli); - } else if(in_chr == CliSymbolAsciiSOH) { - furi_delay_ms(33); // We are too fast, Minicom is not ready yet - cli_motd(); - cli_prompt(cli); - } else if(in_chr == CliSymbolAsciiETX) { - cli_reset(cli); - cli_prompt(cli); - } else if(in_chr == CliSymbolAsciiEOT) { - cli_reset(cli); - } else if(in_chr == CliSymbolAsciiEsc) { - rx_len = cli_read(cli, (uint8_t*)&in_chr, 1); - if((rx_len > 0) && (in_chr == '[')) { - cli_read(cli, (uint8_t*)&in_chr, 1); - cli_handle_escape(cli, in_chr); - } else { - cli_putc(cli, CliSymbolAsciiBell); - } - } else if(in_chr == CliSymbolAsciiBackspace || in_chr == CliSymbolAsciiDel) { - cli_handle_backspace(cli); - } else if(in_chr == CliSymbolAsciiCR) { - cli_handle_enter(cli); - } else if( - (in_chr >= 0x20 && in_chr < 0x7F) && //-V560 - (furi_string_size(cli->line) < CLI_INPUT_LEN_LIMIT)) { - if(cli->cursor_position == furi_string_size(cli->line)) { - furi_string_push_back(cli->line, in_chr); - cli_putc(cli, in_chr); - } else { - // Insert character to line buffer - const char in_str[2] = {in_chr, 0}; - furi_string_replace_at(cli->line, cli->cursor_position, 0, in_str); - - // Print character in replace mode - printf("\e[4h%c\e[4l", in_chr); - fflush(stdout); - } - cli->cursor_position++; - } else { - cli_putc(cli, CliSymbolAsciiBell); - } -} - void cli_add_command( Cli* cli, const char* name, CliCommandFlag flags, - CliCallback callback, + CliExecuteCallback callback, void* context) { + cli_add_command_ex(cli, name, flags, callback, context, CLI_BUILTIN_COMMAND_STACK_SIZE); +} + +void cli_add_command_ex( + Cli* cli, + const char* name, + CliCommandFlag flags, + CliExecuteCallback callback, + void* context, + size_t stack_size) { furi_check(cli); + furi_check(name); + furi_check(callback); + FuriString* name_str; name_str = furi_string_alloc_set(name); - furi_string_trim(name_str); + // command cannot contain spaces + furi_check(furi_string_search_char(name_str, ' ') == FURI_STRING_FAILURE); - size_t name_replace; - do { - name_replace = furi_string_replace(name_str, " ", "_"); - } while(name_replace != FURI_STRING_FAILURE); - - CliCommand c; - c.callback = callback; - c.context = context; - c.flags = flags; + CliCommand command = { + .context = context, + .execute_callback = callback, + .flags = flags, + .stack_depth = stack_size, + }; furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_set_at(cli->commands, name_str, c); + CliCommandTree_set_at(cli->commands, name_str, command); furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); furi_string_free(name_str); @@ -424,61 +75,49 @@ void cli_delete_command(Cli* cli, const char* name) { furi_string_free(name_str); } -void cli_session_open(Cli* cli, const void* session) { - furi_check(cli); - +bool cli_get_command(Cli* cli, FuriString* command, CliCommand* result) { + furi_assert(cli); furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - cli->session = session; - if(cli->session != NULL) { - cli->session->init(); - furi_thread_set_stdout_callback(cli->session->tx_stdout, NULL); - } else { - furi_thread_set_stdout_callback(NULL, NULL); - } - furi_semaphore_release(cli->idle_sem); + CliCommand* data = CliCommandTree_get(cli->commands, command); + if(data) *result = *data; + furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); + + return !!data; } -void cli_session_close(Cli* cli) { - furi_check(cli); - +void cli_lock_commands(Cli* cli) { + furi_assert(cli); furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - if(cli->session != NULL) { - cli->session->deinit(); - } - cli->session = NULL; - furi_thread_set_stdout_callback(NULL, NULL); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); } -int32_t cli_srv(void* p) { - UNUSED(p); +void cli_unlock_commands(Cli* cli) { + furi_assert(cli); + furi_mutex_release(cli->mutex); +} + +CliCommandTree_t* cli_get_commands(Cli* cli) { + furi_assert(cli); + return &cli->commands; +} + +bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side) { + if(pipe_state(side) == PipeStateBroken) return true; + if(!pipe_bytes_available(side)) return false; + char c = getchar(); + return c == CliKeyETX; +} + +void cli_print_usage(const char* cmd, const char* usage, const char* arg) { + furi_check(cmd); + furi_check(arg); + furi_check(usage); + + printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage); +} + +void cli_on_system_start(void) { Cli* cli = cli_alloc(); - - // Init basic cli commands cli_commands_init(cli); - furi_record_create(RECORD_CLI, cli); - - if(cli->session != NULL) { - furi_thread_set_stdout_callback(cli->session->tx_stdout, NULL); - } else { - furi_thread_set_stdout_callback(NULL, NULL); - } - - if(furi_hal_rtc_get_boot_mode() == FuriHalRtcBootModeNormal) { - cli_session_open(cli, &cli_vcp); - } else { - FURI_LOG_W(TAG, "Skipping start in special boot mode"); - } - - while(1) { - if(cli->session != NULL) { - cli_process_input(cli); - } else { - furi_check(furi_semaphore_acquire(cli->idle_sem, FuriWaitForever) == FuriStatusOk); - } - } - - return 0; } diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index fe8f09032..211e89d88 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -1,64 +1,102 @@ /** * @file cli.h - * Cli API + * API for registering commands with the CLI */ #pragma once #include +#include +#include "cli_ansi.h" +#include #ifdef __cplusplus extern "C" { #endif -typedef enum { - CliSymbolAsciiSOH = 0x01, - CliSymbolAsciiETX = 0x03, - CliSymbolAsciiEOT = 0x04, - CliSymbolAsciiBell = 0x07, - CliSymbolAsciiBackspace = 0x08, - CliSymbolAsciiTab = 0x09, - CliSymbolAsciiLF = 0x0A, - CliSymbolAsciiCR = 0x0D, - CliSymbolAsciiEsc = 0x1B, - CliSymbolAsciiUS = 0x1F, - CliSymbolAsciiSpace = 0x20, - CliSymbolAsciiDel = 0x7F, -} CliSymbols; - -typedef enum { - CliCommandFlagDefault = 0, /**< Default, loader lock is used */ - CliCommandFlagParallelSafe = - (1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */ - CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ -} CliCommandFlag; - #define RECORD_CLI "cli" +typedef enum { + CliCommandFlagDefault = 0, /**< Default */ + CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps */ + CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ + CliCommandFlagDontAttachStdio = (1 << 2), /**< Do no attach I/O pipe to thread stdio */ +} CliCommandFlag; + /** Cli type anonymous structure */ typedef struct Cli Cli; -/** Cli callback function pointer. Implement this interface and use - * add_cli_command - * @param args string with what was passed after command - * @param context pointer to whatever you gave us on cli_add_command +/** + * @brief CLI execution callback pointer. Implement this interface and use + * `add_cli_command`. + * + * This callback will be called from a separate thread spawned just for your + * command. The pipe will be installed as the thread's stdio, so you can use + * `printf`, `getchar` and other standard functions to communicate with the + * user. + * + * @param [in] pipe Pipe that can be used to send and receive data. If + * `CliCommandFlagDontAttachStdio` was not set, you can + * also use standard C functions (printf, getc, etc.) to + * access this pipe. + * @param [in] args String with what was passed after the command + * @param [in] context Whatever you provided to `cli_add_command` */ -typedef void (*CliCallback)(Cli* cli, FuriString* args, void* context); +typedef void (*CliExecuteCallback)(PipeSide* pipe, FuriString* args, void* context); -/** Add cli command Registers you command callback +/** + * @brief Registers a command with the CLI. Provides less options than the `_ex` + * counterpart. * - * @param cli pointer to cli instance - * @param name command name - * @param flags CliCommandFlag - * @param callback callback function - * @param context pointer to whatever we need to pass to callback + * @param [in] cli Pointer to CLI instance + * @param [in] name Command name + * @param [in] flags CliCommandFlag + * @param [in] callback Callback function + * @param [in] context Custom context */ void cli_add_command( Cli* cli, const char* name, CliCommandFlag flags, - CliCallback callback, + CliExecuteCallback callback, void* context); +/** + * @brief Registers a command with the CLI. Provides more options than the + * non-`_ex` counterpart. + * + * @param [in] cli Pointer to CLI instance + * @param [in] name Command name + * @param [in] flags CliCommandFlag + * @param [in] callback Callback function + * @param [in] context Custom context + * @param [in] stack_size Thread stack size + */ +void cli_add_command_ex( + Cli* cli, + const char* name, + CliCommandFlag flags, + CliExecuteCallback callback, + void* context, + size_t stack_size); + +/** + * @brief Deletes a cli command + * + * @param [in] cli pointer to cli instance + * @param [in] name command name + */ +void cli_delete_command(Cli* cli, const char* name); + +/** + * @brief Detects if Ctrl+C has been pressed or session has been terminated + * + * @param [in] side Pointer to pipe side given to the command thread + * @warning This function also assumes that the pipe is installed as the + * thread's stdio + * @warning This function will consume 1 byte from the pipe + */ +bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side); + /** Print unified cmd usage tip * * @param cmd cmd name @@ -67,68 +105,6 @@ void cli_add_command( */ void cli_print_usage(const char* cmd, const char* usage, const char* arg); -/** Delete cli command - * - * @param cli pointer to cli instance - * @param name command name - */ -void cli_delete_command(Cli* cli, const char* name); - -/** Read from terminal - * - * @param cli Cli instance - * @param buffer pointer to buffer - * @param size size of buffer in bytes - * - * @return bytes read - */ -size_t cli_read(Cli* cli, uint8_t* buffer, size_t size); - -/** Non-blocking read from terminal - * - * @param cli Cli instance - * @param buffer pointer to buffer - * @param size size of buffer in bytes - * @param timeout timeout value in ms - * - * @return bytes read - */ -size_t cli_read_timeout(Cli* cli, uint8_t* buffer, size_t size, uint32_t timeout); - -/** Non-blocking check for interrupt command received - * - * @param cli Cli instance - * - * @return true if received - */ -bool cli_cmd_interrupt_received(Cli* cli); - -/** Write to terminal Do it only from inside of cli call. - * - * @param cli Cli instance - * @param buffer pointer to buffer - * @param size size of buffer in bytes - */ -void cli_write(Cli* cli, const uint8_t* buffer, size_t size); - -/** Read character - * - * @param cli Cli instance - * - * @return char - */ -char cli_getc(Cli* cli); - -/** New line Send new ine sequence - */ -void cli_nl(Cli* cli); - -void cli_session_open(Cli* cli, const void* session); - -void cli_session_close(Cli* cli); - -bool cli_is_connected(Cli* cli); - #ifdef __cplusplus } #endif diff --git a/applications/services/cli/cli_ansi.c b/applications/services/cli/cli_ansi.c new file mode 100644 index 000000000..81167643b --- /dev/null +++ b/applications/services/cli/cli_ansi.c @@ -0,0 +1,123 @@ +#include "cli_ansi.h" + +typedef enum { + CliAnsiParserStateInitial, + CliAnsiParserStateEscape, + CliAnsiParserStateEscapeBrace, + CliAnsiParserStateEscapeBraceOne, + CliAnsiParserStateEscapeBraceOneSemicolon, + CliAnsiParserStateEscapeBraceOneSemicolonModifiers, +} CliAnsiParserState; + +struct CliAnsiParser { + CliAnsiParserState state; + CliModKey modifiers; +}; + +CliAnsiParser* cli_ansi_parser_alloc(void) { + CliAnsiParser* parser = malloc(sizeof(CliAnsiParser)); + return parser; +} + +void cli_ansi_parser_free(CliAnsiParser* parser) { + free(parser); +} + +/** + * @brief Converts a single character representing a special key into the enum + * representation + */ +static CliKey cli_ansi_key_from_mnemonic(char c) { + switch(c) { + case 'A': + return CliKeyUp; + case 'B': + return CliKeyDown; + case 'C': + return CliKeyRight; + case 'D': + return CliKeyLeft; + case 'F': + return CliKeyEnd; + case 'H': + return CliKeyHome; + default: + return CliKeyUnrecognized; + } +} + +#define PARSER_RESET_AND_RETURN(parser, modifiers_val, key_val) \ + do { \ + parser->state = CliAnsiParserStateInitial; \ + return (CliAnsiParserResult){ \ + .is_done = true, \ + .result = (CliKeyCombo){ \ + .modifiers = modifiers_val, \ + .key = key_val, \ + }}; \ + } while(0); + +CliAnsiParserResult cli_ansi_parser_feed(CliAnsiParser* parser, char c) { + switch(parser->state) { + case CliAnsiParserStateInitial: + // -> + if(c != CliKeyEsc) PARSER_RESET_AND_RETURN(parser, CliModKeyNo, c); // -V1048 + + // ... + parser->state = CliAnsiParserStateEscape; + break; + + case CliAnsiParserStateEscape: + // -> + if(c == CliKeyEsc) PARSER_RESET_AND_RETURN(parser, CliModKeyNo, c); + + // -> Alt + + if(c != '[') PARSER_RESET_AND_RETURN(parser, CliModKeyAlt, c); + + // [ ... + parser->state = CliAnsiParserStateEscapeBrace; + break; + + case CliAnsiParserStateEscapeBrace: + // [ -> + if(c != '1') PARSER_RESET_AND_RETURN(parser, CliModKeyNo, cli_ansi_key_from_mnemonic(c)); + + // [ 1 ... + parser->state = CliAnsiParserStateEscapeBraceOne; + break; + + case CliAnsiParserStateEscapeBraceOne: + // [ 1 -> error + if(c != ';') PARSER_RESET_AND_RETURN(parser, CliModKeyNo, CliKeyUnrecognized); + + // [ 1 ; ... + parser->state = CliAnsiParserStateEscapeBraceOneSemicolon; + break; + + case CliAnsiParserStateEscapeBraceOneSemicolon: + // [ 1 ; ... + parser->modifiers = (c - '0'); + parser->modifiers &= ~1; + parser->state = CliAnsiParserStateEscapeBraceOneSemicolonModifiers; + break; + + case CliAnsiParserStateEscapeBraceOneSemicolonModifiers: + // [ 1 ; -> + + PARSER_RESET_AND_RETURN(parser, parser->modifiers, cli_ansi_key_from_mnemonic(c)); + } + + return (CliAnsiParserResult){.is_done = false}; +} + +CliAnsiParserResult cli_ansi_parser_feed_timeout(CliAnsiParser* parser) { + CliAnsiParserResult result = {.is_done = false}; + + if(parser->state == CliAnsiParserStateEscape) { + result.is_done = true; + result.result.key = CliKeyEsc; + result.result.modifiers = CliModKeyNo; + } + + parser->state = CliAnsiParserStateInitial; + return result; +} diff --git a/applications/services/cli/cli_ansi.h b/applications/services/cli/cli_ansi.h new file mode 100644 index 000000000..20bf33d8e --- /dev/null +++ b/applications/services/cli/cli_ansi.h @@ -0,0 +1,153 @@ +#pragma once + +#include "cli.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// text styling + +#define ANSI_RESET "\e[0m" +#define ANSI_BOLD "\e[1m" +#define ANSI_FAINT "\e[2m" +#define ANSI_INVERT "\e[7m" + +#define ANSI_FG_BLACK "\e[30m" +#define ANSI_FG_RED "\e[31m" +#define ANSI_FG_GREEN "\e[32m" +#define ANSI_FG_YELLOW "\e[33m" +#define ANSI_FG_BLUE "\e[34m" +#define ANSI_FG_MAGENTA "\e[35m" +#define ANSI_FG_CYAN "\e[36m" +#define ANSI_FG_WHITE "\e[37m" +#define ANSI_FG_BR_BLACK "\e[90m" +#define ANSI_FG_BR_RED "\e[91m" +#define ANSI_FG_BR_GREEN "\e[92m" +#define ANSI_FG_BR_YELLOW "\e[93m" +#define ANSI_FG_BR_BLUE "\e[94m" +#define ANSI_FG_BR_MAGENTA "\e[95m" +#define ANSI_FG_BR_CYAN "\e[96m" +#define ANSI_FG_BR_WHITE "\e[97m" + +#define ANSI_BG_BLACK "\e[40m" +#define ANSI_BG_RED "\e[41m" +#define ANSI_BG_GREEN "\e[42m" +#define ANSI_BG_YELLOW "\e[43m" +#define ANSI_BG_BLUE "\e[44m" +#define ANSI_BG_MAGENTA "\e[45m" +#define ANSI_BG_CYAN "\e[46m" +#define ANSI_BG_WHITE "\e[47m" +#define ANSI_BG_BR_BLACK "\e[100m" +#define ANSI_BG_BR_RED "\e[101m" +#define ANSI_BG_BR_GREEN "\e[102m" +#define ANSI_BG_BR_YELLOW "\e[103m" +#define ANSI_BG_BR_BLUE "\e[104m" +#define ANSI_BG_BR_MAGENTA "\e[105m" +#define ANSI_BG_BR_CYAN "\e[106m" +#define ANSI_BG_BR_WHITE "\e[107m" + +#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m" + +// cursor positioning + +#define ANSI_CURSOR_UP_BY(rows) "\e[" rows "A" +#define ANSI_CURSOR_DOWN_BY(rows) "\e[" rows "B" +#define ANSI_CURSOR_RIGHT_BY(cols) "\e[" cols "C" +#define ANSI_CURSOR_LEFT_BY(cols) "\e[" cols "D" +#define ANSI_CURSOR_DOWN_BY_AND_FIRST_COLUMN(rows) "\e[" rows "E" +#define ANSI_CURSOR_UP_BY_AND_FIRST_COLUMN(rows) "\e[" rows "F" +#define ANSI_CURSOR_HOR_POS(pos) "\e[" pos "G" +#define ANSI_CURSOR_POS(row, col) "\e[" row ";" col "H" + +// erasing + +#define ANSI_ERASE_FROM_CURSOR_TO_END "0" +#define ANSI_ERASE_FROM_START_TO_CURSOR "1" +#define ANSI_ERASE_ENTIRE "2" + +#define ANSI_ERASE_DISPLAY(portion) "\e[" portion "J" +#define ANSI_ERASE_LINE(portion) "\e[" portion "K" +#define ANSI_ERASE_SCROLLBACK_BUFFER ANSI_ERASE_DISPLAY("3") + +// misc + +#define ANSI_INSERT_MODE_ENABLE "\e[4h" +#define ANSI_INSERT_MODE_DISABLE "\e[4l" + +typedef enum { + CliKeyUnrecognized = 0, + + CliKeySOH = 0x01, + CliKeyETX = 0x03, + CliKeyEOT = 0x04, + CliKeyBell = 0x07, + CliKeyBackspace = 0x08, + CliKeyTab = 0x09, + CliKeyLF = 0x0A, + CliKeyFF = 0x0C, + CliKeyCR = 0x0D, + CliKeyETB = 0x17, + CliKeyEsc = 0x1B, + CliKeyUS = 0x1F, + CliKeySpace = 0x20, + CliKeyDEL = 0x7F, + + CliKeySpecial = 0x80, + CliKeyLeft, + CliKeyRight, + CliKeyUp, + CliKeyDown, + CliKeyHome, + CliKeyEnd, +} CliKey; + +typedef enum { + CliModKeyNo = 0, + CliModKeyAlt = 2, + CliModKeyCtrl = 4, + CliModKeyMeta = 8, +} CliModKey; + +typedef struct { + CliModKey modifiers; + CliKey key; +} CliKeyCombo; + +typedef struct CliAnsiParser CliAnsiParser; + +typedef struct { + bool is_done; + CliKeyCombo result; +} CliAnsiParserResult; + +/** + * @brief Allocates an ANSI parser + */ +CliAnsiParser* cli_ansi_parser_alloc(void); + +/** + * @brief Frees an ANSI parser + */ +void cli_ansi_parser_free(CliAnsiParser* parser); + +/** + * @brief Feeds an ANSI parser a character + */ +CliAnsiParserResult cli_ansi_parser_feed(CliAnsiParser* parser, char c); + +/** + * @brief Feeds an ANSI parser a timeout event + * + * As a user of the ANSI parser API, you are responsible for calling this + * function some time after the last character was fed into the parser. The + * recommended timeout is about 10 ms. The exact value does not matter as long + * as it is small enough for the user not notice a delay, but big enough that + * when a terminal is sending an escape sequence, this function does not get + * called in between the characters of the sequence. + */ +CliAnsiParserResult cli_ansi_parser_feed_timeout(CliAnsiParser* parser); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/cli_command_gpio.c b/applications/services/cli/cli_command_gpio.c index 010a7dfbe..f37e6387f 100644 --- a/applications/services/cli/cli_command_gpio.c +++ b/applications/services/cli/cli_command_gpio.c @@ -3,6 +3,7 @@ #include #include #include +#include void cli_command_gpio_print_usage(void) { printf("Usage:\r\n"); @@ -70,8 +71,8 @@ static GpioParseReturn gpio_command_parse(FuriString* args, size_t* pin_num, uin return ret; } -void cli_command_gpio_mode(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_gpio_mode(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); size_t num = 0; @@ -93,7 +94,7 @@ void cli_command_gpio_mode(Cli* cli, FuriString* args, void* context) { if(gpio_pins[num].debug) { //-V779 printf( "Changing this pin mode may damage hardware. Are you sure you want to continue? (y/n)?\r\n"); - char c = cli_getc(cli); + char c = getchar(); if(c != 'y' && c != 'Y') { printf("Cancelled.\r\n"); return; @@ -110,8 +111,8 @@ void cli_command_gpio_mode(Cli* cli, FuriString* args, void* context) { } } -void cli_command_gpio_read(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_gpio_read(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); size_t num = 0; @@ -131,7 +132,8 @@ void cli_command_gpio_read(Cli* cli, FuriString* args, void* context) { printf("Pin %s <= %u", gpio_pins[num].name, val); } -void cli_command_gpio_set(Cli* cli, FuriString* args, void* context) { +void cli_command_gpio_set(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); size_t num = 0; @@ -159,7 +161,7 @@ void cli_command_gpio_set(Cli* cli, FuriString* args, void* context) { if(gpio_pins[num].debug) { printf( "Setting this pin may damage hardware. Are you sure you want to continue? (y/n)?\r\n"); - char c = cli_getc(cli); + char c = getchar(); if(c != 'y' && c != 'Y') { printf("Cancelled.\r\n"); return; @@ -170,7 +172,7 @@ void cli_command_gpio_set(Cli* cli, FuriString* args, void* context) { printf("Pin %s => %u", gpio_pins[num].name, !!value); } -void cli_command_gpio(Cli* cli, FuriString* args, void* context) { +void cli_command_gpio(PipeSide* pipe, FuriString* args, void* context) { FuriString* cmd; cmd = furi_string_alloc(); @@ -181,17 +183,17 @@ void cli_command_gpio(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "mode") == 0) { - cli_command_gpio_mode(cli, args, context); + cli_command_gpio_mode(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "set") == 0) { - cli_command_gpio_set(cli, args, context); + cli_command_gpio_set(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "read") == 0) { - cli_command_gpio_read(cli, args, context); + cli_command_gpio_read(pipe, args, context); break; } diff --git a/applications/services/cli/cli_command_gpio.h b/applications/services/cli/cli_command_gpio.h index 7ae5aa625..0290949e0 100644 --- a/applications/services/cli/cli_command_gpio.h +++ b/applications/services/cli/cli_command_gpio.h @@ -1,5 +1,6 @@ #pragma once #include "cli_i.h" +#include -void cli_command_gpio(Cli* cli, FuriString* args, void* context); +void cli_command_gpio(PipeSide* pipe, FuriString* args, void* context); diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index e4503b274..24917afa9 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -1,5 +1,7 @@ #include "cli_commands.h" #include "cli_command_gpio.h" +#include "cli_ansi.h" +#include "cli.h" #include #include @@ -11,6 +13,7 @@ #include #include #include +#include // Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'` #define CLI_DATE_FORMAT "%.4d-%.2d-%.2d %.2d:%.2d:%.2d %d" @@ -34,8 +37,8 @@ void cli_command_info_callback(const char* key, const char* value, bool last, vo * @param args The arguments * @param context The context */ -void cli_command_info(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_info(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); if(context) { furi_hal_info_get(cli_command_info_callback, '_', NULL); @@ -53,56 +56,57 @@ void cli_command_info(Cli* cli, FuriString* args, void* context) { } } -void cli_command_help(Cli* cli, FuriString* args, void* context) { +void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); - printf("Commands available:"); + printf("Available commands:" ANSI_FG_GREEN); - // Command count - const size_t commands_count = CliCommandTree_size(cli->commands); - const size_t commands_count_mid = commands_count / 2 + commands_count % 2; + // count non-hidden commands + Cli* cli = furi_record_open(RECORD_CLI); + cli_lock_commands(cli); + CliCommandTree_t* commands = cli_get_commands(cli); + size_t commands_count = CliCommandTree_size(*commands); - // Use 2 iterators from start and middle to show 2 columns - CliCommandTree_it_t it_left; - CliCommandTree_it(it_left, cli->commands); - CliCommandTree_it_t it_right; - CliCommandTree_it(it_right, cli->commands); - for(size_t i = 0; i < commands_count_mid; i++) - CliCommandTree_next(it_right); - - // Iterate throw tree - for(size_t i = 0; i < commands_count_mid; i++) { - printf("\r\n"); - // Left Column - if(!CliCommandTree_end_p(it_left)) { - printf("%-30s", furi_string_get_cstr(*CliCommandTree_ref(it_left)->key_ptr)); - CliCommandTree_next(it_left); - } - // Right Column - if(!CliCommandTree_end_p(it_right)) { - printf("%s", furi_string_get_cstr(*CliCommandTree_ref(it_right)->key_ptr)); - CliCommandTree_next(it_right); - } - }; - - if(furi_string_size(args) > 0) { - cli_nl(cli); - printf("`"); - printf("%s", furi_string_get_cstr(args)); - printf("` command not found"); + // create iterators starting at different positions + const size_t columns = 3; + const size_t commands_per_column = (commands_count / columns) + (commands_count % columns); + CliCommandTree_it_t iterators[columns]; + for(size_t c = 0; c < columns; c++) { + CliCommandTree_it(iterators[c], *commands); + for(size_t i = 0; i < c * commands_per_column; i++) + CliCommandTree_next(iterators[c]); } + + // print commands + for(size_t r = 0; r < commands_per_column; r++) { + printf("\r\n"); + + for(size_t c = 0; c < columns; c++) { + if(!CliCommandTree_end_p(iterators[c])) { + const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]); + printf("%-30s", furi_string_get_cstr(*item->key_ptr)); + CliCommandTree_next(iterators[c]); + } + } + } + + printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli"); + + cli_unlock_commands(cli); + furi_record_close(RECORD_CLI); } -void cli_command_uptime(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_uptime(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency(); printf("Uptime: %luh%lum%lus", uptime / 60 / 60, uptime / 60 % 60, uptime % 60); } -void cli_command_date(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_date(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); DateTime datetime = {0}; @@ -174,7 +178,8 @@ void cli_command_date(Cli* cli, FuriString* args, void* context) { #define CLI_COMMAND_LOG_BUFFER_SIZE 64 void cli_command_log_tx_callback(const uint8_t* buffer, size_t size, void* context) { - furi_stream_buffer_send(context, buffer, size, 0); + PipeSide* pipe = context; + pipe_send(pipe, buffer, size); } bool cli_command_log_level_set_from_string(FuriString* level) { @@ -196,16 +201,13 @@ bool cli_command_log_level_set_from_string(FuriString* level) { return false; } -void cli_command_log(Cli* cli, FuriString* args, void* context) { +void cli_command_log(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); - FuriStreamBuffer* ring = furi_stream_buffer_alloc(CLI_COMMAND_LOG_RING_SIZE, 1); - uint8_t buffer[CLI_COMMAND_LOG_BUFFER_SIZE]; FuriLogLevel previous_level = furi_log_get_level(); bool restore_log_level = false; if(furi_string_size(args) > 0) { if(!cli_command_log_level_set_from_string(args)) { - furi_stream_buffer_free(ring); return; } restore_log_level = true; @@ -217,16 +219,15 @@ void cli_command_log(Cli* cli, FuriString* args, void* context) { FuriLogHandler log_handler = { .callback = cli_command_log_tx_callback, - .context = ring, + .context = pipe, }; furi_log_add_handler(log_handler); printf("Use to list available log levels\r\n"); printf("Press CTRL+C to stop...\r\n"); - while(!cli_cmd_interrupt_received(cli)) { - size_t ret = furi_stream_buffer_receive(ring, buffer, CLI_COMMAND_LOG_BUFFER_SIZE, 50); - cli_write(cli, buffer, ret); + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { + furi_delay_ms(100); } furi_log_remove_handler(log_handler); @@ -235,12 +236,10 @@ void cli_command_log(Cli* cli, FuriString* args, void* context) { // There will be strange behaviour if log level is set from settings while log command is running furi_log_set_level(previous_level); } - - furi_stream_buffer_free(ring); } -void cli_command_sysctl_debug(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_sysctl_debug(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); if(!furi_string_cmp(args, "0")) { furi_hal_rtc_reset_flag(FuriHalRtcFlagDebug); @@ -253,8 +252,8 @@ void cli_command_sysctl_debug(Cli* cli, FuriString* args, void* context) { } } -void cli_command_sysctl_heap_track(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_sysctl_heap_track(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); if(!furi_string_cmp(args, "none")) { furi_hal_rtc_set_heap_track_mode(FuriHalRtcHeapTrackModeNone); @@ -288,7 +287,7 @@ void cli_command_sysctl_print_usage(void) { #endif } -void cli_command_sysctl(Cli* cli, FuriString* args, void* context) { +void cli_command_sysctl(PipeSide* pipe, FuriString* args, void* context) { FuriString* cmd; cmd = furi_string_alloc(); @@ -299,12 +298,12 @@ void cli_command_sysctl(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "debug") == 0) { - cli_command_sysctl_debug(cli, args, context); + cli_command_sysctl_debug(pipe, args, context); break; } if(furi_string_cmp_str(cmd, "heap_track") == 0) { - cli_command_sysctl_heap_track(cli, args, context); + cli_command_sysctl_heap_track(pipe, args, context); break; } @@ -314,8 +313,8 @@ void cli_command_sysctl(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -void cli_command_vibro(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_vibro(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); if(!furi_string_cmp(args, "0")) { @@ -341,8 +340,8 @@ void cli_command_vibro(Cli* cli, FuriString* args, void* context) { } } -void cli_command_led(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_led(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); // Get first word as light name NotificationMessage notification_led_message; @@ -396,23 +395,23 @@ void cli_command_led(Cli* cli, FuriString* args, void* context) { furi_record_close(RECORD_NOTIFICATION); } -static void cli_command_top(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void cli_command_top(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); int interval = 1000; args_read_int_and_trim(args, &interval); FuriThreadList* thread_list = furi_thread_list_alloc(); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { uint32_t tick = furi_get_tick(); furi_thread_enumerate(thread_list); - if(interval) printf("\e[2J\e[0;0f"); // Clear display and return to 0 + if(interval) printf(ANSI_CURSOR_POS("1", "1")); uint32_t uptime = tick / furi_kernel_get_tick_frequency(); printf( - "Threads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\r\n", + "Threads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus" ANSI_ERASE_LINE( + ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n", furi_thread_list_size(thread_list), (double)furi_thread_list_get_isr_time(thread_list), uptime / 60 / 60, @@ -420,14 +419,16 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { uptime % 60); printf( - "Heap: total %zu, free %zu, minimum %zu, max block %zu\r\n\r\n", + "Heap: total %zu, free %zu, minimum %zu, max block %zu" ANSI_ERASE_LINE( + ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n", memmgr_get_total_heap(), memmgr_get_free_heap(), memmgr_get_minimum_free_heap(), memmgr_heap_get_max_free_block()); printf( - "%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\r\n", + "%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s" ANSI_ERASE_LINE( + ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n", "AppID", "Name", "State", @@ -436,12 +437,13 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { "Stack", "Stack Min", "Heap", - "CPU"); + "%CPU"); for(size_t i = 0; i < furi_thread_list_size(thread_list); i++) { const FuriThreadListItem* item = furi_thread_list_get_at(thread_list, i); printf( - "%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\r\n", + "%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f" ANSI_ERASE_LINE( + ANSI_ERASE_FROM_CURSOR_TO_END) "\r\n", item->app_id, item->name, item->state, @@ -453,6 +455,9 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { (double)item->cpu); } + printf(ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END)); + fflush(stdout); + if(interval > 0) { furi_delay_ms(interval); } else { @@ -462,8 +467,8 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { furi_thread_list_free(thread_list); } -void cli_command_free(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_free(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); @@ -476,16 +481,16 @@ void cli_command_free(Cli* cli, FuriString* args, void* context) { printf("Maximum pool block: %zu\r\n", memmgr_pool_get_max_block()); } -void cli_command_free_blocks(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_free_blocks(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); memmgr_heap_printf_free_blocks(); } -void cli_command_i2c(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +void cli_command_i2c(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); @@ -507,6 +512,27 @@ void cli_command_i2c(Cli* cli, FuriString* args, void* context) { furi_hal_i2c_release(&furi_hal_i2c_handle_external); } +/** + * Echoes any bytes it receives except ASCII ETX (0x03, Ctrl+C) + */ +void cli_command_echo(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(args); + UNUSED(context); + + uint8_t buffer[256]; + + while(true) { + size_t to_read = CLAMP(pipe_bytes_available(pipe), sizeof(buffer), 1UL); + size_t read = pipe_receive(pipe, buffer, to_read); + if(read < to_read) break; + + if(memchr(buffer, CliKeyETX, read)) break; + + size_t written = pipe_send(pipe, buffer, read); + if(written < read) break; + } +} + void cli_commands_init(Cli* cli) { cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); @@ -522,6 +548,7 @@ void cli_commands_init(Cli* cli) { cli_add_command(cli, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); cli_add_command(cli, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); cli_add_command(cli, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL); + cli_add_command(cli, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); cli_add_command(cli, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); cli_add_command(cli, "led", CliCommandFlagDefault, cli_command_led, NULL); diff --git a/applications/services/cli/cli_commands.h b/applications/services/cli/cli_commands.h index 184eeb373..77d9930af 100644 --- a/applications/services/cli/cli_commands.h +++ b/applications/services/cli/cli_commands.h @@ -1,5 +1,34 @@ #pragma once -#include "cli_i.h" +#include "cli.h" +#include void cli_commands_init(Cli* cli); + +#define PLUGIN_APP_ID "cli" +#define PLUGIN_API_VERSION 1 + +typedef struct { + char* name; + CliExecuteCallback execute_callback; + CliCommandFlag flags; + size_t stack_depth; +} CliCommandDescriptor; + +#define CLI_COMMAND_INTERFACE(name, execute_callback, flags, stack_depth) \ + static const CliCommandDescriptor cli_##name##_desc = { \ + #name, \ + &execute_callback, \ + flags, \ + stack_depth, \ + }; \ + \ + static const FlipperAppPluginDescriptor plugin_descriptor = { \ + .appid = PLUGIN_APP_ID, \ + .ep_api_version = PLUGIN_API_VERSION, \ + .entry_point = &cli_##name##_desc, \ + }; \ + \ + const FlipperAppPluginDescriptor* cli_##name##_ep(void) { \ + return &plugin_descriptor; \ + } diff --git a/applications/services/cli/cli_i.h b/applications/services/cli/cli_i.h index ca126dacd..b990e9960 100644 --- a/applications/services/cli/cli_i.h +++ b/applications/services/cli/cli_i.h @@ -1,67 +1,50 @@ +/** + * @file cli_i.h + * Internal API for getting commands registered with the CLI + */ + #pragma once -#include "cli.h" - #include -#include - -#include #include -#include - -#include "cli_vcp.h" - -#define CLI_LINE_SIZE_MAX -#define CLI_COMMANDS_TREE_RANK 4 +#include "cli.h" #ifdef __cplusplus extern "C" { #endif +#define CLI_BUILTIN_COMMAND_STACK_SIZE (3 * 1024U) + typedef struct { - CliCallback callback; - void* context; - uint32_t flags; + void* context; // #include #include +#include +#include #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; // 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, -}; diff --git a/applications/services/cli/cli_vcp.h b/applications/services/cli/cli_vcp.h index f51625342..10e286183 100644 --- a/applications/services/cli/cli_vcp.h +++ b/applications/services/cli/cli_vcp.h @@ -9,9 +9,12 @@ extern "C" { #endif -typedef struct CliSession CliSession; +#define RECORD_CLI_VCP "cli_vcp" -extern const CliSession cli_vcp; +typedef struct CliVcp CliVcp; + +void cli_vcp_enable(CliVcp* cli_vcp); +void cli_vcp_disable(CliVcp* cli_vcp); #ifdef __cplusplus } diff --git a/applications/services/cli/shell/cli_shell.c b/applications/services/cli/shell/cli_shell.c new file mode 100644 index 000000000..62cbbd403 --- /dev/null +++ b/applications/services/cli/shell/cli_shell.c @@ -0,0 +1,251 @@ +#include "cli_shell.h" +#include "cli_shell_i.h" +#include "../cli_ansi.h" +#include "../cli_i.h" +#include "../cli_commands.h" +#include "cli_shell_line.h" +#include +#include +#include +#include +#include +#include +#include + +#define TAG "CliShell" + +#define ANSI_TIMEOUT_MS 10 + +typedef enum { + CliShellComponentLine, + CliShellComponentMAX, //cli, command_name, &command_data)) { + printf( + ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET, + furi_string_get_cstr(command_name)); + break; + } + + // lock loader + if(!(command_data.flags & CliCommandFlagParallelSafe)) { + loader = furi_record_open(RECORD_LOADER); + bool success = loader_lock(loader); + if(!success) { + printf(ANSI_FG_RED + "this command cannot be run while an application is open" ANSI_RESET); + break; + } + } + + command_data.execute_callback(cli_shell->pipe, args, command_data.context); + } while(0); + + furi_string_free(command_name); + furi_string_free(args); + + // unlock loader + if(loader) loader_unlock(loader); + furi_record_close(RECORD_LOADER); +} + +// ============== +// Event handlers +// ============== + +static void cli_shell_process_key(CliShell* cli_shell, CliKeyCombo key_combo) { + for(size_t i = 0; i < CliShellComponentMAX; i++) { // -V1008 + CliShellKeyComboSet* set = component_key_combo_sets[i]; + void* component_context = cli_shell->components[i]; + + for(size_t j = 0; j < set->count; j++) { + if(set->records[j].combo.modifiers == key_combo.modifiers && + set->records[j].combo.key == key_combo.key) + if(set->records[j].action(key_combo, component_context)) return; + } + + if(set->fallback) + if(set->fallback(key_combo, component_context)) return; + } +} + +static void cli_shell_pipe_broken(PipeSide* pipe, void* context) { + // allow commands to be processed before we stop the shell + if(pipe_bytes_available(pipe)) return; + + CliShell* cli_shell = context; + furi_event_loop_stop(cli_shell->event_loop); +} + +static void cli_shell_data_available(PipeSide* pipe, void* context) { + UNUSED(pipe); + CliShell* cli_shell = context; + + furi_event_loop_timer_start(cli_shell->ansi_parsing_timer, furi_ms_to_ticks(ANSI_TIMEOUT_MS)); + + // process ANSI escape sequences + int c = getchar(); + furi_assert(c >= 0); + CliAnsiParserResult parse_result = cli_ansi_parser_feed(cli_shell->ansi_parser, c); + if(!parse_result.is_done) return; + CliKeyCombo key_combo = parse_result.result; + if(key_combo.key == CliKeyUnrecognized) return; + + cli_shell_process_key(cli_shell, key_combo); +} + +static void cli_shell_timer_expired(void* context) { + CliShell* cli_shell = context; + CliAnsiParserResult parse_result = cli_ansi_parser_feed_timeout(cli_shell->ansi_parser); + if(!parse_result.is_done) return; + CliKeyCombo key_combo = parse_result.result; + if(key_combo.key == CliKeyUnrecognized) return; + + cli_shell_process_key(cli_shell, key_combo); +} + +// ======= +// Helpers +// ======= + +static CliShell* cli_shell_alloc(PipeSide* pipe) { + CliShell* cli_shell = malloc(sizeof(CliShell)); + + cli_shell->cli = furi_record_open(RECORD_CLI); + cli_shell->ansi_parser = cli_ansi_parser_alloc(); + cli_shell->pipe = pipe; + pipe_install_as_stdio(cli_shell->pipe); + + cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); + + cli_shell->event_loop = furi_event_loop_alloc(); + cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( + cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell); + pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop); + + pipe_set_callback_context(cli_shell->pipe, cli_shell); + pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0); + pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0); + + return cli_shell; +} + +static void cli_shell_free(CliShell* cli_shell) { + cli_shell_line_free(cli_shell->components[CliShellComponentLine]); + + pipe_detach_from_event_loop(cli_shell->pipe); + furi_event_loop_timer_free(cli_shell->ansi_parsing_timer); + furi_event_loop_free(cli_shell->event_loop); + pipe_free(cli_shell->pipe); + cli_ansi_parser_free(cli_shell->ansi_parser); + furi_record_close(RECORD_CLI); + free(cli_shell); +} + +static void cli_shell_motd(void) { + printf(ANSI_FLIPPER_BRAND_ORANGE + "\r\n" + " _.-------.._ -,\r\n" + " .-\"```\"--..,,_/ /`-, -, \\ \r\n" + " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" + " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" + " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" + " | | | 0 | | .-' ,/` /\r\n" + " | ,..\\ \\ ,.-\"` ,/` /\r\n" + " ; : `/`\"\"\\` ,/--==,/-----,\r\n" + " | `-...| -.___-Z:_______J...---;\r\n" + " : ` _-'\r\n" + " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" + "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" + "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" + "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" + "\r\n" ANSI_FG_BR_WHITE "Welcome to Flipper Zero Command Line Interface!\r\n" + "Read the manual: https://docs.flipper.net/development/cli\r\n" + "Run `help` or `?` to list available commands\r\n" + "\r\n" ANSI_RESET); + + const Version* firmware_version = furi_hal_version_get_firmware_version(); + if(firmware_version) { + printf( + "Firmware version: %s %s (%s%s built on %s)\r\n", + version_get_gitbranch(firmware_version), + version_get_version(firmware_version), + version_get_githash(firmware_version), + version_get_dirty_flag(firmware_version) ? "-dirty" : "", + version_get_builddate(firmware_version)); + } +} + +static int32_t cli_shell_thread(void* context) { + PipeSide* pipe = context; + + // Sometimes, the other side closes the pipe even before our thread is started. Although the + // rest of the code will eventually find this out if this check is removed, there's no point in + // wasting time. + if(pipe_state(pipe) == PipeStateBroken) return 0; + + CliShell* cli_shell = cli_shell_alloc(pipe); + + FURI_LOG_D(TAG, "Started"); + cli_shell_motd(); + cli_shell_line_prompt(cli_shell->components[CliShellComponentLine]); + + furi_event_loop_run(cli_shell->event_loop); + + FURI_LOG_D(TAG, "Stopped"); + + cli_shell_free(cli_shell); + return 0; +} + +// ========== +// Public API +// ========== + +FuriThread* cli_shell_start(PipeSide* pipe) { + FuriThread* thread = + furi_thread_alloc_ex("CliShell", CLI_SHELL_STACK_SIZE, cli_shell_thread, pipe); + furi_thread_start(thread); + return thread; +} diff --git a/applications/services/cli/shell/cli_shell.h b/applications/services/cli/shell/cli_shell.h new file mode 100644 index 000000000..e60eefc77 --- /dev/null +++ b/applications/services/cli/shell/cli_shell.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLI_SHELL_STACK_SIZE (4 * 1024U) + +FuriThread* cli_shell_start(PipeSide* pipe); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/shell/cli_shell_i.h b/applications/services/cli/shell/cli_shell_i.h new file mode 100644 index 000000000..e8eae92c6 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_i.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../cli_ansi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CliShell CliShell; + +/** + * @brief Key combo handler + * @return true if the event was handled, false otherwise + */ +typedef bool (*CliShellKeyComboAction)(CliKeyCombo combo, void* context); + +typedef struct { + CliKeyCombo combo; + CliShellKeyComboAction action; +} CliShellKeyComboRecord; + +typedef struct { + CliShellKeyComboAction fallback; + size_t count; + CliShellKeyComboRecord records[]; +} CliShellKeyComboSet; + +void cli_shell_execute_command(CliShell* cli_shell, FuriString* command); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/shell/cli_shell_line.c b/applications/services/cli/shell/cli_shell_line.c new file mode 100644 index 000000000..45bc19d9d --- /dev/null +++ b/applications/services/cli/shell/cli_shell_line.c @@ -0,0 +1,238 @@ +#include "cli_shell_line.h" + +#define HISTORY_DEPTH 10 + +struct CliShellLine { + size_t history_position; + size_t line_position; + FuriString* history[HISTORY_DEPTH]; + size_t history_entries; + CliShell* shell; +}; + +// ========== +// Public API +// ========== + +CliShellLine* cli_shell_line_alloc(CliShell* shell) { + CliShellLine* line = malloc(sizeof(CliShellLine)); + line->shell = shell; + + line->history[0] = furi_string_alloc(); + line->history_entries = 1; + + return line; +} + +void cli_shell_line_free(CliShellLine* line) { + for(size_t i = 0; i < line->history_entries; i++) + furi_string_free(line->history[i]); + + free(line); +} + +FuriString* cli_shell_line_get_selected(CliShellLine* line) { + return line->history[line->history_position]; +} + +FuriString* cli_shell_line_get_editing(CliShellLine* line) { + return line->history[0]; +} + +size_t cli_shell_line_prompt_length(CliShellLine* line) { + UNUSED(line); + return strlen(">: "); +} + +void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length) { + UNUSED(line); + snprintf(buf, length - 1, ">: "); +} + +void cli_shell_line_prompt(CliShellLine* line) { + char buffer[32]; + cli_shell_line_format_prompt(line, buffer, sizeof(buffer)); + printf("\r\n%s", buffer); + fflush(stdout); +} + +void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) { + if(line->history_position > 0) { + FuriString* source = cli_shell_line_get_selected(line); + FuriString* destination = cli_shell_line_get_editing(line); + furi_string_set(destination, source); + line->history_position = 0; + } +} + +// ============== +// Input handlers +// ============== + +static bool cli_shell_line_input_ctrl_c(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // reset input + furi_string_reset(cli_shell_line_get_editing(line)); + line->line_position = 0; + line->history_position = 0; + printf("^C"); + cli_shell_line_prompt(line); + return true; +} + +static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + + FuriString* command = cli_shell_line_get_selected(line); + furi_string_trim(command); + FuriString* command_copy = furi_string_alloc_set(command); + + if(line->history_position > 0) { + // move selected command to the front + memmove( + &line->history[1], &line->history[0], line->history_position * sizeof(FuriString*)); + line->history[0] = command; + } + + // insert empty command + if(line->history_entries == HISTORY_DEPTH) { + furi_string_free(line->history[HISTORY_DEPTH - 1]); + line->history_entries--; + } + memmove(&line->history[1], &line->history[0], line->history_entries * sizeof(FuriString*)); + line->history[0] = furi_string_alloc(); + line->history_entries++; + line->line_position = 0; + line->history_position = 0; + + // execute command + printf("\r\n"); + if(!furi_string_empty(command_copy)) cli_shell_execute_command(line->shell, command_copy); + furi_string_free(command_copy); + + cli_shell_line_prompt(line); + return true; +} + +static bool cli_shell_line_input_up_down(CliKeyCombo combo, void* context) { + CliShellLine* line = context; + // go up and down in history + int increment = (combo.key == CliKeyUp) ? 1 : -1; + size_t new_pos = + CLAMP((int)line->history_position + increment, (int)line->history_entries - 1, 0); + + // print prompt with selected command + if(new_pos != line->history_position) { + line->history_position = new_pos; + FuriString* command = cli_shell_line_get_selected(line); + printf( + ANSI_CURSOR_HOR_POS("1") ">: %s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + furi_string_get_cstr(command)); + fflush(stdout); + line->line_position = furi_string_size(command); + } + return true; +} + +static bool cli_shell_line_input_left_right(CliKeyCombo combo, void* context) { + CliShellLine* line = context; + // go left and right in the current line + FuriString* command = cli_shell_line_get_selected(line); + int increment = (combo.key == CliKeyRight) ? 1 : -1; + size_t new_pos = + CLAMP((int)line->line_position + increment, (int)furi_string_size(command), 0); + + // move cursor + if(new_pos != line->line_position) { + line->line_position = new_pos; + printf("%s", (increment == 1) ? ANSI_CURSOR_RIGHT_BY("1") : ANSI_CURSOR_LEFT_BY("1")); + fflush(stdout); + } + return true; +} + +static bool cli_shell_line_input_home(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // go to the start + line->line_position = 0; + printf(ANSI_CURSOR_HOR_POS("%zu"), cli_shell_line_prompt_length(line) + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_end(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // go to the end + line->line_position = furi_string_size(cli_shell_line_get_selected(line)); + printf( + ANSI_CURSOR_HOR_POS("%zu"), cli_shell_line_prompt_length(line) + line->line_position + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_bksp(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // erase one character + cli_shell_line_ensure_not_overwriting_history(line); + FuriString* editing_line = cli_shell_line_get_editing(line); + if(line->line_position == 0) { + putc(CliKeyBell, stdout); + fflush(stdout); + return true; + } + line->line_position--; + furi_string_replace_at(editing_line, line->line_position, 1, ""); + + // move cursor, print the rest of the line, restore cursor + printf( + ANSI_CURSOR_LEFT_BY("1") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + furi_string_get_cstr(editing_line) + line->line_position); + size_t left_by = furi_string_size(editing_line) - line->line_position; + if(left_by) // apparently LEFT_BY("0") still shifts left by one ._ . + printf(ANSI_CURSOR_LEFT_BY("%zu"), left_by); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) { + CliShellLine* line = context; + if(combo.modifiers != CliModKeyNo) return false; + if(combo.key < CliKeySpace || combo.key >= CliKeyDEL) return false; + // insert character + cli_shell_line_ensure_not_overwriting_history(line); + FuriString* editing_line = cli_shell_line_get_editing(line); + if(line->line_position == furi_string_size(editing_line)) { + furi_string_push_back(editing_line, combo.key); + printf("%c", combo.key); + } else { + const char in_str[2] = {combo.key, 0}; + furi_string_replace_at(editing_line, line->line_position, 0, in_str); + printf(ANSI_INSERT_MODE_ENABLE "%c" ANSI_INSERT_MODE_DISABLE, combo.key); + } + fflush(stdout); + line->line_position++; + return true; +} + +CliShellKeyComboSet cli_shell_line_key_combo_set = { + .fallback = cli_shell_line_input_fallback, + .count = 10, + .records = + { + {{CliModKeyNo, CliKeyETX}, cli_shell_line_input_ctrl_c}, + {{CliModKeyNo, CliKeyCR}, cli_shell_line_input_cr}, + {{CliModKeyNo, CliKeyUp}, cli_shell_line_input_up_down}, + {{CliModKeyNo, CliKeyDown}, cli_shell_line_input_up_down}, + {{CliModKeyNo, CliKeyLeft}, cli_shell_line_input_left_right}, + {{CliModKeyNo, CliKeyRight}, cli_shell_line_input_left_right}, + {{CliModKeyNo, CliKeyHome}, cli_shell_line_input_home}, + {{CliModKeyNo, CliKeyEnd}, cli_shell_line_input_end}, + {{CliModKeyNo, CliKeyBackspace}, cli_shell_line_input_bksp}, + {{CliModKeyNo, CliKeyDEL}, cli_shell_line_input_bksp}, + }, +}; diff --git a/applications/services/cli/shell/cli_shell_line.h b/applications/services/cli/shell/cli_shell_line.h new file mode 100644 index 000000000..c1c810ee4 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_line.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "cli_shell_i.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CliShellLine CliShellLine; + +CliShellLine* cli_shell_line_alloc(CliShell* shell); + +void cli_shell_line_free(CliShellLine* line); + +FuriString* cli_shell_line_get_selected(CliShellLine* line); + +FuriString* cli_shell_line_get_editing(CliShellLine* line); + +size_t cli_shell_line_prompt_length(CliShellLine* line); + +void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length); + +void cli_shell_line_prompt(CliShellLine* line); + +/** + * @brief If a line from history has been selected, moves it into the active line + */ +void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line); + +extern CliShellKeyComboSet cli_shell_line_key_combo_set; + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index 744fa7151..bfefaf0f1 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -2,6 +2,7 @@ #include #include +#include #include void crypto_cli_print_usage(void) { @@ -17,7 +18,7 @@ void crypto_cli_print_usage(void) { "\tstore_key \t - Store key in secure enclave. !!! NON-REVERSABLE OPERATION - READ MANUAL FIRST !!!\r\n"); } -void crypto_cli_encrypt(Cli* cli, FuriString* args) { +void crypto_cli_encrypt(PipeSide* pipe, FuriString* args) { int key_slot = 0; bool key_loaded = false; uint8_t iv[16]; @@ -44,15 +45,15 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) { FuriString* input; input = furi_string_alloc(); char c; - while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliSymbolAsciiETX) { + while(pipe_receive(pipe, (uint8_t*)&c, 1) == 1) { + if(c == CliKeyETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(input, c); - } else if(c == CliSymbolAsciiCR) { + } else if(c == CliKeyCR) { printf("\r\n"); furi_string_cat(input, "\r\n"); } @@ -92,7 +93,7 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) { } } -void crypto_cli_decrypt(Cli* cli, FuriString* args) { +void crypto_cli_decrypt(PipeSide* pipe, FuriString* args) { int key_slot = 0; bool key_loaded = false; uint8_t iv[16]; @@ -119,15 +120,15 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) { FuriString* hex_input; hex_input = furi_string_alloc(); char c; - while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliSymbolAsciiETX) { + while(pipe_receive(pipe, (uint8_t*)&c, 1) == 1) { + if(c == CliKeyETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(hex_input, c); - } else if(c == CliSymbolAsciiCR) { + } else if(c == CliKeyCR) { printf("\r\n"); } } @@ -164,8 +165,8 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) { } } -void crypto_cli_has_key(Cli* cli, FuriString* args) { - UNUSED(cli); +void crypto_cli_has_key(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); int key_slot = 0; uint8_t iv[16] = {0}; @@ -186,8 +187,8 @@ void crypto_cli_has_key(Cli* cli, FuriString* args) { } while(0); } -void crypto_cli_store_key(Cli* cli, FuriString* args) { - UNUSED(cli); +void crypto_cli_store_key(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); int key_slot = 0; int key_size = 0; FuriString* key_type; @@ -279,7 +280,7 @@ void crypto_cli_store_key(Cli* cli, FuriString* args) { furi_string_free(key_type); } -static void crypto_cli(Cli* cli, FuriString* args, void* context) { +static void crypto_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -291,22 +292,22 @@ static void crypto_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "encrypt") == 0) { - crypto_cli_encrypt(cli, args); + crypto_cli_encrypt(pipe, args); break; } if(furi_string_cmp_str(cmd, "decrypt") == 0) { - crypto_cli_decrypt(cli, args); + crypto_cli_decrypt(pipe, args); break; } if(furi_string_cmp_str(cmd, "has_key") == 0) { - crypto_cli_has_key(cli, args); + crypto_cli_has_key(pipe, args); break; } if(furi_string_cmp_str(cmd, "store_key") == 0) { - crypto_cli_store_key(cli, args); + crypto_cli_store_key(pipe, args); break; } diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 1132760d5..185fb9c3b 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -396,8 +396,8 @@ void desktop_lock(Desktop* desktop) { furi_hal_rtc_set_flag(FuriHalRtcFlagLock); if(desktop_pin_code_is_set()) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_close(cli); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_disable(cli_vcp); furi_record_close(RECORD_CLI); } @@ -426,8 +426,8 @@ void desktop_unlock(Desktop* desktop) { furi_hal_rtc_set_pin_fails(0); if(desktop_pin_code_is_set()) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_session_open(cli, &cli_vcp); + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_enable(cli_vcp); furi_record_close(RECORD_CLI); } @@ -525,6 +525,10 @@ int32_t desktop_srv(void* p) { if(desktop_pin_code_is_set()) { desktop_lock(desktop); + } else { + CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); + cli_vcp_enable(cli_vcp); + furi_record_close(RECORD_CLI); } if(storage_file_exists(desktop->storage, SLIDESHOW_FS_PATH)) { diff --git a/applications/services/input/input.c b/applications/services/input/input.c index 6cbafb795..c77771f3e 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -6,6 +6,7 @@ #include #include #include +#include #define INPUT_DEBOUNCE_TICKS_HALF (INPUT_DEBOUNCE_TICKS / 2) #define INPUT_PRESS_TICKS 150 @@ -25,7 +26,7 @@ typedef struct { } InputPinState; /** Input CLI command handler */ -void input_cli(Cli* cli, FuriString* args, void* context); +void input_cli(PipeSide* pipe, FuriString* args, void* context); // #define INPUT_DEBUG diff --git a/applications/services/input/input_cli.c b/applications/services/input/input_cli.c index 8e711c895..a34ec3c36 100644 --- a/applications/services/input/input_cli.c +++ b/applications/services/input/input_cli.c @@ -3,6 +3,7 @@ #include #include #include +#include static void input_cli_usage(void) { printf("Usage:\r\n"); @@ -19,7 +20,7 @@ static void input_cli_dump_events_callback(const void* value, void* ctx) { furi_message_queue_put(input_queue, value, FuriWaitForever); } -static void input_cli_dump(Cli* cli, FuriString* args, FuriPubSub* event_pubsub) { +static void input_cli_dump(PipeSide* pipe, FuriString* args, FuriPubSub* event_pubsub) { UNUSED(args); FuriMessageQueue* input_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); FuriPubSubSubscription* input_subscription = @@ -27,7 +28,7 @@ static void input_cli_dump(Cli* cli, FuriString* args, FuriPubSub* event_pubsub) InputEvent input_event; printf("Press CTRL+C to stop\r\n"); - while(!cli_cmd_interrupt_received(cli)) { + while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { if(furi_message_queue_get(input_queue, &input_event, 100) == FuriStatusOk) { printf( "key: %s type: %s\r\n", @@ -47,8 +48,8 @@ static void input_cli_send_print_usage(void) { printf("\t\t \t - one of 'press', 'release', 'short', 'long'\r\n"); } -static void input_cli_send(Cli* cli, FuriString* args, FuriPubSub* event_pubsub) { - UNUSED(cli); +static void input_cli_send(PipeSide* pipe, FuriString* args, FuriPubSub* event_pubsub) { + UNUSED(pipe); InputEvent event; FuriString* key_str; key_str = furi_string_alloc(); @@ -97,8 +98,7 @@ static void input_cli_send(Cli* cli, FuriString* args, FuriPubSub* event_pubsub) furi_string_free(key_str); } -void input_cli(Cli* cli, FuriString* args, void* context) { - furi_assert(cli); +void input_cli(PipeSide* pipe, FuriString* args, void* context) { furi_assert(context); FuriPubSub* event_pubsub = context; FuriString* cmd; @@ -110,11 +110,11 @@ void input_cli(Cli* cli, FuriString* args, void* context) { break; } if(furi_string_cmp_str(cmd, "dump") == 0) { - input_cli_dump(cli, args, event_pubsub); + input_cli_dump(pipe, args, event_pubsub); break; } if(furi_string_cmp_str(cmd, "send") == 0) { - input_cli_send(cli, args, event_pubsub); + input_cli_send(pipe, args, event_pubsub); break; } diff --git a/applications/services/loader/loader_cli.c b/applications/services/loader/loader_cli.c index f3ea30df2..40312d8b3 100644 --- a/applications/services/loader/loader_cli.c +++ b/applications/services/loader/loader_cli.c @@ -6,6 +6,7 @@ #include #include #include +#include static void loader_cli_print_usage(void) { printf("Usage:\r\n"); @@ -110,8 +111,8 @@ static void loader_cli_signal(FuriString* args, Loader* loader) { } } -static void loader_cli(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void loader_cli(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); Loader* loader = furi_record_open(RECORD_LOADER); diff --git a/applications/services/power/power_cli.c b/applications/services/power/power_cli.c index 121552768..f1771d9f1 100644 --- a/applications/services/power/power_cli.c +++ b/applications/services/power/power_cli.c @@ -4,9 +4,10 @@ #include #include #include +#include -void power_cli_off(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_off(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); UNUSED(args); Power* power = furi_record_open(RECORD_POWER); printf("It's now safe to disconnect USB from your flipper\r\n"); @@ -14,22 +15,22 @@ void power_cli_off(Cli* cli, FuriString* args) { power_off(power); } -void power_cli_reboot(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_reboot(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); UNUSED(args); Power* power = furi_record_open(RECORD_POWER); power_reboot(power, PowerBootModeNormal); } -void power_cli_reboot2dfu(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_reboot2dfu(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); UNUSED(args); Power* power = furi_record_open(RECORD_POWER); power_reboot(power, PowerBootModeDfu); } -void power_cli_5v(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_5v(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); Power* power = furi_record_open(RECORD_POWER); if(!furi_string_cmp(args, "0")) { power_enable_otg(power, false); @@ -42,8 +43,8 @@ void power_cli_5v(Cli* cli, FuriString* args) { furi_record_close(RECORD_POWER); } -void power_cli_3v3(Cli* cli, FuriString* args) { - UNUSED(cli); +void power_cli_3v3(PipeSide* pipe, FuriString* args) { + UNUSED(pipe); if(!furi_string_cmp(args, "0")) { furi_hal_power_disable_external_3_3v(); } else if(!furi_string_cmp(args, "1")) { @@ -67,7 +68,7 @@ static void power_cli_command_print_usage(void) { } } -void power_cli(Cli* cli, FuriString* args, void* context) { +void power_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -79,28 +80,28 @@ void power_cli(Cli* cli, FuriString* args, void* context) { } if(furi_string_cmp_str(cmd, "off") == 0) { - power_cli_off(cli, args); + power_cli_off(pipe, args); break; } if(furi_string_cmp_str(cmd, "reboot") == 0) { - power_cli_reboot(cli, args); + power_cli_reboot(pipe, args); break; } if(furi_string_cmp_str(cmd, "reboot2dfu") == 0) { - power_cli_reboot2dfu(cli, args); + power_cli_reboot2dfu(pipe, args); break; } if(furi_string_cmp_str(cmd, "5v") == 0) { - power_cli_5v(cli, args); + power_cli_5v(pipe, args); break; } if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) { if(furi_string_cmp_str(cmd, "3v3") == 0) { - power_cli_3v3(cli, args); + power_cli_3v3(pipe, args); break; } } diff --git a/applications/services/rpc/rpc_cli.c b/applications/services/rpc/rpc_cli.c index 4612752a8..3280fe702 100644 --- a/applications/services/rpc/rpc_cli.c +++ b/applications/services/rpc/rpc_cli.c @@ -2,24 +2,24 @@ #include #include #include +#include #define TAG "RpcCli" typedef struct { - Cli* cli; + PipeSide* pipe; bool session_close_request; FuriSemaphore* terminate_semaphore; } CliRpc; -#define CLI_READ_BUFFER_SIZE 64 +#define CLI_READ_BUFFER_SIZE 64UL static void rpc_cli_send_bytes_callback(void* context, uint8_t* bytes, size_t bytes_len) { furi_assert(context); furi_assert(bytes); furi_assert(bytes_len > 0); CliRpc* cli_rpc = context; - - cli_write(cli_rpc->cli, bytes, bytes_len); + pipe_send(cli_rpc->pipe, bytes, bytes_len); } static void rpc_cli_session_close_callback(void* context) { @@ -36,9 +36,9 @@ static void rpc_cli_session_terminated_callback(void* context) { furi_semaphore_release(cli_rpc->terminate_semaphore); } -void rpc_cli_command_start_session(Cli* cli, FuriString* args, void* context) { +void rpc_cli_command_start_session(PipeSide* pipe, FuriString* args, void* context) { UNUSED(args); - furi_assert(cli); + furi_assert(pipe); furi_assert(context); Rpc* rpc = context; @@ -53,7 +53,7 @@ void rpc_cli_command_start_session(Cli* cli, FuriString* args, void* context) { return; } - CliRpc cli_rpc = {.cli = cli, .session_close_request = false}; + CliRpc cli_rpc = {.pipe = pipe, .session_close_request = false}; cli_rpc.terminate_semaphore = furi_semaphore_alloc(1, 0); rpc_session_set_context(rpc_session, &cli_rpc); rpc_session_set_send_bytes_callback(rpc_session, rpc_cli_send_bytes_callback); @@ -64,8 +64,9 @@ void rpc_cli_command_start_session(Cli* cli, FuriString* args, void* context) { size_t size_received = 0; while(1) { - size_received = cli_read_timeout(cli_rpc.cli, buffer, CLI_READ_BUFFER_SIZE, 50); - if(!cli_is_connected(cli_rpc.cli) || cli_rpc.session_close_request) { + size_t to_receive = CLAMP(pipe_bytes_available(cli_rpc.pipe), CLI_READ_BUFFER_SIZE, 1UL); + size_received = pipe_receive(cli_rpc.pipe, buffer, to_receive); + if(size_received < to_receive || cli_rpc.session_close_request) { break; } diff --git a/applications/services/rpc/rpc_i.h b/applications/services/rpc/rpc_i.h index 0342df2b6..51903a276 100644 --- a/applications/services/rpc/rpc_i.h +++ b/applications/services/rpc/rpc_i.h @@ -6,6 +6,7 @@ #include #include #include +#include #ifdef __cplusplus extern "C" { @@ -46,7 +47,7 @@ void rpc_desktop_free(void* ctx); void rpc_debug_print_message(const PB_Main* message); void rpc_debug_print_data(const char* prefix, uint8_t* buffer, size_t size); -void rpc_cli_command_start_session(Cli* cli, FuriString* args, void* context); +void rpc_cli_command_start_session(PipeSide* pipe, FuriString* args, void* context); PB_CommandStatus rpc_system_storage_get_error(FS_Error fs_error); diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 441b58da6..416ecce0e 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -10,6 +10,7 @@ #include #include #include +#include #define MAX_NAME_LENGTH 255 @@ -19,8 +20,8 @@ static void storage_cli_print_error(FS_Error error) { printf("Storage error: %s\r\n", storage_error_get_desc(error)); } -static void storage_cli_info(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_info(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); @@ -69,13 +70,14 @@ static void storage_cli_info(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_format(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_format(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); if(furi_string_cmp_str(path, STORAGE_INT_PATH_PREFIX) == 0) { storage_cli_print_error(FSE_NOT_IMPLEMENTED); } else if(furi_string_cmp_str(path, STORAGE_EXT_PATH_PREFIX) == 0) { printf("Formatting SD card, All data will be lost! Are you sure (y/n)?\r\n"); - char answer = cli_getc(cli); + char answer = getchar(); if(answer == 'y' || answer == 'Y') { Storage* api = furi_record_open(RECORD_STORAGE); printf("Formatting, please wait...\r\n"); @@ -96,8 +98,8 @@ static void storage_cli_format(Cli* cli, FuriString* path, FuriString* args) { } } -static void storage_cli_list(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_list(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); if(furi_string_cmp_str(path, "/") == 0) { printf("\t[D] int\r\n"); @@ -134,13 +136,13 @@ static void storage_cli_list(Cli* cli, FuriString* path, FuriString* args) { } } -static void storage_cli_tree(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_tree(PipeSide* pipe, FuriString* path, FuriString* args) { UNUSED(args); if(furi_string_cmp_str(path, "/") == 0) { furi_string_set(path, STORAGE_INT_PATH_PREFIX); - storage_cli_tree(cli, path, NULL); + storage_cli_tree(pipe, path, NULL); furi_string_set(path, STORAGE_EXT_PATH_PREFIX); - storage_cli_tree(cli, path, NULL); + storage_cli_tree(pipe, path, NULL); } else { Storage* api = furi_record_open(RECORD_STORAGE); DirWalk* dir_walk = dir_walk_alloc(api); @@ -176,8 +178,8 @@ static void storage_cli_tree(Cli* cli, FuriString* path, FuriString* args) { } } -static void storage_cli_read(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_read(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); @@ -208,7 +210,8 @@ static void storage_cli_read(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_write(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); @@ -222,9 +225,9 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) { uint32_t read_index = 0; while(true) { - uint8_t symbol = cli_getc(cli); + uint8_t symbol = getchar(); - if(symbol == CliSymbolAsciiETX) { + if(symbol == CliKeyETX) { size_t write_size = read_index % buffer_size; if(write_size > 0) { @@ -263,7 +266,8 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_read_chunks(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_read_chunks(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); @@ -280,7 +284,7 @@ static void storage_cli_read_chunks(Cli* cli, FuriString* path, FuriString* args uint8_t* data = malloc(buffer_size); while(file_size > 0) { printf("\r\nReady?\r\n"); - cli_getc(cli); + getchar(); size_t read_size = storage_file_read(file, data, buffer_size); for(size_t i = 0; i < read_size; i++) { @@ -302,31 +306,32 @@ static void storage_cli_read_chunks(Cli* cli, FuriString* path, FuriString* args furi_record_close(RECORD_STORAGE); } -static void storage_cli_write_chunk(Cli* cli, FuriString* path, FuriString* args) { +static void storage_cli_write_chunk(PipeSide* pipe, FuriString* path, FuriString* args) { Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); - uint32_t buffer_size; - if(strint_to_uint32(furi_string_get_cstr(args), NULL, &buffer_size, 10) != + uint32_t need_to_read; + if(strint_to_uint32(furi_string_get_cstr(args), NULL, &need_to_read, 10) != StrintParseNoError) { storage_cli_print_usage(); } else { if(storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_OPEN_APPEND)) { printf("Ready\r\n"); + const size_t buffer_size = 1024; + uint8_t* buffer = malloc(buffer_size); - if(buffer_size) { - uint8_t* buffer = malloc(buffer_size); + while(need_to_read) { + size_t read_this_time = pipe_receive(pipe, buffer, MIN(buffer_size, need_to_read)); + size_t wrote_this_time = storage_file_write(file, buffer, read_this_time); - size_t read_bytes = cli_read(cli, buffer, buffer_size); - - size_t written_size = storage_file_write(file, buffer, read_bytes); - - if(written_size != buffer_size) { + if(wrote_this_time != read_this_time) { storage_cli_print_error(storage_file_get_error(file)); + break; } - - free(buffer); + need_to_read -= read_this_time; } + + free(buffer); } else { storage_cli_print_error(storage_file_get_error(file)); } @@ -337,8 +342,8 @@ static void storage_cli_write_chunk(Cli* cli, FuriString* path, FuriString* args furi_record_close(RECORD_STORAGE); } -static void storage_cli_stat(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_stat(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); @@ -379,8 +384,8 @@ static void storage_cli_stat(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_timestamp(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_timestamp(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); @@ -396,8 +401,8 @@ static void storage_cli_timestamp(Cli* cli, FuriString* path, FuriString* args) furi_record_close(RECORD_STORAGE); } -static void storage_cli_copy(Cli* cli, FuriString* old_path, FuriString* args) { - UNUSED(cli); +static void storage_cli_copy(PipeSide* pipe, FuriString* old_path, FuriString* args) { + UNUSED(pipe); Storage* api = furi_record_open(RECORD_STORAGE); FuriString* new_path; new_path = furi_string_alloc(); @@ -417,8 +422,8 @@ static void storage_cli_copy(Cli* cli, FuriString* old_path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_remove(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_remove(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); FS_Error error = storage_common_remove(api, furi_string_get_cstr(path)); @@ -430,8 +435,8 @@ static void storage_cli_remove(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_rename(Cli* cli, FuriString* old_path, FuriString* args) { - UNUSED(cli); +static void storage_cli_rename(PipeSide* pipe, FuriString* old_path, FuriString* args) { + UNUSED(pipe); Storage* api = furi_record_open(RECORD_STORAGE); FuriString* new_path; new_path = furi_string_alloc(); @@ -451,8 +456,8 @@ static void storage_cli_rename(Cli* cli, FuriString* old_path, FuriString* args) furi_record_close(RECORD_STORAGE); } -static void storage_cli_mkdir(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_mkdir(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); FS_Error error = storage_common_mkdir(api, furi_string_get_cstr(path)); @@ -464,8 +469,8 @@ static void storage_cli_mkdir(Cli* cli, FuriString* path, FuriString* args) { furi_record_close(RECORD_STORAGE); } -static void storage_cli_md5(Cli* cli, FuriString* path, FuriString* args) { - UNUSED(cli); +static void storage_cli_md5(PipeSide* pipe, FuriString* path, FuriString* args) { + UNUSED(pipe); UNUSED(args); Storage* api = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(api); @@ -491,8 +496,8 @@ static bool tar_extract_file_callback(const char* name, bool is_directory, void* return true; } -static void storage_cli_extract(Cli* cli, FuriString* old_path, FuriString* args) { - UNUSED(cli); +static void storage_cli_extract(PipeSide* pipe, FuriString* old_path, FuriString* args) { + UNUSED(pipe); FuriString* new_path = furi_string_alloc(); if(!args_read_probably_quoted_string_and_trim(args, new_path)) { @@ -526,7 +531,7 @@ static void storage_cli_extract(Cli* cli, FuriString* old_path, FuriString* args furi_record_close(RECORD_STORAGE); } -typedef void (*StorageCliCommandCallback)(Cli* cli, FuriString* path, FuriString* args); +typedef void (*StorageCliCommandCallback)(PipeSide* pipe, FuriString* path, FuriString* args); typedef struct { const char* command; @@ -631,7 +636,7 @@ static void storage_cli_print_usage(void) { } } -void storage_cli(Cli* cli, FuriString* args, void* context) { +void storage_cli(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; FuriString* path; @@ -653,7 +658,7 @@ void storage_cli(Cli* cli, FuriString* args, void* context) { for(; i < COUNT_OF(storage_cli_commands); ++i) { const StorageCliCommand* command_descr = &storage_cli_commands[i]; if(furi_string_cmp_str(cmd, command_descr->command) == 0) { - command_descr->impl(cli, path, args); + command_descr->impl(pipe, path, args); break; } } @@ -667,11 +672,12 @@ void storage_cli(Cli* cli, FuriString* args, void* context) { furi_string_free(cmd); } -static void storage_cli_factory_reset(Cli* cli, FuriString* args, void* context) { +static void storage_cli_factory_reset(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(args); UNUSED(context); printf("All data will be lost! Are you sure (y/n)?\r\n"); - char c = cli_getc(cli); + char c = getchar(); if(c == 'y' || c == 'Y') { printf("Data will be wiped after reboot.\r\n"); @@ -688,7 +694,7 @@ static void storage_cli_factory_reset(Cli* cli, FuriString* args, void* context) void storage_on_system_start(void) { #ifdef SRV_CLI Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_STORAGE, CliCommandFlagParallelSafe, storage_cli, NULL); + cli_add_command_ex(cli, "storage", CliCommandFlagParallelSafe, storage_cli, NULL, 512); cli_add_command( cli, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); furi_record_close(RECORD_CLI); diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index c321150df..bdb2ccc28 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -5,6 +5,7 @@ #include #include #include +#include #define TAG "JS app" @@ -131,12 +132,14 @@ int32_t js_app(void* arg) { } //-V773 typedef struct { - Cli* cli; + PipeSide* pipe; FuriSemaphore* exit_sem; } JsCliContext; static void js_cli_print(JsCliContext* ctx, const char* msg) { - cli_write(ctx->cli, (uint8_t*)msg, strlen(msg)); + UNUSED(ctx); + UNUSED(msg); + pipe_send(ctx->pipe, msg, strlen(msg)); } static void js_cli_exit(JsCliContext* ctx) { @@ -170,7 +173,7 @@ static void js_cli_callback(JsThreadEvent event, const char* msg, void* context) } } -void js_cli_execute(Cli* cli, FuriString* args, void* context) { +void js_cli_execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); const char* path = furi_string_get_cstr(args); @@ -187,14 +190,14 @@ void js_cli_execute(Cli* cli, FuriString* args, void* context) { break; } - JsCliContext ctx = {.cli = cli}; + JsCliContext ctx = {.pipe = pipe}; ctx.exit_sem = furi_semaphore_alloc(1, 0); printf("Running script %s, press CTRL+C to stop\r\n", path); JsThread* js_thread = js_thread_run(path, js_cli_callback, &ctx); while(furi_semaphore_acquire(ctx.exit_sem, 100) != FuriStatusOk) { - if(cli_cmd_interrupt_received(cli)) break; + if(cli_is_pipe_broken_or_is_etx_next_char(pipe)) break; } js_thread_stop(js_thread); diff --git a/applications/system/updater/cli/updater_cli.c b/applications/system/updater/cli/updater_cli.c index 56a16bd9d..01949269f 100644 --- a/applications/system/updater/cli/updater_cli.c +++ b/applications/system/updater/cli/updater_cli.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -63,8 +64,8 @@ static const CliSubcommand update_cli_subcommands[] = { {.command = "help", .handler = updater_cli_help}, }; -static void updater_cli_ep(Cli* cli, FuriString* args, void* context) { - UNUSED(cli); +static void updater_cli_ep(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); UNUSED(context); FuriString* subcommand; subcommand = furi_string_alloc(); diff --git a/furi/core/core_defines.h b/furi/core/core_defines.h index fa56150ce..2d6ced025 100644 --- a/furi/core/core_defines.h +++ b/furi/core/core_defines.h @@ -41,6 +41,16 @@ extern "C" { #define CLAMP(x, upper, lower) (MIN(upper, MAX(x, lower))) #endif +#ifndef CLAMP_WRAPAROUND +#define CLAMP_WRAPAROUND(x, upper, lower) \ + ({ \ + __typeof__(x) _x = (x); \ + __typeof__(upper) _upper = (upper); \ + __typeof__(lower) _lower = (lower); \ + (_x > _upper) ? _lower : ((_x < _lower) ? _upper : _x); \ + }) +#endif + #ifndef COUNT_OF #define COUNT_OF(x) (sizeof(x) / sizeof(x[0])) #endif diff --git a/furi/core/event_loop.c b/furi/core/event_loop.c index e09be0ca4..800fea86c 100644 --- a/furi/core/event_loop.c +++ b/furi/core/event_loop.c @@ -78,6 +78,7 @@ void furi_event_loop_free(FuriEventLoop* instance) { furi_event_loop_process_timer_queue(instance); furi_check(TimerList_empty_p(instance->timer_list)); furi_check(WaitingList_empty_p(instance->waiting_list)); + furi_check(!instance->are_thread_flags_subscribed); FuriEventLoopTree_clear(instance->tree); PendingQueue_clear(instance->pending_queue); @@ -243,6 +244,10 @@ void furi_event_loop_run(FuriEventLoop* instance) { } else if(flags & FuriEventLoopFlagPending) { furi_event_loop_process_pending_callbacks(instance); + } else if(flags & FuriEventLoopFlagThreadFlag) { + if(instance->are_thread_flags_subscribed) + instance->thread_flags_callback(instance->thread_flags_callback_context); + } else { furi_crash(); } @@ -416,6 +421,24 @@ void furi_event_loop_subscribe_mutex( instance, mutex, &furi_mutex_event_loop_contract, event, callback, context); } +void furi_event_loop_subscribe_thread_flags( + FuriEventLoop* instance, + FuriEventLoopThreadFlagsCallback callback, + void* context) { + furi_check(instance); + furi_check(callback); + furi_check(!instance->are_thread_flags_subscribed); + instance->are_thread_flags_subscribed = true; + instance->thread_flags_callback = callback; + instance->thread_flags_callback_context = context; +} + +void furi_event_loop_unsubscribe_thread_flags(FuriEventLoop* instance) { + furi_check(instance); + furi_check(instance->are_thread_flags_subscribed); + instance->are_thread_flags_subscribed = false; +} + /** * Public generic unsubscription API */ @@ -538,6 +561,25 @@ static bool furi_event_loop_item_is_waiting(FuriEventLoopItem* instance) { return instance->WaitingList.prev || instance->WaitingList.next; } +void furi_event_loop_thread_flag_callback(FuriThreadId thread_id) { + TaskHandle_t hTask = (TaskHandle_t)thread_id; + BaseType_t yield; + + if(FURI_IS_IRQ_MODE()) { + yield = pdFALSE; + (void)xTaskNotifyIndexedFromISR( + hTask, + FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, + FuriEventLoopFlagThreadFlag, + eSetBits, + &yield); + portYIELD_FROM_ISR(yield); + } else { + (void)xTaskNotifyIndexed( + hTask, FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, FuriEventLoopFlagThreadFlag, eSetBits); + } +} + /* * Internal event loop link API, used by supported primitives */ diff --git a/furi/core/event_loop.h b/furi/core/event_loop.h index d5e8710a6..63f020f78 100644 --- a/furi/core/event_loop.h +++ b/furi/core/event_loop.h @@ -203,6 +203,12 @@ typedef void FuriEventLoopObject; */ typedef void (*FuriEventLoopEventCallback)(FuriEventLoopObject* object, void* context); +/** Callback type for event loop thread flag events + * + * @param context The context that was provided upon subscription + */ +typedef void (*FuriEventLoopThreadFlagsCallback)(void* context); + /** Opaque event flag type */ typedef struct FuriEventFlag FuriEventFlag; @@ -304,6 +310,23 @@ void furi_event_loop_subscribe_mutex( FuriEventLoopEventCallback callback, void* context); +/** Subscribe to thread flag events of the current thread + * + * @param instance The Event Loop instance + * @param callback The callback to call when a flag has been set + * @param context The context for callback + */ +void furi_event_loop_subscribe_thread_flags( + FuriEventLoop* instance, + FuriEventLoopThreadFlagsCallback callback, + void* context); + +/** Unsubscribe from thread flag events of the current thread + * + * @param instance The Event Loop instance + */ +void furi_event_loop_unsubscribe_thread_flags(FuriEventLoop* instance); + /** Unsubscribe from events (common) * * @param instance The Event Loop instance diff --git a/furi/core/event_loop_i.h b/furi/core/event_loop_i.h index ef2774b97..c2f04a359 100644 --- a/furi/core/event_loop_i.h +++ b/furi/core/event_loop_i.h @@ -4,12 +4,14 @@ #include "event_loop_link_i.h" #include "event_loop_timer_i.h" #include "event_loop_tick_i.h" +#include "event_loop_thread_flag_interface.h" #include #include #include #include "thread.h" +#include "thread_i.h" struct FuriEventLoopItem { // Source @@ -50,11 +52,12 @@ typedef enum { FuriEventLoopFlagStop = (1 << 1), FuriEventLoopFlagTimer = (1 << 2), FuriEventLoopFlagPending = (1 << 3), + FuriEventLoopFlagThreadFlag = (1 << 4), } FuriEventLoopFlag; #define FuriEventLoopFlagAll \ (FuriEventLoopFlagEvent | FuriEventLoopFlagStop | FuriEventLoopFlagTimer | \ - FuriEventLoopFlagPending) + FuriEventLoopFlagPending | FuriEventLoopFlagThreadFlag) typedef enum { FuriEventLoopProcessStatusComplete, @@ -94,4 +97,9 @@ struct FuriEventLoop { PendingQueue_t pending_queue; // Tick event FuriEventLoopTick tick; + + // Thread flags callback + bool are_thread_flags_subscribed; + FuriEventLoopThreadFlagsCallback thread_flags_callback; + void* thread_flags_callback_context; }; diff --git a/furi/core/event_loop_thread_flag_interface.h b/furi/core/event_loop_thread_flag_interface.h new file mode 100644 index 000000000..05fcd47de --- /dev/null +++ b/furi/core/event_loop_thread_flag_interface.h @@ -0,0 +1,10 @@ +#pragma once + +#include "thread.h" + +/** + * @brief Notify `FuriEventLoop` that `furi_thread_flags_set` has been called + * + * @param thread_id Thread id + */ +extern void furi_event_loop_thread_flag_callback(FuriThreadId thread_id); diff --git a/furi/core/thread.c b/furi/core/thread.c index d6132cdc2..e29a8711e 100644 --- a/furi/core/thread.c +++ b/furi/core/thread.c @@ -7,6 +7,7 @@ #include "check.h" #include "common_defines.h" #include "string.h" +#include "event_loop_thread_flag_interface.h" #include "log.h" #include @@ -501,6 +502,9 @@ uint32_t furi_thread_flags_set(FuriThreadId thread_id, uint32_t flags) { (void)xTaskNotifyAndQueryIndexed(hTask, THREAD_NOTIFY_INDEX, 0, eNoAction, &rflags); } } + + furi_event_loop_thread_flag_callback(thread_id); + /* Return flags after setting */ return rflags; } diff --git a/lib/toolbox/pipe.c b/lib/toolbox/pipe.c index c6a45abf1..59b2f63f4 100644 --- a/lib/toolbox/pipe.c +++ b/lib/toolbox/pipe.c @@ -1,6 +1,8 @@ #include "pipe.h" #include +#define PIPE_DEFAULT_STATE_CHECK_PERIOD furi_ms_to_ticks(100) + /** * Data shared between both sides. */ @@ -23,7 +25,7 @@ struct PipeSide { PipeSideDataArrivedCallback on_data_arrived; PipeSideSpaceFreedCallback on_space_freed; PipeSideBrokenCallback on_pipe_broken; - FuriWait stdout_timeout; + FuriWait state_check_period; }; PipeSideBundle pipe_alloc(size_t capacity, size_t trigger_level) { @@ -53,14 +55,14 @@ PipeSideBundle pipe_alloc_ex(PipeSideReceiveSettings alice, PipeSideReceiveSetti .shared = shared, .sending = alice_to_bob, .receiving = bob_to_alice, - .stdout_timeout = FuriWaitForever, + .state_check_period = PIPE_DEFAULT_STATE_CHECK_PERIOD, }; *bobs_side = (PipeSide){ .role = PipeRoleBob, .shared = shared, .sending = bob_to_alice, .receiving = alice_to_bob, - .stdout_timeout = FuriWaitForever, + .state_check_period = PIPE_DEFAULT_STATE_CHECK_PERIOD, }; return (PipeSideBundle){.alices_side = alices_side, .bobs_side = bobs_side}; @@ -99,42 +101,62 @@ void pipe_free(PipeSide* pipe) { } } -static void _pipe_stdout_cb(const char* data, size_t size, void* context) { +static void pipe_stdout_cb(const char* data, size_t size, void* context) { furi_assert(context); PipeSide* pipe = context; - while(size) { - size_t sent = pipe_send(pipe, data, size, pipe->stdout_timeout); - if(!sent) break; - data += sent; - size -= sent; - } + pipe_send(pipe, data, size); } -static size_t _pipe_stdin_cb(char* data, size_t size, FuriWait timeout, void* context) { +static size_t pipe_stdin_cb(char* data, size_t size, FuriWait timeout, void* context) { + UNUSED(timeout); furi_assert(context); PipeSide* pipe = context; - return pipe_receive(pipe, data, size, timeout); + return pipe_receive(pipe, data, size); } void pipe_install_as_stdio(PipeSide* pipe) { furi_check(pipe); - furi_thread_set_stdout_callback(_pipe_stdout_cb, pipe); - furi_thread_set_stdin_callback(_pipe_stdin_cb, pipe); + furi_thread_set_stdout_callback(pipe_stdout_cb, pipe); + furi_thread_set_stdin_callback(pipe_stdin_cb, pipe); } -void pipe_set_stdout_timeout(PipeSide* pipe, FuriWait timeout) { +void pipe_set_state_check_period(PipeSide* pipe, FuriWait check_period) { furi_check(pipe); - pipe->stdout_timeout = timeout; + pipe->state_check_period = check_period; } -size_t pipe_receive(PipeSide* pipe, void* data, size_t length, FuriWait timeout) { +size_t pipe_receive(PipeSide* pipe, void* data, size_t length) { furi_check(pipe); - return furi_stream_buffer_receive(pipe->receiving, data, length, timeout); + + size_t received = 0; + while(length) { + size_t received_this_time = + furi_stream_buffer_receive(pipe->receiving, data, length, pipe->state_check_period); + if(!received_this_time && pipe_state(pipe) == PipeStateBroken) break; + + received += received_this_time; + length -= received_this_time; + data += received_this_time; + } + + return received; } -size_t pipe_send(PipeSide* pipe, const void* data, size_t length, FuriWait timeout) { +size_t pipe_send(PipeSide* pipe, const void* data, size_t length) { furi_check(pipe); - return furi_stream_buffer_send(pipe->sending, data, length, timeout); + + size_t sent = 0; + while(length) { + size_t sent_this_time = + furi_stream_buffer_send(pipe->sending, data, length, pipe->state_check_period); + if(!sent_this_time && pipe_state(pipe) == PipeStateBroken) break; + + sent += sent_this_time; + length -= sent_this_time; + data += sent_this_time; + } + + return sent; } size_t pipe_bytes_available(PipeSide* pipe) { @@ -151,14 +173,14 @@ static void pipe_receiving_buffer_callback(FuriEventLoopObject* buffer, void* co UNUSED(buffer); PipeSide* pipe = context; furi_assert(pipe); - if(pipe->on_space_freed) pipe->on_data_arrived(pipe, pipe->callback_context); + if(pipe->on_data_arrived) pipe->on_data_arrived(pipe, pipe->callback_context); } static void pipe_sending_buffer_callback(FuriEventLoopObject* buffer, void* context) { UNUSED(buffer); PipeSide* pipe = context; furi_assert(pipe); - if(pipe->on_data_arrived) pipe->on_space_freed(pipe, pipe->callback_context); + if(pipe->on_space_freed) pipe->on_space_freed(pipe, pipe->callback_context); } static void pipe_semaphore_callback(FuriEventLoopObject* semaphore, void* context) { diff --git a/lib/toolbox/pipe.h b/lib/toolbox/pipe.h index c05e60d0c..b071975f7 100644 --- a/lib/toolbox/pipe.h +++ b/lib/toolbox/pipe.h @@ -148,38 +148,48 @@ void pipe_free(PipeSide* pipe); void pipe_install_as_stdio(PipeSide* pipe); /** - * @brief Sets the timeout for `stdout` write operations + * @brief Sets the state check period for `send` and `receive` operations * - * @note This value is set to `FuriWaitForever` when the pipe is created + * @note This value is set to 100 ms when the pipe is created * - * @param [in] pipe Pipe side to set the timeout of - * @param [in] timeout Timeout value in ticks + * `send` and `receive` will check the state of the pipe if exactly 0 bytes were + * sent or received during any given `check_period`. Read the documentation for + * `pipe_send` and `pipe_receive` for more info. + * + * @param [in] pipe Pipe side to set the check period of + * @param [in] check_period Period in ticks */ -void pipe_set_stdout_timeout(PipeSide* pipe, FuriWait timeout); +void pipe_set_state_check_period(PipeSide* pipe, FuriWait check_period); /** * @brief Receives data from the pipe. * + * This function will try to receive all of the requested bytes from the pipe. + * If at some point during the operation the pipe becomes broken, this function + * will return prematurely, in which case the return value will be less than the + * requested `length`. + * * @param [in] pipe The pipe side to read data out of * @param [out] data The buffer to fill with data * @param length Maximum length of data to read - * @param timeout The timeout (in ticks) after which the read operation is - * interrupted * @returns The number of bytes actually written into the provided buffer */ -size_t pipe_receive(PipeSide* pipe, void* data, size_t length, FuriWait timeout); +size_t pipe_receive(PipeSide* pipe, void* data, size_t length); /** * @brief Sends data into the pipe. * + * This function will try to send all of the requested bytes to the pipe. + * If at some point during the operation the pipe becomes broken, this function + * will return prematurely, in which case the return value will be less than the + * requested `length`. + * * @param [in] pipe The pipe side to send data into * @param [out] data The buffer to get data from * @param length Maximum length of data to send - * @param timeout The timeout (in ticks) after which the write operation is - * interrupted * @returns The number of bytes actually read from the provided buffer */ -size_t pipe_send(PipeSide* pipe, const void* data, size_t length, FuriWait timeout); +size_t pipe_send(PipeSide* pipe, const void* data, size_t length); /** * @brief Determines how many bytes there are in the pipe available to be read. diff --git a/scripts/serial_cli_perf.py b/scripts/serial_cli_perf.py new file mode 100644 index 000000000..0fed7c393 --- /dev/null +++ b/scripts/serial_cli_perf.py @@ -0,0 +1,59 @@ +import argparse +import logging +from serial import Serial +from random import randint +from time import time + +from flipper.utils.cdc import resolve_port + + +def main(): + logger = logging.getLogger() + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--port", help="CDC Port", default="auto") + parser.add_argument( + "-l", "--length", type=int, help="Number of bytes to send", default=1024**2 + ) + args = parser.parse_args() + + if not (port := resolve_port(logger, args.port)): + logger.error("Is Flipper connected via USB and not in DFU mode?") + return 1 + port = Serial(port, 230400) + port.timeout = 2 + + port.read_until(b">: ") + port.write(b"echo\r") + port.read_until(b">: ") + + print(f"Transferring {args.length} bytes. Hang tight...") + + start_time = time() + + bytes_to_send = args.length + block_size = 1024 + while bytes_to_send: + actual_size = min(block_size, bytes_to_send) + # can't use 0x03 because that's ASCII ETX, or Ctrl+C + block = bytes([randint(4, 255) for _ in range(actual_size)]) + + port.write(block) + return_block = port.read(actual_size) + + if return_block != block: + logger.error("Incorrect block received") + break + + bytes_to_send -= actual_size + + end_time = time() + delta = end_time - start_time + speed = args.length / delta + print(f"Speed: {speed/1024:.2f} KiB/s") + + port.write(b"\x03") # Ctrl+C + port.close() + + +if __name__ == "__main__": + main() diff --git a/scripts/update.py b/scripts/update.py index 992f75603..1c877c23e 100755 --- a/scripts/update.py +++ b/scripts/update.py @@ -34,7 +34,7 @@ class Main(App): FLASH_BASE = 0x8000000 FLASH_PAGE_SIZE = 4 * 1024 - MIN_GAP_PAGES = 1 + MIN_GAP_PAGES = 0 # Update stage file larger than that is not loadable without fix # https://github.com/flipperdevices/flipperzero-firmware/pull/3676 diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 981340138..bfe7afbcd 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,8 +1,9 @@ entry,status,name,type,params -Version,+,83.0,, +Version,+,84.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, +Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -778,18 +779,17 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliCallback, void*" -Function,+,cli_cmd_interrupt_received,_Bool,Cli* +Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" +Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" +Function,+,cli_ansi_parser_alloc,CliAnsiParser*, +Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" +Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* +Function,+,cli_ansi_parser_free,void,CliAnsiParser* Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_getc,char,Cli* -Function,+,cli_is_connected,_Bool,Cli* -Function,+,cli_nl,void,Cli* +Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_read,size_t,"Cli*, uint8_t*, size_t" -Function,+,cli_read_timeout,size_t,"Cli*, uint8_t*, size_t, uint32_t" -Function,+,cli_session_close,void,Cli* -Function,+,cli_session_open,void,"Cli*, const void*" -Function,+,cli_write,void,"Cli*, const uint8_t*, size_t" +Function,+,cli_vcp_disable,void,CliVcp* +Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" Function,+,composite_api_resolver_alloc,CompositeApiResolver*, Function,+,composite_api_resolver_free,void,CompositeApiResolver* @@ -1138,6 +1138,7 @@ Function,+,furi_event_loop_subscribe_message_queue,void,"FuriEventLoop*, FuriMes Function,+,furi_event_loop_subscribe_mutex,void,"FuriEventLoop*, FuriMutex*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_semaphore,void,"FuriEventLoop*, FuriSemaphore*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_stream_buffer,void,"FuriEventLoop*, FuriStreamBuffer*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" +Function,+,furi_event_loop_subscribe_thread_flags,void,"FuriEventLoop*, FuriEventLoopThreadFlagsCallback, void*" Function,+,furi_event_loop_tick_set,void,"FuriEventLoop*, uint32_t, FuriEventLoopTickCallback, void*" Function,+,furi_event_loop_timer_alloc,FuriEventLoopTimer*,"FuriEventLoop*, FuriEventLoopTimerCallback, FuriEventLoopTimerType, void*" Function,+,furi_event_loop_timer_free,void,FuriEventLoopTimer* @@ -1148,6 +1149,7 @@ Function,+,furi_event_loop_timer_restart,void,FuriEventLoopTimer* Function,+,furi_event_loop_timer_start,void,"FuriEventLoopTimer*, uint32_t" Function,+,furi_event_loop_timer_stop,void,FuriEventLoopTimer* Function,+,furi_event_loop_unsubscribe,void,"FuriEventLoop*, FuriEventLoopObject*" +Function,+,furi_event_loop_unsubscribe_thread_flags,void,FuriEventLoop* Function,+,furi_get_tick,uint32_t, Function,+,furi_hal_adc_acquire,FuriHalAdcHandle*, Function,+,furi_hal_adc_configure,void,FuriHalAdcHandle* @@ -2329,14 +2331,14 @@ Function,+,pipe_bytes_available,size_t,PipeSide* Function,+,pipe_detach_from_event_loop,void,PipeSide* Function,+,pipe_free,void,PipeSide* Function,+,pipe_install_as_stdio,void,PipeSide* -Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t, FuriWait" +Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t" Function,+,pipe_role,PipeRole,PipeSide* -Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t, FuriWait" +Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t" Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" -Function,+,pipe_set_stdout_timeout,void,"PipeSide*, FuriWait" +Function,+,pipe_set_state_check_period,void,"PipeSide*, FuriWait" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* Function,+,plugin_manager_alloc,PluginManager*,"const char*, uint32_t, const ElfApiInterface*" @@ -2939,7 +2941,6 @@ Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, Variable,+,ble_profile_serial,const FuriHalBleProfileTemplate* const, -Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index da275594b..1a8c46f10 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,9 +1,10 @@ entry,status,name,type,params -Version,+,83.0,, +Version,+,84.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, +Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -855,18 +856,17 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliCallback, void*" -Function,+,cli_cmd_interrupt_received,_Bool,Cli* +Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" +Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" +Function,+,cli_ansi_parser_alloc,CliAnsiParser*, +Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" +Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* +Function,+,cli_ansi_parser_free,void,CliAnsiParser* Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_getc,char,Cli* -Function,+,cli_is_connected,_Bool,Cli* -Function,+,cli_nl,void,Cli* +Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_read,size_t,"Cli*, uint8_t*, size_t" -Function,+,cli_read_timeout,size_t,"Cli*, uint8_t*, size_t, uint32_t" -Function,+,cli_session_close,void,Cli* -Function,+,cli_session_open,void,"Cli*, const void*" -Function,+,cli_write,void,"Cli*, const uint8_t*, size_t" +Function,+,cli_vcp_disable,void,CliVcp* +Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" Function,+,composite_api_resolver_alloc,CompositeApiResolver*, Function,+,composite_api_resolver_free,void,CompositeApiResolver* @@ -1250,6 +1250,7 @@ Function,+,furi_event_loop_subscribe_message_queue,void,"FuriEventLoop*, FuriMes Function,+,furi_event_loop_subscribe_mutex,void,"FuriEventLoop*, FuriMutex*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_semaphore,void,"FuriEventLoop*, FuriSemaphore*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_stream_buffer,void,"FuriEventLoop*, FuriStreamBuffer*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" +Function,+,furi_event_loop_subscribe_thread_flags,void,"FuriEventLoop*, FuriEventLoopThreadFlagsCallback, void*" Function,+,furi_event_loop_tick_set,void,"FuriEventLoop*, uint32_t, FuriEventLoopTickCallback, void*" Function,+,furi_event_loop_timer_alloc,FuriEventLoopTimer*,"FuriEventLoop*, FuriEventLoopTimerCallback, FuriEventLoopTimerType, void*" Function,+,furi_event_loop_timer_free,void,FuriEventLoopTimer* @@ -1260,6 +1261,7 @@ Function,+,furi_event_loop_timer_restart,void,FuriEventLoopTimer* Function,+,furi_event_loop_timer_start,void,"FuriEventLoopTimer*, uint32_t" Function,+,furi_event_loop_timer_stop,void,FuriEventLoopTimer* Function,+,furi_event_loop_unsubscribe,void,"FuriEventLoop*, FuriEventLoopObject*" +Function,+,furi_event_loop_unsubscribe_thread_flags,void,FuriEventLoop* Function,+,furi_get_tick,uint32_t, Function,+,furi_hal_adc_acquire,FuriHalAdcHandle*, Function,+,furi_hal_adc_configure,void,FuriHalAdcHandle* @@ -2967,14 +2969,14 @@ Function,+,pipe_bytes_available,size_t,PipeSide* Function,+,pipe_detach_from_event_loop,void,PipeSide* Function,+,pipe_free,void,PipeSide* Function,+,pipe_install_as_stdio,void,PipeSide* -Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t, FuriWait" +Function,+,pipe_receive,size_t,"PipeSide*, void*, size_t" Function,+,pipe_role,PipeRole,PipeSide* -Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t, FuriWait" +Function,+,pipe_send,size_t,"PipeSide*, const void*, size_t" Function,+,pipe_set_broken_callback,void,"PipeSide*, PipeSideBrokenCallback, FuriEventLoopEvent" Function,+,pipe_set_callback_context,void,"PipeSide*, void*" Function,+,pipe_set_data_arrived_callback,void,"PipeSide*, PipeSideDataArrivedCallback, FuriEventLoopEvent" Function,+,pipe_set_space_freed_callback,void,"PipeSide*, PipeSideSpaceFreedCallback, FuriEventLoopEvent" -Function,+,pipe_set_stdout_timeout,void,"PipeSide*, FuriWait" +Function,+,pipe_set_state_check_period,void,"PipeSide*, FuriWait" Function,+,pipe_spaces_available,size_t,PipeSide* Function,+,pipe_state,PipeState,PipeSide* Function,+,plugin_manager_alloc,PluginManager*,"const char*, uint32_t, const ElfApiInterface*" @@ -3791,7 +3793,6 @@ Variable,-,_sys_errlist,const char* const[], Variable,-,_sys_nerr,int, Variable,-,ble_profile_hid,const FuriHalBleProfileTemplate*, Variable,+,ble_profile_serial,const FuriHalBleProfileTemplate* const, -Variable,+,cli_vcp,const CliSession, Variable,+,compress_config_heatshrink_default,const CompressConfigHeatshrink, Variable,+,firmware_api_interface,const ElfApiInterface* const, Variable,+,furi_hal_i2c_bus_external,FuriHalI2cBus, diff --git a/targets/f7/furi_hal/furi_hal_usb_cdc.h b/targets/f7/furi_hal/furi_hal_usb_cdc.h index 89b68991b..50d456698 100644 --- a/targets/f7/furi_hal/furi_hal_usb_cdc.h +++ b/targets/f7/furi_hal/furi_hal_usb_cdc.h @@ -9,11 +9,21 @@ extern "C" { #endif +typedef enum { + CdcStateDisconnected, + CdcStateConnected, +} CdcState; + +typedef enum { + CdcCtrlLineDTR = (1 << 0), + CdcCtrlLineRTS = (1 << 1), +} CdcCtrlLine; + typedef struct { void (*tx_ep_callback)(void* context); void (*rx_ep_callback)(void* context); - void (*state_callback)(void* context, uint8_t state); - void (*ctrl_line_callback)(void* context, uint8_t state); + void (*state_callback)(void* context, CdcState state); + void (*ctrl_line_callback)(void* context, CdcCtrlLine ctrl_lines); void (*config_callback)(void* context, struct usb_cdc_line_coding* config); } CdcCallbacks; From 24fbfd16636ec82febfb099341b2a8f4930258e8 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Thu, 3 Apr 2025 17:07:47 +0400 Subject: [PATCH 2/9] [FL-3956] CLI autocomplete and other sugar (#4115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 completions * more key combos * 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 * fix merge * fix merge * cli_shell: fix autocomplete up/down logic * cli_shell: don't add empty line to history * cli_shell: fix history recall * cli_shell: find manually typed command in history * cli_shell: different up/down completions navigation * fix formatting * cli_shell: fix memory leak * cli_shell: silence pvs warning * test_pipe: fix race condition * storage_cli: terminate on pipe broken --------- Co-authored-by: Georgii Surkov Co-authored-by: あく --- .../debug/unit_tests/tests/pipe/pipe_test.c | 9 +- applications/services/cli/application.fam | 1 + applications/services/cli/shell/cli_shell.c | 6 + .../cli/shell/cli_shell_completions.c | 362 ++++++++++++++++++ .../cli/shell/cli_shell_completions.h | 24 ++ .../services/cli/shell/cli_shell_line.c | 145 ++++++- .../services/cli/shell/cli_shell_line.h | 4 + applications/services/storage/storage_cli.c | 6 +- 8 files changed, 540 insertions(+), 17 deletions(-) create mode 100644 applications/services/cli/shell/cli_shell_completions.c create mode 100644 applications/services/cli/shell/cli_shell_completions.h diff --git a/applications/debug/unit_tests/tests/pipe/pipe_test.c b/applications/debug/unit_tests/tests/pipe/pipe_test.c index 4eae39636..f0227b353 100644 --- a/applications/debug/unit_tests/tests/pipe/pipe_test.c +++ b/applications/debug/unit_tests/tests/pipe/pipe_test.c @@ -70,10 +70,9 @@ static void on_data_arrived(PipeSide* pipe, void* context) { } static void on_space_freed(PipeSide* pipe, void* context) { + UNUSED(pipe); AncillaryThreadContext* ctx = context; ctx->flag |= TestFlagSpaceFreed; - const char* message = "Hi!"; - pipe_send(pipe, message, strlen(message)); } static void on_became_broken(PipeSide* pipe, void* context) { @@ -119,16 +118,10 @@ MU_TEST(pipe_test_event_loop) { size_t size = pipe_receive(alice, buffer_1, strlen(message)); buffer_1[size] = 0; - char buffer_2[16]; - const char* expected_reply = "Hi!"; - size = pipe_receive(alice, buffer_2, strlen(expected_reply)); - buffer_2[size] = 0; - pipe_free(alice); furi_thread_join(thread); mu_assert_string_eq(message, buffer_1); - mu_assert_string_eq(expected_reply, buffer_2); mu_assert_int_eq( TestFlagDataArrived | TestFlagSpaceFreed | TestFlagBecameBroken, furi_thread_get_return_code(thread)); diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 60af336e5..9c00f442b 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -6,6 +6,7 @@ App( sources=[ "cli.c", "shell/cli_shell.c", + "shell/cli_shell_completions.c", "shell/cli_shell_line.c", "cli_commands.c", "cli_command_gpio.c", diff --git a/applications/services/cli/shell/cli_shell.c b/applications/services/cli/shell/cli_shell.c index 62cbbd403..2e95c767b 100644 --- a/applications/services/cli/shell/cli_shell.c +++ b/applications/services/cli/shell/cli_shell.c @@ -4,6 +4,7 @@ #include "../cli_i.h" #include "../cli_commands.h" #include "cli_shell_line.h" +#include "cli_shell_completions.h" #include #include #include @@ -17,11 +18,13 @@ #define ANSI_TIMEOUT_MS 10 typedef enum { + CliShellComponentCompletions, CliShellComponentLine, CliShellComponentMAX, //pipe); cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); + cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( + cli_shell->cli, cli_shell, cli_shell->components[CliShellComponentLine]); cli_shell->event_loop = furi_event_loop_alloc(); cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( @@ -172,6 +177,7 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) { } static void cli_shell_free(CliShell* cli_shell) { + cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]); cli_shell_line_free(cli_shell->components[CliShellComponentLine]); pipe_detach_from_event_loop(cli_shell->pipe); diff --git a/applications/services/cli/shell/cli_shell_completions.c b/applications/services/cli/shell/cli_shell_completions.c new file mode 100644 index 000000000..6edb6eaf1 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_completions.c @@ -0,0 +1,362 @@ +#include "cli_shell_completions.h" + +ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524 +#define M_OPL_CommandCompletions_t() ARRAY_OPLIST(CommandCompletions) + +struct CliShellCompletions { + Cli* cli; + CliShell* shell; + CliShellLine* line; + CommandCompletions_t variants; + size_t selected; + bool is_displaying; +}; + +#define COMPLETION_COLUMNS 3 +#define COMPLETION_COLUMN_WIDTH "30" +#define COMPLETION_COLUMN_WIDTH_I 30 + +/** + * @brief Update for the completions menu + */ +typedef enum { + CliShellCompletionsActionOpen, + CliShellCompletionsActionClose, + CliShellCompletionsActionUp, + CliShellCompletionsActionDown, + CliShellCompletionsActionLeft, + CliShellCompletionsActionRight, + CliShellCompletionsActionSelect, + CliShellCompletionsActionSelectNoClose, +} CliShellCompletionsAction; + +typedef enum { + CliShellCompletionSegmentTypeCommand, + CliShellCompletionSegmentTypeArguments, +} CliShellCompletionSegmentType; + +typedef struct { + CliShellCompletionSegmentType type; + size_t start; + size_t length; +} CliShellCompletionSegment; + +// ========== +// Public API +// ========== + +CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line) { + CliShellCompletions* completions = malloc(sizeof(CliShellCompletions)); + + completions->cli = cli; + completions->shell = shell; + completions->line = line; + CommandCompletions_init(completions->variants); + + return completions; +} + +void cli_shell_completions_free(CliShellCompletions* completions) { + CommandCompletions_clear(completions->variants); + free(completions); +} + +// ======= +// Helpers +// ======= + +CliShellCompletionSegment cli_shell_completions_segment(CliShellCompletions* completions) { + furi_assert(completions); + CliShellCompletionSegment segment; + + FuriString* input = furi_string_alloc_set(cli_shell_line_get_editing(completions->line)); + furi_string_left(input, cli_shell_line_get_line_position(completions->line)); + + // find index of first non-space character + size_t first_non_space = 0; + while(1) { + size_t ret = furi_string_search_char(input, ' ', first_non_space); + if(ret == FURI_STRING_FAILURE) break; + if(ret - first_non_space > 1) break; + first_non_space++; + } + + size_t first_space_in_command = furi_string_search_char(input, ' ', first_non_space); + + if(first_space_in_command == FURI_STRING_FAILURE) { + segment.type = CliShellCompletionSegmentTypeCommand; + segment.start = first_non_space; + segment.length = furi_string_size(input) - first_non_space; + } else { + segment.type = CliShellCompletionSegmentTypeArguments; + segment.start = 0; + segment.length = 0; + // support removed, might reimplement in the future + } + + furi_string_free(input); + return segment; +} + +void cli_shell_completions_fill_variants(CliShellCompletions* completions) { + furi_assert(completions); + CommandCompletions_reset(completions->variants); + + CliShellCompletionSegment segment = cli_shell_completions_segment(completions); + FuriString* input = furi_string_alloc_set(cli_shell_line_get_editing(completions->line)); + furi_string_right(input, segment.start); + furi_string_left(input, segment.length); + + if(segment.type == CliShellCompletionSegmentTypeCommand) { + CliCommandTree_t* commands = cli_get_commands(completions->cli); + for + M_EACH(registered_command, *commands, CliCommandTree_t) { + FuriString* command_name = *registered_command->key_ptr; + if(furi_string_start_with(command_name, input)) { + CommandCompletions_push_back(completions->variants, command_name); + } + } + + } else { + // support removed, might reimplement in the future + } + + furi_string_free(input); +} + +static size_t cli_shell_completions_rows_at_column(CliShellCompletions* completions, size_t x) { + size_t completions_size = CommandCompletions_size(completions->variants); + size_t n_full_rows = completions_size / COMPLETION_COLUMNS; + size_t n_cols_in_last_row = completions_size % COMPLETION_COLUMNS; + size_t n_rows_at_x = n_full_rows + ((x >= n_cols_in_last_row) ? 0 : 1); + return n_rows_at_x; +} + +void cli_shell_completions_render( + CliShellCompletions* completions, + CliShellCompletionsAction action) { + furi_assert(completions); + if(action == CliShellCompletionsActionOpen) furi_check(!completions->is_displaying); + if(action == CliShellCompletionsActionClose) furi_check(completions->is_displaying); + + char prompt[64]; + cli_shell_line_format_prompt(completions->line, prompt, sizeof(prompt)); + + if(action == CliShellCompletionsActionOpen) { + cli_shell_completions_fill_variants(completions); + completions->selected = 0; + + if(CommandCompletions_size(completions->variants) == 1) { + cli_shell_completions_render(completions, CliShellCompletionsActionSelectNoClose); + return; + } + + // show completions menu (full re-render) + printf("\n\r"); + size_t position = 0; + for + M_EACH(completion, completions->variants, CommandCompletions_t) { + if(position == completions->selected) printf(ANSI_INVERT); + printf("%-" COMPLETION_COLUMN_WIDTH "s", furi_string_get_cstr(*completion)); + if(position == completions->selected) printf(ANSI_RESET); + if((position % COMPLETION_COLUMNS == COMPLETION_COLUMNS - 1) && + position != CommandCompletions_size(completions->variants)) { + printf("\r\n"); + } + position++; + } + + if(!position) { + printf(ANSI_FG_RED "no completions" ANSI_RESET); + } + + size_t total_rows = (position / COMPLETION_COLUMNS) + 1; + printf( + ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END) ANSI_CURSOR_UP_BY("%zu") + ANSI_CURSOR_HOR_POS("%zu"), + total_rows, + strlen(prompt) + cli_shell_line_get_line_position(completions->line) + 1); + + completions->is_displaying = true; + + } else if(action == CliShellCompletionsActionClose) { + // clear completions menu + printf( + ANSI_CURSOR_HOR_POS("%zu") ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END) + ANSI_CURSOR_HOR_POS("%zu"), + strlen(prompt) + furi_string_size(cli_shell_line_get_selected(completions->line)) + 1, + strlen(prompt) + cli_shell_line_get_line_position(completions->line) + 1); + completions->is_displaying = false; + + } else if( + action == CliShellCompletionsActionUp || action == CliShellCompletionsActionDown || + action == CliShellCompletionsActionLeft || action == CliShellCompletionsActionRight) { + if(CommandCompletions_empty_p(completions->variants)) return; + + // move selection + size_t completions_size = CommandCompletions_size(completions->variants); + size_t old_selection = completions->selected; + int n_columns = (completions_size >= COMPLETION_COLUMNS) ? COMPLETION_COLUMNS : + completions_size; + int selection_unclamped = old_selection; + if(action == CliShellCompletionsActionLeft) { + selection_unclamped--; + } else if(action == CliShellCompletionsActionRight) { + selection_unclamped++; + } else { + int selection_x = old_selection % COMPLETION_COLUMNS; + int selection_y_unclamped = old_selection / COMPLETION_COLUMNS; + if(action == CliShellCompletionsActionUp) selection_y_unclamped--; + if(action == CliShellCompletionsActionDown) selection_y_unclamped++; + size_t selection_y = 0; + if(selection_y_unclamped < 0) { + selection_x = CLAMP_WRAPAROUND(selection_x - 1, n_columns - 1, 0); + selection_y = + cli_shell_completions_rows_at_column(completions, selection_x) - 1; // -V537 + } else if( + (size_t)selection_y_unclamped > + cli_shell_completions_rows_at_column(completions, selection_x) - 1) { + selection_x = CLAMP_WRAPAROUND(selection_x + 1, n_columns - 1, 0); + selection_y = 0; + } else { + selection_y = selection_y_unclamped; + } + selection_unclamped = (selection_y * COMPLETION_COLUMNS) + selection_x; + } + size_t new_selection = CLAMP_WRAPAROUND(selection_unclamped, (int)completions_size - 1, 0); + completions->selected = new_selection; + + if(new_selection != old_selection) { + // determine selection coordinates relative to top-left of suggestion menu + size_t old_x = (old_selection % COMPLETION_COLUMNS) * COMPLETION_COLUMN_WIDTH_I; + size_t old_y = old_selection / COMPLETION_COLUMNS; + size_t new_x = (new_selection % COMPLETION_COLUMNS) * COMPLETION_COLUMN_WIDTH_I; + size_t new_y = new_selection / COMPLETION_COLUMNS; + printf("\n\r"); + + // print old selection in normal colors + if(old_y) printf(ANSI_CURSOR_DOWN_BY("%zu"), old_y); + printf(ANSI_CURSOR_HOR_POS("%zu"), old_x + 1); + printf( + "%-" COMPLETION_COLUMN_WIDTH "s", + furi_string_get_cstr( + *CommandCompletions_cget(completions->variants, old_selection))); + if(old_y) printf(ANSI_CURSOR_UP_BY("%zu"), old_y); + printf(ANSI_CURSOR_HOR_POS("1")); + + // print new selection in inverted colors + if(new_y) printf(ANSI_CURSOR_DOWN_BY("%zu"), new_y); + printf(ANSI_CURSOR_HOR_POS("%zu"), new_x + 1); + printf( + ANSI_INVERT "%-" COMPLETION_COLUMN_WIDTH "s" ANSI_RESET, + furi_string_get_cstr( + *CommandCompletions_cget(completions->variants, new_selection))); + + // return cursor + printf(ANSI_CURSOR_UP_BY("%zu"), new_y + 1); + printf( + ANSI_CURSOR_HOR_POS("%zu"), + strlen(prompt) + furi_string_size(cli_shell_line_get_selected(completions->line)) + + 1); + } + + } else if(action == CliShellCompletionsActionSelectNoClose) { + // insert selection into prompt + CliShellCompletionSegment segment = cli_shell_completions_segment(completions); + FuriString* input = cli_shell_line_get_selected(completions->line); + FuriString* completion = + *CommandCompletions_cget(completions->variants, completions->selected); + furi_string_replace_at( + input, segment.start, segment.length, furi_string_get_cstr(completion)); + printf( + ANSI_CURSOR_HOR_POS("%zu") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + strlen(prompt) + 1, + furi_string_get_cstr(input)); + + int position_change = (int)furi_string_size(completion) - (int)segment.length; + cli_shell_line_set_line_position( + completions->line, + MAX(0, (int)cli_shell_line_get_line_position(completions->line) + position_change)); + + } else if(action == CliShellCompletionsActionSelect) { + cli_shell_completions_render(completions, CliShellCompletionsActionSelectNoClose); + cli_shell_completions_render(completions, CliShellCompletionsActionClose); + + } else { + furi_crash(); + } + + fflush(stdout); +} + +// ============== +// Input handlers +// ============== + +static bool hide_if_open_and_continue_handling(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + if(completions->is_displaying) + cli_shell_completions_render(completions, CliShellCompletionsActionClose); + return false; // process other home events +} + +static bool key_combo_cr(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render(completions, CliShellCompletionsActionSelect); + return true; +} + +static bool key_combo_up_down(CliKeyCombo combo, void* context) { + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render( + completions, + (combo.key == CliKeyUp) ? CliShellCompletionsActionUp : CliShellCompletionsActionDown); + return true; +} + +static bool key_combo_left_right(CliKeyCombo combo, void* context) { + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render( + completions, + (combo.key == CliKeyLeft) ? CliShellCompletionsActionLeft : + CliShellCompletionsActionRight); + return true; +} + +static bool key_combo_tab(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + cli_shell_completions_render( + completions, + completions->is_displaying ? CliShellCompletionsActionRight : + CliShellCompletionsActionOpen); + return true; +} + +static bool key_combo_esc(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render(completions, CliShellCompletionsActionClose); + return true; +} + +CliShellKeyComboSet cli_shell_completions_key_combo_set = { + .fallback = hide_if_open_and_continue_handling, + .count = 7, + .records = + { + {{CliModKeyNo, CliKeyCR}, key_combo_cr}, + {{CliModKeyNo, CliKeyUp}, key_combo_up_down}, + {{CliModKeyNo, CliKeyDown}, key_combo_up_down}, + {{CliModKeyNo, CliKeyLeft}, key_combo_left_right}, + {{CliModKeyNo, CliKeyRight}, key_combo_left_right}, + {{CliModKeyNo, CliKeyTab}, key_combo_tab}, + {{CliModKeyNo, CliKeyEsc}, key_combo_esc}, + }, +}; diff --git a/applications/services/cli/shell/cli_shell_completions.h b/applications/services/cli/shell/cli_shell_completions.h new file mode 100644 index 000000000..6353bde71 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_completions.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include "cli_shell_i.h" +#include "cli_shell_line.h" +#include "../cli.h" +#include "../cli_i.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CliShellCompletions CliShellCompletions; + +CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line); + +void cli_shell_completions_free(CliShellCompletions* completions); + +extern CliShellKeyComboSet cli_shell_completions_key_combo_set; + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/shell/cli_shell_line.c b/applications/services/cli/shell/cli_shell_line.c index 45bc19d9d..959cd0b3b 100644 --- a/applications/services/cli/shell/cli_shell_line.c +++ b/applications/services/cli/shell/cli_shell_line.c @@ -65,6 +65,64 @@ void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) { } } +size_t cli_shell_line_get_line_position(CliShellLine* line) { + return line->line_position; +} + +void cli_shell_line_set_line_position(CliShellLine* line, size_t position) { + line->line_position = position; +} + +// ======= +// Helpers +// ======= + +typedef enum { + CliCharClassWord, + CliCharClassSpace, + CliCharClassOther, +} CliCharClass; + +typedef enum { + CliSkipDirectionLeft, + CliSkipDirectionRight, +} CliSkipDirection; + +CliCharClass cli_shell_line_char_class(char c) { + if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { + return CliCharClassWord; + } else if(c == ' ') { + return CliCharClassSpace; + } else { + return CliCharClassOther; + } +} + +size_t + cli_shell_line_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) { + if(furi_string_size(string) == 0) return original_pos; + if(direction == CliSkipDirectionLeft && original_pos == 0) return original_pos; + if(direction == CliSkipDirectionRight && original_pos == furi_string_size(string)) + return original_pos; + + int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0; + int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1; + int32_t position = original_pos; + CliCharClass start_class = + cli_shell_line_char_class(furi_string_get_char(string, position + look_offset)); + + while(true) { + position += increment; + if(position < 0) break; + if(position >= (int32_t)furi_string_size(string)) break; + if(cli_shell_line_char_class(furi_string_get_char(string, position + look_offset)) != + start_class) + break; + } + + return MAX(0, position); +} + // ============== // Input handlers // ============== @@ -87,13 +145,32 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { FuriString* command = cli_shell_line_get_selected(line); furi_string_trim(command); + if(furi_string_empty(command)) { + cli_shell_line_prompt(line); + return true; + } + FuriString* command_copy = furi_string_alloc_set(command); + if(line->history_position == 0) { + for(size_t i = 1; i < line->history_entries; i++) { + if(furi_string_cmp(line->history[i], command) == 0) { + line->history_position = i; + command = cli_shell_line_get_selected(line); + furi_string_trim(command); + break; + } + } + } + + // move selected command to the front if(line->history_position > 0) { - // move selected command to the front + size_t pos = line->history_position; + size_t len = line->history_entries; memmove( - &line->history[1], &line->history[0], line->history_position * sizeof(FuriString*)); - line->history[0] = command; + &line->history[pos], &line->history[pos + 1], (len - pos - 1) * sizeof(FuriString*)); + furi_string_move(line->history[0], command); + line->history_entries--; } // insert empty command @@ -109,7 +186,7 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { // execute command printf("\r\n"); - if(!furi_string_empty(command_copy)) cli_shell_execute_command(line->shell, command_copy); + cli_shell_execute_command(line->shell, command_copy); furi_string_free(command_copy); cli_shell_line_prompt(line); @@ -199,7 +276,57 @@ static bool cli_shell_line_input_bksp(CliKeyCombo combo, void* context) { return true; } -static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) { +static bool cli_shell_line_input_ctrl_l(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // clear screen + FuriString* command = cli_shell_line_get_selected(line); + char prompt[64]; + cli_shell_line_format_prompt(line, prompt, sizeof(prompt)); + printf( + ANSI_ERASE_DISPLAY(ANSI_ERASE_ENTIRE) ANSI_ERASE_SCROLLBACK_BUFFER ANSI_CURSOR_POS( + "1", "1") "%s%s" ANSI_CURSOR_HOR_POS("%zu"), + prompt, + furi_string_get_cstr(command), + strlen(prompt) + line->line_position + 1 /* 1-based column indexing */); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_ctrl_left_right(CliKeyCombo combo, void* context) { + CliShellLine* line = context; + // skip run of similar chars to the left or right + FuriString* selected_line = cli_shell_line_get_selected(line); + CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft : + CliSkipDirectionRight; + line->line_position = cli_shell_line_skip_run(selected_line, line->line_position, direction); + printf( + ANSI_CURSOR_HOR_POS("%zu"), cli_shell_line_prompt_length(line) + line->line_position + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_ctrl_bksp(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // delete run of similar chars to the left + cli_shell_line_ensure_not_overwriting_history(line); + FuriString* selected_line = cli_shell_line_get_selected(line); + size_t run_start = + cli_shell_line_skip_run(selected_line, line->line_position, CliSkipDirectionLeft); + furi_string_replace_at(selected_line, run_start, line->line_position - run_start, ""); + line->line_position = run_start; + printf( + ANSI_CURSOR_HOR_POS("%zu") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END) + ANSI_CURSOR_HOR_POS("%zu"), + cli_shell_line_prompt_length(line) + line->line_position + 1, + furi_string_get_cstr(selected_line) + run_start, + cli_shell_line_prompt_length(line) + run_start + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_normal(CliKeyCombo combo, void* context) { CliShellLine* line = context; if(combo.modifiers != CliModKeyNo) return false; if(combo.key < CliKeySpace || combo.key >= CliKeyDEL) return false; @@ -220,8 +347,8 @@ static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) { } CliShellKeyComboSet cli_shell_line_key_combo_set = { - .fallback = cli_shell_line_input_fallback, - .count = 10, + .fallback = cli_shell_line_input_normal, + .count = 14, .records = { {{CliModKeyNo, CliKeyETX}, cli_shell_line_input_ctrl_c}, @@ -234,5 +361,9 @@ CliShellKeyComboSet cli_shell_line_key_combo_set = { {{CliModKeyNo, CliKeyEnd}, cli_shell_line_input_end}, {{CliModKeyNo, CliKeyBackspace}, cli_shell_line_input_bksp}, {{CliModKeyNo, CliKeyDEL}, cli_shell_line_input_bksp}, + {{CliModKeyNo, CliKeyFF}, cli_shell_line_input_ctrl_l}, + {{CliModKeyCtrl, CliKeyLeft}, cli_shell_line_input_ctrl_left_right}, + {{CliModKeyCtrl, CliKeyRight}, cli_shell_line_input_ctrl_left_right}, + {{CliModKeyNo, CliKeyETB}, cli_shell_line_input_ctrl_bksp}, }, }; diff --git a/applications/services/cli/shell/cli_shell_line.h b/applications/services/cli/shell/cli_shell_line.h index c1c810ee4..1e4b9e32a 100644 --- a/applications/services/cli/shell/cli_shell_line.h +++ b/applications/services/cli/shell/cli_shell_line.h @@ -24,6 +24,10 @@ void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length); void cli_shell_line_prompt(CliShellLine* line); +size_t cli_shell_line_get_line_position(CliShellLine* line); + +void cli_shell_line_set_line_position(CliShellLine* line, size_t position); + /** * @brief If a line from history has been selected, moves it into the active line */ diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 416ecce0e..903aa1644 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -321,9 +321,11 @@ static void storage_cli_write_chunk(PipeSide* pipe, FuriString* path, FuriString uint8_t* buffer = malloc(buffer_size); while(need_to_read) { - size_t read_this_time = pipe_receive(pipe, buffer, MIN(buffer_size, need_to_read)); - size_t wrote_this_time = storage_file_write(file, buffer, read_this_time); + size_t to_read_this_time = MIN(buffer_size, need_to_read); + size_t read_this_time = pipe_receive(pipe, buffer, to_read_this_time); + if(read_this_time != to_read_this_time) break; + size_t wrote_this_time = storage_file_write(file, buffer, read_this_time); if(wrote_this_time != read_this_time) { storage_cli_print_error(storage_file_get_error(file)); break; From fa09a18483f15709ecb37c3ec003943e70127c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Pomp=C3=B2?= Date: Thu, 3 Apr 2025 15:14:47 +0200 Subject: [PATCH 3/9] Added Vivax and Sansui under Elitelux section (#4173) Apparently both the mentioned Elitelux and Vivax models are the same, so they share the same codes. Sansui is a brand that is also associated with them as one of the available Sansui codes is the same as this one. This code might also work for the non-smart version of the Vivax TV, the TV-32LE114T2S2, but it has not been tested. Co-authored-by: hedger --- applications/main/infrared/resources/infrared/assets/tv.ir | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/main/infrared/resources/infrared/assets/tv.ir b/applications/main/infrared/resources/infrared/assets/tv.ir index 6311f500c..bff8adfa0 100644 --- a/applications/main/infrared/resources/infrared/assets/tv.ir +++ b/applications/main/infrared/resources/infrared/assets/tv.ir @@ -2474,7 +2474,7 @@ protocol: RC5 address: 01 00 00 00 command: 14 00 00 00 # -# Model: Elitelux L32HD1000 +# Model: Elitelux L32HD1000 / Vivax TV-32LE114T2S2SM / Sansui # name: Power type: parsed From 5dcf6b55ef07482189c724cfccd19628968fdef5 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Thu, 3 Apr 2025 21:39:53 +0400 Subject: [PATCH 4/9] [FL-3928, FL-3929] CLI commands in fals and threads (#4116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 completions * more key combos * commands in fals * move commands out of flash * ci: fix errors * speedup cli file transfer * merge fixups * fix f18 * 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_shell: give up pipe to command thread * fix formatting * fix: format * fix merge * fix. merge. * cli_shell: fix detach ordering * desktop: record_cli -> record_cli_vcp * cli: fix spelling, reload/remove ext cmds on card mount/unmount * cli: fix race conditions and formatting * scripts: wait for CTS to go high before starting flipper * scripts: better race condition fix * REVERT THIS: test script race condition fix * Revert "REVERT THIS: test script race condition fix" This reverts commit 3b028d29b07212755872c5706c8c6a58be551636. * REVERT THIS: test script fix * scripts: sleep? * cli: updated oplist for CliCommandTree * Revert "REVERT THIS: test script fix" This reverts commit e9846318549ce092ef422ff97522ba51916163be. * cli: mention memory leak in FL ticket --------- Co-authored-by: Georgii Surkov Co-authored-by: あく Co-authored-by: hedger --- applications/debug/unit_tests/application.fam | 2 +- applications/main/application.fam | 6 - applications/main/ibutton/application.fam | 8 +- applications/main/ibutton/ibutton_cli.c | 13 +- applications/main/infrared/application.fam | 8 +- applications/main/infrared/infrared_cli.c | 13 +- applications/main/lfrfid/application.fam | 8 +- applications/main/lfrfid/lfrfid_cli.c | 8 +- applications/main/nfc/application.fam | 8 +- applications/main/nfc/nfc_cli.c | 12 +- applications/main/onewire/application.fam | 10 +- applications/main/onewire/onewire_cli.c | 13 +- applications/main/subghz/application.fam | 8 +- applications/main/subghz/subghz_cli.c | 14 +- applications/services/cli/application.fam | 16 ++ applications/services/cli/cli.c | 71 +++++++- applications/services/cli/cli.h | 21 +++ applications/services/cli/cli_commands.c | 14 ++ applications/services/cli/cli_i.h | 4 +- .../services/cli/commands/hello_world.c | 10 ++ applications/services/cli/commands/neofetch.c | 159 ++++++++++++++++ applications/services/cli/shell/cli_shell.c | 170 +++++++++++++++--- .../cli/shell/cli_shell_completions.c | 2 + applications/services/desktop/desktop.c | 6 +- applications/services/storage/storage_cli.c | 8 +- scripts/flipper/storage.py | 2 + targets/f18/api_symbols.csv | 4 +- targets/f7/api_symbols.csv | 18 +- 28 files changed, 507 insertions(+), 129 deletions(-) create mode 100644 applications/services/cli/commands/hello_world.c create mode 100644 applications/services/cli/commands/neofetch.c diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index f92d7e66f..05e834402 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -4,7 +4,7 @@ App( entry_point="unit_tests_on_system_start", sources=["unit_tests.c", "test_runner.c", "unit_test_api_table.cpp"], cdefines=["APP_UNIT_TESTS"], - requires=["system_settings", "subghz_start"], + requires=["system_settings", "cli_subghz"], provides=["delay_test"], resources="resources", order=100, diff --git a/applications/main/application.fam b/applications/main/application.fam index 4d3162337..9d8604206 100644 --- a/applications/main/application.fam +++ b/applications/main/application.fam @@ -22,11 +22,5 @@ App( apptype=FlipperAppType.METAPACKAGE, provides=[ "cli", - "ibutton_start", - "onewire_start", - "subghz_start", - "infrared_start", - "lfrfid_start", - "nfc_start", ], ) diff --git a/applications/main/ibutton/application.fam b/applications/main/ibutton/application.fam index 01c02ec23..84afe0f02 100644 --- a/applications/main/ibutton/application.fam +++ b/applications/main/ibutton/application.fam @@ -13,10 +13,10 @@ App( ) App( - appid="ibutton_start", - apptype=FlipperAppType.STARTUP, + appid="cli_ikey", targets=["f7"], - entry_point="ibutton_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_ikey_ep", + requires=["cli"], sources=["ibutton_cli.c"], - order=60, ) diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index e11ace1d0..2ff0860bb 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -216,8 +216,7 @@ void ibutton_cli_emulate(PipeSide* pipe, FuriString* args) { ibutton_protocols_free(protocols); } -void ibutton_cli(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -241,12 +240,4 @@ void ibutton_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -void ibutton_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(ibutton_cli); -#endif -} +CLI_COMMAND_INTERFACE(ikey, execute, CliCommandFlagDefault, 1024); diff --git a/applications/main/infrared/application.fam b/applications/main/infrared/application.fam index 575bebbe4..79b3fdbfa 100644 --- a/applications/main/infrared/application.fam +++ b/applications/main/infrared/application.fam @@ -15,14 +15,14 @@ App( ) App( - appid="infrared_start", - apptype=FlipperAppType.STARTUP, + appid="cli_ir", targets=["f7"], - entry_point="infrared_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_ir_ep", + requires=["cli"], sources=[ "infrared_cli.c", "infrared_brute_force.c", "infrared_signal.c", ], - order=20, ) diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index e62da5fd2..22d916d15 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -526,7 +526,7 @@ static void infrared_cli_process_universal(PipeSide* pipe, FuriString* args) { furi_string_free(arg2); } -static void infrared_cli_start_ir(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); if(furi_hal_infrared_is_busy()) { printf("INFRARED is busy. Exiting."); @@ -553,12 +553,5 @@ static void infrared_cli_start_ir(PipeSide* pipe, FuriString* args, void* contex furi_string_free(command); } -void infrared_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = (Cli*)furi_record_open(RECORD_CLI); - cli_add_command(cli, "ir", CliCommandFlagDefault, infrared_cli_start_ir, NULL); - furi_record_close(RECORD_CLI); -#else - UNUSED(infrared_cli_start_ir); -#endif -} + +CLI_COMMAND_INTERFACE(ir, execute, CliCommandFlagDefault, 2048); diff --git a/applications/main/lfrfid/application.fam b/applications/main/lfrfid/application.fam index c067d786f..d6fca74f4 100644 --- a/applications/main/lfrfid/application.fam +++ b/applications/main/lfrfid/application.fam @@ -13,10 +13,10 @@ App( ) App( - appid="lfrfid_start", + appid="cli_rfid", targets=["f7"], - apptype=FlipperAppType.STARTUP, - entry_point="lfrfid_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_rfid_ep", + requires=["cli"], sources=["lfrfid_cli.c"], - order=50, ) diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index fa74906c0..cefc55f65 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -536,7 +536,7 @@ static void lfrfid_cli_raw_emulate(PipeSide* pipe, FuriString* args) { furi_string_free(filepath); } -static void lfrfid_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -566,8 +566,4 @@ static void lfrfid_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -void lfrfid_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli, NULL); - furi_record_close(RECORD_CLI); -} +CLI_COMMAND_INTERFACE(rfid, execute, CliCommandFlagDefault, 2048); diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index 29bdf390a..f645033b2 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -258,10 +258,10 @@ App( ) App( - appid="nfc_start", + appid="cli_nfc", targets=["f7"], - apptype=FlipperAppType.STARTUP, - entry_point="nfc_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_nfc_ep", + requires=["cli"], sources=["nfc_cli.c"], - order=30, ) diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index 8a9b1fec4..fd5598fc6 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -42,7 +42,7 @@ static void nfc_cli_field(PipeSide* pipe, FuriString* args) { furi_hal_nfc_release(); } -static void nfc_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -65,12 +65,4 @@ static void nfc_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -void nfc_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "nfc", CliCommandFlagDefault, nfc_cli, NULL); - furi_record_close(RECORD_CLI); -#else - UNUSED(nfc_cli); -#endif -} +CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024); diff --git a/applications/main/onewire/application.fam b/applications/main/onewire/application.fam index 3d35abce9..e38bcdfef 100644 --- a/applications/main/onewire/application.fam +++ b/applications/main/onewire/application.fam @@ -1,6 +1,8 @@ App( - appid="onewire_start", - apptype=FlipperAppType.STARTUP, - entry_point="onewire_on_system_start", - order=60, + appid="cli_onewire", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_onewire_ep", + requires=["cli"], + sources=["onewire_cli.c"], ) diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 63e3d696f..83bbc6770 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -45,7 +46,7 @@ static void onewire_cli_search(PipeSide* pipe) { furi_record_close(RECORD_POWER); } -static void onewire_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -63,12 +64,4 @@ static void onewire_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -void onewire_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(onewire_cli); -#endif -} +CLI_COMMAND_INTERFACE(onewire, execute, CliCommandFlagDefault, 1024); diff --git a/applications/main/subghz/application.fam b/applications/main/subghz/application.fam index 1abcf7f54..fe7b07b1e 100644 --- a/applications/main/subghz/application.fam +++ b/applications/main/subghz/application.fam @@ -20,10 +20,10 @@ App( ) App( - appid="subghz_start", + appid="cli_subghz", targets=["f7"], - apptype=FlipperAppType.STARTUP, - entry_point="subghz_on_system_start", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_subghz_ep", + requires=["cli"], sources=["subghz_cli.c", "helpers/subghz_chat.c"], - order=40, ) diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index a07ea5a7e..3c29aeeaf 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -1116,7 +1116,7 @@ static void subghz_cli_command_chat(PipeSide* pipe, FuriString* args) { printf("\r\nExit chat\r\n"); } -static void subghz_cli_command(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { FuriString* cmd; cmd = furi_string_alloc(); @@ -1184,14 +1184,4 @@ static void subghz_cli_command(PipeSide* pipe, FuriString* args, void* context) furi_string_free(cmd); } -void subghz_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - - cli_add_command(cli, "subghz", CliCommandFlagDefault, subghz_cli_command, NULL); - - furi_record_close(RECORD_CLI); -#else - UNUSED(subghz_cli_command); -#endif -} +CLI_COMMAND_INTERFACE(subghz, execute, CliCommandFlagDefault, 2048); diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 9c00f442b..6e2e393e0 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -33,3 +33,19 @@ App( sdk_headers=["cli_vcp.h"], sources=["cli_vcp.c"], ) + +App( + appid="cli_hello_world", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_hello_world_ep", + requires=["cli"], + sources=["commands/hello_world.c"], +) + +App( + appid="cli_neofetch", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_neofetch_ep", + requires=["cli"], + sources=["commands/neofetch.c"], +) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index b51715660..2bfce3a63 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -14,7 +14,7 @@ struct Cli { Cli* cli_alloc(void) { Cli* cli = malloc(sizeof(Cli)); CliCommandTree_init(cli->commands); - cli->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + cli->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); return cli; } @@ -38,6 +38,9 @@ void cli_add_command_ex( furi_check(name); furi_check(callback); + // the shell always attaches the pipe to the stdio, thus both flags can't be used at once + if(flags & CliCommandFlagUseShellThread) furi_check(!(flags & CliCommandFlagDontAttachStdio)); + FuriString* name_str; name_str = furi_string_alloc_set(name); // command cannot contain spaces @@ -86,18 +89,75 @@ bool cli_get_command(Cli* cli, FuriString* command, CliCommand* result) { return !!data; } +void cli_remove_external_commands(Cli* cli) { + furi_check(cli); + furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); + + // FIXME FL-3977: memory leak somewhere within this function + + CliCommandTree_t internal_cmds; + CliCommandTree_init(internal_cmds); + for + M_EACH(item, cli->commands, CliCommandTree_t) { + if(!(item->value_ptr->flags & CliCommandFlagExternal)) + CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr); + } + CliCommandTree_move(cli->commands, internal_cmds); + + furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); +} + +void cli_enumerate_external_commands(Cli* cli) { + furi_check(cli); + furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); + FURI_LOG_D(TAG, "Enumerating external commands"); + + cli_remove_external_commands(cli); + + // iterate over files in plugin directory + Storage* storage = furi_record_open(RECORD_STORAGE); + File* plugin_dir = storage_file_alloc(storage); + + if(storage_dir_open(plugin_dir, CLI_COMMANDS_PATH)) { + char plugin_filename[64]; + FuriString* plugin_name = furi_string_alloc(); + + while(storage_dir_read(plugin_dir, NULL, plugin_filename, sizeof(plugin_filename))) { + FURI_LOG_T(TAG, "Plugin: %s", plugin_filename); + furi_string_set_str(plugin_name, plugin_filename); + furi_string_replace_all_str(plugin_name, ".fal", ""); + furi_string_replace_at(plugin_name, 0, 4, ""); // remove "cli_" in the beginning + CliCommand command = { + .context = NULL, + .execute_callback = NULL, + .flags = CliCommandFlagExternal, + }; + CliCommandTree_set_at(cli->commands, plugin_name, command); + } + + furi_string_free(plugin_name); + } + + storage_file_free(plugin_dir); + furi_record_close(RECORD_STORAGE); + + FURI_LOG_D(TAG, "Finished enumerating external commands"); + furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); +} + void cli_lock_commands(Cli* cli) { - furi_assert(cli); + furi_check(cli); furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); } void cli_unlock_commands(Cli* cli) { - furi_assert(cli); - furi_mutex_release(cli->mutex); + furi_check(cli); + furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); } CliCommandTree_t* cli_get_commands(Cli* cli) { - furi_assert(cli); + furi_check(cli); + furi_check(furi_mutex_get_owner(cli->mutex) == furi_thread_get_current_id()); return &cli->commands; } @@ -119,5 +179,6 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) { void cli_on_system_start(void) { Cli* cli = cli_alloc(); cli_commands_init(cli); + cli_enumerate_external_commands(cli); furi_record_create(RECORD_CLI, cli); } diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index 211e89d88..2352e1806 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -20,6 +20,13 @@ typedef enum { CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps */ CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ CliCommandFlagDontAttachStdio = (1 << 2), /**< Do no attach I/O pipe to thread stdio */ + CliCommandFlagUseShellThread = + (1 + << 3), /**< Don't start a separate thread to run the command in. Incompatible with DontAttachStdio */ + + // internal flags (do not set them yourselves!) + + CliCommandFlagExternal = (1 << 4), /**< The command comes from a .fal file */ } CliCommandFlag; /** Cli type anonymous structure */ @@ -87,6 +94,20 @@ void cli_add_command_ex( */ void cli_delete_command(Cli* cli, const char* name); +/** + * @brief Unregisters all external commands + * + * @param [in] cli pointer to the cli instance + */ +void cli_remove_external_commands(Cli* cli); + +/** + * @brief Reloads the list of externally available commands + * + * @param [in] cli pointer to cli instance + */ +void cli_enumerate_external_commands(Cli* cli); + /** * @brief Detects if Ctrl+C has been pressed or session has been terminated * diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index 24917afa9..723a4d556 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -91,6 +91,8 @@ void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { } } + printf(ANSI_RESET + "\r\nIf you just added a new command and can't see it above, run `reload_ext_cmds`"); printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli"); cli_unlock_commands(cli); @@ -512,6 +514,16 @@ void cli_command_i2c(PipeSide* pipe, FuriString* args, void* context) { furi_hal_i2c_release(&furi_hal_i2c_handle_external); } +void cli_command_reload_external(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + UNUSED(context); + Cli* cli = furi_record_open(RECORD_CLI); + cli_enumerate_external_commands(cli); + furi_record_close(RECORD_CLI); + printf("OK!"); +} + /** * Echoes any bytes it receives except ASCII ETX (0x03, Ctrl+C) */ @@ -537,6 +549,8 @@ void cli_commands_init(Cli* cli) { cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); + cli_add_command( + cli, "reload_ext_cmds", CliCommandFlagDefault, cli_command_reload_external, NULL); cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); diff --git a/applications/services/cli/cli_i.h b/applications/services/cli/cli_i.h index b990e9960..3e948c345 100644 --- a/applications/services/cli/cli_i.h +++ b/applications/services/cli/cli_i.h @@ -14,6 +14,7 @@ extern "C" { #endif #define CLI_BUILTIN_COMMAND_STACK_SIZE (3 * 1024U) +#define CLI_COMMANDS_PATH "/ext/apps_data/cli/plugins" typedef struct { void* context; // +#include +#include +#include +#include + +static void execute(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + UNUSED(context); + + static const char* const neofetch_logo[] = { + " _.-------.._ -,", + " .-\"```\"--..,,_/ /`-, -, \\ ", + " .:\" /:/ /'\\ \\ ,_..., `. | |", + " / ,----/:/ /`\\ _\\~`_-\"` _;", + " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ ", + " | | | 0 | | .-' ,/` /", + " | ,..\\ \\ ,.-\"` ,/` /", + "; : `/`\"\"\\` ,/--==,/-----,", + "| `-...| -.___-Z:_______J...---;", + ": ` _-'", + }; +#define NEOFETCH_COLOR ANSI_FLIPPER_BRAND_ORANGE + + // Determine logo parameters + size_t logo_height = COUNT_OF(neofetch_logo), logo_width = 0; + for(size_t i = 0; i < logo_height; i++) + logo_width = MAX(logo_width, strlen(neofetch_logo[i])); + logo_width += 4; // space between logo and info + + // Format hostname delimiter + const size_t size_of_hostname = 4 + strlen(furi_hal_version_get_name_ptr()); + char delimiter[64]; + memset(delimiter, '-', size_of_hostname); + delimiter[size_of_hostname] = '\0'; + + // Get heap info + size_t heap_total = memmgr_get_total_heap(); + size_t heap_used = heap_total - memmgr_get_free_heap(); + uint16_t heap_percent = (100 * heap_used) / heap_total; + + // Get storage info + Storage* storage = furi_record_open(RECORD_STORAGE); + uint64_t ext_total, ext_free, ext_used, ext_percent; + storage_common_fs_info(storage, "/ext", &ext_total, &ext_free); + ext_used = ext_total - ext_free; + ext_percent = (100 * ext_used) / ext_total; + ext_used /= 1024 * 1024; + ext_total /= 1024 * 1024; + furi_record_close(RECORD_STORAGE); + + // Get battery info + uint16_t charge_percent = furi_hal_power_get_pct(); + const char* charge_state; + if(furi_hal_power_is_charging()) { + if((charge_percent < 100) && (!furi_hal_power_is_charging_done())) { + charge_state = "charging"; + } else { + charge_state = "charged"; + } + } else { + charge_state = "discharging"; + } + + // Get misc info + uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency(); + const Version* version = version_get(); + uint16_t major, minor; + furi_hal_info_get_api_version(&major, &minor); + + // Print ASCII art with info + const size_t info_height = 16; + for(size_t i = 0; i < MAX(logo_height, info_height); i++) { + printf(NEOFETCH_COLOR "%-*s", logo_width, (i < logo_height) ? neofetch_logo[i] : ""); + switch(i) { + case 0: // you@ + printf("you" ANSI_RESET "@" NEOFETCH_COLOR "%s", furi_hal_version_get_name_ptr()); + break; + case 1: // delimiter + printf(ANSI_RESET "%s", delimiter); + break; + case 2: // OS: FURI (SDK .) + printf( + "OS" ANSI_RESET ": FURI %s %s %s %s (SDK %hu.%hu)", + version_get_version(version), + version_get_gitbranch(version), + version_get_version(version), + version_get_githash(version), + major, + minor); + break; + case 3: // Host: + printf( + "Host" ANSI_RESET ": %s %s", + furi_hal_version_get_model_code(), + furi_hal_version_get_device_name_ptr()); + break; + case 4: // Kernel: FreeRTOS .. + printf( + "Kernel" ANSI_RESET ": FreeRTOS %d.%d.%d", + tskKERNEL_VERSION_MAJOR, + tskKERNEL_VERSION_MINOR, + tskKERNEL_VERSION_BUILD); + break; + case 5: // Uptime: ?h?m?s + printf( + "Uptime" ANSI_RESET ": %luh%lum%lus", + uptime / 60 / 60, + uptime / 60 % 60, + uptime % 60); + break; + case 6: // ST7567 128x64 @ 1 bpp in 1.4" + printf("Display" ANSI_RESET ": ST7567 128x64 @ 1 bpp in 1.4\""); + break; + case 7: // DE: GuiSrv + printf("DE" ANSI_RESET ": GuiSrv"); + break; + case 8: // Shell: CliSrv + printf("Shell" ANSI_RESET ": CliShell"); + break; + case 9: // CPU: STM32WB55RG @ 64 MHz + printf("CPU" ANSI_RESET ": STM32WB55RG @ 64 MHz"); + break; + case 10: // Memory: / B (??%) + printf( + "Memory" ANSI_RESET ": %zu / %zu B (%hu%%)", heap_used, heap_total, heap_percent); + break; + case 11: // Disk (/ext): / MiB (??%) + printf( + "Disk (/ext)" ANSI_RESET ": %llu / %llu MiB (%llu%%)", + ext_used, + ext_total, + ext_percent); + break; + case 12: // Battery: ??% () + printf("Battery" ANSI_RESET ": %hu%% (%s)" ANSI_RESET, charge_percent, charge_state); + break; + case 13: // empty space + break; + case 14: // Colors (line 1) + for(size_t j = 30; j <= 37; j++) + printf("\e[%dm███", j); + break; + case 15: // Colors (line 2) + for(size_t j = 90; j <= 97; j++) + printf("\e[%dm███", j); + break; + default: + break; + } + printf("\r\n"); + } + printf(ANSI_RESET); +#undef NEOFETCH_COLOR +} + +CLI_COMMAND_INTERFACE(neofetch, execute, CliCommandFlagDefault, 2048); diff --git a/applications/services/cli/shell/cli_shell.c b/applications/services/cli/shell/cli_shell.c index 2e95c767b..22a5e7e78 100644 --- a/applications/services/cli/shell/cli_shell.c +++ b/applications/services/cli/shell/cli_shell.c @@ -12,6 +12,7 @@ #include #include #include +#include #define TAG "CliShell" @@ -29,6 +30,11 @@ CliShellKeyComboSet* component_key_combo_sets[] = { }; static_assert(CliShellComponentMAX == COUNT_OF(component_key_combo_sets)); +typedef enum { + CliShellStorageEventMount, + CliShellStorageEventUnmount, +} CliShellStorageEvent; + struct CliShell { Cli* cli; FuriEventLoop* event_loop; @@ -37,6 +43,10 @@ struct CliShell { CliAnsiParser* ansi_parser; FuriEventLoopTimer* ansi_parsing_timer; + Storage* storage; + FuriPubSubSubscription* storage_subscription; + FuriMessageQueue* storage_event_queue; + void* components[CliShellComponentMAX]; }; @@ -46,10 +56,39 @@ typedef struct { FuriString* args; } CliCommandThreadData; +static void cli_shell_data_available(PipeSide* pipe, void* context); +static void cli_shell_pipe_broken(PipeSide* pipe, void* context); + +static void cli_shell_install_pipe(CliShell* cli_shell) { + pipe_install_as_stdio(cli_shell->pipe); + pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop); + pipe_set_callback_context(cli_shell->pipe, cli_shell); + pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0); + pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0); +} + +static void cli_shell_detach_pipe(CliShell* cli_shell) { + pipe_detach_from_event_loop(cli_shell->pipe); + furi_thread_set_stdin_callback(NULL, NULL); + furi_thread_set_stdout_callback(NULL, NULL); +} + // ========= // Execution // ========= +static int32_t cli_command_thread(void* context) { + CliCommandThreadData* thread_data = context; + if(!(thread_data->command->flags & CliCommandFlagDontAttachStdio)) + pipe_install_as_stdio(thread_data->pipe); + + thread_data->command->execute_callback( + thread_data->pipe, thread_data->args, thread_data->command->context); + + fflush(stdout); + return 0; +} + void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // split command into command and args size_t space = furi_string_search_char(command, ' '); @@ -59,6 +98,7 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { FuriString* args = furi_string_alloc_set(command); furi_string_right(args, space + 1); + PluginManager* plugin_manager = NULL; Loader* loader = NULL; CliCommand command_data; @@ -71,6 +111,34 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { break; } + // load external command + if(command_data.flags & CliCommandFlagExternal) { + plugin_manager = + plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface); + FuriString* path = furi_string_alloc_printf( + "%s/cli_%s.fal", CLI_COMMANDS_PATH, furi_string_get_cstr(command_name)); + uint32_t plugin_cnt_last = plugin_manager_get_count(plugin_manager); + PluginManagerError error = + plugin_manager_load_single(plugin_manager, furi_string_get_cstr(path)); + furi_string_free(path); + + if(error != PluginManagerErrorNone) { + printf(ANSI_FG_RED "failed to load external command" ANSI_RESET); + break; + } + + const CliCommandDescriptor* plugin = + plugin_manager_get_ep(plugin_manager, plugin_cnt_last); + furi_assert(plugin); + furi_check(furi_string_cmp_str(command_name, plugin->name) == 0); + command_data.execute_callback = plugin->execute_callback; + command_data.flags = plugin->flags | CliCommandFlagExternal; + command_data.stack_depth = plugin->stack_depth; + + // external commands have to run in an external thread + furi_check(!(command_data.flags & CliCommandFlagUseShellThread)); + } + // lock loader if(!(command_data.flags & CliCommandFlagParallelSafe)) { loader = furi_record_open(RECORD_LOADER); @@ -82,7 +150,27 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { } } - command_data.execute_callback(cli_shell->pipe, args, command_data.context); + if(command_data.flags & CliCommandFlagUseShellThread) { + // run command in this thread + command_data.execute_callback(cli_shell->pipe, args, command_data.context); + } else { + // run command in separate thread + cli_shell_detach_pipe(cli_shell); + CliCommandThreadData thread_data = { + .command = &command_data, + .pipe = cli_shell->pipe, + .args = args, + }; + FuriThread* thread = furi_thread_alloc_ex( + furi_string_get_cstr(command_name), + command_data.stack_depth, + cli_command_thread, + &thread_data); + furi_thread_start(thread); + furi_thread_join(thread); + furi_thread_free(thread); + cli_shell_install_pipe(cli_shell); + } } while(0); furi_string_free(command_name); @@ -91,13 +179,51 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // unlock loader if(loader) loader_unlock(loader); furi_record_close(RECORD_LOADER); + + // unload external command + if(plugin_manager) plugin_manager_free(plugin_manager); } // ============== // Event handlers // ============== -static void cli_shell_process_key(CliShell* cli_shell, CliKeyCombo key_combo) { +static void cli_shell_storage_event(const void* message, void* context) { + CliShell* cli_shell = context; + const StorageEvent* event = message; + + if(event->type == StorageEventTypeCardMount) { + CliShellStorageEvent cli_event = CliShellStorageEventMount; + furi_check( + furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk); + } else if(event->type == StorageEventTypeCardUnmount) { + CliShellStorageEvent cli_event = CliShellStorageEventUnmount; + furi_check( + furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk); + } +} + +static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* context) { + CliShell* cli_shell = context; + FuriMessageQueue* queue = object; + CliShellStorageEvent event; + furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); + + if(event == CliShellStorageEventMount) { + cli_enumerate_external_commands(cli_shell->cli); + } else if(event == CliShellStorageEventUnmount) { + cli_remove_external_commands(cli_shell->cli); + } else { + furi_crash(); + } +} + +static void + cli_shell_process_parser_result(CliShell* cli_shell, CliAnsiParserResult parse_result) { + if(!parse_result.is_done) return; + CliKeyCombo key_combo = parse_result.result; + if(key_combo.key == CliKeyUnrecognized) return; + for(size_t i = 0; i < CliShellComponentMAX; i++) { // -V1008 CliShellKeyComboSet* set = component_key_combo_sets[i]; void* component_context = cli_shell->components[i]; @@ -130,22 +256,13 @@ static void cli_shell_data_available(PipeSide* pipe, void* context) { // process ANSI escape sequences int c = getchar(); furi_assert(c >= 0); - CliAnsiParserResult parse_result = cli_ansi_parser_feed(cli_shell->ansi_parser, c); - if(!parse_result.is_done) return; - CliKeyCombo key_combo = parse_result.result; - if(key_combo.key == CliKeyUnrecognized) return; - - cli_shell_process_key(cli_shell, key_combo); + cli_shell_process_parser_result(cli_shell, cli_ansi_parser_feed(cli_shell->ansi_parser, c)); } static void cli_shell_timer_expired(void* context) { CliShell* cli_shell = context; - CliAnsiParserResult parse_result = cli_ansi_parser_feed_timeout(cli_shell->ansi_parser); - if(!parse_result.is_done) return; - CliKeyCombo key_combo = parse_result.result; - if(key_combo.key == CliKeyUnrecognized) return; - - cli_shell_process_key(cli_shell, key_combo); + cli_shell_process_parser_result( + cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser)); } // ======= @@ -158,7 +275,6 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) { cli_shell->cli = furi_record_open(RECORD_CLI); cli_shell->ansi_parser = cli_ansi_parser_alloc(); cli_shell->pipe = pipe; - pipe_install_as_stdio(cli_shell->pipe); cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( @@ -167,20 +283,34 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) { cli_shell->event_loop = furi_event_loop_alloc(); cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell); - pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop); - pipe_set_callback_context(cli_shell->pipe, cli_shell); - pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0); - pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0); + cli_shell_install_pipe(cli_shell); + + cli_shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); + furi_event_loop_subscribe_message_queue( + cli_shell->event_loop, + cli_shell->storage_event_queue, + FuriEventLoopEventIn, + cli_shell_storage_internal_event, + cli_shell); + cli_shell->storage = furi_record_open(RECORD_STORAGE); + cli_shell->storage_subscription = furi_pubsub_subscribe( + storage_get_pubsub(cli_shell->storage), cli_shell_storage_event, cli_shell); return cli_shell; } static void cli_shell_free(CliShell* cli_shell) { + furi_pubsub_unsubscribe( + storage_get_pubsub(cli_shell->storage), cli_shell->storage_subscription); + furi_record_close(RECORD_STORAGE); + furi_event_loop_unsubscribe(cli_shell->event_loop, cli_shell->storage_event_queue); + furi_message_queue_free(cli_shell->storage_event_queue); + cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]); cli_shell_line_free(cli_shell->components[CliShellComponentLine]); - pipe_detach_from_event_loop(cli_shell->pipe); + cli_shell_detach_pipe(cli_shell); furi_event_loop_timer_free(cli_shell->ansi_parsing_timer); furi_event_loop_free(cli_shell->event_loop); pipe_free(cli_shell->pipe); diff --git a/applications/services/cli/shell/cli_shell_completions.c b/applications/services/cli/shell/cli_shell_completions.c index 6edb6eaf1..0b32c18a2 100644 --- a/applications/services/cli/shell/cli_shell_completions.c +++ b/applications/services/cli/shell/cli_shell_completions.c @@ -108,6 +108,7 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { furi_string_left(input, segment.length); if(segment.type == CliShellCompletionSegmentTypeCommand) { + cli_lock_commands(completions->cli); CliCommandTree_t* commands = cli_get_commands(completions->cli); for M_EACH(registered_command, *commands, CliCommandTree_t) { @@ -116,6 +117,7 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { CommandCompletions_push_back(completions->variants, command_name); } } + cli_unlock_commands(completions->cli); } else { // support removed, might reimplement in the future diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 185fb9c3b..0f6304823 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -398,7 +398,7 @@ void desktop_lock(Desktop* desktop) { if(desktop_pin_code_is_set()) { CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); cli_vcp_disable(cli_vcp); - furi_record_close(RECORD_CLI); + furi_record_close(RECORD_CLI_VCP); } desktop_auto_lock_inhibit(desktop); @@ -428,7 +428,7 @@ void desktop_unlock(Desktop* desktop) { if(desktop_pin_code_is_set()) { CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); cli_vcp_enable(cli_vcp); - furi_record_close(RECORD_CLI); + furi_record_close(RECORD_CLI_VCP); } DesktopStatus status = {.locked = false}; @@ -528,7 +528,7 @@ int32_t desktop_srv(void* p) { } else { CliVcp* cli_vcp = furi_record_open(RECORD_CLI_VCP); cli_vcp_enable(cli_vcp); - furi_record_close(RECORD_CLI); + furi_record_close(RECORD_CLI_VCP); } if(storage_file_exists(desktop->storage, SLIDESHOW_FS_PATH)) { diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 903aa1644..58b851926 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -696,7 +696,13 @@ static void storage_cli_factory_reset(PipeSide* pipe, FuriString* args, void* co void storage_on_system_start(void) { #ifdef SRV_CLI Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command_ex(cli, "storage", CliCommandFlagParallelSafe, storage_cli, NULL, 512); + cli_add_command_ex( + cli, + "storage", + CliCommandFlagParallelSafe | CliCommandFlagUseShellThread, + storage_cli, + NULL, + 512); cli_add_command( cli, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); furi_record_close(RECORD_CLI); diff --git a/scripts/flipper/storage.py b/scripts/flipper/storage.py index 40af5cebc..0182cf45f 100644 --- a/scripts/flipper/storage.py +++ b/scripts/flipper/storage.py @@ -109,6 +109,8 @@ class FlipperStorage: def start(self): self.port.open() + time.sleep(0.5) + self.read.until(self.CLI_PROMPT) self.port.reset_input_buffer() # Send a command with a known syntax to make sure the buffer is flushed self.send("device_info\r") diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index bfe7afbcd..72512a46f 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,84.0,, +Version,+,84.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -786,8 +786,10 @@ Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* Function,+,cli_delete_command,void,"Cli*, const char*" +Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" +Function,+,cli_remove_external_commands,void,Cli* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 1a8c46f10..73ad2dcd5 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,84.0,, +Version,+,84.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -863,8 +863,10 @@ Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* Function,+,cli_delete_command,void,"Cli*, const char*" +Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" +Function,+,cli_remove_external_commands,void,Cli* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" @@ -3454,13 +3456,13 @@ Function,+,subghz_file_encoder_worker_get_level_duration,LevelDuration,void* Function,+,subghz_file_encoder_worker_is_running,_Bool,SubGhzFileEncoderWorker* Function,+,subghz_file_encoder_worker_start,_Bool,"SubGhzFileEncoderWorker*, const char*, const char*" Function,+,subghz_file_encoder_worker_stop,void,SubGhzFileEncoderWorker* -Function,-,subghz_keystore_alloc,SubGhzKeystore*, -Function,-,subghz_keystore_free,void,SubGhzKeystore* -Function,-,subghz_keystore_get_data,SubGhzKeyArray_t*,SubGhzKeystore* -Function,-,subghz_keystore_load,_Bool,"SubGhzKeystore*, const char*" -Function,-,subghz_keystore_raw_encrypted_save,_Bool,"const char*, const char*, uint8_t*" -Function,-,subghz_keystore_raw_get_data,_Bool,"const char*, size_t, uint8_t*, size_t" -Function,-,subghz_keystore_save,_Bool,"SubGhzKeystore*, const char*, uint8_t*" +Function,+,subghz_keystore_alloc,SubGhzKeystore*, +Function,+,subghz_keystore_free,void,SubGhzKeystore* +Function,+,subghz_keystore_get_data,SubGhzKeyArray_t*,SubGhzKeystore* +Function,+,subghz_keystore_load,_Bool,"SubGhzKeystore*, const char*" +Function,+,subghz_keystore_raw_encrypted_save,_Bool,"const char*, const char*, uint8_t*" +Function,+,subghz_keystore_raw_get_data,_Bool,"const char*, size_t, uint8_t*, size_t" +Function,+,subghz_keystore_save,_Bool,"SubGhzKeystore*, const char*, uint8_t*" Function,+,subghz_protocol_blocks_add_bit,void,"SubGhzBlockDecoder*, uint8_t" Function,+,subghz_protocol_blocks_add_bytes,uint8_t,"const uint8_t[], size_t" Function,+,subghz_protocol_blocks_add_to_128_bit,void,"SubGhzBlockDecoder*, uint8_t, uint64_t*" From 09c61ecbdeab8d1b1ffc46f814fb4a26302347ed Mon Sep 17 00:00:00 2001 From: hedger Date: Thu, 3 Apr 2025 20:42:40 +0100 Subject: [PATCH 5/9] cli: fixed `free_blocks` command (#4174) * cli: fixed free_blocks command - regression after new heap implementation * github: updated codeowners --- .github/CODEOWNERS | 100 ++++++++++++++++++++-------------------- furi/core/memmgr_heap.c | 8 ++-- 2 files changed, 55 insertions(+), 53 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ef8b79370..675679080 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,69 +1,69 @@ # Who owns all the fish by default -* @skotopes @DrZlo13 @hedger @gsurkov +* @DrZlo13 @hedger @gsurkov # Apps -/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/debug/accessor/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/battery_test_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/debug/file_browser_test/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/lfrfid_debug/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/text_box_test/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/uart_echo/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/usb_mouse/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/debug/usb_test/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/bt_debug_app/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/debug/accessor/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/battery_test_app/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/debug/bt_debug_app/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/debug/file_browser_test/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/lfrfid_debug/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/text_box_test/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/uart_echo/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/usb_mouse/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/debug/usb_test/ @DrZlo13 @hedger @gsurkov @nminaylov -/applications/main/archive/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/main/bad_usb/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/main/gpio/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/main/ibutton/ @skotopes @DrZlo13 @hedger @gsurkov -/applications/main/infrared/ @skotopes @DrZlo13 @hedger @gsurkov -/applications/main/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/main/subghz/ @skotopes @DrZlo13 @hedger @gsurkov @Skorpionm -/applications/main/u2f/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov +/applications/main/archive/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/main/bad_usb/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/main/gpio/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/main/ibutton/ @DrZlo13 @hedger @gsurkov +/applications/main/infrared/ @DrZlo13 @hedger @gsurkov +/applications/main/nfc/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/main/subghz/ @DrZlo13 @hedger @gsurkov @Skorpionm +/applications/main/u2f/ @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/bt/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/services/cli/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/crypto/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/desktop/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/dolphin/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/power/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/services/rpc/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/bt/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/services/cli/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/crypto/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/desktop/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/dolphin/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/power/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/services/rpc/ @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/bt_settings_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/applications/services/desktop_settings/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/dolphin_passport/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/services/power_settings_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich +/applications/services/bt_settings_app/ @DrZlo13 @hedger @gsurkov @gornekich +/applications/services/desktop_settings/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/dolphin_passport/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/services/power_settings_app/ @DrZlo13 @hedger @gsurkov @gornekich -/applications/system/storage_move_to_sd/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/applications/system/js_app/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 +/applications/system/storage_move_to_sd/ @DrZlo13 @hedger @gsurkov @nminaylov +/applications/system/js_app/ @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 -/applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @gornekich @Skorpionm +/applications/debug/unit_tests/ @DrZlo13 @hedger @gsurkov @nminaylov @gornekich @Skorpionm -/applications/examples/example_thermo/ @skotopes @DrZlo13 @hedger @gsurkov +/applications/examples/example_thermo/ @DrZlo13 @hedger @gsurkov # Firmware targets -/targets/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov +/targets/ @DrZlo13 @hedger @gsurkov @nminaylov # Assets -/applications/main/infrared/resources/ @skotopes @DrZlo13 @hedger @gsurkov +/applications/main/infrared/resources/ @DrZlo13 @hedger @gsurkov # Documentation -/documentation/ @skotopes @DrZlo13 @hedger @gsurkov @portasynthinca3 -/scripts/toolchain/ @skotopes @DrZlo13 @hedger @gsurkov +/documentation/ @DrZlo13 @hedger @gsurkov @portasynthinca3 +/scripts/toolchain/ @DrZlo13 @hedger @gsurkov # Lib -/lib/stm32wb_copro/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/lib/digital_signal/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/lib/infrared/ @skotopes @DrZlo13 @hedger @gsurkov -/lib/lfrfid/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/lib/libusb_stm32/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/lib/mbedtls/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/lib/mjs/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 -/lib/nanopb/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov -/lib/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich -/lib/one_wire/ @skotopes @DrZlo13 @hedger @gsurkov -/lib/subghz/ @skotopes @DrZlo13 @hedger @gsurkov @Skorpionm +/lib/stm32wb_copro/ @DrZlo13 @hedger @gsurkov @gornekich +/lib/digital_signal/ @DrZlo13 @hedger @gsurkov @gornekich +/lib/infrared/ @DrZlo13 @hedger @gsurkov +/lib/lfrfid/ @DrZlo13 @hedger @gsurkov @nminaylov +/lib/libusb_stm32/ @DrZlo13 @hedger @gsurkov @nminaylov +/lib/mbedtls/ @DrZlo13 @hedger @gsurkov @nminaylov +/lib/mjs/ @DrZlo13 @hedger @gsurkov @nminaylov @portasynthinca3 +/lib/nanopb/ @DrZlo13 @hedger @gsurkov @nminaylov +/lib/nfc/ @DrZlo13 @hedger @gsurkov @gornekich +/lib/one_wire/ @DrZlo13 @hedger @gsurkov +/lib/subghz/ @DrZlo13 @hedger @gsurkov @Skorpionm # CI/CD -/.github/workflows/ @skotopes @DrZlo13 @hedger @gsurkov +/.github/workflows/ @DrZlo13 @hedger @gsurkov diff --git a/furi/core/memmgr_heap.c b/furi/core/memmgr_heap.c index c8a72bc8c..3ce0558a3 100644 --- a/furi/core/memmgr_heap.c +++ b/furi/core/memmgr_heap.c @@ -295,10 +295,12 @@ void memmgr_heap_printf_free_blocks(void) { //can be enabled once we can do printf with a locked scheduler //vTaskSuspendAll(); - pxBlock = xStart.pxNextFreeBlock; - while(pxBlock->pxNextFreeBlock != NULL) { + pxBlock = heapPROTECT_BLOCK_POINTER(xStart.pxNextFreeBlock); + heapVALIDATE_BLOCK_POINTER(pxBlock); + while(pxBlock->pxNextFreeBlock != heapPROTECT_BLOCK_POINTER(NULL)) { printf("A %p S %lu\r\n", (void*)pxBlock, (uint32_t)pxBlock->xBlockSize); - pxBlock = pxBlock->pxNextFreeBlock; + pxBlock = heapPROTECT_BLOCK_POINTER(pxBlock->pxNextFreeBlock); + heapVALIDATE_BLOCK_POINTER(pxBlock); } //xTaskResumeAll(); From 6f852e646c8ed0c4bed70274adc06974fa8b533d Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Fri, 4 Apr 2025 02:48:09 +0400 Subject: [PATCH 6/9] docs: badusb arbitrary modkey chains (#4176) --- .../file_formats/BadUsbScriptFormat.md | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/documentation/file_formats/BadUsbScriptFormat.md b/documentation/file_formats/BadUsbScriptFormat.md index 11977c9cb..a26f12489 100644 --- a/documentation/file_formats/BadUsbScriptFormat.md +++ b/documentation/file_formats/BadUsbScriptFormat.md @@ -57,19 +57,17 @@ Pause script execution by a defined time. ### Modifier keys -Can be combined with a special key command or a single character. -| Command | Notes | -| -------------- | ---------- | -| CONTROL / CTRL | | -| SHIFT | | -| ALT | | -| WINDOWS / GUI | | -| CTRL-ALT | CTRL+ALT | -| CTRL-SHIFT | CTRL+SHIFT | -| ALT-SHIFT | ALT+SHIFT | -| ALT-GUI | ALT+WIN | -| GUI-SHIFT | WIN+SHIFT | -| GUI-CTRL | WIN+CTRL | +The following modifier keys are recognized: +| Command | Notes | +| ------- | ------------ | +| CTRL | | +| CONTROL | Same as CTRL | +| SHIFT | | +| ALT | | +| GUI | | +| WINDOWS | Same as GUI | + +You can chain multiple modifier keys together using hyphens (`-`) or spaces. ## Key hold and release From 7192c9e68b47e1a52444ba056a55b5ca32c221c6 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Sat, 5 Apr 2025 02:58:58 +0400 Subject: [PATCH 7/9] [FL-3965] Separate cli_shell into toolbox (#4175) * cli_shell: separate into toolbox * fix: cmd flags * fix formatting * cli: increase default stack depth * cli_shell: fix loader lock logic * cli: fix command flags * fix f18 * speaker_debug: fix * cli_registry: fix docs * ufbt: rename cli target back * cli: rename app and record * cli: fix and simplify help command * cli_master_shell: fix ext commands * fix formatting * cli: rename master to main * fix formatting --------- Co-authored-by: hedger --- .../debug/speaker_debug/speaker_debug.c | 12 +- applications/debug/unit_tests/test_runner.c | 2 +- .../debug/unit_tests/tests/rpc/rpc_test.c | 1 - applications/debug/unit_tests/unit_tests.c | 9 +- applications/main/gpio/usb_uart_bridge.c | 1 - applications/main/ibutton/ibutton_cli.c | 4 +- applications/main/infrared/infrared_cli.c | 4 +- applications/main/lfrfid/lfrfid_cli.c | 4 +- applications/main/lfrfid/lfrfid_i.h | 1 - applications/main/nfc/nfc_app_i.h | 1 - applications/main/nfc/nfc_cli.c | 5 +- applications/main/onewire/onewire_cli.c | 6 +- .../main/subghz/helpers/subghz_chat.h | 1 - applications/main/subghz/subghz_cli.c | 5 +- applications/main/subghz/subghz_cli.h | 2 - applications/services/bt/bt_cli.c | 7 +- applications/services/cli/application.fam | 20 +- applications/services/cli/cli.c | 184 ----------- applications/services/cli/cli.h | 131 -------- applications/services/cli/cli_command_gpio.c | 1 + applications/services/cli/cli_command_gpio.h | 1 - applications/services/cli/cli_commands.h | 34 -- .../{cli_commands.c => cli_main_commands.c} | 105 ++---- applications/services/cli/cli_main_commands.h | 9 + applications/services/cli/cli_main_shell.c | 46 +++ applications/services/cli/cli_main_shell.h | 7 + applications/services/cli/cli_vcp.c | 20 +- .../services/cli/commands/hello_world.c | 4 +- applications/services/cli/commands/neofetch.c | 5 +- .../services/cli/commands/subshell_demo.c | 43 +++ applications/services/cli/shell/cli_shell.h | 16 - applications/services/crypto/crypto_cli.c | 8 +- applications/services/desktop/desktop.c | 1 - applications/services/input/input.c | 9 +- applications/services/input/input_cli.c | 2 +- applications/services/loader/loader_cli.c | 8 +- applications/services/power/power_cli.c | 9 +- applications/services/rpc/rpc.c | 14 +- applications/services/rpc/rpc_cli.c | 3 +- applications/services/rpc/rpc_i.h | 1 - applications/services/storage/storage_cli.c | 17 +- applications/system/js_app/js_app.c | 7 +- applications/system/updater/cli/updater_cli.c | 7 +- lib/toolbox/SConscript | 4 + .../services => lib/toolbox}/cli/cli_ansi.c | 0 .../services => lib/toolbox}/cli/cli_ansi.h | 2 +- lib/toolbox/cli/cli_command.c | 17 + lib/toolbox/cli/cli_command.h | 103 ++++++ lib/toolbox/cli/cli_registry.c | 178 +++++++++++ lib/toolbox/cli/cli_registry.h | 92 ++++++ .../toolbox/cli/cli_registry_i.h | 26 +- .../toolbox}/cli/shell/cli_shell.c | 301 ++++++++++++------ lib/toolbox/cli/shell/cli_shell.h | 75 +++++ .../cli/shell/cli_shell_completions.c | 14 +- .../cli/shell/cli_shell_completions.h | 7 +- .../toolbox}/cli/shell/cli_shell_i.h | 2 + .../toolbox}/cli/shell/cli_shell_line.c | 27 +- .../toolbox}/cli/shell/cli_shell_line.h | 2 + targets/f18/api_symbols.csv | 25 +- targets/f7/api_symbols.csv | 25 +- 60 files changed, 994 insertions(+), 683 deletions(-) delete mode 100644 applications/services/cli/cli.c delete mode 100644 applications/services/cli/cli.h delete mode 100644 applications/services/cli/cli_commands.h rename applications/services/cli/{cli_commands.c => cli_main_commands.c} (82%) create mode 100644 applications/services/cli/cli_main_commands.h create mode 100644 applications/services/cli/cli_main_shell.c create mode 100644 applications/services/cli/cli_main_shell.h create mode 100644 applications/services/cli/commands/subshell_demo.c delete mode 100644 applications/services/cli/shell/cli_shell.h rename {applications/services => lib/toolbox}/cli/cli_ansi.c (100%) rename {applications/services => lib/toolbox}/cli/cli_ansi.h (99%) create mode 100644 lib/toolbox/cli/cli_command.c create mode 100644 lib/toolbox/cli/cli_command.h create mode 100644 lib/toolbox/cli/cli_registry.c create mode 100644 lib/toolbox/cli/cli_registry.h rename applications/services/cli/cli_i.h => lib/toolbox/cli/cli_registry_i.h (51%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell.c (57%) create mode 100644 lib/toolbox/cli/shell/cli_shell.h rename {applications/services => lib/toolbox}/cli/shell/cli_shell_completions.c (97%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell_completions.h (67%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell_i.h (91%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell_line.c (95%) rename {applications/services => lib/toolbox}/cli/shell/cli_shell_line.h (94%) diff --git a/applications/debug/speaker_debug/speaker_debug.c b/applications/debug/speaker_debug/speaker_debug.c index d2a40a2a4..6a9956b07 100644 --- a/applications/debug/speaker_debug/speaker_debug.c +++ b/applications/debug/speaker_debug/speaker_debug.c @@ -1,9 +1,10 @@ #include #include #include -#include #include #include +#include +#include #define TAG "SpeakerDebug" @@ -20,14 +21,14 @@ typedef struct { typedef struct { MusicWorker* music_worker; FuriMessageQueue* message_queue; - Cli* cli; + CliRegistry* cli_registry; } SpeakerDebugApp; static SpeakerDebugApp* speaker_app_alloc(void) { SpeakerDebugApp* app = (SpeakerDebugApp*)malloc(sizeof(SpeakerDebugApp)); app->music_worker = music_worker_alloc(); app->message_queue = furi_message_queue_alloc(8, sizeof(SpeakerDebugAppMessage)); - app->cli = furi_record_open(RECORD_CLI); + app->cli_registry = furi_record_open(RECORD_CLI); return app; } @@ -96,7 +97,8 @@ static void speaker_app_run(SpeakerDebugApp* app, const char* arg) { return; } - cli_add_command(app->cli, CLI_COMMAND, CliCommandFlagParallelSafe, speaker_app_cli, app); + cli_registry_add_command( + app->cli_registry, CLI_COMMAND, CliCommandFlagParallelSafe, speaker_app_cli, app); SpeakerDebugAppMessage message; FuriStatus status; @@ -111,7 +113,7 @@ static void speaker_app_run(SpeakerDebugApp* app, const char* arg) { } } - cli_delete_command(app->cli, CLI_COMMAND); + cli_registry_delete_command(app->cli_registry, CLI_COMMAND); } int32_t speaker_debug_app(void* arg) { diff --git a/applications/debug/unit_tests/test_runner.c b/applications/debug/unit_tests/test_runner.c index 4de051125..ad7d31b02 100644 --- a/applications/debug/unit_tests/test_runner.c +++ b/applications/debug/unit_tests/test_runner.c @@ -2,7 +2,7 @@ #include "tests/test_api.h" -#include +#include #include #include #include diff --git a/applications/debug/unit_tests/tests/rpc/rpc_test.c b/applications/debug/unit_tests/tests/rpc/rpc_test.c index 5d26bdb30..82ab872ce 100644 --- a/applications/debug/unit_tests/tests/rpc/rpc_test.c +++ b/applications/debug/unit_tests/tests/rpc/rpc_test.c @@ -3,7 +3,6 @@ #include #include -#include #include #include #include diff --git a/applications/debug/unit_tests/unit_tests.c b/applications/debug/unit_tests/unit_tests.c index db78d8ced..8605cb781 100644 --- a/applications/debug/unit_tests/unit_tests.c +++ b/applications/debug/unit_tests/unit_tests.c @@ -1,6 +1,8 @@ #include -#include #include +#include +#include +#include #include "test_runner.h" @@ -14,8 +16,9 @@ void unit_tests_cli(PipeSide* pipe, FuriString* args, void* context) { void unit_tests_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "unit_tests", CliCommandFlagParallelSafe, unit_tests_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, "unit_tests", CliCommandFlagParallelSafe, unit_tests_cli, NULL); furi_record_close(RECORD_CLI); #endif } diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index 15170f0d0..77cd02f63 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -1,7 +1,6 @@ #include "usb_uart_bridge.h" #include "usb_cdc.h" #include -#include #include #include #include diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index 2ff0860bb..0b9a59586 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -1,7 +1,7 @@ #include #include -#include +#include #include #include @@ -240,4 +240,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(ikey, execute, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(ikey, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index 22d916d15..eb13bcd79 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -554,4 +554,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(command); } -CLI_COMMAND_INTERFACE(ir, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(ir, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index cefc55f65..63ca046b9 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include #include #include @@ -566,4 +566,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(rfid, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(rfid, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/main/lfrfid/lfrfid_i.h b/applications/main/lfrfid/lfrfid_i.h index 2fcedcd7f..29a5eb902 100644 --- a/applications/main/lfrfid/lfrfid_i.h +++ b/applications/main/lfrfid/lfrfid_i.h @@ -8,7 +8,6 @@ #include #include #include -#include #include #include diff --git a/applications/main/nfc/nfc_app_i.h b/applications/main/nfc/nfc_app_i.h index 14e484622..1101b7bb2 100644 --- a/applications/main/nfc/nfc_app_i.h +++ b/applications/main/nfc/nfc_app_i.h @@ -10,7 +10,6 @@ #include #include #include -#include #include #include diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index fd5598fc6..af3fd62eb 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -1,7 +1,6 @@ #include #include -#include -#include +#include #include #include #include @@ -65,4 +64,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 83bbc6770..193de76e4 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -1,9 +1,9 @@ #include #include -#include +#include #include -#include +#include #include #include @@ -64,4 +64,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(onewire, execute, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(onewire, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/subghz/helpers/subghz_chat.h b/applications/main/subghz/helpers/subghz_chat.h index 0d1497506..25fce0ecf 100644 --- a/applications/main/subghz/helpers/subghz_chat.h +++ b/applications/main/subghz/helpers/subghz_chat.h @@ -1,7 +1,6 @@ #pragma once #include "../subghz_i.h" #include -#include #include typedef struct SubGhzChatWorker SubGhzChatWorker; diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 3c29aeeaf..674738851 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -4,7 +4,8 @@ #include #include -#include +#include +#include #include #include @@ -1184,4 +1185,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(subghz, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(subghz, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/main/subghz/subghz_cli.h b/applications/main/subghz/subghz_cli.h index f6388218f..275135581 100644 --- a/applications/main/subghz/subghz_cli.h +++ b/applications/main/subghz/subghz_cli.h @@ -1,5 +1,3 @@ #pragma once -#include - void subghz_on_system_start(void); diff --git a/applications/services/bt/bt_cli.c b/applications/services/bt/bt_cli.c index 9ef3ef8a2..d81bc0505 100644 --- a/applications/services/bt/bt_cli.c +++ b/applications/services/bt/bt_cli.c @@ -1,8 +1,9 @@ #include #include -#include #include #include +#include +#include #include #include "bt_settings.h" @@ -230,8 +231,8 @@ static void bt_cli(PipeSide* pipe, FuriString* args, void* context) { void bt_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_BT, CliCommandFlagDefault, bt_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "bt", CliCommandFlagDefault, bt_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(bt_cli); diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 6e2e393e0..e1e79f43d 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -4,17 +4,9 @@ App( entry_point="cli_on_system_start", cdefines=["SRV_CLI"], sources=[ - "cli.c", - "shell/cli_shell.c", - "shell/cli_shell_completions.c", - "shell/cli_shell_line.c", - "cli_commands.c", "cli_command_gpio.c", - "cli_ansi.c", - ], - sdk_headers=[ - "cli.h", - "cli_ansi.h", + "cli_main_commands.c", + "cli_main_shell.c", ], # This STARTUP has to be processed before those that depend on the "cli" record. # "cli" used to be a SERVICE, but it's been converted into a STARTUP in order to @@ -49,3 +41,11 @@ App( requires=["cli"], sources=["commands/neofetch.c"], ) + +App( + appid="cli_subshell_demo", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_subshell_demo_ep", + requires=["cli"], + sources=["commands/subshell_demo.c"], +) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c deleted file mode 100644 index 2bfce3a63..000000000 --- a/applications/services/cli/cli.c +++ /dev/null @@ -1,184 +0,0 @@ -#include "cli.h" -#include "cli_i.h" -#include "cli_commands.h" -#include "cli_ansi.h" -#include - -#define TAG "cli" - -struct Cli { - CliCommandTree_t commands; - FuriMutex* mutex; -}; - -Cli* cli_alloc(void) { - Cli* cli = malloc(sizeof(Cli)); - CliCommandTree_init(cli->commands); - cli->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); - return cli; -} - -void cli_add_command( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context) { - cli_add_command_ex(cli, name, flags, callback, context, CLI_BUILTIN_COMMAND_STACK_SIZE); -} - -void cli_add_command_ex( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context, - size_t stack_size) { - furi_check(cli); - furi_check(name); - furi_check(callback); - - // the shell always attaches the pipe to the stdio, thus both flags can't be used at once - if(flags & CliCommandFlagUseShellThread) furi_check(!(flags & CliCommandFlagDontAttachStdio)); - - FuriString* name_str; - name_str = furi_string_alloc_set(name); - // command cannot contain spaces - furi_check(furi_string_search_char(name_str, ' ') == FURI_STRING_FAILURE); - - CliCommand command = { - .context = context, - .execute_callback = callback, - .flags = flags, - .stack_depth = stack_size, - }; - - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_set_at(cli->commands, name_str, command); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - - furi_string_free(name_str); -} - -void cli_delete_command(Cli* cli, const char* name) { - furi_check(cli); - FuriString* name_str; - name_str = furi_string_alloc_set(name); - furi_string_trim(name_str); - - size_t name_replace; - do { - name_replace = furi_string_replace(name_str, " ", "_"); - } while(name_replace != FURI_STRING_FAILURE); - - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_erase(cli->commands, name_str); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - - furi_string_free(name_str); -} - -bool cli_get_command(Cli* cli, FuriString* command, CliCommand* result) { - furi_assert(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommand* data = CliCommandTree_get(cli->commands, command); - if(data) *result = *data; - - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - - return !!data; -} - -void cli_remove_external_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - - // FIXME FL-3977: memory leak somewhere within this function - - CliCommandTree_t internal_cmds; - CliCommandTree_init(internal_cmds); - for - M_EACH(item, cli->commands, CliCommandTree_t) { - if(!(item->value_ptr->flags & CliCommandFlagExternal)) - CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr); - } - CliCommandTree_move(cli->commands, internal_cmds); - - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); -} - -void cli_enumerate_external_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - FURI_LOG_D(TAG, "Enumerating external commands"); - - cli_remove_external_commands(cli); - - // iterate over files in plugin directory - Storage* storage = furi_record_open(RECORD_STORAGE); - File* plugin_dir = storage_file_alloc(storage); - - if(storage_dir_open(plugin_dir, CLI_COMMANDS_PATH)) { - char plugin_filename[64]; - FuriString* plugin_name = furi_string_alloc(); - - while(storage_dir_read(plugin_dir, NULL, plugin_filename, sizeof(plugin_filename))) { - FURI_LOG_T(TAG, "Plugin: %s", plugin_filename); - furi_string_set_str(plugin_name, plugin_filename); - furi_string_replace_all_str(plugin_name, ".fal", ""); - furi_string_replace_at(plugin_name, 0, 4, ""); // remove "cli_" in the beginning - CliCommand command = { - .context = NULL, - .execute_callback = NULL, - .flags = CliCommandFlagExternal, - }; - CliCommandTree_set_at(cli->commands, plugin_name, command); - } - - furi_string_free(plugin_name); - } - - storage_file_free(plugin_dir); - furi_record_close(RECORD_STORAGE); - - FURI_LOG_D(TAG, "Finished enumerating external commands"); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); -} - -void cli_lock_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); -} - -void cli_unlock_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); -} - -CliCommandTree_t* cli_get_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_get_owner(cli->mutex) == furi_thread_get_current_id()); - return &cli->commands; -} - -bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side) { - if(pipe_state(side) == PipeStateBroken) return true; - if(!pipe_bytes_available(side)) return false; - char c = getchar(); - return c == CliKeyETX; -} - -void cli_print_usage(const char* cmd, const char* usage, const char* arg) { - furi_check(cmd); - furi_check(arg); - furi_check(usage); - - printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage); -} - -void cli_on_system_start(void) { - Cli* cli = cli_alloc(); - cli_commands_init(cli); - cli_enumerate_external_commands(cli); - furi_record_create(RECORD_CLI, cli); -} diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h deleted file mode 100644 index 2352e1806..000000000 --- a/applications/services/cli/cli.h +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @file cli.h - * API for registering commands with the CLI - */ - -#pragma once -#include -#include -#include "cli_ansi.h" -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define RECORD_CLI "cli" - -typedef enum { - CliCommandFlagDefault = 0, /**< Default */ - CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps */ - CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ - CliCommandFlagDontAttachStdio = (1 << 2), /**< Do no attach I/O pipe to thread stdio */ - CliCommandFlagUseShellThread = - (1 - << 3), /**< Don't start a separate thread to run the command in. Incompatible with DontAttachStdio */ - - // internal flags (do not set them yourselves!) - - CliCommandFlagExternal = (1 << 4), /**< The command comes from a .fal file */ -} CliCommandFlag; - -/** Cli type anonymous structure */ -typedef struct Cli Cli; - -/** - * @brief CLI execution callback pointer. Implement this interface and use - * `add_cli_command`. - * - * This callback will be called from a separate thread spawned just for your - * command. The pipe will be installed as the thread's stdio, so you can use - * `printf`, `getchar` and other standard functions to communicate with the - * user. - * - * @param [in] pipe Pipe that can be used to send and receive data. If - * `CliCommandFlagDontAttachStdio` was not set, you can - * also use standard C functions (printf, getc, etc.) to - * access this pipe. - * @param [in] args String with what was passed after the command - * @param [in] context Whatever you provided to `cli_add_command` - */ -typedef void (*CliExecuteCallback)(PipeSide* pipe, FuriString* args, void* context); - -/** - * @brief Registers a command with the CLI. Provides less options than the `_ex` - * counterpart. - * - * @param [in] cli Pointer to CLI instance - * @param [in] name Command name - * @param [in] flags CliCommandFlag - * @param [in] callback Callback function - * @param [in] context Custom context - */ -void cli_add_command( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context); - -/** - * @brief Registers a command with the CLI. Provides more options than the - * non-`_ex` counterpart. - * - * @param [in] cli Pointer to CLI instance - * @param [in] name Command name - * @param [in] flags CliCommandFlag - * @param [in] callback Callback function - * @param [in] context Custom context - * @param [in] stack_size Thread stack size - */ -void cli_add_command_ex( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context, - size_t stack_size); - -/** - * @brief Deletes a cli command - * - * @param [in] cli pointer to cli instance - * @param [in] name command name - */ -void cli_delete_command(Cli* cli, const char* name); - -/** - * @brief Unregisters all external commands - * - * @param [in] cli pointer to the cli instance - */ -void cli_remove_external_commands(Cli* cli); - -/** - * @brief Reloads the list of externally available commands - * - * @param [in] cli pointer to cli instance - */ -void cli_enumerate_external_commands(Cli* cli); - -/** - * @brief Detects if Ctrl+C has been pressed or session has been terminated - * - * @param [in] side Pointer to pipe side given to the command thread - * @warning This function also assumes that the pipe is installed as the - * thread's stdio - * @warning This function will consume 1 byte from the pipe - */ -bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side); - -/** Print unified cmd usage tip - * - * @param cmd cmd name - * @param usage usage tip - * @param arg arg passed by user - */ -void cli_print_usage(const char* cmd, const char* usage, const char* arg); - -#ifdef __cplusplus -} -#endif diff --git a/applications/services/cli/cli_command_gpio.c b/applications/services/cli/cli_command_gpio.c index f37e6387f..f6337265d 100644 --- a/applications/services/cli/cli_command_gpio.c +++ b/applications/services/cli/cli_command_gpio.c @@ -4,6 +4,7 @@ #include #include #include +#include void cli_command_gpio_print_usage(void) { printf("Usage:\r\n"); diff --git a/applications/services/cli/cli_command_gpio.h b/applications/services/cli/cli_command_gpio.h index 0290949e0..c1911fb65 100644 --- a/applications/services/cli/cli_command_gpio.h +++ b/applications/services/cli/cli_command_gpio.h @@ -1,6 +1,5 @@ #pragma once -#include "cli_i.h" #include void cli_command_gpio(PipeSide* pipe, FuriString* args, void* context); diff --git a/applications/services/cli/cli_commands.h b/applications/services/cli/cli_commands.h deleted file mode 100644 index 77d9930af..000000000 --- a/applications/services/cli/cli_commands.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "cli.h" -#include - -void cli_commands_init(Cli* cli); - -#define PLUGIN_APP_ID "cli" -#define PLUGIN_API_VERSION 1 - -typedef struct { - char* name; - CliExecuteCallback execute_callback; - CliCommandFlag flags; - size_t stack_depth; -} CliCommandDescriptor; - -#define CLI_COMMAND_INTERFACE(name, execute_callback, flags, stack_depth) \ - static const CliCommandDescriptor cli_##name##_desc = { \ - #name, \ - &execute_callback, \ - flags, \ - stack_depth, \ - }; \ - \ - static const FlipperAppPluginDescriptor plugin_descriptor = { \ - .appid = PLUGIN_APP_ID, \ - .ep_api_version = PLUGIN_API_VERSION, \ - .entry_point = &cli_##name##_desc, \ - }; \ - \ - const FlipperAppPluginDescriptor* cli_##name##_ep(void) { \ - return &plugin_descriptor; \ - } diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_main_commands.c similarity index 82% rename from applications/services/cli/cli_commands.c rename to applications/services/cli/cli_main_commands.c index 723a4d556..f84c03d8a 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_main_commands.c @@ -1,7 +1,6 @@ -#include "cli_commands.h" +#include "cli_main_commands.h" #include "cli_command_gpio.h" -#include "cli_ansi.h" -#include "cli.h" +#include #include #include @@ -56,49 +55,6 @@ void cli_command_info(PipeSide* pipe, FuriString* args, void* context) { } } -void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); - UNUSED(args); - UNUSED(context); - printf("Available commands:" ANSI_FG_GREEN); - - // count non-hidden commands - Cli* cli = furi_record_open(RECORD_CLI); - cli_lock_commands(cli); - CliCommandTree_t* commands = cli_get_commands(cli); - size_t commands_count = CliCommandTree_size(*commands); - - // create iterators starting at different positions - const size_t columns = 3; - const size_t commands_per_column = (commands_count / columns) + (commands_count % columns); - CliCommandTree_it_t iterators[columns]; - for(size_t c = 0; c < columns; c++) { - CliCommandTree_it(iterators[c], *commands); - for(size_t i = 0; i < c * commands_per_column; i++) - CliCommandTree_next(iterators[c]); - } - - // print commands - for(size_t r = 0; r < commands_per_column; r++) { - printf("\r\n"); - - for(size_t c = 0; c < columns; c++) { - if(!CliCommandTree_end_p(iterators[c])) { - const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]); - printf("%-30s", furi_string_get_cstr(*item->key_ptr)); - CliCommandTree_next(iterators[c]); - } - } - } - - printf(ANSI_RESET - "\r\nIf you just added a new command and can't see it above, run `reload_ext_cmds`"); - printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli"); - - cli_unlock_commands(cli); - furi_record_close(RECORD_CLI); -} - void cli_command_uptime(PipeSide* pipe, FuriString* args, void* context) { UNUSED(pipe); UNUSED(args); @@ -514,16 +470,6 @@ void cli_command_i2c(PipeSide* pipe, FuriString* args, void* context) { furi_hal_i2c_release(&furi_hal_i2c_handle_external); } -void cli_command_reload_external(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); - UNUSED(args); - UNUSED(context); - Cli* cli = furi_record_open(RECORD_CLI); - cli_enumerate_external_commands(cli); - furi_record_close(RECORD_CLI); - printf("OK!"); -} - /** * Echoes any bytes it receives except ASCII ETX (0x03, Ctrl+C) */ @@ -545,27 +491,32 @@ void cli_command_echo(PipeSide* pipe, FuriString* args, void* context) { } } -void cli_commands_init(Cli* cli) { - cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); - cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command( - cli, "reload_ext_cmds", CliCommandFlagDefault, cli_command_reload_external, NULL); +void cli_main_commands_init(CliRegistry* registry) { + cli_registry_add_command( + registry, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); + cli_registry_add_command(registry, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); + cli_registry_add_command( + registry, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); - cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); + cli_registry_add_command( + registry, "uptime", CliCommandFlagParallelSafe, cli_command_uptime, NULL); + cli_registry_add_command(registry, "date", CliCommandFlagParallelSafe, cli_command_date, NULL); + cli_registry_add_command(registry, "log", CliCommandFlagParallelSafe, cli_command_log, NULL); + cli_registry_add_command(registry, "sysctl", CliCommandFlagDefault, cli_command_sysctl, NULL); + cli_registry_add_command(registry, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); + cli_registry_add_command(registry, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); + cli_registry_add_command( + registry, "free_blocks", CliCommandFlagDefault, cli_command_free_blocks, NULL); + cli_registry_add_command(registry, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); - cli_add_command(cli, "uptime", CliCommandFlagDefault, cli_command_uptime, NULL); - cli_add_command(cli, "date", CliCommandFlagParallelSafe, cli_command_date, NULL); - cli_add_command(cli, "log", CliCommandFlagParallelSafe, cli_command_log, NULL); - cli_add_command(cli, "sysctl", CliCommandFlagDefault, cli_command_sysctl, NULL); - cli_add_command(cli, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); - cli_add_command(cli, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); - cli_add_command(cli, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL); - cli_add_command(cli, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); - - cli_add_command(cli, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); - cli_add_command(cli, "led", CliCommandFlagDefault, cli_command_led, NULL); - cli_add_command(cli, "gpio", CliCommandFlagDefault, cli_command_gpio, NULL); - cli_add_command(cli, "i2c", CliCommandFlagDefault, cli_command_i2c, NULL); + cli_registry_add_command(registry, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); + cli_registry_add_command(registry, "led", CliCommandFlagDefault, cli_command_led, NULL); + cli_registry_add_command(registry, "gpio", CliCommandFlagDefault, cli_command_gpio, NULL); + cli_registry_add_command(registry, "i2c", CliCommandFlagDefault, cli_command_i2c, NULL); +} + +void cli_on_system_start(void) { + CliRegistry* registry = cli_registry_alloc(); + cli_main_commands_init(registry); + furi_record_create(RECORD_CLI, registry); } diff --git a/applications/services/cli/cli_main_commands.h b/applications/services/cli/cli_main_commands.h new file mode 100644 index 000000000..d8058f467 --- /dev/null +++ b/applications/services/cli/cli_main_commands.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +#define RECORD_CLI "cli" +#define CLI_APPID "cli" + +void cli_main_commands_init(CliRegistry* registry); diff --git a/applications/services/cli/cli_main_shell.c b/applications/services/cli/cli_main_shell.c new file mode 100644 index 000000000..7550bef04 --- /dev/null +++ b/applications/services/cli/cli_main_shell.c @@ -0,0 +1,46 @@ +#include "cli_main_shell.h" +#include "cli_main_commands.h" +#include +#include +#include + +void cli_main_motd(void* context) { + UNUSED(context); + printf(ANSI_FLIPPER_BRAND_ORANGE + "\r\n" + " _.-------.._ -,\r\n" + " .-\"```\"--..,,_/ /`-, -, \\ \r\n" + " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" + " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" + " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" + " | | | 0 | | .-' ,/` /\r\n" + " | ,..\\ \\ ,.-\"` ,/` /\r\n" + " ; : `/`\"\"\\` ,/--==,/-----,\r\n" + " | `-...| -.___-Z:_______J...---;\r\n" + " : ` _-'\r\n" + " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" + "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" + "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" + "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" + "\r\n" ANSI_FG_BR_WHITE "Welcome to Flipper Zero Command Line Interface!\r\n" + "Read the manual: https://docs.flipper.net/development/cli\r\n" + "Run `help` or `?` to list available commands\r\n" + "\r\n" ANSI_RESET); + + const Version* firmware_version = furi_hal_version_get_firmware_version(); + if(firmware_version) { + printf( + "Firmware version: %s %s (%s%s built on %s)\r\n", + version_get_gitbranch(firmware_version), + version_get_version(firmware_version), + version_get_githash(firmware_version), + version_get_dirty_flag(firmware_version) ? "-dirty" : "", + version_get_builddate(firmware_version)); + } +} + +const CliCommandExternalConfig cli_main_ext_config = { + .search_directory = "/ext/apps_data/cli/plugins", + .fal_prefix = "cli_", + .appid = CLI_APPID, +}; diff --git a/applications/services/cli/cli_main_shell.h b/applications/services/cli/cli_main_shell.h new file mode 100644 index 000000000..576839990 --- /dev/null +++ b/applications/services/cli/cli_main_shell.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +void cli_main_motd(void* context); + +extern const CliCommandExternalConfig cli_main_ext_config; diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 03ad6c610..f4b539e26 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -1,10 +1,12 @@ #include "cli_vcp.h" -#include "shell/cli_shell.h" #include #include #include #include #include +#include +#include "cli_main_shell.h" +#include "cli_main_commands.h" #define TAG "CliVcp" @@ -47,10 +49,12 @@ struct CliVcp { FuriHalUsbInterface* previous_interface; PipeSide* own_pipe; + PipeSide* shell_pipe; bool is_currently_transmitting; size_t previous_tx_length; - FuriThread* shell; + CliRegistry* main_registry; + CliShell* shell; }; // ============ @@ -210,13 +214,15 @@ static void cli_vcp_internal_event_happened(void* context) { // 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); + cli_shell_join(cli_vcp->shell); + cli_shell_free(cli_vcp->shell); + pipe_free(cli_vcp->shell_pipe); } // start shell thread PipeSideBundle bundle = pipe_alloc(VCP_BUF_SIZE, 1); cli_vcp->own_pipe = bundle.alices_side; + cli_vcp->shell_pipe = bundle.bobs_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( @@ -224,7 +230,9 @@ static void cli_vcp_internal_event_happened(void* context) { 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); + cli_vcp->shell = cli_shell_alloc( + cli_main_motd, NULL, cli_vcp->shell_pipe, cli_vcp->main_registry, &cli_main_ext_config); + cli_shell_start(cli_vcp->shell); } if(event & CliVcpInternalEventRx) { @@ -260,6 +268,8 @@ static CliVcp* cli_vcp_alloc(void) { furi_event_loop_subscribe_thread_flags( cli_vcp->event_loop, cli_vcp_internal_event_happened, cli_vcp); + cli_vcp->main_registry = furi_record_open(RECORD_CLI); + return cli_vcp; } diff --git a/applications/services/cli/commands/hello_world.c b/applications/services/cli/commands/hello_world.c index 81be97298..b77f3e663 100644 --- a/applications/services/cli/commands/hello_world.c +++ b/applications/services/cli/commands/hello_world.c @@ -1,4 +1,4 @@ -#include "../cli_commands.h" +#include "../cli_main_commands.h" static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(pipe); @@ -7,4 +7,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { puts("Hello, World!"); } -CLI_COMMAND_INTERFACE(hello_world, execute, CliCommandFlagDefault, 768); +CLI_COMMAND_INTERFACE(hello_world, execute, CliCommandFlagParallelSafe, 768, CLI_APPID); diff --git a/applications/services/cli/commands/neofetch.c b/applications/services/cli/commands/neofetch.c index e652212eb..0e50a0d8d 100644 --- a/applications/services/cli/commands/neofetch.c +++ b/applications/services/cli/commands/neofetch.c @@ -1,4 +1,5 @@ -#include "../cli_commands.h" +#include "../cli_main_commands.h" +#include #include #include #include @@ -156,4 +157,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { #undef NEOFETCH_COLOR } -CLI_COMMAND_INTERFACE(neofetch, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(neofetch, execute, CliCommandFlagParallelSafe, 2048, CLI_APPID); diff --git a/applications/services/cli/commands/subshell_demo.c b/applications/services/cli/commands/subshell_demo.c new file mode 100644 index 000000000..f0013c4a0 --- /dev/null +++ b/applications/services/cli/commands/subshell_demo.c @@ -0,0 +1,43 @@ +#include "../cli_main_commands.h" +#include +#include +#include + +#define RAINBOW_SUBCOMMAND \ + ANSI_FG_RED "s" ANSI_FG_YELLOW "u" ANSI_FG_BLUE "b" ANSI_FG_GREEN "c" ANSI_FG_MAGENTA \ + "o" ANSI_FG_RED "m" ANSI_FG_YELLOW "m" ANSI_FG_BLUE "a" ANSI_FG_GREEN \ + "n" ANSI_FG_MAGENTA "d" + +static void subcommand(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + UNUSED(context); + printf("This is a ✨" RAINBOW_SUBCOMMAND ANSI_RESET "✨!"); +} + +static void motd(void* context) { + UNUSED(context); + printf("\r\n"); + printf("+------------------------------------+\r\n"); + printf("| Hello world! |\r\n"); + printf("| This is the " ANSI_FG_GREEN "MOTD" ANSI_RESET " for our " ANSI_FG_BLUE + "subshell" ANSI_RESET "! |\r\n"); + printf("+------------------------------------+\r\n"); +} + +static void execute(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(args); + UNUSED(context); + CliRegistry* registry = cli_registry_alloc(); + cli_registry_add_command(registry, "subcommand", CliCommandFlagParallelSafe, subcommand, NULL); + + CliShell* shell = cli_shell_alloc(motd, NULL, pipe, registry, NULL); + cli_shell_set_prompt(shell, "subshell"); + cli_shell_start(shell); + cli_shell_join(shell); + + cli_shell_free(shell); + cli_registry_free(registry); +} + +CLI_COMMAND_INTERFACE(subshell_demo, execute, CliCommandFlagParallelSafe, 2048, CLI_APPID); diff --git a/applications/services/cli/shell/cli_shell.h b/applications/services/cli/shell/cli_shell.h deleted file mode 100644 index e60eefc77..000000000 --- a/applications/services/cli/shell/cli_shell.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define CLI_SHELL_STACK_SIZE (4 * 1024U) - -FuriThread* cli_shell_start(PipeSide* pipe); - -#ifdef __cplusplus -} -#endif diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index bfefaf0f1..32b4199f5 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -3,7 +3,9 @@ #include #include -#include +#include +#include +#include void crypto_cli_print_usage(void) { printf("Usage:\r\n"); @@ -319,8 +321,8 @@ static void crypto_cli(PipeSide* pipe, FuriString* args, void* context) { void crypto_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "crypto", CliCommandFlagDefault, crypto_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "crypto", CliCommandFlagDefault, crypto_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(crypto_cli); diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 0f6304823..36536b99f 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -1,6 +1,5 @@ #include "desktop_i.h" -#include #include #include diff --git a/applications/services/input/input.c b/applications/services/input/input.c index c77771f3e..d7e9f3d31 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -4,8 +4,9 @@ #include #include #include -#include #include +#include +#include #include #define INPUT_DEBOUNCE_TICKS_HALF (INPUT_DEBOUNCE_TICKS / 2) @@ -93,8 +94,10 @@ int32_t input_srv(void* p) { #endif #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "input", CliCommandFlagParallelSafe, input_cli, event_pubsub); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, "input", CliCommandFlagParallelSafe, input_cli, event_pubsub); + furi_record_close(RECORD_CLI); #endif InputPinState pin_states[input_pins_count]; diff --git a/applications/services/input/input_cli.c b/applications/services/input/input_cli.c index a34ec3c36..d5ea57418 100644 --- a/applications/services/input/input_cli.c +++ b/applications/services/input/input_cli.c @@ -1,7 +1,7 @@ #include "input.h" #include -#include +#include #include #include diff --git a/applications/services/loader/loader_cli.c b/applications/services/loader/loader_cli.c index 40312d8b3..265779e8f 100644 --- a/applications/services/loader/loader_cli.c +++ b/applications/services/loader/loader_cli.c @@ -1,7 +1,8 @@ #include "loader.h" #include -#include +#include +#include #include #include #include @@ -141,8 +142,9 @@ static void loader_cli(PipeSide* pipe, FuriString* args, void* context) { void loader_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_LOADER, CliCommandFlagParallelSafe, loader_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, RECORD_LOADER, CliCommandFlagParallelSafe, loader_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(loader_cli); diff --git a/applications/services/power/power_cli.c b/applications/services/power/power_cli.c index f1771d9f1..f630cb2e8 100644 --- a/applications/services/power/power_cli.c +++ b/applications/services/power/power_cli.c @@ -1,7 +1,8 @@ #include "power_cli.h" #include -#include +#include +#include #include #include #include @@ -114,10 +115,8 @@ void power_cli(PipeSide* pipe, FuriString* args, void* context) { void power_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - - cli_add_command(cli, "power", CliCommandFlagParallelSafe, power_cli, NULL); - + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "power", CliCommandFlagParallelSafe, power_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(power_cli); diff --git a/applications/services/rpc/rpc.c b/applications/services/rpc/rpc.c index 41d55841e..1a19348ff 100644 --- a/applications/services/rpc/rpc.c +++ b/applications/services/rpc/rpc.c @@ -9,7 +9,8 @@ #include -#include +#include +#include #include #include #include @@ -435,9 +436,14 @@ void rpc_on_system_start(void* p) { rpc->busy_mutex = furi_mutex_alloc(FuriMutexTypeNormal); - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command( - cli, "start_rpc_session", CliCommandFlagParallelSafe, rpc_cli_command_start_session, rpc); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, + "start_rpc_session", + CliCommandFlagParallelSafe, + rpc_cli_command_start_session, + rpc); + furi_record_close(RECORD_CLI); furi_record_create(RECORD_RPC, rpc); } diff --git a/applications/services/rpc/rpc_cli.c b/applications/services/rpc/rpc_cli.c index 3280fe702..fda059ec8 100644 --- a/applications/services/rpc/rpc_cli.c +++ b/applications/services/rpc/rpc_cli.c @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include diff --git a/applications/services/rpc/rpc_i.h b/applications/services/rpc/rpc_i.h index 51903a276..df1f17de4 100644 --- a/applications/services/rpc/rpc_i.h +++ b/applications/services/rpc/rpc_i.h @@ -5,7 +5,6 @@ #include #include #include -#include #include #ifdef __cplusplus diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 58b851926..2dab63e53 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -1,7 +1,9 @@ #include #include -#include +#include +#include +#include #include #include #include @@ -695,16 +697,15 @@ static void storage_cli_factory_reset(PipeSide* pipe, FuriString* args, void* co void storage_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command_ex( - cli, + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, "storage", CliCommandFlagParallelSafe | CliCommandFlagUseShellThread, storage_cli, - NULL, - 512); - cli_add_command( - cli, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); + NULL); + cli_registry_add_command( + registry, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); furi_record_close(RECORD_CLI); #else UNUSED(storage_cli_factory_reset); diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index bdb2ccc28..3084b9b2a 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -4,7 +4,8 @@ #include "js_app_i.h" #include #include -#include +#include +#include #include #define TAG "JS app" @@ -209,8 +210,8 @@ void js_cli_execute(PipeSide* pipe, FuriString* args, void* context) { void js_app_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "js", CliCommandFlagDefault, js_cli_execute, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "js", CliCommandFlagDefault, js_cli_execute, NULL); furi_record_close(RECORD_CLI); #endif } diff --git a/applications/system/updater/cli/updater_cli.c b/applications/system/updater/cli/updater_cli.c index 01949269f..bad8f0cd6 100644 --- a/applications/system/updater/cli/updater_cli.c +++ b/applications/system/updater/cli/updater_cli.c @@ -1,7 +1,8 @@ #include #include -#include +#include +#include #include #include #include @@ -106,8 +107,8 @@ static void updater_start_app(void* context, uint32_t arg) { void updater_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = (Cli*)furi_record_open(RECORD_CLI); - cli_add_command(cli, "update", CliCommandFlagDefault, updater_cli_ep, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "update", CliCommandFlagDefault, updater_cli_ep, NULL); furi_record_close(RECORD_CLI); #else UNUSED(updater_cli_ep); diff --git a/lib/toolbox/SConscript b/lib/toolbox/SConscript index 8a1c4a8c5..ad368e2a1 100644 --- a/lib/toolbox/SConscript +++ b/lib/toolbox/SConscript @@ -12,6 +12,10 @@ env.Append( ], SDK_HEADERS=[ File("api_lock.h"), + File("cli/cli_ansi.h"), + File("cli/cli_command.h"), + File("cli/cli_registry.h"), + File("cli/shell/cli_shell.h"), File("compress.h"), File("manchester_decoder.h"), File("manchester_encoder.h"), diff --git a/applications/services/cli/cli_ansi.c b/lib/toolbox/cli/cli_ansi.c similarity index 100% rename from applications/services/cli/cli_ansi.c rename to lib/toolbox/cli/cli_ansi.c diff --git a/applications/services/cli/cli_ansi.h b/lib/toolbox/cli/cli_ansi.h similarity index 99% rename from applications/services/cli/cli_ansi.h rename to lib/toolbox/cli/cli_ansi.h index 20bf33d8e..04b6d7759 100644 --- a/applications/services/cli/cli_ansi.h +++ b/lib/toolbox/cli/cli_ansi.h @@ -1,6 +1,6 @@ #pragma once -#include "cli.h" +#include #ifdef __cplusplus extern "C" { diff --git a/lib/toolbox/cli/cli_command.c b/lib/toolbox/cli/cli_command.c new file mode 100644 index 000000000..a3c9ff292 --- /dev/null +++ b/lib/toolbox/cli/cli_command.c @@ -0,0 +1,17 @@ +#include "cli_command.h" +#include "cli_ansi.h" + +bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side) { + if(pipe_state(side) == PipeStateBroken) return true; + if(!pipe_bytes_available(side)) return false; + char c = getchar(); + return c == CliKeyETX; +} + +void cli_print_usage(const char* cmd, const char* usage, const char* arg) { + furi_check(cmd); + furi_check(arg); + furi_check(usage); + + printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage); +} diff --git a/lib/toolbox/cli/cli_command.h b/lib/toolbox/cli/cli_command.h new file mode 100644 index 000000000..2d1d851d6 --- /dev/null +++ b/lib/toolbox/cli/cli_command.h @@ -0,0 +1,103 @@ +/** + * @file cli_command.h + * Command metadata and helpers + */ + +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLI_PLUGIN_API_VERSION 1 + +typedef enum { + CliCommandFlagDefault = 0, /**< Default */ + CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps */ + CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ + CliCommandFlagDontAttachStdio = (1 << 2), /**< Do no attach I/O pipe to thread stdio */ + CliCommandFlagUseShellThread = + (1 + << 3), /**< Don't start a separate thread to run the command in. Incompatible with DontAttachStdio */ + + // internal flags (do not set them yourselves!) + + CliCommandFlagExternal = (1 << 4), /**< The command comes from a .fal file */ +} CliCommandFlag; + +/** + * @brief CLI command execution callback pointer + * + * This callback will be called from a separate thread spawned just for your + * command. The pipe will be installed as the thread's stdio, so you can use + * `printf`, `getchar` and other standard functions to communicate with the + * user. + * + * @param [in] pipe Pipe that can be used to send and receive data. If + * `CliCommandFlagDontAttachStdio` was not set, you can + * also use standard C functions (printf, getc, etc.) to + * access this pipe. + * @param [in] args String with what was passed after the command + * @param [in] context Whatever you provided to `cli_add_command` + */ +typedef void (*CliCommandExecuteCallback)(PipeSide* pipe, FuriString* args, void* context); + +typedef struct { + char* name; + CliCommandExecuteCallback execute_callback; + CliCommandFlag flags; + size_t stack_depth; +} CliCommandDescriptor; + +/** + * @brief Configuration for locating external commands + */ +typedef struct { + const char* search_directory; // +#include + +#define TAG "cli" + +struct CliRegistry { + CliCommandTree_t commands; + FuriMutex* mutex; +}; + +CliRegistry* cli_registry_alloc(void) { + CliRegistry* registry = malloc(sizeof(CliRegistry)); + CliCommandTree_init(registry->commands); + registry->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); + return registry; +} + +void cli_registry_free(CliRegistry* registry) { + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + furi_mutex_free(registry->mutex); + CliCommandTree_clear(registry->commands); + free(registry); +} + +void cli_registry_add_command( + CliRegistry* registry, + const char* name, + CliCommandFlag flags, + CliCommandExecuteCallback callback, + void* context) { + cli_registry_add_command_ex( + registry, name, flags, callback, context, CLI_BUILTIN_COMMAND_STACK_SIZE); +} + +void cli_registry_add_command_ex( + CliRegistry* registry, + const char* name, + CliCommandFlag flags, + CliCommandExecuteCallback callback, + void* context, + size_t stack_size) { + furi_check(registry); + furi_check(name); + furi_check(callback); + + // the shell always attaches the pipe to the stdio, thus both flags can't be used at once + if(flags & CliCommandFlagUseShellThread) furi_check(!(flags & CliCommandFlagDontAttachStdio)); + + FuriString* name_str; + name_str = furi_string_alloc_set(name); + // command cannot contain spaces + furi_check(furi_string_search_char(name_str, ' ') == FURI_STRING_FAILURE); + + CliRegistryCommand command = { + .context = context, + .execute_callback = callback, + .flags = flags, + .stack_depth = stack_size, + }; + + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + CliCommandTree_set_at(registry->commands, name_str, command); + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); + + furi_string_free(name_str); +} + +void cli_registry_delete_command(CliRegistry* registry, const char* name) { + furi_check(registry); + FuriString* name_str; + name_str = furi_string_alloc_set(name); + furi_string_trim(name_str); + + size_t name_replace; + do { + name_replace = furi_string_replace(name_str, " ", "_"); + } while(name_replace != FURI_STRING_FAILURE); + + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + CliCommandTree_erase(registry->commands, name_str); + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); + + furi_string_free(name_str); +} + +bool cli_registry_get_command( + CliRegistry* registry, + FuriString* command, + CliRegistryCommand* result) { + furi_assert(registry); + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + CliRegistryCommand* data = CliCommandTree_get(registry->commands, command); + if(data) *result = *data; + + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); + + return !!data; +} + +void cli_registry_remove_external_commands(CliRegistry* registry) { + furi_check(registry); + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + + // FIXME FL-3977: memory leak somewhere within this function + + CliCommandTree_t internal_cmds; + CliCommandTree_init(internal_cmds); + for + M_EACH(item, registry->commands, CliCommandTree_t) { + if(!(item->value_ptr->flags & CliCommandFlagExternal)) + CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr); + } + CliCommandTree_move(registry->commands, internal_cmds); + + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); +} + +void cli_registry_reload_external_commands( + CliRegistry* registry, + const CliCommandExternalConfig* config) { + furi_check(registry); + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); + FURI_LOG_D(TAG, "Reloading ext commands"); + + cli_registry_remove_external_commands(registry); + + // iterate over files in plugin directory + Storage* storage = furi_record_open(RECORD_STORAGE); + File* plugin_dir = storage_file_alloc(storage); + + if(storage_dir_open(plugin_dir, config->search_directory)) { + char plugin_filename[64]; + FuriString* plugin_name = furi_string_alloc(); + + while(storage_dir_read(plugin_dir, NULL, plugin_filename, sizeof(plugin_filename))) { + FURI_LOG_T(TAG, "Plugin: %s", plugin_filename); + furi_string_set_str(plugin_name, plugin_filename); + + furi_check(furi_string_end_with_str(plugin_name, ".fal")); + furi_string_replace_all_str(plugin_name, ".fal", ""); + furi_check(furi_string_start_with_str(plugin_name, config->fal_prefix)); + furi_string_replace_at(plugin_name, 0, strlen(config->fal_prefix), ""); + + CliRegistryCommand command = { + .context = NULL, + .execute_callback = NULL, + .flags = CliCommandFlagExternal, + }; + CliCommandTree_set_at(registry->commands, plugin_name, command); + } + + furi_string_free(plugin_name); + } + + storage_dir_close(plugin_dir); + storage_file_free(plugin_dir); + furi_record_close(RECORD_STORAGE); + + FURI_LOG_D(TAG, "Done reloading ext commands"); + furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk); +} + +void cli_registry_lock(CliRegistry* registry) { + furi_assert(registry); + furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk); +} + +void cli_registry_unlock(CliRegistry* registry) { + furi_assert(registry); + furi_mutex_release(registry->mutex); +} + +CliCommandTree_t* cli_registry_get_commands(CliRegistry* registry) { + furi_assert(registry); + return ®istry->commands; +} diff --git a/lib/toolbox/cli/cli_registry.h b/lib/toolbox/cli/cli_registry.h new file mode 100644 index 000000000..44650e79b --- /dev/null +++ b/lib/toolbox/cli/cli_registry.h @@ -0,0 +1,92 @@ +/** + * @file cli_registry.h + * API for registering commands with a CLI shell + */ + +#pragma once + +#include +#include +#include +#include "cli_command.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CliRegistry CliRegistry; + +/** + * @brief Allocates a `CliRegistry`. + */ +CliRegistry* cli_registry_alloc(void); + +/** + * @brief Frees a `CliRegistry`. + */ +void cli_registry_free(CliRegistry* registry); + +/** + * @brief Registers a command with the registry. Provides less options than the + * `_ex` counterpart. + * + * @param [in] registry Pointer to registry instance + * @param [in] name Command name + * @param [in] flags see CliCommandFlag + * @param [in] callback Callback function + * @param [in] context Custom context + */ +void cli_registry_add_command( + CliRegistry* registry, + const char* name, + CliCommandFlag flags, + CliCommandExecuteCallback callback, + void* context); + +/** + * @brief Registers a command with the registry. Provides more options than the + * non-`_ex` counterpart. + * + * @param [in] registry Pointer to registry instance + * @param [in] name Command name + * @param [in] flags see CliCommandFlag + * @param [in] callback Callback function + * @param [in] context Custom context + * @param [in] stack_size Thread stack size + */ +void cli_registry_add_command_ex( + CliRegistry* registry, + const char* name, + CliCommandFlag flags, + CliCommandExecuteCallback callback, + void* context, + size_t stack_size); + +/** + * @brief Deletes a cli command + * + * @param [in] registry Pointer to registry instance + * @param [in] name Command name + */ +void cli_registry_delete_command(CliRegistry* registry, const char* name); + +/** + * @brief Unregisters all external commands + * + * @param [in] registry Pointer to registry instance + */ +void cli_registry_remove_external_commands(CliRegistry* registry); + +/** + * @brief Reloads the list of externally available commands + * + * @param [in] registry Pointer to registry instance + * @param [in] config See `CliCommandExternalConfig` + */ +void cli_registry_reload_external_commands( + CliRegistry* registry, + const CliCommandExternalConfig* config); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/cli_i.h b/lib/toolbox/cli/cli_registry_i.h similarity index 51% rename from applications/services/cli/cli_i.h rename to lib/toolbox/cli/cli_registry_i.h index 3e948c345..95b7c55da 100644 --- a/applications/services/cli/cli_i.h +++ b/lib/toolbox/cli/cli_registry_i.h @@ -1,5 +1,5 @@ /** - * @file cli_i.h + * @file cli_registry_i.h * Internal API for getting commands registered with the CLI */ @@ -7,21 +7,20 @@ #include #include -#include "cli.h" +#include "cli_registry.h" #ifdef __cplusplus extern "C" { #endif -#define CLI_BUILTIN_COMMAND_STACK_SIZE (3 * 1024U) -#define CLI_COMMANDS_PATH "/ext/apps_data/cli/plugins" +#define CLI_BUILTIN_COMMAND_STACK_SIZE (4 * 1024U) typedef struct { void* context; // @@ -36,10 +36,17 @@ typedef enum { } CliShellStorageEvent; struct CliShell { - Cli* cli; - FuriEventLoop* event_loop; + // Set and freed by external thread + CliShellMotd motd; + void* callback_context; PipeSide* pipe; + CliRegistry* registry; + const CliCommandExternalConfig* ext_config; + FuriThread* thread; + const char* prompt; + // Set and freed by shell thread + FuriEventLoop* event_loop; CliAnsiParser* ansi_parser; FuriEventLoopTimer* ansi_parsing_timer; @@ -51,7 +58,7 @@ struct CliShell { }; typedef struct { - CliCommand* command; + CliRegistryCommand* command; PipeSide* pipe; FuriString* args; } CliCommandThreadData; @@ -73,9 +80,62 @@ static void cli_shell_detach_pipe(CliShell* cli_shell) { furi_thread_set_stdout_callback(NULL, NULL); } -// ========= -// Execution -// ========= +// ================= +// Built-in commands +// ================= + +void cli_command_reload_external(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + CliShell* shell = context; + furi_check(shell->ext_config); + cli_registry_reload_external_commands(shell->registry, shell->ext_config); + printf("OK!"); +} + +void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + CliShell* shell = context; + CliRegistry* registry = shell->registry; + + const size_t columns = 3; + + printf("Available commands:\r\n" ANSI_FG_GREEN); + cli_registry_lock(registry); + CliCommandTree_t* commands = cli_registry_get_commands(registry); + size_t commands_count = CliCommandTree_size(*commands); + + CliCommandTree_it_t iterator; + CliCommandTree_it(iterator, *commands); + for(size_t i = 0; i < commands_count; i++) { + const CliCommandTree_itref_t* item = CliCommandTree_cref(iterator); + printf("%-30s", furi_string_get_cstr(*item->key_ptr)); + CliCommandTree_next(iterator); + + if(i % columns == columns - 1) printf("\r\n"); + } + + if(shell->ext_config) + printf( + ANSI_RESET + "\r\nIf you added a new external command and can't see it above, run `reload_ext_cmds`"); + printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli"); + + cli_registry_unlock(registry); +} + +void cli_command_exit(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + CliShell* shell = context; + cli_shell_line_set_about_to_exit(shell->components[CliShellComponentLine]); + furi_event_loop_stop(shell->event_loop); +} + +// ================== +// Internal functions +// ================== static int32_t cli_command_thread(void* context) { CliCommandThreadData* thread_data = context; @@ -99,12 +159,13 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { furi_string_right(args, space + 1); PluginManager* plugin_manager = NULL; - Loader* loader = NULL; - CliCommand command_data; + Loader* loader = furi_record_open(RECORD_LOADER); + bool loader_locked = false; + CliRegistryCommand command_data; do { // find handler - if(!cli_get_command(cli_shell->cli, command_name, &command_data)) { + if(!cli_registry_get_command(cli_shell->registry, command_name, &command_data)) { printf( ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET, furi_string_get_cstr(command_name)); @@ -113,10 +174,14 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // load external command if(command_data.flags & CliCommandFlagExternal) { - plugin_manager = - plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface); + const CliCommandExternalConfig* ext_config = cli_shell->ext_config; + plugin_manager = plugin_manager_alloc( + ext_config->appid, CLI_PLUGIN_API_VERSION, firmware_api_interface); FuriString* path = furi_string_alloc_printf( - "%s/cli_%s.fal", CLI_COMMANDS_PATH, furi_string_get_cstr(command_name)); + "%s/%s%s.fal", + ext_config->search_directory, + ext_config->fal_prefix, + furi_string_get_cstr(command_name)); uint32_t plugin_cnt_last = plugin_manager_get_count(plugin_manager); PluginManagerError error = plugin_manager_load_single(plugin_manager, furi_string_get_cstr(path)); @@ -141,9 +206,8 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // lock loader if(!(command_data.flags & CliCommandFlagParallelSafe)) { - loader = furi_record_open(RECORD_LOADER); - bool success = loader_lock(loader); - if(!success) { + loader_locked = loader_lock(loader); + if(!loader_locked) { printf(ANSI_FG_RED "this command cannot be run while an application is open" ANSI_RESET); break; @@ -177,13 +241,17 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { furi_string_free(args); // unlock loader - if(loader) loader_unlock(loader); + if(loader_locked) loader_unlock(loader); furi_record_close(RECORD_LOADER); // unload external command if(plugin_manager) plugin_manager_free(plugin_manager); } +const char* cli_shell_get_prompt(CliShell* cli_shell) { + return cli_shell->prompt; +} + // ============== // Event handlers // ============== @@ -210,9 +278,9 @@ static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); if(event == CliShellStorageEventMount) { - cli_enumerate_external_commands(cli_shell->cli); + cli_registry_reload_external_commands(cli_shell->registry, cli_shell->ext_config); } else if(event == CliShellStorageEventUnmount) { - cli_remove_external_commands(cli_shell->cli); + cli_registry_remove_external_commands(cli_shell->registry); } else { furi_crash(); } @@ -265,113 +333,97 @@ static void cli_shell_timer_expired(void* context) { cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser)); } -// ======= -// Helpers -// ======= +// =========== +// Thread code +// =========== -static CliShell* cli_shell_alloc(PipeSide* pipe) { - CliShell* cli_shell = malloc(sizeof(CliShell)); +static void cli_shell_init(CliShell* shell) { + cli_registry_add_command( + shell->registry, + "help", + CliCommandFlagUseShellThread | CliCommandFlagParallelSafe, + cli_command_help, + shell); + cli_registry_add_command( + shell->registry, + "?", + CliCommandFlagUseShellThread | CliCommandFlagParallelSafe, + cli_command_help, + shell); + cli_registry_add_command( + shell->registry, + "exit", + CliCommandFlagUseShellThread | CliCommandFlagParallelSafe, + cli_command_exit, + shell); - cli_shell->cli = furi_record_open(RECORD_CLI); - cli_shell->ansi_parser = cli_ansi_parser_alloc(); - cli_shell->pipe = pipe; + if(shell->ext_config) { + cli_registry_add_command( + shell->registry, + "reload_ext_cmds", + CliCommandFlagUseShellThread, + cli_command_reload_external, + shell); + cli_registry_reload_external_commands(shell->registry, shell->ext_config); + } - cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); - cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( - cli_shell->cli, cli_shell, cli_shell->components[CliShellComponentLine]); + shell->components[CliShellComponentLine] = cli_shell_line_alloc(shell); + shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( + shell->registry, shell, shell->components[CliShellComponentLine]); - cli_shell->event_loop = furi_event_loop_alloc(); - cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( - cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell); + shell->ansi_parser = cli_ansi_parser_alloc(); - cli_shell_install_pipe(cli_shell); + shell->event_loop = furi_event_loop_alloc(); + shell->ansi_parsing_timer = furi_event_loop_timer_alloc( + shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, shell); - cli_shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); + shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); furi_event_loop_subscribe_message_queue( - cli_shell->event_loop, - cli_shell->storage_event_queue, + shell->event_loop, + shell->storage_event_queue, FuriEventLoopEventIn, cli_shell_storage_internal_event, - cli_shell); - cli_shell->storage = furi_record_open(RECORD_STORAGE); - cli_shell->storage_subscription = furi_pubsub_subscribe( - storage_get_pubsub(cli_shell->storage), cli_shell_storage_event, cli_shell); + shell); + shell->storage = furi_record_open(RECORD_STORAGE); + shell->storage_subscription = + furi_pubsub_subscribe(storage_get_pubsub(shell->storage), cli_shell_storage_event, shell); - return cli_shell; + cli_shell_install_pipe(shell); } -static void cli_shell_free(CliShell* cli_shell) { - furi_pubsub_unsubscribe( - storage_get_pubsub(cli_shell->storage), cli_shell->storage_subscription); +static void cli_shell_deinit(CliShell* shell) { + furi_pubsub_unsubscribe(storage_get_pubsub(shell->storage), shell->storage_subscription); furi_record_close(RECORD_STORAGE); - furi_event_loop_unsubscribe(cli_shell->event_loop, cli_shell->storage_event_queue); - furi_message_queue_free(cli_shell->storage_event_queue); + furi_event_loop_unsubscribe(shell->event_loop, shell->storage_event_queue); + furi_message_queue_free(shell->storage_event_queue); - cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]); - cli_shell_line_free(cli_shell->components[CliShellComponentLine]); + cli_shell_completions_free(shell->components[CliShellComponentCompletions]); + cli_shell_line_free(shell->components[CliShellComponentLine]); - cli_shell_detach_pipe(cli_shell); - furi_event_loop_timer_free(cli_shell->ansi_parsing_timer); - furi_event_loop_free(cli_shell->event_loop); - pipe_free(cli_shell->pipe); - cli_ansi_parser_free(cli_shell->ansi_parser); - furi_record_close(RECORD_CLI); - free(cli_shell); -} - -static void cli_shell_motd(void) { - printf(ANSI_FLIPPER_BRAND_ORANGE - "\r\n" - " _.-------.._ -,\r\n" - " .-\"```\"--..,,_/ /`-, -, \\ \r\n" - " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" - " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" - " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" - " | | | 0 | | .-' ,/` /\r\n" - " | ,..\\ \\ ,.-\"` ,/` /\r\n" - " ; : `/`\"\"\\` ,/--==,/-----,\r\n" - " | `-...| -.___-Z:_______J...---;\r\n" - " : ` _-'\r\n" - " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" - "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" - "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" - "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" - "\r\n" ANSI_FG_BR_WHITE "Welcome to Flipper Zero Command Line Interface!\r\n" - "Read the manual: https://docs.flipper.net/development/cli\r\n" - "Run `help` or `?` to list available commands\r\n" - "\r\n" ANSI_RESET); - - const Version* firmware_version = furi_hal_version_get_firmware_version(); - if(firmware_version) { - printf( - "Firmware version: %s %s (%s%s built on %s)\r\n", - version_get_gitbranch(firmware_version), - version_get_version(firmware_version), - version_get_githash(firmware_version), - version_get_dirty_flag(firmware_version) ? "-dirty" : "", - version_get_builddate(firmware_version)); - } + cli_shell_detach_pipe(shell); + furi_event_loop_timer_free(shell->ansi_parsing_timer); + furi_event_loop_free(shell->event_loop); + cli_ansi_parser_free(shell->ansi_parser); } static int32_t cli_shell_thread(void* context) { - PipeSide* pipe = context; + CliShell* shell = context; // Sometimes, the other side closes the pipe even before our thread is started. Although the // rest of the code will eventually find this out if this check is removed, there's no point in // wasting time. - if(pipe_state(pipe) == PipeStateBroken) return 0; - - CliShell* cli_shell = cli_shell_alloc(pipe); + if(pipe_state(shell->pipe) == PipeStateBroken) return 0; + cli_shell_init(shell); FURI_LOG_D(TAG, "Started"); - cli_shell_motd(); - cli_shell_line_prompt(cli_shell->components[CliShellComponentLine]); - furi_event_loop_run(cli_shell->event_loop); + shell->motd(shell->callback_context); + cli_shell_line_prompt(shell->components[CliShellComponentLine]); + + furi_event_loop_run(shell->event_loop); FURI_LOG_D(TAG, "Stopped"); - - cli_shell_free(cli_shell); + cli_shell_deinit(shell); return 0; } @@ -379,9 +431,48 @@ static int32_t cli_shell_thread(void* context) { // Public API // ========== -FuriThread* cli_shell_start(PipeSide* pipe) { - FuriThread* thread = - furi_thread_alloc_ex("CliShell", CLI_SHELL_STACK_SIZE, cli_shell_thread, pipe); - furi_thread_start(thread); - return thread; +CliShell* cli_shell_alloc( + CliShellMotd motd, + void* context, + PipeSide* pipe, + CliRegistry* registry, + const CliCommandExternalConfig* ext_config) { + furi_check(motd); + furi_check(pipe); + furi_check(registry); + + CliShell* shell = malloc(sizeof(CliShell)); + *shell = (CliShell){ + .motd = motd, + .callback_context = context, + .pipe = pipe, + .registry = registry, + .ext_config = ext_config, + }; + + shell->thread = + furi_thread_alloc_ex("CliShell", CLI_SHELL_STACK_SIZE, cli_shell_thread, shell); + + return shell; +} + +void cli_shell_free(CliShell* shell) { + furi_check(shell); + furi_thread_free(shell->thread); + free(shell); +} + +void cli_shell_start(CliShell* shell) { + furi_check(shell); + furi_thread_start(shell->thread); +} + +void cli_shell_join(CliShell* shell) { + furi_check(shell); + furi_thread_join(shell->thread); +} + +void cli_shell_set_prompt(CliShell* shell, const char* prompt) { + furi_check(shell); + shell->prompt = prompt; } diff --git a/lib/toolbox/cli/shell/cli_shell.h b/lib/toolbox/cli/shell/cli_shell.h new file mode 100644 index 000000000..74f71273e --- /dev/null +++ b/lib/toolbox/cli/shell/cli_shell.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include "../cli_registry.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLI_SHELL_STACK_SIZE (4 * 1024U) + +typedef struct CliShell CliShell; + +/** + * Called from the shell thread to print the Message of the Day when the shell + * is started. + */ +typedef void (*CliShellMotd)(void* context); + +/** + * @brief Allocates a shell + * + * @param [in] motd Message of the Day callback + * @param [in] context Callback context + * @param [in] pipe Pipe side to be used by the shell + * @param [in] registry Command registry + * @param [in] ext_config External command configuration. See + * `CliCommandExternalConfig`. May be NULL if support for + * external commands is not required. + * + * @return Shell instance + */ +CliShell* cli_shell_alloc( + CliShellMotd motd, + void* context, + PipeSide* pipe, + CliRegistry* registry, + const CliCommandExternalConfig* ext_config); + +/** + * @brief Frees a shell + * + * @param [in] shell Shell instance + */ +void cli_shell_free(CliShell* shell); + +/** + * @brief Starts a shell + * + * The shell runs in a separate thread. This call is non-blocking. + * + * @param [in] shell Shell instance + */ +void cli_shell_start(CliShell* shell); + +/** + * @brief Joins the shell thread + * + * @warning This call is blocking. + * + * @param [in] shell Shell instance + */ +void cli_shell_join(CliShell* shell); + +/** + * @brief Sets optional text before prompt (`>:`) + * + * @param [in] shell Shell instance + */ +void cli_shell_set_prompt(CliShell* shell, const char* prompt); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/shell/cli_shell_completions.c b/lib/toolbox/cli/shell/cli_shell_completions.c similarity index 97% rename from applications/services/cli/shell/cli_shell_completions.c rename to lib/toolbox/cli/shell/cli_shell_completions.c index 0b32c18a2..823f91fb9 100644 --- a/applications/services/cli/shell/cli_shell_completions.c +++ b/lib/toolbox/cli/shell/cli_shell_completions.c @@ -4,7 +4,7 @@ ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524 #define M_OPL_CommandCompletions_t() ARRAY_OPLIST(CommandCompletions) struct CliShellCompletions { - Cli* cli; + CliRegistry* registry; CliShell* shell; CliShellLine* line; CommandCompletions_t variants; @@ -45,10 +45,11 @@ typedef struct { // Public API // ========== -CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line) { +CliShellCompletions* + cli_shell_completions_alloc(CliRegistry* registry, CliShell* shell, CliShellLine* line) { CliShellCompletions* completions = malloc(sizeof(CliShellCompletions)); - completions->cli = cli; + completions->registry = registry; completions->shell = shell; completions->line = line; CommandCompletions_init(completions->variants); @@ -108,8 +109,9 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { furi_string_left(input, segment.length); if(segment.type == CliShellCompletionSegmentTypeCommand) { - cli_lock_commands(completions->cli); - CliCommandTree_t* commands = cli_get_commands(completions->cli); + CliRegistry* registry = completions->registry; + cli_registry_lock(registry); + CliCommandTree_t* commands = cli_registry_get_commands(registry); for M_EACH(registered_command, *commands, CliCommandTree_t) { FuriString* command_name = *registered_command->key_ptr; @@ -117,7 +119,7 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { CommandCompletions_push_back(completions->variants, command_name); } } - cli_unlock_commands(completions->cli); + cli_registry_unlock(registry); } else { // support removed, might reimplement in the future diff --git a/applications/services/cli/shell/cli_shell_completions.h b/lib/toolbox/cli/shell/cli_shell_completions.h similarity index 67% rename from applications/services/cli/shell/cli_shell_completions.h rename to lib/toolbox/cli/shell/cli_shell_completions.h index 6353bde71..d49a1982d 100644 --- a/applications/services/cli/shell/cli_shell_completions.h +++ b/lib/toolbox/cli/shell/cli_shell_completions.h @@ -4,8 +4,8 @@ #include #include "cli_shell_i.h" #include "cli_shell_line.h" -#include "../cli.h" -#include "../cli_i.h" +#include "../cli_registry.h" +#include "../cli_registry_i.h" #ifdef __cplusplus extern "C" { @@ -13,7 +13,8 @@ extern "C" { typedef struct CliShellCompletions CliShellCompletions; -CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line); +CliShellCompletions* + cli_shell_completions_alloc(CliRegistry* registry, CliShell* shell, CliShellLine* line); void cli_shell_completions_free(CliShellCompletions* completions); diff --git a/applications/services/cli/shell/cli_shell_i.h b/lib/toolbox/cli/shell/cli_shell_i.h similarity index 91% rename from applications/services/cli/shell/cli_shell_i.h rename to lib/toolbox/cli/shell/cli_shell_i.h index e8eae92c6..0b676b7de 100644 --- a/applications/services/cli/shell/cli_shell_i.h +++ b/lib/toolbox/cli/shell/cli_shell_i.h @@ -27,6 +27,8 @@ typedef struct { void cli_shell_execute_command(CliShell* cli_shell, FuriString* command); +const char* cli_shell_get_prompt(CliShell* cli_shell); + #ifdef __cplusplus } #endif diff --git a/applications/services/cli/shell/cli_shell_line.c b/lib/toolbox/cli/shell/cli_shell_line.c similarity index 95% rename from applications/services/cli/shell/cli_shell_line.c rename to lib/toolbox/cli/shell/cli_shell_line.c index 959cd0b3b..4826ba252 100644 --- a/applications/services/cli/shell/cli_shell_line.c +++ b/lib/toolbox/cli/shell/cli_shell_line.c @@ -8,6 +8,7 @@ struct CliShellLine { FuriString* history[HISTORY_DEPTH]; size_t history_entries; CliShell* shell; + bool about_to_exit; }; // ========== @@ -39,14 +40,16 @@ FuriString* cli_shell_line_get_editing(CliShellLine* line) { return line->history[0]; } -size_t cli_shell_line_prompt_length(CliShellLine* line) { - UNUSED(line); - return strlen(">: "); -} - void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length) { UNUSED(line); - snprintf(buf, length - 1, ">: "); + const char* prompt = cli_shell_get_prompt(line->shell); + snprintf(buf, length - 1, "%s>: ", prompt ? prompt : ""); +} + +size_t cli_shell_line_prompt_length(CliShellLine* line) { + char buffer[128]; + cli_shell_line_format_prompt(line, buffer, sizeof(buffer)); + return strlen(buffer); } void cli_shell_line_prompt(CliShellLine* line) { @@ -65,6 +68,10 @@ void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) { } } +void cli_shell_line_set_about_to_exit(CliShellLine* line) { + line->about_to_exit = true; +} + size_t cli_shell_line_get_line_position(CliShellLine* line) { return line->line_position; } @@ -188,8 +195,7 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { printf("\r\n"); cli_shell_execute_command(line->shell, command_copy); furi_string_free(command_copy); - - cli_shell_line_prompt(line); + if(!line->about_to_exit) cli_shell_line_prompt(line); return true; } @@ -202,10 +208,13 @@ static bool cli_shell_line_input_up_down(CliKeyCombo combo, void* context) { // print prompt with selected command if(new_pos != line->history_position) { + char prompt[64]; + cli_shell_line_format_prompt(line, prompt, sizeof(prompt)); line->history_position = new_pos; FuriString* command = cli_shell_line_get_selected(line); printf( - ANSI_CURSOR_HOR_POS("1") ">: %s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + ANSI_CURSOR_HOR_POS("1") "%s%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + prompt, furi_string_get_cstr(command)); fflush(stdout); line->line_position = furi_string_size(command); diff --git a/applications/services/cli/shell/cli_shell_line.h b/lib/toolbox/cli/shell/cli_shell_line.h similarity index 94% rename from applications/services/cli/shell/cli_shell_line.h rename to lib/toolbox/cli/shell/cli_shell_line.h index 1e4b9e32a..e40a12bd6 100644 --- a/applications/services/cli/shell/cli_shell_line.h +++ b/lib/toolbox/cli/shell/cli_shell_line.h @@ -33,6 +33,8 @@ void cli_shell_line_set_line_position(CliShellLine* line, size_t position); */ void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line); +void cli_shell_line_set_about_to_exit(CliShellLine* line); + extern CliShellKeyComboSet cli_shell_line_key_combo_set; #ifdef __cplusplus diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 72512a46f..ce47fe5c2 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,9 +1,7 @@ entry,status,name,type,params -Version,+,84.1,, +Version,+,85.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, -Header,+,applications/services/cli/cli.h,, -Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -153,6 +151,10 @@ Header,+,lib/stm32wb_hal/Inc/stm32wbxx_ll_wwdg.h,, Header,+,lib/toolbox/api_lock.h,, Header,+,lib/toolbox/args.h,, Header,+,lib/toolbox/bit_buffer.h,, +Header,+,lib/toolbox/cli/cli_ansi.h,, +Header,+,lib/toolbox/cli/cli_command.h,, +Header,+,lib/toolbox/cli/cli_registry.h,, +Header,+,lib/toolbox/cli/shell/cli_shell.h,, Header,+,lib/toolbox/compress.h,, Header,+,lib/toolbox/crc32_calc.h,, Header,+,lib/toolbox/dir_walk.h,, @@ -779,17 +781,24 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" -Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" Function,+,cli_ansi_parser_alloc,CliAnsiParser*, Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* -Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_remove_external_commands,void,Cli* +Function,+,cli_registry_add_command,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*" +Function,+,cli_registry_add_command_ex,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*, size_t" +Function,+,cli_registry_alloc,CliRegistry*, +Function,+,cli_registry_delete_command,void,"CliRegistry*, const char*" +Function,+,cli_registry_free,void,CliRegistry* +Function,+,cli_registry_reload_external_commands,void,"CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_registry_remove_external_commands,void,CliRegistry* +Function,+,cli_shell_alloc,CliShell*,"CliShellMotd, void*, PipeSide*, CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_shell_free,void,CliShell* +Function,+,cli_shell_join,void,CliShell* +Function,+,cli_shell_set_prompt,void,"CliShell*, const char*" +Function,+,cli_shell_start,void,CliShell* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 73ad2dcd5..e19281e2a 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,10 +1,8 @@ entry,status,name,type,params -Version,+,84.1,, +Version,+,85.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, -Header,+,applications/services/cli/cli.h,, -Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -225,6 +223,10 @@ Header,+,lib/subghz/transmitter.h,, Header,+,lib/toolbox/api_lock.h,, Header,+,lib/toolbox/args.h,, Header,+,lib/toolbox/bit_buffer.h,, +Header,+,lib/toolbox/cli/cli_ansi.h,, +Header,+,lib/toolbox/cli/cli_command.h,, +Header,+,lib/toolbox/cli/cli_registry.h,, +Header,+,lib/toolbox/cli/shell/cli_shell.h,, Header,+,lib/toolbox/compress.h,, Header,+,lib/toolbox/crc32_calc.h,, Header,+,lib/toolbox/dir_walk.h,, @@ -856,17 +858,24 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" -Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" Function,+,cli_ansi_parser_alloc,CliAnsiParser*, Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* -Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_remove_external_commands,void,Cli* +Function,+,cli_registry_add_command,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*" +Function,+,cli_registry_add_command_ex,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*, size_t" +Function,+,cli_registry_alloc,CliRegistry*, +Function,+,cli_registry_delete_command,void,"CliRegistry*, const char*" +Function,+,cli_registry_free,void,CliRegistry* +Function,+,cli_registry_reload_external_commands,void,"CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_registry_remove_external_commands,void,CliRegistry* +Function,+,cli_shell_alloc,CliShell*,"CliShellMotd, void*, PipeSide*, CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_shell_free,void,CliShell* +Function,+,cli_shell_join,void,CliShell* +Function,+,cli_shell_set_prompt,void,"CliShell*, const char*" +Function,+,cli_shell_start,void,CliShell* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" From dac1457f0a2a4befc4d8e3441adc02d98b896da9 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Sat, 5 Apr 2025 03:17:30 +0400 Subject: [PATCH 8/9] [FL-3963] Move JS modules to new arg parser (#4139) * js: value destructuring and tests * js: temporary fix to see size impact * js_val: reduce code size 1 * i may be stupid. * test: js_value args * Revert "js: temporary fix to see size impact" This reverts commit f51d726dbafc4300d3552020de1c3b8f9ecd3ac1. * pvs: silence warnings * style: formatting * pvs: silence warnings? * pvs: silence warnings?? * js_value: redesign declaration types for less code * js: temporary fix to see size impact * style: formatting * pvs: fix helpful warnings * js_value: reduce .rodata size * pvs: fix helpful warning * js_value: reduce code size 1 * fix build error * style: format * Revert "js: temporary fix to see size impact" This reverts commit d6a46f01794132e882e03fd273dec24386a4f8ba. * style: format * js: move to new arg parser * style: format --------- Co-authored-by: hedger --- applications/system/js_app/js_modules.c | 26 +- applications/system/js_app/js_modules.h | 228 +------------- applications/system/js_app/js_thread.c | 19 +- applications/system/js_app/js_thread_i.h | 8 + .../modules/js_event_loop/js_event_loop.c | 52 +++- applications/system/js_app/modules/js_gpio.c | 196 +++++++----- .../js_app/modules/js_gui/file_picker.c | 8 +- .../system/js_app/modules/js_gui/icon.c | 9 +- .../system/js_app/modules/js_gui/js_gui.c | 90 ++++-- .../system/js_app/modules/js_serial.c | 283 +++++++----------- .../system/js_app/modules/js_storage.c | 187 ++++++++---- .../js_app/plugin_api/app_api_table_i.h | 17 +- .../system/js_app/plugin_api/js_plugin_api.h | 22 -- 13 files changed, 525 insertions(+), 620 deletions(-) delete mode 100644 applications/system/js_app/plugin_api/js_plugin_api.h diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index a8480e6a2..f9c08058f 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -202,12 +202,15 @@ static JsSdkCompatStatus return JsSdkCompatStatusCompatible; } -#define JS_SDK_COMPAT_ARGS \ - int32_t major, minor; \ - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&major), JS_ARG_INT32(&minor)); +static const JsValueDeclaration js_sdk_version_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE(JsValueTypeInt32), +}; +static const JsValueArguments js_sdk_version_args = JS_VALUE_ARGS(js_sdk_version_arg_list); void js_sdk_compatibility_status(struct mjs* mjs) { - JS_SDK_COMPAT_ARGS; + int32_t major, minor; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor); JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); switch(status) { case JsSdkCompatStatusCompatible: @@ -223,7 +226,8 @@ void js_sdk_compatibility_status(struct mjs* mjs) { } void js_is_sdk_compatible(struct mjs* mjs) { - JS_SDK_COMPAT_ARGS; + int32_t major, minor; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor); JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); mjs_return(mjs, mjs_mk_boolean(mjs, status == JsSdkCompatStatusCompatible)); } @@ -246,7 +250,8 @@ static bool js_internal_compat_ask_user(const char* message) { } void js_check_sdk_compatibility(struct mjs* mjs) { - JS_SDK_COMPAT_ARGS; + int32_t major, minor; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_version_args, &major, &minor); JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); if(status != JsSdkCompatStatusCompatible) { FURI_LOG_E( @@ -300,15 +305,20 @@ static bool js_internal_supports_all_of(struct mjs* mjs, mjs_val_t feature_arr) return true; } +static const JsValueDeclaration js_sdk_features_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAnyArray), +}; +static const JsValueArguments js_sdk_features_args = JS_VALUE_ARGS(js_sdk_features_arg_list); + void js_does_sdk_support(struct mjs* mjs) { mjs_val_t features; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_features_args, &features); mjs_return(mjs, mjs_mk_boolean(mjs, js_internal_supports_all_of(mjs, features))); } void js_check_sdk_features(struct mjs* mjs) { mjs_val_t features; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_sdk_features_args, &features); if(!js_internal_supports_all_of(mjs, features)) { FURI_LOG_E(TAG, "Script requests unsupported features"); diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index fb1cca915..c6f72bbe2 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -7,6 +7,10 @@ #include #include +#ifdef __cplusplus +extern "C" { +#endif + #define PLUGIN_APP_ID "js" #define PLUGIN_API_VERSION 1 @@ -64,226 +68,6 @@ typedef enum { JsForeignMagic_JsEventLoopContract, } JsForeignMagic; -// Are you tired of your silly little JS+C glue code functions being 75% -// argument validation code and 25% actual logic? Introducing: ASS (Argument -// Schema for Scripts)! ASS is a set of macros that reduce the typical -// boilerplate code of "check argument count, get arguments, validate arguments, -// extract C values from arguments" down to just one line! - -/** - * When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies - * that the function requires exactly as many arguments as were specified. - */ -#define JS_EXACTLY == -/** - * When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies - * that the function requires at least as many arguments as were specified. - */ -#define JS_AT_LEAST >= - -typedef struct { - const char* name; - size_t value; -} JsEnumMapping; - -#define JS_ENUM_MAP(var_name, ...) \ - static const JsEnumMapping var_name##_mapping[] = { \ - {NULL, sizeof(var_name)}, \ - __VA_ARGS__, \ - {NULL, 0}, \ - }; - -typedef struct { - const char* name; - size_t offset; -} JsObjectMapping; - -#define JS_OBJ_MAP(var_name, ...) \ - static const JsObjectMapping var_name##_mapping[] = { \ - __VA_ARGS__, \ - {NULL, 0}, \ - }; - -typedef struct { - void* out; - int (*validator)(mjs_val_t); - void (*converter)(struct mjs*, mjs_val_t*, void* out, const void* extra); - const char* expected_type; - bool (*extended_validator)(struct mjs*, mjs_val_t, const void* extra); - const void* extra_data; -} _js_arg_decl; - -static inline void _js_to_int32(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - *(int32_t*)out = mjs_get_int32(mjs, *in); -} -#define JS_ARG_INT32(out) ((_js_arg_decl){out, mjs_is_number, _js_to_int32, "number", NULL, NULL}) - -static inline void _js_to_ptr(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - *(void**)out = mjs_get_ptr(mjs, *in); -} -#define JS_ARG_PTR(out) \ - ((_js_arg_decl){out, mjs_is_foreign, _js_to_ptr, "opaque pointer", NULL, NULL}) - -static inline void _js_to_string(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - *(const char**)out = mjs_get_string(mjs, in, NULL); -} -#define JS_ARG_STR(out) ((_js_arg_decl){out, mjs_is_string, _js_to_string, "string", NULL, NULL}) - -static inline void _js_to_bool(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - *(bool*)out = !!mjs_get_bool(mjs, *in); -} -#define JS_ARG_BOOL(out) ((_js_arg_decl){out, mjs_is_boolean, _js_to_bool, "boolean", NULL, NULL}) - -static inline void _js_passthrough(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { - UNUSED(extra); - UNUSED(mjs); - *(mjs_val_t*)out = *in; -} -#define JS_ARG_ANY(out) ((_js_arg_decl){out, NULL, _js_passthrough, "any", NULL, NULL}) -#define JS_ARG_OBJ(out) ((_js_arg_decl){out, mjs_is_object, _js_passthrough, "any", NULL, NULL}) -#define JS_ARG_FN(out) \ - ((_js_arg_decl){out, mjs_is_function, _js_passthrough, "function", NULL, NULL}) -#define JS_ARG_ARR(out) ((_js_arg_decl){out, mjs_is_array, _js_passthrough, "array", NULL, NULL}) - -static inline bool _js_validate_struct(struct mjs* mjs, mjs_val_t val, const void* extra) { - JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra; - JsForeignMagic struct_magic = *(JsForeignMagic*)mjs_get_ptr(mjs, val); - return struct_magic == expected_magic; -} -#define JS_ARG_STRUCT(type, out) \ - ((_js_arg_decl){ \ - out, \ - mjs_is_foreign, \ - _js_to_ptr, \ - #type, \ - _js_validate_struct, \ - (void*)JsForeignMagic##_##type}) - -static inline bool _js_validate_obj_w_struct(struct mjs* mjs, mjs_val_t val, const void* extra) { - JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra; - JsForeignMagic struct_magic = *(JsForeignMagic*)JS_GET_INST(mjs, val); - return struct_magic == expected_magic; -} -#define JS_ARG_OBJ_WITH_STRUCT(type, out) \ - ((_js_arg_decl){ \ - out, \ - mjs_is_object, \ - _js_passthrough, \ - #type, \ - _js_validate_obj_w_struct, \ - (void*)JsForeignMagic##_##type}) - -static inline bool _js_validate_enum(struct mjs* mjs, mjs_val_t val, const void* extra) { - for(const JsEnumMapping* mapping = (JsEnumMapping*)extra + 1; mapping->name; mapping++) - if(strcmp(mapping->name, mjs_get_string(mjs, &val, NULL)) == 0) return true; - return false; -} -static inline void - _js_convert_enum(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) { - const JsEnumMapping* mapping = (JsEnumMapping*)extra; - size_t size = mapping->value; // get enum size from first entry - for(mapping++; mapping->name; mapping++) { - if(strcmp(mapping->name, mjs_get_string(mjs, val, NULL)) == 0) { - if(size == 1) - *(uint8_t*)out = mapping->value; - else if(size == 2) - *(uint16_t*)out = mapping->value; - else if(size == 4) - *(uint32_t*)out = mapping->value; - else if(size == 8) - *(uint64_t*)out = mapping->value; - return; - } - } - // unreachable, thanks to _js_validate_enum -} -#define JS_ARG_ENUM(var_name, name) \ - ((_js_arg_decl){ \ - &var_name, \ - mjs_is_string, \ - _js_convert_enum, \ - name " enum", \ - _js_validate_enum, \ - var_name##_mapping}) - -static inline bool _js_validate_object(struct mjs* mjs, mjs_val_t val, const void* extra) { - for(const JsObjectMapping* mapping = (JsObjectMapping*)extra; mapping->name; mapping++) - if(mjs_get(mjs, val, mapping->name, ~0) == MJS_UNDEFINED) return false; - return true; -} -static inline void - _js_convert_object(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) { - const JsObjectMapping* mapping = (JsObjectMapping*)extra; - for(; mapping->name; mapping++) { - mjs_val_t field_val = mjs_get(mjs, *val, mapping->name, ~0); - *(mjs_val_t*)((uint8_t*)out + mapping->offset) = field_val; - } -} -#define JS_ARG_OBJECT(var_name, name) \ - ((_js_arg_decl){ \ - &var_name, \ - mjs_is_object, \ - _js_convert_object, \ - name " object", \ - _js_validate_object, \ - var_name##_mapping}) - -/** - * @brief Validates and converts a JS value with a declarative interface - * - * Example: `int32_t my_value; JS_CONVERT_OR_RETURN(mjs, &mjs_val, JS_ARG_INT32(&my_value), "value source");` - * - * @warning This macro executes `return;` by design in case of a validation failure - */ -#define JS_CONVERT_OR_RETURN(mjs, value, decl, source, ...) \ - if(decl.validator) \ - if(!decl.validator(*value)) \ - JS_ERROR_AND_RETURN( \ - mjs, \ - MJS_BAD_ARGS_ERROR, \ - source ": expected %s", \ - ##__VA_ARGS__, \ - decl.expected_type); \ - if(decl.extended_validator) \ - if(!decl.extended_validator(mjs, *value, decl.extra_data)) \ - JS_ERROR_AND_RETURN( \ - mjs, \ - MJS_BAD_ARGS_ERROR, \ - source ": expected %s", \ - ##__VA_ARGS__, \ - decl.expected_type); \ - decl.converter(mjs, value, decl.out, decl.extra_data); - -//-V:JS_FETCH_ARGS_OR_RETURN:1008 -/** - * @brief Fetches and validates the arguments passed to a JS function - * - * Example: `int32_t my_arg; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&my_arg));` - * - * @warning This macro executes `return;` by design in case of an argument count - * mismatch or a validation failure - */ -#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \ - _js_arg_decl _js_args[] = {__VA_ARGS__}; \ - int _js_arg_cnt = COUNT_OF(_js_args); \ - mjs_val_t _js_arg_vals[_js_arg_cnt]; \ - if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \ - JS_ERROR_AND_RETURN( \ - mjs, \ - MJS_BAD_ARGS_ERROR, \ - "expected %s%d arguments, got %d", \ - #arg_operator, \ - _js_arg_cnt, \ - mjs_nargs(mjs)); \ - for(int _i = 0; _i < _js_arg_cnt; _i++) { \ - _js_arg_vals[_i] = mjs_arg(mjs, _i); \ - JS_CONVERT_OR_RETURN(mjs, &_js_arg_vals[_i], _js_args[_i], "argument %d", _i); \ - } - /** * @brief Prepends an error, sets the JS return value to `undefined` and returns * from the C function @@ -358,3 +142,7 @@ void js_does_sdk_support(struct mjs* mjs); * @brief `checkSdkFeatures` function */ void js_check_sdk_features(struct mjs* mjs); + +#ifdef __cplusplus +} +#endif diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 4a6d23011..a41a28d11 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -198,18 +198,15 @@ static void js_require(struct mjs* mjs) { } static void js_parse_int(struct mjs* mjs) { - const char* str; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_STR(&str)); + static const JsValueDeclaration js_parse_int_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 10), + }; + static const JsValueArguments js_parse_int_args = JS_VALUE_ARGS(js_parse_int_arg_list); - int32_t base = 10; - if(mjs_nargs(mjs) >= 2) { - mjs_val_t base_arg = mjs_arg(mjs, 1); - if(!mjs_is_number(base_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base must be a number"); - mjs_return(mjs, MJS_UNDEFINED); - } - base = mjs_get_int(mjs, base_arg); - } + const char* str; + int32_t base; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_parse_int_args, &str, &base); int32_t num; if(strint_to_int32(str, NULL, &num, base) != StrintParseNoError) { diff --git a/applications/system/js_app/js_thread_i.h b/applications/system/js_app/js_thread_i.h index a73cbb4bc..5fbdb06d0 100644 --- a/applications/system/js_app/js_thread_i.h +++ b/applications/system/js_app/js_thread_i.h @@ -11,6 +11,10 @@ #include #include +#ifdef __cplusplus +extern "C" { +#endif + #define INST_PROP_NAME "_" typedef enum { @@ -23,3 +27,7 @@ bool js_delay_with_flags(struct mjs* mjs, uint32_t time); void js_flags_set(struct mjs* mjs, uint32_t flags); uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout); + +#ifdef __cplusplus +} +#endif diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop.c b/applications/system/js_app/modules/js_event_loop/js_event_loop.c index 625301ad1..1aded8de4 100644 --- a/applications/system/js_app/modules/js_event_loop/js_event_loop.c +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop.c @@ -144,10 +144,16 @@ static void js_event_loop_subscribe(struct mjs* mjs) { JsEventLoop* module = JS_GET_CONTEXT(mjs); // get arguments + static const JsValueDeclaration js_loop_subscribe_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeRawPointer), + JS_VALUE_SIMPLE(JsValueTypeFunction), + }; + static const JsValueArguments js_loop_subscribe_args = + JS_VALUE_ARGS(js_loop_subscribe_arg_list); + JsEventLoopContract* contract; mjs_val_t callback; - JS_FETCH_ARGS_OR_RETURN( - mjs, JS_AT_LEAST, JS_ARG_STRUCT(JsEventLoopContract, &contract), JS_ARG_FN(&callback)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_subscribe_args, &contract, &callback); // create subscription object JsEventLoopSubscription* subscription = malloc(sizeof(JsEventLoopSubscription)); @@ -242,20 +248,22 @@ static void js_event_loop_stop(struct mjs* mjs) { * event */ static void js_event_loop_timer(struct mjs* mjs) { - // get arguments - const char* mode_str; - int32_t interval; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&mode_str), JS_ARG_INT32(&interval)); - JsEventLoop* module = JS_GET_CONTEXT(mjs); + static const JsValueEnumVariant js_loop_timer_mode_variants[] = { + {"periodic", FuriEventLoopTimerTypePeriodic}, + {"oneshot", FuriEventLoopTimerTypeOnce}, + }; + + static const JsValueDeclaration js_loop_timer_arg_list[] = { + JS_VALUE_ENUM(FuriEventLoopTimerType, js_loop_timer_mode_variants), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_loop_timer_args = JS_VALUE_ARGS(js_loop_timer_arg_list); FuriEventLoopTimerType mode; - if(strcasecmp(mode_str, "periodic") == 0) { - mode = FuriEventLoopTimerTypePeriodic; - } else if(strcasecmp(mode_str, "oneshot") == 0) { - mode = FuriEventLoopTimerTypeOnce; - } else { - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: unknown mode"); - } + int32_t interval; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_timer_args, &mode, &interval); + + JsEventLoop* module = JS_GET_CONTEXT(mjs); // make timer contract JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract)); @@ -293,8 +301,14 @@ static mjs_val_t */ static void js_event_loop_queue_send(struct mjs* mjs) { // get arguments + static const JsValueDeclaration js_loop_q_send_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_loop_q_send_args = JS_VALUE_ARGS(js_loop_q_send_arg_list); + mjs_val_t message; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&message)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_q_send_args, &message); + JsEventLoopContract* contract = JS_GET_CONTEXT(mjs); // send message @@ -311,8 +325,14 @@ static void js_event_loop_queue_send(struct mjs* mjs) { */ static void js_event_loop_queue(struct mjs* mjs) { // get arguments + static const JsValueDeclaration js_loop_q_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_loop_q_args = JS_VALUE_ARGS(js_loop_q_arg_list); + int32_t length; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&length)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_loop_q_args, &length); + JsEventLoop* module = JS_GET_CONTEXT(mjs); // make queue contract diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index 2a559570f..63de6900a 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -54,83 +54,114 @@ static void js_gpio_int_cb(void* arg) { * ``` */ static void js_gpio_init(struct mjs* mjs) { - // deconstruct mode object - mjs_val_t mode_arg; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&mode_arg)); - mjs_val_t direction_arg = mjs_get(mjs, mode_arg, "direction", ~0); - mjs_val_t out_mode_arg = mjs_get(mjs, mode_arg, "outMode", ~0); - mjs_val_t in_mode_arg = mjs_get(mjs, mode_arg, "inMode", ~0); - mjs_val_t edge_arg = mjs_get(mjs, mode_arg, "edge", ~0); - mjs_val_t pull_arg = mjs_get(mjs, mode_arg, "pull", ~0); + // direction variants + typedef enum { + JsGpioDirectionIn, + JsGpioDirectionOut, + } JsGpioDirection; + static const JsValueEnumVariant js_gpio_direction_variants[] = { + {"in", JsGpioDirectionIn}, + {"out", JsGpioDirectionOut}, + }; + static const JsValueDeclaration js_gpio_direction = + JS_VALUE_ENUM(JsGpioDirection, js_gpio_direction_variants); - // get strings - const char* direction = mjs_get_string(mjs, &direction_arg, NULL); - const char* out_mode = mjs_get_string(mjs, &out_mode_arg, NULL); - const char* in_mode = mjs_get_string(mjs, &in_mode_arg, NULL); - const char* edge = mjs_get_string(mjs, &edge_arg, NULL); - const char* pull = mjs_get_string(mjs, &pull_arg, NULL); - if(!direction) - JS_ERROR_AND_RETURN( - mjs, MJS_BAD_ARGS_ERROR, "Expected string in \"direction\" field of mode object"); - if(!out_mode) out_mode = "open_drain"; - if(!in_mode) in_mode = "plain_digital"; - if(!edge) edge = "rising"; + // inMode variants + typedef enum { + JsGpioInModeAnalog = (0 << 0), + JsGpioInModePlainDigital = (1 << 0), + JsGpioInModeInterrupt = (2 << 0), + JsGpioInModeEvent = (3 << 0), + } JsGpioInMode; + static const JsValueEnumVariant js_gpio_in_mode_variants[] = { + {"analog", JsGpioInModeAnalog}, + {"plain_digital", JsGpioInModePlainDigital}, + {"interrupt", JsGpioInModeInterrupt}, + {"event", JsGpioInModeEvent}, + }; + static const JsValueDeclaration js_gpio_in_mode = + JS_VALUE_ENUM_W_DEFAULT(JsGpioInMode, js_gpio_in_mode_variants, JsGpioInModePlainDigital); + + // outMode variants + typedef enum { + JsGpioOutModePushPull, + JsGpioOutModeOpenDrain, + } JsGpioOutMode; + static const JsValueEnumVariant js_gpio_out_mode_variants[] = { + {"push_pull", JsGpioOutModePushPull}, + {"open_drain", JsGpioOutModeOpenDrain}, + }; + static const JsValueDeclaration js_gpio_out_mode = + JS_VALUE_ENUM_W_DEFAULT(JsGpioOutMode, js_gpio_out_mode_variants, JsGpioOutModeOpenDrain); + + // edge variants + typedef enum { + JsGpioEdgeRising = (0 << 2), + JsGpioEdgeFalling = (1 << 2), + JsGpioEdgeBoth = (2 << 2), + } JsGpioEdge; + static const JsValueEnumVariant js_gpio_edge_variants[] = { + {"rising", JsGpioEdgeRising}, + {"falling", JsGpioEdgeFalling}, + {"both", JsGpioEdgeBoth}, + }; + static const JsValueDeclaration js_gpio_edge = + JS_VALUE_ENUM_W_DEFAULT(JsGpioEdge, js_gpio_edge_variants, JsGpioEdgeRising); + + // pull variants + static const JsValueEnumVariant js_gpio_pull_variants[] = { + {"up", GpioPullUp}, + {"down", GpioPullDown}, + }; + static const JsValueDeclaration js_gpio_pull = + JS_VALUE_ENUM_W_DEFAULT(GpioPull, js_gpio_pull_variants, GpioPullNo); + + // complete mode object + static const JsValueObjectField js_gpio_mode_object_fields[] = { + {"direction", &js_gpio_direction}, + {"inMode", &js_gpio_in_mode}, + {"outMode", &js_gpio_out_mode}, + {"edge", &js_gpio_edge}, + {"pull", &js_gpio_pull}, + }; + + // function args + static const JsValueDeclaration js_gpio_init_arg_list[] = { + JS_VALUE_OBJECT_W_DEFAULTS(js_gpio_mode_object_fields), + }; + static const JsValueArguments js_gpio_init_args = JS_VALUE_ARGS(js_gpio_init_arg_list); + + JsGpioDirection direction; + JsGpioInMode in_mode; + JsGpioOutMode out_mode; + JsGpioEdge edge; + GpioPull pull; + JS_VALUE_PARSE_ARGS_OR_RETURN( + mjs, &js_gpio_init_args, &direction, &in_mode, &out_mode, &edge, &pull); - // convert strings to mode GpioMode mode; - if(strcmp(direction, "out") == 0) { - if(strcmp(out_mode, "push_pull") == 0) - mode = GpioModeOutputPushPull; - else if(strcmp(out_mode, "open_drain") == 0) - mode = GpioModeOutputOpenDrain; - else - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid outMode"); - } else if(strcmp(direction, "in") == 0) { - if(strcmp(in_mode, "analog") == 0) { - mode = GpioModeAnalog; - } else if(strcmp(in_mode, "plain_digital") == 0) { - mode = GpioModeInput; - } else if(strcmp(in_mode, "interrupt") == 0) { - if(strcmp(edge, "rising") == 0) - mode = GpioModeInterruptRise; - else if(strcmp(edge, "falling") == 0) - mode = GpioModeInterruptFall; - else if(strcmp(edge, "both") == 0) - mode = GpioModeInterruptRiseFall; - else - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge"); - } else if(strcmp(in_mode, "event") == 0) { - if(strcmp(edge, "rising") == 0) - mode = GpioModeEventRise; - else if(strcmp(edge, "falling") == 0) - mode = GpioModeEventFall; - else if(strcmp(edge, "both") == 0) - mode = GpioModeEventRiseFall; - else - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge"); - } else { - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid inMode"); - } + if(direction == JsGpioDirectionOut) { + static const GpioMode js_gpio_out_mode_lut[] = { + [JsGpioOutModePushPull] = GpioModeOutputPushPull, + [JsGpioOutModeOpenDrain] = GpioModeOutputOpenDrain, + }; + mode = js_gpio_out_mode_lut[out_mode]; } else { - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid direction"); + static const GpioMode js_gpio_in_mode_lut[] = { + [JsGpioInModeAnalog] = GpioModeAnalog, + [JsGpioInModePlainDigital] = GpioModeInput, + [JsGpioInModeInterrupt | JsGpioEdgeRising] = GpioModeInterruptRise, + [JsGpioInModeInterrupt | JsGpioEdgeFalling] = GpioModeInterruptFall, + [JsGpioInModeInterrupt | JsGpioEdgeBoth] = GpioModeInterruptRiseFall, + [JsGpioInModeEvent | JsGpioEdgeRising] = GpioModeEventRise, + [JsGpioInModeEvent | JsGpioEdgeFalling] = GpioModeEventFall, + [JsGpioInModeEvent | JsGpioEdgeBoth] = GpioModeEventRiseFall, + }; + mode = js_gpio_in_mode_lut[in_mode | edge]; } - // convert pull - GpioPull pull_mode; - if(!pull) { - pull_mode = GpioPullNo; - } else if(strcmp(pull, "up") == 0) { - pull_mode = GpioPullUp; - } else if(strcmp(pull, "down") == 0) { - pull_mode = GpioPullDown; - } else { - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid pull"); - } - - // init GPIO JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); - furi_hal_gpio_init(manager_data->pin, mode, pull_mode, GpioSpeedVeryHigh); - mjs_return(mjs, MJS_UNDEFINED); + furi_hal_gpio_init(manager_data->pin, mode, pull, GpioSpeedVeryHigh); } /** @@ -146,8 +177,13 @@ static void js_gpio_init(struct mjs* mjs) { * ``` */ static void js_gpio_write(struct mjs* mjs) { + static const JsValueDeclaration js_gpio_write_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeBool), + }; + static const JsValueArguments js_gpio_write_args = JS_VALUE_ARGS(js_gpio_write_arg_list); bool level; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_BOOL(&level)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_write_args, &level); + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); furi_hal_gpio_write(manager_data->pin, level); mjs_return(mjs, MJS_UNDEFINED); @@ -261,9 +297,16 @@ static void js_gpio_is_pwm_supported(struct mjs* mjs) { * ``` */ static void js_gpio_pwm_write(struct mjs* mjs) { - JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + static const JsValueDeclaration js_gpio_pwm_write_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_gpio_pwm_write_args = + JS_VALUE_ARGS(js_gpio_pwm_write_arg_list); int32_t frequency, duty; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&frequency), JS_ARG_INT32(&duty)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_pwm_write_args, &frequency, &duty); + + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); if(manager_data->pwm_output == FuriHalPwmOutputIdNone) { JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin"); } @@ -326,8 +369,13 @@ static void js_gpio_pwm_stop(struct mjs* mjs) { * ``` */ static void js_gpio_get(struct mjs* mjs) { + static const JsValueDeclaration js_gpio_get_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gpio_get_args = JS_VALUE_ARGS(js_gpio_get_arg_list); mjs_val_t name_arg; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&name_arg)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gpio_get_args, &name_arg); + const char* name_string = mjs_get_string(mjs, &name_arg, NULL); const GpioPinRecord* pin_record = NULL; diff --git a/applications/system/js_app/modules/js_gui/file_picker.c b/applications/system/js_app/modules/js_gui/file_picker.c index 49cf5e89d..7b36596cd 100644 --- a/applications/system/js_app/modules/js_gui/file_picker.c +++ b/applications/system/js_app/modules/js_gui/file_picker.c @@ -3,8 +3,14 @@ #include static void js_gui_file_picker_pick_file(struct mjs* mjs) { + static const JsValueDeclaration js_picker_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeString), + }; + static const JsValueArguments js_picker_args = JS_VALUE_ARGS(js_picker_arg_list); + const char *base_path, *extension; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&base_path), JS_ARG_STR(&extension)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_picker_args, &base_path, &extension); DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); const DialogsFileBrowserOptions browser_options = { diff --git a/applications/system/js_app/modules/js_gui/icon.c b/applications/system/js_app/modules/js_gui/icon.c index 3d8a67a8b..4fc6da2e0 100644 --- a/applications/system/js_app/modules/js_gui/icon.c +++ b/applications/system/js_app/modules/js_gui/icon.c @@ -39,9 +39,14 @@ typedef struct { FxbmIconWrapperList_t fxbm_list; } JsGuiIconInst; +static const JsValueDeclaration js_icon_get_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), +}; +static const JsValueArguments js_icon_get_args = JS_VALUE_ARGS(js_icon_get_arg_list); + static void js_gui_icon_get_builtin(struct mjs* mjs) { const char* icon_name; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&icon_name)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_icon_get_args, &icon_name); for(size_t i = 0; i < COUNT_OF(builtin_icons); i++) { if(strcmp(icon_name, builtin_icons[i].name) == 0) { @@ -55,7 +60,7 @@ static void js_gui_icon_get_builtin(struct mjs* mjs) { static void js_gui_icon_load_fxbm(struct mjs* mjs) { const char* fxbm_path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fxbm_path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_icon_get_args, &fxbm_path); Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c index e505681df..c20d980aa 100644 --- a/applications/system/js_app/modules/js_gui/js_gui.c +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -68,8 +68,14 @@ static bool js_gui_vd_nav_callback(void* context) { * @brief `viewDispatcher.sendCustom` */ static void js_gui_vd_send_custom(struct mjs* mjs) { + static const JsValueDeclaration js_gui_vd_send_custom_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_gui_vd_send_custom_args = + JS_VALUE_ARGS(js_gui_vd_send_custom_arg_list); + int32_t event; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&event)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vd_send_custom_args, &event); JsGui* module = JS_GET_CONTEXT(mjs); view_dispatcher_send_custom_event(module->dispatcher, (uint32_t)event); @@ -79,15 +85,25 @@ static void js_gui_vd_send_custom(struct mjs* mjs) { * @brief `viewDispatcher.sendTo` */ static void js_gui_vd_send_to(struct mjs* mjs) { - enum { - SendDirToFront, - SendDirToBack, - } send_direction; - JS_ENUM_MAP(send_direction, {"front", SendDirToFront}, {"back", SendDirToBack}); - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ENUM(send_direction, "SendDirection")); + typedef enum { + JsSendDirToFront, + JsSendDirToBack, + } JsSendDir; + static const JsValueEnumVariant js_send_dir_variants[] = { + {"front", JsSendDirToFront}, + {"back", JsSendDirToBack}, + }; + static const JsValueDeclaration js_gui_vd_send_to_arg_list[] = { + JS_VALUE_ENUM(JsSendDir, js_send_dir_variants), + }; + static const JsValueArguments js_gui_vd_send_to_args = + JS_VALUE_ARGS(js_gui_vd_send_to_arg_list); + + JsSendDir send_direction; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vd_send_to_args, &send_direction); JsGui* module = JS_GET_CONTEXT(mjs); - if(send_direction == SendDirToBack) { + if(send_direction == JsSendDirToBack) { view_dispatcher_send_to_back(module->dispatcher); } else { view_dispatcher_send_to_front(module->dispatcher); @@ -98,8 +114,15 @@ static void js_gui_vd_send_to(struct mjs* mjs) { * @brief `viewDispatcher.switchTo` */ static void js_gui_vd_switch_to(struct mjs* mjs) { + static const JsValueDeclaration js_gui_vd_switch_to_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gui_vd_switch_to_args = + JS_VALUE_ARGS(js_gui_vd_switch_to_arg_list); + mjs_val_t view; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vd_switch_to_args, &view); + JsGuiViewData* view_data = JS_GET_INST(mjs, view); mjs_val_t vd_obj = mjs_get_this(mjs); JsGui* module = JS_GET_INST(mjs, vd_obj); @@ -267,9 +290,16 @@ static bool * @brief `View.set` */ static void js_gui_view_set(struct mjs* mjs) { + static const JsValueDeclaration js_gui_view_set_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gui_view_set_args = JS_VALUE_ARGS(js_gui_view_set_arg_list); + const char* name; mjs_val_t value; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name), JS_ARG_ANY(&value)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_view_set_args, &name, &value); + JsGuiViewData* data = JS_GET_CONTEXT(mjs); bool success = js_gui_view_assign(mjs, name, value, data); UNUSED(success); @@ -280,12 +310,19 @@ static void js_gui_view_set(struct mjs* mjs) { * @brief `View.addChild` */ static void js_gui_view_add_child(struct mjs* mjs) { + static const JsValueDeclaration js_gui_view_add_child_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gui_view_add_child_args = + JS_VALUE_ARGS(js_gui_view_add_child_arg_list); + + mjs_val_t child; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_view_add_child_args, &child); + JsGuiViewData* data = JS_GET_CONTEXT(mjs); if(!data->descriptor->add_child || !data->descriptor->reset_children) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); - mjs_val_t child; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&child)); bool success = data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child); UNUSED(success); mjs_return(mjs, MJS_UNDEFINED); @@ -307,12 +344,19 @@ static void js_gui_view_reset_children(struct mjs* mjs) { * @brief `View.setChildren` */ static void js_gui_view_set_children(struct mjs* mjs) { + static const JsValueDeclaration js_gui_view_set_children_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAnyArray), + }; + static const JsValueArguments js_gui_view_set_children_args = + JS_VALUE_ARGS(js_gui_view_set_children_arg_list); + + mjs_val_t children; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_view_set_children_args, &children); + JsGuiViewData* data = JS_GET_CONTEXT(mjs); if(!data->descriptor->add_child || !data->descriptor->reset_children) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); - mjs_val_t children; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&children)); js_gui_view_internal_set_children(mjs, children, data); } @@ -365,7 +409,6 @@ static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descr * @brief `ViewFactory.make` */ static void js_gui_vf_make(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs); mjs_return(mjs, js_gui_make_view(mjs, descriptor)); } @@ -374,8 +417,15 @@ static void js_gui_vf_make(struct mjs* mjs) { * @brief `ViewFactory.makeWith` */ static void js_gui_vf_make_with(struct mjs* mjs) { - mjs_val_t props; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_OBJ(&props)); + static const JsValueDeclaration js_gui_vf_make_with_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAnyObject), + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_gui_vf_make_with_args = + JS_VALUE_ARGS(js_gui_vf_make_with_arg_list); + + mjs_val_t props, children; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_gui_vf_make_with_args, &props, &children); const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs); // make the object like normal @@ -396,14 +446,10 @@ static void js_gui_vf_make_with(struct mjs* mjs) { } // assign children - if(mjs_nargs(mjs) >= 2) { + if(mjs_is_array(children)) { if(!data->descriptor->add_child || !data->descriptor->reset_children) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children"); - mjs_val_t children = mjs_arg(mjs, 1); - if(!mjs_is_array(children)) - JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 1: expected array"); - if(!js_gui_view_internal_set_children(mjs, children, data)) return; } diff --git a/applications/system/js_app/modules/js_serial.c b/applications/system/js_app/modules/js_serial.c index 20b18a4f1..d903939ce 100644 --- a/applications/system/js_app/modules/js_serial.c +++ b/applications/system/js_app/modules/js_serial.c @@ -35,60 +35,62 @@ static void } static void js_serial_setup(struct mjs* mjs) { + static const JsValueEnumVariant js_serial_id_variants[] = { + {"lpuart", FuriHalSerialIdLpuart}, + {"usart", FuriHalSerialIdUsart}, + }; + + static const JsValueEnumVariant js_serial_data_bit_variants[] = { + {"6", FuriHalSerialDataBits6}, + {"7", FuriHalSerialDataBits7}, + {"8", FuriHalSerialDataBits8}, + {"9", FuriHalSerialDataBits9}, + }; + static const JsValueDeclaration js_serial_data_bits = JS_VALUE_ENUM_W_DEFAULT( + FuriHalSerialDataBits, js_serial_data_bit_variants, FuriHalSerialDataBits8); + + static const JsValueEnumVariant js_serial_parity_variants[] = { + {"none", FuriHalSerialParityNone}, + {"even", FuriHalSerialParityEven}, + {"odd", FuriHalSerialParityOdd}, + }; + static const JsValueDeclaration js_serial_parity = JS_VALUE_ENUM_W_DEFAULT( + FuriHalSerialParity, js_serial_parity_variants, FuriHalSerialParityNone); + + static const JsValueEnumVariant js_serial_stop_bit_variants[] = { + {"0.5", FuriHalSerialStopBits0_5}, + {"1", FuriHalSerialStopBits1}, + {"1.5", FuriHalSerialStopBits1_5}, + {"2", FuriHalSerialStopBits2}, + }; + static const JsValueDeclaration js_serial_stop_bits = JS_VALUE_ENUM_W_DEFAULT( + FuriHalSerialStopBits, js_serial_stop_bit_variants, FuriHalSerialStopBits1); + + static const JsValueObjectField js_serial_framing_fields[] = { + {"dataBits", &js_serial_data_bits}, + {"parity", &js_serial_parity}, + {"stopBits", &js_serial_stop_bits}, + }; + + static const JsValueDeclaration js_serial_setup_arg_list[] = { + JS_VALUE_ENUM(FuriHalSerialId, js_serial_id_variants), + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_OBJECT_W_DEFAULTS(js_serial_framing_fields), + }; + static const JsValueArguments js_serial_setup_args = JS_VALUE_ARGS(js_serial_setup_arg_list); + FuriHalSerialId serial_id; int32_t baudrate; - JS_ENUM_MAP(serial_id, {"lpuart", FuriHalSerialIdLpuart}, {"usart", FuriHalSerialIdUsart}); - JS_FETCH_ARGS_OR_RETURN( - mjs, JS_AT_LEAST, JS_ARG_ENUM(serial_id, "SerialId"), JS_ARG_INT32(&baudrate)); - FuriHalSerialDataBits data_bits = FuriHalSerialDataBits8; FuriHalSerialParity parity = FuriHalSerialParityNone; FuriHalSerialStopBits stop_bits = FuriHalSerialStopBits1; - if(mjs_nargs(mjs) > 2) { - struct framing { - mjs_val_t data_bits; - mjs_val_t parity; - mjs_val_t stop_bits; - } framing; - JS_OBJ_MAP( - framing, - {"dataBits", offsetof(struct framing, data_bits)}, - {"parity", offsetof(struct framing, parity)}, - {"stopBits", offsetof(struct framing, stop_bits)}); - JS_ENUM_MAP( - data_bits, - {"6", FuriHalSerialDataBits6}, - {"7", FuriHalSerialDataBits7}, - {"8", FuriHalSerialDataBits8}, - {"9", FuriHalSerialDataBits9}); - JS_ENUM_MAP( - parity, - {"none", FuriHalSerialParityNone}, - {"even", FuriHalSerialParityEven}, - {"odd", FuriHalSerialParityOdd}); - JS_ENUM_MAP( - stop_bits, - {"0.5", FuriHalSerialStopBits0_5}, - {"1", FuriHalSerialStopBits1}, - {"1.5", FuriHalSerialStopBits1_5}, - {"2", FuriHalSerialStopBits2}); - mjs_val_t framing_obj = mjs_arg(mjs, 2); - JS_CONVERT_OR_RETURN(mjs, &framing_obj, JS_ARG_OBJECT(framing, "Framing"), "argument 2"); - JS_CONVERT_OR_RETURN( - mjs, &framing.data_bits, JS_ARG_ENUM(data_bits, "DataBits"), "argument 2: dataBits"); - JS_CONVERT_OR_RETURN( - mjs, &framing.parity, JS_ARG_ENUM(parity, "Parity"), "argument 2: parity"); - JS_CONVERT_OR_RETURN( - mjs, &framing.stop_bits, JS_ARG_ENUM(stop_bits, "StopBits"), "argument 2: stopBits"); - } + JS_VALUE_PARSE_ARGS_OR_RETURN( + mjs, &js_serial_setup_args, &serial_id, &baudrate, &data_bits, &parity, &stop_bits); JsSerialInst* serial = JS_GET_CONTEXT(mjs); - if(serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is already configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is already configured"); expansion_disable(furi_record_open(RECORD_EXPANSION)); furi_record_close(RECORD_EXPANSION); @@ -123,28 +125,20 @@ static void js_serial_deinit(JsSerialInst* js_serial) { } static void js_serial_end(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); js_serial_deinit(serial); } static void js_serial_write(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); bool args_correct = true; @@ -228,43 +222,20 @@ static size_t js_serial_receive(JsSerialInst* serial, char* buf, size_t len, uin return bytes_read; } +static const JsValueDeclaration js_serial_read_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, INT32_MAX), +}; +static const JsValueArguments js_serial_read_args = JS_VALUE_ARGS(js_serial_read_arg_list); + static void js_serial_read(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - size_t read_len = 0; - uint32_t timeout = FuriWaitForever; - - do { - size_t num_args = mjs_nargs(mjs); - if(num_args == 1) { - mjs_val_t arg = mjs_arg(mjs, 0); - if(!mjs_is_number(arg)) { - break; - } - read_len = mjs_get_int32(mjs, arg); - } else if(num_args == 2) { - mjs_val_t len_arg = mjs_arg(mjs, 0); - mjs_val_t timeout_arg = mjs_arg(mjs, 1); - if((!mjs_is_number(len_arg)) || (!mjs_is_number(timeout_arg))) { - break; - } - read_len = mjs_get_int32(mjs, len_arg); - timeout = mjs_get_int32(mjs, timeout_arg); - } - } while(0); - - if(read_len == 0) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + int32_t read_len, timeout; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_read_args, &read_len, &timeout); char* read_buf = malloc(read_len); size_t bytes_read = js_serial_receive(serial, read_buf, read_len, timeout); @@ -278,37 +249,19 @@ static void js_serial_read(struct mjs* mjs) { } static void js_serial_readln(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - bool args_correct = false; - uint32_t timeout = FuriWaitForever; + static const JsValueDeclaration js_serial_readln_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_serial_readln_args = JS_VALUE_ARGS(js_serial_readln_arg_list); - do { - size_t num_args = mjs_nargs(mjs); - if(num_args > 1) { - break; - } else if(num_args == 1) { - mjs_val_t arg = mjs_arg(mjs, 0); - if(!mjs_is_number(arg)) { - break; - } - timeout = mjs_get_int32(mjs, arg); - } - args_correct = true; - } while(0); + int32_t timeout; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_readln_args, &timeout); - if(!args_correct) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); - mjs_return(mjs, MJS_UNDEFINED); - return; - } FuriString* rx_buf = furi_string_alloc(); size_t bytes_read = 0; char read_char = 0; @@ -335,42 +288,13 @@ static void js_serial_readln(struct mjs* mjs) { } static void js_serial_read_bytes(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - size_t read_len = 0; - uint32_t timeout = FuriWaitForever; - - do { - size_t num_args = mjs_nargs(mjs); - if(num_args == 1) { - mjs_val_t arg = mjs_arg(mjs, 0); - if(!mjs_is_number(arg)) { - break; - } - read_len = mjs_get_int32(mjs, arg); - } else if(num_args == 2) { - mjs_val_t len_arg = mjs_arg(mjs, 0); - mjs_val_t timeout_arg = mjs_arg(mjs, 1); - if((!mjs_is_number(len_arg)) || (!mjs_is_number(timeout_arg))) { - break; - } - read_len = mjs_get_int32(mjs, len_arg); - timeout = mjs_get_int32(mjs, timeout_arg); - } - } while(0); - - if(read_len == 0) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + int32_t read_len, timeout; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_read_args, &read_len, &timeout); char* read_buf = malloc(read_len); size_t bytes_read = js_serial_receive(serial, read_buf, read_len, timeout); @@ -399,27 +323,19 @@ static char* js_serial_receive_any(JsSerialInst* serial, size_t* len, uint32_t t } static void js_serial_read_any(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSerialInst* serial = mjs_get_ptr(mjs, obj_inst); + JsSerialInst* serial = JS_GET_CONTEXT(mjs); furi_assert(serial); - if(!serial->setup_done) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!serial->setup_done) + JS_ERROR_AND_RETURN(mjs, MJS_INTERNAL_ERROR, "Serial is not configured"); - uint32_t timeout = FuriWaitForever; + static const JsValueDeclaration js_serial_read_any_arg_list[] = { + JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, INT32_MAX), + }; + static const JsValueArguments js_serial_read_any_args = + JS_VALUE_ARGS(js_serial_read_any_arg_list); - do { - size_t num_args = mjs_nargs(mjs); - if(num_args == 1) { - mjs_val_t timeout_arg = mjs_arg(mjs, 0); - if(!mjs_is_number(timeout_arg)) { - break; - } - timeout = mjs_get_int32(mjs, timeout_arg); - } - } while(0); + int32_t timeout; + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_serial_read_any_args, &timeout); size_t bytes_read = 0; char* read_buf = js_serial_receive_any(serial, &bytes_read, timeout); @@ -663,16 +579,19 @@ static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* mod UNUSED(modules); JsSerialInst* js_serial = malloc(sizeof(JsSerialInst)); js_serial->mjs = mjs; + mjs_val_t serial_obj = mjs_mk_object(mjs); - mjs_set(mjs, serial_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, js_serial)); - mjs_set(mjs, serial_obj, "setup", ~0, MJS_MK_FN(js_serial_setup)); - mjs_set(mjs, serial_obj, "end", ~0, MJS_MK_FN(js_serial_end)); - mjs_set(mjs, serial_obj, "write", ~0, MJS_MK_FN(js_serial_write)); - mjs_set(mjs, serial_obj, "read", ~0, MJS_MK_FN(js_serial_read)); - mjs_set(mjs, serial_obj, "readln", ~0, MJS_MK_FN(js_serial_readln)); - mjs_set(mjs, serial_obj, "readBytes", ~0, MJS_MK_FN(js_serial_read_bytes)); - mjs_set(mjs, serial_obj, "readAny", ~0, MJS_MK_FN(js_serial_read_any)); - mjs_set(mjs, serial_obj, "expect", ~0, MJS_MK_FN(js_serial_expect)); + JS_ASSIGN_MULTI(mjs, serial_obj) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, js_serial)); + JS_FIELD("setup", MJS_MK_FN(js_serial_setup)); + JS_FIELD("end", MJS_MK_FN(js_serial_end)); + JS_FIELD("write", MJS_MK_FN(js_serial_write)); + JS_FIELD("read", MJS_MK_FN(js_serial_read)); + JS_FIELD("readln", MJS_MK_FN(js_serial_readln)); + JS_FIELD("readBytes", MJS_MK_FN(js_serial_read_bytes)); + JS_FIELD("readAny", MJS_MK_FN(js_serial_read_any)); + JS_FIELD("expect", MJS_MK_FN(js_serial_expect)); + } *object = serial_obj; return js_serial; diff --git a/applications/system/js_app/modules/js_storage.c b/applications/system/js_app/modules/js_storage.c index 1d4053a5f..66d002f33 100644 --- a/applications/system/js_app/modules/js_storage.c +++ b/applications/system/js_app/modules/js_storage.c @@ -1,42 +1,79 @@ #include "../js_modules.h" // IWYU pragma: keep #include -// ---=== file ops ===--- +// ========================== +// Common argument signatures +// ========================== + +static const JsValueDeclaration js_storage_1_int_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeInt32), +}; +static const JsValueArguments js_storage_1_int_args = JS_VALUE_ARGS(js_storage_1_int_arg_list); + +static const JsValueDeclaration js_storage_1_str_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), +}; +static const JsValueArguments js_storage_1_str_args = JS_VALUE_ARGS(js_storage_1_str_arg_list); + +static const JsValueDeclaration js_storage_2_str_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeString), +}; +static const JsValueArguments js_storage_2_str_args = JS_VALUE_ARGS(js_storage_2_str_arg_list); + +// ====================== +// File object operations +// ====================== static void js_storage_file_close(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_close(file))); } static void js_storage_file_is_open(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_is_open(file))); } static void js_storage_file_read(struct mjs* mjs) { - enum { - ReadModeAscii, - ReadModeBinary, - } read_mode; - JS_ENUM_MAP(read_mode, {"ascii", ReadModeAscii}, {"binary", ReadModeBinary}); + typedef enum { + JsStorageReadModeAscii, + JsStorageReadModeBinary, + } JsStorageReadMode; + static const JsValueEnumVariant js_storage_read_mode_variants[] = { + {"ascii", JsStorageReadModeAscii}, + {"binary", JsStorageReadModeBinary}, + }; + static const JsValueDeclaration js_storage_read_arg_list[] = { + JS_VALUE_ENUM(JsStorageReadMode, js_storage_read_mode_variants), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_storage_read_args = JS_VALUE_ARGS(js_storage_read_arg_list); + + JsStorageReadMode read_mode; int32_t length; - JS_FETCH_ARGS_OR_RETURN( - mjs, JS_EXACTLY, JS_ARG_ENUM(read_mode, "ReadMode"), JS_ARG_INT32(&length)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_read_args, &read_mode, &length); + File* file = JS_GET_CONTEXT(mjs); char buffer[length]; size_t actually_read = storage_file_read(file, buffer, length); - if(read_mode == ReadModeAscii) { + if(read_mode == JsStorageReadModeAscii) { mjs_return(mjs, mjs_mk_string(mjs, buffer, actually_read, true)); - } else if(read_mode == ReadModeBinary) { + } else if(read_mode == JsStorageReadModeBinary) { mjs_return(mjs, mjs_mk_array_buf(mjs, buffer, actually_read)); } } static void js_storage_file_write(struct mjs* mjs) { + static const JsValueDeclaration js_storage_file_write_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + }; + static const JsValueArguments js_storage_file_write_args = + JS_VALUE_ARGS(js_storage_file_write_arg_list); + mjs_val_t data; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&data)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_file_write_args, &data); + const void* buf; size_t len; if(mjs_is_string(data)) { @@ -52,52 +89,58 @@ static void js_storage_file_write(struct mjs* mjs) { static void js_storage_file_seek_relative(struct mjs* mjs) { int32_t offset; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_int_args, &offset); File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, false))); } static void js_storage_file_seek_absolute(struct mjs* mjs) { int32_t offset; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_int_args, &offset); File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, true))); } static void js_storage_file_tell(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_number(mjs, storage_file_tell(file))); } static void js_storage_file_truncate(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_truncate(file))); } static void js_storage_file_size(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_number(mjs, storage_file_size(file))); } static void js_storage_file_eof(struct mjs* mjs) { - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args File* file = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_eof(file))); } static void js_storage_file_copy_to(struct mjs* mjs) { - File* source = JS_GET_CONTEXT(mjs); + static const JsValueDeclaration js_storage_file_write_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeAny), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_storage_file_write_args = + JS_VALUE_ARGS(js_storage_file_write_arg_list); + mjs_val_t dest_obj; int32_t bytes; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&dest_obj), JS_ARG_INT32(&bytes)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_file_write_args, &dest_obj, &bytes); + + File* source = JS_GET_CONTEXT(mjs); File* destination = JS_GET_INST(mjs, dest_obj); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_copy_to_file(source, destination, bytes))); } -// ---=== top-level file ops ===--- +// ========================= +// Top-level file operations +// ========================= // common destructor for file and dir objects static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) { @@ -106,23 +149,33 @@ static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) { } static void js_storage_open_file(struct mjs* mjs) { - const char* path; - FS_AccessMode access_mode; - FS_OpenMode open_mode; - JS_ENUM_MAP(access_mode, {"r", FSAM_READ}, {"w", FSAM_WRITE}, {"rw", FSAM_READ_WRITE}); - JS_ENUM_MAP( - open_mode, + static const JsValueEnumVariant js_storage_fsam_variants[] = { + {"r", FSAM_READ}, + {"w", FSAM_WRITE}, + {"rw", FSAM_READ_WRITE}, + }; + + static const JsValueEnumVariant js_storage_fsom_variants[] = { {"open_existing", FSOM_OPEN_EXISTING}, {"open_always", FSOM_OPEN_ALWAYS}, {"open_append", FSOM_OPEN_APPEND}, {"create_new", FSOM_CREATE_NEW}, - {"create_always", FSOM_CREATE_ALWAYS}); - JS_FETCH_ARGS_OR_RETURN( - mjs, - JS_EXACTLY, - JS_ARG_STR(&path), - JS_ARG_ENUM(access_mode, "AccessMode"), - JS_ARG_ENUM(open_mode, "OpenMode")); + {"create_always", FSOM_CREATE_ALWAYS}, + }; + + static const JsValueDeclaration js_storage_open_file_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_ENUM(FS_AccessMode, js_storage_fsam_variants), + JS_VALUE_ENUM(FS_OpenMode, js_storage_fsom_variants), + }; + static const JsValueArguments js_storage_open_file_args = + JS_VALUE_ARGS(js_storage_open_file_arg_list); + + const char* path; + FS_AccessMode access_mode; + FS_OpenMode open_mode; + JS_VALUE_PARSE_ARGS_OR_RETURN( + mjs, &js_storage_open_file_args, &path, &access_mode, &open_mode); Storage* storage = JS_GET_CONTEXT(mjs); File* file = storage_file_alloc(storage); @@ -152,16 +205,18 @@ static void js_storage_open_file(struct mjs* mjs) { static void js_storage_file_exists(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_exists(storage, path))); } -// ---=== dir ops ===--- +// ==================== +// Directory operations +// ==================== static void js_storage_read_directory(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); File* dir = storage_file_alloc(storage); @@ -200,30 +255,32 @@ static void js_storage_read_directory(struct mjs* mjs) { static void js_storage_directory_exists(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_dir_exists(storage, path))); } static void js_storage_make_directory(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage, path))); } -// ---=== common ops ===--- +// ================= +// Common operations +// ================= static void js_storage_file_or_dir_exists(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage, path))); } static void js_storage_stat(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); FileInfo file_info; uint32_t timestamp; @@ -244,21 +301,21 @@ static void js_storage_stat(struct mjs* mjs) { static void js_storage_remove(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage, path))); } static void js_storage_rmrf(struct mjs* mjs) { const char* path; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &path); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove_recursive(storage, path))); } static void js_storage_rename(struct mjs* mjs) { const char *old, *new; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&old), JS_ARG_STR(&new)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &old, &new); Storage* storage = JS_GET_CONTEXT(mjs); FS_Error status = storage_common_rename(storage, old, new); mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK)); @@ -266,7 +323,7 @@ static void js_storage_rename(struct mjs* mjs) { static void js_storage_copy(struct mjs* mjs) { const char *source, *dest; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&source), JS_ARG_STR(&dest)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &source, &dest); Storage* storage = JS_GET_CONTEXT(mjs); FS_Error status = storage_common_copy(storage, source, dest); mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK || status == FSE_EXIST)); @@ -274,7 +331,7 @@ static void js_storage_copy(struct mjs* mjs) { static void js_storage_fs_info(struct mjs* mjs) { const char* fs; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fs)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_1_str_args, &fs); Storage* storage = JS_GET_CONTEXT(mjs); uint64_t total_space, free_space; if(storage_common_fs_info(storage, fs, &total_space, &free_space) != FSE_OK) { @@ -290,15 +347,19 @@ static void js_storage_fs_info(struct mjs* mjs) { } static void js_storage_next_available_filename(struct mjs* mjs) { + static const JsValueDeclaration js_storage_naf_arg_list[] = { + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeString), + JS_VALUE_SIMPLE(JsValueTypeInt32), + }; + static const JsValueArguments js_storage_naf_args = JS_VALUE_ARGS(js_storage_naf_arg_list); + const char *dir_path, *file_name, *file_ext; int32_t max_len; - JS_FETCH_ARGS_OR_RETURN( - mjs, - JS_EXACTLY, - JS_ARG_STR(&dir_path), - JS_ARG_STR(&file_name), - JS_ARG_STR(&file_ext), - JS_ARG_INT32(&max_len)); + JS_VALUE_PARSE_ARGS_OR_RETURN( + mjs, &js_storage_naf_args, &dir_path, &file_name, &file_ext, &max_len); + Storage* storage = JS_GET_CONTEXT(mjs); FuriString* next_name = furi_string_alloc(); storage_get_next_filename(storage, dir_path, file_name, file_ext, next_name, max_len); @@ -306,23 +367,27 @@ static void js_storage_next_available_filename(struct mjs* mjs) { furi_string_free(next_name); } -// ---=== path ops ===--- +// =============== +// Path operations +// =============== static void js_storage_are_paths_equal(struct mjs* mjs) { const char *path1, *path2; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path1), JS_ARG_STR(&path2)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &path1, &path2); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_equivalent_path(storage, path1, path2))); } static void js_storage_is_subpath_of(struct mjs* mjs) { const char *parent, *child; - JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&parent), JS_ARG_STR(&child)); + JS_VALUE_PARSE_ARGS_OR_RETURN(mjs, &js_storage_2_str_args, &parent, &child); Storage* storage = JS_GET_CONTEXT(mjs); mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_is_subdir(storage, parent, child))); } -// ---=== module ctor & dtor ===--- +// ================== +// Module ctor & dtor +// ================== static void* js_storage_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); @@ -363,7 +428,9 @@ static void js_storage_destroy(void* data) { furi_record_close(RECORD_STORAGE); } -// ---=== boilerplate ===--- +// =========== +// Boilerplate +// =========== static const JsModuleDescriptor js_storage_desc = { "storage", diff --git a/applications/system/js_app/plugin_api/app_api_table_i.h b/applications/system/js_app/plugin_api/app_api_table_i.h index b2debbde8..76556bcdd 100644 --- a/applications/system/js_app/plugin_api/app_api_table_i.h +++ b/applications/system/js_app/plugin_api/app_api_table_i.h @@ -1,4 +1,5 @@ -#include "js_plugin_api.h" +#include "../js_modules.h" + /* * A list of app's private functions and objects to expose for plugins. * It is used to generate a table of symbols for import resolver to use. @@ -8,4 +9,16 @@ static constexpr auto app_api_table = sort(create_array_t( API_METHOD(js_delay_with_flags, bool, (struct mjs*, uint32_t)), API_METHOD(js_flags_set, void, (struct mjs*, uint32_t)), API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t)), - API_METHOD(js_module_get, void*, (JsModules*, const char*)))); + API_METHOD(js_module_get, void*, (JsModules*, const char*)), + API_METHOD(js_value_buffer_size, size_t, (const JsValueParseDeclaration declaration)), + API_METHOD( + js_value_parse, + JsValueParseStatus, + (struct mjs * mjs, + const JsValueParseDeclaration declaration, + JsValueParseFlag flags, + mjs_val_t* buffer, + size_t buf_size, + mjs_val_t* source, + size_t n_c_vals, + ...)))); diff --git a/applications/system/js_app/plugin_api/js_plugin_api.h b/applications/system/js_app/plugin_api/js_plugin_api.h deleted file mode 100644 index 421b68576..000000000 --- a/applications/system/js_app/plugin_api/js_plugin_api.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef void JsModules; - -bool js_delay_with_flags(struct mjs* mjs, uint32_t time); - -void js_flags_set(struct mjs* mjs, uint32_t flags); - -uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout); - -void* js_module_get(JsModules* modules, const char* name); - -#ifdef __cplusplus -} -#endif From 6b5d0066902cb03bf2d981f6117e4da34323fc87 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Sat, 5 Apr 2025 20:22:05 +0400 Subject: [PATCH 9/9] [FL-3953] Application chaining (#4105) * feat: app chaining * add `launch_current_app_after_deferred`, remove `get_referring_application` * fix naming * new api * fix f18 * fix deferred launches after errors * fix: memory leak * Updater: MIN_GAP_PAGES = 0 * loader: loader_get_application_launch_path doc * loader: fix freeze * loader: reject mlib, reduce code size * loader: generic synchronous call, reduce size * loader: reject furi_string, reduce size * apps: debug: removed order field from manifests since it is no longer meaningful --------- Co-authored-by: Aleksandr Kutuzov Co-authored-by: hedger --- applications/debug/accessor/application.fam | 1 - .../debug/battery_test_app/application.fam | 1 - applications/debug/blink_test/application.fam | 1 - .../debug/bt_debug_app/application.fam | 1 - applications/debug/ccid_test/application.fam | 1 - .../debug/direct_draw/application.fam | 1 - .../debug/display_test/application.fam | 1 - .../event_loop_blink_test/application.fam | 1 - .../debug/expansion_test/application.fam | 1 - .../debug/file_browser_test/application.fam | 1 - .../debug/keypad_test/application.fam | 1 - .../debug/lfrfid_debug/application.fam | 1 - .../debug/loader_chaining_a/application.fam | 8 + .../loader_chaining_a/loader_chaining_a.c | 164 +++++++++++++++ .../debug/loader_chaining_b/application.fam | 8 + .../loader_chaining_b/loader_chaining_b.c | 27 +++ .../debug/locale_test/application.fam | 1 - .../debug/rpc_debug_app/application.fam | 1 - .../debug/speaker_debug/application.fam | 1 - .../debug/subghz_test/application.fam | 1 - .../text_box_element_test/application.fam | 1 - .../debug/text_box_view_test/application.fam | 1 - applications/debug/uart_echo/application.fam | 1 - applications/debug/unit_tests/application.fam | 1 - applications/debug/usb_mouse/application.fam | 1 - applications/debug/usb_test/application.fam | 1 - applications/debug/vibro_test/application.fam | 1 - .../archive/scenes/archive_scene_browser.c | 2 +- applications/services/desktop/desktop.c | 4 +- applications/services/loader/loader.c | 187 ++++++++++++++---- applications/services/loader/loader.h | 50 ++++- .../services/loader/loader_applications.c | 2 +- applications/services/loader/loader_i.h | 19 ++ applications/services/loader/loader_queue.c | 32 +++ applications/services/loader/loader_queue.h | 53 +++++ targets/f18/api_symbols.csv | 3 + targets/f7/api_symbols.csv | 3 + 37 files changed, 512 insertions(+), 73 deletions(-) create mode 100644 applications/debug/loader_chaining_a/application.fam create mode 100644 applications/debug/loader_chaining_a/loader_chaining_a.c create mode 100644 applications/debug/loader_chaining_b/application.fam create mode 100644 applications/debug/loader_chaining_b/loader_chaining_b.c create mode 100644 applications/services/loader/loader_queue.c create mode 100644 applications/services/loader/loader_queue.h diff --git a/applications/debug/accessor/application.fam b/applications/debug/accessor/application.fam index 65a6c8666..4b24f98eb 100644 --- a/applications/debug/accessor/application.fam +++ b/applications/debug/accessor/application.fam @@ -6,6 +6,5 @@ App( entry_point="accessor_app", requires=["gui"], stack_size=4 * 1024, - order=40, fap_category="Debug", ) diff --git a/applications/debug/battery_test_app/application.fam b/applications/debug/battery_test_app/application.fam index 5f4acd83d..0ab68c086 100644 --- a/applications/debug/battery_test_app/application.fam +++ b/applications/debug/battery_test_app/application.fam @@ -8,7 +8,6 @@ App( "power", ], stack_size=1 * 1024, - order=130, fap_category="Debug", fap_libs=["assets"], ) diff --git a/applications/debug/blink_test/application.fam b/applications/debug/blink_test/application.fam index d7d873fb9..066e7a207 100644 --- a/applications/debug/blink_test/application.fam +++ b/applications/debug/blink_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="blink_test_app", requires=["gui"], stack_size=1 * 1024, - order=10, fap_category="Debug", ) diff --git a/applications/debug/bt_debug_app/application.fam b/applications/debug/bt_debug_app/application.fam index 8ed1ccc05..831b51ade 100644 --- a/applications/debug/bt_debug_app/application.fam +++ b/applications/debug/bt_debug_app/application.fam @@ -13,6 +13,5 @@ App( "bt_debug", ], stack_size=1 * 1024, - order=110, fap_category="Debug", ) diff --git a/applications/debug/ccid_test/application.fam b/applications/debug/ccid_test/application.fam index ad9076770..dfd6de05f 100644 --- a/applications/debug/ccid_test/application.fam +++ b/applications/debug/ccid_test/application.fam @@ -10,6 +10,5 @@ App( "ccid_test", ], stack_size=1 * 1024, - order=120, fap_category="Debug", ) diff --git a/applications/debug/direct_draw/application.fam b/applications/debug/direct_draw/application.fam index 11b3bc6ba..1e7d4b1c4 100644 --- a/applications/debug/direct_draw/application.fam +++ b/applications/debug/direct_draw/application.fam @@ -5,6 +5,5 @@ App( entry_point="direct_draw_app", requires=["gui", "input"], stack_size=2 * 1024, - order=70, fap_category="Debug", ) diff --git a/applications/debug/display_test/application.fam b/applications/debug/display_test/application.fam index 7b2357b01..1e0d3f775 100644 --- a/applications/debug/display_test/application.fam +++ b/applications/debug/display_test/application.fam @@ -6,6 +6,5 @@ App( requires=["gui"], fap_libs=["u8g2"], stack_size=1 * 1024, - order=120, fap_category="Debug", ) diff --git a/applications/debug/event_loop_blink_test/application.fam b/applications/debug/event_loop_blink_test/application.fam index 7d42ad339..6e4aaa48d 100644 --- a/applications/debug/event_loop_blink_test/application.fam +++ b/applications/debug/event_loop_blink_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="event_loop_blink_test_app", requires=["input"], stack_size=1 * 1024, - order=20, fap_category="Debug", ) diff --git a/applications/debug/expansion_test/application.fam b/applications/debug/expansion_test/application.fam index 9bc4b2fc2..30f325a92 100644 --- a/applications/debug/expansion_test/application.fam +++ b/applications/debug/expansion_test/application.fam @@ -6,7 +6,6 @@ App( requires=["expansion_start"], fap_libs=["assets"], stack_size=1 * 1024, - order=20, fap_category="Debug", fap_file_assets="assets", ) diff --git a/applications/debug/file_browser_test/application.fam b/applications/debug/file_browser_test/application.fam index bb08ad2c5..b610558e9 100644 --- a/applications/debug/file_browser_test/application.fam +++ b/applications/debug/file_browser_test/application.fam @@ -5,7 +5,6 @@ App( entry_point="file_browser_app", requires=["gui"], stack_size=2 * 1024, - order=150, fap_category="Debug", fap_icon_assets="icons", ) diff --git a/applications/debug/keypad_test/application.fam b/applications/debug/keypad_test/application.fam index 90851950b..ed7408e71 100644 --- a/applications/debug/keypad_test/application.fam +++ b/applications/debug/keypad_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="keypad_test_app", requires=["gui"], stack_size=1 * 1024, - order=30, fap_category="Debug", ) diff --git a/applications/debug/lfrfid_debug/application.fam b/applications/debug/lfrfid_debug/application.fam index 323f77818..d312dbda2 100644 --- a/applications/debug/lfrfid_debug/application.fam +++ b/applications/debug/lfrfid_debug/application.fam @@ -11,6 +11,5 @@ App( "lfrfid_debug", ], stack_size=1 * 1024, - order=100, fap_category="Debug", ) diff --git a/applications/debug/loader_chaining_a/application.fam b/applications/debug/loader_chaining_a/application.fam new file mode 100644 index 000000000..408efdcb1 --- /dev/null +++ b/applications/debug/loader_chaining_a/application.fam @@ -0,0 +1,8 @@ +App( + appid="loader_chaining_a", + name="Loader Chaining Test: App A", + apptype=FlipperAppType.DEBUG, + entry_point="chaining_test_app_a", + stack_size=1 * 1024, + fap_category="Debug", +) diff --git a/applications/debug/loader_chaining_a/loader_chaining_a.c b/applications/debug/loader_chaining_a/loader_chaining_a.c new file mode 100644 index 000000000..b3f303e2d --- /dev/null +++ b/applications/debug/loader_chaining_a/loader_chaining_a.c @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include +#include + +#define TAG "LoaderChainingA" +#define CHAINING_TEST_B "/ext/apps/Debug/loader_chaining_b.fap" +#define NONEXISTENT_APP "Some nonexistent app" + +typedef struct { + Gui* gui; + ViewDispatcher* view_dispatcher; + Submenu* submenu; + + Loader* loader; + + DialogsApp* dialogs; +} LoaderChainingA; + +typedef enum { + LoaderChainingASubmenuLaunchB, + LoaderChainingASubmenuLaunchBThenA, + LoaderChainingASubmenuLaunchNonexistentSilent, + LoaderChainingASubmenuLaunchNonexistentGui, + LoaderChainingASubmenuLaunchNonexistentGuiThenA, +} LoaderChainingASubmenu; + +static void loader_chaining_a_submenu_callback(void* context, uint32_t index) { + LoaderChainingA* app = context; + + FuriString* self_path = furi_string_alloc(); + furi_check(loader_get_application_launch_path(app->loader, self_path)); + + switch(index) { + case LoaderChainingASubmenuLaunchB: + loader_enqueue_launch(app->loader, CHAINING_TEST_B, "Hello", LoaderDeferredLaunchFlagGui); + view_dispatcher_stop(app->view_dispatcher); + break; + + case LoaderChainingASubmenuLaunchBThenA: + loader_enqueue_launch(app->loader, CHAINING_TEST_B, "Hello", LoaderDeferredLaunchFlagGui); + loader_enqueue_launch( + app->loader, + furi_string_get_cstr(self_path), + "Hello to you from the future", + LoaderDeferredLaunchFlagGui); + + break; + + case LoaderChainingASubmenuLaunchNonexistentSilent: + loader_enqueue_launch(app->loader, NONEXISTENT_APP, NULL, LoaderDeferredLaunchFlagNone); + break; + + case LoaderChainingASubmenuLaunchNonexistentGui: + loader_enqueue_launch(app->loader, NONEXISTENT_APP, NULL, LoaderDeferredLaunchFlagGui); + break; + + case LoaderChainingASubmenuLaunchNonexistentGuiThenA: + loader_enqueue_launch(app->loader, NONEXISTENT_APP, NULL, LoaderDeferredLaunchFlagGui); + loader_enqueue_launch( + app->loader, + furi_string_get_cstr(self_path), + "Hello to you from the future", + LoaderDeferredLaunchFlagGui); + break; + } + + furi_string_free(self_path); + view_dispatcher_stop(app->view_dispatcher); +} + +static bool loader_chaining_a_nav_callback(void* context) { + LoaderChainingA* app = context; + view_dispatcher_stop(app->view_dispatcher); + return true; +} + +LoaderChainingA* loader_chaining_a_alloc(void) { + LoaderChainingA* app = malloc(sizeof(LoaderChainingA)); + app->gui = furi_record_open(RECORD_GUI); + app->loader = furi_record_open(RECORD_LOADER); + app->dialogs = furi_record_open(RECORD_DIALOGS); + app->view_dispatcher = view_dispatcher_alloc(); + app->submenu = submenu_alloc(); + + submenu_add_item( + app->submenu, + "Launch B", + LoaderChainingASubmenuLaunchB, + loader_chaining_a_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Launch B, then A", + LoaderChainingASubmenuLaunchBThenA, + loader_chaining_a_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Trigger error: silent", + LoaderChainingASubmenuLaunchNonexistentSilent, + loader_chaining_a_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Trigger error: GUI", + LoaderChainingASubmenuLaunchNonexistentGui, + loader_chaining_a_submenu_callback, + app); + submenu_add_item( + app->submenu, + "Error, then launch A", + LoaderChainingASubmenuLaunchNonexistentGuiThenA, + loader_chaining_a_submenu_callback, + app); + + view_dispatcher_add_view(app->view_dispatcher, 0, submenu_get_view(app->submenu)); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, loader_chaining_a_nav_callback); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + view_dispatcher_switch_to_view(app->view_dispatcher, 0); + + return app; +} + +void loader_chaining_a_free(LoaderChainingA* app) { + furi_record_close(RECORD_DIALOGS); + furi_record_close(RECORD_LOADER); + furi_record_close(RECORD_GUI); + view_dispatcher_remove_view(app->view_dispatcher, 0); + submenu_free(app->submenu); + view_dispatcher_free(app->view_dispatcher); + free(app); +} + +int32_t chaining_test_app_a(const char* arg) { + LoaderChainingA* app = loader_chaining_a_alloc(); + + if(arg) { + if(strlen(arg)) { + DialogMessage* message = dialog_message_alloc(); + FuriString* text; + + dialog_message_set_header(message, "Hi, I am A", 64, 0, AlignCenter, AlignTop); + text = furi_string_alloc_printf("Me from the past says:\n%s", arg); + dialog_message_set_buttons(message, NULL, "ok!", NULL); + + dialog_message_set_text( + message, furi_string_get_cstr(text), 64, 32, AlignCenter, AlignCenter); + dialog_message_show(app->dialogs, message); + dialog_message_free(message); + furi_string_free(text); + } + } + + view_dispatcher_run(app->view_dispatcher); + + loader_chaining_a_free(app); + return 0; +} diff --git a/applications/debug/loader_chaining_b/application.fam b/applications/debug/loader_chaining_b/application.fam new file mode 100644 index 000000000..5b8767e50 --- /dev/null +++ b/applications/debug/loader_chaining_b/application.fam @@ -0,0 +1,8 @@ +App( + appid="loader_chaining_b", + name="Loader Chaining Test: App B", + apptype=FlipperAppType.DEBUG, + entry_point="chaining_test_app_b", + stack_size=1 * 1024, + fap_category="Debug", +) diff --git a/applications/debug/loader_chaining_b/loader_chaining_b.c b/applications/debug/loader_chaining_b/loader_chaining_b.c new file mode 100644 index 000000000..439e6e25e --- /dev/null +++ b/applications/debug/loader_chaining_b/loader_chaining_b.c @@ -0,0 +1,27 @@ +#include +#include +#include + +int32_t chaining_test_app_b(const char* arg) { + if(!arg) return 0; + + Loader* loader = furi_record_open(RECORD_LOADER); + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header(message, "Hi, I am B", 64, 0, AlignCenter, AlignTop); + FuriString* text = furi_string_alloc_printf("And A told me:\n%s", arg); + dialog_message_set_text(message, furi_string_get_cstr(text), 64, 32, AlignCenter, AlignCenter); + dialog_message_set_buttons(message, "Just quit", NULL, "Launch A"); + DialogMessageButton result = dialog_message_show(dialogs, message); + dialog_message_free(message); + furi_string_free(text); + + if(result == DialogMessageButtonRight) + loader_enqueue_launch( + loader, "/ext/apps/Debug/loader_chaining_a.fap", NULL, LoaderDeferredLaunchFlagGui); + + furi_record_close(RECORD_LOADER); + furi_record_close(RECORD_DIALOGS); + return 0; +} diff --git a/applications/debug/locale_test/application.fam b/applications/debug/locale_test/application.fam index d341122f9..757be7155 100644 --- a/applications/debug/locale_test/application.fam +++ b/applications/debug/locale_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="locale_test_app", requires=["gui", "locale"], stack_size=2 * 1024, - order=70, fap_category="Debug", ) diff --git a/applications/debug/rpc_debug_app/application.fam b/applications/debug/rpc_debug_app/application.fam index d71065afa..795f83287 100644 --- a/applications/debug/rpc_debug_app/application.fam +++ b/applications/debug/rpc_debug_app/application.fam @@ -5,6 +5,5 @@ App( entry_point="rpc_debug_app", requires=["gui", "rpc_start", "notification"], stack_size=2 * 1024, - order=10, fap_category="Debug", ) diff --git a/applications/debug/speaker_debug/application.fam b/applications/debug/speaker_debug/application.fam index 68d8b188b..c7f5629a7 100644 --- a/applications/debug/speaker_debug/application.fam +++ b/applications/debug/speaker_debug/application.fam @@ -5,7 +5,6 @@ App( entry_point="speaker_debug_app", requires=["gui", "notification"], stack_size=2 * 1024, - order=10, fap_category="Debug", fap_libs=["music_worker"], ) diff --git a/applications/debug/subghz_test/application.fam b/applications/debug/subghz_test/application.fam index 1b3e19d73..927ca7f89 100644 --- a/applications/debug/subghz_test/application.fam +++ b/applications/debug/subghz_test/application.fam @@ -6,7 +6,6 @@ App( entry_point="subghz_test_app", requires=["gui"], stack_size=4 * 1024, - order=50, fap_icon="subghz_test_10px.png", fap_category="Debug", fap_icon_assets="images", diff --git a/applications/debug/text_box_element_test/application.fam b/applications/debug/text_box_element_test/application.fam index 5e1abcddc..78dfe75f6 100644 --- a/applications/debug/text_box_element_test/application.fam +++ b/applications/debug/text_box_element_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="text_box_element_test_app", requires=["gui"], stack_size=1 * 1024, - order=140, fap_category="Debug", ) diff --git a/applications/debug/text_box_view_test/application.fam b/applications/debug/text_box_view_test/application.fam index e356a278e..6a3225d88 100644 --- a/applications/debug/text_box_view_test/application.fam +++ b/applications/debug/text_box_view_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="text_box_view_test_app", requires=["gui"], stack_size=1 * 1024, - order=140, fap_category="Debug", ) diff --git a/applications/debug/uart_echo/application.fam b/applications/debug/uart_echo/application.fam index 7b030bcfa..d95302364 100644 --- a/applications/debug/uart_echo/application.fam +++ b/applications/debug/uart_echo/application.fam @@ -5,6 +5,5 @@ App( entry_point="uart_echo_app", requires=["gui"], stack_size=2 * 1024, - order=70, fap_category="Debug", ) diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index 05e834402..ed5f8c9da 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -7,7 +7,6 @@ App( requires=["system_settings", "cli_subghz"], provides=["delay_test"], resources="resources", - order=100, ) App( diff --git a/applications/debug/usb_mouse/application.fam b/applications/debug/usb_mouse/application.fam index 7747613d5..e57b3f108 100644 --- a/applications/debug/usb_mouse/application.fam +++ b/applications/debug/usb_mouse/application.fam @@ -5,6 +5,5 @@ App( entry_point="usb_mouse_app", requires=["gui"], stack_size=1 * 1024, - order=60, fap_category="Debug", ) diff --git a/applications/debug/usb_test/application.fam b/applications/debug/usb_test/application.fam index 463bb4a26..6481518b4 100644 --- a/applications/debug/usb_test/application.fam +++ b/applications/debug/usb_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="usb_test_app", requires=["gui"], stack_size=1 * 1024, - order=50, fap_category="Debug", ) diff --git a/applications/debug/vibro_test/application.fam b/applications/debug/vibro_test/application.fam index c35a7223f..dafa83eac 100644 --- a/applications/debug/vibro_test/application.fam +++ b/applications/debug/vibro_test/application.fam @@ -5,6 +5,5 @@ App( entry_point="vibro_test_app", requires=["gui"], stack_size=1 * 1024, - order=20, fap_category="Debug", ) diff --git a/applications/main/archive/scenes/archive_scene_browser.c b/applications/main/archive/scenes/archive_scene_browser.c index d09595037..1b6088035 100644 --- a/applications/main/archive/scenes/archive_scene_browser.c +++ b/applications/main/archive/scenes/archive_scene_browser.c @@ -42,7 +42,7 @@ static void archive_loader_callback(const void* message, void* context) { const LoaderEvent* event = message; ArchiveApp* archive = (ArchiveApp*)context; - if(event->type == LoaderEventTypeApplicationStopped) { + if(event->type == LoaderEventTypeNoMoreAppsInQueue) { view_dispatcher_send_custom_event( archive->view_dispatcher, ArchiveBrowserEventListRefresh); } diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 36536b99f..60f1c21b9 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -27,9 +27,7 @@ static void desktop_loader_callback(const void* message, void* context) { if(event->type == LoaderEventTypeApplicationBeforeLoad) { view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalBeforeAppStarted); furi_check(furi_semaphore_acquire(desktop->animation_semaphore, 3000) == FuriStatusOk); - } else if( - event->type == LoaderEventTypeApplicationLoadFailed || - event->type == LoaderEventTypeApplicationStopped) { + } else if(event->type == LoaderEventTypeNoMoreAppsInQueue) { view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalAfterAppFinished); } } diff --git a/applications/services/loader/loader.c b/applications/services/loader/loader.c index 72cac4b62..d3cd0022e 100644 --- a/applications/services/loader/loader.c +++ b/applications/services/loader/loader.c @@ -167,6 +167,13 @@ static void loader_show_gui_error( furi_record_close(RECORD_DIALOGS); } +static void loader_generic_synchronous_request(Loader* loader, LoaderMessage* message) { + furi_check(loader); + message->api_lock = api_lock_alloc_locked(); + furi_message_queue_put(loader->queue, message, FuriWaitForever); + api_lock_wait_unlock_and_free(message->api_lock); +} + LoaderStatus loader_start(Loader* loader, const char* name, const char* args, FuriString* error_message) { furi_check(loader); @@ -202,16 +209,12 @@ void loader_start_detached_with_gui_error(Loader* loader, const char* name, cons } bool loader_lock(Loader* loader) { - furi_check(loader); - - LoaderMessage message; LoaderMessageBoolResult result; - message.type = LoaderMessageTypeLock; - message.api_lock = api_lock_alloc_locked(); - message.bool_value = &result; - furi_message_queue_put(loader->queue, &message, FuriWaitForever); - api_lock_wait_unlock_and_free(message.api_lock); - + LoaderMessage message = { + .type = LoaderMessageTypeLock, + .bool_value = &result, + }; + loader_generic_synchronous_request(loader, &message); return result.value; } @@ -225,16 +228,12 @@ void loader_unlock(Loader* loader) { } bool loader_is_locked(Loader* loader) { - furi_check(loader); - - LoaderMessage message; LoaderMessageBoolResult result; - message.type = LoaderMessageTypeIsLocked; - message.api_lock = api_lock_alloc_locked(); - message.bool_value = &result; - furi_message_queue_put(loader->queue, &message, FuriWaitForever); - api_lock_wait_unlock_and_free(message.api_lock); - + LoaderMessage message = { + .type = LoaderMessageTypeIsLocked, + .bool_value = &result, + }; + loader_generic_synchronous_request(loader, &message); return result.value; } @@ -256,42 +255,63 @@ FuriPubSub* loader_get_pubsub(Loader* loader) { } bool loader_signal(Loader* loader, uint32_t signal, void* arg) { - furi_check(loader); - LoaderMessageBoolResult result; - LoaderMessage message = { .type = LoaderMessageTypeSignal, - .api_lock = api_lock_alloc_locked(), .signal.signal = signal, .signal.arg = arg, .bool_value = &result, }; - - furi_message_queue_put(loader->queue, &message, FuriWaitForever); - api_lock_wait_unlock_and_free(message.api_lock); - + loader_generic_synchronous_request(loader, &message); return result.value; } bool loader_get_application_name(Loader* loader, FuriString* name) { - furi_check(loader); - LoaderMessageBoolResult result; - LoaderMessage message = { .type = LoaderMessageTypeGetApplicationName, - .api_lock = api_lock_alloc_locked(), .application_name = name, .bool_value = &result, }; - - furi_message_queue_put(loader->queue, &message, FuriWaitForever); - api_lock_wait_unlock_and_free(message.api_lock); - + loader_generic_synchronous_request(loader, &message); return result.value; } +bool loader_get_application_launch_path(Loader* loader, FuriString* name) { + LoaderMessageBoolResult result; + LoaderMessage message = { + .type = LoaderMessageTypeGetApplicationLaunchPath, + .application_name = name, + .bool_value = &result, + }; + loader_generic_synchronous_request(loader, &message); + return result.value; +} + +void loader_enqueue_launch( + Loader* loader, + const char* name, + const char* args, + LoaderDeferredLaunchFlag flags) { + LoaderMessage message = { + .type = LoaderMessageTypeEnqueueLaunch, + .defer_start = + { + .name_or_path = strdup(name), + .args = args ? strdup(args) : NULL, + .flags = flags, + }, + }; + loader_generic_synchronous_request(loader, &message); +} + +void loader_clear_launch_queue(Loader* loader) { + LoaderMessage message = { + .type = LoaderMessageTypeClearLaunchQueue, + }; + loader_generic_synchronous_request(loader, &message); +} + // callbacks static void loader_menu_closed_callback(void* context) { @@ -328,12 +348,10 @@ static Loader* loader_alloc(void) { Loader* loader = malloc(sizeof(Loader)); loader->pubsub = furi_pubsub_alloc(); loader->queue = furi_message_queue_alloc(1, sizeof(LoaderMessage)); - loader->loader_menu = NULL; - loader->loader_applications = NULL; - loader->app.args = NULL; - loader->app.thread = NULL; - loader->app.insomniac = false; - loader->app.fap = NULL; + loader->gui = furi_record_open(RECORD_GUI); + loader->view_holder = view_holder_alloc(); + loader->loading = loading_alloc(); + view_holder_attach_to_gui(loader->view_holder, loader->gui); return loader; } @@ -656,6 +674,10 @@ static LoaderMessageLoaderStatusResult loader_do_start_by_name( LoaderStatusErrorUnknownApp, error_message, "Application \"%s\" not found", name); } while(false); + if(status.value == LoaderStatusOk) { + loader->app.launch_path = furi_string_alloc_set_str(name); + } + return status; } @@ -673,6 +695,57 @@ static void loader_do_unlock(Loader* loader) { loader->app.thread = NULL; } +static void loader_do_emit_queue_empty_event(Loader* loader) { + FURI_LOG_I(TAG, "Launch queue empty"); + LoaderEvent event; + event.type = LoaderEventTypeNoMoreAppsInQueue; + furi_pubsub_publish(loader->pubsub, &event); +} + +static bool loader_do_deferred_launch(Loader* loader, LoaderDeferredLaunchRecord* record); + +static void loader_do_next_deferred_launch_if_available(Loader* loader) { + LoaderDeferredLaunchRecord record; + if(loader_queue_pop(&loader->launch_queue, &record)) { + loader_do_deferred_launch(loader, &record); + loader_queue_item_clear(&record); + } else { + loader_do_emit_queue_empty_event(loader); + } +} + +static bool loader_do_deferred_launch(Loader* loader, LoaderDeferredLaunchRecord* record) { + furi_assert(loader); + furi_assert(record); + + bool is_successful = false; + FuriString* error_message = furi_string_alloc(); + view_holder_set_view(loader->view_holder, loading_get_view(loader->loading)); + view_holder_send_to_front(loader->view_holder); + + do { + const char* app_name_str = record->name_or_path; + const char* app_args = record->args; + FURI_LOG_I(TAG, "Deferred launch: %s", app_name_str); + + LoaderMessageLoaderStatusResult result = + loader_do_start_by_name(loader, app_name_str, app_args, error_message); + if(result.value == LoaderStatusOk) { + is_successful = true; + break; + } + + if(record->flags & LoaderDeferredLaunchFlagGui) + loader_show_gui_error(result, app_name_str, error_message); + + loader_do_next_deferred_launch_if_available(loader); + } while(false); + + view_holder_set_view(loader->view_holder, NULL); + furi_string_free(error_message); + return is_successful; +} + static void loader_do_app_closed(Loader* loader) { furi_assert(loader->app.thread); @@ -697,11 +770,15 @@ static void loader_do_app_closed(Loader* loader) { loader->app.thread = NULL; } + furi_string_free(loader->app.launch_path); + FURI_LOG_I(TAG, "Application stopped. Free heap: %zu", memmgr_get_free_heap()); LoaderEvent event; event.type = LoaderEventTypeApplicationStopped; furi_pubsub_publish(loader->pubsub, &event); + + loader_do_next_deferred_launch_if_available(loader); } static bool loader_is_application_running(Loader* loader) { @@ -726,6 +803,15 @@ static bool loader_do_get_application_name(Loader* loader, FuriString* name) { return false; } +static bool loader_do_get_application_launch_path(Loader* loader, FuriString* path) { + if(loader_is_application_running(loader)) { + furi_string_set(path, loader->app.launch_path); + return true; + } + + return false; +} + // app int32_t loader_srv(void* p) { @@ -748,16 +834,20 @@ int32_t loader_srv(void* p) { while(true) { if(furi_message_queue_get(loader->queue, &message, FuriWaitForever) == FuriStatusOk) { switch(message.type) { - case LoaderMessageTypeStartByName: - *(message.status_value) = loader_do_start_by_name( + case LoaderMessageTypeStartByName: { + LoaderMessageLoaderStatusResult status = loader_do_start_by_name( loader, message.start.name, message.start.args, message.start.error_message); + *(message.status_value) = status; + if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader); api_lock_unlock(message.api_lock); break; + } case LoaderMessageTypeStartByNameDetachedWithGuiError: { FuriString* error_message = furi_string_alloc(); LoaderMessageLoaderStatusResult status = loader_do_start_by_name( loader, message.start.name, message.start.args, error_message); loader_show_gui_error(status, message.start.name, error_message); + if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader); if(message.start.name) free((void*)message.start.name); if(message.start.args) free((void*)message.start.args); furi_string_free(error_message); @@ -796,6 +886,19 @@ int32_t loader_srv(void* p) { loader_do_get_application_name(loader, message.application_name); api_lock_unlock(message.api_lock); break; + case LoaderMessageTypeGetApplicationLaunchPath: + message.bool_value->value = + loader_do_get_application_launch_path(loader, message.application_name); + api_lock_unlock(message.api_lock); + break; + case LoaderMessageTypeEnqueueLaunch: + furi_check(loader_queue_push(&loader->launch_queue, &message.defer_start)); + api_lock_unlock(message.api_lock); + break; + case LoaderMessageTypeClearLaunchQueue: + loader_queue_clear(&loader->launch_queue); + api_lock_unlock(message.api_lock); + break; } } } diff --git a/applications/services/loader/loader.h b/applications/services/loader/loader.h index cacfbff68..d732379a7 100644 --- a/applications/services/loader/loader.h +++ b/applications/services/loader/loader.h @@ -20,13 +20,19 @@ typedef enum { typedef enum { LoaderEventTypeApplicationBeforeLoad, LoaderEventTypeApplicationLoadFailed, - LoaderEventTypeApplicationStopped + LoaderEventTypeApplicationStopped, + LoaderEventTypeNoMoreAppsInQueue, //type == LoaderEventTypeApplicationStopped) { + if(event->type == LoaderEventTypeNoMoreAppsInQueue) { furi_thread_flags_set(thread_id, APPLICATION_STOP_EVENT); } } diff --git a/applications/services/loader/loader_i.h b/applications/services/loader/loader_i.h index 92f1e88e0..2bf42c655 100644 --- a/applications/services/loader/loader_i.h +++ b/applications/services/loader/loader_i.h @@ -2,11 +2,20 @@ #include #include #include + +#include +#include +#include + +#include + #include "loader.h" #include "loader_menu.h" #include "loader_applications.h" +#include "loader_queue.h" typedef struct { + FuriString* launch_path; char* args; FuriThread* thread; bool insomniac; @@ -19,6 +28,12 @@ struct Loader { LoaderMenu* loader_menu; LoaderApplications* loader_applications; LoaderAppData app; + + LoaderLaunchQueue launch_queue; + + Gui* gui; + ViewHolder* view_holder; + Loading* loading; }; typedef enum { @@ -33,6 +48,9 @@ typedef enum { LoaderMessageTypeStartByNameDetachedWithGuiError, LoaderMessageTypeSignal, LoaderMessageTypeGetApplicationName, + LoaderMessageTypeGetApplicationLaunchPath, + LoaderMessageTypeEnqueueLaunch, + LoaderMessageTypeClearLaunchQueue, } LoaderMessageType; typedef struct { @@ -72,6 +90,7 @@ typedef struct { union { LoaderMessageStartByName start; + LoaderDeferredLaunchRecord defer_start; LoaderMessageSignal signal; FuriString* application_name; }; diff --git a/applications/services/loader/loader_queue.c b/applications/services/loader/loader_queue.c new file mode 100644 index 000000000..517dcad75 --- /dev/null +++ b/applications/services/loader/loader_queue.c @@ -0,0 +1,32 @@ +#include "loader_queue.h" + +void loader_queue_item_clear(LoaderDeferredLaunchRecord* item) { + free(item->args); + free(item->name_or_path); +} + +bool loader_queue_pop(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item) { + if(!queue->item_cnt) return false; + + *item = queue->items[0]; + queue->item_cnt--; + memmove( + &queue->items[0], &queue->items[1], queue->item_cnt * sizeof(LoaderDeferredLaunchRecord)); + + return true; +} + +bool loader_queue_push(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item) { + if(queue->item_cnt == LOADER_QUEUE_MAX_SIZE) return false; + + queue->items[queue->item_cnt] = *item; + queue->item_cnt++; + + return true; +} + +void loader_queue_clear(LoaderLaunchQueue* queue) { + for(size_t i = 0; i < queue->item_cnt; i++) + loader_queue_item_clear(&queue->items[i]); + queue->item_cnt = 0; +} diff --git a/applications/services/loader/loader_queue.h b/applications/services/loader/loader_queue.h new file mode 100644 index 000000000..c40130e39 --- /dev/null +++ b/applications/services/loader/loader_queue.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "loader.h" + +#define LOADER_QUEUE_MAX_SIZE 4 + +typedef struct { + char* name_or_path; + char* args; + LoaderDeferredLaunchFlag flags; +} LoaderDeferredLaunchRecord; + +typedef struct { + LoaderDeferredLaunchRecord items[LOADER_QUEUE_MAX_SIZE]; + size_t item_cnt; +} LoaderLaunchQueue; + +/** + * @brief Frees internal data in a `DeferredLaunchRecord` + * + * @param[out] item Record to clear + */ +void loader_queue_item_clear(LoaderDeferredLaunchRecord* item); + +/** + * @brief Fetches the next item from the launch queue + * + * @param[inout] queue Queue instance + * @param[out] item Item output + * + * @return `true` if `item` was populated, `false` if queue is empty + */ +bool loader_queue_pop(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item); + +/** + * @brief Puts an item into the launch queue + * + * @param[inout] queue Queue instance + * @param[in] item Item to put in the queue + * + * @return `true` if the item was put into the queue, `false` if there's no more + * space left + */ +bool loader_queue_push(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item); + +/** + * @brief Clears the launch queue + * + * @param[inout] queue Queue instance + */ +void loader_queue_clear(LoaderLaunchQueue* queue); diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index ce47fe5c2..aa3a3813a 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1860,6 +1860,9 @@ Function,-,llrintl,long long int,long double Function,-,llround,long long int,double Function,-,llroundf,long long int,float Function,-,llroundl,long long int,long double +Function,+,loader_clear_launch_queue,void,Loader* +Function,+,loader_enqueue_launch,void,"Loader*, const char*, const char*, LoaderDeferredLaunchFlag" +Function,+,loader_get_application_launch_path,_Bool,"Loader*, FuriString*" Function,+,loader_get_application_name,_Bool,"Loader*, FuriString*" Function,+,loader_get_pubsub,FuriPubSub*,Loader* Function,+,loader_is_locked,_Bool,Loader* diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index e19281e2a..437a86a49 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -2288,6 +2288,9 @@ Function,-,llrintl,long long int,long double Function,-,llround,long long int,double Function,-,llroundf,long long int,float Function,-,llroundl,long long int,long double +Function,+,loader_clear_launch_queue,void,Loader* +Function,+,loader_enqueue_launch,void,"Loader*, const char*, const char*, LoaderDeferredLaunchFlag" +Function,+,loader_get_application_launch_path,_Bool,"Loader*, FuriString*" Function,+,loader_get_application_name,_Bool,"Loader*, FuriString*" Function,+,loader_get_pubsub,FuriPubSub*,Loader* Function,+,loader_is_locked,_Bool,Loader*