mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-07-20 00:48:11 -07:00
Merge branch 'feat/nfc-type-4-final' into mntm-dev
This commit is contained in:
@@ -413,6 +413,7 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
|
||||
bt->current_profile = furi_hal_bt_change_app(
|
||||
message->data.profile.template,
|
||||
message->data.profile.params,
|
||||
bt_keys_storage_get_root_keys(bt->keys_storage),
|
||||
bt_on_gap_event_callback,
|
||||
bt);
|
||||
if(bt->current_profile) {
|
||||
@@ -468,7 +469,6 @@ static void bt_load_keys(Bt* bt) {
|
||||
bt_keys_storage_load(bt->keys_storage);
|
||||
|
||||
bt->current_profile = NULL;
|
||||
|
||||
} else {
|
||||
FURI_LOG_I(TAG, "Keys unchanged");
|
||||
}
|
||||
@@ -476,8 +476,12 @@ static void bt_load_keys(Bt* bt) {
|
||||
|
||||
static void bt_start_application(Bt* bt) {
|
||||
if(!bt->current_profile) {
|
||||
bt->current_profile =
|
||||
furi_hal_bt_change_app(ble_profile_serial, NULL, bt_on_gap_event_callback, bt);
|
||||
bt->current_profile = furi_hal_bt_change_app(
|
||||
ble_profile_serial,
|
||||
NULL,
|
||||
bt_keys_storage_get_root_keys(bt->keys_storage),
|
||||
bt_on_gap_event_callback,
|
||||
bt);
|
||||
|
||||
if(!bt->current_profile) {
|
||||
FURI_LOG_E(TAG, "BLE App start failed");
|
||||
|
||||
@@ -2,21 +2,92 @@
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal_bt.h>
|
||||
#include <lib/toolbox/saved_struct.h>
|
||||
#include <storage/storage.h>
|
||||
#include <furi_hal_random.h>
|
||||
|
||||
#define BT_KEYS_STORAGE_VERSION (0)
|
||||
#define BT_KEYS_STORAGE_MAGIC (0x18)
|
||||
#include <gap.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <toolbox/saved_struct.h>
|
||||
|
||||
#define BT_KEYS_STORAGE_MAGIC (0x18)
|
||||
#define BT_KEYS_STORAGE_VERSION (1)
|
||||
#define BT_KEYS_STORAGE_LEGACY_VERSION (0) // Legacy version with no root keys
|
||||
|
||||
#define TAG "BtKeyStorage"
|
||||
|
||||
// Identity root key
|
||||
static const uint8_t gap_legacy_irk[16] =
|
||||
{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0};
|
||||
// Encryption root key
|
||||
static const uint8_t gap_legacy_erk[16] =
|
||||
{0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21};
|
||||
|
||||
struct BtKeysStorage {
|
||||
uint8_t* nvm_sram_buff;
|
||||
uint16_t nvm_sram_buff_size;
|
||||
uint16_t current_size;
|
||||
FuriString* file_path;
|
||||
GapRootSecurityKeys root_keys;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
GapRootSecurityKeys root_keys;
|
||||
uint8_t pairing_data[];
|
||||
} BtKeysStorageFile;
|
||||
|
||||
static bool bt_keys_storage_save(BtKeysStorage* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
size_t total_size = sizeof(BtKeysStorageFile) + instance->current_size;
|
||||
BtKeysStorageFile* save_data = malloc(total_size);
|
||||
|
||||
memcpy(&save_data->root_keys, &instance->root_keys, sizeof(GapRootSecurityKeys));
|
||||
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
memcpy(save_data->pairing_data, instance->nvm_sram_buff, instance->current_size);
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
|
||||
bool saved = saved_struct_save(
|
||||
furi_string_get_cstr(instance->file_path),
|
||||
save_data,
|
||||
sizeof(GapRootSecurityKeys) + instance->current_size,
|
||||
BT_KEYS_STORAGE_MAGIC,
|
||||
BT_KEYS_STORAGE_VERSION);
|
||||
|
||||
free(save_data);
|
||||
return saved;
|
||||
}
|
||||
|
||||
static bool bt_keys_storage_load_keys_and_pairings(
|
||||
BtKeysStorage* instance,
|
||||
const uint8_t* file_data,
|
||||
size_t data_size) {
|
||||
if(data_size < sizeof(GapRootSecurityKeys)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const BtKeysStorageFile* loaded = (const BtKeysStorageFile*)file_data;
|
||||
memcpy(&instance->root_keys, &loaded->root_keys, sizeof(GapRootSecurityKeys));
|
||||
|
||||
size_t ble_data_size = data_size - sizeof(GapRootSecurityKeys);
|
||||
if(ble_data_size > instance->nvm_sram_buff_size) {
|
||||
FURI_LOG_E(TAG, "BLE data too large for SRAM buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
memcpy(instance->nvm_sram_buff, loaded->pairing_data, ble_data_size);
|
||||
instance->current_size = ble_data_size;
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void bt_keys_storage_regenerate_root_keys(BtKeysStorage* instance) {
|
||||
furi_hal_random_fill_buf(instance->root_keys.erk, sizeof(instance->root_keys.erk));
|
||||
furi_hal_random_fill_buf(instance->root_keys.irk, sizeof(instance->root_keys.irk));
|
||||
}
|
||||
|
||||
bool bt_keys_storage_delete(BtKeysStorage* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
@@ -25,6 +96,15 @@ bool bt_keys_storage_delete(BtKeysStorage* instance) {
|
||||
|
||||
furi_hal_bt_stop_advertising();
|
||||
delete_succeed = furi_hal_bt_clear_white_list();
|
||||
|
||||
FURI_LOG_I(TAG, "Root keys regen");
|
||||
bt_keys_storage_regenerate_root_keys(instance);
|
||||
|
||||
instance->current_size = 0;
|
||||
if(!bt_keys_storage_save(instance)) {
|
||||
FURI_LOG_E(TAG, "Save after delete failed");
|
||||
}
|
||||
|
||||
if(bt_is_active) {
|
||||
furi_hal_bt_start_advertising();
|
||||
}
|
||||
@@ -42,6 +122,7 @@ BtKeysStorage* bt_keys_storage_alloc(const char* keys_storage_path) {
|
||||
instance->file_path = furi_string_alloc();
|
||||
furi_string_set_str(instance->file_path, keys_storage_path);
|
||||
|
||||
bt_keys_storage_regenerate_root_keys(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -76,20 +157,31 @@ static bool bt_keys_storage_file_exists(const char* file_path) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool bt_keys_storage_validate_file(const char* file_path, size_t* payload_size) {
|
||||
static bool bt_keys_storage_validate_file(
|
||||
const char* file_path,
|
||||
size_t* payload_size,
|
||||
uint8_t* file_version) {
|
||||
uint8_t magic, version;
|
||||
size_t size;
|
||||
|
||||
if(!saved_struct_get_metadata(file_path, &magic, &version, &size)) {
|
||||
FURI_LOG_E(TAG, "Failed to get metadata");
|
||||
FURI_LOG_W(TAG, "Failed to get metadata");
|
||||
return false;
|
||||
|
||||
} else if(magic != BT_KEYS_STORAGE_MAGIC || version != BT_KEYS_STORAGE_VERSION) {
|
||||
FURI_LOG_E(TAG, "File version mismatch");
|
||||
} else if(magic != BT_KEYS_STORAGE_MAGIC) {
|
||||
FURI_LOG_W(TAG, "File magic mismatch");
|
||||
return false;
|
||||
} else if(version > BT_KEYS_STORAGE_VERSION) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"File version %d is newer than supported version %d",
|
||||
version,
|
||||
BT_KEYS_STORAGE_VERSION);
|
||||
return false;
|
||||
}
|
||||
|
||||
*payload_size = size;
|
||||
*file_version = version;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -102,32 +194,45 @@ bool bt_keys_storage_is_changed(BtKeysStorage* instance) {
|
||||
do {
|
||||
const char* file_path = furi_string_get_cstr(instance->file_path);
|
||||
size_t payload_size;
|
||||
uint8_t file_version;
|
||||
|
||||
if(!bt_keys_storage_file_exists(file_path)) {
|
||||
FURI_LOG_W(TAG, "Missing or empty file");
|
||||
is_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
} else if(!bt_keys_storage_validate_file(file_path, &payload_size)) {
|
||||
FURI_LOG_E(TAG, "Invalid or corrupted file");
|
||||
if(!bt_keys_storage_validate_file(file_path, &payload_size, &file_version)) {
|
||||
FURI_LOG_W(TAG, "Invalid or corrupted file");
|
||||
is_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Early check for legacy version: always considered changed, no need to load
|
||||
if(file_version == BT_KEYS_STORAGE_LEGACY_VERSION) {
|
||||
is_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
data_buffer = malloc(payload_size);
|
||||
|
||||
const bool data_loaded = saved_struct_load(
|
||||
file_path, data_buffer, payload_size, BT_KEYS_STORAGE_MAGIC, BT_KEYS_STORAGE_VERSION);
|
||||
file_path, data_buffer, payload_size, BT_KEYS_STORAGE_MAGIC, file_version);
|
||||
|
||||
if(!data_loaded) {
|
||||
FURI_LOG_E(TAG, "Failed to load file");
|
||||
break;
|
||||
}
|
||||
|
||||
} else if(payload_size == instance->current_size) {
|
||||
// At this point, it's version 1 file we have
|
||||
const BtKeysStorageFile* loaded = (const BtKeysStorageFile*)data_buffer;
|
||||
size_t expected_file_size = sizeof(GapRootSecurityKeys) + instance->current_size;
|
||||
if(payload_size == expected_file_size) {
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
is_changed = memcmp(data_buffer, instance->nvm_sram_buff, payload_size);
|
||||
is_changed =
|
||||
memcmp(loaded->pairing_data, instance->nvm_sram_buff, instance->current_size);
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
|
||||
} else {
|
||||
FURI_LOG_D(TAG, "Size mismatch");
|
||||
FURI_LOG_D(TAG, "NVRAM sz mismatch (v1)");
|
||||
is_changed = true;
|
||||
}
|
||||
} while(false);
|
||||
@@ -139,45 +244,59 @@ bool bt_keys_storage_is_changed(BtKeysStorage* instance) {
|
||||
return is_changed;
|
||||
}
|
||||
|
||||
static bool bt_keys_storage_load_legacy_pairings(
|
||||
BtKeysStorage* instance,
|
||||
const uint8_t* file_data,
|
||||
size_t payload_size) {
|
||||
FURI_LOG_I(TAG, "Loaded v0, upgrading to v1");
|
||||
memcpy(instance->root_keys.irk, gap_legacy_irk, sizeof(instance->root_keys.irk));
|
||||
memcpy(instance->root_keys.erk, gap_legacy_erk, sizeof(instance->root_keys.erk));
|
||||
if(payload_size > instance->nvm_sram_buff_size) {
|
||||
FURI_LOG_E(TAG, "Pairing too large");
|
||||
return false;
|
||||
}
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
memcpy(instance->nvm_sram_buff, file_data, payload_size);
|
||||
instance->current_size = payload_size;
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
if(!bt_keys_storage_save(instance)) {
|
||||
FURI_LOG_W(TAG, "Upgrade to v1 failed");
|
||||
} else {
|
||||
FURI_LOG_I(TAG, "Upgraded to v1");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool bt_keys_storage_load(BtKeysStorage* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
const char* file_path = furi_string_get_cstr(instance->file_path);
|
||||
size_t payload_size;
|
||||
uint8_t file_version;
|
||||
if(!bt_keys_storage_validate_file(file_path, &payload_size, &file_version)) {
|
||||
FURI_LOG_E(TAG, "Invalid or corrupted file");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool loaded = false;
|
||||
uint8_t* file_data = malloc(payload_size);
|
||||
|
||||
do {
|
||||
const char* file_path = furi_string_get_cstr(instance->file_path);
|
||||
|
||||
// Get payload size
|
||||
size_t payload_size;
|
||||
if(!bt_keys_storage_validate_file(file_path, &payload_size)) {
|
||||
FURI_LOG_E(TAG, "Invalid or corrupted file");
|
||||
break;
|
||||
|
||||
} else if(payload_size > instance->nvm_sram_buff_size) {
|
||||
FURI_LOG_E(TAG, "NVM RAM buffer overflow");
|
||||
if(!saved_struct_load(
|
||||
file_path, file_data, payload_size, BT_KEYS_STORAGE_MAGIC, file_version)) {
|
||||
FURI_LOG_E(TAG, "Failed to load");
|
||||
break;
|
||||
}
|
||||
|
||||
// Load saved data to ram
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
const bool data_loaded = saved_struct_load(
|
||||
file_path,
|
||||
instance->nvm_sram_buff,
|
||||
payload_size,
|
||||
BT_KEYS_STORAGE_MAGIC,
|
||||
BT_KEYS_STORAGE_VERSION);
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
|
||||
if(!data_loaded) {
|
||||
FURI_LOG_E(TAG, "Failed to load file");
|
||||
if(file_version == BT_KEYS_STORAGE_LEGACY_VERSION) {
|
||||
loaded = bt_keys_storage_load_legacy_pairings(instance, file_data, payload_size);
|
||||
break;
|
||||
}
|
||||
|
||||
instance->current_size = payload_size;
|
||||
|
||||
loaded = true;
|
||||
// Only v1 left
|
||||
loaded = bt_keys_storage_load_keys_and_pairings(instance, file_data, payload_size);
|
||||
} while(false);
|
||||
|
||||
free(file_data);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@@ -203,14 +322,8 @@ bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32
|
||||
|
||||
instance->current_size = new_size;
|
||||
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
bool data_updated = saved_struct_save(
|
||||
furi_string_get_cstr(instance->file_path),
|
||||
instance->nvm_sram_buff,
|
||||
new_size,
|
||||
BT_KEYS_STORAGE_MAGIC,
|
||||
BT_KEYS_STORAGE_VERSION);
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
// Save using version 1 format with embedded root keys
|
||||
bool data_updated = bt_keys_storage_save(instance);
|
||||
|
||||
if(!data_updated) {
|
||||
FURI_LOG_E(TAG, "Failed to update key storage");
|
||||
@@ -222,3 +335,9 @@ bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
const GapRootSecurityKeys* bt_keys_storage_get_root_keys(BtKeysStorage* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
return &instance->root_keys;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <gap.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -19,6 +21,8 @@ void bt_keys_storage_set_ram_params(BtKeysStorage* instance, uint8_t* buff, uint
|
||||
|
||||
bool bt_keys_storage_is_changed(BtKeysStorage* instance);
|
||||
|
||||
const GapRootSecurityKeys* bt_keys_storage_get_root_keys(BtKeysStorage* instance);
|
||||
|
||||
bool bt_keys_storage_load(BtKeysStorage* instance);
|
||||
|
||||
bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32_t size);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "bt_settings_filename.h"
|
||||
|
||||
#include <furi.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <toolbox/saved_struct.h>
|
||||
|
||||
@@ -13,12 +14,13 @@
|
||||
void bt_settings_load(BtSettings* bt_settings) {
|
||||
furi_assert(bt_settings);
|
||||
|
||||
const bool success = saved_struct_load(
|
||||
const bool load_success = saved_struct_load(
|
||||
BT_SETTINGS_PATH, bt_settings, sizeof(BtSettings), BT_SETTINGS_MAGIC, BT_SETTINGS_VERSION);
|
||||
|
||||
if(!success) {
|
||||
if(!load_success) {
|
||||
FURI_LOG_W(TAG, "Failed to load settings, using defaults");
|
||||
memset(bt_settings, 0, sizeof(BtSettings));
|
||||
|
||||
bt_settings->enabled = false;
|
||||
// bt_settings_save(bt_settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,14 @@ App(
|
||||
sources=["commands/subshell_demo.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="cli_buzzer",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="cli_buzzer_ep",
|
||||
requires=["cli"],
|
||||
sources=["commands/buzzer.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="cli_src",
|
||||
targets=["f7"],
|
||||
|
||||
@@ -409,11 +409,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);
|
||||
|
||||
@@ -469,12 +471,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);
|
||||
}
|
||||
|
||||
@@ -544,6 +542,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_command_clear(PipeSide* pipe, FuriString* args, void* context) {
|
||||
UNUSED(pipe);
|
||||
UNUSED(args);
|
||||
@@ -563,6 +592,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_COMMAND_INTERFACE(src, cli_command_src, CliCommandFlagParallelSafe, 768, CLI_APPID);
|
||||
|
||||
@@ -15,7 +15,6 @@ typedef struct CliVcp CliVcp;
|
||||
|
||||
void cli_vcp_enable(CliVcp* cli_vcp);
|
||||
void cli_vcp_disable(CliVcp* cli_vcp);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#include "../cli_main_commands.h"
|
||||
#include <notification/notification_app.h>
|
||||
#include <math.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));
|
||||
}
|
||||
}
|
||||
|
||||
// Consider volume effectively zero if below this threshold
|
||||
#define BUZZER_VOLUME_EPSILON (1e-3f)
|
||||
|
||||
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", "<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(fabsf(notification->settings.speaker_volume) < BUZZER_VOLUME_EPSILON) {
|
||||
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);
|
||||
@@ -93,12 +93,17 @@ static const FrameBubble*
|
||||
bubble_animation_pick_bubble(BubbleAnimationViewModel* model, bool active) {
|
||||
const FrameBubble* bubble = NULL;
|
||||
|
||||
if((model->active_bubbles == 0) && (model->passive_bubbles == 0)) {
|
||||
// Check for division by zero based on the active parameter
|
||||
if((active && model->active_bubbles == 0) || (!active && model->passive_bubbles == 0)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t index =
|
||||
furi_hal_random_get() % (active ? model->active_bubbles : model->passive_bubbles);
|
||||
uint8_t random_value = furi_hal_random_get();
|
||||
// In case random generator return zero lets set it to 3
|
||||
if(random_value == 0) {
|
||||
random_value = 3;
|
||||
}
|
||||
uint8_t index = random_value % (active ? model->active_bubbles : model->passive_bubbles);
|
||||
const BubbleAnimation* animation = model->current;
|
||||
|
||||
for(int i = 0; i < animation->frame_bubble_sequences_count; ++i) {
|
||||
|
||||
@@ -29,7 +29,7 @@ typedef struct {
|
||||
void* context;
|
||||
} CanvasCallbackPair;
|
||||
|
||||
ARRAY_DEF(CanvasCallbackPairArray, CanvasCallbackPair, M_POD_OPLIST);
|
||||
ARRAY_DEF(CanvasCallbackPairArray, CanvasCallbackPair, M_POD_OPLIST); //-V658
|
||||
|
||||
#define M_OPL_CanvasCallbackPairArray_t() ARRAY_OPLIST(CanvasCallbackPairArray, M_POD_OPLIST)
|
||||
|
||||
|
||||
@@ -925,7 +925,7 @@ void elements_text_box(
|
||||
for(size_t i = 0; i < line_num; i++) {
|
||||
for(size_t j = 0; j < line[i].len; j++) {
|
||||
// Process format symbols
|
||||
if(line[i].text[j] == '\e' && j < line[i].len - 1) {
|
||||
if(line[i].text[j] == '\e' && j < line[i].len - 1) { //-V781
|
||||
++j;
|
||||
if(line[i].text[j] == ELEMENTS_BOLD_MARKER) {
|
||||
if(bold) {
|
||||
|
||||
@@ -25,7 +25,7 @@ struct ButtonMenuItem {
|
||||
void* callback_context;
|
||||
};
|
||||
|
||||
ARRAY_DEF(ButtonMenuItemArray, ButtonMenuItem, M_POD_OPLIST);
|
||||
ARRAY_DEF(ButtonMenuItemArray, ButtonMenuItem, M_POD_OPLIST); //-V658
|
||||
|
||||
struct ButtonMenu {
|
||||
View* view;
|
||||
|
||||
@@ -98,7 +98,7 @@ void button_panel_reserve(ButtonPanel* button_panel, size_t reserve_x, size_t re
|
||||
furi_check(reserve_x > 0);
|
||||
furi_check(reserve_y > 0);
|
||||
|
||||
with_view_model(
|
||||
with_view_model( //-V621
|
||||
button_panel->view,
|
||||
ButtonPanelModel * model,
|
||||
{
|
||||
|
||||
@@ -32,8 +32,8 @@ typedef enum {
|
||||
(WorkerEvtStop | WorkerEvtLoad | WorkerEvtFolderEnter | WorkerEvtFolderExit | \
|
||||
WorkerEvtFolderRefresh | WorkerEvtConfigChange)
|
||||
|
||||
ARRAY_DEF(_IdxLastArray, int32_t) // Unused, kept for compatibility
|
||||
ARRAY_DEF(ExtFilterArray, FuriString*, FURI_STRING_OPLIST)
|
||||
ARRAY_DEF(_IdxLastArray, int32_t) //-V658 // Unused, kept for compatibility
|
||||
ARRAY_DEF(ExtFilterArray, FuriString*, FURI_STRING_OPLIST) //-V658
|
||||
|
||||
struct BrowserWorker {
|
||||
FuriThread* thread;
|
||||
|
||||
@@ -28,7 +28,7 @@ typedef struct {
|
||||
void* callback_context;
|
||||
} MenuItem;
|
||||
|
||||
ARRAY_DEF(MenuItemArray, MenuItem, M_POD_OPLIST);
|
||||
ARRAY_DEF(MenuItemArray, MenuItem, M_POD_OPLIST); //-V658
|
||||
|
||||
#define M_OPL_MenuItemArray_t() ARRAY_OPLIST(MenuItemArray, M_POD_OPLIST)
|
||||
|
||||
|
||||
@@ -93,13 +93,12 @@ static bool popup_view_input_callback(InputEvent* event, void* context) {
|
||||
void popup_start_timer(void* context) {
|
||||
Popup* popup = context;
|
||||
if(popup->timer_enabled) {
|
||||
uint32_t timer_period =
|
||||
popup->timer_period_in_ms / (1000.0f / furi_kernel_get_tick_frequency());
|
||||
uint32_t timer_period = furi_ms_to_ticks(popup->timer_period_in_ms);
|
||||
if(timer_period == 0) timer_period = 1;
|
||||
|
||||
if(furi_timer_start(popup->timer, timer_period) != FuriStatusOk) {
|
||||
furi_crash();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ void popup_free(Popup* popup);
|
||||
*/
|
||||
View* popup_get_view(Popup* popup);
|
||||
|
||||
/** Set popup callback function
|
||||
/** Set popup timeout callback
|
||||
*
|
||||
* @param popup Popup instance
|
||||
* @param callback PopupCallback
|
||||
|
||||
@@ -19,7 +19,7 @@ struct VariableItem {
|
||||
FuriString* locked_message;
|
||||
};
|
||||
|
||||
ARRAY_DEF(VariableItemArray, VariableItem, M_POD_OPLIST);
|
||||
ARRAY_DEF(VariableItemArray, VariableItem, M_POD_OPLIST); //-V658
|
||||
|
||||
struct VariableItemList {
|
||||
View* view;
|
||||
|
||||
@@ -10,7 +10,7 @@ typedef struct {
|
||||
FuriString* text;
|
||||
} TextScrollLineArray;
|
||||
|
||||
ARRAY_DEF(TextScrollLineArray, TextScrollLineArray, M_POD_OPLIST)
|
||||
ARRAY_DEF(TextScrollLineArray, TextScrollLineArray, M_POD_OPLIST) //-V658
|
||||
|
||||
typedef struct {
|
||||
TextScrollLineArray_t line_array;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "scene_manager.h"
|
||||
#include <m-array.h>
|
||||
|
||||
ARRAY_DEF(SceneManagerIdStack, uint32_t, M_DEFAULT_OPLIST);
|
||||
ARRAY_DEF(SceneManagerIdStack, uint32_t, M_DEFAULT_OPLIST); //-V658
|
||||
|
||||
typedef struct {
|
||||
uint32_t state;
|
||||
|
||||
@@ -96,6 +96,7 @@ static void loader_show_gui_error(
|
||||
LoaderMessageLoaderStatusResult status,
|
||||
const char* name,
|
||||
FuriString* error_message) {
|
||||
furi_check(name);
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
DialogMessage* message = dialog_message_alloc();
|
||||
|
||||
@@ -693,6 +694,8 @@ static LoaderMessageLoaderStatusResult loader_do_start_by_name(
|
||||
status.value = loader_make_success_status(error_message);
|
||||
status.error = LoaderStatusErrorUnknown;
|
||||
|
||||
if(name == NULL) return status;
|
||||
|
||||
do {
|
||||
// check lock
|
||||
if(loader_do_is_locked(loader)) {
|
||||
@@ -929,7 +932,10 @@ int32_t loader_srv(void* p) {
|
||||
switch(message.type) {
|
||||
case LoaderMessageTypeStartByName: {
|
||||
LoaderMessageLoaderStatusResult status = loader_do_start_by_name(
|
||||
loader, message.start.name, message.start.args, message.start.error_message);
|
||||
loader,
|
||||
message.start.name,
|
||||
message.start.args,
|
||||
message.start.error_message); //-V595
|
||||
*(message.status_value) = status;
|
||||
if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader);
|
||||
api_lock_unlock(message.api_lock);
|
||||
@@ -938,7 +944,7 @@ int32_t loader_srv(void* p) {
|
||||
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, message.start.name, message.start.args, error_message); //-V595
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -293,7 +293,7 @@ static void rpc_system_storage_list_process(const PB_Main* request, void* contex
|
||||
finish = true;
|
||||
}
|
||||
|
||||
while(!finish) {
|
||||
while(!finish) { //-V1044
|
||||
FileInfo fileinfo;
|
||||
char* name = malloc(MAX_NAME_LENGTH);
|
||||
if(storage_dir_read(dir, &fileinfo, name, MAX_NAME_LENGTH)) {
|
||||
|
||||
@@ -30,7 +30,8 @@ static void rpc_system_system_ping_process(const PB_Main* request, void* context
|
||||
}
|
||||
|
||||
PB_Main response = PB_Main_init_default;
|
||||
response.command_status = PB_CommandStatus_OK;
|
||||
// PB_CommandStatus_OK is 0 in current case, and var by default is 0 too, commenting PVS warn
|
||||
response.command_status = PB_CommandStatus_OK; //-V1048
|
||||
response.command_id = request->command_id;
|
||||
response.which_content = PB_Main_system_ping_response_tag;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user