Jelajahi Sumber

[FL-2269] Core2 OTA (#1144)

* C2OTA: wip
* Update Cube to 1.13.3
* Fixed prio
* Functional Core2 updater
* Removed hardware CRC usage; code cleanup & linter fixes
* Moved hardcoded stack params to copro.mk
* Fixing CI bundling of core2 fw
* Removed last traces of hardcoded radio stack
* OB processing draft
* Python scripts cleanup
* Support for comments in ob data
* Sacrificed SD card icon in favor of faster update. Waiting for Storage fix
* Additional handling for OB mismatched values
* Description for new furi_hal apis; spelling fixes
* Rework of OB write, WIP
* Properly restarting OB verification loop
* Split update_task_workers.c
* Checking OBs after enabling post-update mode
* Moved OB verification before flashing
* Removed ob.data for custom stacks
* Fixed progress calculation for OB
* Removed unnecessary OB mask cast

Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
hedger 3 tahun lalu
induk
melakukan
7ce305fca3
41 mengubah file dengan 1623 tambahan dan 296 penghapusan
  1. 7 7
      .github/workflows/build.yml
  2. 11 6
      Makefile
  3. 1 1
      applications/bt/bt_cli.c
  4. 7 0
      applications/bt/bt_service/bt.c
  5. 3 10
      applications/updater/scenes/updater_scene_main.c
  6. 13 10
      applications/updater/util/update_task.c
  7. 6 3
      applications/updater/util/update_task.h
  8. 6 86
      applications/updater/util/update_task_worker_backup.c
  9. 363 0
      applications/updater/util/update_task_worker_flasher.c
  10. 1 0
      assets/.gitignore
  11. 7 0
      assets/Makefile
  12. 12 0
      assets/copro.mk
  13. 3 5
      firmware/targets/f7/Src/update.c
  14. 180 43
      firmware/targets/f7/ble_glue/ble_glue.c
  15. 71 4
      firmware/targets/f7/ble_glue/ble_glue.h
  16. 0 1
      firmware/targets/f7/furi_hal/furi_hal.c
  17. 31 12
      firmware/targets/f7/furi_hal/furi_hal_bt.c
  18. 6 1
      firmware/targets/f7/furi_hal/furi_hal_crc.c
  19. 130 4
      firmware/targets/f7/furi_hal/furi_hal_flash.c
  20. 45 4
      firmware/targets/f7/furi_hal/furi_hal_flash.h
  21. 19 18
      firmware/targets/f7/furi_hal/furi_hal_info.c
  22. 1 1
      firmware/targets/f7/furi_hal/furi_hal_vcp.c
  23. 0 1
      firmware/targets/furi_hal_include/furi_hal.h
  24. 7 0
      firmware/targets/furi_hal_include/furi_hal_bt.h
  25. 1 0
      firmware/targets/furi_hal_include/furi_hal_rtc.h
  26. 1 1
      lib/STM32CubeWB
  27. 2 1
      lib/lib.mk
  28. 38 0
      lib/toolbox/crc32_calc.c
  29. 18 0
      lib/toolbox/crc32_calc.h
  30. 3 31
      lib/update_util/dfu_file.c
  31. 59 2
      lib/update_util/update_manifest.c
  32. 20 1
      lib/update_util/update_manifest.h
  33. 2 12
      lib/update_util/update_operation.c
  34. 32 5
      scripts/assets.py
  35. 1 0
      scripts/dist.py
  36. 23 4
      scripts/flash.py
  37. 1 1
      scripts/flipper/app.py
  38. 28 14
      scripts/flipper/assets/copro.py
  39. 187 0
      scripts/flipper/assets/coprobin.py
  40. 208 0
      scripts/flipper/assets/obdata.py
  41. 69 7
      scripts/update.py

+ 7 - 7
.github/workflows/build.yml

@@ -121,11 +121,11 @@ jobs:
 
 
       - name: 'Bundle core2 firmware'
       - name: 'Bundle core2 firmware'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         if: ${{ !github.event.pull_request.head.repo.fork }}
-        run: |
-          test -d core2_firmware && rm -rf core2_firmware || true
-          mkdir core2_firmware
-          ./scripts/assets.py copro lib/STM32CubeWB core2_firmware STM32WB5x
-          tar czpf artifacts/flipper-z-any-core2_firmware-${{steps.names.outputs.suffix}}.tgz core2_firmware
+        uses: ./.github/actions/docker
+        with:
+          run: |
+            make -C assets copro_bundle
+            tar czpf artifacts/flipper-z-any-core2_firmware-${{steps.names.outputs.suffix}}.tgz -C assets core2_firmware
 
 
       - name: 'Upload artifacts to update server'
       - name: 'Upload artifacts to update server'
         if: ${{ !github.event.pull_request.head.repo.fork }}
         if: ${{ !github.event.pull_request.head.repo.fork }}
@@ -213,8 +213,8 @@ jobs:
         with:
         with:
           run: |
           run: |
             set -e
             set -e
-            make -C assets clean
-            make -C assets
+            make assets_rebuild assets_manifest
+            git diff --quiet || ( echo "Assets recompilation required."; exit 255 )
 
 
       - name: 'Build the firmware in docker'
       - name: 'Build the firmware in docker'
         uses: ./.github/actions/docker
         uses: ./.github/actions/docker

+ 11 - 6
Makefile

@@ -1,8 +1,7 @@
 PROJECT_ROOT := $(abspath $(dir $(abspath $(firstword $(MAKEFILE_LIST)))))
 PROJECT_ROOT := $(abspath $(dir $(abspath $(firstword $(MAKEFILE_LIST)))))
 
 
 include			$(PROJECT_ROOT)/make/git.mk
 include			$(PROJECT_ROOT)/make/git.mk
-
-COPRO_DIR := $(PROJECT_ROOT)/lib/STM32CubeWB/Projects/STM32WB_Copro_Wireless_Binaries/STM32WB5x
+include			$(PROJECT_ROOT)/assets/copro.mk
 
 
 PROJECT_SOURCE_DIRECTORIES := \
 PROJECT_SOURCE_DIRECTORIES := \
 	$(PROJECT_ROOT)/applications \
 	$(PROJECT_ROOT)/applications \
@@ -97,7 +96,13 @@ updater_package_bin: firmware_all updater
 
 
 .PHONY: updater_package
 .PHONY: updater_package
 updater_package: firmware_all updater assets_manifest
 updater_package: firmware_all updater assets_manifest
-	@$(PROJECT_ROOT)/scripts/dist.py copy -t $(TARGET) -p firmware updater -s $(DIST_SUFFIX) -r $(PROJECT_ROOT)/assets/resources --bundlever "$(VERSION_STRING)"
+	@$(PROJECT_ROOT)/scripts/dist.py copy \
+	-t $(TARGET) -p firmware updater \
+	-s $(DIST_SUFFIX) -r $(PROJECT_ROOT)/assets/resources \
+	--bundlever "$(VERSION_STRING)" \
+	--radio $(COPRO_STACK_BIN_PATH) \
+	--radiotype $(COPRO_STACK_TYPE) \
+	--obdata $(PROJECT_ROOT)/scripts/ob.data
 
 
 .PHONY: assets_manifest
 .PHONY: assets_manifest
 assets_manifest:
 assets_manifest:
@@ -109,7 +114,7 @@ assets_rebuild:
 
 
 .PHONY: flash_radio
 .PHONY: flash_radio
 flash_radio:
 flash_radio:
-	@$(PROJECT_ROOT)/scripts/flash.py core2radio 0x080D7000 $(COPRO_DIR)/stm32wb5x_BLE_Stack_light_fw.bin
+	@$(PROJECT_ROOT)/scripts/flash.py core2radio $(COPRO_STACK_BIN_PATH) --addr=$(COPRO_STACK_ADDR)
 	@$(PROJECT_ROOT)/scripts/ob.py set
 	@$(PROJECT_ROOT)/scripts/ob.py set
 
 
 .PHONY: flash_radio_fus
 .PHONY: flash_radio_fus
@@ -125,8 +130,8 @@ flash_radio_fus:
 
 
 .PHONY: flash_radio_fus_please_i_m_not_going_to_complain
 .PHONY: flash_radio_fus_please_i_m_not_going_to_complain
 flash_radio_fus_please_i_m_not_going_to_complain:
 flash_radio_fus_please_i_m_not_going_to_complain:
-	@$(PROJECT_ROOT)/scripts/flash.py core2fus 0x080EC000 --statement=AGREE_TO_LOSE_FLIPPER_FEATURES_THAT_USE_CRYPTO_ENCLAVE $(COPRO_DIR)/stm32wb5x_FUS_fw_for_fus_0_5_3.bin
-	@$(PROJECT_ROOT)/scripts/flash.py core2fus 0x080EC000 --statement=AGREE_TO_LOSE_FLIPPER_FEATURES_THAT_USE_CRYPTO_ENCLAVE $(COPRO_DIR)/stm32wb5x_FUS_fw.bin
+	@$(PROJECT_ROOT)/scripts/flash.py core2fus 0x080EC000 --statement=AGREE_TO_LOSE_FLIPPER_FEATURES_THAT_USE_CRYPTO_ENCLAVE $(COPRO_FIRMWARE_DIR)/stm32wb5x_FUS_fw_for_fus_0_5_3.bin
+	@$(PROJECT_ROOT)/scripts/flash.py core2fus 0x080EC000 --statement=AGREE_TO_LOSE_FLIPPER_FEATURES_THAT_USE_CRYPTO_ENCLAVE $(COPRO_FIRMWARE_DIR)/stm32wb5x_FUS_fw.bin
 	@$(PROJECT_ROOT)/scripts/ob.py set
 	@$(PROJECT_ROOT)/scripts/ob.py set
 
 
 .PHONY: lint
 .PHONY: lint

+ 1 - 1
applications/bt/bt_cli.c

@@ -148,7 +148,7 @@ static void bt_cli_command_packet_rx(Cli* cli, string_t args, void* context) {
 static void bt_cli_scan_callback(GapAddress address, void* context) {
 static void bt_cli_scan_callback(GapAddress address, void* context) {
     furi_assert(context);
     furi_assert(context);
     osMessageQueueId_t queue = context;
     osMessageQueueId_t queue = context;
-    osMessageQueuePut(queue, &address, NULL, 250);
+    osMessageQueuePut(queue, &address, 0, 250);
 }
 }
 
 
 static void bt_cli_command_scan(Cli* cli, string_t args, void* context) {
 static void bt_cli_command_scan(Cli* cli, string_t args, void* context) {

+ 7 - 0
applications/bt/bt_service/bt.c

@@ -319,6 +319,13 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
 int32_t bt_srv() {
 int32_t bt_srv() {
     Bt* bt = bt_alloc();
     Bt* bt = bt_alloc();
 
 
+    if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) {
+        FURI_LOG_W(TAG, "Skipped BT init: device in special startup mode");
+        ble_glue_wait_for_c2_start(FURI_HAL_BT_C2_START_TIMEOUT);
+        furi_record_create("bt", bt);
+        return 0;
+    }
+
     // Read keys
     // Read keys
     if(!bt_keys_storage_load(bt)) {
     if(!bt_keys_storage_load(bt)) {
         FURI_LOG_W(TAG, "Failed to load bonding keys");
         FURI_LOG_W(TAG, "Failed to load bonding keys");

+ 3 - 10
applications/updater/scenes/updater_scene_main.c

@@ -36,9 +36,9 @@ void updater_scene_main_on_enter(void* context) {
     * will be missing from UI, however, /ext will be fully operational. So, until it's fixed, this
     * will be missing from UI, however, /ext will be fully operational. So, until it's fixed, this
     * should remain commented out. */
     * should remain commented out. */
     // If (somehow) we started after SD card is mounted, initiate update immediately
     // If (somehow) we started after SD card is mounted, initiate update immediately
-    //if(storage_sd_status(updater->storage) == FSE_OK) {
-    //    view_dispatcher_send_custom_event(updater->view_dispatcher, UpdaterCustomEventStartUpdate);
-    //}
+    if(storage_sd_status(updater->storage) == FSE_OK) {
+        view_dispatcher_send_custom_event(updater->view_dispatcher, UpdaterCustomEventStartUpdate);
+    }
 
 
     updater_main_set_view_dispatcher(main_view, updater->view_dispatcher);
     updater_main_set_view_dispatcher(main_view, updater->view_dispatcher);
     view_dispatcher_switch_to_view(updater->view_dispatcher, UpdaterViewMain);
     view_dispatcher_switch_to_view(updater->view_dispatcher, UpdaterViewMain);
@@ -64,13 +64,6 @@ bool updater_scene_main_on_event(void* context, SceneManagerEvent event) {
     } else if(event.type == SceneManagerEventTypeCustom) {
     } else if(event.type == SceneManagerEventTypeCustom) {
         switch(event.event) {
         switch(event.event) {
         case UpdaterCustomEventStartUpdate:
         case UpdaterCustomEventStartUpdate:
-            if(!update_task_is_running(updater->update_task) &&
-               update_task_init(updater->update_task)) {
-                update_task_start(updater->update_task);
-            }
-            consumed = true;
-            break;
-
         case UpdaterCustomEventRetryUpdate:
         case UpdaterCustomEventRetryUpdate:
             if(!update_task_is_running(updater->update_task) &&
             if(!update_task_is_running(updater->update_task) &&
                (update_task_get_state(updater->update_task)->stage != UpdateTaskStageCompleted))
                (update_task_get_state(updater->update_task)->stage != UpdateTaskStageCompleted))

+ 13 - 10
applications/updater/util/update_task.c

@@ -15,13 +15,18 @@ static const char* update_task_stage_descr[] = {
     [UpdateTaskStageValidateDFUImage] = "Checking DFU file",
     [UpdateTaskStageValidateDFUImage] = "Checking DFU file",
     [UpdateTaskStageFlashWrite] = "Writing flash",
     [UpdateTaskStageFlashWrite] = "Writing flash",
     [UpdateTaskStageFlashValidate] = "Validating",
     [UpdateTaskStageFlashValidate] = "Validating",
+    [UpdateTaskStageRadioImageValidate] = "Checking radio image",
+    [UpdateTaskStageRadioErase] = "Removing radio stack",
     [UpdateTaskStageRadioWrite] = "Writing radio stack",
     [UpdateTaskStageRadioWrite] = "Writing radio stack",
-    [UpdateTaskStageRadioCommit] = "Applying radio stack",
+    [UpdateTaskStageRadioInstall] = "Installing radio stack",
+    [UpdateTaskStageRadioBusy] = "Core2 is updating",
+    [UpdateTaskStageOBValidation] = "Validating opt. bytes",
     [UpdateTaskStageLfsBackup] = "Backing up LFS",
     [UpdateTaskStageLfsBackup] = "Backing up LFS",
     [UpdateTaskStageLfsRestore] = "Restoring LFS",
     [UpdateTaskStageLfsRestore] = "Restoring LFS",
     [UpdateTaskStageResourcesUpdate] = "Updating resources",
     [UpdateTaskStageResourcesUpdate] = "Updating resources",
     [UpdateTaskStageCompleted] = "Completed!",
     [UpdateTaskStageCompleted] = "Completed!",
     [UpdateTaskStageError] = "Error",
     [UpdateTaskStageError] = "Error",
+    [UpdateTaskStageOBError] = "OB error, pls report",
 };
 };
 
 
 static void update_task_set_status(UpdateTask* update_task, const char* status) {
 static void update_task_set_status(UpdateTask* update_task, const char* status) {
@@ -37,7 +42,10 @@ static void update_task_set_status(UpdateTask* update_task, const char* status)
 
 
 void update_task_set_progress(UpdateTask* update_task, UpdateTaskStage stage, uint8_t progress) {
 void update_task_set_progress(UpdateTask* update_task, UpdateTaskStage stage, uint8_t progress) {
     if(stage != UpdateTaskStageProgress) {
     if(stage != UpdateTaskStageProgress) {
-        update_task->state.stage = stage;
+        // do not override more specific error states
+        if((update_task->state.stage < UpdateTaskStageError) || (stage < UpdateTaskStageError)) {
+            update_task->state.stage = stage;
+        }
         update_task->state.current_stage_idx++;
         update_task->state.current_stage_idx++;
         update_task_set_status(update_task, NULL);
         update_task_set_status(update_task, NULL);
     }
     }
@@ -53,7 +61,7 @@ void update_task_set_progress(UpdateTask* update_task, UpdateTaskStage stage, ui
             progress,
             progress,
             update_task->state.current_stage_idx,
             update_task->state.current_stage_idx,
             update_task->state.total_stages,
             update_task->state.total_stages,
-            update_task->state.stage == UpdateTaskStageError,
+            update_task->state.stage >= UpdateTaskStageError,
             update_task->status_change_cb_state);
             update_task->status_change_cb_state);
     }
     }
 }
 }
@@ -116,6 +124,7 @@ UpdateTask* update_task_alloc() {
     update_task->storage = furi_record_open("storage");
     update_task->storage = furi_record_open("storage");
     update_task->file = storage_file_alloc(update_task->storage);
     update_task->file = storage_file_alloc(update_task->storage);
     update_task->status_change_cb = NULL;
     update_task->status_change_cb = NULL;
+    string_init(update_task->update_path);
 
 
     FuriThread* thread = update_task->thread = furi_thread_alloc();
     FuriThread* thread = update_task->thread = furi_thread_alloc();
 
 
@@ -152,12 +161,6 @@ void update_task_free(UpdateTask* update_task) {
     free(update_task);
     free(update_task);
 }
 }
 
 
-bool update_task_init(UpdateTask* update_task) {
-    furi_assert(update_task);
-    string_init(update_task->update_path);
-    return true;
-}
-
 bool update_task_parse_manifest(UpdateTask* update_task) {
 bool update_task_parse_manifest(UpdateTask* update_task) {
     furi_assert(update_task);
     furi_assert(update_task);
     update_task_set_progress(update_task, UpdateTaskStageReadManifest, 0);
     update_task_set_progress(update_task, UpdateTaskStageReadManifest, 0);
@@ -224,4 +227,4 @@ UpdateTaskState const* update_task_get_state(UpdateTask* update_task) {
 UpdateManifest const* update_task_get_manifest(UpdateTask* update_task) {
 UpdateManifest const* update_task_get_manifest(UpdateTask* update_task) {
     furi_assert(update_task);
     furi_assert(update_task);
     return update_task->manifest;
     return update_task->manifest;
-}
+}

+ 6 - 3
applications/updater/util/update_task.h

@@ -19,13 +19,18 @@ typedef enum {
     UpdateTaskStageValidateDFUImage,
     UpdateTaskStageValidateDFUImage,
     UpdateTaskStageFlashWrite,
     UpdateTaskStageFlashWrite,
     UpdateTaskStageFlashValidate,
     UpdateTaskStageFlashValidate,
+    UpdateTaskStageRadioImageValidate,
+    UpdateTaskStageRadioErase,
     UpdateTaskStageRadioWrite,
     UpdateTaskStageRadioWrite,
-    UpdateTaskStageRadioCommit,
+    UpdateTaskStageRadioInstall,
+    UpdateTaskStageRadioBusy,
+    UpdateTaskStageOBValidation,
     UpdateTaskStageLfsBackup,
     UpdateTaskStageLfsBackup,
     UpdateTaskStageLfsRestore,
     UpdateTaskStageLfsRestore,
     UpdateTaskStageResourcesUpdate,
     UpdateTaskStageResourcesUpdate,
     UpdateTaskStageCompleted,
     UpdateTaskStageCompleted,
     UpdateTaskStageError,
     UpdateTaskStageError,
+    UpdateTaskStageOBError
 } UpdateTaskStage;
 } UpdateTaskStage;
 
 
 typedef struct {
 typedef struct {
@@ -50,8 +55,6 @@ UpdateTask* update_task_alloc();
 
 
 void update_task_free(UpdateTask* update_task);
 void update_task_free(UpdateTask* update_task);
 
 
-bool update_task_init(UpdateTask* update_task);
-
 void update_task_set_progress_cb(UpdateTask* update_task, updateProgressCb cb, void* state);
 void update_task_set_progress_cb(UpdateTask* update_task, updateProgressCb cb, void* state);
 
 
 bool update_task_start(UpdateTask* update_task);
 bool update_task_start(UpdateTask* update_task);

+ 6 - 86
applications/updater/util/update_task_workers.c → applications/updater/util/update_task_worker_backup.c

@@ -9,99 +9,17 @@
 #include <update_util/lfs_backup.h>
 #include <update_util/lfs_backup.h>
 #include <update_util/update_operation.h>
 #include <update_util/update_operation.h>
 #include <toolbox/tar/tar_archive.h>
 #include <toolbox/tar/tar_archive.h>
+#include <toolbox/crc32_calc.h>
+
+#define TAG "UpdWorkerBackup"
 
 
 #define CHECK_RESULT(x) \
 #define CHECK_RESULT(x) \
     if(!(x)) {          \
     if(!(x)) {          \
         break;          \
         break;          \
     }
     }
 
 
-#define STM_DFU_VENDOR_ID 0x0483
-#define STM_DFU_PRODUCT_ID 0xDF11
-/* Written into DFU file by build pipeline */
-#define FLIPPER_ZERO_DFU_DEVICE_CODE 0xFFFF
-
 #define EXT_PATH "/ext"
 #define EXT_PATH "/ext"
 
 
-static const DfuValidationParams flipper_dfu_params = {
-    .device = FLIPPER_ZERO_DFU_DEVICE_CODE,
-    .product = STM_DFU_PRODUCT_ID,
-    .vendor = STM_DFU_VENDOR_ID,
-};
-
-static void update_task_dfu_progress(const uint8_t progress, void* context) {
-    UpdateTask* update_task = context;
-    update_task_set_progress(update_task, UpdateTaskStageProgress, progress);
-}
-
-static bool page_task_compare_flash(
-    const uint8_t i_page,
-    const uint8_t* update_block,
-    uint16_t update_block_len) {
-    const size_t page_addr = furi_hal_flash_get_base() + furi_hal_flash_get_page_size() * i_page;
-    return (memcmp(update_block, (void*)page_addr, update_block_len) == 0);
-}
-
-/* Verifies a flash operation address for fitting into writable memory
- */
-static bool check_address_boundaries(const size_t address) {
-    const size_t min_allowed_address = furi_hal_flash_get_base();
-    const size_t max_allowed_address = (size_t)furi_hal_flash_get_free_end_address();
-    return ((address >= min_allowed_address) && (address < max_allowed_address));
-}
-
-int32_t update_task_worker_flash_writer(void* context) {
-    furi_assert(context);
-    UpdateTask* update_task = context;
-    bool success = false;
-    DfuUpdateTask page_task = {
-        .address_cb = &check_address_boundaries,
-        .progress_cb = &update_task_dfu_progress,
-        .task_cb = &furi_hal_flash_program_page,
-        .context = update_task,
-    };
-
-    update_task->state.current_stage_idx = 0;
-    update_task->state.total_stages = 4;
-
-    do {
-        CHECK_RESULT(update_task_parse_manifest(update_task));
-
-        if(!string_empty_p(update_task->manifest->firmware_dfu_image)) {
-            update_task_set_progress(update_task, UpdateTaskStageValidateDFUImage, 0);
-            CHECK_RESULT(
-                update_task_open_file(update_task, update_task->manifest->firmware_dfu_image));
-            CHECK_RESULT(
-                dfu_file_validate_crc(update_task->file, &update_task_dfu_progress, update_task));
-
-            const uint8_t valid_targets =
-                dfu_file_validate_headers(update_task->file, &flipper_dfu_params);
-            if(valid_targets == 0) {
-                break;
-            }
-
-            update_task_set_progress(update_task, UpdateTaskStageFlashWrite, 0);
-            CHECK_RESULT(dfu_file_process_targets(&page_task, update_task->file, valid_targets));
-
-            page_task.task_cb = &page_task_compare_flash;
-
-            update_task_set_progress(update_task, UpdateTaskStageFlashValidate, 0);
-            CHECK_RESULT(dfu_file_process_targets(&page_task, update_task->file, valid_targets));
-        }
-
-        update_task_set_progress(update_task, UpdateTaskStageCompleted, 100);
-
-        furi_hal_rtc_set_boot_mode(FuriHalRtcBootModePostUpdate);
-
-        success = true;
-    } while(false);
-
-    if(!success) {
-        update_task_set_progress(update_task, UpdateTaskStageError, update_task->state.progress);
-    }
-
-    return success ? UPDATE_TASK_NOERR : UPDATE_TASK_FAILED;
-}
-
 static bool update_task_pre_update(UpdateTask* update_task) {
 static bool update_task_pre_update(UpdateTask* update_task) {
     bool success = false;
     bool success = false;
     string_t backup_file_path;
     string_t backup_file_path;
@@ -143,7 +61,8 @@ static bool update_task_post_update(UpdateTask* update_task) {
     string_t file_path;
     string_t file_path;
     string_init(file_path);
     string_init(file_path);
 
 
-    update_task->state.total_stages = 2;
+    // status text is too long, too few stages to bother with a counter
+    update_task->state.total_stages = 0;
 
 
     do {
     do {
         CHECK_RESULT(update_task_parse_manifest(update_task));
         CHECK_RESULT(update_task_parse_manifest(update_task));
@@ -184,6 +103,7 @@ static bool update_task_post_update(UpdateTask* update_task) {
             }
             }
             tar_archive_free(archive);
             tar_archive_free(archive);
         }
         }
+        success = true;
     } while(false);
     } while(false);
 
 
     string_clear(file_path);
     string_clear(file_path);

+ 363 - 0
applications/updater/util/update_task_worker_flasher.c

@@ -0,0 +1,363 @@
+#include "update_task.h"
+#include "update_task_i.h"
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <storage/storage.h>
+#include <toolbox/path.h>
+#include <update_util/dfu_file.h>
+#include <update_util/lfs_backup.h>
+#include <update_util/update_operation.h>
+#include <toolbox/tar/tar_archive.h>
+#include <toolbox/crc32_calc.h>
+
+#define TAG "UpdWorkerRAM"
+
+#define CHECK_RESULT(x) \
+    if(!(x)) {          \
+        break;          \
+    }
+
+#define STM_DFU_VENDOR_ID 0x0483
+#define STM_DFU_PRODUCT_ID 0xDF11
+/* Written into DFU file by build pipeline */
+#define FLIPPER_ZERO_DFU_DEVICE_CODE 0xFFFF
+/* Time, in ms, to wait for system restart by C2 before crashing */
+#define C2_MODE_SWITCH_TIMEOUT 10000
+
+static const DfuValidationParams flipper_dfu_params = {
+    .device = FLIPPER_ZERO_DFU_DEVICE_CODE,
+    .product = STM_DFU_PRODUCT_ID,
+    .vendor = STM_DFU_VENDOR_ID,
+};
+
+static void update_task_file_progress(const uint8_t progress, void* context) {
+    UpdateTask* update_task = context;
+    update_task_set_progress(update_task, UpdateTaskStageProgress, progress);
+}
+
+static bool page_task_compare_flash(
+    const uint8_t i_page,
+    const uint8_t* update_block,
+    uint16_t update_block_len) {
+    const size_t page_addr = furi_hal_flash_get_base() + furi_hal_flash_get_page_size() * i_page;
+    return (memcmp(update_block, (void*)page_addr, update_block_len) == 0);
+}
+
+/* Verifies a flash operation address for fitting into writable memory
+ */
+static bool check_address_boundaries(const size_t address) {
+    const size_t min_allowed_address = furi_hal_flash_get_base();
+    const size_t max_allowed_address = (size_t)furi_hal_flash_get_free_end_address();
+    return ((address >= min_allowed_address) && (address < max_allowed_address));
+}
+
+static bool update_task_write_dfu(UpdateTask* update_task) {
+    DfuUpdateTask page_task = {
+        .address_cb = &check_address_boundaries,
+        .progress_cb = &update_task_file_progress,
+        .task_cb = &furi_hal_flash_program_page,
+        .context = update_task,
+    };
+
+    bool success = false;
+    do {
+        update_task_set_progress(update_task, UpdateTaskStageValidateDFUImage, 0);
+        CHECK_RESULT(
+            update_task_open_file(update_task, update_task->manifest->firmware_dfu_image));
+        CHECK_RESULT(
+            dfu_file_validate_crc(update_task->file, &update_task_file_progress, update_task));
+
+        const uint8_t valid_targets =
+            dfu_file_validate_headers(update_task->file, &flipper_dfu_params);
+        if(valid_targets == 0) {
+            break;
+        }
+
+        update_task_set_progress(update_task, UpdateTaskStageFlashWrite, 0);
+        CHECK_RESULT(dfu_file_process_targets(&page_task, update_task->file, valid_targets));
+
+        page_task.task_cb = &page_task_compare_flash;
+
+        update_task_set_progress(update_task, UpdateTaskStageFlashValidate, 0);
+        CHECK_RESULT(dfu_file_process_targets(&page_task, update_task->file, valid_targets));
+        success = true;
+    } while(false);
+
+    return success;
+}
+
+static bool update_task_write_stack_data(UpdateTask* update_task) {
+    furi_check(storage_file_is_open(update_task->file));
+    const size_t FLASH_PAGE_SIZE = furi_hal_flash_get_page_size();
+
+    uint32_t stack_size = storage_file_size(update_task->file);
+    storage_file_seek(update_task->file, 0, true);
+
+    if(!check_address_boundaries(update_task->manifest->radio_address) ||
+       !check_address_boundaries(update_task->manifest->radio_address + stack_size)) {
+        return false;
+    }
+
+    update_task_set_progress(update_task, UpdateTaskStageRadioWrite, 0);
+    uint8_t* fw_block = malloc(FLASH_PAGE_SIZE);
+    uint16_t bytes_read = 0;
+    uint32_t element_offs = 0;
+
+    while(element_offs < stack_size) {
+        uint32_t n_bytes_to_read = FLASH_PAGE_SIZE;
+        if((element_offs + n_bytes_to_read) > stack_size) {
+            n_bytes_to_read = stack_size - element_offs;
+        }
+
+        bytes_read = storage_file_read(update_task->file, fw_block, n_bytes_to_read);
+        if(bytes_read == 0) {
+            break;
+        }
+
+        int16_t i_page =
+            furi_hal_flash_get_page_number(update_task->manifest->radio_address + element_offs);
+        if(i_page < 0) {
+            break;
+        }
+
+        if(!furi_hal_flash_program_page(i_page, fw_block, bytes_read)) {
+            break;
+        }
+
+        element_offs += bytes_read;
+        update_task_set_progress(
+            update_task, UpdateTaskStageProgress, element_offs * 100 / stack_size);
+    }
+
+    free(fw_block);
+    return element_offs == stack_size;
+}
+
+static void update_task_wait_for_restart(UpdateTask* update_task) {
+    update_task_set_progress(update_task, UpdateTaskStageRadioBusy, 10);
+    osDelay(C2_MODE_SWITCH_TIMEOUT);
+    furi_crash("C2 timeout");
+}
+
+static bool update_task_write_stack(UpdateTask* update_task) {
+    bool success = false;
+    do {
+        FURI_LOG_W(TAG, "Writing stack");
+        update_task_set_progress(update_task, UpdateTaskStageRadioImageValidate, 0);
+        CHECK_RESULT(update_task_open_file(update_task, update_task->manifest->radio_image));
+        CHECK_RESULT(
+            crc32_calc_file(update_task->file, &update_task_file_progress, update_task) ==
+            update_task->manifest->radio_crc);
+
+        CHECK_RESULT(update_task_write_stack_data(update_task));
+        update_task_set_progress(update_task, UpdateTaskStageRadioInstall, 0);
+        CHECK_RESULT(
+            ble_glue_fus_stack_install(update_task->manifest->radio_address, 0) !=
+            BleGlueCommandResultError);
+        update_task_set_progress(update_task, UpdateTaskStageRadioInstall, 80);
+        CHECK_RESULT(ble_glue_fus_wait_operation() == BleGlueCommandResultOK);
+        update_task_set_progress(update_task, UpdateTaskStageRadioInstall, 100);
+        /* ...system will restart here. */
+        update_task_wait_for_restart(update_task);
+        success = true;
+    } while(false);
+    return success;
+}
+
+static bool update_task_remove_stack(UpdateTask* update_task) {
+    bool success = false;
+    do {
+        FURI_LOG_W(TAG, "Removing stack");
+        update_task_set_progress(update_task, UpdateTaskStageRadioErase, 30);
+        CHECK_RESULT(ble_glue_fus_stack_delete() != BleGlueCommandResultError);
+        update_task_set_progress(update_task, UpdateTaskStageRadioErase, 80);
+        CHECK_RESULT(ble_glue_fus_wait_operation() == BleGlueCommandResultOK);
+        update_task_set_progress(update_task, UpdateTaskStageRadioErase, 100);
+        /* ...system will restart here. */
+        update_task_wait_for_restart(update_task);
+        success = true;
+    } while(false);
+    return success;
+}
+
+static bool update_task_manage_radiostack(UpdateTask* update_task) {
+    bool success = false;
+    do {
+        CHECK_RESULT(ble_glue_wait_for_c2_start(FURI_HAL_BT_C2_START_TIMEOUT));
+
+        const BleGlueC2Info* c2_state = ble_glue_get_c2_info();
+
+        const UpdateManifestRadioVersion* radio_ver = &update_task->manifest->radio_version;
+        bool stack_version_match = (c2_state->VersionMajor == radio_ver->version.major) &&
+                                   (c2_state->VersionMinor == radio_ver->version.minor) &&
+                                   (c2_state->VersionSub == radio_ver->version.sub) &&
+                                   (c2_state->VersionBranch == radio_ver->version.branch) &&
+                                   (c2_state->VersionReleaseType == radio_ver->version.release);
+        bool stack_missing = (c2_state->VersionMajor == 0) && (c2_state->VersionMinor == 0);
+
+        if(c2_state->mode == BleGlueC2ModeStack) {
+            /* Stack type is not available when we have FUS running. */
+            bool total_stack_match = stack_version_match &&
+                                     (c2_state->StackType == radio_ver->version.type);
+            if(total_stack_match) {
+                /* Nothing to do. */
+                FURI_LOG_W(TAG, "Stack version is up2date");
+                furi_hal_rtc_reset_flag(FuriHalRtcFlagC2Update);
+                success = true;
+                break;
+            } else {
+                /* Version or type mismatch. Let's boot to FUS and start updating. */
+                FURI_LOG_W(TAG, "Restarting to FUS");
+                furi_hal_rtc_set_flag(FuriHalRtcFlagC2Update);
+                CHECK_RESULT(furi_hal_bt_ensure_c2_mode(BleGlueC2ModeFUS));
+                /* ...system will restart here. */
+                update_task_wait_for_restart(update_task);
+            }
+        } else if(c2_state->mode == BleGlueC2ModeFUS) {
+            /* OK, we're in FUS mode. */
+            update_task_set_progress(update_task, UpdateTaskStageRadioBusy, 10);
+            FURI_LOG_W(TAG, "Waiting for FUS to settle");
+            ble_glue_fus_wait_operation();
+            if(stack_version_match) {
+                /* We can't check StackType with FUS, but partial version matches */
+                if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagC2Update)) {
+                    /* This flag was set when full version was checked.
+                     * And something in versions of the stack didn't match.
+                     * So, clear the flag and drop the stack. */
+                    furi_hal_rtc_reset_flag(FuriHalRtcFlagC2Update);
+                    FURI_LOG_W(TAG, "Forcing stack removal (match)");
+                    CHECK_RESULT(update_task_remove_stack(update_task));
+                } else {
+                    /* We might just had the stack installed.
+                     * Let's start it up to check its version */
+                    FURI_LOG_W(TAG, "Starting stack to check full version");
+                    update_task_set_progress(update_task, UpdateTaskStageRadioBusy, 40);
+                    CHECK_RESULT(furi_hal_bt_ensure_c2_mode(BleGlueC2ModeStack));
+                    /* ...system will restart here. */
+                    update_task_wait_for_restart(update_task);
+                }
+            } else {
+                if(stack_missing) {
+                    /* Install stack. */
+                    CHECK_RESULT(update_task_write_stack(update_task));
+                } else {
+                    CHECK_RESULT(update_task_remove_stack(update_task));
+                }
+            }
+        }
+    } while(false);
+
+    return success;
+}
+
+bool update_task_validate_optionbytes(UpdateTask* update_task) {
+    update_task_set_progress(update_task, UpdateTaskStageOBValidation, 0);
+
+    bool match = true;
+    bool ob_dirty = false;
+    const UpdateManifest* manifest = update_task->manifest;
+    const FuriHalFlashRawOptionByteData* device_data = furi_hal_flash_ob_get_raw_ptr();
+    for(size_t idx = 0; idx < FURI_HAL_FLASH_OB_TOTAL_VALUES; ++idx) {
+        update_task_set_progress(
+            update_task, UpdateTaskStageProgress, idx * 100 / FURI_HAL_FLASH_OB_TOTAL_VALUES);
+        const uint32_t ref_value = manifest->ob_reference.obs[idx].values.base;
+        const uint32_t device_ob_value = device_data->obs[idx].values.base;
+        const uint32_t device_ob_value_masked = device_ob_value &
+                                                manifest->ob_compare_mask.obs[idx].values.base;
+        if(ref_value != device_ob_value_masked) {
+            match = false;
+            FURI_LOG_E(
+                TAG,
+                "OB MISMATCH: #%d: real %08X != %08X (exp.), full %08X",
+                idx,
+                device_ob_value_masked,
+                ref_value,
+                device_ob_value);
+
+            /* any bits we are allowed to write?.. */
+            bool can_patch = ((device_ob_value_masked ^ ref_value) &
+                              manifest->ob_write_mask.obs[idx].values.base) != 0;
+
+            if(can_patch) {
+                /* patch & restart loop */
+                const uint32_t patched_value =
+                    /* take all non-writable bits from real value */
+                    (device_ob_value & ~(manifest->ob_write_mask.obs[idx].values.base)) |
+                    /* take all writable bits from reference value */
+                    (manifest->ob_reference.obs[idx].values.base &
+                     manifest->ob_write_mask.obs[idx].values.base);
+
+                FURI_LOG_W(TAG, "Fixing up OB byte #%d to %08X", idx, patched_value);
+                ob_dirty = true;
+
+                bool is_fixed = furi_hal_flash_ob_set_word(idx, patched_value) &&
+                                ((device_data->obs[idx].values.base &
+                                  manifest->ob_compare_mask.obs[idx].values.base) == ref_value);
+
+                if(!is_fixed) {
+                    /* Things are so bad that fixing what we are allowed to still doesn't match
+                     * reference value 
+                     */
+                    FURI_LOG_W(
+                        TAG,
+                        "OB #%d is FUBAR (fixed&masked %08X, not %08X)",
+                        idx,
+                        patched_value,
+                        ref_value);
+                }
+            }
+        } else {
+            FURI_LOG_I(
+                TAG,
+                "OB MATCH: #%d: real %08X == %08X (exp.)",
+                idx,
+                device_ob_value_masked,
+                ref_value);
+        }
+    }
+    if(!match) {
+        update_task_set_progress(update_task, UpdateTaskStageOBError, 95);
+    }
+
+    if(ob_dirty) {
+        FURI_LOG_W(TAG, "OB were changed, applying");
+        furi_hal_flash_ob_apply();
+    }
+    return match;
+}
+
+int32_t update_task_worker_flash_writer(void* context) {
+    furi_assert(context);
+    UpdateTask* update_task = context;
+    bool success = false;
+
+    update_task->state.current_stage_idx = 0;
+    update_task->state.total_stages = 0;
+
+    do {
+        CHECK_RESULT(update_task_parse_manifest(update_task));
+
+        if(!string_empty_p(update_task->manifest->radio_image)) {
+            CHECK_RESULT(update_task_manage_radiostack(update_task));
+        }
+
+        bool check_ob = update_manifest_has_obdata(update_task->manifest);
+        if(check_ob) {
+            update_task->state.total_stages++;
+            CHECK_RESULT(update_task_validate_optionbytes(update_task));
+        }
+
+        if(!string_empty_p(update_task->manifest->firmware_dfu_image)) {
+            update_task->state.total_stages += 4;
+            CHECK_RESULT(update_task_write_dfu(update_task));
+        }
+
+        furi_hal_rtc_set_boot_mode(FuriHalRtcBootModePostUpdate);
+
+        update_task_set_progress(update_task, UpdateTaskStageCompleted, 100);
+        success = true;
+    } while(false);
+
+    return success ? UPDATE_TASK_NOERR : UPDATE_TASK_FAILED;
+}

+ 1 - 0
assets/.gitignore

@@ -1 +1,2 @@
 /headers
 /headers
+/core2_firmware

+ 7 - 0
assets/Makefile

@@ -1,6 +1,7 @@
 PROJECT_ROOT		= $(abspath $(dir $(abspath $(firstword $(MAKEFILE_LIST))))..)
 PROJECT_ROOT		= $(abspath $(dir $(abspath $(firstword $(MAKEFILE_LIST))))..)
 
 
 include				$(PROJECT_ROOT)/assets/assets.mk
 include				$(PROJECT_ROOT)/assets/assets.mk
+include				$(PROJECT_ROOT)/assets/copro.mk
 
 
 .PHONY: all
 .PHONY: all
 all: icons protobuf dolphin manifest
 all: icons protobuf dolphin manifest
@@ -35,7 +36,13 @@ manifest:
 .PHONY: dolphin
 .PHONY: dolphin
 dolphin: $(DOLPHIN_EXTERNAL_OUTPUT_DIR)
 dolphin: $(DOLPHIN_EXTERNAL_OUTPUT_DIR)
 
 
+.PHONY: copro_bundle
+copro_bundle:
+	@mkdir -p $(COPRO_BUNDLE_DIR)
+	@$(ASSETS_COMPILER) copro $(COPRO_CUBE_DIR) $(COPRO_BUNDLE_DIR) $(COPRO_MCU_FAMILY) --cube_ver=$(COPRO_CUBE_VERSION) --stack_type=$(COPRO_STACK_TYPE) --stack_file=$(COPRO_STACK_BIN) --stack_addr=$(COPRO_STACK_ADDR)
+
 clean:
 clean:
 	@echo "\tCLEAN\t"
 	@echo "\tCLEAN\t"
 	@$(RM) $(ASSETS_COMPILED_DIR)/*
 	@$(RM) $(ASSETS_COMPILED_DIR)/*
+	@$(RM) -rf $(COPRO_BUNDLE_DIR)
 	@$(RM) -rf $(DOLPHIN_EXTERNAL_OUTPUT_DIR)
 	@$(RM) -rf $(DOLPHIN_EXTERNAL_OUTPUT_DIR)

+ 12 - 0
assets/copro.mk

@@ -0,0 +1,12 @@
+COPRO_CUBE_VERSION	:= 1.13.3
+COPRO_MCU_FAMILY	:= STM32WB5x
+COPRO_STACK_BIN		:= stm32wb5x_BLE_Stack_light_fw.bin
+#  See __STACK_TYPE_CODES in scripts/flipper/assets/coprobin.py
+COPRO_STACK_TYPE	:= ble_light
+#  Keep 0 for auto, or put a value from release_notes for chosen stack
+COPRO_STACK_ADDR	:= 0
+
+COPRO_BUNDLE_DIR	:= $(ASSETS_DIR)/core2_firmware
+COPRO_CUBE_DIR		:= $(PROJECT_ROOT)/lib/STM32CubeWB
+COPRO_FIRMWARE_DIR	:= $(COPRO_CUBE_DIR)/Projects/STM32WB_Copro_Wireless_Binaries/$(COPRO_MCU_FAMILY)
+COPRO_STACK_BIN_PATH	:= $(COPRO_FIRMWARE_DIR)/$(COPRO_STACK_BIN)

+ 3 - 5
firmware/targets/f7/Src/update.c

@@ -7,7 +7,8 @@
 #include <flipper_format/flipper_format.h>
 #include <flipper_format/flipper_format.h>
 
 
 #include <update_util/update_manifest.h>
 #include <update_util/update_manifest.h>
-#include <lib/toolbox/path.h>
+#include <toolbox/path.h>
+#include <toolbox/crc32_calc.h>
 
 
 static FATFS* pfs = NULL;
 static FATFS* pfs = NULL;
 
 
@@ -27,7 +28,6 @@ static bool flipper_update_init() {
     furi_hal_delay_init();
     furi_hal_delay_init();
 
 
     furi_hal_spi_init();
     furi_hal_spi_init();
-    furi_hal_crc_init(false);
 
 
     MX_FATFS_Init();
     MX_FATFS_Init();
     if(!hal_sd_detect()) {
     if(!hal_sd_detect()) {
@@ -62,17 +62,15 @@ static bool flipper_update_load_stage(const string_t work_dir, UpdateManifest* m
     uint32_t bytes_read = 0;
     uint32_t bytes_read = 0;
     const uint16_t MAX_READ = 0xFFFF;
     const uint16_t MAX_READ = 0xFFFF;
 
 
-    furi_hal_crc_reset();
     uint32_t crc = 0;
     uint32_t crc = 0;
     do {
     do {
         uint16_t size_read = 0;
         uint16_t size_read = 0;
         if(f_read(&file, img + bytes_read, MAX_READ, &size_read) != FR_OK) {
         if(f_read(&file, img + bytes_read, MAX_READ, &size_read) != FR_OK) {
             break;
             break;
         }
         }
-        crc = furi_hal_crc_feed(img + bytes_read, size_read);
+        crc = crc32_calc_buffer(crc, img + bytes_read, size_read);
         bytes_read += size_read;
         bytes_read += size_read;
     } while(bytes_read == MAX_READ);
     } while(bytes_read == MAX_READ);
-    furi_hal_crc_reset();
 
 
     do {
     do {
         if((bytes_read != stat.fsize) || (crc != manifest->staged_loader_crc)) {
         if((bytes_read != stat.fsize) || (crc != manifest->staged_loader_crc)) {

+ 180 - 43
firmware/targets/f7/ble_glue/ble_glue.c

@@ -6,7 +6,9 @@
 #include "shci.h"
 #include "shci.h"
 #include "shci_tl.h"
 #include "shci_tl.h"
 #include "app_debug.h"
 #include "app_debug.h"
+
 #include <furi_hal.h>
 #include <furi_hal.h>
+#include <shci/shci.h>
 
 
 #define TAG "Core2"
 #define TAG "Core2"
 
 
@@ -27,22 +29,13 @@ PLACE_IN_SECTION("MB_MEM2")
 ALIGN(4)
 ALIGN(4)
 static uint8_t ble_glue_ble_spare_event_buff[sizeof(TL_PacketHeader_t) + TL_EVT_HDR_SIZE + 255];
 static uint8_t ble_glue_ble_spare_event_buff[sizeof(TL_PacketHeader_t) + TL_EVT_HDR_SIZE + 255];
 
 
-typedef enum {
-    // Stage 1: core2 startup and FUS
-    BleGlueStatusStartup,
-    BleGlueStatusBroken,
-    BleGlueStatusFusStarted,
-    // Stage 2: radio stack
-    BleGlueStatusRadioStackStarted,
-    BleGlueStatusRadioStackMissing
-} BleGlueStatus;
-
 typedef struct {
 typedef struct {
     osMutexId_t shci_mtx;
     osMutexId_t shci_mtx;
     osSemaphoreId_t shci_sem;
     osSemaphoreId_t shci_sem;
     FuriThread* thread;
     FuriThread* thread;
     BleGlueStatus status;
     BleGlueStatus status;
     BleGlueKeyStorageChangedCallback callback;
     BleGlueKeyStorageChangedCallback callback;
+    BleGlueC2Info c2_info;
     void* context;
     void* context;
 } BleGlue;
 } BleGlue;
 
 
@@ -111,33 +104,96 @@ void ble_glue_init() {
      */
      */
 }
 }
 
 
-bool ble_glue_wait_for_fus_start(WirelessFwInfo_t* info) {
-    bool ret = false;
+const BleGlueC2Info* ble_glue_get_c2_info() {
+    return &ble_glue->c2_info;
+}
 
 
-    size_t countdown = 1000;
-    while(countdown > 0) {
-        if(ble_glue->status == BleGlueStatusFusStarted) {
-            ret = true;
-            break;
-        }
-        countdown--;
-        osDelay(1);
-    }
+BleGlueStatus ble_glue_get_c2_status() {
+    return ble_glue->status;
+}
 
 
-    if(ble_glue->status == BleGlueStatusFusStarted) {
-        SHCI_GetWirelessFwInfo(info);
+static void ble_glue_update_c2_fw_info() {
+    WirelessFwInfo_t wireless_info;
+    SHCI_GetWirelessFwInfo(&wireless_info);
+    BleGlueC2Info* local_info = &ble_glue->c2_info;
+
+    local_info->VersionMajor = wireless_info.VersionMajor;
+    local_info->VersionMinor = wireless_info.VersionMinor;
+    local_info->VersionMajor = wireless_info.VersionMajor;
+    local_info->VersionMinor = wireless_info.VersionMinor;
+    local_info->VersionSub = wireless_info.VersionSub;
+    local_info->VersionBranch = wireless_info.VersionBranch;
+    local_info->VersionReleaseType = wireless_info.VersionReleaseType;
+
+    local_info->MemorySizeSram2B = wireless_info.MemorySizeSram2B;
+    local_info->MemorySizeSram2A = wireless_info.MemorySizeSram2A;
+    local_info->MemorySizeSram1 = wireless_info.MemorySizeSram1;
+    local_info->MemorySizeFlash = wireless_info.MemorySizeFlash;
+
+    local_info->StackType = wireless_info.StackType;
+
+    local_info->FusVersionMajor = wireless_info.FusVersionMajor;
+    local_info->FusVersionMinor = wireless_info.FusVersionMinor;
+    local_info->FusVersionSub = wireless_info.FusVersionSub;
+    local_info->FusMemorySizeSram2B = wireless_info.FusMemorySizeSram2B;
+    local_info->FusMemorySizeSram2A = wireless_info.FusMemorySizeSram2A;
+    local_info->FusMemorySizeFlash = wireless_info.FusMemorySizeFlash;
+}
+
+static void ble_glue_dump_stack_info() {
+    const BleGlueC2Info* c2_info = &ble_glue->c2_info;
+    FURI_LOG_I(
+        TAG,
+        "Core2: FUS: %d.%d.%d, mem %d/%d, flash %d pages",
+        c2_info->FusVersionMajor,
+        c2_info->FusVersionMinor,
+        c2_info->FusVersionSub,
+        c2_info->FusMemorySizeSram2B,
+        c2_info->FusMemorySizeSram2A,
+        c2_info->FusMemorySizeFlash);
+    FURI_LOG_I(
+        TAG,
+        "Core2: Stack: %d.%d.%d, branch %d, reltype %d, stacktype %d, flash %d pages",
+        c2_info->VersionMajor,
+        c2_info->VersionMinor,
+        c2_info->VersionSub,
+        c2_info->VersionBranch,
+        c2_info->VersionReleaseType,
+        c2_info->StackType,
+        c2_info->MemorySizeFlash);
+}
+
+bool ble_glue_wait_for_c2_start(int32_t timeout) {
+    bool started = false;
+
+    do {
+        // TODO: use mutex?
+        started = ble_glue->status == BleGlueStatusC2Started;
+        if(!started) {
+            timeout--;
+            osDelay(1);
+        }
+    } while(!started && (timeout > 0));
+
+    if(started) {
+        FURI_LOG_I(
+            TAG,
+            "C2 boot completed, mode: %s",
+            ble_glue->c2_info.mode == BleGlueC2ModeFUS ? "FUS" : "Stack");
+        ble_glue_update_c2_fw_info();
+        ble_glue_dump_stack_info();
     } else {
     } else {
-        FURI_LOG_E(TAG, "Failed to start FUS");
+        FURI_LOG_E(TAG, "C2 startup failed");
         ble_glue->status = BleGlueStatusBroken;
         ble_glue->status = BleGlueStatusBroken;
     }
     }
 
 
-    return ret;
+    return started;
 }
 }
 
 
 bool ble_glue_start() {
 bool ble_glue_start() {
     furi_assert(ble_glue);
     furi_assert(ble_glue);
 
 
-    if(ble_glue->status != BleGlueStatusFusStarted) {
+    if(ble_glue->status != BleGlueStatusC2Started) {
         return false;
         return false;
     }
     }
 
 
@@ -145,7 +201,7 @@ bool ble_glue_start() {
     furi_hal_power_insomnia_enter();
     furi_hal_power_insomnia_enter();
     if(ble_app_init()) {
     if(ble_app_init()) {
         FURI_LOG_I(TAG, "Radio stack started");
         FURI_LOG_I(TAG, "Radio stack started");
-        ble_glue->status = BleGlueStatusRadioStackStarted;
+        ble_glue->status = BleGlueStatusRadioStackRunning;
         ret = true;
         ret = true;
         if(SHCI_C2_SetFlashActivityControl(FLASH_ACTIVITY_CONTROL_SEM7) == SHCI_Success) {
         if(SHCI_C2_SetFlashActivityControl(FLASH_ACTIVITY_CONTROL_SEM7) == SHCI_Success) {
             FURI_LOG_I(TAG, "Flash activity control switched to SEM7");
             FURI_LOG_I(TAG, "Flash activity control switched to SEM7");
@@ -167,7 +223,7 @@ bool ble_glue_is_alive() {
         return false;
         return false;
     }
     }
 
 
-    return ble_glue->status >= BleGlueStatusFusStarted;
+    return ble_glue->status >= BleGlueStatusC2Started;
 }
 }
 
 
 bool ble_glue_is_radio_stack_ready() {
 bool ble_glue_is_radio_stack_ready() {
@@ -175,26 +231,42 @@ bool ble_glue_is_radio_stack_ready() {
         return false;
         return false;
     }
     }
 
 
-    return ble_glue->status == BleGlueStatusRadioStackStarted;
+    return ble_glue->status == BleGlueStatusRadioStackRunning;
 }
 }
 
 
-bool ble_glue_radio_stack_fw_launch_started() {
-    bool ret = false;
-    // Get FUS status
-    SHCI_FUS_GetState_ErrorCode_t err_code = 0;
-    uint8_t state = SHCI_C2_FUS_GetState(&err_code);
-    if(state == FUS_STATE_VALUE_IDLE) {
-        // When FUS is running we can't read radio stack version correctly
-        // Trying to start radio stack fw, which leads to reset
-        FURI_LOG_W(TAG, "FUS is running. Restart to launch Radio Stack");
+BleGlueCommandResult ble_glue_force_c2_mode(BleGlueC2Mode desired_mode) {
+    furi_check(desired_mode > BleGlueC2ModeUnknown);
+
+    if(desired_mode == ble_glue->c2_info.mode) {
+        return BleGlueCommandResultOK;
+    }
+
+    if((ble_glue->c2_info.mode == BleGlueC2ModeFUS) && (desired_mode == BleGlueC2ModeStack)) {
+        if((ble_glue->c2_info.VersionMajor == 0) && (ble_glue->c2_info.VersionMinor == 0)) {
+            FURI_LOG_W(TAG, "Stack isn't installed!");
+            return BleGlueCommandResultError;
+        }
         SHCI_CmdStatus_t status = SHCI_C2_FUS_StartWs();
         SHCI_CmdStatus_t status = SHCI_C2_FUS_StartWs();
         if(status) {
         if(status) {
             FURI_LOG_E(TAG, "Failed to start Radio Stack with status: %02X", status);
             FURI_LOG_E(TAG, "Failed to start Radio Stack with status: %02X", status);
-        } else {
-            ret = true;
+            return BleGlueCommandResultError;
         }
         }
+        return BleGlueCommandResultRestartPending;
     }
     }
-    return ret;
+    if((ble_glue->c2_info.mode == BleGlueC2ModeStack) && (desired_mode == BleGlueC2ModeFUS)) {
+        SHCI_FUS_GetState_ErrorCode_t error_code = 0;
+        uint8_t fus_state = SHCI_C2_FUS_GetState(&error_code);
+        FURI_LOG_D(TAG, "FUS state: %X, error = %x", fus_state, error_code);
+        if(fus_state == SHCI_FUS_CMD_NOT_SUPPORTED) {
+            // Second call to SHCI_C2_FUS_GetState() restarts whole MCU & boots FUS
+            fus_state = SHCI_C2_FUS_GetState(&error_code);
+            FURI_LOG_D(TAG, "FUS state#2: %X, error = %x", fus_state, error_code);
+            return BleGlueCommandResultRestartPending;
+        }
+        return BleGlueCommandResultOK;
+    }
+
+    return BleGlueCommandResultError;
 }
 }
 
 
 static void ble_glue_sys_status_not_callback(SHCI_TL_CmdStatus_t status) {
 static void ble_glue_sys_status_not_callback(SHCI_TL_CmdStatus_t status) {
@@ -228,8 +300,15 @@ static void ble_glue_sys_user_event_callback(void* pPayload) {
         (TL_AsynchEvt_t*)(((tSHCI_UserEvtRxParam*)pPayload)->pckt->evtserial.evt.payload);
         (TL_AsynchEvt_t*)(((tSHCI_UserEvtRxParam*)pPayload)->pckt->evtserial.evt.payload);
 
 
     if(p_sys_event->subevtcode == SHCI_SUB_EVT_CODE_READY) {
     if(p_sys_event->subevtcode == SHCI_SUB_EVT_CODE_READY) {
-        FURI_LOG_I(TAG, "Fus started");
-        ble_glue->status = BleGlueStatusFusStarted;
+        FURI_LOG_I(TAG, "Core2 started");
+        SHCI_C2_Ready_Evt_t* p_c2_ready_evt = (SHCI_C2_Ready_Evt_t*)p_sys_event->payload;
+        if(p_c2_ready_evt->sysevt_ready_rsp == WIRELESS_FW_RUNNING) {
+            ble_glue->c2_info.mode = BleGlueC2ModeStack;
+        } else if(p_c2_ready_evt->sysevt_ready_rsp == FUS_FW_RUNNING) {
+            ble_glue->c2_info.mode = BleGlueC2ModeFUS;
+        }
+
+        ble_glue->status = BleGlueStatusC2Started;
         furi_hal_power_insomnia_exit();
         furi_hal_power_insomnia_exit();
     } else if(p_sys_event->subevtcode == SHCI_SUB_EVT_ERROR_NOTIF) {
     } else if(p_sys_event->subevtcode == SHCI_SUB_EVT_ERROR_NOTIF) {
         FURI_LOG_E(TAG, "Error during initialization");
         FURI_LOG_E(TAG, "Error during initialization");
@@ -308,3 +387,61 @@ void shci_cmd_resp_wait(uint32_t timeout) {
         osSemaphoreAcquire(ble_glue->shci_sem, osWaitForever);
         osSemaphoreAcquire(ble_glue->shci_sem, osWaitForever);
     }
     }
 }
 }
+
+bool ble_glue_reinit_c2() {
+    return SHCI_C2_Reinit() == SHCI_Success;
+}
+
+BleGlueCommandResult ble_glue_fus_stack_delete() {
+    FURI_LOG_I(TAG, "Erasing stack");
+    SHCI_CmdStatus_t erase_stat = SHCI_C2_FUS_FwDelete();
+    FURI_LOG_I(TAG, "Cmd res = %x", erase_stat);
+    if(erase_stat == SHCI_Success) {
+        return BleGlueCommandResultOperationOngoing;
+    }
+    ble_glue_fus_get_status();
+    return BleGlueCommandResultError;
+}
+
+BleGlueCommandResult ble_glue_fus_stack_install(uint32_t src_addr, uint32_t dst_addr) {
+    FURI_LOG_I(TAG, "Installing stack");
+    SHCI_CmdStatus_t write_stat = SHCI_C2_FUS_FwUpgrade(src_addr, dst_addr);
+    FURI_LOG_I(TAG, "Cmd res = %x", write_stat);
+    if(write_stat == SHCI_Success) {
+        return BleGlueCommandResultOperationOngoing;
+    }
+    ble_glue_fus_get_status();
+    return BleGlueCommandResultError;
+}
+
+BleGlueCommandResult ble_glue_fus_get_status() {
+    furi_check(ble_glue->c2_info.mode == BleGlueC2ModeFUS);
+    SHCI_FUS_GetState_ErrorCode_t error_code = 0;
+    uint8_t fus_state = SHCI_C2_FUS_GetState(&error_code);
+    FURI_LOG_I(TAG, "FUS state: %x, error: %x", fus_state, error_code);
+    if((error_code != 0) || (fus_state == FUS_STATE_VALUE_ERROR)) {
+        return BleGlueCommandResultError;
+    } else if(
+        (fus_state >= FUS_STATE_VALUE_FW_UPGRD_ONGOING) &&
+        (fus_state <= FUS_STATE_VALUE_SERVICE_ONGOING_END)) {
+        return BleGlueCommandResultOperationOngoing;
+    }
+    return BleGlueCommandResultOK;
+}
+
+BleGlueCommandResult ble_glue_fus_wait_operation() {
+    furi_check(ble_glue->c2_info.mode == BleGlueC2ModeFUS);
+    bool wip;
+    do {
+        BleGlueCommandResult fus_status = ble_glue_fus_get_status();
+        if(fus_status == BleGlueCommandResultError) {
+            return BleGlueCommandResultError;
+        }
+        wip = fus_status == BleGlueCommandResultOperationOngoing;
+        if(wip) {
+            osDelay(20);
+        }
+    } while(wip);
+
+    return BleGlueCommandResultOK;
+}

+ 71 - 4
firmware/targets/f7/ble_glue/ble_glue.h

@@ -2,12 +2,53 @@
 
 
 #include <stdint.h>
 #include <stdint.h>
 #include <stdbool.h>
 #include <stdbool.h>
-#include <shci/shci.h>
 
 
 #ifdef __cplusplus
 #ifdef __cplusplus
 extern "C" {
 extern "C" {
 #endif
 #endif
 
 
+typedef enum {
+    BleGlueC2ModeUnknown = 0,
+    BleGlueC2ModeFUS,
+    BleGlueC2ModeStack,
+} BleGlueC2Mode;
+
+typedef struct {
+    BleGlueC2Mode mode;
+    /**
+     * Wireless Info
+     */
+    uint8_t VersionMajor;
+    uint8_t VersionMinor;
+    uint8_t VersionSub;
+    uint8_t VersionBranch;
+    uint8_t VersionReleaseType;
+    uint8_t MemorySizeSram2B; /*< Multiple of 1K */
+    uint8_t MemorySizeSram2A; /*< Multiple of 1K */
+    uint8_t MemorySizeSram1; /*< Multiple of 1K */
+    uint8_t MemorySizeFlash; /*< Multiple of 4K */
+    uint8_t StackType;
+    /**
+     * Fus Info
+     */
+    uint8_t FusVersionMajor;
+    uint8_t FusVersionMinor;
+    uint8_t FusVersionSub;
+    uint8_t FusMemorySizeSram2B; /*< Multiple of 1K */
+    uint8_t FusMemorySizeSram2A; /*< Multiple of 1K */
+    uint8_t FusMemorySizeFlash; /*< Multiple of 4K */
+} BleGlueC2Info;
+
+typedef enum {
+    // Stage 1: core2 startup and FUS
+    BleGlueStatusStartup,
+    BleGlueStatusBroken,
+    BleGlueStatusC2Started,
+    // Stage 2: radio stack
+    BleGlueStatusRadioStackRunning,
+    BleGlueStatusRadioStackMissing
+} BleGlueStatus;
+
 typedef void (
 typedef void (
     *BleGlueKeyStorageChangedCallback)(uint8_t* change_addr_start, uint16_t size, void* context);
     *BleGlueKeyStorageChangedCallback)(uint8_t* change_addr_start, uint16_t size, void* context);
 
 
@@ -26,7 +67,15 @@ bool ble_glue_start();
  */
  */
 bool ble_glue_is_alive();
 bool ble_glue_is_alive();
 
 
-bool ble_glue_wait_for_fus_start(WirelessFwInfo_t* info);
+/** Waits for C2 to reports its mode to callback
+ *
+ * @return     true if it reported before reaching timeout
+ */
+bool ble_glue_wait_for_c2_start(int32_t timeout);
+
+BleGlueStatus ble_glue_get_c2_status();
+
+const BleGlueC2Info* ble_glue_get_c2_info();
 
 
 /** Is core2 radio stack present and ready
 /** Is core2 radio stack present and ready
  *
  *
@@ -46,12 +95,30 @@ void ble_glue_set_key_storage_changed_callback(
 /** Stop SHCI thread */
 /** Stop SHCI thread */
 void ble_glue_thread_stop();
 void ble_glue_thread_stop();
 
 
+bool ble_glue_reinit_c2();
+
+typedef enum {
+    BleGlueCommandResultUnknown,
+    BleGlueCommandResultOK,
+    BleGlueCommandResultError,
+    BleGlueCommandResultRestartPending,
+    BleGlueCommandResultOperationOngoing,
+} BleGlueCommandResult;
+
 /** Restart MCU to launch radio stack firmware if necessary
 /** Restart MCU to launch radio stack firmware if necessary
  *
  *
  * @return      true on radio stack start command
  * @return      true on radio stack start command
  */
  */
-bool ble_glue_radio_stack_fw_launch_started();
+BleGlueCommandResult ble_glue_force_c2_mode(BleGlueC2Mode mode);
+
+BleGlueCommandResult ble_glue_fus_stack_delete();
+
+BleGlueCommandResult ble_glue_fus_stack_install(uint32_t src_addr, uint32_t dst_addr);
+
+BleGlueCommandResult ble_glue_fus_get_status();
+
+BleGlueCommandResult ble_glue_fus_wait_operation();
 
 
 #ifdef __cplusplus
 #ifdef __cplusplus
 }
 }
-#endif
+#endif

+ 0 - 1
firmware/targets/f7/furi_hal/furi_hal.c

@@ -55,7 +55,6 @@ void furi_hal_init() {
     FURI_LOG_I(TAG, "Speaker OK");
     FURI_LOG_I(TAG, "Speaker OK");
 
 
     furi_hal_crypto_init();
     furi_hal_crypto_init();
-    furi_hal_crc_init(true);
 
 
     // USB
     // USB
 #ifndef FURI_RAM_EXEC
 #ifndef FURI_RAM_EXEC

+ 31 - 12
firmware/targets/f7/furi_hal/furi_hal_bt.c

@@ -16,6 +16,9 @@
 #define FURI_HAL_BT_DEFAULT_MAC_ADDR \
 #define FURI_HAL_BT_DEFAULT_MAC_ADDR \
     { 0x6c, 0x7a, 0xd8, 0xac, 0x57, 0x72 }
     { 0x6c, 0x7a, 0xd8, 0xac, 0x57, 0x72 }
 
 
+/* Time, in ms, to wait for mode transition before crashing */
+#define C2_MODE_SWITCH_TIMEOUT 10000
+
 osMutexId_t furi_hal_bt_core2_mtx = NULL;
 osMutexId_t furi_hal_bt_core2_mtx = NULL;
 static FuriHalBtStack furi_hal_bt_stack = FuriHalBtStackUnknown;
 static FuriHalBtStack furi_hal_bt_stack = FuriHalBtStackUnknown;
 
 
@@ -99,7 +102,7 @@ void furi_hal_bt_unlock_core2() {
     furi_check(osMutexRelease(furi_hal_bt_core2_mtx) == osOK);
     furi_check(osMutexRelease(furi_hal_bt_core2_mtx) == osOK);
 }
 }
 
 
-static bool furi_hal_bt_radio_stack_is_supported(WirelessFwInfo_t* info) {
+static bool furi_hal_bt_radio_stack_is_supported(const BleGlueC2Info* info) {
     bool supported = false;
     bool supported = false;
     if(info->StackType == INFO_STACK_TYPE_BLE_HCI) {
     if(info->StackType == INFO_STACK_TYPE_BLE_HCI) {
         furi_hal_bt_stack = FuriHalBtStackHciLayer;
         furi_hal_bt_stack = FuriHalBtStackHciLayer;
@@ -128,21 +131,22 @@ bool furi_hal_bt_start_radio_stack() {
     }
     }
 
 
     do {
     do {
-        // Wait until FUS is started or timeout
-        WirelessFwInfo_t info = {};
-        if(!ble_glue_wait_for_fus_start(&info)) {
-            FURI_LOG_E(TAG, "FUS start failed");
+        // Wait until C2 is started or timeout
+        if(!ble_glue_wait_for_c2_start(FURI_HAL_BT_C2_START_TIMEOUT)) {
+            FURI_LOG_E(TAG, "Core2 start failed");
             LL_C2_PWR_SetPowerMode(LL_PWR_MODE_SHUTDOWN);
             LL_C2_PWR_SetPowerMode(LL_PWR_MODE_SHUTDOWN);
             ble_glue_thread_stop();
             ble_glue_thread_stop();
             break;
             break;
         }
         }
-        // If FUS is running, start radio stack fw
-        if(ble_glue_radio_stack_fw_launch_started()) {
-            // If FUS is running do nothing and wait for system reset
-            furi_crash("Waiting for FUS to launch radio stack firmware");
+
+        // If C2 is running, start radio stack fw
+        if(!furi_hal_bt_ensure_c2_mode(BleGlueC2ModeStack)) {
+            break;
         }
         }
-        // Check weather we support radio stack
-        if(!furi_hal_bt_radio_stack_is_supported(&info)) {
+
+        // Check whether we support radio stack
+        const BleGlueC2Info* c2_info = ble_glue_get_c2_info();
+        if(!furi_hal_bt_radio_stack_is_supported(c2_info)) {
             FURI_LOG_E(TAG, "Unsupported radio stack");
             FURI_LOG_E(TAG, "Unsupported radio stack");
             // Don't stop SHCI for crypto enclave support
             // Don't stop SHCI for crypto enclave support
             break;
             break;
@@ -232,7 +236,7 @@ bool furi_hal_bt_change_app(FuriHalBtProfile profile, GapEventCallback event_cb,
     ble_app_thread_stop();
     ble_app_thread_stop();
     gap_thread_stop();
     gap_thread_stop();
     FURI_LOG_I(TAG, "Reset SHCI");
     FURI_LOG_I(TAG, "Reset SHCI");
-    SHCI_C2_Reinit();
+    ble_glue_reinit_c2();
     osDelay(100);
     osDelay(100);
     ble_glue_thread_stop();
     ble_glue_thread_stop();
     FURI_LOG_I(TAG, "Start BT initialization");
     FURI_LOG_I(TAG, "Start BT initialization");
@@ -404,3 +408,18 @@ void furi_hal_bt_stop_scan() {
         gap_stop_scan();
         gap_stop_scan();
     }
     }
 }
 }
+
+bool furi_hal_bt_ensure_c2_mode(BleGlueC2Mode mode) {
+    BleGlueCommandResult fw_start_res = ble_glue_force_c2_mode(mode);
+    if(fw_start_res == BleGlueCommandResultOK) {
+        return true;
+    } else if(fw_start_res == BleGlueCommandResultRestartPending) {
+        // Do nothing and wait for system reset
+        osDelay(C2_MODE_SWITCH_TIMEOUT);
+        furi_crash("Waiting for FUS->radio stack transition");
+        return true;
+    }
+
+    FURI_LOG_E(TAG, "Failed to switch C2 mode: %d", fw_start_res);
+    return false;
+}

+ 6 - 1
firmware/targets/f7/furi_hal/furi_hal_crc.c

@@ -34,6 +34,7 @@ void furi_hal_crc_init(bool synchronize) {
 void furi_hal_crc_reset() {
 void furi_hal_crc_reset() {
     furi_check(hal_crc_control.state == CRC_State_Ready);
     furi_check(hal_crc_control.state == CRC_State_Ready);
     if(hal_crc_control.mtx) {
     if(hal_crc_control.mtx) {
+        furi_check(osMutexGetOwner(hal_crc_control.mtx) == osThreadGetId());
         osMutexRelease(hal_crc_control.mtx);
         osMutexRelease(hal_crc_control.mtx);
     }
     }
     LL_CRC_ResetCRCCalculationUnit(CRC);
     LL_CRC_ResetCRCCalculationUnit(CRC);
@@ -84,5 +85,9 @@ uint32_t furi_hal_crc_feed(void* data, uint16_t length) {
 
 
 bool furi_hal_crc_acquire(uint32_t timeout) {
 bool furi_hal_crc_acquire(uint32_t timeout) {
     furi_assert(hal_crc_control.mtx);
     furi_assert(hal_crc_control.mtx);
-    return osMutexAcquire(hal_crc_control.mtx, timeout) == osOK;
+    if(osMutexAcquire(hal_crc_control.mtx, timeout) == osOK) {
+        LL_CRC_ResetCRCCalculationUnit(CRC);
+        return true;
+    }
+    return false;
 }
 }

+ 130 - 4
firmware/targets/f7/furi_hal/furi_hal_flash.c

@@ -6,7 +6,8 @@
 
 
 #include <stm32wbxx.h>
 #include <stm32wbxx.h>
 
 
-#define FURI_HAL_TAG "FuriHalFlash"
+#define TAG "FuriHalFlash"
+
 #define FURI_HAL_CRITICAL_MSG "Critical flash operation fail"
 #define FURI_HAL_CRITICAL_MSG "Critical flash operation fail"
 #define FURI_HAL_FLASH_READ_BLOCK 8
 #define FURI_HAL_FLASH_READ_BLOCK 8
 #define FURI_HAL_FLASH_WRITE_BLOCK 8
 #define FURI_HAL_FLASH_WRITE_BLOCK 8
@@ -14,13 +15,17 @@
 #define FURI_HAL_FLASH_CYCLES_COUNT 10000
 #define FURI_HAL_FLASH_CYCLES_COUNT 10000
 #define FURI_HAL_FLASH_TIMEOUT 1000
 #define FURI_HAL_FLASH_TIMEOUT 1000
 #define FURI_HAL_FLASH_KEY1 0x45670123U
 #define FURI_HAL_FLASH_KEY1 0x45670123U
-
 #define FURI_HAL_FLASH_KEY2 0xCDEF89ABU
 #define FURI_HAL_FLASH_KEY2 0xCDEF89ABU
 #define FURI_HAL_FLASH_TOTAL_PAGES 256
 #define FURI_HAL_FLASH_TOTAL_PAGES 256
 #define FURI_HAL_FLASH_SR_ERRORS                                                               \
 #define FURI_HAL_FLASH_SR_ERRORS                                                               \
     (FLASH_SR_OPERR | FLASH_SR_PROGERR | FLASH_SR_WRPERR | FLASH_SR_PGAERR | FLASH_SR_SIZERR | \
     (FLASH_SR_OPERR | FLASH_SR_PROGERR | FLASH_SR_WRPERR | FLASH_SR_PGAERR | FLASH_SR_SIZERR | \
      FLASH_SR_PGSERR | FLASH_SR_MISERR | FLASH_SR_FASTERR | FLASH_SR_RDERR | FLASH_SR_OPTVERR)
      FLASH_SR_PGSERR | FLASH_SR_MISERR | FLASH_SR_FASTERR | FLASH_SR_RDERR | FLASH_SR_OPTVERR)
 
 
+//#define FURI_HAL_FLASH_OB_START_ADDRESS 0x1FFF8000
+#define FURI_HAL_FLASH_OPT_KEY1 0x08192A3B
+#define FURI_HAL_FLASH_OPT_KEY2 0x4C5D6E7F
+#define FURI_HAL_FLASH_OB_TOTAL_WORDS (0x80 / (sizeof(uint32_t) * 2))
+
 #define IS_ADDR_ALIGNED_64BITS(__VALUE__) (((__VALUE__)&0x7U) == (0x00UL))
 #define IS_ADDR_ALIGNED_64BITS(__VALUE__) (((__VALUE__)&0x7U) == (0x00UL))
 #define IS_FLASH_PROGRAM_ADDRESS(__VALUE__)                                             \
 #define IS_FLASH_PROGRAM_ADDRESS(__VALUE__)                                             \
     (((__VALUE__) >= FLASH_BASE) && ((__VALUE__) <= (FLASH_BASE + FLASH_SIZE - 8UL)) && \
     (((__VALUE__) >= FLASH_BASE) && ((__VALUE__) <= (FLASH_BASE + FLASH_SIZE - 8UL)) && \
@@ -88,7 +93,7 @@ static void furi_hal_flash_unlock() {
     WRITE_REG(FLASH->KEYR, FURI_HAL_FLASH_KEY1);
     WRITE_REG(FLASH->KEYR, FURI_HAL_FLASH_KEY1);
     WRITE_REG(FLASH->KEYR, FURI_HAL_FLASH_KEY2);
     WRITE_REG(FLASH->KEYR, FURI_HAL_FLASH_KEY2);
 
 
-    /* verify Flash is unlock */
+    /* verify Flash is unlocked */
     furi_check(READ_BIT(FLASH->CR, FLASH_CR_LOCK) == 0U);
     furi_check(READ_BIT(FLASH->CR, FLASH_CR_LOCK) == 0U);
 }
 }
 
 
@@ -386,4 +391,125 @@ int16_t furi_hal_flash_get_page_number(size_t address) {
     }
     }
 
 
     return (address - flash_base) / FURI_HAL_FLASH_PAGE_SIZE;
     return (address - flash_base) / FURI_HAL_FLASH_PAGE_SIZE;
-}
+}
+
+uint32_t furi_hal_flash_ob_get_word(size_t word_idx, bool complementary) {
+    furi_check(word_idx <= FURI_HAL_FLASH_OB_TOTAL_WORDS);
+    const uint32_t* ob_data = (const uint32_t*)(OPTION_BYTE_BASE);
+    size_t raw_word_idx = word_idx * 2;
+    if(complementary) {
+        raw_word_idx += 1;
+    }
+    return ob_data[raw_word_idx];
+}
+
+void furi_hal_flash_ob_unlock() {
+    furi_check(READ_BIT(FLASH->CR, FLASH_CR_OPTLOCK) != 0U);
+    furi_hal_flash_begin(true);
+    WRITE_REG(FLASH->OPTKEYR, FURI_HAL_FLASH_OPT_KEY1);
+    __ISB();
+    WRITE_REG(FLASH->OPTKEYR, FURI_HAL_FLASH_OPT_KEY2);
+    /* verify OB area is unlocked */
+    furi_check(READ_BIT(FLASH->CR, FLASH_CR_OPTLOCK) == 0U);
+}
+
+void furi_hal_flash_ob_lock() {
+    furi_check(READ_BIT(FLASH->CR, FLASH_CR_OPTLOCK) == 0U);
+    SET_BIT(FLASH->CR, FLASH_CR_OPTLOCK);
+    furi_hal_flash_end(true);
+    furi_check(READ_BIT(FLASH->CR, FLASH_CR_OPTLOCK) != 0U);
+}
+
+typedef enum {
+    FuriHalFlashObInvalid,
+    FuriHalFlashObRegisterUserRead,
+    FuriHalFlashObRegisterPCROP1AStart,
+    FuriHalFlashObRegisterPCROP1AEnd,
+    FuriHalFlashObRegisterWRPA,
+    FuriHalFlashObRegisterWRPB,
+    FuriHalFlashObRegisterPCROP1BStart,
+    FuriHalFlashObRegisterPCROP1BEnd,
+    FuriHalFlashObRegisterIPCCMail,
+    FuriHalFlashObRegisterSecureFlash,
+    FuriHalFlashObRegisterC2Opts,
+} FuriHalFlashObRegister;
+
+typedef struct {
+    FuriHalFlashObRegister ob_reg;
+    uint32_t* ob_register_address;
+} FuriHalFlashObMapping;
+
+#define OB_REG_DEF(INDEX, REG) \
+    { .ob_reg = INDEX, .ob_register_address = (uint32_t*)(REG) }
+
+static const FuriHalFlashObMapping furi_hal_flash_ob_reg_map[FURI_HAL_FLASH_OB_TOTAL_WORDS] = {
+    OB_REG_DEF(FuriHalFlashObRegisterUserRead, (&FLASH->OPTR)),
+    OB_REG_DEF(FuriHalFlashObRegisterPCROP1AStart, (&FLASH->PCROP1ASR)),
+    OB_REG_DEF(FuriHalFlashObRegisterPCROP1AEnd, (&FLASH->PCROP1AER)),
+    OB_REG_DEF(FuriHalFlashObRegisterWRPA, (&FLASH->WRP1AR)),
+    OB_REG_DEF(FuriHalFlashObRegisterWRPB, (&FLASH->WRP1BR)),
+    OB_REG_DEF(FuriHalFlashObRegisterPCROP1BStart, (&FLASH->PCROP1BSR)),
+    OB_REG_DEF(FuriHalFlashObRegisterPCROP1BEnd, (&FLASH->PCROP1BER)),
+
+    OB_REG_DEF(FuriHalFlashObInvalid, (NULL)),
+    OB_REG_DEF(FuriHalFlashObInvalid, (NULL)),
+    OB_REG_DEF(FuriHalFlashObInvalid, (NULL)),
+    OB_REG_DEF(FuriHalFlashObInvalid, (NULL)),
+    OB_REG_DEF(FuriHalFlashObInvalid, (NULL)),
+    OB_REG_DEF(FuriHalFlashObInvalid, (NULL)),
+
+    OB_REG_DEF(FuriHalFlashObRegisterIPCCMail, (NULL)),
+    OB_REG_DEF(FuriHalFlashObRegisterSecureFlash, (NULL)),
+    OB_REG_DEF(FuriHalFlashObRegisterC2Opts, (NULL)),
+};
+
+void furi_hal_flash_ob_apply() {
+    furi_hal_flash_ob_unlock();
+    /* OBL_LAUNCH: When set to 1, this bit forces the option byte reloading. 
+     * It cannot be written if OPTLOCK is set */
+    SET_BIT(FLASH->CR, FLASH_CR_OBL_LAUNCH);
+    furi_check(furi_hal_flash_wait_last_operation(FURI_HAL_FLASH_TIMEOUT));
+    furi_hal_flash_ob_lock();
+}
+
+bool furi_hal_flash_ob_set_word(size_t word_idx, const uint32_t value) {
+    furi_check(word_idx < FURI_HAL_FLASH_OB_TOTAL_WORDS);
+
+    const FuriHalFlashObMapping* reg_def = &furi_hal_flash_ob_reg_map[word_idx];
+    if(reg_def->ob_register_address == NULL) {
+        FURI_LOG_E(TAG, "Attempt to set RO OB word %d", word_idx);
+        return false;
+    }
+
+    FURI_LOG_W(
+        TAG,
+        "Setting OB reg %d for word %d (addr 0x%08X) to 0x%08X",
+        reg_def->ob_reg,
+        word_idx,
+        reg_def->ob_register_address,
+        value);
+
+    /* 1. Clear OPTLOCK option lock bit with the clearing sequence */
+    furi_hal_flash_ob_unlock();
+
+    /* 2. Write the desired options value in the options registers */
+    *reg_def->ob_register_address = value;
+
+    /* 3. Check that no Flash memory operation is on going by checking the BSY && PESD */
+    furi_check(furi_hal_flash_wait_last_operation(FURI_HAL_FLASH_TIMEOUT));
+    while(LL_FLASH_IsActiveFlag_OperationSuspended()) {
+        osThreadYield();
+    };
+
+    /* 4. Set the Options start bit OPTSTRT */
+    SET_BIT(FLASH->CR, FLASH_CR_OPTSTRT);
+
+    /* 5. Wait for the BSY bit to be cleared. */
+    furi_check(furi_hal_flash_wait_last_operation(FURI_HAL_FLASH_TIMEOUT));
+    furi_hal_flash_ob_lock();
+    return true;
+}
+
+const FuriHalFlashRawOptionByteData* furi_hal_flash_ob_get_raw_ptr() {
+    return (const FuriHalFlashRawOptionByteData*)OPTION_BYTE_BASE;
+}

+ 45 - 4
firmware/targets/f7/furi_hal/furi_hal_flash.h

@@ -4,6 +4,25 @@
 #include <stdint.h>
 #include <stdint.h>
 #include <stddef.h>
 #include <stddef.h>
 
 
+#define FURI_HAL_FLASH_OB_RAW_SIZE_BYTES 0x80
+#define FURI_HAL_FLASH_OB_SIZE_WORDS (FURI_HAL_FLASH_OB_RAW_SIZE_BYTES / sizeof(uint32_t))
+#define FURI_HAL_FLASH_OB_TOTAL_VALUES (FURI_HAL_FLASH_OB_SIZE_WORDS / 2)
+
+typedef union {
+    uint8_t bytes[FURI_HAL_FLASH_OB_RAW_SIZE_BYTES];
+    union {
+        struct {
+            uint32_t base;
+            uint32_t complementary_value;
+        } values;
+        uint64_t dword;
+    } obs[FURI_HAL_FLASH_OB_TOTAL_VALUES];
+} FuriHalFlashRawOptionByteData;
+
+_Static_assert(
+    sizeof(FuriHalFlashRawOptionByteData) == FURI_HAL_FLASH_OB_RAW_SIZE_BYTES,
+    "UpdateManifestOptionByteData size error");
+
 /** Init flash, applying necessary workarounds
 /** Init flash, applying necessary workarounds
  */
  */
 void furi_hal_flash_init();
 void furi_hal_flash_init();
@@ -64,7 +83,7 @@ size_t furi_hal_flash_get_free_page_count();
 
 
 /** Erase Flash
 /** Erase Flash
  *
  *
- * @warning    locking operation with critical section, stales execution
+ * @warning    locking operation with critical section, stalls execution
  *
  *
  * @param      page  The page to erase
  * @param      page  The page to erase
  *
  *
@@ -74,7 +93,7 @@ bool furi_hal_flash_erase(uint8_t page);
 
 
 /** Write double word (64 bits)
 /** Write double word (64 bits)
  *
  *
- * @warning locking operation with critical section, stales execution
+ * @warning locking operation with critical section, stalls execution
  *
  *
  * @param      address  destination address, must be double word aligned.
  * @param      address  destination address, must be double word aligned.
  * @param      data     data to write
  * @param      data     data to write
@@ -85,7 +104,7 @@ bool furi_hal_flash_write_dword(size_t address, uint64_t data);
 
 
 /** Write aligned page data (up to page size)
 /** Write aligned page data (up to page size)
  *
  *
- * @warning locking operation with critical section, stales execution
+ * @warning locking operation with critical section, stalls execution
  *
  *
  * @param      address  destination address, must be page aligned.
  * @param      address  destination address, must be page aligned.
  * @param      data     data to write
  * @param      data     data to write
@@ -99,5 +118,27 @@ bool furi_hal_flash_program_page(const uint8_t page, const uint8_t* data, uint16
  *
  *
  * @return     page number, -1 for invalid address
  * @return     page number, -1 for invalid address
  */
  */
+int16_t furi_hal_flash_get_page_number(size_t address);
+
+/** Writes OB word, using non-compl. index of register in Flash, OPTION_BYTE_BASE
+ *
+ * @warning locking operation with critical section, stalls execution
+ *
+ * @param      word_idx  OB word number
+ * @param      value    data to write
+ * @return     true if value was written, false for read-only word
+ */
+bool furi_hal_flash_ob_set_word(size_t word_idx, const uint32_t value);
 
 
-int16_t furi_hal_flash_get_page_number(size_t address);
+/** Forces a reload of OB data from flash to registers
+ *
+ * @warning Initializes system restart
+ *
+ */
+void furi_hal_flash_ob_apply();
+
+/** Get raw OB storage data
+ *
+ * @return     pointer to read-only data of OB (raw + complementary values)
+ */
+const FuriHalFlashRawOptionByteData* furi_hal_flash_ob_get_raw_ptr();

+ 19 - 18
firmware/targets/f7/furi_hal/furi_hal_info.c

@@ -66,44 +66,45 @@ void furi_hal_info_get(FuriHalInfoValueCallback out, void* context) {
         out("firmware_target", string_get_cstr(value), false, context);
         out("firmware_target", string_get_cstr(value), false, context);
     }
     }
 
 
-    WirelessFwInfo_t pWirelessInfo;
-    if(furi_hal_bt_is_alive() && SHCI_GetWirelessFwInfo(&pWirelessInfo) == SHCI_Success) {
+    if(furi_hal_bt_is_alive()) {
+        const BleGlueC2Info* ble_c2_info = ble_glue_get_c2_info();
         out("radio_alive", "true", false, context);
         out("radio_alive", "true", false, context);
+        out("radio_mode", ble_c2_info->mode == BleGlueC2ModeFUS ? "FUS" : "Stack", false, context);
 
 
         // FUS Info
         // FUS Info
-        string_printf(value, "%d", pWirelessInfo.FusVersionMajor);
+        string_printf(value, "%d", ble_c2_info->FusVersionMajor);
         out("radio_fus_major", string_get_cstr(value), false, context);
         out("radio_fus_major", string_get_cstr(value), false, context);
-        string_printf(value, "%d", pWirelessInfo.FusVersionMinor);
+        string_printf(value, "%d", ble_c2_info->FusVersionMinor);
         out("radio_fus_minor", string_get_cstr(value), false, context);
         out("radio_fus_minor", string_get_cstr(value), false, context);
-        string_printf(value, "%d", pWirelessInfo.FusVersionSub);
+        string_printf(value, "%d", ble_c2_info->FusVersionSub);
         out("radio_fus_sub", string_get_cstr(value), false, context);
         out("radio_fus_sub", string_get_cstr(value), false, context);
-        string_printf(value, "%dK", pWirelessInfo.FusMemorySizeSram2B);
+        string_printf(value, "%dK", ble_c2_info->FusMemorySizeSram2B);
         out("radio_fus_sram2b", string_get_cstr(value), false, context);
         out("radio_fus_sram2b", string_get_cstr(value), false, context);
-        string_printf(value, "%dK", pWirelessInfo.FusMemorySizeSram2A);
+        string_printf(value, "%dK", ble_c2_info->FusMemorySizeSram2A);
         out("radio_fus_sram2a", string_get_cstr(value), false, context);
         out("radio_fus_sram2a", string_get_cstr(value), false, context);
-        string_printf(value, "%dK", pWirelessInfo.FusMemorySizeFlash * 4);
+        string_printf(value, "%dK", ble_c2_info->FusMemorySizeFlash * 4);
         out("radio_fus_flash", string_get_cstr(value), false, context);
         out("radio_fus_flash", string_get_cstr(value), false, context);
 
 
         // Stack Info
         // Stack Info
-        string_printf(value, "%d", pWirelessInfo.StackType);
+        string_printf(value, "%d", ble_c2_info->StackType);
         out("radio_stack_type", string_get_cstr(value), false, context);
         out("radio_stack_type", string_get_cstr(value), false, context);
-        string_printf(value, "%d", pWirelessInfo.VersionMajor);
+        string_printf(value, "%d", ble_c2_info->VersionMajor);
         out("radio_stack_major", string_get_cstr(value), false, context);
         out("radio_stack_major", string_get_cstr(value), false, context);
-        string_printf(value, "%d", pWirelessInfo.VersionMinor);
+        string_printf(value, "%d", ble_c2_info->VersionMinor);
         out("radio_stack_minor", string_get_cstr(value), false, context);
         out("radio_stack_minor", string_get_cstr(value), false, context);
-        string_printf(value, "%d", pWirelessInfo.VersionSub);
+        string_printf(value, "%d", ble_c2_info->VersionSub);
         out("radio_stack_sub", string_get_cstr(value), false, context);
         out("radio_stack_sub", string_get_cstr(value), false, context);
-        string_printf(value, "%d", pWirelessInfo.VersionBranch);
+        string_printf(value, "%d", ble_c2_info->VersionBranch);
         out("radio_stack_branch", string_get_cstr(value), false, context);
         out("radio_stack_branch", string_get_cstr(value), false, context);
-        string_printf(value, "%d", pWirelessInfo.VersionReleaseType);
+        string_printf(value, "%d", ble_c2_info->VersionReleaseType);
         out("radio_stack_release", string_get_cstr(value), false, context);
         out("radio_stack_release", string_get_cstr(value), false, context);
-        string_printf(value, "%dK", pWirelessInfo.MemorySizeSram2B);
+        string_printf(value, "%dK", ble_c2_info->MemorySizeSram2B);
         out("radio_stack_sram2b", string_get_cstr(value), false, context);
         out("radio_stack_sram2b", string_get_cstr(value), false, context);
-        string_printf(value, "%dK", pWirelessInfo.MemorySizeSram2A);
+        string_printf(value, "%dK", ble_c2_info->MemorySizeSram2A);
         out("radio_stack_sram2a", string_get_cstr(value), false, context);
         out("radio_stack_sram2a", string_get_cstr(value), false, context);
-        string_printf(value, "%dK", pWirelessInfo.MemorySizeSram1);
+        string_printf(value, "%dK", ble_c2_info->MemorySizeSram1);
         out("radio_stack_sram1", string_get_cstr(value), false, context);
         out("radio_stack_sram1", string_get_cstr(value), false, context);
-        string_printf(value, "%dK", pWirelessInfo.MemorySizeFlash * 4);
+        string_printf(value, "%dK", ble_c2_info->MemorySizeFlash * 4);
         out("radio_stack_flash", string_get_cstr(value), false, context);
         out("radio_stack_flash", string_get_cstr(value), false, context);
 
 
         // Mac address
         // Mac address

+ 1 - 1
firmware/targets/f7/furi_hal/furi_hal_vcp.c

@@ -64,7 +64,7 @@ void furi_hal_vcp_init() {
     vcp->rx_stream = xStreamBufferCreate(VCP_RX_BUF_SIZE, 1);
     vcp->rx_stream = xStreamBufferCreate(VCP_RX_BUF_SIZE, 1);
 
 
     if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) {
     if(furi_hal_rtc_get_boot_mode() != FuriHalRtcBootModeNormal) {
-        FURI_LOG_W(TAG, "Skipped worker init: device in special startup mode=");
+        FURI_LOG_W(TAG, "Skipped worker init: device in special startup mode");
         return;
         return;
     }
     }
 
 

+ 0 - 1
firmware/targets/furi_hal_include/furi_hal.h

@@ -39,7 +39,6 @@ template <unsigned int N> struct STOP_EXTERNING_ME {};
 #include "furi_hal_uart.h"
 #include "furi_hal_uart.h"
 #include "furi_hal_info.h"
 #include "furi_hal_info.h"
 #include "furi_hal_random.h"
 #include "furi_hal_random.h"
-#include "furi_hal_crc.h"
 
 
 #ifdef __cplusplus
 #ifdef __cplusplus
 extern "C" {
 extern "C" {

+ 7 - 0
firmware/targets/furi_hal_include/furi_hal_bt.h

@@ -16,6 +16,7 @@
 
 
 #define FURI_HAL_BT_STACK_VERSION_MAJOR (1)
 #define FURI_HAL_BT_STACK_VERSION_MAJOR (1)
 #define FURI_HAL_BT_STACK_VERSION_MINOR (13)
 #define FURI_HAL_BT_STACK_VERSION_MINOR (13)
+#define FURI_HAL_BT_C2_START_TIMEOUT 1000
 
 
 #ifdef __cplusplus
 #ifdef __cplusplus
 extern "C" {
 extern "C" {
@@ -207,6 +208,12 @@ bool furi_hal_bt_start_scan(GapScanCallback callback, void* context);
 /** Stop MAC addresses scan */
 /** Stop MAC addresses scan */
 void furi_hal_bt_stop_scan();
 void furi_hal_bt_stop_scan();
 
 
+/** Check & switch C2 to given mode
+ *
+ * @param[in]  mode  mode to switch into
+ */
+bool furi_hal_bt_ensure_c2_mode(BleGlueC2Mode mode);
+
 #ifdef __cplusplus
 #ifdef __cplusplus
 }
 }
 #endif
 #endif

+ 1 - 0
firmware/targets/furi_hal_include/furi_hal_rtc.h

@@ -28,6 +28,7 @@ typedef enum {
     FuriHalRtcFlagDebug = (1 << 0),
     FuriHalRtcFlagDebug = (1 << 0),
     FuriHalRtcFlagFactoryReset = (1 << 1),
     FuriHalRtcFlagFactoryReset = (1 << 1),
     FuriHalRtcFlagLock = (1 << 2),
     FuriHalRtcFlagLock = (1 << 2),
+    FuriHalRtcFlagC2Update = (1 << 3),
 } FuriHalRtcFlag;
 } FuriHalRtcFlag;
 
 
 typedef enum {
 typedef enum {

+ 1 - 1
lib/STM32CubeWB

@@ -1 +1 @@
-Subproject commit 528461f8276f06783d46461bfb31d77aa8bac419
+Subproject commit a9e29b431f6dac95b6fc860a717834f35b7f78e5

+ 2 - 1
lib/lib.mk

@@ -97,7 +97,8 @@ CPP_SOURCES		+= $(wildcard $(LIB_DIR)/toolbox/*/*.cpp)
 
 
 # USB Stack
 # USB Stack
 CFLAGS			+= -I$(LIB_DIR)/libusb_stm32/inc
 CFLAGS			+= -I$(LIB_DIR)/libusb_stm32/inc
-C_SOURCES		+= $(wildcard $(LIB_DIR)/libusb_stm32/src/*.c)
+C_SOURCES		+= $(LIB_DIR)/libusb_stm32/src/usbd_stm32wb55_devfs.c
+C_SOURCES		+= $(LIB_DIR)/libusb_stm32/src/usbd_core.c
 
 
 # protobuf
 # protobuf
 CFLAGS			+= -I$(LIB_DIR)/nanopb
 CFLAGS			+= -I$(LIB_DIR)/nanopb

+ 38 - 0
lib/toolbox/crc32_calc.c

@@ -0,0 +1,38 @@
+#include "crc32_calc.h"
+#include <littlefs/lfs_util.h>
+
+#define CRC_DATA_BUFFER_MAX_LEN 512
+
+uint32_t crc32_calc_buffer(uint32_t crc, const void* buffer, size_t size) {
+    // TODO: consider removing dependency on LFS
+    return ~lfs_crc(~crc, buffer, size);
+}
+
+uint32_t crc32_calc_file(File* file, const FileCrcProgressCb progress_cb, void* context) {
+    furi_check(storage_file_is_open(file) && storage_file_seek(file, 0, true));
+
+    uint32_t file_crc = 0;
+
+    uint8_t* data_buffer = malloc(CRC_DATA_BUFFER_MAX_LEN);
+    uint16_t data_buffer_valid_len;
+
+    uint32_t file_size = storage_file_size(file);
+
+    /* Feed file contents per sector into CRC calc */
+    for(uint32_t fptr = 0; fptr < file_size;) {
+        data_buffer_valid_len = storage_file_read(file, data_buffer, CRC_DATA_BUFFER_MAX_LEN);
+        if(data_buffer_valid_len == 0) {
+            break;
+        }
+        fptr += data_buffer_valid_len;
+
+        if(progress_cb && (fptr % CRC_DATA_BUFFER_MAX_LEN == 0)) {
+            progress_cb(fptr * 100 / file_size, context);
+        }
+
+        file_crc = crc32_calc_buffer(file_crc, data_buffer, data_buffer_valid_len);
+    }
+    free(data_buffer);
+
+    return file_crc;
+}

+ 18 - 0
lib/toolbox/crc32_calc.h

@@ -0,0 +1,18 @@
+#pragma once
+
+#include <stdint.h>
+#include <storage/storage.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+uint32_t crc32_calc_buffer(uint32_t crc, const void* buffer, size_t size);
+
+typedef void (*FileCrcProgressCb)(const uint8_t progress, void* context);
+
+uint32_t crc32_calc_file(File* file, const FileCrcProgressCb progress_cb, void* context);
+
+#ifdef __cplusplus
+}
+#endif

+ 3 - 31
lib/update_util/dfu_file.c

@@ -1,42 +1,14 @@
 #include "dfu_file.h"
 #include "dfu_file.h"
+
 #include <furi_hal.h>
 #include <furi_hal.h>
+#include <toolbox/crc32_calc.h>
 
 
 #define VALID_WHOLE_FILE_CRC 0xFFFFFFFF
 #define VALID_WHOLE_FILE_CRC 0xFFFFFFFF
 #define DFU_SUFFIX_VERSION 0x011A
 #define DFU_SUFFIX_VERSION 0x011A
-#define DFU_DATA_BUFFER_MAX_LEN 512
 #define DFU_SIGNATURE "DfuSe"
 #define DFU_SIGNATURE "DfuSe"
 
 
 bool dfu_file_validate_crc(File* dfuf, const DfuPageTaskProgressCb progress_cb, void* context) {
 bool dfu_file_validate_crc(File* dfuf, const DfuPageTaskProgressCb progress_cb, void* context) {
-    if(!storage_file_is_open(dfuf) || !storage_file_seek(dfuf, 0, true)) {
-        return false;
-    }
-
-    furi_hal_crc_reset();
-
-    uint32_t file_crc = 0;
-
-    uint8_t* data_buffer = malloc(DFU_DATA_BUFFER_MAX_LEN);
-    uint16_t data_buffer_valid_len;
-
-    uint32_t file_size = storage_file_size(dfuf);
-
-    /* Feed file contents per sector into CRC calc */
-    furi_hal_crc_acquire(osWaitForever);
-    for(uint32_t fptr = 0; fptr < file_size;) {
-        data_buffer_valid_len = storage_file_read(dfuf, data_buffer, DFU_DATA_BUFFER_MAX_LEN);
-        if(data_buffer_valid_len == 0) {
-            break;
-        }
-        fptr += data_buffer_valid_len;
-
-        if((fptr % DFU_DATA_BUFFER_MAX_LEN == 0)) {
-            progress_cb(fptr * 100 / file_size, context);
-        }
-
-        file_crc = furi_hal_crc_feed(data_buffer, data_buffer_valid_len);
-    }
-    furi_hal_crc_reset();
-    free(data_buffer);
+    uint32_t file_crc = crc32_calc_file(dfuf, progress_cb, context);
 
 
     /* Last 4 bytes of DFU file = CRC of previous file contents, inverted
     /* Last 4 bytes of DFU file = CRC of previous file contents, inverted
      * If we calculate whole file CRC32, incl. embedded CRC,
      * If we calculate whole file CRC32, incl. embedded CRC,

+ 59 - 2
lib/update_util/update_manifest.c

@@ -14,6 +14,9 @@
 #define MANIFEST_KEY_RADIO_VERSION "Radio version"
 #define MANIFEST_KEY_RADIO_VERSION "Radio version"
 #define MANIFEST_KEY_RADIO_CRC "Radio CRC"
 #define MANIFEST_KEY_RADIO_CRC "Radio CRC"
 #define MANIFEST_KEY_ASSETS_FILE "Resources"
 #define MANIFEST_KEY_ASSETS_FILE "Resources"
+#define MANIFEST_KEY_OB_REFERENCE "OB reference"
+#define MANIFEST_KEY_OB_MASK "OB mask"
+#define MANIFEST_KEY_OB_WRITE_MASK "OB write mask"
 
 
 UpdateManifest* update_manifest_alloc() {
 UpdateManifest* update_manifest_alloc() {
     UpdateManifest* update_manifest = malloc(sizeof(UpdateManifest));
     UpdateManifest* update_manifest = malloc(sizeof(UpdateManifest));
@@ -23,6 +26,9 @@ UpdateManifest* update_manifest_alloc() {
     string_init(update_manifest->staged_loader_file);
     string_init(update_manifest->staged_loader_file);
     string_init(update_manifest->resource_bundle);
     string_init(update_manifest->resource_bundle);
     update_manifest->target = 0;
     update_manifest->target = 0;
+    memset(update_manifest->ob_reference.bytes, 0, FURI_HAL_FLASH_OB_RAW_SIZE_BYTES);
+    memset(update_manifest->ob_compare_mask.bytes, 0, FURI_HAL_FLASH_OB_RAW_SIZE_BYTES);
+    memset(update_manifest->ob_write_mask.bytes, 0, FURI_HAL_FLASH_OB_RAW_SIZE_BYTES);
     update_manifest->valid = false;
     update_manifest->valid = false;
     return update_manifest;
     return update_manifest;
 }
 }
@@ -75,8 +81,8 @@ static bool
         flipper_format_read_hex(
         flipper_format_read_hex(
             flipper_file,
             flipper_file,
             MANIFEST_KEY_RADIO_VERSION,
             MANIFEST_KEY_RADIO_VERSION,
-            (uint8_t*)&update_manifest->radio_version,
-            sizeof(uint32_t));
+            update_manifest->radio_version.raw,
+            sizeof(UpdateManifestRadioVersion));
         flipper_format_read_hex(
         flipper_format_read_hex(
             flipper_file,
             flipper_file,
             MANIFEST_KEY_RADIO_CRC,
             MANIFEST_KEY_RADIO_CRC,
@@ -85,6 +91,22 @@ static bool
         flipper_format_read_string(
         flipper_format_read_string(
             flipper_file, MANIFEST_KEY_ASSETS_FILE, update_manifest->resource_bundle);
             flipper_file, MANIFEST_KEY_ASSETS_FILE, update_manifest->resource_bundle);
 
 
+        flipper_format_read_hex(
+            flipper_file,
+            MANIFEST_KEY_OB_REFERENCE,
+            update_manifest->ob_reference.bytes,
+            FURI_HAL_FLASH_OB_RAW_SIZE_BYTES);
+        flipper_format_read_hex(
+            flipper_file,
+            MANIFEST_KEY_OB_MASK,
+            update_manifest->ob_compare_mask.bytes,
+            FURI_HAL_FLASH_OB_RAW_SIZE_BYTES);
+        flipper_format_read_hex(
+            flipper_file,
+            MANIFEST_KEY_OB_WRITE_MASK,
+            update_manifest->ob_write_mask.bytes,
+            FURI_HAL_FLASH_OB_RAW_SIZE_BYTES);
+
         update_manifest->valid =
         update_manifest->valid =
             (!string_empty_p(update_manifest->firmware_dfu_image) ||
             (!string_empty_p(update_manifest->firmware_dfu_image) ||
              !string_empty_p(update_manifest->radio_image) ||
              !string_empty_p(update_manifest->radio_image) ||
@@ -94,6 +116,41 @@ static bool
     return update_manifest->valid;
     return update_manifest->valid;
 }
 }
 
 
+// Verifies that mask values are same for adjacent words (value & inverted)
+static bool ob_data_check_mask_valid(const FuriHalFlashRawOptionByteData* mask) {
+    bool mask_valid = true;
+    for(size_t idx = 0; mask_valid && (idx < FURI_HAL_FLASH_OB_TOTAL_VALUES); ++idx) {
+        mask_valid &= mask->obs[idx].values.base == mask->obs[idx].values.complementary_value;
+    }
+    return mask_valid;
+}
+
+// Verifies that all reference values have no unmasked bits
+static bool ob_data_check_masked_values_valid(
+    const FuriHalFlashRawOptionByteData* data,
+    const FuriHalFlashRawOptionByteData* mask) {
+    bool valid = true;
+    for(size_t idx = 0; valid && (idx < FURI_HAL_FLASH_OB_TOTAL_VALUES); ++idx) {
+        valid &= (data->obs[idx]. dword & mask->obs[idx].dword) ==
+                 data->obs[idx].dword;
+    }
+    return valid;
+}
+
+bool update_manifest_has_obdata(UpdateManifest* update_manifest) {
+    bool ob_data_valid = false;
+    // do we have at least 1 value?
+    for(size_t idx = 0; !ob_data_valid && (idx < FURI_HAL_FLASH_OB_RAW_SIZE_BYTES); ++idx) {
+        ob_data_valid |= update_manifest->ob_reference.bytes[idx] != 0;
+    }
+    // sanity checks
+    ob_data_valid &= ob_data_check_mask_valid(&update_manifest->ob_write_mask);
+    ob_data_valid &= ob_data_check_mask_valid(&update_manifest->ob_compare_mask);
+    ob_data_valid &= ob_data_check_masked_values_valid(
+        &update_manifest->ob_reference, &update_manifest->ob_compare_mask);
+    return ob_data_valid;
+}
+
 bool update_manifest_init(UpdateManifest* update_manifest, const char* manifest_filename) {
 bool update_manifest_init(UpdateManifest* update_manifest, const char* manifest_filename) {
     Storage* storage = furi_record_open("storage");
     Storage* storage = furi_record_open("storage");
     FlipperFormat* flipper_file = flipper_format_file_alloc(storage);
     FlipperFormat* flipper_file = flipper_format_file_alloc(storage);

+ 20 - 1
lib/update_util/update_manifest.h

@@ -7,12 +7,26 @@ extern "C" {
 #include <stdint.h>
 #include <stdint.h>
 #include <stdbool.h>
 #include <stdbool.h>
 #include <m-string.h>
 #include <m-string.h>
+#include <furi_hal_flash.h>
 
 
 /* Paths don't include /ext -- because at startup SD card is mounted as root */
 /* Paths don't include /ext -- because at startup SD card is mounted as root */
 #define UPDATE_DIR_DEFAULT_REL_PATH "/update"
 #define UPDATE_DIR_DEFAULT_REL_PATH "/update"
 #define UPDATE_MANIFEST_DEFAULT_NAME "update.fuf"
 #define UPDATE_MANIFEST_DEFAULT_NAME "update.fuf"
 #define UPDATE_MAINFEST_DEFAULT_PATH UPDATE_DIR_DEFAULT_REL_PATH "/" UPDATE_MANIFEST_DEFAULT_NAME
 #define UPDATE_MAINFEST_DEFAULT_PATH UPDATE_DIR_DEFAULT_REL_PATH "/" UPDATE_MANIFEST_DEFAULT_NAME
 
 
+typedef union {
+    uint8_t raw[6];
+    struct {
+        uint8_t major;
+        uint8_t minor;
+        uint8_t sub;
+        uint8_t branch;
+        uint8_t release;
+        uint8_t type;
+    } version;
+} UpdateManifestRadioVersion;
+_Static_assert(sizeof(UpdateManifestRadioVersion) == 6, "UpdateManifestRadioVersion size error");
+
 typedef struct {
 typedef struct {
     string_t version;
     string_t version;
     uint32_t target;
     uint32_t target;
@@ -21,9 +35,12 @@ typedef struct {
     string_t firmware_dfu_image;
     string_t firmware_dfu_image;
     string_t radio_image;
     string_t radio_image;
     uint32_t radio_address;
     uint32_t radio_address;
-    uint32_t radio_version;
+    UpdateManifestRadioVersion radio_version;
     uint32_t radio_crc;
     uint32_t radio_crc;
     string_t resource_bundle;
     string_t resource_bundle;
+    FuriHalFlashRawOptionByteData ob_reference;
+    FuriHalFlashRawOptionByteData ob_compare_mask;
+    FuriHalFlashRawOptionByteData ob_write_mask;
     bool valid;
     bool valid;
 } UpdateManifest;
 } UpdateManifest;
 
 
@@ -38,6 +55,8 @@ bool update_manifest_init_mem(
     const uint8_t* manifest_data,
     const uint8_t* manifest_data,
     const uint16_t length);
     const uint16_t length);
 
 
+bool update_manifest_has_obdata(UpdateManifest* update_manifest);
+
 #ifdef __cplusplus
 #ifdef __cplusplus
 }
 }
 #endif
 #endif

+ 2 - 12
lib/update_util/update_operation.c

@@ -7,6 +7,7 @@
 #include <m-string.h>
 #include <m-string.h>
 #include <loader/loader.h>
 #include <loader/loader.h>
 #include <lib/toolbox/path.h>
 #include <lib/toolbox/path.h>
+#include <lib/toolbox/crc32_calc.h>
 
 
 static const char* UPDATE_ROOT_DIR = "/ext" UPDATE_DIR_DEFAULT_REL_PATH;
 static const char* UPDATE_ROOT_DIR = "/ext" UPDATE_DIR_DEFAULT_REL_PATH;
 static const char* UPDATE_PREFIX = "/ext" UPDATE_DIR_DEFAULT_REL_PATH "/";
 static const char* UPDATE_PREFIX = "/ext" UPDATE_DIR_DEFAULT_REL_PATH "/";
@@ -156,8 +157,6 @@ UpdatePrepareResult update_operation_prepare(const char* manifest_file_path) {
         path_extract_dirname(manifest_file_path, stage_path);
         path_extract_dirname(manifest_file_path, stage_path);
         path_append(stage_path, string_get_cstr(manifest->staged_loader_file));
         path_append(stage_path, string_get_cstr(manifest->staged_loader_file));
 
 
-        const uint16_t READ_BLOCK = 0x1000;
-        uint8_t* read_buffer = malloc(READ_BLOCK);
         uint32_t crc = 0;
         uint32_t crc = 0;
         do {
         do {
             if(!storage_file_open(
             if(!storage_file_open(
@@ -166,19 +165,10 @@ UpdatePrepareResult update_operation_prepare(const char* manifest_file_path) {
             }
             }
 
 
             result = UpdatePrepareResultStageIntegrityError;
             result = UpdatePrepareResultStageIntegrityError;
-            furi_hal_crc_acquire(osWaitForever);
-
-            uint16_t bytes_read = 0;
-            do {
-                bytes_read = storage_file_read(file, read_buffer, READ_BLOCK);
-                crc = furi_hal_crc_feed(read_buffer, bytes_read);
-            } while(bytes_read == READ_BLOCK);
-
-            furi_hal_crc_reset();
+            crc = crc32_calc_file(file, NULL, NULL);
         } while(false);
         } while(false);
 
 
         string_clear(stage_path);
         string_clear(stage_path);
-        free(read_buffer);
         storage_file_free(file);
         storage_file_free(file);
 
 
         if(crc == manifest->staged_loader_crc) {
         if(crc == manifest->staged_loader_crc) {

+ 32 - 5
scripts/assets.py

@@ -50,6 +50,26 @@ class Main(App):
         self.parser_copro.add_argument("cube_dir", help="Path to Cube folder")
         self.parser_copro.add_argument("cube_dir", help="Path to Cube folder")
         self.parser_copro.add_argument("output_dir", help="Path to output folder")
         self.parser_copro.add_argument("output_dir", help="Path to output folder")
         self.parser_copro.add_argument("mcu", help="MCU series as in copro folder")
         self.parser_copro.add_argument("mcu", help="MCU series as in copro folder")
+        self.parser_copro.add_argument(
+            "--cube_ver", dest="cube_ver", help="Cube version", required=True
+        )
+        self.parser_copro.add_argument(
+            "--stack_type", dest="stack_type", help="Stack type", required=True
+        )
+        self.parser_copro.add_argument(
+            "--stack_file",
+            dest="stack_file",
+            help="Stack file name in copro folder",
+            required=True,
+        )
+        self.parser_copro.add_argument(
+            "--stack_addr",
+            dest="stack_addr",
+            help="Stack flash address, as per release_notes",
+            type=lambda x: int(x, 16),
+            default=0,
+            required=False,
+        )
         self.parser_copro.set_defaults(func=self.copro)
         self.parser_copro.set_defaults(func=self.copro)
 
 
         self.parser_dolphin = self.subparsers.add_parser(
         self.parser_dolphin = self.subparsers.add_parser(
@@ -203,13 +223,15 @@ class Main(App):
         manifest_file = os.path.join(directory_path, "Manifest")
         manifest_file = os.path.join(directory_path, "Manifest")
         old_manifest = Manifest()
         old_manifest = Manifest()
         if os.path.exists(manifest_file):
         if os.path.exists(manifest_file):
-            self.logger.info("old manifest is present, loading for compare")
+            self.logger.info("Manifest is present, loading to compare")
             old_manifest.load(manifest_file)
             old_manifest.load(manifest_file)
-        self.logger.info(f'Creating new Manifest for directory "{directory_path}"')
+        self.logger.info(
+            f'Creating temporary Manifest for directory "{directory_path}"'
+        )
         new_manifest = Manifest()
         new_manifest = Manifest()
         new_manifest.create(directory_path)
         new_manifest.create(directory_path)
 
 
-        self.logger.info(f"Comparing new manifest with old")
+        self.logger.info(f"Comparing new manifest with existing")
         only_in_old, changed, only_in_new = Manifest.compare(old_manifest, new_manifest)
         only_in_old, changed, only_in_new = Manifest.compare(old_manifest, new_manifest)
         for record in only_in_old:
         for record in only_in_old:
             self.logger.info(f"Only in old: {record}")
             self.logger.info(f"Only in old: {record}")
@@ -233,9 +255,14 @@ class Main(App):
         self.logger.info(f"Bundling coprocessor binaries")
         self.logger.info(f"Bundling coprocessor binaries")
         copro = Copro(self.args.mcu)
         copro = Copro(self.args.mcu)
         self.logger.info(f"Loading CUBE info")
         self.logger.info(f"Loading CUBE info")
-        copro.loadCubeInfo(self.args.cube_dir)
+        copro.loadCubeInfo(self.args.cube_dir, self.args.cube_ver)
         self.logger.info(f"Bundling")
         self.logger.info(f"Bundling")
-        copro.bundle(self.args.output_dir)
+        copro.bundle(
+            self.args.output_dir,
+            self.args.stack_file,
+            self.args.stack_type,
+            self.args.stack_addr,
+        )
         self.logger.info(f"Complete")
         self.logger.info(f"Complete")
 
 
         return 0
         return 0

+ 1 - 0
scripts/dist.py

@@ -91,6 +91,7 @@ class Main(App):
                         self.args.resources,
                         self.args.resources,
                     )
                     )
                 )
                 )
+            bundle_args.extend(self.other_args)
             self.logger.info(
             self.logger.info(
                 f"Use this directory to self-update your Flipper:\n\t{bundle_dir}"
                 f"Use this directory to self-update your Flipper:\n\t{bundle_dir}"
             )
             )

+ 23 - 4
scripts/flash.py

@@ -7,6 +7,7 @@ import os
 
 
 from flipper.app import App
 from flipper.app import App
 from flipper.cube import CubeProgrammer
 from flipper.cube import CubeProgrammer
+from flipper.assets.coprobin import CoproBinary
 
 
 STATEMENT = "AGREE_TO_LOSE_FLIPPER_FEATURES_THAT_USE_CRYPTO_ENCLAVE"
 STATEMENT = "AGREE_TO_LOSE_FLIPPER_FEATURES_THAT_USE_CRYPTO_ENCLAVE"
 
 
@@ -68,10 +69,15 @@ class Main(App):
         )
         )
         self._addArgsSWD(self.parser_core2radio)
         self._addArgsSWD(self.parser_core2radio)
         self.parser_core2radio.add_argument(
         self.parser_core2radio.add_argument(
-            "radio_address", type=str, help="Radio Stack Binary Address"
+            "radio", type=str, help="Radio Stack Binary"
         )
         )
         self.parser_core2radio.add_argument(
         self.parser_core2radio.add_argument(
-            "radio", type=str, help="Radio Stack Binary"
+            "--addr",
+            dest="radio_address",
+            help="Radio Stack Binary Address, as per release_notes",
+            type=lambda x: int(x, 16),
+            default=0,
+            required=False,
         )
         )
         self.parser_core2radio.set_defaults(func=self.core2radio)
         self.parser_core2radio.set_defaults(func=self.core2radio)
 
 
@@ -144,14 +150,27 @@ class Main(App):
         return 0
         return 0
 
 
     def core2radio(self):
     def core2radio(self):
-        if int(self.args.radio_address, 16) > 0x080E0000:
+        stack_info = CoproBinary(self.args.radio)
+        if not stack_info.is_stack():
+            self.logger.error("Not a Radio Stack")
+            return 1
+        self.logger.info(f"Will flash {stack_info.img_sig.get_version()}")
+
+        radio_address = self.args.radio_address
+        if not radio_address:
+            radio_address = stack_info.get_flash_load_addr()
+            self.logger.warning(
+                f"Radio address not provided, guessed as 0x{radio_address:X}"
+            )
+        if radio_address > 0x080E0000:
             self.logger.error(f"I KNOW WHAT YOU DID LAST SUMMER")
             self.logger.error(f"I KNOW WHAT YOU DID LAST SUMMER")
             return 1
             return 1
+
         cp = CubeProgrammer(self._getCubeParams())
         cp = CubeProgrammer(self._getCubeParams())
         self.logger.info(f"Removing Current Radio Stack")
         self.logger.info(f"Removing Current Radio Stack")
         cp.deleteCore2RadioStack()
         cp.deleteCore2RadioStack()
         self.logger.info(f"Flashing Radio Stack")
         self.logger.info(f"Flashing Radio Stack")
-        cp.flashCore2(self.args.radio_address, self.args.radio)
+        cp.flashCore2(radio_address, self.args.radio)
         self.logger.info(f"Complete")
         self.logger.info(f"Complete")
         return 0
         return 0
 
 

+ 1 - 1
scripts/flipper/app.py

@@ -16,7 +16,7 @@ class App:
         self.init()
         self.init()
 
 
     def __call__(self, args=None):
     def __call__(self, args=None):
-        self.args, _ = self.parser.parse_known_args(args=args)
+        self.args, self.other_args = self.parser.parse_known_args(args=args)
         # configure log output
         # configure log output
         self.log_level = logging.DEBUG if self.args.debug else logging.INFO
         self.log_level = logging.DEBUG if self.args.debug else logging.INFO
         self.logger.setLevel(self.log_level)
         self.logger.setLevel(self.log_level)

+ 28 - 14
scripts/flipper/assets/copro.py

@@ -2,9 +2,12 @@ import logging
 import datetime
 import datetime
 import shutil
 import shutil
 import json
 import json
+from os.path import basename
 
 
 import xml.etree.ElementTree as ET
 import xml.etree.ElementTree as ET
 from flipper.utils import *
 from flipper.utils import *
+from flipper.assets.coprobin import CoproBinary, get_stack_type
+
 
 
 CUBE_COPRO_PATH = "Projects/STM32WB_Copro_Wireless_Binaries"
 CUBE_COPRO_PATH = "Projects/STM32WB_Copro_Wireless_Binaries"
 
 
@@ -13,14 +16,7 @@ MANIFEST_TEMPLATE = {
     "copro": {
     "copro": {
         "fus": {"version": {"major": 1, "minor": 2, "sub": 0}, "files": []},
         "fus": {"version": {"major": 1, "minor": 2, "sub": 0}, "files": []},
         "radio": {
         "radio": {
-            "version": {
-                "type": 3,
-                "major": 1,
-                "minor": 13,
-                "sub": 0,
-                "branch": 0,
-                "release": 5,
-            },
+            "version": {},
             "files": [],
             "files": [],
         },
         },
     },
     },
@@ -35,7 +31,7 @@ class Copro:
         self.mcu_copro = None
         self.mcu_copro = None
         self.logger = logging.getLogger(self.__class__.__name__)
         self.logger = logging.getLogger(self.__class__.__name__)
 
 
-    def loadCubeInfo(self, cube_dir):
+    def loadCubeInfo(self, cube_dir, cube_version):
         if not os.path.isdir(cube_dir):
         if not os.path.isdir(cube_dir):
             raise Exception(f'"{cube_dir}" doesn\'t exists')
             raise Exception(f'"{cube_dir}" doesn\'t exists')
         self.cube_dir = cube_dir
         self.cube_dir = cube_dir
@@ -51,8 +47,8 @@ class Copro:
         if not cube_version or not cube_version.startswith("FW.WB"):
         if not cube_version or not cube_version.startswith("FW.WB"):
             raise Exception(f"Incorrect Cube package or version info")
             raise Exception(f"Incorrect Cube package or version info")
         cube_version = cube_version.replace("FW.WB.", "", 1)
         cube_version = cube_version.replace("FW.WB.", "", 1)
-        if cube_version != "1.13.1":
-            raise Exception(f"Unknonwn cube version")
+        if cube_version != cube_version:
+            raise Exception(f"Unsupported cube version")
         self.version = cube_version
         self.version = cube_version
 
 
     def addFile(self, array, filename, **kwargs):
     def addFile(self, array, filename, **kwargs):
@@ -63,14 +59,32 @@ class Copro:
             {"name": filename, "sha256": file_sha256(destination_file), **kwargs}
             {"name": filename, "sha256": file_sha256(destination_file), **kwargs}
         )
         )
 
 
-    def bundle(self, output_dir):
+    def bundle(self, output_dir, stack_file_name, stack_type, stack_addr=None):
         if not os.path.isdir(output_dir):
         if not os.path.isdir(output_dir):
             raise Exception(f'"{output_dir}" doesn\'t exists')
             raise Exception(f'"{output_dir}" doesn\'t exists')
         self.output_dir = output_dir
         self.output_dir = output_dir
+        stack_file = os.path.join(self.mcu_copro, stack_file_name)
         manifest_file = os.path.join(self.output_dir, "Manifest.json")
         manifest_file = os.path.join(self.output_dir, "Manifest.json")
         # Form Manifest
         # Form Manifest
         manifest = dict(MANIFEST_TEMPLATE)
         manifest = dict(MANIFEST_TEMPLATE)
         manifest["manifest"]["timestamp"] = timestamp()
         manifest["manifest"]["timestamp"] = timestamp()
+        copro_bin = CoproBinary(stack_file)
+        self.logger.info(f"Bundling {copro_bin.img_sig.get_version()}")
+        stack_type_code = get_stack_type(stack_type)
+        manifest["copro"]["radio"]["version"].update(
+            {
+                "type": stack_type_code,
+                "major": copro_bin.img_sig.version_major,
+                "minor": copro_bin.img_sig.version_minor,
+                "sub": copro_bin.img_sig.version_sub,
+                "branch": copro_bin.img_sig.version_branch,
+                "release": copro_bin.img_sig.version_build,
+            }
+        )
+        if not stack_addr:
+            stack_addr = copro_bin.get_flash_load_addr()
+            self.logger.info(f"Using guessed flash address 0x{stack_addr:x}")
+
         # Old FUS Update
         # Old FUS Update
         self.addFile(
         self.addFile(
             manifest["copro"]["fus"]["files"],
             manifest["copro"]["fus"]["files"],
@@ -88,8 +102,8 @@ class Copro:
         # BLE Full Stack
         # BLE Full Stack
         self.addFile(
         self.addFile(
             manifest["copro"]["radio"]["files"],
             manifest["copro"]["radio"]["files"],
-            "stm32wb5x_BLE_Stack_light_fw.bin",
-            address="0x080D7000",
+            stack_file_name,
+            address=f"0x{stack_addr:X}",
         )
         )
         # Save manifest to
         # Save manifest to
         json.dump(manifest, open(manifest_file, "w"))
         json.dump(manifest, open(manifest_file, "w"))

+ 187 - 0
scripts/flipper/assets/coprobin.py

@@ -0,0 +1,187 @@
+import struct
+import math
+import os, os.path
+import sys
+
+
+#  From STM32CubeWB\Middlewares\ST\STM32_WPAN\interface\patterns\ble_thread\shci\shci.h
+__STACK_TYPE_CODES = {
+    "BLE_FULL": 0x01,
+    "BLE_HCI": 0x02,
+    "BLE_LIGHT": 0x03,
+    "BLE_BEACON": 0x04,
+    "BLE_BASIC": 0x05,
+    "BLE_FULL_EXT_ADV": 0x06,
+    "BLE_HCI_EXT_ADV": 0x07,
+    "THREAD_FTD": 0x10,
+    "THREAD_MTD": 0x11,
+    "ZIGBEE_FFD": 0x30,
+    "ZIGBEE_RFD": 0x31,
+    "MAC": 0x40,
+    "BLE_THREAD_FTD_STATIC": 0x50,
+    "BLE_THREAD_FTD_DYAMIC": 0x51,
+    "802154_LLD_TESTS": 0x60,
+    "802154_PHY_VALID": 0x61,
+    "BLE_PHY_VALID": 0x62,
+    "BLE_LLD_TESTS": 0x63,
+    "BLE_RLV": 0x64,
+    "802154_RLV": 0x65,
+    "BLE_ZIGBEE_FFD_STATIC": 0x70,
+    "BLE_ZIGBEE_RFD_STATIC": 0x71,
+    "BLE_ZIGBEE_FFD_DYNAMIC": 0x78,
+    "BLE_ZIGBEE_RFD_DYNAMIC": 0x79,
+    "RLV": 0x80,
+    "BLE_MAC_STATIC": 0x90,
+}
+
+
+class CoproException(ValueError):
+    pass
+
+
+#  Formats based on AN5185
+class CoproFooterBase:
+    SIG_BIN_SIZE = 5 * 4
+    _SIG_BIN_COMMON_SIZE = 2 * 4
+
+    def get_version(self):
+        return f"Version {self.version_major}.{self.version_minor}.{self.version_sub}, branch {self.version_branch}, build {self.version_build} (magic {self.magic:X})"
+
+    def get_details(self):
+        raise CoproException("Not implemented")
+
+    def __init__(self, raw: bytes):
+        if len(raw) != self.SIG_BIN_SIZE:
+            raise CoproException("Invalid footer size")
+        sig_common_part = raw[-self._SIG_BIN_COMMON_SIZE :]
+        parts = struct.unpack("BBBBI", sig_common_part)
+        self.version_major = parts[3]
+        self.version_minor = parts[2]
+        self.version_sub = parts[1]
+        #  AN5185 mismatch: swapping byte halves
+        self.version_build = parts[0] & 0x0F
+        self.version_branch = (parts[0] & 0xF0) >> 4
+        self.magic = parts[4]
+
+
+class CoproFusFooter(CoproFooterBase):
+    FUS_MAGIC_IMG_STACK = 0x23372991
+    FUS_MAGIC_IMG_FUS = 0x32279221
+    FUS_MAGIC_IMG_OTHER = 0x42769811
+
+    FUS_BASE = 0x80F4000
+    FLASH_PAGE_SIZE = 4 * 1024
+
+    def __init__(self, raw: bytes):
+        super().__init__(raw)
+        if self.magic not in (
+            self.FUS_MAGIC_IMG_OTHER,
+            self.FUS_MAGIC_IMG_FUS,
+            self.FUS_MAGIC_IMG_STACK,
+        ):
+            raise CoproException(f"Invalid FUS img magic {self.magic:x}")
+        own_data = raw[: -self._SIG_BIN_COMMON_SIZE]
+        parts = struct.unpack("IIBBBB", own_data)
+        self.info1 = parts[0]
+        self.info2 = parts[1]
+        self.sram2b_1ks = parts[5]
+        self.sram2a_1ks = parts[4]
+        self.flash_4ks = parts[2]
+
+    def get_details(self):
+        return f"SRAM2b={self.sram2b_1ks}k SRAM2a={self.sram2a_1ks}k flash={self.flash_4ks}p"
+
+    def is_stack(self):
+        return self.magic == self.FUS_MAGIC_IMG_STACK
+
+    def get_flash_pages(self, fullsize):
+        return math.ceil(fullsize / self.FLASH_PAGE_SIZE)
+
+    def get_flash_base(self, fullsize):
+        if not self.is_stack():
+            raise CoproException("Not a stack image")
+        return self.FUS_BASE - self.get_flash_pages(fullsize) * self.FLASH_PAGE_SIZE
+
+
+class CoproSigFooter(CoproFooterBase):
+    SIG_MAGIC_ST = 0xD3A12C5E
+    SIG_MAGIC_CUSTOMER = 0xE2B51D4A
+
+    def __init__(self, raw: bytes):
+        super().__init__(raw)
+        if self.magic not in (self.SIG_MAGIC_ST, self.SIG_MAGIC_CUSTOMER):
+            raise CoproException(f"Invalid FUS img magic {self.magic:x}")
+        own_data = raw[: -self._SIG_BIN_COMMON_SIZE]
+        parts = struct.unpack("IIBBH", own_data)
+        self.reserved_1 = parts[0]
+        self.reserved_2 = parts[1]
+        self.size = parts[2]
+        self.source = parts[3]
+        self.reserved_34 = parts[4]
+
+    def get_details(self):
+        return f"Signature Src {self.source:x} size {self.size:x}"
+
+
+class CoproBinary:
+    def __init__(self, binary_path):
+        self.binary_path = binary_path
+        self.img_sig_footer = None
+        self.img_sig = None
+        self.binary_size = -1
+        self._load()
+
+    def _load(self):
+        with open(self.binary_path, "rb") as fin:
+            whole_file = fin.read()
+            self.binary_size = len(whole_file)
+
+            img_sig_footer_bin = whole_file[-CoproFooterBase.SIG_BIN_SIZE :]
+            self.img_sig_footer = CoproSigFooter(img_sig_footer_bin)
+            img_sig_size = self.img_sig_footer.size + CoproSigFooter.SIG_BIN_SIZE
+            img_sig_bin = whole_file[
+                -(img_sig_size + CoproFusFooter.SIG_BIN_SIZE) : -img_sig_size
+            ]
+            self.img_sig = CoproFusFooter(img_sig_bin)
+
+    def is_valid(self):
+        return self.img_sig_footer is not None and self.img_sig is not None
+
+    def is_stack(self):
+        return self.img_sig and self.img_sig.is_stack()
+
+    def get_flash_load_addr(self):
+        if not self.is_stack():
+            raise CoproException("Not a stack image")
+        return self.img_sig.get_flash_base(self.binary_size)
+
+
+def get_stack_type(typestr: str):
+    stack_code = __STACK_TYPE_CODES.get(typestr.upper(), None)
+    if stack_code is None:
+        raise CoproException(f"Unknown stack type {typestr}. See shci.h")
+    return stack_code
+
+
+def _load_bin(binary_path: str):
+    print(binary_path)
+    copro_bin = CoproBinary(binary_path)
+    print(copro_bin.img_sig.get_version())
+    if copro_bin.img_sig.is_stack():
+        print(f"\t>> FLASH AT {copro_bin.get_flash_load_addr():X}\n")
+
+
+def main():
+    coprodir = (
+        sys.argv[1]
+        if len(sys.argv) > 1
+        else "../../../lib/STM32CubeWB/Projects/STM32WB_Copro_Wireless_Binaries/STM32WB5x"
+    )
+    for fn in os.listdir(coprodir):
+        if not fn.endswith(".bin"):
+            continue
+        _load_bin(os.path.join(coprodir, fn))
+
+
+if __name__ == "__main__":
+    main()

+ 208 - 0
scripts/flipper/assets/obdata.py

@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+
+import logging
+import struct
+
+from enum import Enum
+from dataclasses import dataclass
+from typing import Tuple
+from array import array
+
+
+class OBException(ValueError):
+    pass
+
+
+@dataclass
+class OBParams:
+    word_idx: int
+    bits: Tuple[int, int]
+    name: str
+
+
+_OBS_descr = (
+    OBParams(0, (0, 8), "RDP"),
+    OBParams(0, (8, 9), "ESE"),
+    OBParams(0, (9, 12), "BOR_LEV"),
+    OBParams(0, (12, 13), "nRST_STOP"),
+    OBParams(0, (13, 14), "nRST_STDBY"),
+    OBParams(0, (14, 15), "nRSTSHDW"),
+    OBParams(0, (15, 16), "UNUSED1"),
+    OBParams(0, (16, 17), "IWDGSW"),
+    OBParams(0, (17, 18), "IWDGSTOP"),
+    OBParams(0, (18, 19), "IWGDSTDBY"),  #  ST's typo: IWDGSTDBY
+    OBParams(0, (18, 19), "IWDGSTDBY"),  #  ST's typo: IWDGSTDBY
+    OBParams(0, (19, 20), "WWDGSW"),
+    OBParams(0, (20, 23), "UNUSED2"),
+    OBParams(0, (23, 24), "nBOOT1"),
+    OBParams(0, (24, 25), "SRAM2PE"),
+    OBParams(0, (25, 26), "SRAM2RST"),
+    OBParams(0, (26, 27), "nSWBOOT0"),
+    OBParams(0, (27, 28), "nBOOT0"),
+    OBParams(0, (28, 29), "UNUSED3"),
+    OBParams(0, (29, 32), "AGC_TRIM"),
+    OBParams(1, (0, 9), "PCROP1A_STRT"),
+    OBParams(1, (9, 32), "UNUSED"),
+    OBParams(2, (0, 9), "PCROP1A_END"),
+    OBParams(2, (9, 31), "UNUSED"),
+    OBParams(2, (31, 32), "PCROP_RDP"),
+    OBParams(3, (0, 8), "WRP1A_STRT"),
+    OBParams(3, (8, 16), "UNUSED1"),
+    OBParams(3, (16, 24), "WRP1A_END"),
+    OBParams(3, (24, 32), "UNUSED2"),
+    OBParams(4, (0, 8), "WRP1B_STRT"),
+    OBParams(4, (8, 16), "UNUSED1"),
+    OBParams(4, (16, 24), "WRP1B_END"),
+    OBParams(4, (24, 32), "UNUSED2"),
+    OBParams(5, (0, 9), "PCROP1B_STRT"),
+    OBParams(5, (9, 32), "UNUSED"),
+    OBParams(6, (0, 9), "PCROP1B_END"),
+    OBParams(6, (9, 32), "UNUSED"),
+    OBParams(13, (0, 14), "IPCCDBA"),
+    OBParams(13, (14, 32), "UNUSED"),
+    OBParams(14, (0, 8), "SFSA"),
+    OBParams(14, (8, 9), "FSD"),
+    OBParams(14, (9, 12), "UNUSED1"),
+    OBParams(14, (12, 13), "DDS"),
+    OBParams(14, (13, 32), "UNUSED2"),
+    OBParams(15, (0, 18), "SBRV"),
+    OBParams(15, (18, 23), "SBRSA"),
+    OBParams(15, (23, 24), "BRSD"),
+    OBParams(15, (24, 25), "UNUSED1"),
+    OBParams(15, (25, 30), "SNBRSA"),
+    OBParams(15, (30, 31), "NBRSD"),
+    OBParams(15, (31, 32), "C2OPT"),
+)
+
+
+_OBS = dict((param.name, param) for param in _OBS_descr)
+
+
+@dataclass
+class EncodedOBValue:
+    value: int
+    mask: int
+    params: OBParams
+
+
+class OptionByte:
+    class OBMode(Enum):
+        IGNORE = 0
+        READ = 1
+        READ_WRITE = 2
+
+        @classmethod
+        def from_str(cls, value):
+            if value == "r":
+                return cls.READ
+            elif value == "rw":
+                return cls.READ_WRITE
+            else:
+                raise OBException(f"Unknown OB check mode '{value}'")
+
+    def __init__(self, obstr):
+        parts = obstr.split(":")
+        if len(parts) != 3:
+            raise OBException(f"Invalid OB value definition {obstr}")
+        self.name = parts[0]
+        self.value = int(parts[1], 16)
+        self.mode = OptionByte.OBMode.from_str(parts[2].strip())
+        self.descr = _OBS.get(self.name, None)
+        if self.descr is None:
+            raise OBException(f"Missing OB descriptor for {self.name}")
+
+    def encode(self):
+        startbit, endbit = self.descr.bits
+        value_mask = 2 ** (endbit - startbit) - 1
+        value_corrected = self.value & value_mask
+
+        value_shifted = value_corrected << startbit
+        value_mask_shifted = value_mask << startbit
+        return EncodedOBValue(value_shifted, value_mask_shifted, self)
+
+    def __repr__(self):
+        return f"<OB {self.name}, 0x{self.value:x}, {self.mode} at 0x{id(self):X}>"
+
+
+@dataclass
+class ObReferenceValues:
+    reference: bytes
+    compare_mask: bytes
+    write_mask: bytes
+
+
+class ObReferenceValuesGenerator:
+    def __init__(self):
+        self.compare_mask = array("I", [0] * 16)
+        self.write_mask = array("I", [0] * 16)
+        self.ref_values = array("I", [0] * 16)
+
+    def __repr__(self):
+        return (
+            f"<OBRefs REFS=[{' '.join(hex(v) for v in self.ref_values)}] "
+            f"CMPMASK=[{' '.join(hex(v) for v in self.compare_mask)}] "
+            f"WRMASK=[{' '.join(hex(v) for v in self.write_mask)}] "
+        )
+
+    def export_values(self):
+        export_cmpmask = array("I")
+        for value in self.compare_mask:
+            export_cmpmask.append(value)
+            export_cmpmask.append(value)
+        export_wrmask = array("I")
+        for value in self.write_mask:
+            export_wrmask.append(value)
+            export_wrmask.append(value)
+        export_refvals = array("I")
+        for cmpmask, refval in zip(self.compare_mask, self.ref_values):
+            export_refvals.append(refval)
+            export_refvals.append((refval ^ 0xFFFFFFFF) & cmpmask)
+        return export_refvals, export_cmpmask, export_wrmask
+
+    def export(self):
+        return ObReferenceValues(*map(lambda a: a.tobytes(), self.export_values()))
+
+    def apply(self, ob):
+        ob_params = ob.descr
+        encoded_ob = ob.encode()
+        self.compare_mask[ob_params.word_idx] |= encoded_ob.mask
+        self.ref_values[ob_params.word_idx] |= encoded_ob.value
+        if ob.mode == OptionByte.OBMode.READ_WRITE:
+            self.write_mask[ob_params.word_idx] |= encoded_ob.mask
+
+
+class OptionBytesData:
+    def __init__(self, obfname):
+        self.obs = list()
+        with open(obfname, "rt") as obfin:
+            self.obs = list(
+                OptionByte(line) for line in obfin if not line.startswith("#")
+            )
+
+    def gen_values(self):
+        obref = ObReferenceValuesGenerator()
+        converted_refs = list(obref.apply(ob) for ob in self.obs)
+        return obref
+
+
+def main():
+    with open("../../../../logs/obs.bin", "rb") as obsbin:
+        ob_sample = obsbin.read(128)
+        ob_sample_arr = array("I", ob_sample)
+    print(ob_sample_arr)
+
+    obd = OptionBytesData("../../ob.data")
+    print(obd.obs)
+    # print(obd.gen_values().export())
+    ref, mask, wrmask = obd.gen_values().export_values()
+    for idx in range(len(ob_sample_arr)):
+        real_masked = ob_sample_arr[idx] & mask[idx]
+        print(
+            f"#{idx}: ref {ref[idx]:08x} real {real_masked:08x} ({ob_sample_arr[idx]:08x} & {mask[idx]:08x}) match {ref[idx]==real_masked}"
+        )
+
+    # print(ob_sample)
+
+
+if __name__ == "__main__":
+    main()

+ 69 - 7
scripts/update.py

@@ -2,11 +2,14 @@
 
 
 from flipper.app import App
 from flipper.app import App
 from flipper.utils.fff import FlipperFormatFile
 from flipper.utils.fff import FlipperFormatFile
+from flipper.assets.coprobin import CoproBinary, get_stack_type
+from flipper.assets.obdata import OptionBytesData
 from os.path import basename, join, exists
 from os.path import basename, join, exists
 import os
 import os
 import shutil
 import shutil
 import zlib
 import zlib
 import tarfile
 import tarfile
+import math
 
 
 
 
 class Main(App):
 class Main(App):
@@ -28,19 +31,28 @@ class Main(App):
         self.parser_generate.add_argument("-d", dest="directory", required=True)
         self.parser_generate.add_argument("-d", dest="directory", required=True)
         self.parser_generate.add_argument("-v", dest="version", required=True)
         self.parser_generate.add_argument("-v", dest="version", required=True)
         self.parser_generate.add_argument("-t", dest="target", required=True)
         self.parser_generate.add_argument("-t", dest="target", required=True)
-        self.parser_generate.add_argument("--dfu", dest="dfu", required=False)
+        self.parser_generate.add_argument(
+            "--dfu", dest="dfu", default="", required=False
+        )
         self.parser_generate.add_argument("-r", dest="resources", required=False)
         self.parser_generate.add_argument("-r", dest="resources", required=False)
         self.parser_generate.add_argument("--stage", dest="stage", required=True)
         self.parser_generate.add_argument("--stage", dest="stage", required=True)
         self.parser_generate.add_argument(
         self.parser_generate.add_argument(
             "--radio", dest="radiobin", default="", required=False
             "--radio", dest="radiobin", default="", required=False
         )
         )
         self.parser_generate.add_argument(
         self.parser_generate.add_argument(
-            "--radioaddr", dest="radioaddr", required=False
+            "--radioaddr",
+            dest="radioaddr",
+            type=lambda x: int(x, 16),
+            default=0,
+            required=False,
         )
         )
+
         self.parser_generate.add_argument(
         self.parser_generate.add_argument(
-            "--radiover", dest="radioversion", required=False
+            "--radiotype", dest="radiotype", required=False
         )
         )
 
 
+        self.parser_generate.add_argument("--obdata", dest="obdata", required=False)
+
         self.parser_generate.set_defaults(func=self.generate)
         self.parser_generate.set_defaults(func=self.generate)
 
 
     def generate(self):
     def generate(self):
@@ -49,11 +61,27 @@ class Main(App):
         radiobin_basename = basename(self.args.radiobin)
         radiobin_basename = basename(self.args.radiobin)
         resources_basename = ""
         resources_basename = ""
 
 
+        radio_version = 0
+        radio_meta = None
+        radio_addr = self.args.radioaddr
+        if self.args.radiobin:
+            if not self.args.radiotype:
+                raise ValueError("Missing --radiotype")
+            radio_meta = CoproBinary(self.args.radiobin)
+            radio_version = self.copro_version_as_int(radio_meta, self.args.radiotype)
+            if radio_addr == 0:
+                radio_addr = radio_meta.get_flash_load_addr()
+                self.logger.info(
+                    f"Using guessed radio address 0x{radio_addr:X}, verify with Release_Notes"
+                    " or specify --radioaddr"
+                )
+
         if not exists(self.args.directory):
         if not exists(self.args.directory):
             os.makedirs(self.args.directory)
             os.makedirs(self.args.directory)
 
 
         shutil.copyfile(self.args.stage, join(self.args.directory, stage_basename))
         shutil.copyfile(self.args.stage, join(self.args.directory, stage_basename))
-        shutil.copyfile(self.args.dfu, join(self.args.directory, dfu_basename))
+        if self.args.dfu:
+            shutil.copyfile(self.args.dfu, join(self.args.directory, dfu_basename))
         if radiobin_basename:
         if radiobin_basename:
             shutil.copyfile(
             shutil.copyfile(
                 self.args.radiobin, join(self.args.directory, radiobin_basename)
                 self.args.radiobin, join(self.args.directory, radiobin_basename)
@@ -73,13 +101,22 @@ class Main(App):
         file.writeKey("Loader CRC", self.int2ffhex(self.crc(self.args.stage)))
         file.writeKey("Loader CRC", self.int2ffhex(self.crc(self.args.stage)))
         file.writeKey("Firmware", dfu_basename)
         file.writeKey("Firmware", dfu_basename)
         file.writeKey("Radio", radiobin_basename or "")
         file.writeKey("Radio", radiobin_basename or "")
-        file.writeKey("Radio address", self.int2ffhex(self.args.radioaddr or 0))
-        file.writeKey("Radio version", self.int2ffhex(self.args.radioversion or 0))
+        file.writeKey("Radio address", self.int2ffhex(radio_addr))
+        file.writeKey("Radio version", self.int2ffhex(radio_version))
         if radiobin_basename:
         if radiobin_basename:
             file.writeKey("Radio CRC", self.int2ffhex(self.crc(self.args.radiobin)))
             file.writeKey("Radio CRC", self.int2ffhex(self.crc(self.args.radiobin)))
         else:
         else:
             file.writeKey("Radio CRC", self.int2ffhex(0))
             file.writeKey("Radio CRC", self.int2ffhex(0))
         file.writeKey("Resources", resources_basename)
         file.writeKey("Resources", resources_basename)
+        file.writeComment(
+            "NEVER EVER MESS WITH THESE VALUES, YOU WILL BRICK YOUR DEVICE"
+        )
+        if self.args.obdata:
+            obd = OptionBytesData(self.args.obdata)
+            obvalues = obd.gen_values().export()
+            file.writeKey("OB reference", self.bytes2ffhex(obvalues.reference))
+            file.writeKey("OB mask", self.bytes2ffhex(obvalues.compare_mask))
+            file.writeKey("OB write mask", self.bytes2ffhex(obvalues.write_mask))
         file.save(join(self.args.directory, self.UPDATE_MANIFEST_NAME))
         file.save(join(self.args.directory, self.UPDATE_MANIFEST_NAME))
 
 
         return 0
         return 0
@@ -90,9 +127,34 @@ class Main(App):
         ) as tarball:
         ) as tarball:
             tarball.add(srcdir, arcname="")
             tarball.add(srcdir, arcname="")
 
 
+    @staticmethod
+    def copro_version_as_int(coprometa, stacktype):
+        major = coprometa.img_sig.version_major
+        minor = coprometa.img_sig.version_minor
+        sub = coprometa.img_sig.version_sub
+        branch = coprometa.img_sig.version_branch
+        release = coprometa.img_sig.version_build
+        stype = get_stack_type(stacktype)
+        return (
+            major
+            | (minor << 8)
+            | (sub << 16)
+            | (branch << 24)
+            | (release << 32)
+            | (stype << 40)
+        )
+
+    @staticmethod
+    def bytes2ffhex(value: bytes):
+        return " ".join(f"{b:02X}" for b in value)
+
     @staticmethod
     @staticmethod
     def int2ffhex(value: int):
     def int2ffhex(value: int):
-        hexstr = "%08X" % value
+        n_hex_bytes = 4
+        if value:
+            n_hex_bytes = math.ceil(math.ceil(math.log2(value)) / 8) * 2
+        fmtstr = f"%0{n_hex_bytes}X"
+        hexstr = fmtstr % value
         return " ".join(list(Main.batch(hexstr, 2))[::-1])
         return " ".join(list(Main.batch(hexstr, 2))[::-1])
 
 
     @staticmethod
     @staticmethod