diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 361b647f7..eb687b6c9 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -72,12 +72,12 @@ jobs: - name: 'Get last release tag' id: release_tag - if: success() + if: always() run: | echo "tag=$(git tag -l --sort=-version:refname | grep -v "rc\|RC" | head -1)" >> $GITHUB_OUTPUT - name: 'Decontaminate previous build leftovers' - if: success() + if: always() run: | if [ -d .git ]; then git submodule status || git checkout "$(git rev-list --max-parents=0 HEAD | tail -n 1)" @@ -85,25 +85,25 @@ jobs: - name: 'Checkout latest release' uses: actions/checkout@v3 - if: success() + if: always() with: fetch-depth: 0 ref: ${{ steps.release_tag.outputs.tag }} - name: 'Flash last release' - if: success() + if: always() run: | ./fbt flash OPENOCD_ADAPTER_SERIAL=2A0906016415303030303032 FIRMWARE_APP_SET=unit_tests FORCE=1 - name: 'Wait for flipper to finish updating' - if: success() + if: always() run: | source scripts/toolchain/fbtenv.sh python3 scripts/testing/await_flipper.py ${{steps.device.outputs.flipper}} - name: 'Format flipper SD card' id: format - if: success() + if: always() run: | source scripts/toolchain/fbtenv.sh python3 scripts/storage.py -p ${{steps.device.outputs.flipper}} format_ext diff --git a/applications/main/nfc/nfc.c b/applications/main/nfc/nfc.c index 0dd071bc4..6e6dc4dcc 100644 --- a/applications/main/nfc/nfc.c +++ b/applications/main/nfc/nfc.c @@ -46,6 +46,7 @@ Nfc* nfc_alloc() { // Nfc device nfc->dev = nfc_device_alloc(); + furi_string_set(nfc->dev->folder, NFC_APP_FOLDER); // Open GUI record nfc->gui = furi_record_open(RECORD_GUI); diff --git a/applications/main/nfc/nfc_i.h b/applications/main/nfc/nfc_i.h index d49b84766..f7e489902 100644 --- a/applications/main/nfc/nfc_i.h +++ b/applications/main/nfc/nfc_i.h @@ -44,6 +44,7 @@ ARRAY_DEF(MfClassicUserKeys, char*, M_PTR_OPLIST); #define NFC_TEXT_STORE_SIZE 128 +#define NFC_APP_FOLDER ANY_PATH("nfc") typedef enum { NfcRpcStateIdle, diff --git a/applications/plugins/clock/application.fam b/applications/plugins/clock/application.fam new file mode 100644 index 000000000..590f5dfe0 --- /dev/null +++ b/applications/plugins/clock/application.fam @@ -0,0 +1,10 @@ +App( + appid="clock", + name="Clock", + apptype=FlipperAppType.PLUGIN, + entry_point="clock_app", + requires=["gui"], + stack_size=2 * 1024, + fap_icon="clock.png", + fap_category="Tools", +) diff --git a/applications/plugins/clock/clock.png b/applications/plugins/clock/clock.png new file mode 100644 index 000000000..0d96df102 Binary files /dev/null and b/applications/plugins/clock/clock.png differ diff --git a/applications/plugins/clock/clock_app.c b/applications/plugins/clock/clock_app.c new file mode 100644 index 000000000..e196f0d29 --- /dev/null +++ b/applications/plugins/clock/clock_app.c @@ -0,0 +1,136 @@ +#include +#include + +#include +#include + +typedef enum { + ClockEventTypeTick, + ClockEventTypeKey, +} ClockEventType; + +typedef struct { + ClockEventType type; + InputEvent input; +} ClockEvent; + +typedef struct { + FuriString* buffer; + FuriHalRtcDateTime datetime; + LocaleTimeFormat timeformat; + LocaleDateFormat dateformat; +} ClockData; + +typedef struct { + FuriMutex* mutex; + FuriMessageQueue* queue; + ClockData* data; +} Clock; + +static void clock_input_callback(InputEvent* input_event, FuriMessageQueue* queue) { + furi_assert(queue); + ClockEvent event = {.type = ClockEventTypeKey, .input = *input_event}; + furi_message_queue_put(queue, &event, FuriWaitForever); +} + +static void clock_render_callback(Canvas* canvas, void* ctx) { + Clock* clock = ctx; + if(furi_mutex_acquire(clock->mutex, 200) != FuriStatusOk) { + return; + } + + ClockData* data = clock->data; + + canvas_set_font(canvas, FontBigNumbers); + locale_format_time(data->buffer, &data->datetime, data->timeformat, true); + canvas_draw_str_aligned( + canvas, 64, 28, AlignCenter, AlignCenter, furi_string_get_cstr(data->buffer)); + + // Special case to cover missing glyphs in FontBigNumbers + if(data->timeformat == LocaleTimeFormat12h) { + size_t time_width = canvas_string_width(canvas, furi_string_get_cstr(data->buffer)); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned( + canvas, + 64 + (time_width / 2) - 10, + 31, + AlignLeft, + AlignCenter, + (data->datetime.hour > 12) ? "AM" : "PM"); + } + + canvas_set_font(canvas, FontSecondary); + locale_format_date(data->buffer, &data->datetime, data->dateformat, "/"); + canvas_draw_str_aligned( + canvas, 64, 42, AlignCenter, AlignTop, furi_string_get_cstr(data->buffer)); + + furi_mutex_release(clock->mutex); +} + +static void clock_tick(void* ctx) { + furi_assert(ctx); + FuriMessageQueue* queue = ctx; + ClockEvent event = {.type = ClockEventTypeTick}; + // It's OK to loose this event if system overloaded + furi_message_queue_put(queue, &event, 0); +} + +int32_t clock_app(void* p) { + UNUSED(p); + Clock* clock = malloc(sizeof(Clock)); + clock->data = malloc(sizeof(ClockData)); + clock->data->buffer = furi_string_alloc(); + + clock->queue = furi_message_queue_alloc(8, sizeof(ClockEvent)); + clock->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + + furi_hal_rtc_get_datetime(&clock->data->datetime); + clock->data->timeformat = locale_get_time_format(); + clock->data->dateformat = locale_get_date_format(); + + // Set ViewPort callbacks + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, clock_render_callback, clock); + view_port_input_callback_set(view_port, clock_input_callback, clock->queue); + + FuriTimer* timer = furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, clock->queue); + + // Open GUI and register view_port + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + furi_timer_start(timer, 100); + + // Main loop + ClockEvent event; + for(bool processing = true; processing;) { + furi_check(furi_message_queue_get(clock->queue, &event, FuriWaitForever) == FuriStatusOk); + furi_mutex_acquire(clock->mutex, FuriWaitForever); + if(event.type == ClockEventTypeKey) { + if(event.input.type == InputTypeShort && event.input.key == InputKeyBack) { + processing = false; + } + } else if(event.type == ClockEventTypeTick) { + furi_hal_rtc_get_datetime(&clock->data->datetime); + } + + furi_mutex_release(clock->mutex); + view_port_update(view_port); + } + + furi_timer_free(timer); + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + view_port_free(view_port); + furi_record_close(RECORD_GUI); + + furi_message_queue_free(clock->queue); + furi_mutex_free(clock->mutex); + + furi_string_free(clock->data->buffer); + + free(clock->data); + free(clock); + + return 0; +} \ No newline at end of file diff --git a/applications/plugins/nfc_magic/nfc_magic.c b/applications/plugins/nfc_magic/nfc_magic.c index 38eecba6a..e4e0ffde0 100644 --- a/applications/plugins/nfc_magic/nfc_magic.c +++ b/applications/plugins/nfc_magic/nfc_magic.c @@ -49,6 +49,7 @@ NfcMagic* nfc_magic_alloc() { // Nfc device nfc_magic->nfc_dev = nfc_device_alloc(); + furi_string_set(nfc_magic->nfc_dev->folder, NFC_APP_FOLDER); // Open GUI record nfc_magic->gui = furi_record_open(RECORD_GUI); diff --git a/applications/plugins/nfc_magic/nfc_magic_i.h b/applications/plugins/nfc_magic/nfc_magic_i.h index 01b300826..378912e5b 100644 --- a/applications/plugins/nfc_magic/nfc_magic_i.h +++ b/applications/plugins/nfc_magic/nfc_magic_i.h @@ -27,6 +27,8 @@ #include #include "nfc_magic_icons.h" +#define NFC_APP_FOLDER ANY_PATH("nfc") + enum NfcMagicCustomEvent { // Reserve first 100 events for button types and indexes, starting from 0 NfcMagicCustomEventReserved = 100, diff --git a/applications/plugins/weather_station/helpers/weather_station_types.h b/applications/plugins/weather_station/helpers/weather_station_types.h index 16c195fa1..d512251f1 100644 --- a/applications/plugins/weather_station/helpers/weather_station_types.h +++ b/applications/plugins/weather_station/helpers/weather_station_types.h @@ -3,7 +3,7 @@ #include #include -#define WS_VERSION_APP "0.6" +#define WS_VERSION_APP "0.6.1" #define WS_DEVELOPED "SkorP" #define WS_GITHUB "https://github.com/flipperdevices/flipperzero-firmware" diff --git a/applications/plugins/weather_station/protocols/ambient_weather.c b/applications/plugins/weather_station/protocols/ambient_weather.c index 5ae22b790..e3c85f40b 100644 --- a/applications/plugins/weather_station/protocols/ambient_weather.c +++ b/applications/plugins/weather_station/protocols/ambient_weather.c @@ -134,8 +134,8 @@ static void ws_protocol_ambient_weather_remote_controller(WSBlockGeneric* instan instance->id = (instance->data >> 32) & 0xFF; instance->battery_low = (instance->data >> 31) & 1; instance->channel = ((instance->data >> 28) & 0x07) + 1; - instance->temp = ws_block_generic_fahrenheit_to_celsius( - ((float)((instance->data >> 16) & 0x0FFF) - 400.0f) / 10.0f); + instance->temp = + locale_fahrenheit_to_celsius(((float)((instance->data >> 16) & 0x0FFF) - 400.0f) / 10.0f); instance->humidity = (instance->data >> 8) & 0xFF; instance->btn = WS_NO_BTN; diff --git a/applications/plugins/weather_station/protocols/infactory.c b/applications/plugins/weather_station/protocols/infactory.c index 2d444d981..53a656d73 100644 --- a/applications/plugins/weather_station/protocols/infactory.c +++ b/applications/plugins/weather_station/protocols/infactory.c @@ -143,8 +143,8 @@ static void ws_protocol_infactory_remote_controller(WSBlockGeneric* instance) { instance->id = instance->data >> 32; instance->battery_low = (instance->data >> 26) & 1; instance->btn = WS_NO_BTN; - instance->temp = ws_block_generic_fahrenheit_to_celsius( - ((float)((instance->data >> 12) & 0x0FFF) - 900.0f) / 10.0f); + instance->temp = + locale_fahrenheit_to_celsius(((float)((instance->data >> 12) & 0x0FFF) - 900.0f) / 10.0f); instance->humidity = (((instance->data >> 8) & 0x0F) * 10) + ((instance->data >> 4) & 0x0F); // BCD, 'A0'=100%rH instance->channel = instance->data & 0x03; diff --git a/applications/plugins/weather_station/protocols/ws_generic.c b/applications/plugins/weather_station/protocols/ws_generic.c index cd5bf6557..dcacda2e4 100644 --- a/applications/plugins/weather_station/protocols/ws_generic.c +++ b/applications/plugins/weather_station/protocols/ws_generic.c @@ -209,7 +209,3 @@ bool ws_block_generic_deserialize(WSBlockGeneric* instance, FlipperFormat* flipp return res; } - -float ws_block_generic_fahrenheit_to_celsius(float fahrenheit) { - return (fahrenheit - 32.0f) / 1.8f; -} \ No newline at end of file diff --git a/applications/plugins/weather_station/protocols/ws_generic.h b/applications/plugins/weather_station/protocols/ws_generic.h index 657f8a1fc..8e6e061ad 100644 --- a/applications/plugins/weather_station/protocols/ws_generic.h +++ b/applications/plugins/weather_station/protocols/ws_generic.h @@ -8,6 +8,7 @@ #include "furi.h" #include "furi_hal.h" #include +#include #ifdef __cplusplus extern "C" { @@ -62,8 +63,6 @@ bool ws_block_generic_serialize( */ bool ws_block_generic_deserialize(WSBlockGeneric* instance, FlipperFormat* flipper_format); -float ws_block_generic_fahrenheit_to_celsius(float fahrenheit); - #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/applications/plugins/weather_station/views/weather_station_receiver_info.c b/applications/plugins/weather_station/views/weather_station_receiver_info.c index 058099217..55d239aad 100644 --- a/applications/plugins/weather_station/views/weather_station_receiver_info.c +++ b/applications/plugins/weather_station/views/weather_station_receiver_info.c @@ -81,13 +81,33 @@ void ws_view_receiver_info_draw(Canvas* canvas, WSReceiverInfoModel* model) { if(model->generic->temp != WS_NO_TEMPERATURE) { canvas_draw_icon(canvas, 6, 43, &I_Therm_7x16); - snprintf(buffer, sizeof(buffer), "%3.1f C", (double)model->generic->temp); - uint8_t temp_x1 = 47; - uint8_t temp_x2 = 38; - if(model->generic->temp < -9.0) { - temp_x1 = 49; - temp_x2 = 40; + + uint8_t temp_x1 = 0; + uint8_t temp_x2 = 0; + if(furi_hal_rtc_get_locale_units() == FuriHalRtcLocaleUnitsMetric) { + snprintf(buffer, sizeof(buffer), "%3.1f C", (double)model->generic->temp); + if(model->generic->temp < -9.0f) { + temp_x1 = 49; + temp_x2 = 40; + } else { + temp_x1 = 47; + temp_x2 = 38; + } + } else { + snprintf( + buffer, + sizeof(buffer), + "%3.1f F", + (double)locale_celsius_to_fahrenheit(model->generic->temp)); + if((model->generic->temp < -27.77f) || (model->generic->temp > 37.77f)) { + temp_x1 = 50; + temp_x2 = 42; + } else { + temp_x1 = 48; + temp_x2 = 40; + } } + canvas_draw_str_aligned(canvas, temp_x1, 47, AlignRight, AlignTop, buffer); canvas_draw_circle(canvas, temp_x2, 46, 1); } diff --git a/applications/services/locale/locale.c b/applications/services/locale/locale.c index 07f56b1b0..e8b6a9fc5 100644 --- a/applications/services/locale/locale.c +++ b/applications/services/locale/locale.c @@ -51,6 +51,9 @@ void locale_format_time( } else { am_pm = 1; } + if(hours == 0) { + hours = 12; + } } if(show_seconds) { diff --git a/lib/nfc/nfc_device.c b/lib/nfc/nfc_device.c index 3ab10a4f6..49eebc37d 100644 --- a/lib/nfc/nfc_device.c +++ b/lib/nfc/nfc_device.c @@ -27,6 +27,7 @@ NfcDevice* nfc_device_alloc() { nfc_dev->dialogs = furi_record_open(RECORD_DIALOGS); nfc_dev->load_path = furi_string_alloc(); nfc_dev->dev_data.parsed_data = furi_string_alloc(); + nfc_dev->folder = furi_string_alloc(); // Rename cache folder name for backward compatibility if(storage_common_stat(nfc_dev->storage, "/ext/nfc/cache", NULL) == FSE_OK) { @@ -42,6 +43,7 @@ void nfc_device_free(NfcDevice* nfc_dev) { furi_record_close(RECORD_DIALOGS); furi_string_free(nfc_dev->load_path); furi_string_free(nfc_dev->dev_data.parsed_data); + furi_string_free(nfc_dev->folder); free(nfc_dev); } @@ -1018,6 +1020,16 @@ static void nfc_device_get_shadow_path(FuriString* orig_path, FuriString* shadow furi_string_cat_printf(shadow_path, "%s", NFC_APP_SHADOW_EXTENSION); } +static void nfc_device_get_folder_from_path(FuriString* path, FuriString* folder) { + size_t last_slash = furi_string_search_rchar(path, '/'); + if(last_slash == FURI_STRING_FAILURE) { + // No slashes in the path, treat the whole path as a folder + furi_string_set(folder, path); + } else { + furi_string_set_n(folder, path, 0, last_slash); + } +} + bool nfc_device_save(NfcDevice* dev, const char* dev_name) { furi_assert(dev); @@ -1028,10 +1040,19 @@ bool nfc_device_save(NfcDevice* dev, const char* dev_name) { temp_str = furi_string_alloc(); do { - // Create nfc directory if necessary - if(!storage_simply_mkdir(dev->storage, NFC_APP_FOLDER)) break; + // Create directory if necessary + FuriString* folder = furi_string_alloc(); + // Get folder from filename (filename is in the form of "folder/filename.nfc", so the folder is "folder/") + furi_string_set(temp_str, dev_name); + // Get folder from filename + nfc_device_get_folder_from_path(temp_str, folder); + FURI_LOG_I("Nfc", "Saving to folder %s", furi_string_get_cstr(folder)); + if(!storage_simply_mkdir(dev->storage, furi_string_get_cstr(folder))) { + FURI_LOG_E("Nfc", "Failed to create folder %s", furi_string_get_cstr(folder)); + break; + } + furi_string_free(folder); // First remove nfc device file if it was saved - furi_string_printf(temp_str, "%s", dev_name); // Open file if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break; // Write header @@ -1199,10 +1220,9 @@ bool nfc_device_load(NfcDevice* dev, const char* file_path, bool show_dialog) { bool nfc_file_select(NfcDevice* dev) { furi_assert(dev); + const char* folder = furi_string_get_cstr(dev->folder); // Input events and views are managed by file_browser - FuriString* nfc_app_folder; - nfc_app_folder = furi_string_alloc_set(NFC_APP_FOLDER); const DialogsFileBrowserOptions browser_options = { .extension = NFC_APP_EXTENSION, @@ -1212,13 +1232,12 @@ bool nfc_file_select(NfcDevice* dev) { .hide_ext = true, .item_loader_callback = NULL, .item_loader_context = NULL, - .base_path = NFC_APP_FOLDER, + .base_path = folder, }; bool res = dialog_file_browser_show(dev->dialogs, dev->load_path, dev->load_path, &browser_options); - furi_string_free(nfc_app_folder); if(res) { FuriString* filename; filename = furi_string_alloc(); @@ -1271,7 +1290,11 @@ bool nfc_device_delete(NfcDevice* dev, bool use_load_path) { furi_string_set(file_path, dev->load_path); } else { furi_string_printf( - file_path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_EXTENSION); + file_path, + "%s/%s%s", + furi_string_get_cstr(dev->folder), + dev->dev_name, + NFC_APP_EXTENSION); } if(!storage_simply_remove(dev->storage, furi_string_get_cstr(file_path))) break; // Delete shadow file if it exists @@ -1280,7 +1303,11 @@ bool nfc_device_delete(NfcDevice* dev, bool use_load_path) { nfc_device_get_shadow_path(dev->load_path, file_path); } else { furi_string_printf( - file_path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_SHADOW_EXTENSION); + file_path, + "%s/%s%s", + furi_string_get_cstr(dev->folder), + dev->dev_name, + NFC_APP_SHADOW_EXTENSION); } if(!storage_simply_remove(dev->storage, furi_string_get_cstr(file_path))) break; } @@ -1309,14 +1336,23 @@ bool nfc_device_restore(NfcDevice* dev, bool use_load_path) { nfc_device_get_shadow_path(dev->load_path, path); } else { furi_string_printf( - path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_SHADOW_EXTENSION); + path, + "%s/%s%s", + furi_string_get_cstr(dev->folder), + dev->dev_name, + NFC_APP_SHADOW_EXTENSION); } if(!storage_simply_remove(dev->storage, furi_string_get_cstr(path))) break; dev->shadow_file_exist = false; if(use_load_path && !furi_string_empty(dev->load_path)) { furi_string_set(path, dev->load_path); } else { - furi_string_printf(path, "%s/%s%s", NFC_APP_FOLDER, dev->dev_name, NFC_APP_EXTENSION); + furi_string_printf( + path, + "%s/%s%s", + furi_string_get_cstr(dev->folder), + dev->dev_name, + NFC_APP_EXTENSION); } if(!nfc_device_load_data(dev, path, true)) break; restored = true; diff --git a/lib/nfc/nfc_device.h b/lib/nfc/nfc_device.h index 4be07f016..55ee4ac4c 100644 --- a/lib/nfc/nfc_device.h +++ b/lib/nfc/nfc_device.h @@ -20,7 +20,6 @@ extern "C" { #define NFC_READER_DATA_MAX_SIZE 64 #define NFC_DICT_KEY_BATCH_SIZE 50 -#define NFC_APP_FOLDER ANY_PATH("nfc") #define NFC_APP_EXTENSION ".nfc" #define NFC_APP_SHADOW_EXTENSION ".shd" @@ -84,6 +83,7 @@ typedef struct { NfcDeviceData dev_data; char dev_name[NFC_DEV_NAME_MAX_LEN + 1]; FuriString* load_path; + FuriString* folder; NfcDeviceSaveFormat format; bool shadow_file_exist;