diff --git a/core/SConscript.bootloader b/core/SConscript.bootloader
index 54c95fbcf5..6b9116e773 100644
--- a/core/SConscript.bootloader
+++ b/core/SConscript.bootloader
@@ -105,11 +105,29 @@ SOURCE_BOOTLOADER = [
     'embed/projects/bootloader/header.S',
     'embed/projects/bootloader/bootui.c',
     'embed/projects/bootloader/main.c',
-    'embed/projects/bootloader/messages.c',
-    'embed/projects/bootloader/protob/messages.pb.c',
+    'embed/projects/bootloader/poll.c',
+    'embed/projects/bootloader/antiglitch.c',
+    'embed/projects/bootloader/workflow/wf_firmware_update.c',
+    'embed/projects/bootloader/workflow/wf_wipe_device.c',
+    'embed/projects/bootloader/workflow/wf_get_features.c',
+    'embed/projects/bootloader/workflow/wf_initialize.c',
+    'embed/projects/bootloader/workflow/wf_ping.c',
+    'embed/projects/bootloader/workflow/wf_bootloader.c',
+    'embed/projects/bootloader/workflow/wf_empty_device.c',
+    'embed/projects/bootloader/workflow/wf_auto_update.c',
+    'embed/projects/bootloader/workflow/wf_host_control.c',
+    'embed/projects/bootloader/wire/codec_v1.c',
+    'embed/projects/bootloader/wire/wire_iface_usb.c',
+    'embed/projects/bootloader/protob/protob.c',
+    'embed/projects/bootloader/protob/pb/messages.pb.c',
     'embed/projects/bootloader/version_check.c',
 ]
 
+if 'optiga' in FEATURES_AVAILABLE:
+    SOURCE_BOOTLOADER += [
+        'embed/projects/bootloader/workflow/wf_unlock_bootloader.c',
+    ]
+
 
 env.Replace(
     CAT='cat',
diff --git a/core/SConscript.bootloader_ci b/core/SConscript.bootloader_ci
index 6de6dbe4e7..3a56880ab4 100644
--- a/core/SConscript.bootloader_ci
+++ b/core/SConscript.bootloader_ci
@@ -99,7 +99,7 @@ SOURCE_BOOTLOADER = [
     'embed/projects/bootloader_ci/bootui.c',
     'embed/projects/bootloader_ci/main.c',
     'embed/projects/bootloader_ci/messages.c',
-    'embed/projects/bootloader_ci/protob/messages.pb.c',
+    'embed/projects/bootloader_ci/protob/pb/messages.pb.c',
     'embed/projects/bootloader_ci/version_check.c',
 ]
 
diff --git a/core/SConscript.bootloader_emu b/core/SConscript.bootloader_emu
index 3a727a3925..c338d3b0d8 100644
--- a/core/SConscript.bootloader_emu
+++ b/core/SConscript.bootloader_emu
@@ -99,10 +99,23 @@ SOURCE_NANOPB = [
 SOURCE_BOOTLOADER = [
     'embed/projects/bootloader/bootui.c',
     'embed/projects/bootloader/main.c',
-    'embed/projects/bootloader/messages.c',
-    'embed/projects/bootloader/emulator.c',
+    'embed/projects/bootloader/poll.c',
+    'embed/projects/bootloader/antiglitch.c',
+    'embed/projects/bootloader/workflow/wf_firmware_update.c',
+    'embed/projects/bootloader/workflow/wf_wipe_device.c',
+    'embed/projects/bootloader/workflow/wf_get_features.c',
+    'embed/projects/bootloader/workflow/wf_initialize.c',
+    'embed/projects/bootloader/workflow/wf_ping.c',
+    'embed/projects/bootloader/workflow/wf_bootloader.c',
+    'embed/projects/bootloader/workflow/wf_empty_device.c',
+    'embed/projects/bootloader/workflow/wf_auto_update.c',
+    'embed/projects/bootloader/workflow/wf_host_control.c',
+    'embed/projects/bootloader/wire/codec_v1.c',
+    'embed/projects/bootloader/wire/wire_iface_usb.c',
+    'embed/projects/bootloader/protob/protob.c',
+    'embed/projects/bootloader/protob/pb/messages.pb.c',
     'embed/projects/bootloader/version_check.c',
-    'embed/projects/bootloader/protob/messages.pb.c',
+    'embed/projects/bootloader/emulator.c',
 ]
 
 SOURCE_TREZORHAL = [
@@ -137,6 +150,11 @@ env = Environment(
 
 FEATURES_AVAILABLE = models.configure_board(TREZOR_MODEL, HW_REVISION, FEATURES_WANTED, env, CPPDEFINES_HAL, SOURCE_UNIX, PATH_HAL)
 
+if 'optiga' in FEATURES_AVAILABLE:
+    SOURCE_BOOTLOADER += [
+        'embed/projects/bootloader/workflow/wf_unlock_bootloader.c',
+    ]
+
 env.Replace(
     CP='cp',
     AS='as',
diff --git a/core/embed/projects/bootloader/.changelog.d/4572.changed b/core/embed/projects/bootloader/.changelog.d/4572.changed
new file mode 100644
index 0000000000..1fc7107384
--- /dev/null
+++ b/core/embed/projects/bootloader/.changelog.d/4572.changed
@@ -0,0 +1 @@
+Major code clean-up and refactoring.
diff --git a/core/embed/projects/bootloader/antiglitch.c b/core/embed/projects/bootloader/antiglitch.c
new file mode 100644
index 0000000000..fa06bb2cb4
--- /dev/null
+++ b/core/embed/projects/bootloader/antiglitch.c
@@ -0,0 +1,35 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_rtl.h>
+
+// anti-glitch
+static volatile secbool continue_to_firmware = secfalse;
+static volatile secbool continue_to_firmware_backup = secfalse;
+
+secbool jump_is_allowed_1(void) { return continue_to_firmware; }
+secbool jump_is_allowed_2(void) { return continue_to_firmware_backup; }
+
+void jump_allow_1(void) { continue_to_firmware = sectrue; }
+void jump_allow_2(void) { continue_to_firmware_backup = sectrue; }
+
+void jump_reset(void) {
+  continue_to_firmware_backup = secfalse;
+  continue_to_firmware = secfalse;
+}
diff --git a/core/embed/projects/bootloader/antiglitch.h b/core/embed/projects/bootloader/antiglitch.h
new file mode 100644
index 0000000000..60cf8a076f
--- /dev/null
+++ b/core/embed/projects/bootloader/antiglitch.h
@@ -0,0 +1,30 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <trezor_types.h>
+
+secbool jump_is_allowed_1(void);
+secbool jump_is_allowed_2(void);
+
+void jump_allow_1(void);
+void jump_allow_2(void);
+
+void jump_reset(void);
diff --git a/core/embed/projects/bootloader/bootui.c b/core/embed/projects/bootloader/bootui.c
index c2aa9b24b9..51ac3da056 100644
--- a/core/embed/projects/bootloader/bootui.c
+++ b/core/embed/projects/bootloader/bootui.c
@@ -22,35 +22,13 @@
 #include <io/display.h>
 #include <io/display_utils.h>
 #include <rtl/mini_printf.h>
+
 #include "bootui.h"
 #include "rust_ui.h"
 #include "version.h"
 
 #define BACKLIGHT_NORMAL 150
 
-#define COLOR_BL_BG COLOR_WHITE  // background
-#define COLOR_BL_FG COLOR_BLACK  // foreground
-
-#ifdef RGB16
-#define COLOR_BL_FAIL RGB16(0xFF, 0x00, 0x00)     // red
-#define COLOR_BL_DONE RGB16(0x00, 0xAE, 0x0B)     // green
-#define COLOR_BL_PROCESS RGB16(0x4A, 0x90, 0xE2)  // blue
-#define COLOR_BL_GRAY RGB16(0x99, 0x99, 0x99)     // gray
-#else
-#define COLOR_BL_FAIL COLOR_BL_FG
-#define COLOR_BL_DONE COLOR_BL_FG
-#define COLOR_BL_PROCESS COLOR_BL_FG
-#define COLOR_BL_GRAY COLOR_BL_FG
-#endif
-
-#if !defined TREZOR_MODEL_T2B1 && !defined TREZOR_MODEL_T3B1
-#define BOOT_WAIT_HEIGHT 25
-#define BOOT_WAIT_Y_TOP (DISPLAY_RESY - BOOT_WAIT_HEIGHT)
-#else
-#define BOOT_WAIT_HEIGHT 12
-#define BOOT_WAIT_Y_TOP (DISPLAY_RESY - BOOT_WAIT_HEIGHT)
-#endif
-
 #define TOIF_LENGTH(ptr) ((*(uint32_t *)((ptr) + 8)) + 12)
 
 // common shared functions
@@ -139,13 +117,15 @@ uint32_t ui_screen_menu(secbool firmware_present) {
   return screen_menu(firmware_present);
 }
 
+void ui_screen_connect(void) { screen_connect(initial_setup); }
+
 // install UI
 
-uint32_t ui_screen_install_confirm(const vendor_header *const vhdr,
-                                   const image_header *const hdr,
-                                   secbool should_keep_seed,
-                                   secbool is_newvendor, secbool is_newinstall,
-                                   int version_cmp) {
+ui_result_t ui_screen_install_confirm(const vendor_header *const vhdr,
+                                      const image_header *const hdr,
+                                      secbool should_keep_seed,
+                                      secbool is_newvendor,
+                                      secbool is_newinstall, int version_cmp) {
   uint8_t fingerprint[32];
   char ver_str[64];
   get_image_fingerprint(hdr, fingerprint);
@@ -171,7 +151,7 @@ void ui_screen_install_progress_upload(int pos) {
 
 // wipe UI
 
-uint32_t ui_screen_wipe_confirm(void) { return screen_wipe_confirm(); }
+ui_result_t ui_screen_wipe_confirm(void) { return screen_wipe_confirm(); }
 
 void ui_screen_wipe(void) { screen_wipe_progress(0, true); }
 
diff --git a/core/embed/projects/bootloader/bootui.h b/core/embed/projects/bootloader/bootui.h
index b3fc7e082a..8dd4c556d8 100644
--- a/core/embed/projects/bootloader/bootui.h
+++ b/core/embed/projects/bootloader/bootui.h
@@ -17,23 +17,32 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef __BOOTUI_H__
-#define __BOOTUI_H__
+#pragma once
 
 #include <trezor_types.h>
 
 #include <util/image.h>
 
+// todo: use bindgen to tie this to rust
 typedef enum {
-  SCREEN_INTRO = 0,
-  SCREEN_MENU = 1,
-  SCREEN_WIPE_CONFIRM = 2,
-  SCREEN_FINGER_PRINT = 3,
-  SCREEN_WAIT_FOR_HOST = 4,
-  SCREEN_WELCOME = 5,
-} screen_t;
+  UI_RESULT_CANCEL = 1,
+  UI_RESULT_CONFIRM = 2,
+} ui_result_t;
 
-// Displays a warning screeen before jumping to the untrusted firmware
+// todo: use bindgen to tie this to rust
+typedef enum {
+  MENU_EXIT = 0xAABBCCDD,
+  MENU_REBOOT = 0x11223344,
+  MENU_WIPE = 0x55667788,
+} menu_result_t;
+
+// todo: use bindgen to tie this to rust
+typedef enum {
+  INTRO_MENU = 1,
+  INTRO_HOST = 2,
+} intro_result_t;
+
+// Displays a warning screen before jumping to the untrusted firmware
 //
 // Shows vendor image, vendor string and firmware version
 // and optional message to the user (see `wait` argument)
@@ -58,16 +67,18 @@ uint32_t ui_screen_intro(const vendor_header* const vhdr,
 
 uint32_t ui_screen_menu(secbool firmware_present);
 
-uint32_t ui_screen_install_confirm(const vendor_header* const vhdr,
-                                   const image_header* const hdr,
-                                   secbool shold_keep_seed,
-                                   secbool is_newvendor, secbool is_newinstall,
-                                   int version_cmp);
+void ui_screen_connect(void);
+
+ui_result_t ui_screen_install_confirm(const vendor_header* const vhdr,
+                                      const image_header* const hdr,
+                                      secbool shold_keep_seed,
+                                      secbool is_newvendor,
+                                      secbool is_newinstall, int version_cmp);
 void ui_screen_install_start();
 void ui_screen_install_progress_erase(int pos, int len);
 void ui_screen_install_progress_upload(int pos);
 
-uint32_t ui_screen_wipe_confirm(void);
+ui_result_t ui_screen_wipe_confirm(void);
 void ui_screen_wipe(void);
 void ui_screen_wipe_progress(int pos, int len);
 
@@ -84,12 +95,3 @@ void ui_screen_boot_stage_1(bool fading);
 #ifdef USE_OPTIGA
 uint32_t ui_screen_unlock_bootloader_confirm(void);
 #endif
-
-// clang-format off
-#define INPUT_CANCEL 0x01        // Cancel button
-#define INPUT_CONFIRM 0x02       // Confirm button
-#define INPUT_LONG_CONFIRM 0x04  // Long Confirm button
-#define INPUT_INFO 0x08          // Info icon
-// clang-format on
-
-#endif
diff --git a/core/embed/projects/bootloader/emulator.c b/core/embed/projects/bootloader/emulator.c
index 8f46770f5a..0c9bbb8df4 100644
--- a/core/embed/projects/bootloader/emulator.c
+++ b/core/embed/projects/bootloader/emulator.c
@@ -8,11 +8,9 @@
 #include <io/display.h>
 #include <sys/bootargs.h>
 #include <sys/bootutils.h>
-#include <sys/systick.h>
 #include <util/flash.h>
 #include <util/flash_otp.h>
 #include "bootui.h"
-#include "rust_ui.h"
 
 #ifdef USE_OPTIGA
 #include <sec/secret.h>
@@ -188,10 +186,12 @@ int main(int argc, char **argv) {
   (void)!flash_otp_write(FLASH_OTP_BLOCK_DEVICE_VARIANT, 0, otp_data,
                          sizeof(otp_data));
 
-  bootloader_main();
-  hal_delay(3000);
-  jump_to_next_stage(0);
+  int exit_code = bootloader_main();
 
+  char msg[64];
+  snprintf(msg, sizeof(msg), "Exit code: %d", exit_code);
+
+  error_shutdown_ex("BOOTLOADER ERROR", msg, "UNEXPECTED EXIT");
   return 0;
 }
 
diff --git a/core/embed/projects/bootloader/emulator.h b/core/embed/projects/bootloader/emulator.h
index 721490e912..e3d362cde4 100644
--- a/core/embed/projects/bootloader/emulator.h
+++ b/core/embed/projects/bootloader/emulator.h
@@ -1,10 +1,26 @@
-#ifndef __EMULATOR_H__
-#define __EMULATOR_H__
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
 
 #include <trezor_types.h>
 
 #undef FIRMWARE_START
 
 extern uint8_t *FIRMWARE_START;
-
-#endif
diff --git a/core/embed/projects/bootloader/main.c b/core/embed/projects/bootloader/main.c
index 3f9c896e58..b34924182e 100644
--- a/core/embed/projects/bootloader/main.c
+++ b/core/embed/projects/bootloader/main.c
@@ -20,27 +20,20 @@
 #include <trezor_model.h>
 #include <trezor_rtl.h>
 
-#include <sys/types.h>
-
-#include <gfx/gfx_bitblt.h>
 #include <io/display.h>
 #include <io/display_utils.h>
-#include <sec/monoctr.h>
 #include <sec/random_delays.h>
 #include <sec/secret.h>
 #include <sys/bootargs.h>
 #include <sys/bootutils.h>
-#include <sys/mpu.h>
 #include <sys/system.h>
 #include <sys/systick.h>
-#include <sys/systimer.h>
-#include <util/flash.h>
+#include <sys/types.h>
 #include <util/flash_otp.h>
 #include <util/flash_utils.h>
 #include <util/image.h>
 #include <util/rsod.h>
 #include <util/unit_properties.h>
-#include "messages.pb.h"
 
 #ifdef USE_PVD
 #include <sys/pvd.h>
@@ -67,27 +60,16 @@
 #include <sys/tamper.h>
 #endif
 
-#include <io/usb.h>
-#include "version.h"
-
+#include "antiglitch.h"
 #include "bootui.h"
-#include "messages.h"
-#include "rust_ui.h"
 #include "version_check.h"
+#include "workflow/workflow.h"
 
 #ifdef TREZOR_EMULATOR
 #include "SDL.h"
 #include "emulator.h"
 #endif
 
-#define USB_IFACE_NUM 0
-
-typedef enum {
-  SHUTDOWN = 0,
-  CONTINUE_TO_FIRMWARE = 0xAABBCCDD,
-  RETURN_TO_MENU = 0x55667788,
-} usb_result_t;
-
 void failed_jump_to_firmware(void);
 
 CONFIDENTIAL volatile secbool dont_optimize_out_true = sectrue;
@@ -154,158 +136,6 @@ static void drivers_deinit(void) {
   display_deinit(DISPLAY_JUMP_BEHAVIOR);
 }
 
-static void usb_init_all(secbool usb21_landing) {
-  usb_dev_info_t dev_info = {
-      .device_class = 0x00,
-      .device_subclass = 0x00,
-      .device_protocol = 0x00,
-      .vendor_id = 0x1209,
-      .product_id = 0x53C0,
-      .release_num = 0x0200,
-      .manufacturer = MODEL_USB_MANUFACTURER,
-      .product = MODEL_USB_PRODUCT,
-      .serial_number = "000000000000000000000000",
-      .interface = "TREZOR Interface",
-      .usb21_enabled = sectrue,
-      .usb21_landing = usb21_landing,
-  };
-
-  static uint8_t rx_buffer[USB_PACKET_SIZE];
-
-  static const usb_webusb_info_t webusb_info = {
-      .iface_num = USB_IFACE_NUM,
-#ifdef TREZOR_EMULATOR
-      .emu_port = 21324,
-#else
-      .ep_in = 0x01,
-      .ep_out = 0x01,
-#endif
-      .subclass = 0,
-      .protocol = 0,
-      .max_packet_len = sizeof(rx_buffer),
-      .rx_buffer = rx_buffer,
-      .polling_interval = 1,
-  };
-
-  ensure(usb_init(&dev_info), NULL);
-
-  ensure(usb_webusb_add(&webusb_info), NULL);
-
-  ensure(usb_start(), NULL);
-}
-
-static usb_result_t bootloader_usb_loop(const vendor_header *const vhdr,
-                                        const image_header *const hdr) {
-  // if both are NULL, we don't have a firmware installed
-  // let's show a webusb landing page in this case
-  usb_init_all((vhdr == NULL && hdr == NULL) ? sectrue : secfalse);
-
-  uint8_t buf[USB_PACKET_SIZE];
-
-  for (;;) {
-#ifdef TREZOR_EMULATOR
-    // Ensures that SDL events are processed. This prevents the emulator from
-    // freezing when the user interacts with the window.
-    SDL_PumpEvents();
-#endif
-    int r = usb_webusb_read_blocking(USB_IFACE_NUM, buf, USB_PACKET_SIZE,
-                                     USB_TIMEOUT);
-    if (r != USB_PACKET_SIZE) {
-      continue;
-    }
-    uint16_t msg_id;
-    uint32_t msg_size;
-    uint32_t response;
-    if (sectrue != msg_parse_header(buf, &msg_id, &msg_size)) {
-      // invalid header -> discard
-      continue;
-    }
-    switch (msg_id) {
-      case MessageType_MessageType_Initialize:
-        process_msg_Initialize(USB_IFACE_NUM, msg_size, buf, vhdr, hdr);
-        break;
-      case MessageType_MessageType_Ping:
-        process_msg_Ping(USB_IFACE_NUM, msg_size, buf);
-        break;
-      case MessageType_MessageType_WipeDevice:
-        response = ui_screen_wipe_confirm();
-        if (INPUT_CANCEL == response) {
-          send_user_abort(USB_IFACE_NUM, "Wipe cancelled");
-          hal_delay(100);
-          usb_deinit();
-          return RETURN_TO_MENU;
-        }
-        ui_screen_wipe();
-        r = process_msg_WipeDevice(USB_IFACE_NUM, msg_size, buf);
-        if (r < 0) {  // error
-          screen_wipe_fail();
-          hal_delay(100);
-          usb_deinit();
-          return SHUTDOWN;
-        } else {  // success
-          screen_wipe_success();
-          hal_delay(100);
-          usb_deinit();
-          return SHUTDOWN;
-        }
-        break;
-      case MessageType_MessageType_FirmwareErase:
-        process_msg_FirmwareErase(USB_IFACE_NUM, msg_size, buf);
-        break;
-      case MessageType_MessageType_FirmwareUpload:
-        r = process_msg_FirmwareUpload(USB_IFACE_NUM, msg_size, buf);
-        if (r < 0 && r != UPLOAD_ERR_USER_ABORT) {  // error, but not user abort
-          if (r == UPLOAD_ERR_BOOTLOADER_LOCKED) {
-            // This function does not return
-            show_install_restricted_screen();
-          } else {
-            ui_screen_fail();
-          }
-          usb_deinit();
-          return SHUTDOWN;
-        } else if (r == UPLOAD_ERR_USER_ABORT) {
-          hal_delay(100);
-          usb_deinit();
-          return RETURN_TO_MENU;
-        } else if (r == 0) {  // last chunk received
-          ui_screen_install_progress_upload(1000);
-          ui_screen_done(4, sectrue);
-          ui_screen_done(3, secfalse);
-          hal_delay(1000);
-          ui_screen_done(2, secfalse);
-          hal_delay(1000);
-          ui_screen_done(1, secfalse);
-          hal_delay(1000);
-          usb_deinit();
-          return CONTINUE_TO_FIRMWARE;
-        }
-        break;
-      case MessageType_MessageType_GetFeatures:
-        process_msg_GetFeatures(USB_IFACE_NUM, msg_size, buf, vhdr, hdr);
-        break;
-#if defined USE_OPTIGA
-      case MessageType_MessageType_UnlockBootloader:
-        response = ui_screen_unlock_bootloader_confirm();
-        if (INPUT_CANCEL == response) {
-          send_user_abort(USB_IFACE_NUM, "Bootloader unlock cancelled");
-          hal_delay(100);
-          usb_deinit();
-          return RETURN_TO_MENU;
-        }
-        process_msg_UnlockBootloader(USB_IFACE_NUM, msg_size, buf);
-        screen_unlock_bootloader_success();
-        hal_delay(100);
-        usb_deinit();
-        return SHUTDOWN;
-        break;
-#endif
-      default:
-        process_msg_unknown(USB_IFACE_NUM, msg_size, buf);
-        break;
-    }
-  }
-}
-
 static secbool check_vendor_header_lock(const vendor_header *const vhdr) {
   uint8_t lock[FLASH_OTP_BLOCK_SIZE];
   ensure(flash_otp_read(FLASH_OTP_BLOCK_VENDOR_HEADER_LOCK, 0, lock,
@@ -400,13 +230,10 @@ void real_jump_to_firmware(void) {
       IMAGE_CODE_ALIGN(FIRMWARE_START + vhdr.hdrlen + IMAGE_HEADER_SIZE));
 }
 
-#ifdef USE_RESET_TO_BOOT
 __attribute__((noreturn)) void jump_to_fw_through_reset(void) {
   display_fade(display_get_backlight(), 0, 200);
-
   reboot_device();
 }
-#endif
 
 #ifndef TREZOR_EMULATOR
 int main(void) {
@@ -551,143 +378,47 @@ int bootloader_main(void) {
   // ... or there is no valid firmware
   if (touched || stay_in_bootloader == sectrue || firmware_present != sectrue ||
       auto_upgrade == sectrue) {
-    screen_t screen;
-    ui_set_initial_setup(true);
+    workflow_result_t result;
+
+    jump_reset();
     if (header_present == sectrue) {
       if (auto_upgrade == sectrue) {
-        screen = SCREEN_WAIT_FOR_HOST;
+        result = workflow_auto_update(&vhdr, hdr);
       } else {
-        ui_set_initial_setup(false);
-        screen = SCREEN_INTRO;
+        result = workflow_bootloader(&vhdr, hdr, firmware_present);
       }
-
     } else {
-      screen = SCREEN_WELCOME;
-
-#ifdef USE_STORAGE_HWKEY
-      secret_bhk_regenerate();
-#endif
-      ensure(erase_storage(NULL), NULL);
-
-      // keep the model screen up for a while
-#ifndef USE_BACKLIGHT
-      hal_delay(1500);
-#else
-      // backlight fading takes some time so the explicit delay here is
-      // shorter
-      hal_delay(1000);
-#endif
+      result = workflow_empty_device();
     }
 
-    while (true) {
-      volatile secbool continue_to_firmware = secfalse;
-      volatile secbool continue_to_firmware_backup = secfalse;
-      uint32_t ui_result = 0;
+    switch (result) {
+      case WF_OK_FIRMWARE_INSTALLED:
+        firmware_present = sectrue;
+        firmware_present_backup = sectrue;
+      case WF_OK_REBOOT_SELECTED:
+        ensure(dont_optimize_out_true *
+                   (jump_is_allowed_1() == jump_is_allowed_2()),
+               NULL);
 
-      switch (screen) {
-        case SCREEN_WELCOME:
-
-          ui_screen_welcome();
-
-          // and start the usb loop
-          switch (bootloader_usb_loop(NULL, NULL)) {
-            case CONTINUE_TO_FIRMWARE:
-              continue_to_firmware = sectrue;
-              continue_to_firmware_backup = sectrue;
-              break;
-            case RETURN_TO_MENU:
-              break;
-            default:
-            case SHUTDOWN:
-              return 1;
-              break;
-          }
-          break;
-
-        case SCREEN_INTRO:
-          ui_result = ui_screen_intro(&vhdr, hdr, firmware_present);
-          if (ui_result == 1) {
-            screen = SCREEN_MENU;
-          }
-          if (ui_result == 2) {
-            screen = SCREEN_WAIT_FOR_HOST;
-          }
-          break;
-        case SCREEN_MENU:
-          ui_result = ui_screen_menu(firmware_present);
-          if (ui_result == 0xAABBCCDD) {  // exit menu
-            screen = SCREEN_INTRO;
-          }
-          if (ui_result == 0x11223344) {  // reboot
-#ifndef USE_HASH_PROCESSOR
-            ui_screen_boot_stage_1(true);
-#endif
-            continue_to_firmware = firmware_present;
-            continue_to_firmware_backup = firmware_present_backup;
-          }
-          if (ui_result == 0x55667788) {  // wipe
-            screen = SCREEN_WIPE_CONFIRM;
-          }
-          break;
-        case SCREEN_WIPE_CONFIRM:
-          ui_result = screen_wipe_confirm();
-          if (ui_result == INPUT_CANCEL) {
-            // canceled
-            screen = SCREEN_MENU;
-          }
-          if (ui_result == INPUT_CONFIRM) {
-            ui_screen_wipe();
-            secbool r = bootloader_WipeDevice();
-            if (r != sectrue) {  // error
-              screen_wipe_fail();
-              return 1;
-            } else {  // success
-              screen_wipe_success();
-              return 1;
-            }
-          }
-          break;
-        case SCREEN_WAIT_FOR_HOST:
-          screen_connect(auto_upgrade == sectrue);
-          switch (bootloader_usb_loop(&vhdr, hdr)) {
-            case CONTINUE_TO_FIRMWARE:
-              continue_to_firmware = sectrue;
-              continue_to_firmware_backup = sectrue;
-              break;
-            case RETURN_TO_MENU:
-              screen = SCREEN_INTRO;
-              break;
-            case SHUTDOWN:
-              return 1;
-              break;
-            default:
-              break;
-          }
-          break;
-        default:
-          break;
-      }
-
-      if (continue_to_firmware != continue_to_firmware_backup) {
+        ensure(dont_optimize_out_true *
+                   (firmware_present == firmware_present_backup),
+               NULL);
+        jump_to_fw_through_reset();
+        break;
+      case WF_OK_DEVICE_WIPED:
+      case WF_OK_BOOTLOADER_UNLOCKED:
+      case WF_ERROR:
+        reboot_or_halt_after_rsod();
+        return 0;
+      case WF_ERROR_FATAL:
+      default: {
         // erase storage if we saw flips randomly flip, most likely due to
         // glitch
-
 #ifdef USE_STORAGE_HWKEY
         secret_bhk_regenerate();
 #endif
         ensure(erase_storage(NULL), NULL);
-      }
-      ensure(dont_optimize_out_true *
-                 (continue_to_firmware == continue_to_firmware_backup),
-             NULL);
-      if (sectrue == continue_to_firmware) {
-#ifdef USE_RESET_TO_BOOT
-        firmware_jump_fn = jump_to_fw_through_reset;
-#else
-        ui_screen_boot_stage_1(true);
-        firmware_jump_fn = real_jump_to_firmware;
-#endif
-        break;
+        error_shutdown("Bootloader fatal error");
       }
     }
   }
@@ -695,16 +426,9 @@ int bootloader_main(void) {
   ensure(dont_optimize_out_true * (firmware_present == firmware_present_backup),
          NULL);
 
-#ifdef USE_RESET_TO_BOOT
-  if (sectrue == firmware_present &&
-      firmware_jump_fn != jump_to_fw_through_reset) {
-    firmware_jump_fn = real_jump_to_firmware;
-  }
-#else
   if (sectrue == firmware_present) {
     firmware_jump_fn = real_jump_to_firmware;
   }
-#endif
 
   firmware_jump_fn();
 
diff --git a/core/embed/projects/bootloader/messages.c b/core/embed/projects/bootloader/messages.c
deleted file mode 100644
index f1371721d8..0000000000
--- a/core/embed/projects/bootloader/messages.c
+++ /dev/null
@@ -1,884 +0,0 @@
-/*
- * This file is part of the Trezor project, https://trezor.io/
- *
- * Copyright (c) SatoshiLabs
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <trezor_model.h>
-#include <trezor_rtl.h>
-
-#include <pb.h>
-#include <pb_decode.h>
-#include <pb_encode.h>
-#include "messages.pb.h"
-
-#include <io/usb.h>
-#include <sec/secret.h>
-#include <sys/bootargs.h>
-#include <util/flash.h>
-#include <util/flash_utils.h>
-#include <util/image.h>
-#include <util/unit_properties.h>
-#include "version.h"
-
-#include "bootui.h"
-#include "messages.h"
-#include "rust_ui.h"
-#include "version_check.h"
-
-#include "memzero.h"
-
-#ifdef TREZOR_EMULATOR
-#include "emulator.h"
-#endif
-
-#if USE_OPTIGA
-#include <sec/secret.h>
-#endif
-
-#define MSG_HEADER1_LEN 9
-#define MSG_HEADER2_LEN 1
-
-secbool msg_parse_header(const uint8_t *buf, uint16_t *msg_id,
-                         uint32_t *msg_size) {
-  if (buf[0] != '?' || buf[1] != '#' || buf[2] != '#') {
-    return secfalse;
-  }
-  *msg_id = (buf[3] << 8) + buf[4];
-  *msg_size = (buf[5] << 24) + (buf[6] << 16) + (buf[7] << 8) + buf[8];
-  return sectrue;
-}
-
-typedef struct {
-  uint8_t iface_num;
-  uint8_t packet_index;
-  uint8_t packet_pos;
-  uint8_t buf[USB_PACKET_SIZE];
-} usb_write_state;
-
-/* we don't use secbool/sectrue/secfalse here as it is a nanopb api */
-static bool _usb_write(pb_ostream_t *stream, const pb_byte_t *buf,
-                       size_t count) {
-  usb_write_state *state = (usb_write_state *)(stream->state);
-
-  size_t written = 0;
-  // while we have data left
-  while (written < count) {
-    size_t remaining = count - written;
-    // if all remaining data fit into our packet
-    if (state->packet_pos + remaining <= USB_PACKET_SIZE) {
-      // append data from buf to state->buf
-      memcpy(state->buf + state->packet_pos, buf + written, remaining);
-      // advance position
-      state->packet_pos += remaining;
-      // and return
-      return true;
-    } else {
-      // append data that fits
-      memcpy(state->buf + state->packet_pos, buf + written,
-             USB_PACKET_SIZE - state->packet_pos);
-      written += USB_PACKET_SIZE - state->packet_pos;
-      // send packet
-      int r = usb_webusb_write_blocking(state->iface_num, state->buf,
-                                        USB_PACKET_SIZE, USB_TIMEOUT);
-      ensure(sectrue * (r == USB_PACKET_SIZE), NULL);
-      // prepare new packet
-      state->packet_index++;
-      memzero(state->buf, USB_PACKET_SIZE);
-      state->buf[0] = '?';
-      state->packet_pos = MSG_HEADER2_LEN;
-    }
-  }
-
-  return true;
-}
-
-static void _usb_write_flush(usb_write_state *state) {
-  // if packet is not filled up completely
-  if (state->packet_pos < USB_PACKET_SIZE) {
-    // pad it with zeroes
-    memzero(state->buf + state->packet_pos,
-            USB_PACKET_SIZE - state->packet_pos);
-  }
-  // send packet
-  int r = usb_webusb_write_blocking(state->iface_num, state->buf,
-                                    USB_PACKET_SIZE, USB_TIMEOUT);
-  ensure(sectrue * (r == USB_PACKET_SIZE), NULL);
-}
-
-static secbool _send_msg(uint8_t iface_num, uint16_t msg_id,
-                         const pb_msgdesc_t *fields, const void *msg) {
-  // determine message size by serializing it into a dummy stream
-  pb_ostream_t sizestream = {.callback = NULL,
-                             .state = NULL,
-                             .max_size = SIZE_MAX,
-                             .bytes_written = 0,
-                             .errmsg = NULL};
-  if (false == pb_encode(&sizestream, fields, msg)) {
-    return secfalse;
-  }
-  const uint32_t msg_size = sizestream.bytes_written;
-
-  usb_write_state state = {
-      .iface_num = iface_num,
-      .packet_index = 0,
-      .packet_pos = MSG_HEADER1_LEN,
-      .buf =
-          {
-              '?',
-              '#',
-              '#',
-              (msg_id >> 8) & 0xFF,
-              msg_id & 0xFF,
-              (msg_size >> 24) & 0xFF,
-              (msg_size >> 16) & 0xFF,
-              (msg_size >> 8) & 0xFF,
-              msg_size & 0xFF,
-          },
-  };
-
-  pb_ostream_t stream = {.callback = &_usb_write,
-                         .state = &state,
-                         .max_size = SIZE_MAX,
-                         .bytes_written = 0,
-                         .errmsg = NULL};
-
-  if (false == pb_encode(&stream, fields, msg)) {
-    return secfalse;
-  }
-
-  _usb_write_flush(&state);
-
-  return sectrue;
-}
-
-#define MSG_SEND_INIT(TYPE) TYPE msg_send = TYPE##_init_default
-#define MSG_SEND_ASSIGN_REQUIRED_VALUE(FIELD, VALUE) \
-  { msg_send.FIELD = VALUE; }
-#define MSG_SEND_ASSIGN_VALUE(FIELD, VALUE) \
-  {                                         \
-    msg_send.has_##FIELD = true;            \
-    msg_send.FIELD = VALUE;                 \
-  }
-#define MSG_SEND_ASSIGN_STRING(FIELD, VALUE)                    \
-  {                                                             \
-    msg_send.has_##FIELD = true;                                \
-    memzero(msg_send.FIELD, sizeof(msg_send.FIELD));            \
-    strncpy(msg_send.FIELD, VALUE, sizeof(msg_send.FIELD) - 1); \
-  }
-#define MSG_SEND_ASSIGN_STRING_LEN(FIELD, VALUE, LEN)                     \
-  {                                                                       \
-    msg_send.has_##FIELD = true;                                          \
-    memzero(msg_send.FIELD, sizeof(msg_send.FIELD));                      \
-    strncpy(msg_send.FIELD, VALUE, MIN(LEN, sizeof(msg_send.FIELD) - 1)); \
-  }
-#define MSG_SEND_ASSIGN_BYTES(FIELD, VALUE, LEN)                  \
-  {                                                               \
-    msg_send.has_##FIELD = true;                                  \
-    memzero(msg_send.FIELD.bytes, sizeof(msg_send.FIELD.bytes));  \
-    memcpy(msg_send.FIELD.bytes, VALUE,                           \
-           MIN(LEN, sizeof(msg_send.FIELD.bytes)));               \
-    msg_send.FIELD.size = MIN(LEN, sizeof(msg_send.FIELD.bytes)); \
-  }
-#define MSG_SEND(TYPE) \
-  _send_msg(iface_num, MessageType_MessageType_##TYPE, TYPE##_fields, &msg_send)
-
-typedef struct {
-  uint8_t iface_num;
-  uint8_t packet_index;
-  uint8_t packet_pos;
-  uint8_t *buf;
-} usb_read_state;
-
-static void _usb_webusb_read_retry(uint8_t iface_num, uint8_t *buf) {
-  for (int retry = 0;; retry++) {
-    int r =
-        usb_webusb_read_blocking(iface_num, buf, USB_PACKET_SIZE, USB_TIMEOUT);
-    if (r != USB_PACKET_SIZE) {  // reading failed
-      if (r == 0 && retry < 10) {
-        // only timeout => let's try again
-        continue;
-      } else {
-        // error
-        error_shutdown_ex("USB ERROR",
-                          "Error reading from USB. Try different USB cable.",
-                          NULL);
-      }
-    }
-    return;  // success
-  }
-}
-
-/* we don't use secbool/sectrue/secfalse here as it is a nanopb api */
-static bool _usb_read(pb_istream_t *stream, uint8_t *buf, size_t count) {
-  usb_read_state *state = (usb_read_state *)(stream->state);
-
-  size_t read = 0;
-  // while we have data left
-  while (read < count) {
-    size_t remaining = count - read;
-    // if all remaining data fit into our packet
-    if (state->packet_pos + remaining <= USB_PACKET_SIZE) {
-      // append data from buf to state->buf
-      memcpy(buf + read, state->buf + state->packet_pos, remaining);
-      // advance position
-      state->packet_pos += remaining;
-      // and return
-      return true;
-    } else {
-      // append data that fits
-      memcpy(buf + read, state->buf + state->packet_pos,
-             USB_PACKET_SIZE - state->packet_pos);
-      read += USB_PACKET_SIZE - state->packet_pos;
-      // read next packet (with retry)
-      _usb_webusb_read_retry(state->iface_num, state->buf);
-      // prepare next packet
-      state->packet_index++;
-      state->packet_pos = MSG_HEADER2_LEN;
-    }
-  }
-
-  return true;
-}
-
-static void _usb_read_flush(usb_read_state *state) { (void)state; }
-
-static secbool _recv_msg(uint8_t iface_num, uint32_t msg_size, uint8_t *buf,
-                         const pb_msgdesc_t *fields, void *msg) {
-  usb_read_state state = {.iface_num = iface_num,
-                          .packet_index = 0,
-                          .packet_pos = MSG_HEADER1_LEN,
-                          .buf = buf};
-
-  pb_istream_t stream = {.callback = &_usb_read,
-                         .state = &state,
-                         .bytes_left = msg_size,
-                         .errmsg = NULL};
-
-  if (false == pb_decode_noinit(&stream, fields, msg)) {
-    return secfalse;
-  }
-
-  _usb_read_flush(&state);
-
-  return sectrue;
-}
-
-#define MSG_RECV_INIT(TYPE) TYPE msg_recv = TYPE##_init_default
-#define MSG_RECV_CALLBACK(FIELD, CALLBACK, ARGUMENT) \
-  {                                                  \
-    msg_recv.FIELD.funcs.decode = &CALLBACK;         \
-    msg_recv.FIELD.arg = (void *)ARGUMENT;           \
-  }
-#define MSG_RECV(TYPE) \
-  _recv_msg(iface_num, msg_size, buf, TYPE##_fields, &msg_recv)
-
-void send_user_abort(uint8_t iface_num, const char *msg) {
-  MSG_SEND_INIT(Failure);
-  MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ActionCancelled);
-  MSG_SEND_ASSIGN_STRING(message, msg);
-  MSG_SEND(Failure);
-}
-
-static void send_msg_features(uint8_t iface_num,
-                              const vendor_header *const vhdr,
-                              const image_header *const hdr) {
-  MSG_SEND_INIT(Features);
-  MSG_SEND_ASSIGN_STRING(vendor, "trezor.io");
-  MSG_SEND_ASSIGN_REQUIRED_VALUE(major_version, VERSION_MAJOR);
-  MSG_SEND_ASSIGN_REQUIRED_VALUE(minor_version, VERSION_MINOR);
-  MSG_SEND_ASSIGN_REQUIRED_VALUE(patch_version, VERSION_PATCH);
-  MSG_SEND_ASSIGN_VALUE(bootloader_mode, true);
-  MSG_SEND_ASSIGN_STRING(model, MODEL_NAME);
-  MSG_SEND_ASSIGN_STRING(internal_model, MODEL_INTERNAL_NAME);
-  if (vhdr && hdr) {
-    MSG_SEND_ASSIGN_VALUE(firmware_present, true);
-    MSG_SEND_ASSIGN_VALUE(fw_major, (hdr->version & 0xFF));
-    MSG_SEND_ASSIGN_VALUE(fw_minor, ((hdr->version >> 8) & 0xFF));
-    MSG_SEND_ASSIGN_VALUE(fw_patch, ((hdr->version >> 16) & 0xFF));
-    MSG_SEND_ASSIGN_STRING_LEN(fw_vendor, vhdr->vstr, vhdr->vstr_len);
-  } else {
-    MSG_SEND_ASSIGN_VALUE(firmware_present, false);
-  }
-  if (unit_properties()->color_is_valid) {
-    MSG_SEND_ASSIGN_VALUE(unit_color, unit_properties()->color);
-  }
-  if (unit_properties()->packaging_is_valid) {
-    MSG_SEND_ASSIGN_VALUE(unit_packaging, unit_properties()->packaging);
-  }
-  if (unit_properties()->btconly_is_valid) {
-    MSG_SEND_ASSIGN_VALUE(unit_btconly, unit_properties()->btconly);
-  }
-
-#if USE_OPTIGA
-  MSG_SEND_ASSIGN_VALUE(bootloader_locked,
-                        (secret_bootloader_locked() == sectrue));
-#endif
-  MSG_SEND(Features);
-}
-
-void process_msg_Initialize(uint8_t iface_num, uint32_t msg_size, uint8_t *buf,
-                            const vendor_header *const vhdr,
-                            const image_header *const hdr) {
-  MSG_RECV_INIT(Initialize);
-  MSG_RECV(Initialize);
-  send_msg_features(iface_num, vhdr, hdr);
-}
-
-void process_msg_GetFeatures(uint8_t iface_num, uint32_t msg_size, uint8_t *buf,
-                             const vendor_header *const vhdr,
-                             const image_header *const hdr) {
-  MSG_RECV_INIT(GetFeatures);
-  MSG_RECV(GetFeatures);
-  send_msg_features(iface_num, vhdr, hdr);
-}
-
-void process_msg_Ping(uint8_t iface_num, uint32_t msg_size, uint8_t *buf) {
-  MSG_RECV_INIT(Ping);
-  MSG_RECV(Ping);
-
-  MSG_SEND_INIT(Success);
-  MSG_SEND_ASSIGN_STRING(message, msg_recv.message);
-  MSG_SEND(Success);
-}
-
-static uint32_t firmware_remaining;
-static uint32_t firmware_block;
-static uint32_t chunk_requested;
-static uint32_t erase_offset;
-
-void process_msg_FirmwareErase(uint8_t iface_num, uint32_t msg_size,
-                               uint8_t *buf) {
-  firmware_remaining = 0;
-  firmware_block = 0;
-  chunk_requested = 0;
-  erase_offset = 0;
-
-  MSG_RECV_INIT(FirmwareErase);
-  MSG_RECV(FirmwareErase);
-
-  firmware_remaining = msg_recv.has_length ? msg_recv.length : 0;
-  if ((firmware_remaining > 0) &&
-      ((firmware_remaining % sizeof(uint32_t)) == 0) &&
-      (firmware_remaining <= FIRMWARE_MAXSIZE)) {
-    // request new firmware
-    chunk_requested = (firmware_remaining > IMAGE_INIT_CHUNK_SIZE)
-                          ? IMAGE_INIT_CHUNK_SIZE
-                          : firmware_remaining;
-    MSG_SEND_INIT(FirmwareRequest);
-    MSG_SEND_ASSIGN_REQUIRED_VALUE(offset, 0);
-    MSG_SEND_ASSIGN_REQUIRED_VALUE(length, chunk_requested);
-    MSG_SEND(FirmwareRequest);
-  } else {
-    // invalid firmware size
-    MSG_SEND_INIT(Failure);
-    MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-    MSG_SEND_ASSIGN_STRING(message, "Wrong firmware size");
-    MSG_SEND(Failure);
-  }
-}
-
-static uint32_t chunk_size = 0;
-
-#ifndef TREZOR_EMULATOR
-__attribute__((section(".buf")))
-#endif
-uint32_t chunk_buffer[IMAGE_CHUNK_SIZE / 4];
-
-#define CHUNK_BUFFER_PTR ((const uint8_t *const)&chunk_buffer)
-
-/* we don't use secbool/sectrue/secfalse here as it is a nanopb api */
-static bool _read_payload(pb_istream_t *stream, const pb_field_t *field,
-                          void **arg) {
-#define BUFSIZE 32768
-
-  size_t offset = (size_t)(*arg);
-
-  if (stream->bytes_left > IMAGE_CHUNK_SIZE) {
-    chunk_size = 0;
-    return false;
-  }
-
-  if (offset == 0) {
-    // clear chunk buffer
-    memset((uint8_t *)&chunk_buffer, 0xFF, IMAGE_CHUNK_SIZE);
-  }
-
-  uint32_t chunk_written = offset;
-  chunk_size = offset + stream->bytes_left;
-
-  while (stream->bytes_left) {
-    // update loader but skip first block
-    if (firmware_block > 0) {
-      ui_screen_install_progress_upload(
-          1000 * (firmware_block * IMAGE_CHUNK_SIZE + chunk_written) /
-          (firmware_block * IMAGE_CHUNK_SIZE + firmware_remaining));
-    }
-    // read data
-    if (!pb_read(
-            stream, (pb_byte_t *)(CHUNK_BUFFER_PTR + chunk_written),
-            (stream->bytes_left > BUFSIZE) ? BUFSIZE : stream->bytes_left)) {
-      chunk_size = 0;
-      return false;
-    }
-    chunk_written += BUFSIZE;
-  }
-
-  return true;
-}
-
-static int version_compare(uint32_t vera, uint32_t verb) {
-  /* Explicit casts so that we control how compiler does the unsigned shift
-   * and correctly then promote uint8_t to int without possibility of
-   * having implementation-defined right shift on negative int
-   * in case compiler promoted the wrong unsinged int
-   */
-  int a, b;
-  a = (uint8_t)vera & 0xFF;
-  b = (uint8_t)verb & 0xFF;
-  if (a != b) return a - b;
-  a = (uint8_t)(vera >> 8) & 0xFF;
-  b = (uint8_t)(verb >> 8) & 0xFF;
-  if (a != b) return a - b;
-  a = (uint8_t)(vera >> 16) & 0xFF;
-  b = (uint8_t)(verb >> 16) & 0xFF;
-  if (a != b) return a - b;
-  a = (uint8_t)(vera >> 24) & 0xFF;
-  b = (uint8_t)(verb >> 24) & 0xFF;
-  return a - b;
-}
-
-static void detect_installation(const vendor_header *current_vhdr,
-                                const image_header *current_hdr,
-                                const vendor_header *const new_vhdr,
-                                const image_header *const new_hdr,
-                                secbool *is_new, secbool *keep_seed,
-                                secbool *is_newvendor, secbool *is_upgrade) {
-  *is_new = secfalse;
-  *keep_seed = secfalse;
-  *is_newvendor = secfalse;
-  *is_upgrade = secfalse;
-  if (sectrue != check_vendor_header_keys(current_vhdr)) {
-    *is_new = sectrue;
-    return;
-  }
-  if (sectrue != check_image_model(current_hdr)) {
-    *is_new = sectrue;
-    return;
-  }
-  if (sectrue != check_firmware_min_version(current_hdr->monotonic)) {
-    *is_new = sectrue;
-    return;
-  }
-  if (sectrue != check_image_header_sig(current_hdr, current_vhdr->vsig_m,
-                                        current_vhdr->vsig_n,
-                                        current_vhdr->vpub)) {
-    *is_new = sectrue;
-    return;
-  }
-  uint8_t hash1[32], hash2[32];
-  vendor_header_hash(new_vhdr, hash1);
-  vendor_header_hash(current_vhdr, hash2);
-  if (0 != memcmp(hash1, hash2, 32)) {
-    *is_newvendor = sectrue;
-    return;
-  }
-  if (version_compare(new_hdr->version, current_hdr->fix_version) < 0) {
-    return;
-  }
-  if (version_compare(new_hdr->version, current_hdr->version) > 0) {
-    *is_upgrade = sectrue;
-  }
-
-  *keep_seed = sectrue;
-}
-
-static int firmware_upload_chunk_retry = FIRMWARE_UPLOAD_CHUNK_RETRY_COUNT;
-static size_t headers_offset = 0;
-static size_t read_offset = 0;
-
-int process_msg_FirmwareUpload(uint8_t iface_num, uint32_t msg_size,
-                               uint8_t *buf) {
-  MSG_RECV_INIT(FirmwareUpload);
-  MSG_RECV_CALLBACK(payload, _read_payload, read_offset);
-  const secbool r = MSG_RECV(FirmwareUpload);
-
-  if (sectrue != r || chunk_size != (chunk_requested + read_offset)) {
-    MSG_SEND_INIT(Failure);
-    MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-    MSG_SEND_ASSIGN_STRING(message, "Invalid chunk size");
-    MSG_SEND(Failure);
-    return UPLOAD_ERR_INVALID_CHUNK_SIZE;
-  }
-
-  static image_header hdr;
-
-  if (firmware_block == 0) {
-    if (headers_offset == 0) {
-      // first block and headers are not yet parsed
-      vendor_header vhdr;
-
-      if (sectrue != read_vendor_header(CHUNK_BUFFER_PTR, &vhdr)) {
-        MSG_SEND_INIT(Failure);
-        MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-        MSG_SEND_ASSIGN_STRING(message, "Invalid vendor header");
-        MSG_SEND(Failure);
-        return UPLOAD_ERR_INVALID_VENDOR_HEADER;
-      }
-
-      if (sectrue != check_vendor_header_model(&vhdr)) {
-        MSG_SEND_INIT(Failure);
-        MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-        MSG_SEND_ASSIGN_STRING(message, "Wrong model");
-        MSG_SEND(Failure);
-        return UPLOAD_ERR_INVALID_VENDOR_HEADER_MODEL;
-      }
-
-      if (sectrue != check_vendor_header_keys(&vhdr)) {
-        MSG_SEND_INIT(Failure);
-        MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-        MSG_SEND_ASSIGN_STRING(message, "Invalid vendor header signature");
-        MSG_SEND(Failure);
-        return UPLOAD_ERR_INVALID_VENDOR_HEADER_SIG;
-      }
-
-      const image_header *received_hdr =
-          read_image_header(CHUNK_BUFFER_PTR + vhdr.hdrlen,
-                            FIRMWARE_IMAGE_MAGIC, FIRMWARE_MAXSIZE);
-
-      if (received_hdr !=
-          (const image_header *)(CHUNK_BUFFER_PTR + vhdr.hdrlen)) {
-        MSG_SEND_INIT(Failure);
-        MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-        MSG_SEND_ASSIGN_STRING(message, "Invalid firmware header");
-        MSG_SEND(Failure);
-        return UPLOAD_ERR_INVALID_IMAGE_HEADER;
-      }
-
-      if (sectrue != check_image_model(received_hdr)) {
-        MSG_SEND_INIT(Failure);
-        MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-        MSG_SEND_ASSIGN_STRING(message, "Wrong firmware model");
-        MSG_SEND(Failure);
-        return UPLOAD_ERR_INVALID_IMAGE_MODEL;
-      }
-
-      if (sectrue != check_image_header_sig(received_hdr, vhdr.vsig_m,
-                                            vhdr.vsig_n, vhdr.vpub)) {
-        MSG_SEND_INIT(Failure);
-        MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-        MSG_SEND_ASSIGN_STRING(message, "Invalid firmware signature");
-        MSG_SEND(Failure);
-        return UPLOAD_ERR_INVALID_IMAGE_HEADER_SIG;
-      }
-
-      if (sectrue != check_firmware_min_version(received_hdr->monotonic)) {
-        MSG_SEND_INIT(Failure);
-        MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-        MSG_SEND_ASSIGN_STRING(message, "Cannot downgrade to this version");
-        MSG_SEND(Failure);
-        return UPLOAD_ERR_INVALID_IMAGE_HEADER_VERSION;
-      }
-
-      memcpy(&hdr, received_hdr, sizeof(hdr));
-
-      vendor_header current_vhdr;
-
-      secbool is_new = secfalse;
-
-      if (sectrue !=
-          read_vendor_header((const uint8_t *)FIRMWARE_START, &current_vhdr)) {
-        is_new = sectrue;
-      }
-
-      const image_header *current_hdr = NULL;
-
-      if (is_new == secfalse) {
-        current_hdr = read_image_header(
-            (const uint8_t *)FIRMWARE_START + current_vhdr.hdrlen,
-            FIRMWARE_IMAGE_MAGIC, FIRMWARE_MAXSIZE);
-
-        if (current_hdr !=
-            (const image_header *)(FIRMWARE_START + current_vhdr.hdrlen)) {
-          is_new = sectrue;
-        }
-      }
-
-      secbool should_keep_seed = secfalse;
-      secbool is_newvendor = secfalse;
-      secbool is_upgrade = secfalse;
-      if (is_new == secfalse) {
-        detect_installation(&current_vhdr, current_hdr, &vhdr, &hdr, &is_new,
-                            &should_keep_seed, &is_newvendor, &is_upgrade);
-      }
-
-      secbool is_ilu = secfalse;  // interaction-less update
-
-      if (bootargs_get_command() == BOOT_COMMAND_INSTALL_UPGRADE) {
-        IMAGE_HASH_CTX ctx;
-        uint8_t hash[IMAGE_HASH_DIGEST_LENGTH];
-        IMAGE_HASH_INIT(&ctx);
-        IMAGE_HASH_UPDATE(&ctx, CHUNK_BUFFER_PTR,
-                          vhdr.hdrlen + received_hdr->hdrlen);
-        IMAGE_HASH_FINAL(&ctx, hash);
-
-        // the firmware must be the same as confirmed by the user
-        boot_args_t args = {0};
-        bootargs_get_args(&args);
-
-        if (memcmp(args.hash, hash, sizeof(hash)) != 0) {
-          MSG_SEND_INIT(Failure);
-          MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-          MSG_SEND_ASSIGN_STRING(message, "Firmware mismatch");
-          MSG_SEND(Failure);
-          return UPLOAD_ERR_FIRMWARE_MISMATCH;
-        }
-
-        // the firmware must be from the same vendor
-        // the firmware must be newer
-        if (is_upgrade != sectrue || is_newvendor != secfalse) {
-          MSG_SEND_INIT(Failure);
-          MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-          MSG_SEND_ASSIGN_STRING(message, "Not a firmware upgrade");
-          MSG_SEND(Failure);
-          return UPLOAD_ERR_NOT_FIRMWARE_UPGRADE;
-        }
-
-        if ((vhdr.vtrust & VTRUST_NO_WARNING) != VTRUST_NO_WARNING) {
-          MSG_SEND_INIT(Failure);
-          MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-          MSG_SEND_ASSIGN_STRING(message, "Not a full-trust image");
-          MSG_SEND(Failure);
-          return UPLOAD_ERR_NOT_FULLTRUST_IMAGE;
-        }
-
-        // upload the firmware without confirmation
-        is_ilu = sectrue;
-      }
-
-#if defined USE_OPTIGA
-      if (secfalse != secret_optiga_present() &&
-          ((vhdr.vtrust & VTRUST_SECRET_MASK) != VTRUST_SECRET_ALLOW)) {
-        MSG_SEND_INIT(Failure);
-        MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-        MSG_SEND_ASSIGN_STRING(message, "Install restricted");
-        MSG_SEND(Failure);
-        return UPLOAD_ERR_BOOTLOADER_LOCKED;
-      }
-#endif
-
-      uint32_t response = INPUT_CANCEL;
-      if (((vhdr.vtrust & VTRUST_NO_WARNING) == VTRUST_NO_WARNING) &&
-          (sectrue == is_new || sectrue == is_ilu)) {
-        // new installation or interaction less updated - auto confirm
-        // only allowed for full-trust images
-        response = INPUT_CONFIRM;
-      } else {
-        if (sectrue != is_new) {
-          int version_cmp = version_compare(hdr.version, current_hdr->version);
-          response = ui_screen_install_confirm(
-              &vhdr, &hdr, should_keep_seed, is_newvendor, is_new, version_cmp);
-        } else {
-          response = ui_screen_install_confirm(&vhdr, &hdr, sectrue,
-                                               is_newvendor, is_new, 0);
-        }
-      }
-
-      if (INPUT_CANCEL == response) {
-        send_user_abort(iface_num, "Firmware install cancelled");
-        return UPLOAD_ERR_USER_ABORT;
-      }
-
-      ui_screen_install_start();
-
-      // if firmware is not upgrade, erase storage
-      if (sectrue != should_keep_seed) {
-#ifdef USE_STORAGE_HWKEY
-        secret_bhk_regenerate();
-#endif
-        ensure(erase_storage(NULL), NULL);
-      }
-
-      headers_offset = IMAGE_HEADER_SIZE + vhdr.hdrlen;
-      read_offset = IMAGE_INIT_CHUNK_SIZE;
-
-      // request the rest of the first chunk
-      MSG_SEND_INIT(FirmwareRequest);
-      uint32_t chunk_limit = (firmware_remaining > IMAGE_CHUNK_SIZE)
-                                 ? IMAGE_CHUNK_SIZE
-                                 : firmware_remaining;
-      chunk_requested = chunk_limit - read_offset;
-      MSG_SEND_ASSIGN_REQUIRED_VALUE(offset, read_offset);
-      MSG_SEND_ASSIGN_REQUIRED_VALUE(length, chunk_requested);
-      MSG_SEND(FirmwareRequest);
-
-      firmware_remaining -= read_offset;
-      return (int)firmware_remaining;
-    } else {
-      // first block with the headers parsed -> the first chunk is now complete
-      read_offset = 0;
-    }
-  }
-
-  // should not happen, but double-check
-  if (flash_area_get_address(&FIRMWARE_AREA, firmware_block * IMAGE_CHUNK_SIZE,
-                             0) == NULL) {
-    MSG_SEND_INIT(Failure);
-    MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-    MSG_SEND_ASSIGN_STRING(message, "Firmware too big");
-    MSG_SEND(Failure);
-    return UPLOAD_ERR_FIRMWARE_TOO_BIG;
-  }
-
-  if (sectrue != check_single_hash(hdr.hashes + firmware_block * 32,
-                                   CHUNK_BUFFER_PTR + headers_offset,
-                                   chunk_size - headers_offset)) {
-    if (firmware_upload_chunk_retry > 0) {
-      --firmware_upload_chunk_retry;
-      MSG_SEND_INIT(FirmwareRequest);
-      MSG_SEND_ASSIGN_REQUIRED_VALUE(offset, firmware_block * IMAGE_CHUNK_SIZE);
-      MSG_SEND_ASSIGN_REQUIRED_VALUE(length, chunk_requested);
-      MSG_SEND(FirmwareRequest);
-      return (int)firmware_remaining;
-    }
-
-    MSG_SEND_INIT(Failure);
-    MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-    MSG_SEND_ASSIGN_STRING(message, "Invalid chunk hash");
-    MSG_SEND(Failure);
-    return UPLOAD_ERR_INVALID_CHUNK_HASH;
-  }
-
-  // buffer with the received data
-  const uint32_t *src = (const uint32_t *)CHUNK_BUFFER_PTR;
-  // number of received bytes
-  uint32_t bytes_remaining = chunk_size;
-  // offset into the FIRMWARE_AREA part of the flash
-  uint32_t write_offset = firmware_block * IMAGE_CHUNK_SIZE;
-
-  ensure((chunk_size % FLASH_BLOCK_SIZE == 0) * sectrue, NULL);
-
-  while (bytes_remaining > 0) {
-    // erase flash before writing
-    uint32_t bytes_erased = 0;
-
-    if (write_offset >= erase_offset) {
-      // erase the next flash section
-      ensure(
-          flash_area_erase_partial(&FIRMWARE_AREA, erase_offset, &bytes_erased),
-          NULL);
-      erase_offset += bytes_erased;
-    } else {
-      // some erased space left from the previous round => use it
-      bytes_erased = erase_offset - write_offset;
-    }
-
-    // write the received data
-    uint32_t bytes_to_write = MIN(bytes_erased, bytes_remaining);
-    ensure(flash_unlock_write(), NULL);
-    ensure(flash_area_write_data(&FIRMWARE_AREA, write_offset, src,
-                                 bytes_to_write),
-           NULL);
-    ensure(flash_lock_write(), NULL);
-
-    write_offset += bytes_to_write;
-    src += bytes_to_write / sizeof(uint32_t);
-
-    bytes_remaining -= bytes_to_write;
-  }
-
-  firmware_remaining -= chunk_requested;
-
-  if (firmware_remaining == 0) {
-    // erase the rest (unused part) of the FIRMWARE_AREA
-    uint32_t bytes_erased = 0;
-    do {
-      ensure(
-          flash_area_erase_partial(&FIRMWARE_AREA, erase_offset, &bytes_erased),
-          NULL);
-      erase_offset += bytes_erased;
-    } while (bytes_erased > 0);
-  }
-
-  headers_offset = 0;
-  firmware_block++;
-  firmware_upload_chunk_retry = FIRMWARE_UPLOAD_CHUNK_RETRY_COUNT;
-
-  if (firmware_remaining > 0) {
-    chunk_requested = (firmware_remaining > IMAGE_CHUNK_SIZE)
-                          ? IMAGE_CHUNK_SIZE
-                          : firmware_remaining;
-    MSG_SEND_INIT(FirmwareRequest);
-    MSG_SEND_ASSIGN_REQUIRED_VALUE(offset, firmware_block * IMAGE_CHUNK_SIZE);
-    MSG_SEND_ASSIGN_REQUIRED_VALUE(length, chunk_requested);
-    MSG_SEND(FirmwareRequest);
-  } else {
-    MSG_SEND_INIT(Success);
-    MSG_SEND(Success);
-  }
-  return (int)firmware_remaining;
-}
-
-secbool bootloader_WipeDevice(void) {
-  return erase_device(ui_screen_wipe_progress);
-}
-
-int process_msg_WipeDevice(uint8_t iface_num, uint32_t msg_size, uint8_t *buf) {
-  secbool wipe_result = bootloader_WipeDevice();
-  if (sectrue != wipe_result) {
-    MSG_SEND_INIT(Failure);
-    MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ProcessError);
-    MSG_SEND_ASSIGN_STRING(message, "Could not erase flash");
-    MSG_SEND(Failure);
-    return WIPE_ERR_CANNOT_ERASE;
-  } else {
-    MSG_SEND_INIT(Success);
-    MSG_SEND(Success);
-    return WIPE_OK;
-  }
-}
-
-void process_msg_unknown(uint8_t iface_num, uint32_t msg_size, uint8_t *buf) {
-  // consume remaining message
-  int remaining_chunks = 0;
-
-  if (msg_size > (USB_PACKET_SIZE - MSG_HEADER1_LEN)) {
-    // calculate how many blocks need to be read to drain the message (rounded
-    // up to not leave any behind)
-    remaining_chunks = (msg_size - (USB_PACKET_SIZE - MSG_HEADER1_LEN) +
-                        ((USB_PACKET_SIZE - MSG_HEADER2_LEN) - 1)) /
-                       (USB_PACKET_SIZE - MSG_HEADER2_LEN);
-  }
-
-  for (int i = 0; i < remaining_chunks; i++) {
-    // read next packet (with retry)
-    _usb_webusb_read_retry(iface_num, buf);
-  }
-
-  MSG_SEND_INIT(Failure);
-  MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_UnexpectedMessage);
-  MSG_SEND_ASSIGN_STRING(message, "Unexpected message");
-  MSG_SEND(Failure);
-}
-
-#if defined USE_OPTIGA
-void process_msg_UnlockBootloader(uint8_t iface_num, uint32_t msg_size,
-                                  uint8_t *buf) {
-  secret_optiga_erase();
-  MSG_SEND_INIT(Success);
-  MSG_SEND(Success);
-}
-#endif
diff --git a/core/embed/projects/bootloader/messages.h b/core/embed/projects/bootloader/messages.h
deleted file mode 100644
index 5623ff3b5f..0000000000
--- a/core/embed/projects/bootloader/messages.h
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * This file is part of the Trezor project, https://trezor.io/
- *
- * Copyright (c) SatoshiLabs
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#ifndef __MESSAGES_H__
-#define __MESSAGES_H__
-
-#include <trezor_types.h>
-
-#include <util/image.h>
-
-#define USB_TIMEOUT 500
-#define USB_PACKET_SIZE 64
-
-#define FIRMWARE_UPLOAD_CHUNK_RETRY_COUNT 2
-
-enum {
-  UPLOAD_OK = 0,
-  UPLOAD_ERR_INVALID_CHUNK_SIZE = -1,
-  UPLOAD_ERR_INVALID_VENDOR_HEADER = -2,
-  UPLOAD_ERR_INVALID_VENDOR_HEADER_SIG = -3,
-  UPLOAD_ERR_INVALID_VENDOR_HEADER_MODEL = -15,
-  UPLOAD_ERR_INVALID_IMAGE_HEADER = -4,
-  UPLOAD_ERR_INVALID_IMAGE_MODEL = -5,
-  UPLOAD_ERR_INVALID_IMAGE_HEADER_SIG = -6,
-  UPLOAD_ERR_INVALID_IMAGE_HEADER_VERSION = -16,
-  UPLOAD_ERR_USER_ABORT = -7,
-  UPLOAD_ERR_FIRMWARE_TOO_BIG = -8,
-  UPLOAD_ERR_INVALID_CHUNK_HASH = -9,
-  UPLOAD_ERR_BOOTLOADER_LOCKED = -10,
-  UPLOAD_ERR_FIRMWARE_MISMATCH = -11,
-  UPLOAD_ERR_NOT_FIRMWARE_UPGRADE = -12,
-  UPLOAD_ERR_NOT_FULLTRUST_IMAGE = -13,
-  UPLOAD_ERR_INVALID_CHUNK_PADDING = -14,
-};
-
-enum {
-  WIPE_OK = 0,
-  WIPE_ERR_CANNOT_ERASE = -1,
-};
-
-secbool msg_parse_header(const uint8_t *buf, uint16_t *msg_id,
-                         uint32_t *msg_size);
-
-void send_user_abort(uint8_t iface_num, const char *msg);
-
-void process_msg_Initialize(uint8_t iface_num, uint32_t msg_size, uint8_t *buf,
-                            const vendor_header *const vhdr,
-                            const image_header *const hdr);
-void process_msg_GetFeatures(uint8_t iface_num, uint32_t msg_size, uint8_t *buf,
-                             const vendor_header *const vhdr,
-                             const image_header *const hdr);
-void process_msg_Ping(uint8_t iface_num, uint32_t msg_size, uint8_t *buf);
-void process_msg_FirmwareErase(uint8_t iface_num, uint32_t msg_size,
-                               uint8_t *buf);
-int process_msg_FirmwareUpload(uint8_t iface_num, uint32_t msg_size,
-                               uint8_t *buf);
-int process_msg_WipeDevice(uint8_t iface_num, uint32_t msg_size, uint8_t *buf);
-void process_msg_unknown(uint8_t iface_num, uint32_t msg_size, uint8_t *buf);
-
-#ifdef USE_OPTIGA
-void process_msg_UnlockBootloader(uint8_t iface_num, uint32_t msg_size,
-                                  uint8_t *buf);
-#endif
-
-secbool bootloader_WipeDevice(void);
-
-#endif
diff --git a/core/embed/projects/bootloader/poll.c b/core/embed/projects/bootloader/poll.c
new file mode 100644
index 0000000000..6d6bc70e73
--- /dev/null
+++ b/core/embed/projects/bootloader/poll.c
@@ -0,0 +1,62 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_bsp.h>
+
+#include "poll.h"
+
+#include <io/usb.h>
+#include <sys/systick.h>
+
+#ifdef TREZOR_EMULATOR
+#include "SDL.h"
+#endif
+
+uint8_t poll_events(const uint16_t* ifaces, size_t ifaces_num,
+                    poll_event_t* event, uint32_t timeout_ms) {
+  uint32_t deadline = ticks_timeout(timeout_ms);
+
+  while (!ticks_expired(deadline)) {
+#ifdef TREZOR_EMULATOR
+    // Ensures that SDL events are processed. This prevents the emulator from
+    // freezing when the user interacts with the window.
+    SDL_PumpEvents();
+#endif
+
+    for (size_t i = 0; i < ifaces_num; i++) {
+      uint8_t iface_num = ifaces[i] & 0xFF;
+      if (iface_num < IFACE_USB_MAX) {
+        if ((ifaces[i] & MODE_READ) == MODE_READ) {
+          // check if USB can read
+          if (sectrue == usb_webusb_can_read(iface_num)) {
+            event->type = EVENT_USB_CAN_READ;
+            return iface_num;
+          }
+        }
+      }
+    }
+
+#ifndef TREZOR_EMULATOR
+    __WFI();
+#endif
+  }
+
+  event->type = EVENT_NONE;
+  return 0;
+}
diff --git a/core/embed/projects/bootloader/poll.h b/core/embed/projects/bootloader/poll.h
new file mode 100644
index 0000000000..61d7d4e79c
--- /dev/null
+++ b/core/embed/projects/bootloader/poll.h
@@ -0,0 +1,39 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <trezor_types.h>
+
+#define IFACE_USB_MAX (15)  // 0-15 reserved for USB
+
+#define MODE_READ 0x0000
+#define MODE_WRITE 0x0100
+
+typedef enum {
+  EVENT_NONE = 0,
+  EVENT_USB_CAN_READ = 0x01,
+} poll_event_type_t;
+
+typedef struct {
+  poll_event_type_t type;
+} poll_event_t;
+
+uint8_t poll_events(const uint16_t* ifaces, size_t ifaces_num,
+                    poll_event_t* event, uint32_t timeout_ms);
diff --git a/core/embed/projects/bootloader/protob/.gitignore b/core/embed/projects/bootloader/protob/pb/.gitignore
similarity index 100%
rename from core/embed/projects/bootloader/protob/.gitignore
rename to core/embed/projects/bootloader/protob/pb/.gitignore
diff --git a/core/embed/projects/bootloader/protob/Makefile b/core/embed/projects/bootloader/protob/pb/Makefile
similarity index 100%
rename from core/embed/projects/bootloader/protob/Makefile
rename to core/embed/projects/bootloader/protob/pb/Makefile
diff --git a/core/embed/projects/bootloader/protob/messages.options b/core/embed/projects/bootloader/protob/pb/messages.options
similarity index 100%
rename from core/embed/projects/bootloader/protob/messages.options
rename to core/embed/projects/bootloader/protob/pb/messages.options
diff --git a/core/embed/projects/bootloader/protob/messages.pb.c b/core/embed/projects/bootloader/protob/pb/messages.pb.c
similarity index 100%
rename from core/embed/projects/bootloader/protob/messages.pb.c
rename to core/embed/projects/bootloader/protob/pb/messages.pb.c
diff --git a/core/embed/projects/bootloader/protob/messages.pb.h b/core/embed/projects/bootloader/protob/pb/messages.pb.h
similarity index 100%
rename from core/embed/projects/bootloader/protob/messages.pb.h
rename to core/embed/projects/bootloader/protob/pb/messages.pb.h
diff --git a/core/embed/projects/bootloader/protob/messages.proto b/core/embed/projects/bootloader/protob/pb/messages.proto
similarity index 100%
rename from core/embed/projects/bootloader/protob/messages.proto
rename to core/embed/projects/bootloader/protob/pb/messages.proto
diff --git a/core/embed/projects/bootloader/protob/protob.c b/core/embed/projects/bootloader/protob/protob.c
new file mode 100644
index 0000000000..c0ee01d89d
--- /dev/null
+++ b/core/embed/projects/bootloader/protob/protob.c
@@ -0,0 +1,245 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <pb.h>
+#include <pb_decode.h>
+
+#include <util/image.h>
+#include <util/unit_properties.h>
+
+#if USE_OPTIGA
+#include <sec/secret.h>
+#endif
+
+#include "memzero.h"
+#include "pb/messages.pb.h"
+#include "protob.h"
+#include "version.h"
+#include "wire/codec_v1.h"
+
+#define MSG_SEND_INIT(TYPE) TYPE msg_send = TYPE##_init_default
+#define MSG_SEND_ASSIGN_REQUIRED_VALUE(FIELD, VALUE) \
+  { msg_send.FIELD = VALUE; }
+#define MSG_SEND_ASSIGN_VALUE(FIELD, VALUE) \
+  {                                         \
+    msg_send.has_##FIELD = true;            \
+    msg_send.FIELD = VALUE;                 \
+  }
+#define MSG_SEND_ASSIGN_STRING(FIELD, VALUE)                    \
+  {                                                             \
+    msg_send.has_##FIELD = true;                                \
+    memzero(msg_send.FIELD, sizeof(msg_send.FIELD));            \
+    strncpy(msg_send.FIELD, VALUE, sizeof(msg_send.FIELD) - 1); \
+  }
+#define MSG_SEND_ASSIGN_STRING_LEN(FIELD, VALUE, LEN)                     \
+  {                                                                       \
+    msg_send.has_##FIELD = true;                                          \
+    memzero(msg_send.FIELD, sizeof(msg_send.FIELD));                      \
+    strncpy(msg_send.FIELD, VALUE, MIN(LEN, sizeof(msg_send.FIELD) - 1)); \
+  }
+#define MSG_SEND_ASSIGN_BYTES(FIELD, VALUE, LEN)                  \
+  {                                                               \
+    msg_send.has_##FIELD = true;                                  \
+    memzero(msg_send.FIELD.bytes, sizeof(msg_send.FIELD.bytes));  \
+    memcpy(msg_send.FIELD.bytes, VALUE,                           \
+           MIN(LEN, sizeof(msg_send.FIELD.bytes)));               \
+    msg_send.FIELD.size = MIN(LEN, sizeof(msg_send.FIELD.bytes)); \
+  }
+#define MSG_SEND(TYPE)                                                       \
+  codec_send_msg(iface->wire, MessageType_MessageType_##TYPE, TYPE##_fields, \
+                 &msg_send)
+
+#define MSG_RECV_INIT(TYPE) TYPE msg_recv = TYPE##_init_default
+#define MSG_RECV_CALLBACK(FIELD, CALLBACK, ARGUMENT) \
+  {                                                  \
+    msg_recv.FIELD.funcs.decode = &CALLBACK;         \
+    msg_recv.FIELD.arg = (void *)ARGUMENT;           \
+  }
+#define MSG_RECV(TYPE)                                                        \
+  codec_recv_message(iface->wire, iface->msg_size, iface->buf, TYPE##_fields, \
+                     &msg_recv)
+
+secbool send_user_abort(protob_io_t *iface, const char *msg) {
+  MSG_SEND_INIT(Failure);
+  MSG_SEND_ASSIGN_VALUE(code, FailureType_Failure_ActionCancelled);
+  MSG_SEND_ASSIGN_STRING(message, msg);
+  return MSG_SEND(Failure);
+}
+
+secbool send_msg_failure(protob_io_t *iface, FailureType type,
+                         const char *msg) {
+  MSG_SEND_INIT(Failure);
+  MSG_SEND_ASSIGN_VALUE(code, type);
+  MSG_SEND_ASSIGN_STRING(message, msg);
+  return MSG_SEND(Failure);
+}
+
+secbool send_msg_success(protob_io_t *iface, const char *msg) {
+  MSG_SEND_INIT(Success);
+  if (msg != NULL) {
+    MSG_SEND_ASSIGN_STRING(message, msg);
+  }
+  return MSG_SEND(Success);
+}
+
+secbool send_msg_features(protob_io_t *iface, const vendor_header *const vhdr,
+                          const image_header *const hdr) {
+  MSG_SEND_INIT(Features);
+  MSG_SEND_ASSIGN_STRING(vendor, "trezor.io");
+  MSG_SEND_ASSIGN_REQUIRED_VALUE(major_version, VERSION_MAJOR);
+  MSG_SEND_ASSIGN_REQUIRED_VALUE(minor_version, VERSION_MINOR);
+  MSG_SEND_ASSIGN_REQUIRED_VALUE(patch_version, VERSION_PATCH);
+  MSG_SEND_ASSIGN_VALUE(bootloader_mode, true);
+  MSG_SEND_ASSIGN_STRING(model, MODEL_NAME);
+  MSG_SEND_ASSIGN_STRING(internal_model, MODEL_INTERNAL_NAME);
+  if (vhdr && hdr) {
+    MSG_SEND_ASSIGN_VALUE(firmware_present, true);
+    MSG_SEND_ASSIGN_VALUE(fw_major, (hdr->version & 0xFF));
+    MSG_SEND_ASSIGN_VALUE(fw_minor, ((hdr->version >> 8) & 0xFF));
+    MSG_SEND_ASSIGN_VALUE(fw_patch, ((hdr->version >> 16) & 0xFF));
+    MSG_SEND_ASSIGN_STRING_LEN(fw_vendor, vhdr->vstr, vhdr->vstr_len);
+  } else {
+    MSG_SEND_ASSIGN_VALUE(firmware_present, false);
+  }
+  if (unit_properties()->color_is_valid) {
+    MSG_SEND_ASSIGN_VALUE(unit_color, unit_properties()->color);
+  }
+  if (unit_properties()->packaging_is_valid) {
+    MSG_SEND_ASSIGN_VALUE(unit_packaging, unit_properties()->packaging);
+  }
+  if (unit_properties()->btconly_is_valid) {
+    MSG_SEND_ASSIGN_VALUE(unit_btconly, unit_properties()->btconly);
+  }
+
+#if USE_OPTIGA
+  MSG_SEND_ASSIGN_VALUE(bootloader_locked,
+                        (secret_bootloader_locked() == sectrue));
+#endif
+  return MSG_SEND(Features);
+}
+
+secbool recv_msg_initialize(protob_io_t *iface, Initialize *msg) {
+  MSG_RECV_INIT(Initialize);
+  secbool result = MSG_RECV(Initialize);
+  memcpy(msg, &msg_recv, sizeof(Initialize));
+  return result;
+}
+
+secbool recv_msg_get_features(protob_io_t *iface, GetFeatures *msg) {
+  MSG_RECV_INIT(GetFeatures);
+  secbool result = MSG_RECV(GetFeatures);
+  memcpy(msg, &msg_recv, sizeof(GetFeatures));
+  return result;
+}
+
+secbool recv_msg_ping(protob_io_t *iface, Ping *msg) {
+  MSG_RECV_INIT(Ping);
+  secbool result = MSG_RECV(Ping);
+  memcpy(msg, &msg_recv, sizeof(Ping));
+  return result;
+}
+
+secbool recv_msg_firmware_erase(protob_io_t *iface, FirmwareErase *msg) {
+  MSG_RECV_INIT(FirmwareErase);
+  secbool result = MSG_RECV(FirmwareErase);
+  memcpy(msg, &msg_recv, sizeof(FirmwareErase));
+  return result;
+}
+
+secbool send_msg_request_firmware(protob_io_t *iface, uint32_t offset,
+                                  uint32_t length) {
+  MSG_SEND_INIT(FirmwareRequest);
+  MSG_SEND_ASSIGN_REQUIRED_VALUE(offset, offset);
+  MSG_SEND_ASSIGN_REQUIRED_VALUE(length, length);
+  return MSG_SEND(FirmwareRequest);
+}
+
+typedef struct {
+  void (*cb)(size_t len, void *ctx);
+  void *ctx;
+  uint8_t *buffer;
+  size_t buffer_size;
+} payload_ctx_t;
+
+/* we don't use secbool/sectrue/secfalse here as it is a nanopb api */
+static bool read_payload(pb_istream_t *stream, const pb_field_t *field,
+                         void **arg) {
+  payload_ctx_t *payload_ctx = (payload_ctx_t *)*arg;
+#define BUFSIZE 32768
+
+  if (stream->bytes_left > payload_ctx->buffer_size) {
+    return false;
+  }
+
+  uint32_t bytes_written = 0;
+
+  while (stream->bytes_left) {
+    uint32_t received =
+        stream->bytes_left > BUFSIZE ? BUFSIZE : stream->bytes_left;
+
+    // notify of received data
+    payload_ctx->cb(received, payload_ctx->ctx);
+
+    // read data
+    if (!pb_read(stream, (pb_byte_t *)(payload_ctx->buffer + bytes_written),
+                 (received))) {
+      return false;
+    }
+    bytes_written += received;
+  }
+
+  return true;
+}
+
+secbool recv_msg_firmware_upload(protob_io_t *iface, FirmwareUpload *msg,
+                                 void *ctx,
+                                 void (*data_cb)(size_t len, void *ctx),
+                                 uint8_t *buffer, size_t buffer_size) {
+  payload_ctx_t payload_ctx = {
+      .cb = data_cb, .ctx = ctx, .buffer = buffer, .buffer_size = buffer_size};
+
+  MSG_RECV_INIT(FirmwareUpload);
+  MSG_RECV_CALLBACK(payload, read_payload, &payload_ctx);
+  secbool result = MSG_RECV(FirmwareUpload);
+  memcpy(msg, &msg_recv, sizeof(FirmwareUpload));
+  return result;
+}
+
+void recv_msg_unknown(protob_io_t *iface) {
+  codec_flush(iface->wire, iface->msg_size, iface->buf);
+  send_msg_failure(iface, FailureType_Failure_UnexpectedMessage,
+                   "Unexpected message");
+}
+
+void protob_init(protob_io_t *iface, wire_iface_t *wire) {
+  memset(iface, 0, sizeof(protob_io_t));
+  iface->wire = wire;
+}
+
+uint32_t protob_get_iface_flag(protob_io_t *iface) {
+  return iface->wire->poll_iface_id;
+}
+
+secbool protob_get_msg_header(protob_io_t *iface, uint16_t *msg_id) {
+  iface->wire->read(iface->buf, iface->wire->rx_packet_size);
+  return codec_parse_header(iface->buf, msg_id, &iface->msg_size);
+}
diff --git a/core/embed/projects/bootloader/protob/protob.h b/core/embed/projects/bootloader/protob/protob.h
new file mode 100644
index 0000000000..bcf4687ac8
--- /dev/null
+++ b/core/embed/projects/bootloader/protob/protob.h
@@ -0,0 +1,68 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <trezor_types.h>
+
+#include <util/image.h>
+
+#include "pb/messages.pb.h"
+
+#include "wire/codec_v1.h"
+
+typedef struct {
+  wire_iface_t *wire;
+  uint8_t buf[MAX_PACKET_SIZE];
+  size_t msg_size;
+
+} protob_io_t;
+
+secbool send_user_abort(protob_io_t *iface, const char *msg);
+
+secbool send_msg_features(protob_io_t *iface, const vendor_header *const vhdr,
+                          const image_header *const hdr);
+
+secbool send_msg_failure(protob_io_t *iface, FailureType type, const char *msg);
+
+secbool send_msg_success(protob_io_t *iface, const char *msg);
+
+secbool send_msg_request_firmware(protob_io_t *iface, uint32_t offset,
+                                  uint32_t length);
+
+secbool recv_msg_initialize(protob_io_t *iface, Initialize *msg);
+
+secbool recv_msg_get_features(protob_io_t *iface, GetFeatures *msg);
+
+secbool recv_msg_ping(protob_io_t *iface, Ping *msg);
+
+secbool recv_msg_firmware_erase(protob_io_t *iface, FirmwareErase *msg);
+
+secbool recv_msg_firmware_upload(protob_io_t *iface, FirmwareUpload *msg,
+                                 void *ctx,
+                                 void (*data_cb)(size_t len, void *ctx),
+                                 uint8_t *buffer, size_t buffer_size);
+
+void recv_msg_unknown(protob_io_t *iface);
+
+void protob_init(protob_io_t *iface, wire_iface_t *wire);
+
+uint32_t protob_get_iface_flag(protob_io_t *iface);
+
+secbool protob_get_msg_header(protob_io_t *iface, uint16_t *msg_id);
diff --git a/core/embed/projects/bootloader/wire/codec_v1.c b/core/embed/projects/bootloader/wire/codec_v1.c
new file mode 100644
index 0000000000..d7dcf773f5
--- /dev/null
+++ b/core/embed/projects/bootloader/wire/codec_v1.c
@@ -0,0 +1,248 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <pb.h>
+#include <pb_decode.h>
+#include <pb_encode.h>
+#include "memzero.h"
+
+#include "codec_v1.h"
+
+#define MSG_HEADER1_LEN 9
+#define MSG_HEADER2_LEN 1
+
+typedef struct {
+  wire_iface_t *iface;
+
+  uint8_t packet_index;
+  uint8_t packet_pos;
+  uint8_t buf[MAX_PACKET_SIZE];
+
+} packet_write_state_t;
+
+typedef struct {
+  wire_iface_t *iface;
+  uint8_t packet_index;
+  uint8_t packet_pos;
+  uint8_t *buf;
+} packet_read_state_t;
+
+secbool codec_parse_header(const uint8_t *buf, uint16_t *msg_id,
+                           size_t *msg_size) {
+  if (buf[0] != '?' || buf[1] != '#' || buf[2] != '#') {
+    return secfalse;
+  }
+  *msg_id = (buf[3] << 8) + buf[4];
+  *msg_size = (buf[5] << 24) + (buf[6] << 16) + (buf[7] << 8) + buf[8];
+  return sectrue;
+}
+
+/* we don't use secbool/sectrue/secfalse here as it is a nanopb api */
+static bool write(pb_ostream_t *stream, const pb_byte_t *buf, size_t count) {
+  packet_write_state_t *state = (packet_write_state_t *)(stream->state);
+
+  size_t tx_len = state->iface->tx_packet_size;
+  uint8_t *tx_buf = state->buf;
+
+  size_t written = 0;
+  // while we have data left
+  while (written < count) {
+    size_t remaining = count - written;
+    // if all remaining data fit into our packet
+    if (state->packet_pos + remaining <= tx_len) {
+      // append data from buf to state->buf
+      memcpy(tx_buf + state->packet_pos, buf + written, remaining);
+      // advance position
+      state->packet_pos += remaining;
+      // and return
+      return true;
+    } else {
+      // append data that fits
+      memcpy(tx_buf + state->packet_pos, buf + written,
+             tx_len - state->packet_pos);
+      written += tx_len - state->packet_pos;
+      // send packet
+      bool ok = state->iface->write(tx_buf, tx_len);
+      ensure(sectrue * ok, NULL);
+      // prepare new packet
+      state->packet_index++;
+      memzero(tx_buf, tx_len);
+      tx_buf[0] = '?';
+      state->packet_pos = MSG_HEADER2_LEN;
+    }
+  }
+
+  return true;
+}
+
+static void write_flush(packet_write_state_t *state) {
+  size_t packet_size = state->iface->tx_packet_size;
+
+  // if packet is not filled up completely
+  if (state->packet_pos < packet_size) {
+    // pad it with zeroes
+    memzero(state->buf + state->packet_pos, packet_size - state->packet_pos);
+  }
+  // send packet
+  bool ok = state->iface->write(state->buf, packet_size);
+  ensure(sectrue * (ok), NULL);
+}
+
+secbool codec_send_msg(wire_iface_t *iface, uint16_t msg_id,
+                       const pb_msgdesc_t *fields, const void *msg) {
+  // determine message size by serializing it into a dummy stream
+  pb_ostream_t sizestream = {.callback = NULL,
+                             .state = NULL,
+                             .max_size = SIZE_MAX,
+                             .bytes_written = 0,
+                             .errmsg = NULL};
+  if (false == pb_encode(&sizestream, fields, msg)) {
+    return secfalse;
+  }
+  const uint32_t msg_size = sizestream.bytes_written;
+
+  packet_write_state_t state = {
+      .iface = iface,
+      .packet_index = 0,
+      .packet_pos = MSG_HEADER1_LEN,
+      .buf =
+          {
+              '?',
+              '#',
+              '#',
+              (msg_id >> 8) & 0xFF,
+              msg_id & 0xFF,
+              (msg_size >> 24) & 0xFF,
+              (msg_size >> 16) & 0xFF,
+              (msg_size >> 8) & 0xFF,
+              msg_size & 0xFF,
+          },
+  };
+
+  pb_ostream_t stream = {.callback = &write,
+                         .state = &state,
+                         .max_size = SIZE_MAX,
+                         .bytes_written = 0,
+                         .errmsg = NULL};
+
+  if (false == pb_encode(&stream, fields, msg)) {
+    return secfalse;
+  }
+
+  write_flush(&state);
+
+  return sectrue;
+}
+
+static void read_retry(wire_iface_t *iface, uint8_t *buf) {
+  size_t packet_size = iface->rx_packet_size;
+
+  for (int retry = 0;; retry++) {
+    int r = iface->read(buf, packet_size);
+    if (r != packet_size) {  // reading failed
+      if (r == 0 && retry < 10) {
+        // only timeout => let's try again
+        continue;
+      } else {
+        iface->error();
+      }
+    }
+    return;  // success
+  }
+}
+
+/* we don't use secbool/sectrue/secfalse here as it is a nanopb api */
+static bool read(pb_istream_t *stream, uint8_t *buf, size_t count) {
+  packet_read_state_t *state = (packet_read_state_t *)(stream->state);
+
+  size_t packet_size = state->iface->rx_packet_size;
+
+  size_t read = 0;
+  // while we have data left
+  while (read < count) {
+    size_t remaining = count - read;
+    // if all remaining data fit into our packet
+    if (state->packet_pos + remaining <= packet_size) {
+      // append data from buf to state->buf
+      memcpy(buf + read, state->buf + state->packet_pos, remaining);
+      // advance position
+      state->packet_pos += remaining;
+      // and return
+      return true;
+    } else {
+      // append data that fits
+      memcpy(buf + read, state->buf + state->packet_pos,
+             packet_size - state->packet_pos);
+      read += packet_size - state->packet_pos;
+      // read next packet (with retry)
+      read_retry(state->iface, state->buf);
+      // prepare next packet
+      state->packet_index++;
+      state->packet_pos = MSG_HEADER2_LEN;
+    }
+  }
+
+  return true;
+}
+
+static void read_flush(packet_read_state_t *state) { (void)state; }
+
+secbool codec_recv_message(wire_iface_t *iface, uint32_t msg_size, uint8_t *buf,
+                           const pb_msgdesc_t *fields, void *msg) {
+  packet_read_state_t state = {.iface = iface,
+                               .packet_index = 0,
+                               .packet_pos = MSG_HEADER1_LEN,
+                               .buf = buf};
+
+  pb_istream_t stream = {.callback = &read,
+                         .state = &state,
+                         .bytes_left = msg_size,
+                         .errmsg = NULL};
+
+  if (false == pb_decode_noinit(&stream, fields, msg)) {
+    return secfalse;
+  }
+
+  read_flush(&state);
+
+  return sectrue;
+}
+
+void codec_flush(wire_iface_t *iface, uint32_t msg_size, uint8_t *buf) {
+  // consume remaining message
+  int remaining_chunks = 0;
+
+  size_t packet_size = iface->rx_packet_size;
+
+  if (msg_size > (packet_size - MSG_HEADER1_LEN)) {
+    // calculate how many blocks need to be read to drain the message (rounded
+    // up to not leave any behind)
+    remaining_chunks = (msg_size - (packet_size - MSG_HEADER1_LEN) +
+                        ((packet_size - MSG_HEADER2_LEN) - 1)) /
+                       (packet_size - MSG_HEADER2_LEN);
+  }
+
+  for (int i = 0; i < remaining_chunks; i++) {
+    // read next packet (with retry)
+    read_retry(iface, buf);
+  }
+}
diff --git a/core/embed/projects/bootloader/wire/codec_v1.h b/core/embed/projects/bootloader/wire/codec_v1.h
new file mode 100644
index 0000000000..82450250ac
--- /dev/null
+++ b/core/embed/projects/bootloader/wire/codec_v1.h
@@ -0,0 +1,54 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <trezor_types.h>
+
+#include <pb.h>
+
+#define MAX_PACKET_SIZE 256
+
+typedef struct {
+  // identifier of the interface used for polling communication events
+  uint8_t poll_iface_id;
+  // size of TX packet
+  size_t tx_packet_size;
+  // size of RX packet
+  size_t rx_packet_size;
+
+  // write function pointer
+  bool (*write)(uint8_t *data, size_t size);
+  // read function pointer
+  int (*read)(uint8_t *buffer, size_t buffer_size);
+
+  // RSOD function pointer
+  void (*error)(void);
+} wire_iface_t;
+
+secbool codec_parse_header(const uint8_t *buf, uint16_t *msg_id,
+                           size_t *msg_size);
+
+secbool codec_send_msg(wire_iface_t *iface, uint16_t msg_id,
+                       const pb_msgdesc_t *fields, const void *msg);
+
+secbool codec_recv_message(wire_iface_t *iface, uint32_t msg_size, uint8_t *buf,
+                           const pb_msgdesc_t *fields, void *msg);
+
+void codec_flush(wire_iface_t *iface, uint32_t msg_size, uint8_t *buf);
diff --git a/core/embed/projects/bootloader/wire/wire_iface_usb.c b/core/embed/projects/bootloader/wire/wire_iface_usb.c
new file mode 100644
index 0000000000..4ebb691cef
--- /dev/null
+++ b/core/embed/projects/bootloader/wire/wire_iface_usb.c
@@ -0,0 +1,113 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include "wire_iface_usb.h"
+
+#include <io/usb.h>
+
+#define USB_TIMEOUT 500
+#define USB_PACKET_SIZE 64
+#define USB_IFACE_NUM 0
+
+static bool usb_write(uint8_t* data, size_t size) {
+  if (size != USB_PACKET_SIZE) {
+    return false;
+  }
+
+  int r = usb_webusb_write_blocking(USB_IFACE_NUM, data, size, USB_TIMEOUT);
+
+  return r == size;
+}
+
+static int usb_read(uint8_t* buffer, size_t buffer_size) {
+  if (buffer_size != USB_PACKET_SIZE) {
+    return -1;
+  }
+
+  int r = usb_webusb_read_blocking(USB_IFACE_NUM, buffer, USB_PACKET_SIZE,
+                                   USB_TIMEOUT);
+
+  return r;
+}
+
+static void usb_error(void) {
+  error_shutdown_ex("USB ERROR",
+                    "Error reading from USB. Try different USB cable.", NULL);
+}
+
+static void usb_init_all(secbool usb21_landing) {
+  usb_dev_info_t dev_info = {
+      .device_class = 0x00,
+      .device_subclass = 0x00,
+      .device_protocol = 0x00,
+      .vendor_id = 0x1209,
+      .product_id = 0x53C0,
+      .release_num = 0x0200,
+      .manufacturer = MODEL_USB_MANUFACTURER,
+      .product = MODEL_USB_PRODUCT,
+      .serial_number = "000000000000000000000000",
+      .interface = "TREZOR Interface",
+      .usb21_enabled = sectrue,
+      .usb21_landing = usb21_landing,
+  };
+
+  static uint8_t rx_buffer[USB_PACKET_SIZE];
+
+  static const usb_webusb_info_t webusb_info = {
+      .iface_num = USB_IFACE_NUM,
+#ifdef TREZOR_EMULATOR
+      .emu_port = 21324,
+#else
+      .ep_in = 0x01,
+      .ep_out = 0x01,
+#endif
+      .subclass = 0,
+      .protocol = 0,
+      .max_packet_len = sizeof(rx_buffer),
+      .rx_buffer = rx_buffer,
+      .polling_interval = 1,
+  };
+
+  ensure(usb_init(&dev_info), NULL);
+
+  ensure(usb_webusb_add(&webusb_info), NULL);
+
+  ensure(usb_start(), NULL);
+}
+
+void usb_iface_init(wire_iface_t* iface, secbool usb21_landing) {
+  usb_init_all(usb21_landing);
+
+  memset(iface, 0, sizeof(wire_iface_t));
+
+  iface->poll_iface_id = USB_IFACE_NUM;
+  iface->tx_packet_size = USB_PACKET_SIZE;
+  iface->rx_packet_size = USB_PACKET_SIZE;
+  iface->write = &usb_write;
+  iface->read = &usb_read;
+  iface->error = &usb_error;
+}
+
+void usb_iface_deinit(wire_iface_t* iface) {
+  memset(iface, 0, sizeof(wire_iface_t));
+  usb_deinit();
+}
diff --git a/core/embed/projects/bootloader/wire/wire_iface_usb.h b/core/embed/projects/bootloader/wire/wire_iface_usb.h
new file mode 100644
index 0000000000..6b2057071c
--- /dev/null
+++ b/core/embed/projects/bootloader/wire/wire_iface_usb.h
@@ -0,0 +1,26 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "codec_v1.h"
+
+void usb_iface_init(wire_iface_t* iface, secbool usb21_landing);
+
+void usb_iface_deinit(wire_iface_t* iface);
diff --git a/core/embed/projects/bootloader/workflow/wf_auto_update.c b/core/embed/projects/bootloader/workflow/wf_auto_update.c
new file mode 100644
index 0000000000..d5a4e64260
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_auto_update.c
@@ -0,0 +1,37 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <util/image.h>
+
+#include "bootui.h"
+#include "workflow.h"
+
+workflow_result_t workflow_auto_update(const vendor_header *const vhdr,
+                                       const image_header *const hdr) {
+  ui_set_initial_setup(true);
+
+  workflow_result_t res = WF_CANCELLED;
+  while (res == WF_CANCELLED) {
+    res = workflow_host_control(vhdr, hdr, ui_screen_connect);
+  }
+  return res;
+}
diff --git a/core/embed/projects/bootloader/workflow/wf_bootloader.c b/core/embed/projects/bootloader/workflow/wf_bootloader.c
new file mode 100644
index 0000000000..f2191b73fa
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_bootloader.c
@@ -0,0 +1,99 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <sys/types.h>
+#include <util/image.h>
+
+#include "antiglitch.h"
+#include "bootui.h"
+#include "workflow.h"
+
+typedef enum {
+  SCREEN_INTRO,
+  SCREEN_MENU,
+  SCREEN_WAIT_FOR_HOST,
+} screen_t;
+
+workflow_result_t workflow_bootloader(const vendor_header *const vhdr,
+                                      const image_header *const hdr,
+                                      secbool firmware_present) {
+  ui_set_initial_setup(false);
+
+  screen_t screen = SCREEN_INTRO;
+
+  while (true) {
+    switch (screen) {
+      case SCREEN_INTRO:
+        intro_result_t ui_result = ui_screen_intro(vhdr, hdr, firmware_present);
+        if (ui_result == INTRO_MENU) {
+          screen = SCREEN_MENU;
+        }
+        if (ui_result == INTRO_HOST) {
+          screen = SCREEN_WAIT_FOR_HOST;
+        }
+        break;
+      case SCREEN_MENU:
+        menu_result_t menu_result = ui_screen_menu(firmware_present);
+        if (menu_result == MENU_EXIT) {  // exit menu
+          screen = SCREEN_INTRO;
+        }
+        if (menu_result == MENU_REBOOT) {  // reboot
+#ifndef USE_HASH_PROCESSOR
+          ui_screen_boot_stage_1(true);
+#endif
+          jump_allow_1();
+          jump_allow_2();
+          return WF_OK_REBOOT_SELECTED;
+        }
+        if (menu_result == MENU_WIPE) {  // wipe
+          workflow_result_t r = workflow_wipe_device(NULL);
+          if (r == WF_ERROR) {
+            return r;
+          }
+          if (r == WF_OK) {
+            return WF_OK_DEVICE_WIPED;
+          }
+
+          if (r == WF_CANCELLED) {
+            screen = SCREEN_MENU;
+            continue;
+          }
+          return WF_ERROR_FATAL;
+        }
+        break;
+      case SCREEN_WAIT_FOR_HOST:
+        workflow_result_t res =
+            workflow_host_control(vhdr, hdr, ui_screen_connect);
+        switch (res) {
+          case WF_CANCELLED:
+            screen = SCREEN_INTRO;
+            continue;
+          default:
+            return res;
+        }
+        break;
+      default:
+        return WF_ERROR_FATAL;
+        break;
+    }
+  }
+}
diff --git a/core/embed/projects/bootloader/workflow/wf_empty_device.c b/core/embed/projects/bootloader/workflow/wf_empty_device.c
new file mode 100644
index 0000000000..136e6f5617
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_empty_device.c
@@ -0,0 +1,57 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <sys/systick.h>
+#include <sys/types.h>
+#include <util/flash_utils.h>
+#include <util/image.h>
+
+#ifdef USE_STORAGE_HWKEY
+#include <sec/secret.h>
+#endif
+
+#include "bootui.h"
+#include "workflow.h"
+
+workflow_result_t workflow_empty_device(void) {
+  ui_set_initial_setup(true);
+
+#ifdef USE_STORAGE_HWKEY
+  secret_bhk_regenerate();
+#endif
+  ensure(erase_storage(NULL), NULL);
+
+  // keep the model screen up for a while
+#ifndef USE_BACKLIGHT
+  systick_delay_ms(1500);
+#else
+  // backlight fading takes some time so the explicit delay here is
+  // shorter
+  systick_delay_ms(1000);
+#endif
+
+  workflow_result_t res = WF_CANCELLED;
+  while (res == WF_CANCELLED) {
+    res = workflow_host_control(NULL, NULL, ui_screen_welcome);
+  }
+  return res;
+}
diff --git a/core/embed/projects/bootloader/workflow/wf_firmware_update.c b/core/embed/projects/bootloader/workflow/wf_firmware_update.c
new file mode 100644
index 0000000000..b60dbcf9ce
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_firmware_update.c
@@ -0,0 +1,561 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <sys/bootargs.h>
+#include <sys/systick.h>
+#include <util/flash.h>
+#include <util/flash_utils.h>
+
+#if USE_OPTIGA
+#include <sec/secret.h>
+#endif
+
+#include <poll.h>
+
+#include "bootui.h"
+#include "protob/protob.h"
+#include "version_check.h"
+#include "workflow.h"
+
+#ifdef TREZOR_EMULATOR
+#include "emulator.h"
+#endif
+
+typedef enum {
+  UPLOAD_OK = 0,
+  UPLOAD_IN_PROGRESS = 1,
+  UPLOAD_ERR_INVALID_CHUNK_SIZE = -1,
+  UPLOAD_ERR_INVALID_VENDOR_HEADER = -2,
+  UPLOAD_ERR_INVALID_VENDOR_HEADER_SIG = -3,
+  UPLOAD_ERR_INVALID_VENDOR_HEADER_MODEL = -15,
+  UPLOAD_ERR_INVALID_IMAGE_HEADER = -4,
+  UPLOAD_ERR_INVALID_IMAGE_MODEL = -5,
+  UPLOAD_ERR_INVALID_IMAGE_HEADER_SIG = -6,
+  UPLOAD_ERR_INVALID_IMAGE_HEADER_VERSION = -16,
+  UPLOAD_ERR_USER_ABORT = -7,
+  UPLOAD_ERR_FIRMWARE_TOO_BIG = -8,
+  UPLOAD_ERR_INVALID_CHUNK_HASH = -9,
+  UPLOAD_ERR_BOOTLOADER_LOCKED = -10,
+  UPLOAD_ERR_FIRMWARE_MISMATCH = -11,
+  UPLOAD_ERR_NOT_FIRMWARE_UPGRADE = -12,
+  UPLOAD_ERR_NOT_FULLTRUST_IMAGE = -13,
+  UPLOAD_ERR_INVALID_CHUNK_PADDING = -14,
+  UPLOAD_ERR_COMMUNICATION = -17,
+} upload_status_t;
+
+#define FIRMWARE_UPLOAD_CHUNK_RETRY_COUNT 2
+
+#ifndef TREZOR_EMULATOR
+__attribute__((section(".buf")))
+#endif
+uint32_t chunk_buffer[IMAGE_CHUNK_SIZE / 4];
+
+typedef struct {
+  uint32_t firmware_remaining;
+  uint32_t firmware_block;
+  uint32_t chunk_requested;
+  uint32_t erase_offset;
+  int32_t firmware_upload_chunk_retry;
+  size_t headers_offset;
+  size_t read_offset;
+  uint32_t chunk_size;
+} firmware_update_ctx_t;
+
+static int version_compare(uint32_t vera, uint32_t verb) {
+  /* Explicit casts so that we control how compiler does the unsigned shift
+   * and correctly then promote uint8_t to int without possibility of
+   * having implementation-defined right shift on negative int
+   * in case compiler promoted the wrong unsigned int
+   */
+  int a, b;
+  a = (uint8_t)vera & 0xFF;
+  b = (uint8_t)verb & 0xFF;
+  if (a != b) return a - b;
+  a = (uint8_t)(vera >> 8) & 0xFF;
+  b = (uint8_t)(verb >> 8) & 0xFF;
+  if (a != b) return a - b;
+  a = (uint8_t)(vera >> 16) & 0xFF;
+  b = (uint8_t)(verb >> 16) & 0xFF;
+  if (a != b) return a - b;
+  a = (uint8_t)(vera >> 24) & 0xFF;
+  b = (uint8_t)(verb >> 24) & 0xFF;
+  return a - b;
+}
+
+static void detect_installation(const vendor_header *current_vhdr,
+                                const image_header *current_hdr,
+                                const vendor_header *const new_vhdr,
+                                const image_header *const new_hdr,
+                                secbool *is_new, secbool *keep_seed,
+                                secbool *is_newvendor, secbool *is_upgrade) {
+  *is_new = secfalse;
+  *keep_seed = secfalse;
+  *is_newvendor = secfalse;
+  *is_upgrade = secfalse;
+  if (sectrue != check_vendor_header_keys(current_vhdr)) {
+    *is_new = sectrue;
+    return;
+  }
+  if (sectrue != check_image_model(current_hdr)) {
+    *is_new = sectrue;
+    return;
+  }
+  if (sectrue != check_firmware_min_version(current_hdr->monotonic)) {
+    *is_new = sectrue;
+    return;
+  }
+  if (sectrue != check_image_header_sig(current_hdr, current_vhdr->vsig_m,
+                                        current_vhdr->vsig_n,
+                                        current_vhdr->vpub)) {
+    *is_new = sectrue;
+    return;
+  }
+  uint8_t hash1[32], hash2[32];
+  vendor_header_hash(new_vhdr, hash1);
+  vendor_header_hash(current_vhdr, hash2);
+  if (0 != memcmp(hash1, hash2, 32)) {
+    *is_newvendor = sectrue;
+    return;
+  }
+  if (version_compare(new_hdr->version, current_hdr->fix_version) < 0) {
+    return;
+  }
+  if (version_compare(new_hdr->version, current_hdr->version) > 0) {
+    *is_upgrade = sectrue;
+  }
+
+  *keep_seed = sectrue;
+}
+
+static void fw_data_received(size_t len, void *ctx) {
+  firmware_update_ctx_t *context = (firmware_update_ctx_t *)ctx;
+
+  context->chunk_size += len;
+  // update loader but skip first block
+  if (context->firmware_block > 0) {
+    ui_screen_install_progress_upload(
+        1000 *
+        (context->firmware_block * IMAGE_CHUNK_SIZE + context->chunk_size) /
+        (context->firmware_block * IMAGE_CHUNK_SIZE +
+         context->firmware_remaining));
+  }
+}
+
+static upload_status_t process_msg_FirmwareUpload(protob_io_t *iface,
+                                                  firmware_update_ctx_t *ctx) {
+  FirmwareUpload msg;
+
+  const secbool r =
+      recv_msg_firmware_upload(iface, &msg, ctx, fw_data_received,
+                               &((uint8_t *)chunk_buffer)[ctx->read_offset],
+                               sizeof(chunk_buffer) - ctx->read_offset);
+
+  if (sectrue != r ||
+      ctx->chunk_size != (ctx->chunk_requested + ctx->read_offset)) {
+    send_msg_failure(iface, FailureType_Failure_ProcessError,
+                     "Invalid chunk size");
+    return UPLOAD_ERR_INVALID_CHUNK_SIZE;
+  }
+
+  static image_header hdr;
+
+  if (ctx->firmware_block == 0) {
+    if (ctx->headers_offset == 0) {
+      // first block and headers are not yet parsed
+      vendor_header vhdr;
+
+      if (sectrue != read_vendor_header((uint8_t *)chunk_buffer, &vhdr)) {
+        send_msg_failure(iface, FailureType_Failure_ProcessError,
+                         "Invalid vendor header");
+        return UPLOAD_ERR_INVALID_VENDOR_HEADER;
+      }
+
+      if (sectrue != check_vendor_header_model(&vhdr)) {
+        send_msg_failure(iface, FailureType_Failure_ProcessError,
+                         "Wrong model");
+        return UPLOAD_ERR_INVALID_VENDOR_HEADER_MODEL;
+      }
+
+      if (sectrue != check_vendor_header_keys(&vhdr)) {
+        send_msg_failure(iface, FailureType_Failure_ProcessError,
+                         "Invalid vendor header signature");
+        return UPLOAD_ERR_INVALID_VENDOR_HEADER_SIG;
+      }
+
+      const image_header *received_hdr =
+          read_image_header((uint8_t *)chunk_buffer + vhdr.hdrlen,
+                            FIRMWARE_IMAGE_MAGIC, FIRMWARE_MAXSIZE);
+
+      if (received_hdr !=
+          (const image_header *)((uint8_t *)chunk_buffer + vhdr.hdrlen)) {
+        send_msg_failure(iface, FailureType_Failure_ProcessError,
+                         "Invalid firmware header");
+        return UPLOAD_ERR_INVALID_IMAGE_HEADER;
+      }
+
+      if (sectrue != check_image_model(received_hdr)) {
+        send_msg_failure(iface, FailureType_Failure_ProcessError,
+                         "Wrong firmware model");
+        return UPLOAD_ERR_INVALID_IMAGE_MODEL;
+      }
+
+      if (sectrue != check_image_header_sig(received_hdr, vhdr.vsig_m,
+                                            vhdr.vsig_n, vhdr.vpub)) {
+        send_msg_failure(iface, FailureType_Failure_ProcessError,
+                         "Invalid firmware signature");
+        return UPLOAD_ERR_INVALID_IMAGE_HEADER_SIG;
+      }
+
+      if (sectrue != check_firmware_min_version(received_hdr->monotonic)) {
+        send_msg_failure(iface, FailureType_Failure_ProcessError,
+                         "Firmware downgrade protection");
+        return UPLOAD_ERR_INVALID_IMAGE_HEADER_VERSION;
+      }
+
+      memcpy(&hdr, received_hdr, sizeof(hdr));
+
+      vendor_header current_vhdr;
+
+      secbool is_new = secfalse;
+
+      if (sectrue !=
+          read_vendor_header((const uint8_t *)FIRMWARE_START, &current_vhdr)) {
+        is_new = sectrue;
+      }
+
+      const image_header *current_hdr = NULL;
+
+      if (is_new == secfalse) {
+        current_hdr = read_image_header(
+            (const uint8_t *)FIRMWARE_START + current_vhdr.hdrlen,
+            FIRMWARE_IMAGE_MAGIC, FIRMWARE_MAXSIZE);
+
+        if (current_hdr !=
+            (const image_header *)(void *)(FIRMWARE_START +
+                                           current_vhdr.hdrlen)) {
+          is_new = sectrue;
+        }
+      }
+
+      secbool should_keep_seed = secfalse;
+      secbool is_newvendor = secfalse;
+      secbool is_upgrade = secfalse;
+      if (is_new == secfalse) {
+        detect_installation(&current_vhdr, current_hdr, &vhdr, &hdr, &is_new,
+                            &should_keep_seed, &is_newvendor, &is_upgrade);
+      }
+
+      secbool is_ilu = secfalse;  // interaction-less update
+
+      if (bootargs_get_command() == BOOT_COMMAND_INSTALL_UPGRADE) {
+        IMAGE_HASH_CTX ctx;
+        uint8_t hash[IMAGE_HASH_DIGEST_LENGTH];
+        IMAGE_HASH_INIT(&ctx);
+        IMAGE_HASH_UPDATE(&ctx, (uint8_t *)chunk_buffer,
+                          vhdr.hdrlen + received_hdr->hdrlen);
+        IMAGE_HASH_FINAL(&ctx, hash);
+
+        // the firmware must be the same as confirmed by the user
+        boot_args_t args = {0};
+        bootargs_get_args(&args);
+
+        if (memcmp(args.hash, hash, sizeof(hash)) != 0) {
+          send_msg_failure(iface, FailureType_Failure_ProcessError,
+                           "Firmware mismatch");
+          return UPLOAD_ERR_FIRMWARE_MISMATCH;
+        }
+
+        // the firmware must be from the same vendor
+        // the firmware must be newer
+        if (is_upgrade != sectrue || is_newvendor != secfalse) {
+          send_msg_failure(iface, FailureType_Failure_ProcessError,
+                           "Not a firmware upgrade");
+          return UPLOAD_ERR_NOT_FIRMWARE_UPGRADE;
+        }
+
+        if ((vhdr.vtrust & VTRUST_NO_WARNING) != VTRUST_NO_WARNING) {
+          send_msg_failure(iface, FailureType_Failure_ProcessError,
+                           "Not a full-trust image");
+          return UPLOAD_ERR_NOT_FULLTRUST_IMAGE;
+        }
+
+        // upload the firmware without confirmation
+        is_ilu = sectrue;
+      }
+
+#if defined USE_OPTIGA
+      if (secfalse != secret_optiga_present() &&
+          ((vhdr.vtrust & VTRUST_SECRET_MASK) != VTRUST_SECRET_ALLOW)) {
+        send_msg_failure(iface, FailureType_Failure_ProcessError,
+                         "Install restricted");
+        return UPLOAD_ERR_BOOTLOADER_LOCKED;
+      }
+#endif
+
+      ui_result_t response = UI_RESULT_CANCEL;
+      if (((vhdr.vtrust & VTRUST_NO_WARNING) == VTRUST_NO_WARNING) &&
+          (sectrue == is_new || sectrue == is_ilu)) {
+        // new installation or interaction less updated - auto confirm
+        // only allowed for full-trust images
+        response = UI_RESULT_CONFIRM;
+      } else {
+        if (sectrue != is_new) {
+          int version_cmp = version_compare(hdr.version, current_hdr->version);
+          response = ui_screen_install_confirm(
+              &vhdr, &hdr, should_keep_seed, is_newvendor, is_new, version_cmp);
+        } else {
+          response = ui_screen_install_confirm(&vhdr, &hdr, sectrue,
+                                               is_newvendor, is_new, 0);
+        }
+      }
+
+      if (UI_RESULT_CONFIRM != response) {
+        send_user_abort(iface, "Firmware install cancelled");
+        return UPLOAD_ERR_USER_ABORT;
+      }
+
+      ui_screen_install_start();
+
+      // if firmware is not upgrade, erase storage
+      if (sectrue != should_keep_seed) {
+#ifdef USE_STORAGE_HWKEY
+        secret_bhk_regenerate();
+#endif
+        ensure(erase_storage(NULL), NULL);
+      }
+
+      ctx->headers_offset = IMAGE_HEADER_SIZE + vhdr.hdrlen;
+      ctx->read_offset = IMAGE_INIT_CHUNK_SIZE;
+
+      // request the rest of the first chunk
+      uint32_t chunk_limit = (ctx->firmware_remaining > IMAGE_CHUNK_SIZE)
+                                 ? IMAGE_CHUNK_SIZE
+                                 : ctx->firmware_remaining;
+      ctx->chunk_requested = chunk_limit - ctx->read_offset;
+
+      if (sectrue != send_msg_request_firmware(iface, ctx->read_offset,
+                                               ctx->chunk_requested)) {
+        return UPLOAD_ERR_COMMUNICATION;
+      }
+
+      ctx->firmware_remaining -= ctx->read_offset;
+      if (ctx->firmware_remaining > 0) {
+        return UPLOAD_IN_PROGRESS;
+      }
+      return UPLOAD_OK;
+    } else {
+      // first block with the headers parsed -> the first chunk is now complete
+      ctx->read_offset = 0;
+    }
+  }
+
+  // should not happen, but double-check
+  if (flash_area_get_address(
+          &FIRMWARE_AREA, ctx->firmware_block * IMAGE_CHUNK_SIZE, 0) == NULL) {
+    send_msg_failure(iface, FailureType_Failure_ProcessError,
+                     "Firmware too big");
+    return UPLOAD_ERR_FIRMWARE_TOO_BIG;
+  }
+
+  if (sectrue !=
+      check_single_hash(hdr.hashes + ctx->firmware_block * 32,
+                        (uint8_t *)chunk_buffer + ctx->headers_offset,
+                        ctx->chunk_size - ctx->headers_offset)) {
+    if (ctx->firmware_upload_chunk_retry > 0) {
+      --ctx->firmware_upload_chunk_retry;
+
+      // clear chunk buffer
+      memset((uint8_t *)&chunk_buffer, 0xFF, IMAGE_CHUNK_SIZE);
+      ctx->chunk_size = 0;
+
+      if (sectrue != send_msg_request_firmware(
+                         iface, ctx->firmware_block * IMAGE_CHUNK_SIZE,
+                         ctx->chunk_requested)) {
+        return UPLOAD_ERR_COMMUNICATION;
+      }
+      if (ctx->firmware_remaining > 0) {
+        return UPLOAD_IN_PROGRESS;
+      }
+      return UPLOAD_OK;
+    }
+
+    send_msg_failure(iface, FailureType_Failure_ProcessError,
+                     "Invalid chunk hash");
+    return UPLOAD_ERR_INVALID_CHUNK_HASH;
+  }
+
+  // buffer with the received data
+  const uint32_t *src = (const uint32_t *)chunk_buffer;
+  // number of received bytes
+  uint32_t bytes_remaining = ctx->chunk_size;
+  // offset into the FIRMWARE_AREA part of the flash
+  uint32_t write_offset = ctx->firmware_block * IMAGE_CHUNK_SIZE;
+
+  ensure((ctx->chunk_size % FLASH_BLOCK_SIZE == 0) * sectrue, NULL);
+
+  while (bytes_remaining > 0) {
+    // erase flash before writing
+    uint32_t bytes_erased = 0;
+
+    if (write_offset >= ctx->erase_offset) {
+      // erase the next flash section
+      ensure(flash_area_erase_partial(&FIRMWARE_AREA, ctx->erase_offset,
+                                      &bytes_erased),
+             NULL);
+      ctx->erase_offset += bytes_erased;
+    } else {
+      // some erased space left from the previous round => use it
+      bytes_erased = ctx->erase_offset - write_offset;
+    }
+
+    // write the received data
+    uint32_t bytes_to_write = MIN(bytes_erased, bytes_remaining);
+    ensure(flash_unlock_write(), NULL);
+    ensure(flash_area_write_data(&FIRMWARE_AREA, write_offset, src,
+                                 bytes_to_write),
+           NULL);
+    ensure(flash_lock_write(), NULL);
+
+    write_offset += bytes_to_write;
+    src += bytes_to_write / sizeof(uint32_t);
+
+    bytes_remaining -= bytes_to_write;
+  }
+
+  ctx->firmware_remaining -= ctx->chunk_requested;
+
+  if (ctx->firmware_remaining == 0) {
+    // erase the rest (unused part) of the FIRMWARE_AREA
+    uint32_t bytes_erased = 0;
+    do {
+      ensure(flash_area_erase_partial(&FIRMWARE_AREA, ctx->erase_offset,
+                                      &bytes_erased),
+             NULL);
+      ctx->erase_offset += bytes_erased;
+    } while (bytes_erased > 0);
+  }
+
+  ctx->headers_offset = 0;
+  ctx->firmware_block++;
+  ctx->firmware_upload_chunk_retry = FIRMWARE_UPLOAD_CHUNK_RETRY_COUNT;
+
+  if (ctx->firmware_remaining > 0) {
+    ctx->chunk_requested = (ctx->firmware_remaining > IMAGE_CHUNK_SIZE)
+                               ? IMAGE_CHUNK_SIZE
+                               : ctx->firmware_remaining;
+
+    // clear chunk buffer
+    ctx->chunk_size = 0;
+    memset((uint8_t *)&chunk_buffer, 0xFF, IMAGE_CHUNK_SIZE);
+    if (sectrue !=
+        send_msg_request_firmware(iface, ctx->firmware_block * IMAGE_CHUNK_SIZE,
+                                  ctx->chunk_requested)) {
+      return UPLOAD_ERR_COMMUNICATION;
+    }
+  } else {
+    send_msg_success(iface, NULL);
+  }
+
+  if (ctx->firmware_remaining > 0) {
+    return UPLOAD_IN_PROGRESS;
+  }
+  return UPLOAD_OK;
+}
+
+workflow_result_t workflow_firmware_update(protob_io_t *iface) {
+  firmware_update_ctx_t ctx = {
+      .firmware_upload_chunk_retry = FIRMWARE_UPLOAD_CHUNK_RETRY_COUNT,
+  };
+
+  FirmwareErase msg;
+  secbool res = recv_msg_firmware_erase(iface, &msg);
+
+  if (res != sectrue) {
+    return WF_ERROR;
+  }
+
+  ctx.firmware_remaining = msg.has_length ? msg.length : 0;
+  if ((ctx.firmware_remaining > 0) &&
+      ((ctx.firmware_remaining % sizeof(uint32_t)) == 0) &&
+      (ctx.firmware_remaining <= FIRMWARE_MAXSIZE)) {
+    // clear chunk buffer
+    memset((uint8_t *)&chunk_buffer, 0xFF, IMAGE_CHUNK_SIZE);
+    ctx.chunk_size = 0;
+
+    // request new firmware
+    ctx.chunk_requested = (ctx.firmware_remaining > IMAGE_INIT_CHUNK_SIZE)
+                              ? IMAGE_INIT_CHUNK_SIZE
+                              : ctx.firmware_remaining;
+    if (sectrue != send_msg_request_firmware(iface, 0, ctx.chunk_requested)) {
+      ui_screen_fail();
+      return WF_ERROR;
+    }
+  } else {
+    // invalid firmware size
+    send_msg_failure(iface, FailureType_Failure_ProcessError,
+                     "Wrong firmware size");
+    return WF_ERROR;
+  }
+
+  upload_status_t s = UPLOAD_IN_PROGRESS;
+
+  while (true) {
+    uint16_t ifaces[1] = {protob_get_iface_flag(iface) | MODE_READ};
+    poll_event_t e = {0};
+    uint8_t i = poll_events(ifaces, 1, &e, 100);
+
+    if (e.type == EVENT_NONE || i != protob_get_iface_flag(iface)) {
+      continue;
+    }
+
+    uint16_t msg_id = 0;
+
+    if (sectrue != protob_get_msg_header(iface, &msg_id)) {
+      // invalid header -> discard
+      return WF_ERROR;
+    }
+    s = process_msg_FirmwareUpload(iface, &ctx);
+
+    if (s < 0 && s != UPLOAD_ERR_USER_ABORT) {  // error, but not user abort
+      if (s == UPLOAD_ERR_BOOTLOADER_LOCKED) {
+        // This function does not return
+        show_install_restricted_screen();
+      } else {
+        ui_screen_fail();
+      }
+      return WF_ERROR;
+    } else if (s == UPLOAD_ERR_USER_ABORT) {
+      systick_delay_ms(100);
+      return WF_CANCELLED;
+    } else if (s == UPLOAD_OK) {  // last chunk received
+      ui_screen_install_progress_upload(1000);
+      ui_screen_done(4, sectrue);
+      ui_screen_done(3, secfalse);
+      systick_delay_ms(1000);
+      ui_screen_done(2, secfalse);
+      systick_delay_ms(1000);
+      ui_screen_done(1, secfalse);
+      systick_delay_ms(1000);
+      return WF_OK;
+    }
+  }
+}
diff --git a/core/embed/projects/bootloader/workflow/wf_get_features.c b/core/embed/projects/bootloader/workflow/wf_get_features.c
new file mode 100644
index 0000000000..5f65eed4bf
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_get_features.c
@@ -0,0 +1,33 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include "protob.h"
+#include "workflow.h"
+
+workflow_result_t workflow_get_features(protob_io_t *iface,
+                                        const vendor_header *const vhdr,
+                                        const image_header *const hdr) {
+  GetFeatures msg_recv;
+  recv_msg_get_features(iface, &msg_recv);
+  send_msg_features(iface, vhdr, hdr);
+  return WF_OK;
+}
diff --git a/core/embed/projects/bootloader/workflow/wf_host_control.c b/core/embed/projects/bootloader/workflow/wf_host_control.c
new file mode 100644
index 0000000000..5ac20be205
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_host_control.c
@@ -0,0 +1,123 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <sys/systick.h>
+#include <sys/types.h>
+#include <util/image.h>
+
+#include "antiglitch.h"
+#include "poll.h"
+#include "protob/protob.h"
+#include "wire/wire_iface_usb.h"
+#include "workflow.h"
+
+workflow_result_t workflow_host_control(const vendor_header *const vhdr,
+                                        const image_header *const hdr,
+                                        void (*redraw_wait_screen)(void)) {
+  wire_iface_t usb_iface = {0};
+  protob_io_t protob_usb_iface = {0};
+
+  redraw_wait_screen();
+
+  // if both are NULL, we don't have a firmware installed
+  // let's show a webusb landing page in this case
+  usb_iface_init(&usb_iface,
+                 (vhdr == NULL && hdr == NULL) ? sectrue : secfalse);
+
+  protob_init(&protob_usb_iface, &usb_iface);
+
+  workflow_result_t result = WF_ERROR_FATAL;
+
+  for (;;) {
+    uint16_t ifaces[1] = {protob_get_iface_flag(&protob_usb_iface) | MODE_READ};
+    poll_event_t e = {0};
+
+    uint8_t i = poll_events(ifaces, 1, &e, 100);
+
+    uint16_t msg_id = 0;
+    protob_io_t *active_iface = NULL;
+
+    switch (e.type) {
+      case EVENT_USB_CAN_READ:
+        if (i == protob_get_iface_flag(&protob_usb_iface) &&
+            sectrue == protob_get_msg_header(&protob_usb_iface, &msg_id)) {
+          active_iface = &protob_usb_iface;
+        } else {
+          continue;
+        }
+        break;
+      case EVENT_NONE:
+      default:
+        continue;
+    }
+
+    switch (msg_id) {
+      case MessageType_MessageType_Initialize:
+        workflow_initialize(active_iface, vhdr, hdr);
+        // whatever the result, we stay here and continue
+        continue;
+      case MessageType_MessageType_Ping:
+        workflow_ping(active_iface);
+        // whatever the result, we stay here and continue
+        continue;
+      case MessageType_MessageType_GetFeatures:
+        workflow_get_features(active_iface, vhdr, hdr);
+        // whatever the result, we stay here and continue
+        continue;
+      case MessageType_MessageType_WipeDevice:
+        result = workflow_wipe_device(active_iface);
+        if (result == WF_OK) {
+          systick_delay_ms(100);
+          usb_iface_deinit(&usb_iface);
+          return WF_OK_DEVICE_WIPED;
+        }
+        break;
+      case MessageType_MessageType_FirmwareErase:
+        result = workflow_firmware_update(active_iface);
+        if (result == WF_OK) {
+          jump_allow_1();
+          jump_allow_2();
+          systick_delay_ms(100);
+          usb_iface_deinit(&usb_iface);
+          return WF_OK_FIRMWARE_INSTALLED;
+        }
+        break;
+#if defined USE_OPTIGA
+      case MessageType_MessageType_UnlockBootloader:
+        result = workflow_unlock_bootloader(active_iface);
+        if (result == WF_OK) {
+          systick_delay_ms(100);
+          usb_iface_deinit(&usb_iface);
+          return WF_OK_BOOTLOADER_UNLOCKED;
+        }
+        break;
+#endif
+      default:
+        recv_msg_unknown(active_iface);
+        continue;
+    }
+
+    systick_delay_ms(100);
+    usb_iface_deinit(&usb_iface);
+    return result;
+  }
+}
diff --git a/core/embed/projects/bootloader/workflow/wf_initialize.c b/core/embed/projects/bootloader/workflow/wf_initialize.c
new file mode 100644
index 0000000000..c378cf1f72
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_initialize.c
@@ -0,0 +1,33 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include "protob.h"
+#include "workflow.h"
+
+workflow_result_t workflow_initialize(protob_io_t *iface,
+                                      const vendor_header *const vhdr,
+                                      const image_header *const hdr) {
+  Initialize msg_recv;
+  recv_msg_initialize(iface, &msg_recv);
+  send_msg_features(iface, vhdr, hdr);
+  return WF_OK;
+}
diff --git a/core/embed/projects/bootloader/workflow/wf_ping.c b/core/embed/projects/bootloader/workflow/wf_ping.c
new file mode 100644
index 0000000000..60b5140e8f
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_ping.c
@@ -0,0 +1,33 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include "protob.h"
+#include "workflow.h"
+
+workflow_result_t workflow_ping(protob_io_t *iface) {
+  Ping msg_recv;
+  if (sectrue != recv_msg_ping(iface, &msg_recv)) {
+    return WF_ERROR;
+  }
+  send_msg_success(iface, msg_recv.message);
+  return WF_OK;
+}
diff --git a/core/embed/projects/bootloader/workflow/wf_unlock_bootloader.c b/core/embed/projects/bootloader/workflow/wf_unlock_bootloader.c
new file mode 100644
index 0000000000..d6e01e5cb0
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_unlock_bootloader.c
@@ -0,0 +1,41 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <sec/secret.h>
+
+#include "bootui.h"
+#include "protob.h"
+#include "rust_ui.h"
+#include "workflow.h"
+
+workflow_result_t workflow_unlock_bootloader(protob_io_t *iface) {
+  ui_result_t response = ui_screen_unlock_bootloader_confirm();
+  if (UI_RESULT_CONFIRM != response) {
+    send_user_abort(iface, "Bootloader unlock cancelled");
+    return WF_CANCELLED;
+  }
+  secret_optiga_erase();
+  send_msg_success(iface, NULL);
+
+  screen_unlock_bootloader_success();
+  return WF_OK;
+}
diff --git a/core/embed/projects/bootloader/workflow/wf_wipe_device.c b/core/embed/projects/bootloader/workflow/wf_wipe_device.c
new file mode 100644
index 0000000000..51b483d06e
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/wf_wipe_device.c
@@ -0,0 +1,55 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <util/flash_utils.h>
+
+#include "bootui.h"
+#include "protob.h"
+#include "rust_ui.h"
+#include "workflow.h"
+
+workflow_result_t workflow_wipe_device(protob_io_t *iface) {
+  ui_result_t response = ui_screen_wipe_confirm();
+  if (UI_RESULT_CONFIRM != response) {
+    if (iface != NULL) {
+      send_user_abort(iface, "Wipe cancelled");
+    }
+    return WF_CANCELLED;
+  }
+  ui_screen_wipe();
+  secbool wipe_result = erase_device(ui_screen_wipe_progress);
+
+  if (sectrue != wipe_result) {
+    if (iface != NULL) {
+      send_msg_failure(iface, FailureType_Failure_ProcessError,
+                       "Could not erase flash");
+    }
+    screen_wipe_fail();
+    return WF_ERROR;
+  }
+
+  if (iface != NULL) {
+    send_msg_success(iface, NULL);
+  }
+  screen_wipe_success();
+  return WF_OK;
+}
diff --git a/core/embed/projects/bootloader/workflow/workflow.h b/core/embed/projects/bootloader/workflow/workflow.h
new file mode 100644
index 0000000000..2a45dac4b1
--- /dev/null
+++ b/core/embed/projects/bootloader/workflow/workflow.h
@@ -0,0 +1,68 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <trezor_types.h>
+
+#include <util/image.h>
+
+#include "protob/protob.h"
+
+typedef enum {
+  WF_ERROR_FATAL = 0,
+  WF_ERROR = 0x11223344,
+  WF_OK = 0x7ABBCCDD,
+  WF_OK_REBOOT_SELECTED = 0x68A4DABF,
+  WF_OK_FIRMWARE_INSTALLED = 0x04D9D07F,
+  WF_OK_DEVICE_WIPED = 0x30DC3841,
+  WF_OK_BOOTLOADER_UNLOCKED = 0x23FCBD03,
+  WF_CANCELLED = 0x55667788,
+} workflow_result_t;
+
+workflow_result_t workflow_firmware_update(protob_io_t *iface);
+
+workflow_result_t workflow_wipe_device(protob_io_t *iface);
+
+#ifdef USE_OPTIGA
+workflow_result_t workflow_unlock_bootloader(protob_io_t *iface);
+#endif
+
+workflow_result_t workflow_ping(protob_io_t *iface);
+
+workflow_result_t workflow_initialize(protob_io_t *iface,
+                                      const vendor_header *const vhdr,
+                                      const image_header *const hdr);
+
+workflow_result_t workflow_get_features(protob_io_t *iface,
+                                        const vendor_header *const vhdr,
+                                        const image_header *const hdr);
+
+workflow_result_t workflow_bootloader(const vendor_header *const vhdr,
+                                      const image_header *const hdr,
+                                      secbool firmware_present);
+
+workflow_result_t workflow_empty_device(void);
+
+workflow_result_t workflow_host_control(const vendor_header *const vhdr,
+                                        const image_header *const hdr,
+                                        void (*redraw_wait_screen)(void));
+
+workflow_result_t workflow_auto_update(const vendor_header *const vhdr,
+                                       const image_header *const hdr);
diff --git a/core/embed/projects/bootloader_ci/messages.c b/core/embed/projects/bootloader_ci/messages.c
index 9e3251f7a4..67f91cf77f 100644
--- a/core/embed/projects/bootloader_ci/messages.c
+++ b/core/embed/projects/bootloader_ci/messages.c
@@ -23,7 +23,7 @@
 #include <pb.h>
 #include <pb_decode.h>
 #include <pb_encode.h>
-#include "messages.pb.h"
+#include "pb/messages.pb.h"
 
 #include <io/usb.h>
 #include <util/flash.h>
diff --git a/core/site_scons/models/D002/discovery2.py b/core/site_scons/models/D002/discovery2.py
index 1a1bc8ea33..f3499e8df8 100644
--- a/core/site_scons/models/D002/discovery2.py
+++ b/core/site_scons/models/D002/discovery2.py
@@ -102,7 +102,6 @@ def configure(
         "USE_TAMPER=1",
         "USE_FLASH_BURST=1",
         "USE_OEM_KEYS_CHECK=1",
-        "USE_RESET_TO_BOOT=1",
     ]
 
     env.get("ENV")["LINKER_SCRIPT"] = linker_script
diff --git a/core/site_scons/models/T3B1/trezor_t3b1_revB.py b/core/site_scons/models/T3B1/trezor_t3b1_revB.py
index f307a852b3..fd8bc66bb8 100644
--- a/core/site_scons/models/T3B1/trezor_t3b1_revB.py
+++ b/core/site_scons/models/T3B1/trezor_t3b1_revB.py
@@ -106,7 +106,6 @@ def configure(
         ("USE_STORAGE_HWKEY", "1"),
         ("USE_TAMPER", "1"),
         ("USE_FLASH_BURST", "1"),
-        ("USE_RESET_TO_BOOT", "1"),
         ("USE_OEM_KEYS_CHECK", "1"),
         ("USE_PVD", "1"),
     ]
diff --git a/core/site_scons/models/T3T1/trezor_t3t1_revE.py b/core/site_scons/models/T3T1/trezor_t3t1_revE.py
index 6ed6184ad0..a2042f34d0 100644
--- a/core/site_scons/models/T3T1/trezor_t3t1_revE.py
+++ b/core/site_scons/models/T3T1/trezor_t3t1_revE.py
@@ -139,7 +139,6 @@ def configure(
         ("USE_STORAGE_HWKEY", "1"),
         ("USE_TAMPER", "1"),
         ("USE_FLASH_BURST", "1"),
-        ("USE_RESET_TO_BOOT", "1"),
         ("USE_OEM_KEYS_CHECK", "1"),
         ("USE_PVD", "1"),
     ]
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revA.py b/core/site_scons/models/T3W1/trezor_t3w1_revA.py
index 92e0e8190a..8f94eca4bb 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revA.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revA.py
@@ -223,7 +223,6 @@ def configure(
         ("USE_TAMPER", "1"),
         ("USE_FLASH_BURST", "1"),
         ("USE_OEM_KEYS_CHECK", "1"),
-        ("USE_RESET_TO_BOOT", "1"),
     ]
 
     sources += [
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revB.py b/core/site_scons/models/T3W1/trezor_t3w1_revB.py
index cbc89f833e..6d81b310fd 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revB.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revB.py
@@ -228,7 +228,6 @@ def configure(
         ("USE_TAMPER", "1"),
         ("USE_FLASH_BURST", "1"),
         ("USE_OEM_KEYS_CHECK", "1"),
-        ("USE_RESET_TO_BOOT", "1"),
     ]
 
     sources += [
diff --git a/tools/style.c.exclude b/tools/style.c.exclude
index 8d9ca1e4ca..11fed97082 100644
--- a/tools/style.c.exclude
+++ b/tools/style.c.exclude
@@ -1,4 +1,4 @@
-^\./core/embed/projects/bootloader/protob/
+^\./core/embed/projects/bootloader/protob/pb/
 ^\./crypto/aes/
 ^\./crypto/chacha20poly1305/
 ^\./crypto/ed25519-donna/