mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-05-13 20:18:35 -07:00
Merge remote-tracking branch 'OFW/dev' into dev
This commit is contained in:
@@ -19,6 +19,7 @@ App(
|
||||
"view_holder.h",
|
||||
"modules/button_menu.h",
|
||||
"modules/byte_input.h",
|
||||
"modules/number_input.h",
|
||||
"modules/popup.h",
|
||||
"modules/text_input.h",
|
||||
"modules/widget.h",
|
||||
|
||||
@@ -134,7 +134,7 @@ static bool browser_filter_by_name(BrowserWorker* browser, FuriString* name, boo
|
||||
if((furi_string_empty(ext)) || (furi_string_cmp_str(ext, "*") == 0)) {
|
||||
return true;
|
||||
}
|
||||
if(furi_string_end_with(name, ext)) {
|
||||
if(furi_string_end_withi(name, ext)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
443
applications/services/gui/modules/number_input.c
Normal file
443
applications/services/gui/modules/number_input.c
Normal file
@@ -0,0 +1,443 @@
|
||||
#include "number_input.h"
|
||||
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
#include <assets_icons.h>
|
||||
|
||||
struct NumberInput {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
const char text;
|
||||
const size_t x;
|
||||
const size_t y;
|
||||
} NumberInputKey;
|
||||
|
||||
typedef struct {
|
||||
FuriString* header;
|
||||
FuriString* text_buffer;
|
||||
|
||||
int32_t current_number;
|
||||
int32_t max_value;
|
||||
int32_t min_value;
|
||||
|
||||
NumberInputCallback callback;
|
||||
void* callback_context;
|
||||
|
||||
size_t selected_row;
|
||||
size_t selected_column;
|
||||
} NumberInputModel;
|
||||
|
||||
static const size_t keyboard_origin_x = 7;
|
||||
static const size_t keyboard_origin_y = 31;
|
||||
static const size_t keyboard_row_count = 2;
|
||||
static const char enter_symbol = '\r';
|
||||
static const char backspace_symbol = '\b';
|
||||
static const char sign_symbol = '-';
|
||||
|
||||
static const NumberInputKey keyboard_keys_row_1[] = {
|
||||
{'0', 0, 12},
|
||||
{'1', 11, 12},
|
||||
{'2', 22, 12},
|
||||
{'3', 33, 12},
|
||||
{'4', 44, 12},
|
||||
{backspace_symbol, 103, 4},
|
||||
};
|
||||
|
||||
static const NumberInputKey keyboard_keys_row_2[] = {
|
||||
{'5', 0, 26},
|
||||
{'6', 11, 26},
|
||||
{'7', 22, 26},
|
||||
{'8', 33, 26},
|
||||
{'9', 44, 26},
|
||||
{sign_symbol, 55, 17},
|
||||
{enter_symbol, 95, 17},
|
||||
};
|
||||
|
||||
static size_t number_input_get_row_size(size_t row_index) {
|
||||
size_t row_size = 0;
|
||||
|
||||
switch(row_index + 1) {
|
||||
case 1:
|
||||
row_size = COUNT_OF(keyboard_keys_row_1);
|
||||
break;
|
||||
case 2:
|
||||
row_size = COUNT_OF(keyboard_keys_row_2);
|
||||
break;
|
||||
default:
|
||||
furi_crash();
|
||||
}
|
||||
|
||||
return row_size;
|
||||
}
|
||||
|
||||
static const NumberInputKey* number_input_get_row(size_t row_index) {
|
||||
const NumberInputKey* row = NULL;
|
||||
|
||||
switch(row_index + 1) {
|
||||
case 1:
|
||||
row = keyboard_keys_row_1;
|
||||
break;
|
||||
case 2:
|
||||
row = keyboard_keys_row_2;
|
||||
break;
|
||||
default:
|
||||
furi_crash();
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static void number_input_draw_input(Canvas* canvas, NumberInputModel* model) {
|
||||
const size_t text_x = 8;
|
||||
const size_t text_y = 25;
|
||||
|
||||
elements_slightly_rounded_frame(canvas, 6, 14, 116, 15);
|
||||
|
||||
canvas_draw_str(canvas, text_x, text_y, furi_string_get_cstr(model->text_buffer));
|
||||
}
|
||||
|
||||
static bool number_input_use_sign(NumberInputModel* model) {
|
||||
//only show sign button if allowed number range needs it
|
||||
if(model->min_value < 0 && model->max_value >= 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void number_input_backspace_cb(NumberInputModel* model) {
|
||||
size_t text_length = furi_string_utf8_length(model->text_buffer);
|
||||
if(text_length < 1 || (text_length < 2 && model->current_number <= 0)) {
|
||||
return;
|
||||
}
|
||||
furi_string_set_strn(
|
||||
model->text_buffer, furi_string_get_cstr(model->text_buffer), text_length - 1);
|
||||
model->current_number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10);
|
||||
}
|
||||
|
||||
static void number_input_handle_up(NumberInputModel* model) {
|
||||
if(model->selected_row > 0) {
|
||||
model->selected_row--;
|
||||
if(model->selected_column > number_input_get_row_size(model->selected_row) - 1) {
|
||||
model->selected_column = number_input_get_row_size(model->selected_row) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void number_input_handle_down(NumberInputModel* model) {
|
||||
if(model->selected_row < keyboard_row_count - 1) {
|
||||
if(model->selected_column >= number_input_get_row_size(model->selected_row) - 1) {
|
||||
model->selected_column = number_input_get_row_size(model->selected_row + 1) - 1;
|
||||
}
|
||||
model->selected_row += 1;
|
||||
}
|
||||
const NumberInputKey* keys = number_input_get_row(model->selected_row);
|
||||
if(keys[model->selected_column].text == sign_symbol && !number_input_use_sign(model)) {
|
||||
model->selected_column--;
|
||||
}
|
||||
}
|
||||
|
||||
static void number_input_handle_left(NumberInputModel* model) {
|
||||
if(model->selected_column > 0) {
|
||||
model->selected_column--;
|
||||
} else {
|
||||
model->selected_column = number_input_get_row_size(model->selected_row) - 1;
|
||||
}
|
||||
const NumberInputKey* keys = number_input_get_row(model->selected_row);
|
||||
if(keys[model->selected_column].text == sign_symbol && !number_input_use_sign(model)) {
|
||||
model->selected_column--;
|
||||
}
|
||||
}
|
||||
|
||||
static void number_input_handle_right(NumberInputModel* model) {
|
||||
if(model->selected_column < number_input_get_row_size(model->selected_row) - 1) {
|
||||
model->selected_column++;
|
||||
} else {
|
||||
model->selected_column = 0;
|
||||
}
|
||||
const NumberInputKey* keys = number_input_get_row(model->selected_row);
|
||||
if(keys[model->selected_column].text == sign_symbol && !number_input_use_sign(model)) {
|
||||
model->selected_column++;
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_number_too_large(NumberInputModel* model) {
|
||||
int64_t value = strtoll(furi_string_get_cstr(model->text_buffer), NULL, 10);
|
||||
if(value > (int64_t)model->max_value) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_number_too_small(NumberInputModel* model) {
|
||||
int64_t value = strtoll(furi_string_get_cstr(model->text_buffer), NULL, 10);
|
||||
if(value < (int64_t)model->min_value) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void number_input_sign(NumberInputModel* model) {
|
||||
int32_t number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10);
|
||||
if(number == 0 && furi_string_cmp_str(model->text_buffer, "-") != 0) {
|
||||
furi_string_set_str(model->text_buffer, "-");
|
||||
return;
|
||||
}
|
||||
number = number * -1;
|
||||
furi_string_printf(model->text_buffer, "%ld", number);
|
||||
if(is_number_too_large(model) || is_number_too_small(model)) {
|
||||
furi_string_printf(model->text_buffer, "%ld", model->current_number);
|
||||
return;
|
||||
}
|
||||
model->current_number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10);
|
||||
if(model->current_number == 0) {
|
||||
furi_string_set_str(model->text_buffer, ""); //show empty if 0, better for usability
|
||||
}
|
||||
}
|
||||
|
||||
static void number_input_add_digit(NumberInputModel* model, char* newChar) {
|
||||
furi_string_cat_str(model->text_buffer, newChar);
|
||||
if((model->max_value >= 0 && is_number_too_large(model)) ||
|
||||
(model->min_value < 0 && is_number_too_small(model))) {
|
||||
//you still need to be able to type invalid numbers in some cases to reach valid numbers on later keypress
|
||||
furi_string_printf(model->text_buffer, "%ld", model->current_number);
|
||||
return;
|
||||
}
|
||||
model->current_number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10);
|
||||
if(model->current_number == 0) {
|
||||
furi_string_reset(model->text_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
static void number_input_handle_ok(NumberInputModel* model) {
|
||||
char selected = number_input_get_row(model->selected_row)[model->selected_column].text;
|
||||
char temp_str[2] = {selected, '\0'};
|
||||
if(selected == enter_symbol) {
|
||||
if(is_number_too_large(model) || is_number_too_small(model)) {
|
||||
return; //Do nothing if number outside allowed range
|
||||
}
|
||||
model->current_number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10);
|
||||
model->callback(model->callback_context, model->current_number);
|
||||
} else if(selected == backspace_symbol) {
|
||||
number_input_backspace_cb(model);
|
||||
} else if(selected == sign_symbol) {
|
||||
number_input_sign(model);
|
||||
} else {
|
||||
number_input_add_digit(model, temp_str);
|
||||
}
|
||||
}
|
||||
|
||||
static void number_input_view_draw_callback(Canvas* canvas, void* _model) {
|
||||
NumberInputModel* model = _model;
|
||||
|
||||
number_input_draw_input(canvas, model);
|
||||
|
||||
if(!furi_string_empty(model->header)) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str(canvas, 2, 9, furi_string_get_cstr(model->header));
|
||||
}
|
||||
canvas_set_font(canvas, FontKeyboard);
|
||||
// Draw keyboard
|
||||
for(size_t row = 0; row < keyboard_row_count; row++) {
|
||||
const size_t column_count = number_input_get_row_size(row);
|
||||
const NumberInputKey* keys = number_input_get_row(row);
|
||||
|
||||
for(size_t column = 0; column < column_count; column++) {
|
||||
if(keys[column].text == sign_symbol && !number_input_use_sign(model)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(keys[column].text == enter_symbol) {
|
||||
if(is_number_too_small(model) || is_number_too_large(model)) {
|
||||
//in some cases you need to be able to type a number smaller/larger than the limits (expl. min = 50, clear all and editor must allow to type 9 and later 0 for 90)
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySaveBlockedSelected_24x11);
|
||||
} else {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySaveBlocked_24x11);
|
||||
}
|
||||
} else {
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySaveSelected_24x11);
|
||||
} else {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySave_24x11);
|
||||
}
|
||||
}
|
||||
} else if(keys[column].text == backspace_symbol) {
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeyBackspaceSelected_16x9);
|
||||
} else {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeyBackspace_16x9);
|
||||
}
|
||||
} else if(keys[column].text == sign_symbol) {
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySignSelected_21x11);
|
||||
} else {
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
&I_KeySign_21x11);
|
||||
}
|
||||
} else {
|
||||
if(model->selected_row == row && model->selected_column == column) {
|
||||
canvas_draw_box(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x - 3,
|
||||
keyboard_origin_y + keys[column].y - 10,
|
||||
11,
|
||||
13);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
|
||||
canvas_draw_glyph(
|
||||
canvas,
|
||||
keyboard_origin_x + keys[column].x,
|
||||
keyboard_origin_y + keys[column].y,
|
||||
keys[column].text);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool number_input_view_input_callback(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
NumberInput* number_input = context;
|
||||
|
||||
bool consumed = false;
|
||||
|
||||
// Fetch the model
|
||||
NumberInputModel* model = view_get_model(number_input->view);
|
||||
|
||||
if(event->type == InputTypeShort || event->type == InputTypeLong ||
|
||||
event->type == InputTypeRepeat) {
|
||||
consumed = true;
|
||||
switch(event->key) {
|
||||
case InputKeyLeft:
|
||||
number_input_handle_left(model);
|
||||
break;
|
||||
case InputKeyRight:
|
||||
number_input_handle_right(model);
|
||||
break;
|
||||
case InputKeyUp:
|
||||
number_input_handle_up(model);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
number_input_handle_down(model);
|
||||
break;
|
||||
case InputKeyOk:
|
||||
number_input_handle_ok(model);
|
||||
break;
|
||||
default:
|
||||
consumed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// commit view
|
||||
view_commit_model(number_input->view, consumed);
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
NumberInput* number_input_alloc(void) {
|
||||
NumberInput* number_input = malloc(sizeof(NumberInput));
|
||||
number_input->view = view_alloc();
|
||||
view_set_context(number_input->view, number_input);
|
||||
view_allocate_model(number_input->view, ViewModelTypeLocking, sizeof(NumberInputModel));
|
||||
view_set_draw_callback(number_input->view, number_input_view_draw_callback);
|
||||
view_set_input_callback(number_input->view, number_input_view_input_callback);
|
||||
|
||||
with_view_model(
|
||||
number_input->view,
|
||||
NumberInputModel * model,
|
||||
{
|
||||
model->header = furi_string_alloc();
|
||||
model->text_buffer = furi_string_alloc();
|
||||
},
|
||||
true);
|
||||
|
||||
return number_input;
|
||||
}
|
||||
|
||||
void number_input_free(NumberInput* number_input) {
|
||||
furi_check(number_input);
|
||||
with_view_model(
|
||||
number_input->view,
|
||||
NumberInputModel * model,
|
||||
{
|
||||
furi_string_free(model->header);
|
||||
furi_string_free(model->text_buffer);
|
||||
},
|
||||
true);
|
||||
view_free(number_input->view);
|
||||
free(number_input);
|
||||
}
|
||||
|
||||
View* number_input_get_view(NumberInput* number_input) {
|
||||
furi_check(number_input);
|
||||
return number_input->view;
|
||||
}
|
||||
|
||||
void number_input_set_result_callback(
|
||||
NumberInput* number_input,
|
||||
NumberInputCallback callback,
|
||||
void* callback_context,
|
||||
int32_t current_number,
|
||||
int32_t min_value,
|
||||
int32_t max_value) {
|
||||
furi_check(number_input);
|
||||
|
||||
current_number = CLAMP(current_number, max_value, min_value);
|
||||
|
||||
with_view_model(
|
||||
number_input->view,
|
||||
NumberInputModel * model,
|
||||
{
|
||||
model->callback = callback;
|
||||
model->callback_context = callback_context;
|
||||
model->current_number = current_number;
|
||||
furi_string_printf(model->text_buffer, "%ld", current_number);
|
||||
model->min_value = min_value;
|
||||
model->max_value = max_value;
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
void number_input_set_header_text(NumberInput* number_input, const char* text) {
|
||||
furi_check(number_input);
|
||||
with_view_model(
|
||||
number_input->view,
|
||||
NumberInputModel * model,
|
||||
{ furi_string_set(model->header, text); },
|
||||
true);
|
||||
}
|
||||
69
applications/services/gui/modules/number_input.h
Normal file
69
applications/services/gui/modules/number_input.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file number_input.h
|
||||
* GUI: Integer string keyboard view module API
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Number input anonymous structure */
|
||||
typedef struct NumberInput NumberInput;
|
||||
|
||||
/** Callback to be called on save button press */
|
||||
typedef void (*NumberInputCallback)(void* context, int32_t number);
|
||||
|
||||
/** Allocate and initialize Number input.
|
||||
*
|
||||
* This Number input is used to enter Numbers (Integers).
|
||||
*
|
||||
* @return NumberInput instance pointer
|
||||
*/
|
||||
NumberInput* number_input_alloc(void);
|
||||
|
||||
/** Deinitialize and free byte input
|
||||
*
|
||||
* @param number_input Number input instance
|
||||
*/
|
||||
void number_input_free(NumberInput* number_input);
|
||||
|
||||
/** Get byte input view
|
||||
*
|
||||
* @param number_input byte input instance
|
||||
*
|
||||
* @return View instance that can be used for embedding
|
||||
*/
|
||||
View* number_input_get_view(NumberInput* number_input);
|
||||
|
||||
/** Set byte input result callback
|
||||
*
|
||||
* @param number_input byte input instance
|
||||
* @param input_callback input callback fn
|
||||
* @param callback_context callback context
|
||||
* @param[in] current_number The current number
|
||||
* @param min_value Min number value
|
||||
* @param max_value Max number value
|
||||
*/
|
||||
|
||||
void number_input_set_result_callback(
|
||||
NumberInput* number_input,
|
||||
NumberInputCallback input_callback,
|
||||
void* callback_context,
|
||||
int32_t current_number,
|
||||
int32_t min_value,
|
||||
int32_t max_value);
|
||||
|
||||
/** Set byte input header text
|
||||
*
|
||||
* @param number_input byte input instance
|
||||
* @param text text to be shown
|
||||
*/
|
||||
void number_input_set_header_text(NumberInput* number_input, const char* text);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
10
applications/services/region/application.fam
Normal file
10
applications/services/region/application.fam
Normal file
@@ -0,0 +1,10 @@
|
||||
App(
|
||||
appid="region",
|
||||
name="RegionSrv",
|
||||
apptype=FlipperAppType.STARTUP,
|
||||
targets=["f7"],
|
||||
entry_point="region_on_system_start",
|
||||
cdefines=["SRV_REGION"],
|
||||
requires=["storage"],
|
||||
order=170,
|
||||
)
|
||||
147
applications/services/region/region.c
Normal file
147
applications/services/region/region.c
Normal file
@@ -0,0 +1,147 @@
|
||||
#include <furi_hal_region.h>
|
||||
|
||||
#include <furi.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
#include <flipper.pb.h>
|
||||
#include <pb_decode.h>
|
||||
|
||||
#define TAG "RegionSrv"
|
||||
|
||||
#define SUBGHZ_REGION_FILENAME INT_PATH(".region_data")
|
||||
|
||||
static bool region_istream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) {
|
||||
File* file = istream->state;
|
||||
size_t ret = storage_file_read(file, buf, count);
|
||||
return count == ret;
|
||||
}
|
||||
|
||||
static bool region_istream_decode_band(pb_istream_t* stream, const pb_field_t* field, void** arg) {
|
||||
UNUSED(field);
|
||||
|
||||
FuriHalRegion* region = *arg;
|
||||
|
||||
PB_Region_Band band = {0};
|
||||
if(!pb_decode(stream, PB_Region_Band_fields, &band)) {
|
||||
FURI_LOG_E(TAG, "PB Region band decode error: %s", PB_GET_ERROR(stream));
|
||||
return false;
|
||||
}
|
||||
|
||||
region->bands_count += 1;
|
||||
region = realloc( //-V701
|
||||
region,
|
||||
sizeof(FuriHalRegion) + sizeof(FuriHalRegionBand) * region->bands_count);
|
||||
size_t pos = region->bands_count - 1;
|
||||
region->bands[pos].start = band.start;
|
||||
region->bands[pos].end = band.end;
|
||||
region->bands[pos].power_limit = band.power_limit;
|
||||
region->bands[pos].duty_cycle = band.duty_cycle;
|
||||
*arg = region;
|
||||
|
||||
FURI_LOG_I(
|
||||
TAG,
|
||||
"Add allowed band: start %luHz, stop %luHz, power_limit %ddBm, duty_cycle %u%%",
|
||||
band.start,
|
||||
band.end,
|
||||
band.power_limit,
|
||||
band.duty_cycle);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int32_t region_load_file(void* context) {
|
||||
UNUSED(context);
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* file = storage_file_alloc(storage);
|
||||
|
||||
PB_Region pb_region = {0};
|
||||
pb_region.bands.funcs.decode = region_istream_decode_band;
|
||||
|
||||
do {
|
||||
FileInfo fileinfo = {0};
|
||||
|
||||
if(storage_common_stat(storage, SUBGHZ_REGION_FILENAME, &fileinfo) != FSE_OK ||
|
||||
fileinfo.size == 0) {
|
||||
FURI_LOG_W(TAG, "Region file missing or empty");
|
||||
break;
|
||||
|
||||
} else if(!storage_file_open(file, SUBGHZ_REGION_FILENAME, FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
FURI_LOG_E(TAG, "Failed to open region file");
|
||||
break;
|
||||
}
|
||||
|
||||
pb_istream_t istream = {
|
||||
.callback = region_istream_read,
|
||||
.state = file,
|
||||
.errmsg = NULL,
|
||||
.bytes_left = fileinfo.size,
|
||||
};
|
||||
|
||||
pb_region.bands.arg = malloc(sizeof(FuriHalRegion));
|
||||
|
||||
if(!pb_decode(&istream, PB_Region_fields, &pb_region)) {
|
||||
FURI_LOG_E(TAG, "Failed to decode region file");
|
||||
free(pb_region.bands.arg);
|
||||
break;
|
||||
}
|
||||
|
||||
FuriHalRegion* region = pb_region.bands.arg;
|
||||
|
||||
memcpy(
|
||||
region->country_code,
|
||||
pb_region.country_code->bytes,
|
||||
MIN(pb_region.country_code->size, sizeof(region->country_code) - 1));
|
||||
|
||||
furi_hal_region_set(region);
|
||||
|
||||
FURI_LOG_I(TAG, "Dynamic region set: %s", region->country_code);
|
||||
} while(0);
|
||||
|
||||
pb_release(PB_Region_fields, &pb_region);
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void region_loader_pending_callback(void* context, uint32_t arg) {
|
||||
UNUSED(arg);
|
||||
|
||||
FuriThread* loader = context;
|
||||
furi_thread_join(loader);
|
||||
furi_thread_free(loader);
|
||||
}
|
||||
|
||||
static void region_loader_state_callback(FuriThreadState state, void* context) {
|
||||
UNUSED(context);
|
||||
|
||||
if(state == FuriThreadStateStopped) {
|
||||
furi_timer_pending_callback(region_loader_pending_callback, furi_thread_get_current(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void region_storage_callback(const void* message, void* context) {
|
||||
UNUSED(context);
|
||||
const StorageEvent* event = message;
|
||||
|
||||
if(event->type == StorageEventTypeCardMount) {
|
||||
FuriThread* loader = furi_thread_alloc_ex(NULL, 2048, region_load_file, NULL);
|
||||
furi_thread_set_state_callback(loader, region_loader_state_callback);
|
||||
furi_thread_start(loader);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t region_on_system_start(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
furi_pubsub_subscribe(storage_get_pubsub(storage), region_storage_callback, NULL);
|
||||
|
||||
if(storage_sd_status(storage) != FSE_OK) {
|
||||
FURI_LOG_D(TAG, "SD Card not ready, skipping dynamic region");
|
||||
return 0;
|
||||
}
|
||||
|
||||
region_load_file(NULL);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#define STORAGE_INTERNAL_DIR_NAME ".int"
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "storage_processing.h"
|
||||
#include <m-list.h>
|
||||
#include <m-dict.h>
|
||||
|
||||
#include "storage_processing.h"
|
||||
#include "storage_internal_dirname_i.h"
|
||||
|
||||
#define TAG "Storage"
|
||||
|
||||
#define STORAGE_PATH_PREFIX_LEN 4u
|
||||
@@ -555,9 +557,9 @@ void storage_process_alias(
|
||||
|
||||
} else if(furi_string_start_with(path, STORAGE_INT_PATH_PREFIX)) {
|
||||
furi_string_replace_at(
|
||||
path, 0, strlen(STORAGE_INT_PATH_PREFIX), STORAGE_EXT_PATH_PREFIX "/.int");
|
||||
path, 0, strlen(STORAGE_INT_PATH_PREFIX), EXT_PATH(STORAGE_INTERNAL_DIR_NAME));
|
||||
|
||||
FuriString* int_on_ext_path = furi_string_alloc_set(STORAGE_EXT_PATH_PREFIX "/.int");
|
||||
FuriString* int_on_ext_path = furi_string_alloc_set(EXT_PATH(STORAGE_INTERNAL_DIR_NAME));
|
||||
if(storage_process_common_stat(app, int_on_ext_path, NULL) != FSE_OK) {
|
||||
storage_process_common_mkdir(app, int_on_ext_path);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "fatfs.h"
|
||||
#include "../filesystem_api_internal.h"
|
||||
#include "storage_ext.h"
|
||||
#include <fatfs.h>
|
||||
#include <furi_hal.h>
|
||||
#include "sd_notify.h"
|
||||
#include <furi_hal_sd.h>
|
||||
|
||||
#include "sd_notify.h"
|
||||
#include "storage_ext.h"
|
||||
|
||||
#include "../filesystem_api_internal.h"
|
||||
#include "../storage_internal_dirname_i.h"
|
||||
|
||||
typedef FIL SDFile;
|
||||
typedef DIR SDDir;
|
||||
typedef FILINFO SDFileInfo;
|
||||
@@ -93,6 +96,64 @@ static bool sd_mount_card_internal(StorageData* storage, bool notify) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool sd_remove_recursive(const char* path) {
|
||||
SDDir* current_dir = malloc(sizeof(DIR));
|
||||
SDFileInfo* file_info = malloc(sizeof(FILINFO));
|
||||
FuriString* current_path = furi_string_alloc_set(path);
|
||||
|
||||
bool go_deeper = false;
|
||||
SDError status;
|
||||
|
||||
while(true) {
|
||||
status = f_opendir(current_dir, furi_string_get_cstr(current_path));
|
||||
if(status != FR_OK) break;
|
||||
|
||||
while(true) {
|
||||
status = f_readdir(current_dir, file_info);
|
||||
if(status != FR_OK || !strlen(file_info->fname)) break;
|
||||
|
||||
if(file_info->fattrib & AM_DIR) {
|
||||
furi_string_cat_printf(current_path, "/%s", file_info->fname);
|
||||
go_deeper = true;
|
||||
break;
|
||||
|
||||
} else {
|
||||
FuriString* file_path = furi_string_alloc_printf(
|
||||
"%s/%s", furi_string_get_cstr(current_path), file_info->fname);
|
||||
status = f_unlink(furi_string_get_cstr(file_path));
|
||||
furi_string_free(file_path);
|
||||
|
||||
if(status != FR_OK) break;
|
||||
}
|
||||
}
|
||||
|
||||
status = f_closedir(current_dir);
|
||||
if(status != FR_OK) break;
|
||||
|
||||
if(go_deeper) {
|
||||
go_deeper = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
status = f_unlink(furi_string_get_cstr(current_path));
|
||||
if(status != FR_OK) break;
|
||||
|
||||
if(!furi_string_equal(current_path, path)) {
|
||||
size_t last_char_pos = furi_string_search_rchar(current_path, '/');
|
||||
furi_assert(last_char_pos != FURI_STRING_FAILURE);
|
||||
furi_string_left(current_path, last_char_pos);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(current_dir);
|
||||
free(file_info);
|
||||
furi_string_free(current_path);
|
||||
|
||||
return status == FR_OK;
|
||||
}
|
||||
|
||||
FS_Error sd_unmount_card(StorageData* storage) {
|
||||
SDData* sd_data = storage->data;
|
||||
SDError error;
|
||||
@@ -112,21 +173,32 @@ FS_Error sd_mount_card(StorageData* storage, bool notify) {
|
||||
|
||||
if(storage->status != StorageStatusOK) {
|
||||
FURI_LOG_E(TAG, "sd init error: %s", storage_data_status_text(storage));
|
||||
if(notify) {
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
sd_notify_error(notification);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
}
|
||||
error = FSE_INTERNAL;
|
||||
|
||||
} else {
|
||||
FURI_LOG_I(TAG, "card mounted");
|
||||
if(notify) {
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
sd_notify_success(notification);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
}
|
||||
|
||||
#ifndef FURI_RAM_EXEC
|
||||
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagStorageFormatInternal)) {
|
||||
FURI_LOG_I(TAG, "deleting internal storage directory");
|
||||
error = sd_remove_recursive(STORAGE_INTERNAL_DIR_NAME) ? FSE_OK : FSE_INTERNAL;
|
||||
} else {
|
||||
error = FSE_OK;
|
||||
}
|
||||
#else
|
||||
UNUSED(sd_remove_recursive);
|
||||
error = FSE_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
if(notify) {
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
if(error != FSE_OK) {
|
||||
sd_notify_error(notification);
|
||||
} else {
|
||||
sd_notify_success(notification);
|
||||
}
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
}
|
||||
|
||||
return error;
|
||||
@@ -654,4 +726,8 @@ void storage_ext_init(StorageData* storage) {
|
||||
|
||||
// do not notify on first launch, notifications app is waiting for our thread to read settings
|
||||
storage_ext_tick_internal(storage, false);
|
||||
#ifndef FURI_RAM_EXEC
|
||||
// always reset the flag to prevent accidental wipe on SD card insertion
|
||||
furi_hal_rtc_reset_flag(FuriHalRtcFlagStorageFormatInternal);
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user