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*"