diff --git a/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_app.h b/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_app.h index 809fcc6f..3a30d16c 100644 --- a/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_app.h +++ b/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_app.h @@ -4,7 +4,7 @@ extern "C" { #endif -#define ESP_FLASHER_APP_VERSION "v1.1" +#define ESP_FLASHER_APP_VERSION "v1.2" typedef struct EspFlasherApp EspFlasherApp; diff --git a/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_app_i.h b/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_app_i.h index 5b313345..37264a2e 100644 --- a/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_app_i.h +++ b/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_app_i.h @@ -33,11 +33,18 @@ typedef enum SelectedFlashOptions { SelectedFlashPart, SelectedFlashNvs, SelectedFlashBootApp0, - SelectedFlashApp, + SelectedFlashAppA, + SelectedFlashAppB, SelectedFlashCustom, NUM_FLASH_OPTIONS } SelectedFlashOptions; +typedef enum { + SwitchNotSet, + SwitchToFirmwareA, + SwitchToFirmwareB, +} SwitchFirmware; + struct EspFlasherApp { Gui* gui; ViewDispatcher* view_dispatcher; @@ -59,13 +66,16 @@ struct EspFlasherApp { bool reset; bool boot; + SwitchFirmware switch_fw; + bool selected_flash_options[NUM_FLASH_OPTIONS]; int num_selected_flash_options; char bin_file_path_boot[100]; char bin_file_path_part[100]; char bin_file_path_nvs[100]; char bin_file_path_boot_app0[100]; - char bin_file_path_app[100]; + char bin_file_path_app_a[100]; + char bin_file_path_app_b[100]; char bin_file_path_custom[100]; FuriThread* flash_worker; bool flash_worker_busy; diff --git a/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_worker.c b/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_worker.c index 86f93945..d47fe052 100644 --- a/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_worker.c +++ b/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_worker.c @@ -74,6 +74,85 @@ static esp_loader_error_t _flash_file(EspFlasherApp* app, char* filepath, uint32 return ESP_LOADER_SUCCESS; } +// This in-app FW switch "exploits" the otadata (boot_app0) +// - the first four bytes of each array are the counter and the last four bytes are just a CRC of that counter +// - the bootloader will just boot whichever app has the highest counter in the otadata partition +// so we'll just pick 1 for A, and then B will use either 0 or 2 depending on whether it's the slot in use + +#define MAGIC_PAYLOAD_SIZE (32) + +const uint8_t magic_payload_app_a[MAGIC_PAYLOAD_SIZE] = {0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x9a, 0x98, 0x43, 0x47}; + +const uint8_t magic_payload_app_b_unset[MAGIC_PAYLOAD_SIZE] = { + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + +const uint8_t magic_payload_app_b_set[MAGIC_PAYLOAD_SIZE] = { + 0x02, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x74, 0x37, 0xf6, 0x55}; + +// return true if "switching" fw selected instead of flashing new fw +// (this does not indicate success) +static bool _switch_fw(EspFlasherApp* app) { + if(app->switch_fw == SwitchNotSet) { + return false; + } + + esp_loader_error_t err; + char user_msg[256]; + + loader_port_debug_print("Preparing to set flags for firmware A\n"); + err = esp_loader_flash_start( + ESP_ADDR_BOOT_APP0 + ESP_ADDR_OTADATA_OFFSET_APP_A, + MAGIC_PAYLOAD_SIZE, + MAGIC_PAYLOAD_SIZE); + if(err != ESP_LOADER_SUCCESS) { + snprintf(user_msg, sizeof(user_msg), "Erasing flash failed with error %d\n", err); + loader_port_debug_print(user_msg); + return true; + } + + loader_port_debug_print("Setting flags for firmware A\n"); + const uint8_t* which_payload_app_a = magic_payload_app_a; + err = esp_loader_flash_write((void*)which_payload_app_a, MAGIC_PAYLOAD_SIZE); + if(err != ESP_LOADER_SUCCESS) { + snprintf(user_msg, sizeof(user_msg), "Packet could not be written! Error: %u\n", err); + loader_port_debug_print(user_msg); + return true; + } + + loader_port_debug_print("Preparing to set flags for firmware B\n"); + err = esp_loader_flash_start( + ESP_ADDR_BOOT_APP0 + ESP_ADDR_OTADATA_OFFSET_APP_B, + MAGIC_PAYLOAD_SIZE, + MAGIC_PAYLOAD_SIZE); + if(err != ESP_LOADER_SUCCESS) { + snprintf(user_msg, sizeof(user_msg), "Erasing flash failed with error %d\n", err); + loader_port_debug_print(user_msg); + return true; + } + + loader_port_debug_print("Setting flags for firmware B\n"); + const uint8_t* which_payload_app_b = + (app->switch_fw == SwitchToFirmwareB ? magic_payload_app_b_set : + magic_payload_app_b_unset); + err = esp_loader_flash_write((void*)which_payload_app_b, MAGIC_PAYLOAD_SIZE); + if(err != ESP_LOADER_SUCCESS) { + snprintf(user_msg, sizeof(user_msg), "Packet could not be written! Error: %u\n", err); + loader_port_debug_print(user_msg); + return true; + } + + loader_port_debug_print("Finished programming\n"); + return true; +} + typedef struct { SelectedFlashOptions selected; const char* description; @@ -85,7 +164,7 @@ static void _flash_all_files(EspFlasherApp* app) { esp_loader_error_t err; const int num_steps = app->num_selected_flash_options; -#define NUM_FLASH_ITEMS 6 +#define NUM_FLASH_ITEMS 7 FlashItem items[NUM_FLASH_ITEMS] = { {SelectedFlashBoot, "bootloader", @@ -94,7 +173,8 @@ static void _flash_all_files(EspFlasherApp* app) { {SelectedFlashPart, "partition table", app->bin_file_path_part, ESP_ADDR_PART}, {SelectedFlashNvs, "NVS", app->bin_file_path_nvs, ESP_ADDR_NVS}, {SelectedFlashBootApp0, "boot_app0", app->bin_file_path_boot_app0, ESP_ADDR_BOOT_APP0}, - {SelectedFlashApp, "firmware", app->bin_file_path_app, ESP_ADDR_APP}, + {SelectedFlashAppA, "firmware A", app->bin_file_path_app_a, ESP_ADDR_APP_A}, + {SelectedFlashAppB, "firmware B", app->bin_file_path_app_b, ESP_ADDR_APP_B}, {SelectedFlashCustom, "custom data", app->bin_file_path_custom, 0x0}, /* if you add more entries, update NUM_FLASH_ITEMS above! */ }; @@ -167,12 +247,16 @@ static int32_t esp_flasher_flash_bin(void* context) { if(!err) { loader_port_debug_print("Connected\n"); - _flash_all_files(app); + if(!_switch_fw(app)) { + _flash_all_files(app); + } + app->switch_fw = SwitchNotSet; #if 0 loader_port_debug_print("Restoring transmission rate\n"); furi_hal_uart_set_br(FuriHalUartIdUSART1, 115200); #endif - loader_port_debug_print("Done flashing. Please reset the board manually if it doesn't auto-reset.\n"); + loader_port_debug_print( + "Done flashing. Please reset the board manually if it doesn't auto-reset.\n"); // auto-reset for supported boards loader_port_reset_target(); @@ -222,10 +306,10 @@ static int32_t esp_flasher_reset(void* context) { _setRTS(false); _initRTS(); - if (app->reset) { + if(app->reset) { loader_port_debug_print("Resetting board\n"); loader_port_reset_target(); - } else if (app->boot) { + } else if(app->boot) { loader_port_debug_print("Entering bootloader\n"); loader_port_enter_bootloader(); } @@ -245,7 +329,7 @@ void esp_flasher_worker_start_thread(EspFlasherApp* app) { furi_thread_set_name(app->flash_worker, "EspFlasherFlashWorker"); furi_thread_set_stack_size(app->flash_worker, 2048); furi_thread_set_context(app->flash_worker, app); - if (app->reset || app->boot) { + if(app->reset || app->boot) { furi_thread_set_callback(app->flash_worker, esp_flasher_reset); } else { furi_thread_set_callback(app->flash_worker, esp_flasher_flash_bin); @@ -280,6 +364,8 @@ void loader_port_reset_target(void) { } void loader_port_enter_bootloader(void) { + // adapted from custom usb-jtag-serial reset in esptool + // (works on official wifi dev board) _setDTR(true); loader_port_delay_ms(SERIAL_FLASHER_RESET_HOLD_TIME_MS); _setRTS(true); diff --git a/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_worker.h b/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_worker.h index 44461e7d..0dba16f0 100644 --- a/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_worker.h +++ b/non_catalog_apps/flipperzero-esp-flasher/esp_flasher_worker.h @@ -14,7 +14,11 @@ #define ESP_ADDR_PART 0x8000 #define ESP_ADDR_NVS 0x9000 #define ESP_ADDR_BOOT_APP0 0xE000 -#define ESP_ADDR_APP 0x10000 +#define ESP_ADDR_APP_A 0x10000 +#define ESP_ADDR_APP_B 0x150000 + +#define ESP_ADDR_OTADATA_OFFSET_APP_A 0x0 +#define ESP_ADDR_OTADATA_OFFSET_APP_B 0x1000 void esp_flasher_worker_start_thread(EspFlasherApp* app); void esp_flasher_worker_stop_thread(EspFlasherApp* app); diff --git a/non_catalog_apps/flipperzero-esp-flasher/scenes/esp_flasher_scene_browse.c b/non_catalog_apps/flipperzero-esp-flasher/scenes/esp_flasher_scene_browse.c index 88a9b25e..8b223b29 100644 --- a/non_catalog_apps/flipperzero-esp-flasher/scenes/esp_flasher_scene_browse.c +++ b/non_catalog_apps/flipperzero-esp-flasher/scenes/esp_flasher_scene_browse.c @@ -7,7 +7,8 @@ enum SubmenuIndex { SubmenuIndexPart, SubmenuIndexNvs, SubmenuIndexBootApp0, - SubmenuIndexApp, + SubmenuIndexAppA, + SubmenuIndexAppB, SubmenuIndexCustom, SubmenuIndexFlash, }; @@ -97,19 +98,35 @@ static void esp_flasher_scene_browse_callback(void* context, uint32_t index) { } view_dispatcher_send_custom_event(app->view_dispatcher, EspFlasherEventRefreshSubmenu); break; - case SubmenuIndexApp: - app->selected_flash_options[SelectedFlashApp] = - !app->selected_flash_options[SelectedFlashApp]; + case SubmenuIndexAppA: + app->selected_flash_options[SelectedFlashAppA] = + !app->selected_flash_options[SelectedFlashAppA]; if(dialog_file_browser_show( app->dialogs, selected_filepath, predefined_filepath, &browser_options)) { strncpy( - app->bin_file_path_app, + app->bin_file_path_app_a, furi_string_get_cstr(selected_filepath), - sizeof(app->bin_file_path_app)); + sizeof(app->bin_file_path_app_a)); } - if(app->bin_file_path_app[0] == '\0') { + if(app->bin_file_path_app_a[0] == '\0') { // if user didn't select a file, leave unselected - app->selected_flash_options[SelectedFlashApp] = false; + app->selected_flash_options[SelectedFlashAppA] = false; + } + view_dispatcher_send_custom_event(app->view_dispatcher, EspFlasherEventRefreshSubmenu); + break; + case SubmenuIndexAppB: + app->selected_flash_options[SelectedFlashAppB] = + !app->selected_flash_options[SelectedFlashAppB]; + if(dialog_file_browser_show( + app->dialogs, selected_filepath, predefined_filepath, &browser_options)) { + strncpy( + app->bin_file_path_app_b, + furi_string_get_cstr(selected_filepath), + sizeof(app->bin_file_path_app_b)); + } + if(app->bin_file_path_app_b[0] == '\0') { + // if user didn't select a file, leave unselected + app->selected_flash_options[SelectedFlashAppB] = false; } view_dispatcher_send_custom_event(app->view_dispatcher, EspFlasherEventRefreshSubmenu); break; @@ -157,7 +174,8 @@ static void esp_flasher_scene_browse_callback(void* context, uint32_t index) { #define STR_PART "Part Table (" TOSTRING(ESP_ADDR_PART) ")" #define STR_NVS "NVS (" TOSTRING(ESP_ADDR_NVS) ")" #define STR_BOOT_APP0 "boot_app0 (" TOSTRING(ESP_ADDR_BOOT_APP0) ")" -#define STR_APP "Firmware (" TOSTRING(ESP_ADDR_APP) ")" +#define STR_APP_A "FirmwareA(" TOSTRING(ESP_ADDR_APP_A) ")" +#define STR_APP_B "FirmwareB(" TOSTRING(ESP_ADDR_APP_B) ")" #define STR_CUSTOM "Custom" #define STR_FLASH_S3 "[>] FLASH (ESP32-S3)" #define STR_FLASH "[>] FLASH" @@ -213,9 +231,16 @@ static void _refresh_submenu(EspFlasherApp* app) { app); submenu_add_item( submenu, - app->selected_flash_options[SelectedFlashApp] ? STR_SELECT " " STR_APP : - STR_UNSELECT " " STR_APP, - SubmenuIndexApp, + app->selected_flash_options[SelectedFlashAppA] ? STR_SELECT " " STR_APP_A : + STR_UNSELECT " " STR_APP_A, + SubmenuIndexAppA, + esp_flasher_scene_browse_callback, + app); + submenu_add_item( + submenu, + app->selected_flash_options[SelectedFlashAppB] ? STR_SELECT " " STR_APP_B : + STR_UNSELECT " " STR_APP_B, + SubmenuIndexAppB, esp_flasher_scene_browse_callback, app); // TODO: custom addr @@ -241,7 +266,8 @@ void esp_flasher_scene_browse_on_enter(void* context) { app->bin_file_path_part[0] = '\0'; app->bin_file_path_nvs[0] = '\0'; app->bin_file_path_boot_app0[0] = '\0'; - app->bin_file_path_app[0] = '\0'; + app->bin_file_path_app_a[0] = '\0'; + app->bin_file_path_app_b[0] = '\0'; app->bin_file_path_custom[0] = '\0'; _refresh_submenu(app); diff --git a/non_catalog_apps/flipperzero-esp-flasher/scenes/esp_flasher_scene_start.c b/non_catalog_apps/flipperzero-esp-flasher/scenes/esp_flasher_scene_start.c index 2d91f2d3..8edce577 100644 --- a/non_catalog_apps/flipperzero-esp-flasher/scenes/esp_flasher_scene_start.c +++ b/non_catalog_apps/flipperzero-esp-flasher/scenes/esp_flasher_scene_start.c @@ -2,6 +2,8 @@ enum SubmenuIndex { SubmenuIndexEspFlasherFlash, + SubmenuIndexEspFlasherSwitchA, + SubmenuIndexEspFlasherSwitchB, SubmenuIndexEspFlasherAbout, SubmenuIndexEspFlasherReset, SubmenuIndexEspFlasherBootloader, @@ -25,6 +27,18 @@ void esp_flasher_scene_start_on_enter(void* context) { SubmenuIndexEspFlasherFlash, esp_flasher_scene_start_submenu_callback, app); + submenu_add_item( + submenu, + "Switch to Firmware A", + SubmenuIndexEspFlasherSwitchA, + esp_flasher_scene_start_submenu_callback, + app); + submenu_add_item( + submenu, + "Switch to Firmware B", + SubmenuIndexEspFlasherSwitchB, + esp_flasher_scene_start_submenu_callback, + app); submenu_add_item( submenu, "Reset Board", @@ -62,6 +76,14 @@ bool esp_flasher_scene_start_on_event(void* context, SceneManagerEvent event) { } else if(event.event == SubmenuIndexEspFlasherFlash) { scene_manager_next_scene(app->scene_manager, EspFlasherSceneBrowse); consumed = true; + } else if(event.event == SubmenuIndexEspFlasherSwitchA) { + app->switch_fw = SwitchToFirmwareA; + scene_manager_next_scene(app->scene_manager, EspFlasherSceneConsoleOutput); + consumed = true; + } else if(event.event == SubmenuIndexEspFlasherSwitchB) { + app->switch_fw = SwitchToFirmwareB; + scene_manager_next_scene(app->scene_manager, EspFlasherSceneConsoleOutput); + consumed = true; } else if(event.event == SubmenuIndexEspFlasherReset) { app->reset = true; scene_manager_next_scene(app->scene_manager, EspFlasherSceneConsoleOutput); diff --git a/non_catalog_apps/sudoku/sudoku.c b/non_catalog_apps/sudoku/sudoku.c index 9ba3a6f4..497152f4 100644 --- a/non_catalog_apps/sudoku/sudoku.c +++ b/non_catalog_apps/sudoku/sudoku.c @@ -1,10 +1,11 @@ #include #include #include +#include #include #include #include -#include +#include #define TAG "sudoku" @@ -25,7 +26,8 @@ static_assert(USER_INPUT_FLAG > VALUE_MASK); typedef enum { GameStateRunning, GameStatePaused, - GameStateWin, + GameStateVictory, + GameStateRestart, // util state } GameState; typedef struct { @@ -46,7 +48,7 @@ const char* MENU_ITEMS[] = { "Easy game", "Nornal game", "Hard game", - "Exit", + "Save+Exit", }; /* @@ -80,6 +82,47 @@ const uint8_t u8g2_font_tom_thumb_4x6_tr[725] = "y\11\227\307$\225dJ\0z\7\223\310\254\221\6{\10\227\310\251\32D\1|\6\265\310(\1}\11" "\227\310\310\14RR\0~\6\213\313\215\4\0\0\0\4\377\377\0"; +#define SAVE_VERSION 1 +#define SAVE_FILE APP_DATA_PATH("save.dat") + +bool load_game(SudokuState* state) { + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + bool res = false; + + if(storage_file_open(file, SAVE_FILE, FSAM_READ, FSOM_OPEN_EXISTING)) { + uint16_t version = 0; + uint64_t expectedSize = sizeof(version) + sizeof(SudokuState); + uint64_t fileSize = storage_file_size(file); + if(fileSize >= expectedSize) { + storage_file_read(file, &version, sizeof(version)); + if(version != SAVE_VERSION) { + storage_simply_remove(storage, SAVE_FILE); + } else { + res = storage_file_read(file, state, sizeof(SudokuState)) == sizeof(SudokuState); + } + } + } + + storage_file_free(file); // Closes the file if it was open. + furi_record_close(RECORD_STORAGE); + return res; +} + +void save_game(SudokuState* app) { + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + + if(storage_file_open(file, SAVE_FILE, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + uint16_t version = SAVE_VERSION; + storage_file_write(file, &version, sizeof(version)); + storage_file_write(file, app, sizeof(SudokuState)); + } + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); +} + // inspired by game_2048 static void gray_canvas(Canvas* const canvas) { canvas_set_color(canvas, ColorWhite); @@ -184,7 +227,7 @@ static void draw_callback(Canvas* canvas, void* ctx) { i * FONT_SIZE * 3 + gapY + yOffset); } - if(state->state == GameStateWin || state->state == GameStatePaused) { + if(state->state == GameStateVictory || state->state == GameStatePaused) { gray_canvas(canvas); canvas_set_color(canvas, ColorWhite); int w = canvas_width(canvas); @@ -214,7 +257,7 @@ static void draw_callback(Canvas* canvas, void* ctx) { winY + offY + itemH * i + itemH / 2, AlignCenter, AlignCenter, - i == 0 && state->state == GameStateWin ? "VICTORY!" : MENU_ITEMS[i]); + i == 0 && state->state == GameStateVictory ? "VICTORY!" : MENU_ITEMS[i]); } } furi_mutex_release(state->mutex); @@ -372,11 +415,13 @@ int32_t sudoku_main(void* p) { FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); SudokuState* state = malloc(sizeof(SudokuState)); + if(!load_game(state) || state->state == GameStateRestart) { + state->state = GameStateRunning; + state->menuCursor = 0; + start_game(state, NORMAL_GAPS); + } state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); furi_check(state->mutex, "mutex alloc failed"); - state->state = GameStateRunning; - state->menuCursor = 0; - start_game(state, NORMAL_GAPS); ViewPort* view_port = view_port_alloc(); view_port_draw_callback_set(view_port, draw_callback, state); view_port_input_callback_set(view_port, input_callback, event_queue); @@ -392,7 +437,7 @@ int32_t sudoku_main(void* p) { furi_mutex_acquire(state->mutex, FuriWaitForever); - if(state->state == GameStatePaused || state->state == GameStateWin) { + if(state->state == GameStatePaused || state->state == GameStateVictory) { bool exit = false; if(event.type == InputTypePress || event.type == InputTypeLong || event.type == InputTypeRepeat) { @@ -470,7 +515,7 @@ int32_t sudoku_main(void* p) { } if(invalidField && validate_board(state)) { dolphin_deed(DolphinDeedPluginGameWin); - state->state = GameStateWin; + state->state = GameStateVictory; state->menuCursor = 0; for(int i = 0; i != BOARD_SIZE; ++i) { for(int j = 0; j != BOARD_SIZE; ++j) { @@ -491,6 +536,12 @@ int32_t sudoku_main(void* p) { furi_record_close(RECORD_GUI); furi_mutex_free(state->mutex); + if(state->state == GameStateVictory) { + state->state = GameStateRestart; + } + state->menuCursor = 0; // reset menu cursor, because we stand on exit + state->mutex = NULL; + save_game(state); free(state); return 0;