Просмотр исходного кода

[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 лет назад
Родитель
Сommit
7ce305fca3
41 измененных файлов с 1623 добавлено и 296 удалено
  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'
         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'
         if: ${{ !github.event.pull_request.head.repo.fork }}
@@ -213,8 +213,8 @@ jobs:
         with:
           run: |
             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'
         uses: ./.github/actions/docker

+ 11 - 6
Makefile

@@ -1,8 +1,7 @@
 PROJECT_ROOT := $(abspath $(dir $(abspath $(firstword $(MAKEFILE_LIST)))))
 
 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_ROOT)/applications \
@@ -97,7 +96,13 @@ updater_package_bin: firmware_all updater
 
 .PHONY: updater_package
 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
 assets_manifest:
@@ -109,7 +114,7 @@ assets_rebuild:
 
 .PHONY: 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
 
 .PHONY: flash_radio_fus
@@ -125,8 +130,8 @@ flash_radio_fus:
 
 .PHONY: 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
 
 .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) {
     furi_assert(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) {

+ 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() {
     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
     if(!bt_keys_storage_load(bt)) {
         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
     * should remain commented out. */
     // 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);
     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) {
         switch(event.event) {
         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:
             if(!update_task_is_running(updater->update_task) &&
                (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",
     [UpdateTaskStageFlashWrite] = "Writing flash",
     [UpdateTaskStageFlashValidate] = "Validating",
+    [UpdateTaskStageRadioImageValidate] = "Checking radio image",
+    [UpdateTaskStageRadioErase] = "Removing 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",
     [UpdateTaskStageLfsRestore] = "Restoring LFS",
     [UpdateTaskStageResourcesUpdate] = "Updating resources",
     [UpdateTaskStageCompleted] = "Completed!",
     [UpdateTaskStageError] = "Error",
+    [UpdateTaskStageOBError] = "OB error, pls report",
 };
 
 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) {
     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_set_status(update_task, NULL);
     }
@@ -53,7 +61,7 @@ void update_task_set_progress(UpdateTask* update_task, UpdateTaskStage stage, ui
             progress,
             update_task->state.current_stage_idx,
             update_task->state.total_stages,
-            update_task->state.stage == UpdateTaskStageError,
+            update_task->state.stage >= UpdateTaskStageError,
             update_task->status_change_cb_state);
     }
 }
@@ -116,6 +124,7 @@ UpdateTask* update_task_alloc() {
     update_task->storage = furi_record_open("storage");
     update_task->file = storage_file_alloc(update_task->storage);
     update_task->status_change_cb = NULL;
+    string_init(update_task->update_path);
 
     FuriThread* thread = update_task->thread = furi_thread_alloc();
 
@@ -152,12 +161,6 @@ void update_task_free(UpdateTask* 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) {
     furi_assert(update_task);
     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) {
     furi_assert(update_task);
     return update_task->manifest;
-}
+}

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

@@ -19,13 +19,18 @@ typedef enum {
     UpdateTaskStageValidateDFUImage,
     UpdateTaskStageFlashWrite,
     UpdateTaskStageFlashValidate,
+    UpdateTaskStageRadioImageValidate,
+    UpdateTaskStageRadioErase,
     UpdateTaskStageRadioWrite,
-    UpdateTaskStageRadioCommit,
+    UpdateTaskStageRadioInstall,
+    UpdateTaskStageRadioBusy,
+    UpdateTaskStageOBValidation,
     UpdateTaskStageLfsBackup,
     UpdateTaskStageLfsRestore,
     UpdateTaskStageResourcesUpdate,
     UpdateTaskStageCompleted,
     UpdateTaskStageError,
+    UpdateTaskStageOBError
 } UpdateTaskStage;
 
 typedef struct {
@@ -50,8 +55,6 @@ UpdateTask* update_task_alloc();
 
 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);
 
 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/update_operation.h>
 #include <toolbox/tar/tar_archive.h>
+#include <toolbox/crc32_calc.h>
+
+#define TAG "UpdWorkerBackup"
 
 #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
-
 #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) {
     bool success = false;
     string_t backup_file_path;
@@ -143,7 +61,8 @@ static bool update_task_post_update(UpdateTask* update_task) {
     string_t 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 {
         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);
         }
+        success = true;
     } while(false);
 
     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
+/core2_firmware

+ 7 - 0
assets/Makefile

@@ -1,6 +1,7 @@
 PROJECT_ROOT		= $(abspath $(dir $(abspath $(firstword $(MAKEFILE_LIST))))..)
 
 include				$(PROJECT_ROOT)/assets/assets.mk
+include				$(PROJECT_ROOT)/assets/copro.mk
 
 .PHONY: all
 all: icons protobuf dolphin manifest
@@ -35,7 +36,13 @@ manifest:
 .PHONY: dolphin
 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:
 	@echo "\tCLEAN\t"
 	@$(RM) $(ASSETS_COMPILED_DIR)/*
+	@$(RM) -rf $(COPRO_BUNDLE_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 <update_util/update_manifest.h>
-#include <lib/toolbox/path.h>
+#include <toolbox/path.h>
+#include <toolbox/crc32_calc.h>
 
 static FATFS* pfs = NULL;
 
@@ -27,7 +28,6 @@ static bool flipper_update_init() {
     furi_hal_delay_init();
 
     furi_hal_spi_init();
-    furi_hal_crc_init(false);
 
     MX_FATFS_Init();
     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;
     const uint16_t MAX_READ = 0xFFFF;
 
-    furi_hal_crc_reset();
     uint32_t crc = 0;
     do {
         uint16_t size_read = 0;
         if(f_read(&file, img + bytes_read, MAX_READ, &size_read) != FR_OK) {
             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;
     } while(bytes_read == MAX_READ);
-    furi_hal_crc_reset();
 
     do {
         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_tl.h"
 #include "app_debug.h"
+
 #include <furi_hal.h>
+#include <shci/shci.h>
 
 #define TAG "Core2"
 
@@ -27,22 +29,13 @@ PLACE_IN_SECTION("MB_MEM2")
 ALIGN(4)
 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 {
     osMutexId_t shci_mtx;
     osSemaphoreId_t shci_sem;
     FuriThread* thread;
     BleGlueStatus status;
     BleGlueKeyStorageChangedCallback callback;
+    BleGlueC2Info c2_info;
     void* context;
 } 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 {
-        FURI_LOG_E(TAG, "Failed to start FUS");
+        FURI_LOG_E(TAG, "C2 startup failed");
         ble_glue->status = BleGlueStatusBroken;
     }
 
-    return ret;
+    return started;
 }
 
 bool ble_glue_start() {
     furi_assert(ble_glue);
 
-    if(ble_glue->status != BleGlueStatusFusStarted) {
+    if(ble_glue->status != BleGlueStatusC2Started) {
         return false;
     }
 
@@ -145,7 +201,7 @@ bool ble_glue_start() {
     furi_hal_power_insomnia_enter();
     if(ble_app_init()) {
         FURI_LOG_I(TAG, "Radio stack started");
-        ble_glue->status = BleGlueStatusRadioStackStarted;
+        ble_glue->status = BleGlueStatusRadioStackRunning;
         ret = true;
         if(SHCI_C2_SetFlashActivityControl(FLASH_ACTIVITY_CONTROL_SEM7) == SHCI_Success) {
             FURI_LOG_I(TAG, "Flash activity control switched to SEM7");
@@ -167,7 +223,7 @@ bool ble_glue_is_alive() {
         return false;
     }
 
-    return ble_glue->status >= BleGlueStatusFusStarted;
+    return ble_glue->status >= BleGlueStatusC2Started;
 }
 
 bool ble_glue_is_radio_stack_ready() {
@@ -175,26 +231,42 @@ bool ble_glue_is_radio_stack_ready() {
         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();
         if(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) {
@@ -228,8 +300,15 @@ static void ble_glue_sys_user_event_callback(void* pPayload) {
         (TL_AsynchEvt_t*)(((tSHCI_UserEvtRxParam*)pPayload)->pckt->evtserial.evt.payload);
 
     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();
     } else if(p_sys_event->subevtcode == SHCI_SUB_EVT_ERROR_NOTIF) {
         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);
     }
 }
+
+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 <stdbool.h>
-#include <shci/shci.h>
 
 #ifdef __cplusplus
 extern "C" {
 #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 (
     *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_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
  *
@@ -46,12 +95,30 @@ void ble_glue_set_key_storage_changed_callback(
 /** Stop SHCI thread */
 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
  *
  * @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
 }
-#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_hal_crypto_init();
-    furi_hal_crc_init(true);
 
     // USB
 #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 \
     { 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;
 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);
 }
 
-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;
     if(info->StackType == INFO_STACK_TYPE_BLE_HCI) {
         furi_hal_bt_stack = FuriHalBtStackHciLayer;
@@ -128,21 +131,22 @@ bool furi_hal_bt_start_radio_stack() {
     }
 
     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);
             ble_glue_thread_stop();
             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");
             // Don't stop SHCI for crypto enclave support
             break;
@@ -232,7 +236,7 @@ bool furi_hal_bt_change_app(FuriHalBtProfile profile, GapEventCallback event_cb,
     ble_app_thread_stop();
     gap_thread_stop();
     FURI_LOG_I(TAG, "Reset SHCI");
-    SHCI_C2_Reinit();
+    ble_glue_reinit_c2();
     osDelay(100);
     ble_glue_thread_stop();
     FURI_LOG_I(TAG, "Start BT initialization");
@@ -404,3 +408,18 @@ void furi_hal_bt_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() {
     furi_check(hal_crc_control.state == CRC_State_Ready);
     if(hal_crc_control.mtx) {
+        furi_check(osMutexGetOwner(hal_crc_control.mtx) == osThreadGetId());
         osMutexRelease(hal_crc_control.mtx);
     }
     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) {
     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>
 
-#define FURI_HAL_TAG "FuriHalFlash"
+#define TAG "FuriHalFlash"
+
 #define FURI_HAL_CRITICAL_MSG "Critical flash operation fail"
 #define FURI_HAL_FLASH_READ_BLOCK 8
 #define FURI_HAL_FLASH_WRITE_BLOCK 8
@@ -14,13 +15,17 @@
 #define FURI_HAL_FLASH_CYCLES_COUNT 10000
 #define FURI_HAL_FLASH_TIMEOUT 1000
 #define FURI_HAL_FLASH_KEY1 0x45670123U
-
 #define FURI_HAL_FLASH_KEY2 0xCDEF89ABU
 #define FURI_HAL_FLASH_TOTAL_PAGES 256
 #define FURI_HAL_FLASH_SR_ERRORS                                                               \
     (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)
 
+//#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_FLASH_PROGRAM_ADDRESS(__VALUE__)                                             \
     (((__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_KEY2);
 
-    /* verify Flash is unlock */
+    /* verify Flash is unlocked */
     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;
-}
+}
+
+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 <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
  */
 void furi_hal_flash_init();
@@ -64,7 +83,7 @@ size_t furi_hal_flash_get_free_page_count();
 
 /** Erase Flash
  *
- * @warning    locking operation with critical section, stales execution
+ * @warning    locking operation with critical section, stalls execution
  *
  * @param      page  The page to erase
  *
@@ -74,7 +93,7 @@ bool furi_hal_flash_erase(uint8_t page);
 
 /** 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      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)
  *
- * @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      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
  */
+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);
     }
 
-    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_mode", ble_c2_info->mode == BleGlueC2ModeFUS ? "FUS" : "Stack", false, context);
 
         // 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);
-        string_printf(value, "%d", pWirelessInfo.FusVersionMinor);
+        string_printf(value, "%d", ble_c2_info->FusVersionMinor);
         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);
-        string_printf(value, "%dK", pWirelessInfo.FusMemorySizeSram2B);
+        string_printf(value, "%dK", ble_c2_info->FusMemorySizeSram2B);
         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);
-        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);
 
         // 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);
-        string_printf(value, "%d", pWirelessInfo.VersionMajor);
+        string_printf(value, "%d", ble_c2_info->VersionMajor);
         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);
-        string_printf(value, "%d", pWirelessInfo.VersionSub);
+        string_printf(value, "%d", ble_c2_info->VersionSub);
         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);
-        string_printf(value, "%d", pWirelessInfo.VersionReleaseType);
+        string_printf(value, "%d", ble_c2_info->VersionReleaseType);
         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);
-        string_printf(value, "%dK", pWirelessInfo.MemorySizeSram2A);
+        string_printf(value, "%dK", ble_c2_info->MemorySizeSram2A);
         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);
-        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);
 
         // 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);
 
     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;
     }
 

+ 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_info.h"
 #include "furi_hal_random.h"
-#include "furi_hal_crc.h"
 
 #ifdef __cplusplus
 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_MINOR (13)
+#define FURI_HAL_BT_C2_START_TIMEOUT 1000
 
 #ifdef __cplusplus
 extern "C" {
@@ -207,6 +208,12 @@ bool furi_hal_bt_start_scan(GapScanCallback callback, void* context);
 /** Stop MAC addresses 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
 }
 #endif

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

@@ -28,6 +28,7 @@ typedef enum {
     FuriHalRtcFlagDebug = (1 << 0),
     FuriHalRtcFlagFactoryReset = (1 << 1),
     FuriHalRtcFlagLock = (1 << 2),
+    FuriHalRtcFlagC2Update = (1 << 3),
 } FuriHalRtcFlag;
 
 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
 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
 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 <furi_hal.h>
+#include <toolbox/crc32_calc.h>
 
 #define VALID_WHOLE_FILE_CRC 0xFFFFFFFF
 #define DFU_SUFFIX_VERSION 0x011A
-#define DFU_DATA_BUFFER_MAX_LEN 512
 #define DFU_SIGNATURE "DfuSe"
 
 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
      * 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_CRC "Radio CRC"
 #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 = malloc(sizeof(UpdateManifest));
@@ -23,6 +26,9 @@ UpdateManifest* update_manifest_alloc() {
     string_init(update_manifest->staged_loader_file);
     string_init(update_manifest->resource_bundle);
     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;
     return update_manifest;
 }
@@ -75,8 +81,8 @@ static bool
         flipper_format_read_hex(
             flipper_file,
             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_file,
             MANIFEST_KEY_RADIO_CRC,
@@ -85,6 +91,22 @@ static bool
         flipper_format_read_string(
             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 =
             (!string_empty_p(update_manifest->firmware_dfu_image) ||
              !string_empty_p(update_manifest->radio_image) ||
@@ -94,6 +116,41 @@ static bool
     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) {
     Storage* storage = furi_record_open("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 <stdbool.h>
 #include <m-string.h>
+#include <furi_hal_flash.h>
 
 /* Paths don't include /ext -- because at startup SD card is mounted as root */
 #define UPDATE_DIR_DEFAULT_REL_PATH "/update"
 #define UPDATE_MANIFEST_DEFAULT_NAME "update.fuf"
 #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 {
     string_t version;
     uint32_t target;
@@ -21,9 +35,12 @@ typedef struct {
     string_t firmware_dfu_image;
     string_t radio_image;
     uint32_t radio_address;
-    uint32_t radio_version;
+    UpdateManifestRadioVersion radio_version;
     uint32_t radio_crc;
     string_t resource_bundle;
+    FuriHalFlashRawOptionByteData ob_reference;
+    FuriHalFlashRawOptionByteData ob_compare_mask;
+    FuriHalFlashRawOptionByteData ob_write_mask;
     bool valid;
 } UpdateManifest;
 
@@ -38,6 +55,8 @@ bool update_manifest_init_mem(
     const uint8_t* manifest_data,
     const uint16_t length);
 
+bool update_manifest_has_obdata(UpdateManifest* update_manifest);
+
 #ifdef __cplusplus
 }
 #endif

+ 2 - 12
lib/update_util/update_operation.c

@@ -7,6 +7,7 @@
 #include <m-string.h>
 #include <loader/loader.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_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_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;
         do {
             if(!storage_file_open(
@@ -166,19 +165,10 @@ UpdatePrepareResult update_operation_prepare(const char* manifest_file_path) {
             }
 
             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);
 
         string_clear(stage_path);
-        free(read_buffer);
         storage_file_free(file);
 
         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("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(
+            "--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_dolphin = self.subparsers.add_parser(
@@ -203,13 +223,15 @@ class Main(App):
         manifest_file = os.path.join(directory_path, "Manifest")
         old_manifest = Manifest()
         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)
-        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.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)
         for record in only_in_old:
             self.logger.info(f"Only in old: {record}")
@@ -233,9 +255,14 @@ class Main(App):
         self.logger.info(f"Bundling coprocessor binaries")
         copro = Copro(self.args.mcu)
         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")
-        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")
 
         return 0

+ 1 - 0
scripts/dist.py

@@ -91,6 +91,7 @@ class Main(App):
                         self.args.resources,
                     )
                 )
+            bundle_args.extend(self.other_args)
             self.logger.info(
                 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.cube import CubeProgrammer
+from flipper.assets.coprobin import CoproBinary
 
 STATEMENT = "AGREE_TO_LOSE_FLIPPER_FEATURES_THAT_USE_CRYPTO_ENCLAVE"
 
@@ -68,10 +69,15 @@ class Main(App):
         )
         self._addArgsSWD(self.parser_core2radio)
         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(
-            "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)
 
@@ -144,14 +150,27 @@ class Main(App):
         return 0
 
     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")
             return 1
+
         cp = CubeProgrammer(self._getCubeParams())
         self.logger.info(f"Removing Current Radio Stack")
         cp.deleteCore2RadioStack()
         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")
         return 0
 

+ 1 - 1
scripts/flipper/app.py

@@ -16,7 +16,7 @@ class App:
         self.init()
 
     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
         self.log_level = logging.DEBUG if self.args.debug else logging.INFO
         self.logger.setLevel(self.log_level)

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

@@ -2,9 +2,12 @@ import logging
 import datetime
 import shutil
 import json
+from os.path import basename
 
 import xml.etree.ElementTree as ET
 from flipper.utils import *
+from flipper.assets.coprobin import CoproBinary, get_stack_type
+
 
 CUBE_COPRO_PATH = "Projects/STM32WB_Copro_Wireless_Binaries"
 
@@ -13,14 +16,7 @@ MANIFEST_TEMPLATE = {
     "copro": {
         "fus": {"version": {"major": 1, "minor": 2, "sub": 0}, "files": []},
         "radio": {
-            "version": {
-                "type": 3,
-                "major": 1,
-                "minor": 13,
-                "sub": 0,
-                "branch": 0,
-                "release": 5,
-            },
+            "version": {},
             "files": [],
         },
     },
@@ -35,7 +31,7 @@ class Copro:
         self.mcu_copro = None
         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):
             raise Exception(f'"{cube_dir}" doesn\'t exists')
         self.cube_dir = cube_dir
@@ -51,8 +47,8 @@ class Copro:
         if not cube_version or not cube_version.startswith("FW.WB"):
             raise Exception(f"Incorrect Cube package or version info")
         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
 
     def addFile(self, array, filename, **kwargs):
@@ -63,14 +59,32 @@ class Copro:
             {"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):
             raise Exception(f'"{output_dir}" doesn\'t exists')
         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")
         # Form Manifest
         manifest = dict(MANIFEST_TEMPLATE)
         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
         self.addFile(
             manifest["copro"]["fus"]["files"],
@@ -88,8 +102,8 @@ class Copro:
         # BLE Full Stack
         self.addFile(
             manifest["copro"]["radio"]["files"],
-            "stm32wb5x_BLE_Stack_light_fw.bin",
-            address="0x080D7000",
+            stack_file_name,
+            address=f"0x{stack_addr:X}",
         )
         # Save manifest to
         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.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
 import os
 import shutil
 import zlib
 import tarfile
+import math
 
 
 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("-v", dest="version", 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("--stage", dest="stage", required=True)
         self.parser_generate.add_argument(
             "--radio", dest="radiobin", default="", required=False
         )
         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(
-            "--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)
 
     def generate(self):
@@ -49,11 +61,27 @@ class Main(App):
         radiobin_basename = basename(self.args.radiobin)
         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):
             os.makedirs(self.args.directory)
 
         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:
             shutil.copyfile(
                 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("Firmware", dfu_basename)
         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:
             file.writeKey("Radio CRC", self.int2ffhex(self.crc(self.args.radiobin)))
         else:
             file.writeKey("Radio CRC", self.int2ffhex(0))
         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))
 
         return 0
@@ -90,9 +127,34 @@ class Main(App):
         ) as tarball:
             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
     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])
 
     @staticmethod