diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index 72b8cafcb8..252eb57c54 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -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"], +) diff --git a/applications/debug/unit_tests/tests/args/args_test.c b/applications/debug/unit_tests/tests/args/args_test.c new file mode 100644 index 0000000000..9b1887f0be --- /dev/null +++ b/applications/debug/unit_tests/tests/args/args_test.c @@ -0,0 +1,211 @@ + +#include "../test.h" // IWYU pragma: keep +#include + +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) diff --git a/applications/debug/unit_tests/tests/minunit.h b/applications/debug/unit_tests/tests/minunit.h index c854c4673b..943ed3c671 100644 --- a/applications/debug/unit_tests/tests/minunit.h +++ b/applications/debug/unit_tests/tests/minunit.h @@ -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; \ diff --git a/applications/debug/unit_tests/tests/notification/notes_test.c b/applications/debug/unit_tests/tests/notification/notes_test.c new file mode 100644 index 0000000000..2b6d25c13f --- /dev/null +++ b/applications/debug/unit_tests/tests/notification/notes_test.c @@ -0,0 +1,165 @@ +#include "../test.h" // IWYU pragma: keep +#include +#include + +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) diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index b305fb6b0c..d5acf7752a 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -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"], +) diff --git a/applications/services/cli/cli_main_commands.c b/applications/services/cli/cli_main_commands.c index 508a650de8..a478512d5b 100644 --- a/applications/services/cli/cli_main_commands.c +++ b/applications/services/cli/cli_main_commands.c @@ -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-...>[]]", 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); diff --git a/applications/services/cli/commands/buzzer.c b/applications/services/cli/commands/buzzer.c new file mode 100644 index 0000000000..3c1673149a --- /dev/null +++ b/applications/services/cli/commands/buzzer.c @@ -0,0 +1,135 @@ +#include "../cli_main_commands.h" +#include +#include +#include + +void cli_command_buzzer_print_usage(bool is_freq_subcommand, FuriString* args) { + if(is_freq_subcommand) { + cli_print_usage( + "buzzer freq", " [<0-...>[]]", furi_string_get_cstr(args)); + + } else { + cli_print_usage("buzzer note", " [<0-...>[]]", 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 = { + ¬ification_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", "", 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", "", 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); diff --git a/applications/services/notification/notification_messages_notes.c b/applications/services/notification/notification_messages_notes.c index 18ff94aafb..4a13fe0e6b 100644 --- a/applications/services/notification/notification_messages_notes.c +++ b/applications/services/notification/notification_messages_notes.c @@ -1,4 +1,5 @@ #include "notification.h" +#include /* 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; +} diff --git a/applications/services/notification/notification_messages_notes.h b/applications/services/notification/notification_messages_notes.h index b1040a01e7..dffa2519e3 100644 --- a/applications/services/notification/notification_messages_notes.h +++ b/applications/services/notification/notification_messages_notes.h @@ -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 diff --git a/lib/toolbox/args.c b/lib/toolbox/args.c index 914b093bac..f6a1cfda57 100644 --- a/lib/toolbox/args.c +++ b/lib/toolbox/args.c @@ -2,6 +2,7 @@ #include "hex.h" #include "strint.h" #include "m-core.h" +#include 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; +} diff --git a/lib/toolbox/args.h b/lib/toolbox/args.h index 556fd4a72b..fecf335997 100644 --- a/lib/toolbox/args.h +++ b/lib/toolbox/args.h @@ -9,17 +9,26 @@ extern "C" { #endif /** Extract int value and trim arguments string - * - * @param args - arguments string - * @param word first argument, output + * + * @param args - arguments string + * @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 - * + * * @param args arguments string * @param word first argument, output * @return true - success @@ -29,7 +38,7 @@ bool args_read_string_and_trim(FuriString* args, FuriString* word); /** * @brief Extract the first quoted argument from the argument string and trim the argument string. If the argument is not quoted, calls args_read_string_and_trim. - * + * * @param args arguments string * @param word first argument, output, without quotes * @return true - success @@ -39,7 +48,7 @@ bool args_read_probably_quoted_string_and_trim(FuriString* args, FuriString* wor /** * @brief Convert hex ASCII values to byte array - * + * * @param args arguments string * @param bytes byte array pointer, output * @param bytes_count needed bytes count @@ -48,11 +57,23 @@ 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 ***************************************/ /** * @brief Get length of first word from arguments string - * + * * @param args arguments string * @return size_t length of first word */ @@ -60,7 +81,7 @@ size_t args_get_first_word_length(FuriString* args); /** * @brief Get length of arguments string - * + * * @param args arguments string * @return size_t length of arguments string */ @@ -68,7 +89,7 @@ size_t args_length(FuriString* args); /** * @brief Convert ASCII hex values to byte - * + * * @param hi_nibble ASCII hi nibble character * @param low_nibble ASCII low nibble character * @param byte byte pointer, output diff --git a/lib/toolbox/cli/cli_command.c b/lib/toolbox/cli/cli_command.c index a3c9ff2929..60aa351e72 100644 --- a/lib/toolbox/cli/cli_command.c +++ b/lib/toolbox/cli/cli_command.c @@ -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; +} diff --git a/lib/toolbox/cli/cli_command.h b/lib/toolbox/cli/cli_command.h index 2d1d851d65..9d341f6d23 100644 --- a/lib/toolbox/cli/cli_command.h +++ b/lib/toolbox/cli/cli_command.h @@ -29,14 +29,14 @@ typedef enum { CliCommandFlagExternal = (1 << 4), /**< The command comes from a .fal file */ } CliCommandFlag; -/** +/** * @brief CLI command execution callback pointer - * + * * This callback will be called from a separate thread spawned just for your * command. The pipe will be installed as the thread's stdio, so you can use * `printf`, `getchar` and other standard functions to communicate with the * user. - * + * * @param [in] pipe Pipe that can be used to send and receive data. If * `CliCommandFlagDontAttachStdio` was not set, you can * also use standard C functions (printf, getc, etc.) to @@ -64,7 +64,7 @@ typedef struct { /** * @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 @@ -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, \ diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index a896250804..27b65e2022 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -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* diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index ef938ab270..de385684d9 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -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*