1
mirror of https://github.com/DarkFlippers/unleashed-firmware.git synced 2025-12-12 12:42:30 +04:00

cli: Buzzer command (#4006)

* Add args_read_float_and_trim function

* Add args_read_duration function

* Add notes_frequency_from_name function

* Add cli_sleep function and sleep CLI command

* Update CLI top command to use cli_sleep

* Add buzzer CLI command

* toolbox: make args_read_duration less convoluted

* notification: make notification_messages_notes_frequency_from_name less convoluted

* unit_tests: better float checking

* fix formatting and f18

---------

Co-authored-by: Anna Antonenko <portasynthinca3@gmail.com>
Co-authored-by: hedger <hedger@nanode.su>
This commit is contained in:
Ivan Barsukov
2025-09-29 20:53:10 +03:00
committed by GitHub
parent f78a8328d1
commit 1e0f3a606f
15 changed files with 747 additions and 23 deletions

View File

@@ -244,3 +244,20 @@ App(
entry_point="get_api",
requires=["unit_tests"],
)
App(
appid="test_args",
sources=["tests/common/*.c", "tests/args/*.c"],
apptype=FlipperAppType.PLUGIN,
entry_point="get_api",
requires=["unit_tests"],
)
App(
appid="test_notification",
sources=["tests/common/*.c", "tests/notification/*.c"],
apptype=FlipperAppType.PLUGIN,
entry_point="get_api",
requires=["unit_tests"],
)

View File

@@ -0,0 +1,211 @@
#include "../test.h" // IWYU pragma: keep
#include <toolbox/args.h>
const uint32_t one_ms = 1;
const uint32_t one_s = 1000 * one_ms;
const uint32_t one_m = 60 * one_s;
const uint32_t one_h = 60 * one_m;
MU_TEST(args_read_duration_default_values_test) {
FuriString* args_string;
uint32_t value = 0;
// Check default == NULL (ms)
args_string = furi_string_alloc_set_str("1");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, one_ms);
furi_string_free(args_string);
value = 0;
// Check default == ms
args_string = furi_string_alloc_set_str("1");
mu_check(args_read_duration(args_string, &value, "ms"));
mu_assert_int_eq(value, one_ms);
furi_string_free(args_string);
value = 0;
// Check default == s
args_string = furi_string_alloc_set_str("1");
mu_check(args_read_duration(args_string, &value, "s"));
mu_assert_int_eq(value, one_s);
furi_string_free(args_string);
value = 0;
// Check default == m
args_string = furi_string_alloc_set_str("1");
mu_check(args_read_duration(args_string, &value, "m"));
mu_assert_int_eq(value, one_m);
furi_string_free(args_string);
value = 0;
// Check default == h
args_string = furi_string_alloc_set_str("1");
mu_check(args_read_duration(args_string, &value, "h"));
mu_assert_int_eq(value, one_h);
furi_string_free(args_string);
value = 0;
}
MU_TEST(args_read_duration_suffix_values_test) {
FuriString* args_string;
uint32_t value = 0;
// Check ms
args_string = furi_string_alloc_set_str("1ms");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, one_ms);
furi_string_free(args_string);
value = 0;
// Check s
args_string = furi_string_alloc_set_str("1s");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, one_s);
furi_string_free(args_string);
value = 0;
// Check m
args_string = furi_string_alloc_set_str("1m");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, one_m);
furi_string_free(args_string);
value = 0;
// Check h
args_string = furi_string_alloc_set_str("1h");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, one_h);
furi_string_free(args_string);
value = 0;
}
MU_TEST(args_read_duration_values_test) {
FuriString* args_string;
uint32_t value = 0;
// Check for ms
args_string = furi_string_alloc_set_str("4294967295ms");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 4294967295U);
furi_string_free(args_string);
// Check for s
args_string = furi_string_alloc_set_str("4294967s");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 4294967U * one_s);
furi_string_free(args_string);
// Check for m
args_string = furi_string_alloc_set_str("71582m");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 71582U * one_m);
furi_string_free(args_string);
// Check for h
args_string = furi_string_alloc_set_str("1193h");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 1193U * one_h);
furi_string_free(args_string);
// Check for ms in float
args_string = furi_string_alloc_set_str("4.2ms");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 4);
furi_string_free(args_string);
// Check for s in float
args_string = furi_string_alloc_set_str("1.5s");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, (uint32_t)(1.5 * one_s));
furi_string_free(args_string);
// Check for m in float
args_string = furi_string_alloc_set_str("1.5m");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, (uint32_t)(1.5 * one_m));
furi_string_free(args_string);
// Check for h in float
args_string = furi_string_alloc_set_str("1.5h");
mu_check(args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, (uint32_t)(1.5 * one_h));
furi_string_free(args_string);
}
MU_TEST(args_read_duration_errors_test) {
FuriString* args_string;
uint32_t value = 0;
// Check wrong suffix
args_string = furi_string_alloc_set_str("1x");
mu_check(!args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 0);
furi_string_free(args_string);
// Check wrong suffix
args_string = furi_string_alloc_set_str("1xs");
mu_check(!args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 0);
furi_string_free(args_string);
// Check negative value
args_string = furi_string_alloc_set_str("-1s");
mu_check(!args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 0);
furi_string_free(args_string);
// Check wrong values
// Check only suffix
args_string = furi_string_alloc_set_str("s");
mu_check(!args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 0);
furi_string_free(args_string);
// Check doubled point
args_string = furi_string_alloc_set_str("0.1.1");
mu_check(!args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 0);
furi_string_free(args_string);
// Check overflow values
// Check for ms
args_string = furi_string_alloc_set_str("4294967296ms");
mu_check(!args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 0);
furi_string_free(args_string);
// Check for s
args_string = furi_string_alloc_set_str("4294968s");
mu_check(!args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 0);
furi_string_free(args_string);
// Check for m
args_string = furi_string_alloc_set_str("71583m");
mu_check(!args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 0);
furi_string_free(args_string);
// Check for h
args_string = furi_string_alloc_set_str("1194h");
mu_check(!args_read_duration(args_string, &value, NULL));
mu_assert_int_eq(value, 0);
furi_string_free(args_string);
}
MU_TEST_SUITE(toolbox_args_read_duration_suite) {
MU_RUN_TEST(args_read_duration_default_values_test);
MU_RUN_TEST(args_read_duration_suffix_values_test);
MU_RUN_TEST(args_read_duration_values_test);
MU_RUN_TEST(args_read_duration_errors_test);
}
int run_minunit_test_toolbox_args(void) {
MU_RUN_SUITE(toolbox_args_read_duration_suite);
return MU_EXIT_CODE;
}
TEST_API_DEFINE(run_minunit_test_toolbox_args)

View File

@@ -389,8 +389,8 @@ void minunit_printf_warning(const char* format, ...);
__func__, \
__FILE__, \
__LINE__, \
minunit_tmp_e, \
minunit_tmp_r, \
minunit_tmp_e, \
minunit_tmp_m); \
minunit_status = 1; \
return; \

View File

@@ -0,0 +1,165 @@
#include "../test.h" // IWYU pragma: keep
#include <float_tools.h>
#include <notification/notification_messages_notes.h>
void frequency_assert(const char* note_name, const NotificationMessage* message) {
double a = notification_messages_notes_frequency_from_name(note_name);
double b = message->data.sound.frequency;
const double epsilon = message->data.sound.frequency > 5000 ? 0.02f : 0.01f;
mu_assert_double_between(b - epsilon, b + epsilon, a);
}
MU_TEST(notification_messages_notes_frequency_from_name_test) {
// Upper case
mu_check(float_is_equal(
notification_messages_notes_frequency_from_name("C0"),
notification_messages_notes_frequency_from_name("c0")));
// Mixed case
mu_check(float_is_equal(
notification_messages_notes_frequency_from_name("Cs0"),
notification_messages_notes_frequency_from_name("cs0")));
// Check errors
mu_check(
float_is_equal(notification_messages_notes_frequency_from_name("0"), 0.0)); // Without note
mu_check(float_is_equal(
notification_messages_notes_frequency_from_name("C"), 0.0)); // Without octave
mu_check(float_is_equal(
notification_messages_notes_frequency_from_name("C9"), 0.0)); // Unsupported octave
mu_check(float_is_equal(
notification_messages_notes_frequency_from_name("C10"), 0.0)); // Unsupported octave
mu_check(float_is_equal(
notification_messages_notes_frequency_from_name("X0"), 0.0)); // Unknown note
mu_check(float_is_equal(
notification_messages_notes_frequency_from_name("CCC0"), 0.0)); // Note name overflow
// Notes and structures
frequency_assert("c0", &message_note_c0);
frequency_assert("cs0", &message_note_cs0);
frequency_assert("d0", &message_note_d0);
frequency_assert("ds0", &message_note_ds0);
frequency_assert("e0", &message_note_e0);
frequency_assert("f0", &message_note_f0);
frequency_assert("fs0", &message_note_fs0);
frequency_assert("g0", &message_note_g0);
frequency_assert("gs0", &message_note_gs0);
frequency_assert("a0", &message_note_a0);
frequency_assert("as0", &message_note_as0);
frequency_assert("b0", &message_note_b0);
frequency_assert("c1", &message_note_c1);
frequency_assert("cs1", &message_note_cs1);
frequency_assert("d1", &message_note_d1);
frequency_assert("ds1", &message_note_ds1);
frequency_assert("e1", &message_note_e1);
frequency_assert("f1", &message_note_f1);
frequency_assert("fs1", &message_note_fs1);
frequency_assert("g1", &message_note_g1);
frequency_assert("gs1", &message_note_gs1);
frequency_assert("a1", &message_note_a1);
frequency_assert("as1", &message_note_as1);
frequency_assert("b1", &message_note_b1);
frequency_assert("c2", &message_note_c2);
frequency_assert("cs2", &message_note_cs2);
frequency_assert("d2", &message_note_d2);
frequency_assert("ds2", &message_note_ds2);
frequency_assert("e2", &message_note_e2);
frequency_assert("f2", &message_note_f2);
frequency_assert("fs2", &message_note_fs2);
frequency_assert("g2", &message_note_g2);
frequency_assert("gs2", &message_note_gs2);
frequency_assert("a2", &message_note_a2);
frequency_assert("as2", &message_note_as2);
frequency_assert("b2", &message_note_b2);
frequency_assert("c3", &message_note_c3);
frequency_assert("cs3", &message_note_cs3);
frequency_assert("d3", &message_note_d3);
frequency_assert("ds3", &message_note_ds3);
frequency_assert("e3", &message_note_e3);
frequency_assert("f3", &message_note_f3);
frequency_assert("fs3", &message_note_fs3);
frequency_assert("g3", &message_note_g3);
frequency_assert("gs3", &message_note_gs3);
frequency_assert("a3", &message_note_a3);
frequency_assert("as3", &message_note_as3);
frequency_assert("b3", &message_note_b3);
frequency_assert("c4", &message_note_c4);
frequency_assert("cs4", &message_note_cs4);
frequency_assert("d4", &message_note_d4);
frequency_assert("ds4", &message_note_ds4);
frequency_assert("e4", &message_note_e4);
frequency_assert("f4", &message_note_f4);
frequency_assert("fs4", &message_note_fs4);
frequency_assert("g4", &message_note_g4);
frequency_assert("gs4", &message_note_gs4);
frequency_assert("a4", &message_note_a4);
frequency_assert("as4", &message_note_as4);
frequency_assert("b4", &message_note_b4);
frequency_assert("c5", &message_note_c5);
frequency_assert("cs5", &message_note_cs5);
frequency_assert("d5", &message_note_d5);
frequency_assert("ds5", &message_note_ds5);
frequency_assert("e5", &message_note_e5);
frequency_assert("f5", &message_note_f5);
frequency_assert("fs5", &message_note_fs5);
frequency_assert("g5", &message_note_g5);
frequency_assert("gs5", &message_note_gs5);
frequency_assert("a5", &message_note_a5);
frequency_assert("as5", &message_note_as5);
frequency_assert("b5", &message_note_b5);
frequency_assert("c6", &message_note_c6);
frequency_assert("cs6", &message_note_cs6);
frequency_assert("d6", &message_note_d6);
frequency_assert("ds6", &message_note_ds6);
frequency_assert("e6", &message_note_e6);
frequency_assert("f6", &message_note_f6);
frequency_assert("fs6", &message_note_fs6);
frequency_assert("g6", &message_note_g6);
frequency_assert("gs6", &message_note_gs6);
frequency_assert("a6", &message_note_a6);
frequency_assert("as6", &message_note_as6);
frequency_assert("b6", &message_note_b6);
frequency_assert("c7", &message_note_c7);
frequency_assert("cs7", &message_note_cs7);
frequency_assert("d7", &message_note_d7);
frequency_assert("ds7", &message_note_ds7);
frequency_assert("e7", &message_note_e7);
frequency_assert("f7", &message_note_f7);
frequency_assert("fs7", &message_note_fs7);
frequency_assert("g7", &message_note_g7);
frequency_assert("gs7", &message_note_gs7);
frequency_assert("a7", &message_note_a7);
frequency_assert("as7", &message_note_as7);
frequency_assert("b7", &message_note_b7);
frequency_assert("c8", &message_note_c8);
frequency_assert("cs8", &message_note_cs8);
frequency_assert("d8", &message_note_d8);
frequency_assert("ds8", &message_note_ds8);
frequency_assert("e8", &message_note_e8);
frequency_assert("f8", &message_note_f8);
frequency_assert("fs8", &message_note_fs8);
frequency_assert("g8", &message_note_g8);
frequency_assert("gs8", &message_note_gs8);
frequency_assert("a8", &message_note_a8);
frequency_assert("as8", &message_note_as8);
frequency_assert("b8", &message_note_b8);
}
MU_TEST_SUITE(notes_suite) {
MU_RUN_TEST(notification_messages_notes_frequency_from_name_test);
}
int run_minunit_test_notes(void) {
MU_RUN_SUITE(notes_suite);
return MU_EXIT_CODE;
}
TEST_API_DEFINE(run_minunit_test_notes)

View File

@@ -49,3 +49,11 @@ App(
requires=["cli"],
sources=["commands/subshell_demo.c"],
)
App(
appid="cli_buzzer",
apptype=FlipperAppType.PLUGIN,
entry_point="cli_buzzer_ep",
requires=["cli"],
sources=["commands/buzzer.c"],
)

View File

@@ -356,11 +356,13 @@ void cli_command_led(PipeSide* pipe, FuriString* args, void* context) {
static void cli_command_top(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(context);
int interval = 1000;
args_read_int_and_trim(args, &interval);
uint32_t interval;
if(!args_read_duration(args, &interval, NULL)) {
interval = 1000;
}
FuriThreadList* thread_list = furi_thread_list_alloc();
while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) {
do {
uint32_t tick = furi_get_tick();
furi_thread_enumerate(thread_list);
@@ -416,12 +418,8 @@ static void cli_command_top(PipeSide* pipe, FuriString* args, void* context) {
printf(ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END));
fflush(stdout);
if(interval > 0) {
furi_delay_ms(interval);
} else {
break;
}
}
} while(interval > 0 && cli_sleep(pipe, interval));
furi_thread_list_free(thread_list);
}
@@ -491,6 +489,37 @@ void cli_command_echo(PipeSide* pipe, FuriString* args, void* context) {
}
}
/**
* @brief Pause for a specified duration or until Ctrl+C is pressed or the
* session is terminated.
*
* The duration can be specified in various units such as milliseconds (ms),
* seconds (s), minutes (m), or hours (h). If the unit is not specified, the
* second is used by default.
*
* Example:
* sleep 5s
*/
void cli_command_sleep(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(context);
FuriString* duration_string;
duration_string = furi_string_alloc();
do {
uint32_t duration_in_ms = 0;
if(!args_read_string_and_trim(args, duration_string) ||
!args_read_duration(duration_string, &duration_in_ms, "s")) {
cli_print_usage("sleep", "[<0-...>[<ms|s|m|h>]]", furi_string_get_cstr(args));
break;
}
cli_sleep(pipe, duration_in_ms);
} while(false);
furi_string_free(duration_string);
}
void cli_main_commands_init(CliRegistry* registry) {
cli_registry_add_command(
registry, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true);
@@ -508,6 +537,8 @@ void cli_main_commands_init(CliRegistry* registry) {
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_registry_add_command(
registry, "sleep", CliCommandFlagParallelSafe, cli_command_sleep, NULL);
cli_registry_add_command(registry, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL);
cli_registry_add_command(registry, "led", CliCommandFlagDefault, cli_command_led, NULL);

View File

@@ -0,0 +1,135 @@
#include "../cli_main_commands.h"
#include <notification/notification_app.h>
#include <toolbox/args.h>
#include <toolbox/cli/cli_command.h>
void cli_command_buzzer_print_usage(bool is_freq_subcommand, FuriString* args) {
if(is_freq_subcommand) {
cli_print_usage(
"buzzer freq", "<freq_hz> [<0-...>[<ms|s|m|h>]]", furi_string_get_cstr(args));
} else {
cli_print_usage("buzzer note", "<note> [<0-...>[<ms|s|m|h>]]", furi_string_get_cstr(args));
}
}
float cli_command_buzzer_read_frequency(bool is_freq_subcommand, FuriString* args) {
float frequency = 0.0f;
if(is_freq_subcommand) {
args_read_float_and_trim(args, &frequency);
return frequency;
}
// Extract note frequency from name
FuriString* note_name_string;
note_name_string = furi_string_alloc();
do {
if(!args_read_string_and_trim(args, note_name_string)) {
break;
}
const char* note_name = furi_string_get_cstr(note_name_string);
frequency = notification_messages_notes_frequency_from_name(note_name);
} while(false);
furi_string_free(note_name_string);
return frequency;
}
void cli_command_buzzer_play(
PipeSide* pipe,
NotificationApp* notification,
bool is_freq_subcommand,
FuriString* args) {
FuriString* duration_string;
duration_string = furi_string_alloc();
do {
float frequency = cli_command_buzzer_read_frequency(is_freq_subcommand, args);
if(frequency <= 0.0f) {
cli_command_buzzer_print_usage(is_freq_subcommand, args);
break;
}
const NotificationMessage notification_buzzer_message = {
.type = NotificationMessageTypeSoundOn,
.data.sound.frequency = frequency,
.data.sound.volume = 1.0,
};
// Optional duration
uint32_t duration_ms = 100;
if(args_read_string_and_trim(args, duration_string)) {
if(!args_read_duration(duration_string, &duration_ms, NULL)) {
cli_command_buzzer_print_usage(is_freq_subcommand, args);
break;
}
}
const NotificationSequence sound_on_sequence = {
&notification_buzzer_message,
&message_do_not_reset,
NULL,
};
// Play sound
notification_message_block(notification, &sound_on_sequence);
cli_sleep(pipe, duration_ms);
// Stop sound
const NotificationSequence sound_off_sequence = {
&message_sound_off,
NULL,
};
notification_message_block(notification, &sound_off_sequence);
} while(false);
furi_string_free(duration_string);
}
void execute(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(context);
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
FuriString* command_string;
command_string = furi_string_alloc();
do {
if(!args_read_string_and_trim(args, command_string)) {
cli_print_usage("buzzer", "<freq|note>", furi_string_get_cstr(args));
break;
}
// Check volume
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStealthMode)) {
printf("Flipper is in stealth mode. Unmute the device to control buzzer.");
break;
}
if(notification->settings.speaker_volume == 0.0f) {
printf("Sound is disabled in settings. Increase volume to control buzzer.");
break;
}
if(furi_string_cmp(command_string, "freq") == 0) {
cli_command_buzzer_play(pipe, notification, true, args);
break;
} else if(furi_string_cmp(command_string, "note") == 0) {
cli_command_buzzer_play(pipe, notification, false, args);
break;
}
cli_print_usage("buzzer", "<freq|note>", furi_string_get_cstr(args));
} while(false);
furi_string_free(command_string);
furi_record_close(RECORD_NOTIFICATION);
}
CLI_COMMAND_INTERFACE(buzzer, execute, CliCommandFlagDefault, 2048, CLI_APPID);

View File

@@ -1,4 +1,5 @@
#include "notification.h"
#include <toolbox/strint.h>
/*
Python script for note messages generation
@@ -571,3 +572,35 @@ const NotificationMessage message_note_b8 = {
.data.sound.frequency = 7902.13f,
.data.sound.volume = 1.0f,
};
float notification_messages_notes_frequency_from_name(const char* note_name) {
const float base_note = 16.3515979f; // C0
const char* note_names[] = {"c", "cs", "d", "ds", "e", "f", "fs", "g", "gs", "a", "as", "b"};
const size_t notes_count = COUNT_OF(note_names);
char note_wo_octave[3] = {0};
for(size_t i = 0; i < sizeof(note_wo_octave) - 1; i++) {
char in = *note_name;
if(!in) break;
if(!isalpha(in)) break;
note_wo_octave[i] = in;
note_name++;
}
int note_index = -1;
for(size_t i = 0; i < notes_count; i++) {
if(strcasecmp(note_wo_octave, note_names[i]) == 0) note_index = i;
}
if(note_index < 0) return 0.0;
uint16_t octave;
StrintParseError error = strint_to_uint16(note_name, NULL, &octave, 10);
if(error != StrintParseNoError) return 0.0;
if(octave > 8) return 0.0;
int semitone_index = octave * notes_count + note_index;
float frequency = base_note * powf(2.0f, semitone_index / 12.0f);
return roundf(frequency * 100) / 100.0f;
}

View File

@@ -115,6 +115,17 @@ extern const NotificationMessage message_note_a8;
extern const NotificationMessage message_note_as8;
extern const NotificationMessage message_note_b8;
/**
* @brief Returns the frequency of the given note
*
* This function calculates and returns the frequency (in Hz) of the specified note.
* If the input note name is invalid, the function returns 0.0.
*
* @param [in] note_name The name of the note (e.g., "A4", cs5")
* @return The frequency of the note in Hz, or 0.0 if the note name is invalid
*/
extern float notification_messages_notes_frequency_from_name(const char* note_name);
#ifdef __cplusplus
}
#endif

View File

@@ -2,6 +2,7 @@
#include "hex.h"
#include "strint.h"
#include "m-core.h"
#include <errno.h>
size_t args_get_first_word_length(FuriString* args) {
size_t ws = furi_string_search_char(args, ' ');
@@ -34,6 +35,24 @@ bool args_read_int_and_trim(FuriString* args, int* value) {
return false;
}
bool args_read_float_and_trim(FuriString* args, float* value) {
size_t cmd_length = args_get_first_word_length(args);
if(cmd_length == 0) {
return false;
}
char* end_ptr;
float temp = strtof(furi_string_get_cstr(args), &end_ptr);
if(end_ptr == furi_string_get_cstr(args)) {
return false;
}
*value = temp;
furi_string_right(args, cmd_length);
furi_string_trim(args);
return true;
}
bool args_read_string_and_trim(FuriString* args, FuriString* word) {
size_t cmd_length = args_get_first_word_length(args);
@@ -97,3 +116,38 @@ bool args_read_hex_bytes(FuriString* args, uint8_t* bytes, size_t bytes_count) {
return result;
}
bool args_read_duration(FuriString* args, uint32_t* value, const char* default_unit) {
const char* args_cstr = furi_string_get_cstr(args);
const char* unit;
errno = 0;
double duration_ms = strtod(args_cstr, (char**)&unit);
if(errno) return false;
if(duration_ms < 0) return false;
if(unit == args_cstr) return false;
if(strcmp(unit, "") == 0) {
unit = default_unit;
if(!unit) unit = "ms";
}
uint32_t multiplier;
if(strcasecmp(unit, "ms") == 0) {
multiplier = 1;
} else if(strcasecmp(unit, "s") == 0) {
multiplier = 1000;
} else if(strcasecmp(unit, "m") == 0) {
multiplier = 60 * 1000;
} else if(strcasecmp(unit, "h") == 0) {
multiplier = 60 * 60 * 1000;
} else {
return false;
}
const uint32_t max_pre_multiplication = UINT32_MAX / multiplier;
if(duration_ms > max_pre_multiplication) return false;
*value = round(duration_ms * multiplier);
return true;
}

View File

@@ -11,12 +11,21 @@ extern "C" {
/** Extract int value and trim arguments string
*
* @param args - arguments string
* @param word first argument, output
* @param value first argument, output
* @return true - success
* @return false - arguments string does not contain int
*/
bool args_read_int_and_trim(FuriString* args, int* value);
/** Extract float value and trim arguments string
*
* @param [in, out] args arguments string
* @param [out] value first argument
* @return true - success
* @return false - arguments string does not contain float
*/
bool args_read_float_and_trim(FuriString* args, float* value);
/**
* @brief Extract first argument from arguments string and trim arguments string
*
@@ -48,6 +57,18 @@ bool args_read_probably_quoted_string_and_trim(FuriString* args, FuriString* wor
*/
bool args_read_hex_bytes(FuriString* args, uint8_t* bytes, size_t bytes_count);
/**
* @brief Parses a duration value from a given string and converts it to milliseconds
*
* @param [in] args the input string containing the duration value. The string may include units (e.g., "10s", "0.5m").
* @param [out] value pointer to store the parsed value in milliseconds
* @param [in] default_unit A default unit to be used if the input string does not contain a valid suffix.
* Supported units: `"ms"`, `"s"`, `"m"`, `"h"`
* If NULL, the function will assume milliseconds by default.
* @return `true` if the parsing and conversion succeeded, `false` otherwise.
*/
bool args_read_duration(FuriString* args, uint32_t* value, const char* default_unit);
/************************************ HELPERS ***************************************/
/**

View File

@@ -15,3 +15,18 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) {
printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage);
}
bool cli_sleep(PipeSide* side, uint32_t duration_in_ms) {
uint32_t passed_time = 0;
bool is_interrupted = false;
do {
uint32_t left_time = duration_in_ms - passed_time;
uint32_t check_interval = left_time >= 100 ? 100 : left_time;
furi_delay_ms(check_interval);
passed_time += check_interval;
is_interrupted = cli_is_pipe_broken_or_is_etx_next_char(side);
} while(!is_interrupted && passed_time < duration_in_ms);
return !is_interrupted;
}

View File

@@ -80,6 +80,21 @@ bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side);
*/
void cli_print_usage(const char* cmd, const char* usage, const char* arg);
/**
* @brief Pause for a specified duration or until Ctrl+C is pressed or the
* session is terminated.
*
* @param [in] side Pointer to pipe side given to the command thread.
* @param [in] duration_in_ms Duration of sleep in milliseconds.
* @return `true` if the sleep completed without interruption.
* @return `false` if interrupted.
*
* @warning This function also assumes that the pipe is installed as the
* thread's stdio.
* @warning This function will consume 0 or 1 bytes from the pipe.
*/
bool cli_sleep(PipeSide* side, uint32_t duration_in_ms);
#define CLI_COMMAND_INTERFACE(name, execute_callback, flags, stack_depth, app_id) \
static const CliCommandDescriptor cli_##name##_desc = { \
#name, \

View File

@@ -546,6 +546,8 @@ Function,-,arc4random_uniform,__uint32_t,__uint32_t
Function,+,args_char_to_hex,_Bool,"char, char, uint8_t*"
Function,+,args_get_first_word_length,size_t,FuriString*
Function,+,args_length,size_t,FuriString*
Function,+,args_read_duration,_Bool,"FuriString*, uint32_t*, const char*"
Function,+,args_read_float_and_trim,_Bool,"FuriString*, float*"
Function,+,args_read_hex_bytes,_Bool,"FuriString*, uint8_t*, size_t"
Function,+,args_read_int_and_trim,_Bool,"FuriString*, int*"
Function,+,args_read_probably_quoted_string_and_trim,_Bool,"FuriString*, FuriString*"
@@ -802,6 +804,7 @@ Function,+,cli_shell_free,void,CliShell*
Function,+,cli_shell_join,void,CliShell*
Function,+,cli_shell_set_prompt,void,"CliShell*, const char*"
Function,+,cli_shell_start,void,CliShell*
Function,+,cli_sleep,_Bool,"PipeSide*, uint32_t"
Function,+,cli_vcp_disable,void,CliVcp*
Function,+,cli_vcp_enable,void,CliVcp*
Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*"
@@ -2267,6 +2270,7 @@ Function,+,notification_internal_message,void,"NotificationApp*, const Notificat
Function,+,notification_internal_message_block,void,"NotificationApp*, const NotificationSequence*"
Function,+,notification_message,void,"NotificationApp*, const NotificationSequence*"
Function,+,notification_message_block,void,"NotificationApp*, const NotificationSequence*"
Function,+,notification_messages_notes_frequency_from_name,float,const char*
Function,-,nrand48,long,unsigned short[3]
Function,+,number_input_alloc,NumberInput*,
Function,+,number_input_free,void,NumberInput*
1 entry status name type params
546 Function + args_char_to_hex _Bool char, char, uint8_t*
547 Function + args_get_first_word_length size_t FuriString*
548 Function + args_length size_t FuriString*
549 Function + args_read_duration _Bool FuriString*, uint32_t*, const char*
550 Function + args_read_float_and_trim _Bool FuriString*, float*
551 Function + args_read_hex_bytes _Bool FuriString*, uint8_t*, size_t
552 Function + args_read_int_and_trim _Bool FuriString*, int*
553 Function + args_read_probably_quoted_string_and_trim _Bool FuriString*, FuriString*
804 Function + cli_shell_join void CliShell*
805 Function + cli_shell_set_prompt void CliShell*, const char*
806 Function + cli_shell_start void CliShell*
807 Function + cli_sleep _Bool PipeSide*, uint32_t
808 Function + cli_vcp_disable void CliVcp*
809 Function + cli_vcp_enable void CliVcp*
810 Function + composite_api_resolver_add void CompositeApiResolver*, const ElfApiInterface*
2270 Function + notification_internal_message_block void NotificationApp*, const NotificationSequence*
2271 Function + notification_message void NotificationApp*, const NotificationSequence*
2272 Function + notification_message_block void NotificationApp*, const NotificationSequence*
2273 Function + notification_messages_notes_frequency_from_name float const char*
2274 Function - nrand48 long unsigned short[3]
2275 Function + number_input_alloc NumberInput*
2276 Function + number_input_free void NumberInput*

View File

@@ -625,6 +625,8 @@ Function,-,arc4random_uniform,__uint32_t,__uint32_t
Function,+,args_char_to_hex,_Bool,"char, char, uint8_t*"
Function,+,args_get_first_word_length,size_t,FuriString*
Function,+,args_length,size_t,FuriString*
Function,+,args_read_float_and_trim,_Bool,"FuriString*, float*"
Function,+,args_read_duration,_Bool,"FuriString*, uint32_t*, const char*"
Function,+,args_read_hex_bytes,_Bool,"FuriString*, uint8_t*, size_t"
Function,+,args_read_int_and_trim,_Bool,"FuriString*, int*"
Function,+,args_read_probably_quoted_string_and_trim,_Bool,"FuriString*, FuriString*"
@@ -881,6 +883,7 @@ Function,+,cli_shell_free,void,CliShell*
Function,+,cli_shell_join,void,CliShell*
Function,+,cli_shell_set_prompt,void,"CliShell*, const char*"
Function,+,cli_shell_start,void,CliShell*
Function,+,cli_sleep,_Bool,"PipeSide*, uint32_t"
Function,+,cli_vcp_disable,void,CliVcp*
Function,+,cli_vcp_enable,void,CliVcp*
Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*"
@@ -2919,6 +2922,7 @@ Function,+,notification_internal_message,void,"NotificationApp*, const Notificat
Function,+,notification_internal_message_block,void,"NotificationApp*, const NotificationSequence*"
Function,+,notification_message,void,"NotificationApp*, const NotificationSequence*"
Function,+,notification_message_block,void,"NotificationApp*, const NotificationSequence*"
Function,+,notification_messages_notes_frequency_from_name,float,const char*
Function,-,nrand48,long,unsigned short[3]
Function,+,number_input_alloc,NumberInput*,
Function,+,number_input_free,void,NumberInput*
1 entry status name type params
625 Function + args_char_to_hex _Bool char, char, uint8_t*
626 Function + args_get_first_word_length size_t FuriString*
627 Function + args_length size_t FuriString*
628 Function + args_read_float_and_trim _Bool FuriString*, float*
629 Function + args_read_duration _Bool FuriString*, uint32_t*, const char*
630 Function + args_read_hex_bytes _Bool FuriString*, uint8_t*, size_t
631 Function + args_read_int_and_trim _Bool FuriString*, int*
632 Function + args_read_probably_quoted_string_and_trim _Bool FuriString*, FuriString*
883 Function + cli_shell_join void CliShell*
884 Function + cli_shell_set_prompt void CliShell*, const char*
885 Function + cli_shell_start void CliShell*
886 Function + cli_sleep _Bool PipeSide*, uint32_t
887 Function + cli_vcp_disable void CliVcp*
888 Function + cli_vcp_enable void CliVcp*
889 Function + composite_api_resolver_add void CompositeApiResolver*, const ElfApiInterface*
2922 Function + notification_internal_message_block void NotificationApp*, const NotificationSequence*
2923 Function + notification_message void NotificationApp*, const NotificationSequence*
2924 Function + notification_message_block void NotificationApp*, const NotificationSequence*
2925 Function + notification_messages_notes_frequency_from_name float const char*
2926 Function - nrand48 long unsigned short[3]
2927 Function + number_input_alloc NumberInput*
2928 Function + number_input_free void NumberInput*