Merge remote-tracking branch 'ofw/dev' into mntm-dev

This commit is contained in:
Willy-JL
2025-04-12 06:35:36 +01:00
151 changed files with 2802 additions and 1745 deletions

View File

@@ -1,9 +1,9 @@
#include <furi.h>
#include <furi_hal.h>
#include <cli/cli.h>
#include <cli/cli_commands.h>
#include <lib/toolbox/args.h>
#include <toolbox/pipe.h>
#include <cli/cli_main_commands.h>
#include <toolbox/cli/cli_registry.h>
#include <ble/ble.h>
#include "bt_service/bt.h"
@@ -229,4 +229,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) {
furi_record_close(RECORD_BT);
}
CLI_COMMAND_INTERFACE(bt, execute, CliCommandFlagDefault, 1024);
CLI_COMMAND_INTERFACE(bt, execute, CliCommandFlagDefault, 1024, CLI_APPID);

View File

@@ -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
@@ -29,8 +21,8 @@ App(
apptype=FlipperAppType.SERVICE,
entry_point="cli_vcp_srv",
stack_size=1024,
order=40,
sdk_headers=["cli_vcp.h"],
order=10,
sdk_headers=["cli_vcp.h", "cli.h"],
sources=["cli_vcp.c"],
)
@@ -50,13 +42,21 @@ App(
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"],
)
App(
appid="cli_src",
targets=["f7"],
apptype=FlipperAppType.PLUGIN,
entry_point="cli_src_ep",
requires=["cli"],
sources=["cli_commands.c"],
sources=["cli_main_commands.c"],
)
App(
@@ -65,7 +65,7 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="cli_uptime_ep",
requires=["cli"],
sources=["cli_commands.c"],
sources=["cli_main_commands.c"],
)
App(
@@ -74,7 +74,7 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="cli_date_ep",
requires=["cli"],
sources=["cli_commands.c"],
sources=["cli_main_commands.c"],
)
App(
@@ -83,7 +83,7 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="cli_sysctl_ep",
requires=["cli"],
sources=["cli_commands.c"],
sources=["cli_main_commands.c"],
)
App(
@@ -92,7 +92,7 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="cli_top_ep",
requires=["cli"],
sources=["cli_commands.c"],
sources=["cli_main_commands.c"],
)
App(
@@ -101,7 +101,7 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="cli_vibro_ep",
requires=["cli"],
sources=["cli_commands.c"],
sources=["cli_main_commands.c"],
)
App(
@@ -110,7 +110,7 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="cli_led_ep",
requires=["cli"],
sources=["cli_commands.c"],
sources=["cli_main_commands.c"],
)
App(
@@ -119,7 +119,7 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="cli_gpio_ep",
requires=["cli"],
sources=["cli_commands.c", "cli_command_gpio.c"],
sources=["cli_main_commands.c", "cli_command_gpio.c"],
)
App(
@@ -128,7 +128,7 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="cli_i2c_ep",
requires=["cli"],
sources=["cli_commands.c"],
sources=["cli_main_commands.c"],
)
App(
@@ -137,5 +137,5 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="cli_clear_ep",
requires=["cli"],
sources=["cli_commands.c"],
sources=["cli_main_commands.c"],
)

View File

@@ -1,184 +0,0 @@
#include "cli.h"
#include "cli_i.h"
#include "cli_commands.h"
#include "cli_ansi.h"
#include <toolbox/pipe.h>
#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);
}

View File

@@ -1,131 +1,13 @@
/**
* @file cli.h
* API for registering commands with the CLI
*/
#pragma once
#include <furi.h>
#include <m-array.h>
#include "cli_ansi.h"
#include <toolbox/pipe.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Compatibility header for ease of porting existing apps.
* In short:
* Cli* is replaced with with CliRegistry*
* cli_* functions are replaced with cli_registry_* functions
* (i.e., cli_add_command() is now cli_registry_add_command())
*/
#include <toolbox/cli/cli_registry.h>
#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

View File

@@ -1,123 +0,0 @@
#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:
// <key> -> <key>
if(c != CliKeyEsc) PARSER_RESET_AND_RETURN(parser, CliModKeyNo, c); // -V1048
// <ESC> ...
parser->state = CliAnsiParserStateEscape;
break;
case CliAnsiParserStateEscape:
// <ESC> <ESC> -> <ESC>
if(c == CliKeyEsc) PARSER_RESET_AND_RETURN(parser, CliModKeyNo, c);
// <ESC> <key> -> Alt + <key>
if(c != '[') PARSER_RESET_AND_RETURN(parser, CliModKeyAlt, c);
// <ESC> [ ...
parser->state = CliAnsiParserStateEscapeBrace;
break;
case CliAnsiParserStateEscapeBrace:
// <ESC> [ <key mnemonic> -> <key>
if(c != '1') PARSER_RESET_AND_RETURN(parser, CliModKeyNo, cli_ansi_key_from_mnemonic(c));
// <ESC> [ 1 ...
parser->state = CliAnsiParserStateEscapeBraceOne;
break;
case CliAnsiParserStateEscapeBraceOne:
// <ESC> [ 1 <non-;> -> error
if(c != ';') PARSER_RESET_AND_RETURN(parser, CliModKeyNo, CliKeyUnrecognized);
// <ESC> [ 1 ; ...
parser->state = CliAnsiParserStateEscapeBraceOneSemicolon;
break;
case CliAnsiParserStateEscapeBraceOneSemicolon:
// <ESC> [ 1 ; <modifiers> ...
parser->modifiers = (c - '0');
parser->modifiers &= ~1;
parser->state = CliAnsiParserStateEscapeBraceOneSemicolonModifiers;
break;
case CliAnsiParserStateEscapeBraceOneSemicolonModifiers:
// <ESC> [ 1 ; <modifiers> <key mnemonic> -> <modifiers> + <key>
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;
}

View File

@@ -1,153 +0,0 @@
#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

View File

@@ -4,6 +4,7 @@
#include <furi_hal.h>
#include <lib/toolbox/args.h>
#include <toolbox/pipe.h>
#include <toolbox/cli/cli_command.h>
void cli_command_gpio_print_usage(void) {
printf("Usage:\r\n");

View File

@@ -1,6 +1,5 @@
#pragma once
#include "cli_i.h"
#include <toolbox/pipe.h>
void cli_command_gpio(PipeSide* pipe, FuriString* args, void* context);

View File

@@ -1,34 +0,0 @@
#pragma once
#include "cli.h"
#include <flipper_application/flipper_application.h>
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_##name = { \
.appid = PLUGIN_APP_ID, \
.ep_api_version = PLUGIN_API_VERSION, \
.entry_point = &cli_##name##_desc, \
}; \
\
const FlipperAppPluginDescriptor* cli_##name##_ep(void) { \
return &plugin_descriptor_##name; \
}

View File

@@ -1,53 +0,0 @@
/**
* @file cli_i.h
* Internal API for getting commands registered with the CLI
*/
#pragma once
#include <furi.h>
#include <m-bptree.h>
#include "cli.h"
#ifdef __cplusplus
extern "C" {
#endif
#define CLI_BUILTIN_COMMAND_STACK_SIZE (3 * 1024U)
#define CLI_COMMANDS_PATH "/ext/apps_data/cli/plugins"
typedef struct {
void* context; //<! Context passed to callbacks
CliExecuteCallback execute_callback; //<! Callback for command execution
CliCommandFlag flags;
size_t stack_depth;
} CliCommand;
#define CLI_COMMANDS_TREE_RANK 4
// -V:BPTREE_DEF2:1103
// -V:BPTREE_DEF2:524
BPTREE_DEF2(
CliCommandTree,
CLI_COMMANDS_TREE_RANK,
FuriString*,
FURI_STRING_OPLIST,
CliCommand,
M_POD_OPLIST);
#define M_OPL_CliCommandTree_t() BPTREE_OPLIST2(CliCommandTree, FURI_STRING_OPLIST, M_POD_OPLIST)
bool cli_get_command(Cli* cli, FuriString* command, CliCommand* result);
void cli_lock_commands(Cli* cli);
void cli_unlock_commands(Cli* cli);
/**
* @warning Surround calls to this function with `cli_[un]lock_commands`
*/
CliCommandTree_t* cli_get_commands(Cli* cli);
#ifdef __cplusplus
}
#endif

View File

@@ -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 <toolbox/cli/cli_ansi.h>
#include <core/thread.h>
#include <furi_hal.h>
@@ -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);
@@ -567,16 +523,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)
*/
@@ -605,29 +551,33 @@ void cli_command_clear(PipeSide* pipe, FuriString* args, void* context) {
printf("\e[2J\e[H");
}
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_add_command(cli, "log", CliCommandFlagParallelSafe, cli_command_log, 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_registry_add_command(registry, "log", CliCommandFlagParallelSafe, cli_command_log, NULL);
cli_registry_add_command(registry, "free", CliCommandFlagParallelSafe, cli_command_free, NULL);
cli_registry_add_command(
registry, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL);
cli_registry_add_command(registry, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL);
}
CLI_COMMAND_INTERFACE(src, cli_command_src, CliCommandFlagParallelSafe, 768);
CLI_COMMAND_INTERFACE(uptime, cli_command_uptime, CliCommandFlagDefault, 768);
CLI_COMMAND_INTERFACE(date, cli_command_date, CliCommandFlagParallelSafe, 768);
CLI_COMMAND_INTERFACE(sysctl, cli_command_sysctl, CliCommandFlagDefault, 1024);
CLI_COMMAND_INTERFACE(top, cli_command_top, CliCommandFlagParallelSafe, 1024);
CLI_COMMAND_INTERFACE(vibro, cli_command_vibro, CliCommandFlagDefault, 1024);
CLI_COMMAND_INTERFACE(led, cli_command_led, CliCommandFlagDefault, 1024);
CLI_COMMAND_INTERFACE(gpio, cli_command_gpio, CliCommandFlagDefault, 1024);
CLI_COMMAND_INTERFACE(i2c, cli_command_i2c, CliCommandFlagDefault, 1024);
CLI_COMMAND_INTERFACE(clear, cli_command_clear, CliCommandFlagParallelSafe, 768);
CLI_COMMAND_INTERFACE(src, cli_command_src, CliCommandFlagParallelSafe, 768, CLI_APPID);
CLI_COMMAND_INTERFACE(uptime, cli_command_uptime, CliCommandFlagDefault, 768, CLI_APPID);
CLI_COMMAND_INTERFACE(date, cli_command_date, CliCommandFlagParallelSafe, 768, CLI_APPID);
CLI_COMMAND_INTERFACE(sysctl, cli_command_sysctl, CliCommandFlagDefault, 1024, CLI_APPID);
CLI_COMMAND_INTERFACE(top, cli_command_top, CliCommandFlagParallelSafe, 1024, CLI_APPID);
CLI_COMMAND_INTERFACE(vibro, cli_command_vibro, CliCommandFlagDefault, 1024, CLI_APPID);
CLI_COMMAND_INTERFACE(led, cli_command_led, CliCommandFlagDefault, 1024, CLI_APPID);
CLI_COMMAND_INTERFACE(gpio, cli_command_gpio, CliCommandFlagDefault, 1024, CLI_APPID);
CLI_COMMAND_INTERFACE(i2c, cli_command_i2c, CliCommandFlagDefault, 1024, CLI_APPID);
CLI_COMMAND_INTERFACE(clear, cli_command_clear, CliCommandFlagParallelSafe, 768, CLI_APPID);
void cli_on_system_start(void) {
CliRegistry* registry = cli_registry_alloc();
cli_main_commands_init(registry);
furi_record_create(RECORD_CLI, registry);
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include "cli.h"
#include <toolbox/cli/cli_command.h>
#include <toolbox/cli/cli_registry.h>
#define CLI_APPID "cli"
void cli_main_commands_init(CliRegistry* registry);

View File

@@ -0,0 +1,46 @@
#include "cli_main_shell.h"
#include "cli_main_commands.h"
#include <toolbox/cli/cli_ansi.h>
#include <toolbox/cli/shell/cli_shell.h>
#include <furi_hal_version.h>
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,
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include <toolbox/cli/cli_command.h>
void cli_main_motd(void* context);
extern const CliCommandExternalConfig cli_main_ext_config;

View File

@@ -1,10 +1,12 @@
#include "cli_vcp.h"
#include "shell/cli_shell.h"
#include <furi_hal_usb_cdc.h>
#include <furi_hal.h>
#include <furi.h>
#include <stdint.h>
#include <toolbox/pipe.h>
#include <toolbox/cli/shell/cli_shell.h>
#include "cli_main_shell.h"
#include "cli_main_commands.h"
#define TAG "CliVcp"
@@ -28,29 +30,27 @@ typedef struct {
} CliVcpMessage;
typedef enum {
CliVcpInternalEventConnected = (1 << 0),
CliVcpInternalEventDisconnected = (1 << 1),
CliVcpInternalEventTxDone = (1 << 2),
CliVcpInternalEventRx = (1 << 3),
CliVcpInternalEventConnected,
CliVcpInternalEventDisconnected,
CliVcpInternalEventTxDone,
CliVcpInternalEventRx,
} CliVcpInternalEvent;
#define CliVcpInternalEventAll \
(CliVcpInternalEventConnected | CliVcpInternalEventDisconnected | CliVcpInternalEventTxDone | \
CliVcpInternalEventRx)
struct CliVcp {
FuriEventLoop* event_loop;
FuriMessageQueue* message_queue; // <! external messages
FuriThreadId thread_id;
FuriMessageQueue* internal_evt_queue;
bool is_enabled, is_connected;
FuriHalUsbInterface* previous_interface;
PipeSide* own_pipe;
bool is_currently_transmitting;
PipeSide* shell_pipe;
volatile bool is_currently_transmitting;
size_t previous_tx_length;
FuriThread* shell;
CliRegistry* main_registry;
CliShell* shell;
};
// ============
@@ -97,11 +97,12 @@ static void cli_vcp_maybe_receive_data(CliVcp* cli_vcp) {
// =============
static void cli_vcp_signal_internal_event(CliVcp* cli_vcp, CliVcpInternalEvent event) {
furi_thread_flags_set(cli_vcp->thread_id, event);
furi_check(furi_message_queue_put(cli_vcp->internal_evt_queue, &event, 0) == FuriStatusOk);
}
static void cli_vcp_cdc_tx_done(void* context) {
CliVcp* cli_vcp = context;
cli_vcp->is_currently_transmitting = false;
cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventTxDone);
}
@@ -186,12 +187,25 @@ static void cli_vcp_message_received(FuriEventLoopObject* object, void* context)
/**
* Processes messages arriving from CDC event callbacks
*/
static void cli_vcp_internal_event_happened(void* context) {
static void cli_vcp_internal_event_happened(FuriEventLoopObject* object, void* context) {
CliVcp* cli_vcp = context;
CliVcpInternalEvent event = furi_thread_flags_wait(CliVcpInternalEventAll, FuriFlagWaitAny, 0);
furi_check(!(event & FuriFlagError));
CliVcpInternalEvent event;
furi_check(furi_message_queue_get(object, &event, 0) == FuriStatusOk);
if(event & CliVcpInternalEventDisconnected) {
switch(event) {
case CliVcpInternalEventRx: {
VCP_TRACE(TAG, "Rx");
cli_vcp_maybe_receive_data(cli_vcp);
break;
}
case CliVcpInternalEventTxDone: {
VCP_TRACE(TAG, "TxDone");
cli_vcp_maybe_send_data(cli_vcp);
break;
}
case CliVcpInternalEventDisconnected: {
if(!cli_vcp->is_connected) return;
FURI_LOG_D(TAG, "Disconnected");
cli_vcp->is_connected = false;
@@ -200,9 +214,10 @@ static void cli_vcp_internal_event_happened(void* context) {
pipe_detach_from_event_loop(cli_vcp->own_pipe);
pipe_free(cli_vcp->own_pipe);
cli_vcp->own_pipe = NULL;
break;
}
if(event & CliVcpInternalEventConnected) {
case CliVcpInternalEventConnected: {
if(cli_vcp->is_connected) return;
FURI_LOG_D(TAG, "Connected");
cli_vcp->is_connected = true;
@@ -210,13 +225,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,18 +241,11 @@ 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);
break;
}
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);
}
}
@@ -245,7 +255,6 @@ static void cli_vcp_internal_event_happened(void* context) {
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();
@@ -257,8 +266,16 @@ static CliVcp* cli_vcp_alloc(void) {
cli_vcp_message_received,
cli_vcp);
furi_event_loop_subscribe_thread_flags(
cli_vcp->event_loop, cli_vcp_internal_event_happened, cli_vcp);
cli_vcp->internal_evt_queue =
furi_message_queue_alloc(VCP_MESSAGE_Q_LEN, sizeof(CliVcpInternalEvent));
furi_event_loop_subscribe_message_queue(
cli_vcp->event_loop,
cli_vcp->internal_evt_queue,
FuriEventLoopEventIn,
cli_vcp_internal_event_happened,
cli_vcp);
cli_vcp->main_registry = furi_record_open(RECORD_CLI);
return cli_vcp;
}

View File

@@ -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);

View File

@@ -1,4 +1,5 @@
#include "../cli_commands.h"
#include "../cli_main_commands.h"
#include <toolbox/cli/cli_ansi.h>
#include <toolbox/version.h>
#include <furi_hal.h>
#include <furi_hal_info.h>
@@ -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);

View File

@@ -0,0 +1,43 @@
#include "../cli_main_commands.h"
#include <toolbox/cli/cli_registry.h>
#include <toolbox/cli/shell/cli_shell.h>
#include <toolbox/cli/cli_ansi.h>
#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);

View File

@@ -1,433 +0,0 @@
#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 "cli_shell_completions.h"
#include <stdio.h>
#include <furi_hal_version.h>
#include <m-array.h>
#include <loader/loader.h>
#include <toolbox/pipe.h>
#include <flipper_application/plugins/plugin_manager.h>
#include <loader/firmware_api/firmware_api.h>
#include <storage/storage.h>
#define TAG "CliShell"
#define ANSI_TIMEOUT_MS 10
typedef enum {
CliShellComponentCompletions,
CliShellComponentLine,
CliShellComponentMAX, //<! do not use
} CliShellComponent;
CliShellKeyComboSet* component_key_combo_sets[] = {
[CliShellComponentCompletions] = &cli_shell_completions_key_combo_set,
[CliShellComponentLine] = &cli_shell_line_key_combo_set,
};
static_assert(CliShellComponentMAX == COUNT_OF(component_key_combo_sets));
typedef enum {
CliShellStorageEventMount,
CliShellStorageEventUnmount,
} CliShellStorageEvent;
struct CliShell {
Cli* cli;
FuriEventLoop* event_loop;
PipeSide* pipe;
CliAnsiParser* ansi_parser;
FuriEventLoopTimer* ansi_parsing_timer;
Storage* storage;
FuriPubSubSubscription* storage_subscription;
FuriMessageQueue* storage_event_queue;
void* components[CliShellComponentMAX];
};
typedef struct {
CliCommand* command;
PipeSide* pipe;
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;
}
static size_t cli_shell_string_distance(const char* s1, const char* s2) {
size_t distance = 0;
while(*s1 && *s2) {
if(*s1++ != *s2++) distance++;
}
while(*s1++)
distance++;
while(*s2++)
distance++;
return distance;
}
static void
cli_shell_find_similar_command(CliShell* cli_shell, const char* input, FuriString* suggestion) {
size_t min_distance = (size_t)-1;
const size_t max_allowed = (strlen(input) + 1) / 2;
furi_string_reset(suggestion);
cli_lock_commands(cli_shell->cli);
CliCommandTree_t* commands = cli_get_commands(cli_shell->cli);
for
M_EACH(registered_command, *commands, CliCommandTree_t) {
const char* command_name = furi_string_get_cstr(*registered_command->key_ptr);
const size_t distance = cli_shell_string_distance(input, command_name);
if(distance < min_distance && distance <= max_allowed) {
min_distance = distance;
furi_string_set(suggestion, command_name);
}
}
cli_unlock_commands(cli_shell->cli);
}
void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
// split command into command and args
size_t space = furi_string_search_char(command, ' ');
if(space == FURI_STRING_FAILURE) space = furi_string_size(command);
FuriString* command_name = furi_string_alloc_set(command);
furi_string_left(command_name, space);
FuriString* args = furi_string_alloc_set(command);
furi_string_right(args, space + 1);
PluginManager* plugin_manager = NULL;
Loader* loader = NULL;
CliCommand command_data;
do {
// find handler
if(!cli_get_command(cli_shell->cli, command_name, &command_data)) {
FuriString* suggestion = furi_string_alloc();
cli_shell_find_similar_command(
cli_shell, furi_string_get_cstr(command_name), suggestion);
if(furi_string_empty(suggestion)) {
printf(
ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET,
furi_string_get_cstr(command_name));
} else {
printf(
ANSI_FG_RED
"could not find command `%s`, did you mean `%s`? Use `help` to list all available commands" ANSI_RESET,
furi_string_get_cstr(command_name),
furi_string_get_cstr(suggestion));
}
furi_string_free(suggestion);
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);
bool success = loader_lock(loader);
if(!success) {
printf(ANSI_FG_RED
"this command cannot be run while an application is open" ANSI_RESET);
break;
}
}
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);
furi_string_free(args);
// 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_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];
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);
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;
cli_shell_process_parser_result(
cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser));
}
// =======
// 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;
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(
cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell);
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]);
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));
}
}
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;
}

View File

@@ -1,16 +0,0 @@
#pragma once
#include <furi.h>
#include <toolbox/pipe.h>
#ifdef __cplusplus
extern "C" {
#endif
#define CLI_SHELL_STACK_SIZE (4 * 1024U)
FuriThread* cli_shell_start(PipeSide* pipe);
#ifdef __cplusplus
}
#endif

View File

@@ -1,364 +0,0 @@
#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) {
cli_lock_commands(completions->cli);
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);
}
}
cli_unlock_commands(completions->cli);
} 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},
},
};

View File

@@ -1,24 +0,0 @@
#pragma once
#include <furi.h>
#include <m-array.h>
#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

View File

@@ -1,32 +0,0 @@
#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

View File

@@ -1,369 +0,0 @@
#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;
}
}
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
// ==============
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);
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) {
size_t pos = line->history_position;
size_t len = line->history_entries;
memmove(
&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
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");
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_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;
// 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_normal,
.count = 14,
.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},
{{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},
},
};

View File

@@ -1,40 +0,0 @@
#pragma once
#include <furi.h>
#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);
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
*/
void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line);
extern CliShellKeyComboSet cli_shell_line_key_combo_set;
#ifdef __cplusplus
}
#endif

View File

@@ -3,8 +3,9 @@
#include <lib/toolbox/args.h>
#include <toolbox/pipe.h>
#include <cli/cli.h>
#include <cli/cli_commands.h>
#include <cli/cli_main_commands.h>
#include <toolbox/cli/cli_registry.h>
#include <toolbox/cli/cli_ansi.h>
void crypto_cli_print_usage(void) {
printf("Usage:\r\n");
@@ -318,4 +319,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) {
furi_string_free(cmd);
}
CLI_COMMAND_INTERFACE(crypto, execute, CliCommandFlagDefault, 1024);
CLI_COMMAND_INTERFACE(crypto, execute, CliCommandFlagDefault, 1024, CLI_APPID);

View File

@@ -1,6 +1,5 @@
#include "desktop_i.h"
#include <cli/cli.h>
#include <cli/cli_vcp.h>
#include <gui/gui_i.h>
@@ -29,9 +28,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);
}
}

View File

@@ -216,7 +216,7 @@ void desktop_run_keybind(Desktop* desktop, InputType _type, InputKey _key) {
} else if(furi_string_equal(keybind, "Lock with PIN")) {
view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopMainEventLockWithPin);
} else if(furi_string_equal(keybind, "Wipe Device")) {
loader_start_detached_with_gui_error(desktop->loader, "Storage", "wipe");
loader_start_detached_with_gui_error(desktop->loader, "Storage", "Wipe Device");
} else {
if(storage_common_exists(desktop->storage, furi_string_get_cstr(keybind))) {
run_with_default_app(furi_string_get_cstr(keybind));

View File

@@ -8,5 +8,5 @@ App(
],
requires=["rpc_start"],
provides=["expansion_settings"],
order=150,
order=100,
)

View File

@@ -14,8 +14,12 @@ struct Submenu {
typedef struct {
FuriString* label;
uint32_t index;
SubmenuItemCallback callback;
union {
SubmenuItemCallback callback;
SubmenuItemCallbackEx callback_ex;
};
void* callback_context;
bool has_extended_events;
bool locked;
FuriString* locked_message;
@@ -73,7 +77,7 @@ typedef struct {
static void submenu_process_up(Submenu* submenu);
static void submenu_process_down(Submenu* submenu);
static void submenu_process_ok(Submenu* submenu);
static void submenu_process_ok(Submenu* submenu, InputType input_type);
static size_t submenu_items_on_screen(SubmenuModel* model) {
size_t res = (model->is_vertical) ? 8 : 4;
@@ -181,11 +185,13 @@ static bool submenu_view_input_callback(InputEvent* event, void* context) {
{ locked_message_visible = model->locked_message_visible; },
false);
if((event->type != InputTypePress && event->type != InputTypeRelease) &&
locked_message_visible) {
if(locked_message_visible && (event->type == InputTypeShort || event->type == InputTypeLong)) {
with_view_model(
submenu->view, SubmenuModel * model, { model->locked_message_visible = false; }, true);
consumed = true;
} else if(event->key == InputKeyOk) {
consumed = true;
submenu_process_ok(submenu, event->type);
} else if(event->type == InputTypeShort) {
switch(event->key) {
case InputKeyUp:
@@ -196,10 +202,6 @@ static bool submenu_view_input_callback(InputEvent* event, void* context) {
consumed = true;
submenu_process_down(submenu);
break;
case InputKeyOk:
consumed = true;
submenu_process_ok(submenu);
break;
default:
break;
}
@@ -303,6 +305,7 @@ void submenu_add_lockable_item(
item->index = index;
item->callback = callback;
item->callback_context = callback_context;
item->has_extended_events = false;
item->locked = locked;
if(locked) {
furi_string_set_str(item->locked_message, locked_message);
@@ -311,6 +314,30 @@ void submenu_add_lockable_item(
true);
}
void submenu_add_item_ex(
Submenu* submenu,
const char* label,
uint32_t index,
SubmenuItemCallbackEx callback,
void* callback_context) {
SubmenuItem* item = NULL;
furi_check(label);
furi_check(submenu);
with_view_model(
submenu->view,
SubmenuModel * model,
{
item = SubmenuItemArray_push_new(model->items);
furi_string_set_str(item->label, label);
item->index = index;
item->callback_ex = callback;
item->callback_context = callback_context;
item->has_extended_events = true;
},
true);
}
void submenu_change_item_label(Submenu* submenu, uint32_t index, const char* label) {
furi_check(submenu);
furi_check(label);
@@ -455,7 +482,7 @@ void submenu_process_down(Submenu* submenu) {
true);
}
void submenu_process_ok(Submenu* submenu) {
void submenu_process_ok(Submenu* submenu, InputType input_type) {
SubmenuItem* item = NULL;
with_view_model(
@@ -466,15 +493,20 @@ void submenu_process_ok(Submenu* submenu) {
if(model->position < items_size) {
item = SubmenuItemArray_get(model->items, model->position);
}
if(item && item->locked) {
if(item && item->locked &&
(input_type == InputTypeShort || input_type == InputTypeLong)) {
model->locked_message_visible = true;
furi_timer_start(submenu->locked_timer, furi_kernel_get_tick_frequency() * 3);
}
},
true);
if(item && !item->locked && item->callback) {
if(!item || item->locked) return;
if(!item->has_extended_events && input_type == InputTypeShort && item->callback) {
item->callback(item->callback_context, item->index);
} else if(item->has_extended_events && item->callback_ex) {
item->callback_ex(item->callback_context, input_type, item->index);
}
}

View File

@@ -14,6 +14,7 @@ extern "C" {
/** Submenu anonymous structure */
typedef struct Submenu Submenu;
typedef void (*SubmenuItemCallback)(void* context, uint32_t index);
typedef void (*SubmenuItemCallbackEx)(void* context, InputType input_type, uint32_t index);
/** Allocate and initialize submenu
*
@@ -53,6 +54,22 @@ void submenu_add_item(
SubmenuItemCallback callback,
void* callback_context);
/** Add item to submenu with extended press events
*
* @param submenu Submenu instance
* @param label menu item label
* @param index menu item index, used for callback, may be
* the same with other items
* @param callback menu item extended callback
* @param callback_context menu item callback context
*/
void submenu_add_item_ex(
Submenu* submenu,
const char* label,
uint32_t index,
SubmenuItemCallbackEx callback,
void* callback_context);
/** Add lockable item to submenu
*
* @param submenu Submenu instance

View File

@@ -6,8 +6,9 @@
#include <stdint.h>
#include <stdio.h>
#include <furi.h>
#include <cli/cli.h>
#include <furi_hal_gpio.h>
#include <toolbox/cli/cli_command.h>
#include <cli/cli_main_commands.h>
#include <toolbox/pipe.h>
#include <furi_hal_vibro.h>

View File

@@ -1,8 +1,9 @@
#include "input.h"
#include <furi.h>
#include <cli/cli.h>
#include <cli/cli_commands.h>
#include <cli/cli_main_commands.h>
#include <toolbox/cli/cli_ansi.h>
#include <toolbox/cli/cli_command.h>
#include <toolbox/args.h>
#include <toolbox/pipe.h>
@@ -227,4 +228,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) {
furi_record_close(RECORD_INPUT_EVENTS);
}
CLI_COMMAND_INTERFACE(input, execute, CliCommandFlagParallelSafe, 1024);
CLI_COMMAND_INTERFACE(input, execute, CliCommandFlagParallelSafe, 1024, CLI_APPID);

View File

@@ -175,6 +175,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);
@@ -210,16 +217,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;
}
@@ -233,16 +236,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;
}
@@ -273,42 +272,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) {
@@ -345,12 +365,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;
}
@@ -417,9 +435,6 @@ static void loader_start_internal_app(
const FlipperInternalApplication* app,
const char* args) {
FURI_LOG_I(TAG, "Starting %s", app->name);
LoaderEvent event;
event.type = LoaderEventTypeApplicationBeforeLoad;
furi_pubsub_publish(loader->pubsub, &event);
if(app->flags & FlipperApplicationFlagUnloadAssetPacks) {
loader->app.unloaded_asset_packs = true;
asset_packs_free();
@@ -513,10 +528,6 @@ static LoaderMessageLoaderStatusResult loader_start_external_app(
result.value = loader_make_success_status(error_message);
result.error = LoaderStatusErrorUnknown;
LoaderEvent event;
event.type = LoaderEventTypeApplicationBeforeLoad;
furi_pubsub_publish(loader->pubsub, &event);
do {
loader->app.fap = flipper_application_alloc(storage, firmware_api_interface);
size_t start = furi_get_tick();
@@ -628,6 +639,7 @@ static LoaderMessageLoaderStatusResult loader_start_external_app(
if(result.value != LoaderStatusOk) {
flipper_application_free(loader->app.fap);
loader->app.fap = NULL;
LoaderEvent event;
event.type = LoaderEventTypeApplicationLoadFailed;
furi_pubsub_publish(loader->pubsub, &event);
if(loader->app.unloaded_asset_packs) {
@@ -681,6 +693,10 @@ static LoaderMessageLoaderStatusResult loader_do_start_by_name(
status.value = loader_make_success_status(error_message);
status.error = LoaderStatusErrorUnknown;
LoaderEvent event;
event.type = LoaderEventTypeApplicationBeforeLoad;
furi_pubsub_publish(loader->pubsub, &event);
do {
// check lock
if(loader_do_is_locked(loader)) {
@@ -745,6 +761,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;
}
@@ -762,6 +782,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);
@@ -786,6 +857,8 @@ 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;
@@ -794,6 +867,8 @@ static void loader_do_app_closed(Loader* loader) {
if(loader->app.unloaded_asset_packs) {
asset_packs_init();
}
loader_do_next_deferred_launch_if_available(loader);
}
static bool loader_is_application_running(Loader* loader) {
@@ -818,6 +893,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) {
@@ -840,16 +924,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);
@@ -891,6 +979,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;
}
}
}

View File

@@ -20,13 +20,19 @@ typedef enum {
typedef enum {
LoaderEventTypeApplicationBeforeLoad,
LoaderEventTypeApplicationLoadFailed,
LoaderEventTypeApplicationStopped
LoaderEventTypeApplicationStopped,
LoaderEventTypeNoMoreAppsInQueue, //<! The normal `Stopped` event still fires before this one
} LoaderEventType;
typedef struct {
LoaderEventType type;
} LoaderEvent;
typedef enum {
LoaderDeferredLaunchFlagNone = 0,
LoaderDeferredLaunchFlagGui = (1 << 1), //<! Report launch to the user via a dialog
} LoaderDeferredLaunchFlag;
/**
* @brief Start application
* @param[in] instance loader instance
@@ -99,7 +105,7 @@ FuriPubSub* loader_get_pubsub(Loader* instance);
*
* @param[in] instance pointer to the loader instance
* @param[in] signal signal value to be sent
* @param[in,out] arg optional argument (can be of any value, including NULL)
* @param[inout] arg optional argument (can be of any value, including NULL)
*
* @return true if the signal was handled by the application, false otherwise
*/
@@ -109,11 +115,49 @@ bool loader_signal(Loader* instance, uint32_t signal, void* arg);
* @brief Get the name of the currently running application
*
* @param[in] instance pointer to the loader instance
* @param[in,out] name pointer to the string to contain the name (must be allocated)
* @param[inout] name pointer to the string to contain the name (must be allocated)
* @return true if it was possible to get an application name, false otherwise
*/
bool loader_get_application_name(Loader* instance, FuriString* name);
/**
* @brief Get the launch path or name of the currently running application
*
* This is the string that was supplied to `loader_start` such that the current
* app is running now. It might be a name (in the case of internal apps) or a
* path (in the case of external apps). This value can be used to launch the
* same app again.
*
* @param[in] instance pointer to the loader instance
* @param[inout] name pointer to the string to contain the path or name
* (must be allocated)
* @return true if it was possible to get an application path, false otherwise
*/
bool loader_get_application_launch_path(Loader* instance, FuriString* name);
/**
* @brief Enqueues a request to launch an application after the current one
*
* @param[in] instance pointer to the loader instance
* @param[in] name pointer to the name or path of the application, or NULL to
* cancel a previous request
* @param[in] args pointer to argument to provide to the next app
* @param[in] flags additional flags. see enum documentation for more info
*/
void loader_enqueue_launch(
Loader* instance,
const char* name,
const char* args,
LoaderDeferredLaunchFlag flags);
/**
* @brief Removes all requests to launch applications after the current one
* exits
*
* @param[in] instance pointer to the loader instance
*/
void loader_clear_launch_queue(Loader* instance);
#ifdef __cplusplus
}
#endif

View File

@@ -124,7 +124,7 @@ static void loader_pubsub_callback(const void* message, void* context) {
const LoaderEvent* event = message;
const FuriThreadId thread_id = (FuriThreadId)context;
if(event->type == LoaderEventTypeApplicationStopped) {
if(event->type == LoaderEventTypeNoMoreAppsInQueue) {
furi_thread_flags_set(thread_id, APPLICATION_STOP_EVENT);
}
}

View File

@@ -1,8 +1,8 @@
#include "loader.h"
#include <furi.h>
#include <cli/cli.h>
#include <cli/cli_commands.h>
#include <toolbox/cli/cli_command.h>
#include <cli/cli_main_commands.h>
#include <applications.h>
#include <lib/toolbox/args.h>
#include <lib/toolbox/strint.h>
@@ -143,4 +143,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) {
furi_record_close(RECORD_LOADER);
}
CLI_COMMAND_INTERFACE(loader, execute, CliCommandFlagParallelSafe, 1024);
CLI_COMMAND_INTERFACE(loader, execute, CliCommandFlagParallelSafe, 1024, CLI_APPID);

View File

@@ -2,11 +2,20 @@
#include <furi.h>
#include <toolbox/api_lock.h>
#include <flipper_application/flipper_application.h>
#include <gui/gui.h>
#include <gui/view_holder.h>
#include <gui/modules/loading.h>
#include <m-array.h>
#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;
@@ -21,6 +30,12 @@ struct Loader {
LoaderMenu* loader_menu;
LoaderApplications* loader_applications;
LoaderAppData app;
LoaderLaunchQueue launch_queue;
Gui* gui;
ViewHolder* view_holder;
Loading* loading;
};
typedef enum {
@@ -35,6 +50,9 @@ typedef enum {
LoaderMessageTypeStartByNameDetachedWithGuiError,
LoaderMessageTypeSignal,
LoaderMessageTypeGetApplicationName,
LoaderMessageTypeGetApplicationLaunchPath,
LoaderMessageTypeEnqueueLaunch,
LoaderMessageTypeClearLaunchQueue,
LoaderMessageTypeShowSettings,
} LoaderMessageType;
@@ -76,6 +94,7 @@ typedef struct {
union {
LoaderMessageStartByName start;
LoaderDeferredLaunchRecord defer_start;
LoaderMessageSignal signal;
FuriString* application_name;
};

View File

@@ -4,6 +4,7 @@
#include <gui/modules/submenu.h>
#include <assets_icons.h>
#include <applications.h>
#include <archive/helpers/archive_favorites.h>
#include <toolbox/run_parallel.h>
#include "loader.h"
@@ -55,9 +56,7 @@ static void loader_pubsub_callback(const void* message, void* context) {
furi_thread_free(loader_menu->thread);
loader_menu->thread = NULL;
}
} else if(
event->type == LoaderEventTypeApplicationLoadFailed ||
event->type == LoaderEventTypeApplicationStopped) {
} else if(event->type == LoaderEventTypeNoMoreAppsInQueue) {
if(!loader_menu->thread) {
loader_menu->thread = furi_thread_alloc_ex(TAG, 2048, loader_menu_thread, loader_menu);
furi_thread_start(loader_menu->thread);
@@ -170,25 +169,37 @@ static void loader_menu_applications_callback(void* context, uint32_t index) {
loader_menu_start(name);
}
static void loader_menu_settings_menu_callback(void* context, uint32_t index) {
// Can't do this in GUI callbacks because now ViewHolder waits for ongoing
// input, and inputs are not processed because GUI is processing callbacks
static int32_t loader_menu_setting_pin_unpin_parallel(void* context) {
const char* name = context;
archive_favorites_handle_setting_pin_unpin(name, NULL);
return 0;
}
static void
loader_menu_settings_menu_callback(void* context, InputType input_type, uint32_t index) {
UNUSED(context);
const char* name = FLIPPER_SETTINGS_APPS[index].name;
// Workaround for SD format when app can't be opened
if(!strcmp(name, "Storage")) {
Storage* storage = furi_record_open(RECORD_STORAGE);
FS_Error status = storage_sd_status(storage);
furi_record_close(RECORD_STORAGE);
// If SD card not ready, cannot be formatted, so we want loader to give
// normal error message, with function below
if(status != FSE_NOT_READY) {
// Attempt to launch the app, and if failed offer to format SD card
run_parallel(loader_menu_storage_settings, storage, 512);
return;
if(input_type == InputTypeShort) {
// Workaround for SD format when app can't be opened
if(!strcmp(name, "Storage")) {
Storage* storage = furi_record_open(RECORD_STORAGE);
FS_Error status = storage_sd_status(storage);
furi_record_close(RECORD_STORAGE);
// If SD card not ready, cannot be formatted, so we want loader to give
// normal error message, with function below
if(status != FSE_NOT_READY) {
// Attempt to launch the app, and if failed offer to format SD card
run_parallel(loader_menu_storage_settings, storage, 512);
return;
}
}
loader_menu_start(name);
} else if(input_type == InputTypeLong) {
run_parallel(loader_menu_setting_pin_unpin_parallel, (void*)name, 512);
}
loader_menu_start(name);
}
// Can't do this in GUI callbacks because now ViewHolder waits for ongoing
@@ -357,7 +368,7 @@ static void loader_menu_build_menu(LoaderMenuApp* app, LoaderMenu* menu) {
static void loader_menu_build_submenu(LoaderMenuApp* app, LoaderMenu* loader_menu) {
for(size_t i = 0; i < FLIPPER_SETTINGS_APPS_COUNT; i++) {
submenu_add_item(
submenu_add_item_ex(
app->settings_menu,
FLIPPER_SETTINGS_APPS[i].name,
i,

View File

@@ -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;
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include <furi.h>
#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);

View File

@@ -4,6 +4,6 @@ App(
apptype=FlipperAppType.STARTUP,
entry_point="locale_on_system_start",
cdefines=["SRV_LOCALE"],
order=90,
order=70,
sdk_headers=["locale.h"],
)

View File

@@ -1,8 +1,8 @@
#include "power_cli.h"
#include <furi_hal.h>
#include <cli/cli.h>
#include <cli/cli_commands.h>
#include <toolbox/cli/cli_command.h>
#include <cli/cli_main_commands.h>
#include <lib/toolbox/args.h>
#include <power/power_service/power.h>
#include <toolbox/pipe.h>
@@ -113,4 +113,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) {
furi_string_free(cmd);
}
CLI_COMMAND_INTERFACE(power, execute, CliCommandFlagParallelSafe, 1024);
CLI_COMMAND_INTERFACE(power, execute, CliCommandFlagParallelSafe, 1024, CLI_APPID);

View File

@@ -462,9 +462,7 @@ static void power_loader_callback(const void* message, void* context) {
power->app_running = true;
power_auto_poweroff_disarm(power);
// arm timer if some apps was not loaded or was stoped
} else if(
event->type == LoaderEventTypeApplicationLoadFailed ||
event->type == LoaderEventTypeApplicationStopped) {
} else if(event->type == LoaderEventTypeNoMoreAppsInQueue) {
power->app_running = false;
power_auto_poweroff_arm(power);
}

View File

@@ -6,5 +6,5 @@ App(
entry_point="region_on_system_start",
cdefines=["SRV_REGION"],
requires=["storage"],
order=170,
order=120,
)

View File

@@ -10,7 +10,8 @@
#include <furi.h>
#include <furi_hal_rtc.h>
#include <cli/cli.h>
#include <toolbox/cli/cli_command.h>
#include <cli/cli_main_commands.h>
#include <stdint.h>
#include <stdio.h>
#include <m-dict.h>
@@ -447,9 +448,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);
}

View File

@@ -1,4 +1,5 @@
#include <cli/cli.h>
#include <toolbox/cli/cli_command.h>
#include <cli/cli_main_commands.h>
#include <furi.h>
#include <rpc/rpc.h>
#include <furi_hal.h>

View File

@@ -5,7 +5,6 @@
#include <pb_decode.h>
#include <pb_encode.h>
#include <flipper.pb.h>
#include <cli/cli.h>
#include <toolbox/pipe.h>
#ifdef __cplusplus

View File

@@ -16,5 +16,5 @@ App(
apptype=FlipperAppType.STARTUP,
entry_point="storage_on_system_start",
requires=["storage"],
order=90,
order=60,
)

View File

@@ -1,7 +1,9 @@
#include <furi.h>
#include <furi_hal.h>
#include <cli/cli.h>
#include <toolbox/cli/cli_command.h>
#include <toolbox/cli/cli_ansi.h>
#include <cli/cli_main_commands.h>
#include <lib/toolbox/args.h>
#include <lib/toolbox/dir_walk.h>
#include <lib/toolbox/md5_calc.h>
@@ -721,16 +723,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);