text: # This file is part of the MicroPython project, http://micropython.org/
  #
  # The MIT License (MIT)
  #
  # Copyright (c) 2020 Damien P. George
  # Copyright (c) 2020 Jim Mussared
  #
  # Permission is hereby granted, free of charge, to any person obtaining a copy
  # of this software and associated documentation files (the "Software"), to deal
  # in the Software without restriction, including without limitation the rights
  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  # copies of the Software, and to permit persons to whom the Software is
  # furnished to do so, subject to the following conditions:
  #
  # The above copyright notice and this permission notice shall be included in
  # all copies or substantial portions of the Software.
  #
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  # THE SOFTWARE.
  
  # This script provides helpers for working with the FUS/WS firmware on the WB55.
  # It can be frozen into the MicroPython firmware (via manifest.py)
  #
  # The current FUS and WS firmware version and state can be queried via the
  # `stm` module, e.g.
  # stm.rfcore_status()  (returns the first word of the device info table)
  # stm.rfcore_fw_version(id)  (returns a 5-tuple indicating fw version; id is: 0=FUS, 1=WS)
  # stm.rfcore_sys_hci(ogf, ocf, cmd_buf)  (synchronously execute HCI command on SYS channel)
  #
  # To perform a firmware update:
  #
  # 1. Generate "obfuscated" binary images using rfcore_makefirmware.py
  #  ./boards/NUCLEO_WB55/rfcore_makefirmware.py ~/src/github.com/STMicroelectronics/STM32CubeWB/Projects/STM32WB_Copro_Wireless_Binaries/STM32WB5x/ /tmp
  #  This will generate /tmp/{fus_102,fus_110,ws_ble_hci}.bin
  #
  # 2. Copy required files to the device filesystem.
  # In general, it's always safe to copy all three files and the updater will
  # figure out what needs to be done. This is the recommended option.
  # However, if you already have the latest FUS (1.1.0) installed, then just the
  # WS firmware is required.
  # If a FUS binary is present, then the existing WS will be removed so it's a good
  # idea to always include the WS binary if updating FUS.
  # Note that a WS binary will not be installed unless FUS 1.1.0 is installed.
  #
  # 3. Ensure boot.py calls `rfcore_firmware.resume()`.
  # The WB55 will reset several times during the firmware update process, so this
  # script manages the update state using RTC backup registers.
  # `rfcore_firmware.resume()` will continue the update operation on startup to
  # resume any in-progress update operation, and either trigger another reset, or
  # return 0 to indicate that the operation completed successfully, or a reason
  # code (see REASON_* below) to indicate failure.
  #
  # 4. Call rfcore_firmware.check_for_updates() to start the update process.
  # The device will then immediately reboot and when the firmware update completes,
  # the status will be returned from rfcore_firmware.resume(). See the REASON_ codes below.
  # You can use the built-in stm.rfcore_fw_version() to query the installed version
  # from your application code.
  
  import struct, os
  import machine, stm
  from micropython import const
  
  _OGF_VENDOR = const(0x3F)
  
  _OCF_FUS_GET_STATE = const(0x52)
  _OCF_FUS_FW_UPGRADE = const(0x54)
  _OCF_FUS_FW_DELETE = const(0x55)
  _OCF_FUS_START_WS = const(0x5A)
  _OCF_BLE_INIT = const(0x66)
  
  _HCI_KIND_VENDOR_RESPONSE = const(0x11)
  
  
  # The firmware updater will search all of flash for the image to install, so
  # it's important that the file doesn't exist anywhere on the filesystem and
  # that the updater only finds the version that we copy into the reserved area.
  # Otherwise it will find matching headers/footers in the flash filesystem and
  # get confused leading to either "FUS_STATE_IMG_NOT_AUTHENTIC" or (worse)
  # corrupting the FUS.
  # See footnote [1] referenced by Table 9 in AN5185 - Rev 4 -- the address
  # passed to FUS_FW_UPGRADE is ignored (implying that it must be searching the
  # flash). This requires that the firmware files have been pre-processed by
  # rfcore_makefirmware.py and this key must match the one there.
  _OBFUSCATION_KEY = const(0x0573B55AA)
  
  # On boards using the internal flash filesystem, this must match the
  # `_micropy_hw_internal_flash_storage_end` symbol defined by the linker script
  # (see eg boards/stm32wb55xg.ld).
  # We erase everything from here until the start of the secure area (defined by
  # SFSA) just to ensure that no other fragments of firmware files are left
  # behind. On boards with external flash, this just needs to ensure that it
  # includes any regions that may contain partial firmware data.
  # This is non-const so it can be override.
  STAGING_AREA_START = 0x80C0000
  
  # First word of device info table indicating FUS state (returned by `stm.rfcore_status()`).
  _MAGIC_FUS_ACTIVE = const(0xA94656B9)  # AN5185
  _MAGIC_IPCC_MEM_INCORRECT = const(0x3DE96F61)  # # AN5185
  
  # Argument to `stm.rfcore_fw_version()`.
  _FW_VERSION_FUS = const(0)
  _FW_VERSION_WS = const(1)
  
  # No firmware update in progress. Boot normally.
  _STATE_IDLE = const(0)
  
  # A previous firmware update failed. Will return reason code from resume().
  _STATE_FAILED = const(1)
  
  # Trying to get into the FUS. Keep issuing GET_STATE until the FUS is active.
  _STATE_WAITING_FOR_FUS = const(2)
  
  # Trying to get into the WS. Keep issuing START_WS until the WS is active (or fails).
  _STATE_WAITING_FOR_WS = const(3)
  
  # FW_DELETE has been issued. Waiting for the WS version to report zero.
  _STATE_DELETING_WS = const(4)
  
  # Flash copy has started for FUS/WS. If a reboot occurs, then fail.
  _STATE_COPYING_FUS = const(5)
  _STATE_COPYING_WS = const(6)
  
  # Flash write fully completed, ready for install.
  _STATE_COPIED_FUS = const(7)
  _STATE_COPIED_WS = const(8)
  
  # Check for next update to perform.
  # Either we've just gotten into the FUS, or the first update in a sequence
  # has completed. (e.g. FUS done, now do WS).
  _STATE_CHECK_UPDATES = const(9)
  
  # Installation has started, keep polling GET_STATE.
  _STATE_INSTALLING_WS = const(10)
  _STATE_INSTALLING_FUS = const(11)
  
  # Update completed successfully.
  REASON_OK = const(0)
  # The device reset during flash copy. Possibly WS still installed.
  REASON_FLASH_COPY_FAILED = const(1)
  # Unable to start the WS after firmware update.
  REASON_NO_WS = const(2)
  # Copying FUS image to staging area caused FUS to fail.
  REASON_FLASH_FUS_BAD_STATE = const(3)
  # Copying WS image to staging area caused FUS to fail.
  REASON_FLASH_WS_BAD_STATE = const(4)
  # Cannot get into the FUS. Perhaps rfcore misconfigured.
  REASON_FUS_NOT_RESPONDING = const(5)
  # After a FUS install, unable to get back to the FUS.
  REASON_FUS_NOT_RESPONDING_AFTER_FUS = const(6)
  # After a WS install, unable to get back to the FUS.
  REASON_FUS_NOT_RESPONDING_AFTER_WS = const(7)
  # Unable to query rfcore version/active.
  REASON_RFCORE_NOT_CONFIGURED = const(8)
  # The WS deletion didn't have any effect.
  REASON_WS_STILL_PRESENT = const(9)
  # FUS refused to delete the WS.
  REASON_WS_DELETION_FAILED = const(10)
  # FUS returned a specific code for a FUS update.
  # See AN5185 Rev 4, Table 12. Reason between 0x00-0x11 will be added.
  REASON_FUS_VENDOR = const(0x10)
  # FUS returned a specific code for a WS update. Values as for the FUS update.
  REASON_WS_VENDOR = const(0x30)
  
  # FUS 1.0.2 must be installed before FUS 1.1.0 can be installed.
  # A factory Nucleo board has FUS (0, 5, 3, 0, 0) and WS (0, 5, 1, 0, 0).
  _FUS_VERSION_102 = (1, 0, 2, 0, 0)
  _FUS_VERSION_110 = (1, 1, 0, 0, 0)
  _PATH_FUS_102 = "fus_102.bin"
  _PATH_FUS_110 = "fus_110.bin"
  _PATH_WS_BLE_HCI = "ws_ble_hci.bin"
  
  # This address is correct for versions up to v1.8 (assuming existing firmware deleted).
  # Note any address from the end of the filesystem to the SFSA would be fine, but if
  # the FUS is fixed in the future to use the specified address then these are the "correct"
  # ones.
  _ADDR_FUS = 0x080EC000
  _ADDR_WS_BLE_HCI = 0x080DC000
  
  # When installing the FUS/WS it can take a long time to return to the first
  # GET_STATE HCI command.
  # e.g. Installing stm32wb5x_BLE_Stack_full_fw.bin takes 3600ms to respond.
  _INSTALLING_FUS_GET_STATE_TIMEOUT = const(1000)
  _INSTALLING_WS_GET_STATE_TIMEOUT = const(6000)
  
  
  def log(msg, *args, **kwargs):
      print("[rfcore update]", msg.format(*args, **kwargs))
  
  
  class _Flash:
      _FLASH_KEY1 = 0x45670123
      _FLASH_KEY2 = 0xCDEF89AB
  
      _FLASH_CR_STRT_MASK = 1 << 16
      _FLASH_CR_LOCK_MASK = 1 << 31
      _FLASH_SR_BSY_MASK = 1 << 16
  
      def wait_not_busy(self):
          while machine.mem32[stm.FLASH + stm.FLASH_SR] & _Flash._FLASH_SR_BSY_MASK:
              machine.idle()
  
      def unlock(self):
          if machine.mem32[stm.FLASH + stm.FLASH_CR] & _Flash._FLASH_CR_LOCK_MASK:
              # Only unlock if already locked (i.e. FLASH_CR_LOCK is set).
              machine.mem32[stm.FLASH + stm.FLASH_KEYR] = _Flash._FLASH_KEY1
              machine.mem32[stm.FLASH + stm.FLASH_KEYR] = _Flash._FLASH_KEY2
          else:
              log("Flash was already unlocked.")
  
      def lock(self):
          machine.mem32[stm.FLASH + stm.FLASH_CR] = _Flash._FLASH_CR_LOCK_MASK
  
      def erase_page(self, page):
          assert 0 <= page <= 255  # 1MiB range (4k page)
          self.wait_not_busy()
          cr = page << 3 | 1 << 1  # PNB  # PER
          machine.mem32[stm.FLASH + stm.FLASH_CR] = cr
          machine.mem32[stm.FLASH + stm.FLASH_CR] = cr | _Flash._FLASH_CR_STRT_MASK
          self.wait_not_busy()
          machine.mem32[stm.FLASH + stm.FLASH_CR] = 0
  
      def write(self, addr, buf, sz, key=0):
          assert sz % 4 == 0
          self.wait_not_busy()
          cr = 1 << 0  # PG
          machine.mem32[stm.FLASH + stm.FLASH_CR] = cr
          off = 0
          while off < sz:
              v = (buf[off]) | (buf[off + 1] << 8) | (buf[off + 2] << 16) | (buf[off + 3] << 24)
              machine.mem32[addr + off] = v ^ key
              off += 4
              if off % 8 == 0:
                  self.wait_not_busy()
          if off % 8:
              machine.mem32[addr + off] = 0
              self.wait_not_busy()
          machine.mem32[stm.FLASH + stm.FLASH_CR] = 0
  
  
  def _copy_file_to_flash(filename, addr):
      flash = _Flash()
      flash.unlock()
      try:
          # Erase the entire staging area in flash.
          erase_addr = STAGING_AREA_START
          sfr_sfsa = machine.mem32[stm.FLASH + stm.FLASH_SFR] & 0xFF
          erase_limit = 0x08000000 + sfr_sfsa * 4096
          while erase_addr < erase_limit:
              flash.erase_page((erase_addr - 0x08000000) // 4096)
              erase_addr += 4096
  
          # Write the contents of the firmware (note flash.write will apply the
          # XOR de-obfuscation).
          with open(filename, "rb") as f:
              buf = bytearray(4096)
  
              while 1:
                  sz = f.readinto(buf)
                  if sz == 0:
                      break
                  flash.write(addr, buf, sz, _OBFUSCATION_KEY)
                  addr += 4096
  
      finally:
          flash.lock()
  
  
  def _parse_vendor_response(data):
      assert len(data) >= 7
      assert data[0] == _HCI_KIND_VENDOR_RESPONSE
      assert data[1] == 0x0E
      # assert data[3] == 0xff # "Num HCI" -- docs say 0xff, but we see 0x01
      op = (data[5] << 8) | data[4]
      return (op >> 10, op & 0x3FF, data[6], data[7] if len(data) > 7 else 0)
  
  
  def _run_sys_hci_cmd(ogf, ocf, buf=b"", timeout=0):
      try:
          ogf_out, ocf_out, status, result = _parse_vendor_response(
              stm.rfcore_sys_hci(ogf, ocf, buf, timeout)
          )
      except OSError:
          # Timeout or FUS not active.
          return (0xFF, 0xFF)
      assert ogf_out == ogf
      assert ocf_out == ocf
      return (status, result)
  
  
  def fus_get_state(timeout=0):
      return _run_sys_hci_cmd(_OGF_VENDOR, _OCF_FUS_GET_STATE, timeout=timeout)
  
  
  def fus_is_idle():
      return fus_get_state() == (0, 0)
  
  
  def fus_start_ws():
      return _run_sys_hci_cmd(_OGF_VENDOR, _OCF_FUS_START_WS)
  
  
  def _fus_fwdelete():
      return _run_sys_hci_cmd(_OGF_VENDOR, _OCF_FUS_FW_DELETE)
  
  
  def _fus_run_fwupgrade(addr):
      # Note: Address is ignored by the FUS (see comments above).
      return _run_sys_hci_cmd(_OGF_VENDOR, _OCF_FUS_FW_UPGRADE, struct.pack("<I", addr))
  
  
  # Get/set current state/reason to RTC Backup Domain.
  # Using the second- and third-last registers (17, 18) as the final one (19)
  # is reserved by powerctrl.c for restoring the frequency.
  # Can be overridden if necessary.
  REG_RTC_STATE = stm.RTC + stm.RTC_BKP18R
  REG_RTC_REASON = stm.RTC + stm.RTC_BKP17R
  
  
  def _read_state():
      return machine.mem32[REG_RTC_STATE]
  
  
  def _write_state(state):
      machine.mem32[REG_RTC_STATE] = state
  
  
  def _read_failure_reason():
      return machine.mem32[REG_RTC_REASON]
  
  
  def _write_failure_state(reason):
      machine.mem32[REG_RTC_REASON] = reason
      _write_state(_STATE_FAILED)
      return reason
  
  
  # Check for the presence of a given file and attempt to start installing it.
  def _stat_and_start_copy(path, addr, copying_state, copied_state):
      try:
          os.stat(path)
      except OSError:
          log("{} not found", path)
          return False
  
      log("{} update is available", path)
      if sum(stm.rfcore_fw_version(_FW_VERSION_WS)):
          # There was some WS firmware already installed. Need to remove that
          # before copying to flash (both FUS or WS copy require this).
          log("Removing existing WS firmware")
          _write_state(_STATE_DELETING_WS)
          _fus_fwdelete()
      else:
          log("Copying {} to flash", path)
          # Mark that the flash write has started. Any failure should result in an overall failure.
          _write_state(copying_state)  # Either _STATE_COPYING_FUS or _STATE_COPYING_WS
          _copy_file_to_flash(path, addr)
          log("Copying complete")
          # The entire write has completed successfully, start the install.
          _write_state(copied_state)  # Either _STATE_COPIED_FUS or _STATE_COPIED_WS
  
      return True
  
  
  # This should be called in boot.py to resume any in-progress update.
  # If there's nothing to do, it will return 0 and the app can continue as normal.
  # If a previous update has failed, then it will return the failure reason.
  # Otherwise it will attempt to continue the update from where it left off.
  def resume():
      log("Checking firmware update progress...")
  
      if stm.rfcore_status() == _MAGIC_IPCC_MEM_INCORRECT:
          return _write_failure_state(REASON_RFCORE_NOT_CONFIGURED)
  
      while True:
          state = _read_state()
  
          if state == _STATE_IDLE:
              log("Firmware update complete")
              return 0
  
          elif state == _STATE_FAILED:
              log("Firmware update failed")
              return _read_failure_reason()
  
          # Keep calling GET_STATE until error or FUS.
          elif state == _STATE_WAITING_FOR_FUS:
              log("Querying FUS state")
              status, result = fus_get_state()
              log("FUS state: {} {}", status, result)
  
              if status == 0xFF and result == 0xFF:
                  _write_failure_state(REASON_FUS_NOT_RESPONDING)
              elif status != 0:
                  log("Operation in progress. Re-querying FUS state")
              elif stm.rfcore_status() == _MAGIC_FUS_ACTIVE:
                  log("FUS active")
                  _write_state(_STATE_CHECK_UPDATES)
  
          # Keep trying to start the WS until !fus_active() (or error).
          elif state == _STATE_WAITING_FOR_WS:
              if stm.rfcore_status() != _MAGIC_FUS_ACTIVE:
                  log("WS active")
                  _write_state(_STATE_IDLE)
                  # Need to force a reset otherwise BLE will fail if FUS has changed.
                  machine.reset()
              else:
                  log("Starting WS")
                  status, result = fus_start_ws()
                  if status != 0:
                      log("Can't start WS")
                      log("WS version: {}", stm.rfcore_fw_version(_FW_VERSION_WS))
                      _write_failure_state(REASON_NO_WS)
  
          # Sequence the FUS 1.0.2 -> FUS 1.1.0 -> WS (depending on what's available).
          elif state == _STATE_CHECK_UPDATES:
              log("Checking for updates")
              fus_version = stm.rfcore_fw_version(_FW_VERSION_FUS)
              log("FUS version {}", fus_version)
              if fus_version < _FUS_VERSION_102:
                  log("Factory FUS detected")
                  if _stat_and_start_copy(
                      _PATH_FUS_102, _ADDR_FUS, _STATE_COPYING_FUS, _STATE_COPIED_FUS
                  ):
                      continue
              elif fus_version >= _FUS_VERSION_102 and fus_version < _FUS_VERSION_110:
                  log("FUS 1.0.2 detected")
                  if _stat_and_start_copy(
                      _PATH_FUS_110, _ADDR_FUS, _STATE_COPYING_FUS, _STATE_COPIED_FUS
                  ):
                      continue
              else:
                  log("FUS is up-to-date")
  
              if fus_version >= _FUS_VERSION_110:
                  if _stat_and_start_copy(
                      _PATH_WS_BLE_HCI, _ADDR_WS_BLE_HCI, _STATE_COPYING_WS, _STATE_COPIED_WS
                  ):
                      continue
                  else:
                      log("No WS updates available")
              else:
                  # Don't attempt to install WS if we're running an old FUS.
                  log("Need latest FUS to install WS")
  
              # Attempt to go back to WS.
              # Either this will fail (because WS was removed due to FUS install), or
              # this whole thing was a no-op and we should be fine to restart WS.
              _write_state(_STATE_WAITING_FOR_WS)
  
          # This shouldn't happen - the flash write should always complete and
          # move straight onto the COPIED state. Failure here indicates that
          # the rfcore is misconfigured or the WS firmware was not deleted first.
          elif state == _STATE_COPYING_FUS or state == _STATE_COPYING_WS:
              log("Flash copy failed mid-write")
              _write_failure_state(REASON_FLASH_COPY_FAILED)
  
          # Flash write completed, we should immediately see GET_STATE return 0,0
          # so we can start the FUS install.
          elif state == _STATE_COPIED_FUS:
              if fus_is_idle():
                  log("FUS copy complete, installing")
                  _write_state(_STATE_INSTALLING_FUS)
                  _fus_run_fwupgrade(_ADDR_FUS)
              else:
                  log("FUS copy bad state")
                  _write_failure_state(REASON_FLASH_FUS_BAD_STATE)
  
          # Keep polling the state until we see a 0,0 (success) or non-transient
          # error. In general we should expect to see (16,0) several times,
          # followed by a (255,0), followed by (0, 0).
          elif state == _STATE_INSTALLING_FUS:
              log("Installing FUS...")
              status, result = fus_get_state(_INSTALLING_FUS_GET_STATE_TIMEOUT)
              log("FUS state: {} {}", status, result)
              if 0x20 <= status <= 0x2F and result == 0:
                  # FUS_STATE_FUS_UPGRD_ONGOING
                  log("FUS still in progress...")
              elif 0x10 <= status <= 0x1F and result == 0x11:
                  # FUS_STATE_FW_UPGRD_ONGOING and FUS_FW_ROLLBACK_ERROR
                  # Confusingly this is a "FW_UPGRD" (0x10) not "FUS_UPRD" (0x20).
                  log("Attempted to install same FUS version... re-querying FUS state to resume.")
              elif status == 0:
                  log("FUS update successful")
                  _write_state(_STATE_CHECK_UPDATES)
              elif result == 0:
                  # See below (for equivalent path for WS install -- we
                  # sometimes see (255,0) right at the end).
                  log("Re-querying FUS state...")
              elif result == 0xFF:
                  _write_failure_state(REASON_FUS_NOT_RESPONDING_AFTER_FUS)
              else:
                  _write_failure_state(REASON_FUS_VENDOR + result)
  
          # Keep polling the state until we see 0,0 or failure (1,0). Any other
          # result means retry (but the docs say that 0 and 1 are the only
          # status values).
          elif state == _STATE_DELETING_WS:
              log("Deleting WS...")
              status, result = fus_get_state()
              log("FUS state: {} {}", status, result)
              if status == 0:
                  if sum(stm.rfcore_fw_version(_FW_VERSION_WS)) == 0:
                      log("WS deletion complete")
                      _write_state(_STATE_CHECK_UPDATES)
                  else:
                      log("WS deletion no effect")
                      _write_failure_state(REASON_WS_STILL_PRESENT)
              elif status == 1:
                  log("WS deletion failed")
                  _write_failure_state(REASON_WS_DELETION_FAILED)
  
          # As for _STATE_COPIED_FUS above. We should immediately see 0,0.
          elif state == _STATE_COPIED_WS:
              if fus_is_idle():
                  log("WS copy complete, installing")
                  _write_state(_STATE_INSTALLING_WS)
                  _fus_run_fwupgrade(_ADDR_WS_BLE_HCI)
              else:
                  log("WS copy bad state")
                  _write_failure_state(REASON_FLASH_WS_BAD_STATE)
  
          # As for _STATE_INSTALLING_FUS above.
          elif state == _STATE_INSTALLING_WS:
              log("Installing WS...")
              status, result = fus_get_state(_INSTALLING_WS_GET_STATE_TIMEOUT)
              log("FUS state: {} {}", status, result)
              if 0x10 <= status <= 0x1F and result == 0:
                  # FUS_STATE_FW_UPGRD_ONGOING
                  log("WS still in progress...")
              elif 0x10 <= status <= 0x1F and result == 0x11:
                  # FUS_FW_ROLLBACK_ERROR
                  log("Attempted to install same WS version... re-querying FUS state to resume.")
              elif status == 0:
                  log("WS update successful")
                  _write_state(_STATE_WAITING_FOR_WS)
              elif result in (0, 0xFE):
                  # We get an error response with no payload sometimes at the end
                  # of the update (this is not in AN5185). Additionally during
                  # WS update, newer WS reports (status, result) of (255, 254)
                  # before eventually reporting the correct state of
                  # _STATE_INSTALLING_WS once again. In these cases, re-try the
                  # GET_STATE.
                  # The same thing happens transitioning from WS to FUS mode.
                  # The actual HCI response has no payload, the result=0 comes from
                  # _parse_vendor_response above when len=7.
                  log("Re-querying FUS state...")
              elif result == 0xFF:
                  # This is specifically a failure sending the HCI command.
                  _write_failure_state(REASON_FUS_NOT_RESPONDING_AFTER_WS)
              else:
                  _write_failure_state(REASON_WS_VENDOR + result)
  
  
  # Start a firmware update.
  # This will immediately trigger a reset and start the update process on boot.
  def check_for_updates():
      log("Starting firmware update")
      _write_state(_STATE_WAITING_FOR_FUS)
      machine.reset()
  