#!/usr/bin/env python3

# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# 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 Software shall not be used in any kind of system which includes amongst
#   its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
#   an artificial intelligence, machine learning or language model training
#   dataset, including but not limited to any use that contributes to the
#   training or development of such a model or algorithm.
#
# - 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.

from time import sleep
import argparse
import threading
import sys
import os
import os.path
import struct
import datetime
import time
import math
import hashlib
import zipfile
from urllib.request import urlretrieve
from importlib import util
import RNS

RNS.logtimefmt      = "%H:%M:%S"
RNS.compact_log_fmt = True

program_version = "2.5.0"
eth_addr = "0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E"
btc_addr = "bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6"
xmr_addr = "87HcDx6jRSkMQ9nPRd5K9hGGpZLn2s7vWETjMaVM5KfV4TD36NcYa8J8WSxhTSvBzzFpqDwp2fg5GX2moZ7VAP9QMZCZGET"

rnode = None
rnode_serial = None
rnode_port = None
rnode_baudrate = 115200
known_keys = [["unsigned.io", "30819f300d06092a864886f70d010101050003818d0030818902818100bf831ebd99f43b477caf1a094bec829389da40653e8f1f83fc14bf1b98a3e1cc70e759c213a43f71e5a47eb56a9ca487f241335b3e6ff7cdde0ee0a1c75c698574aeba0485726b6a9dfc046b4188e3520271ee8555a8f405cf21f81f2575771d0b0887adea5dd53c1f594f72c66b5f14904ffc2e72206a6698a490d51ba1105b0203010001"], ["unsigned.io", "30819f300d06092a864886f70d010101050003818d0030818902818100e5d46084e445595376bf7efd9c6ccf19d39abbc59afdb763207e4ff68b8d00ebffb63847aa2fe6dd10783d3ea63b55ac66f71ad885c20e223709f0d51ed5c6c0d0b093be9e1d165bb8a483a548b67a3f7a1e4580f50e75b306593fa6067ae259d3e297717bd7ff8c8f5b07f2bed89929a9a0321026cf3699524db98e2d18fb2d020300ff39"]]
firmware_update_url = "https://github.com/markqvist/RNode_Firmware/releases/download/"
fw_filename = None
fw_url = None
mapped_model = None

class KISS():
    FEND            = 0xC0
    FESC            = 0xDB
    TFEND           = 0xDC
    TFESC           = 0xDD
    
    CMD_UNKNOWN     = 0xFE
    CMD_DATA        = 0x00
    CMD_FREQUENCY   = 0x01
    CMD_BANDWIDTH   = 0x02
    CMD_TXPOWER     = 0x03
    CMD_SF          = 0x04
    CMD_CR          = 0x05
    CMD_RADIO_STATE = 0x06
    CMD_RADIO_LOCK  = 0x07
    CMD_DETECT      = 0x08
    CMD_LEAVE       = 0x0A
    CMD_READY       = 0x0F
    CMD_STAT_RX     = 0x21
    CMD_STAT_TX     = 0x22
    CMD_STAT_RSSI   = 0x23
    CMD_STAT_SNR    = 0x24
    CMD_BLINK       = 0x30
    CMD_RANDOM      = 0x40
    CMD_DISP_INT    = 0x45
    CMD_NP_INT      = 0x65
    CMD_DISP_ADR    = 0x63
    CMD_DISP_BLNK   = 0x64
    CMD_DISP_ROT    = 0x67
    CMD_DISP_RCND   = 0x68
    CMD_BT_CTRL     = 0x46
    CMD_BT_PIN      = 0x62
    CMD_DIS_IA      = 0x69
    CMD_WIFI_MODE   = 0x6A
    CMD_WIFI_SSID   = 0x6B
    CMD_WIFI_PSK    = 0x6C
    CMD_WIFI_CHN    = 0x6E
    CMD_WIFI_IP     = 0x84
    CMD_WIFI_NM     = 0x85
    CMD_BOARD       = 0x47
    CMD_PLATFORM    = 0x48
    CMD_MCU         = 0x49
    CMD_FW_VERSION  = 0x50
    CMD_CFG_READ    = 0x6D
    CMD_ROM_READ    = 0x51
    CMD_ROM_WRITE   = 0x52
    CMD_ROM_WIPE    = 0x59
    CMD_CONF_SAVE   = 0x53
    CMD_CONF_DELETE = 0x54
    CMD_RESET       = 0x55
    CMD_DEV_HASH    = 0x56
    CMD_DEV_SIG     = 0x57
    CMD_HASHES      = 0x60
    CMD_FW_HASH     = 0x58
    CMD_FW_UPD      = 0x61

    DETECT_REQ      = 0x73
    DETECT_RESP     = 0x46
    
    RADIO_STATE_OFF = 0x00
    RADIO_STATE_ON  = 0x01
    RADIO_STATE_ASK = 0xFF
    
    CMD_ERROR           = 0x90
    ERROR_INITRADIO     = 0x01
    ERROR_TXFAILED      = 0x02
    ERROR_EEPROM_LOCKED = 0x03

    @staticmethod
    def escape(data):
        data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
        data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
        return data

class ROM():
    PLATFORM_AVR        = 0x90
    PLATFORM_ESP32      = 0x80
    PLATFORM_NRF52      = 0x70

    MCU_1284P           = 0x91
    MCU_2560            = 0x92
    MCU_ESP32           = 0x81
    MCU_NRF52           = 0x71

    PRODUCT_RNODE       = 0x03
    MODEL_A1            = 0xA1
    MODEL_A6            = 0xA6
    MODEL_A4            = 0xA4
    MODEL_A9            = 0xA9
    MODEL_A3            = 0xA3
    MODEL_A8            = 0xA8
    MODEL_A2            = 0xA2
    MODEL_A7            = 0xA7
    MODEL_A5            = 0xA5
    MODEL_AA            = 0xAA
    MODEL_AC            = 0xAC

    PRODUCT_T32_10      = 0xB2
    MODEL_BA            = 0xBA
    MODEL_BB            = 0xBB

    PRODUCT_T32_20      = 0xB0
    MODEL_B3            = 0xB3
    MODEL_B8            = 0xB8

    PRODUCT_T32_21      = 0xB1
    MODEL_B4            = 0xB4
    MODEL_B9            = 0xB9
    MODEL_B4_TCXO       = 0x04 # The TCXO model codes are only used here to select the correct firmware,
    MODEL_B9_TCXO       = 0x09 # actual model codes in firmware is still 0xB4 and 0xB9.
    
    PRODUCT_H32_V2      = 0xC0
    MODEL_C4            = 0xC4
    MODEL_C9            = 0xC9

    PRODUCT_H32_V3      = 0xC1
    MODEL_C5            = 0xC5
    MODEL_CA            = 0xCA

    PRODUCT_H32_V4      = 0xC3
    MODEL_C8            = 0xC8 # 868/915/923 MHz with PA

    PRODUCT_TBEAM       = 0xE0
    MODEL_E4            = 0xE4
    MODEL_E9            = 0xE9
    MODEL_E3            = 0xE3
    MODEL_E8            = 0xE8

    PRODUCT_TBEAM_S_V1  = 0xEA
    MODEL_DB            = 0xDB
    MODEL_DC            = 0xDC

    PRODUCT_TDECK       = 0xD0
    MODEL_D4            = 0xD4
    MODEL_D9            = 0xD9

    PRODUCT_RAK4631     = 0x10
    MODEL_11            = 0x11
    MODEL_12            = 0x12
    MODEL_13            = 0x13
    MODEL_14            = 0x14

    PRODUCT_OPENCOM_XL  = 0x20
    MODEL_21            = 0x21

    PRODUCT_TECHO       = 0x15
    MODEL_16            = 0x16
    MODEL_17            = 0x17

    PRODUCT_HELTEC_T114 = 0xC2
    BOARD_HELTEC_T114   = 0x3C
    MODEL_C6            = 0xC6 # Heltec Mesh Node T114, 470-510 MHz (HT-n5262-LF)
    MODEL_C7            = 0xC7 # Heltec Mesh Node T114, 863-928 MHz (HT-n5262-HF)
    
    PRODUCT_XIAO_S3     = 0xEB
    BOARD_XIAO_S3       = 0x3E
    MODEL_DE            = 0xDE # Xiao ESP32S3 with Wio-SX1262 module, 433 MHz
    MODEL_DD            = 0xDD # Xiao ESP32S3 with Wio-SX1262 module, 868 MHz

    PRODUCT_HMBRW  = 0xF0
    MODEL_FF       = 0xFF
    MODEL_FE       = 0xFE

    ADDR_PRODUCT   = 0x00
    ADDR_MODEL     = 0x01
    ADDR_HW_REV    = 0x02
    ADDR_SERIAL    = 0x03
    ADDR_MADE      = 0x07
    ADDR_CHKSUM    = 0x0B
    ADDR_SIGNATURE = 0x1B
    ADDR_INFO_LOCK = 0x9B
    ADDR_CONF_SF   = 0x9C
    ADDR_CONF_CR   = 0x9D
    ADDR_CONF_TXP  = 0x9E
    ADDR_CONF_BW   = 0x9F
    ADDR_CONF_FREQ = 0xA3
    ADDR_CONF_OK   = 0xA7

    ADDR_CONF_BT   = 0xB0
    ADDR_CONF_DSET = 0xB1
    ADDR_CONF_DINT = 0xB2
    ADDR_CONF_DADR = 0xB3
    ADDR_CONF_DBLK = 0xB4
    ADDR_CONF_DROT = 0xB8
    ADDR_CONF_PSET = 0xB5
    ADDR_CONF_PINT = 0xB6
    ADDR_CONF_BSET = 0xB7
    ADDR_CONF_DIA  = 0xB9
    ADDR_CONF_WIFI = 0xBA
    ADDR_CONF_WCHN = 0xBB
    ADDR_CONF_SSID = 0x00
    ADDR_CONF_PSK  = 0x21
    ADDR_CONF_IP   = 0x42
    ADDR_CONF_NM   = 0x46

    INFO_LOCK_BYTE = 0x73
    CONF_OK_BYTE   = 0x73

    BOARD_RNODE         = 0x31
    BOARD_HMBRW         = 0x32
    BOARD_TBEAM         = 0x33
    BOARD_TDECK         = 0x3B
    BOARD_HUZZAH32      = 0x34
    BOARD_GENERIC_ESP32 = 0x35
    BOARD_LORA32_V2_0   = 0x36
    BOARD_LORA32_V2_1   = 0x37
    BOARD_TECHO         = 0x43
    BOARD_RAK4631       = 0x51

    MANUAL_FLASH_MODELS = []

mapped_product = ROM.PRODUCT_RNODE
products = {
    ROM.PRODUCT_RNODE:  "RNode",
    ROM.PRODUCT_HMBRW:  "Hombrew RNode",
    ROM.PRODUCT_TBEAM:  "LilyGO T-Beam",
    ROM.PRODUCT_TBEAM_S_V1:"LilyGO T-Beam Supreme",
    ROM.PRODUCT_TDECK:  "LilyGO T-Deck",
    ROM.PRODUCT_T32_10: "LilyGO LoRa32 v1.0",
    ROM.PRODUCT_T32_20: "LilyGO LoRa32 v2.0",
    ROM.PRODUCT_T32_21: "LilyGO LoRa32 v2.1",
    ROM.PRODUCT_H32_V2: "Heltec LoRa32 v2",
    ROM.PRODUCT_H32_V3: "Heltec LoRa32 v3",
    ROM.PRODUCT_H32_V4: "Heltec LoRa32 v4",
    ROM.PRODUCT_TECHO:  "LilyGO T-Echo",
    ROM.PRODUCT_RAK4631: "RAK4631",
    ROM.PRODUCT_OPENCOM_XL: "openCom XL",
    ROM.PRODUCT_HELTEC_T114: "Heltec Mesh Node T114",
    ROM.PRODUCT_XIAO_S3: "Seeed XIAO ESP32S3 Wio-SX1262",
}

platforms = {
    ROM.PLATFORM_AVR: "AVR",
    ROM.PLATFORM_ESP32:"ESP32",
    ROM.PLATFORM_NRF52: "NRF52",
}

mcus = {
    ROM.MCU_1284P: "ATmega1284P",
    ROM.MCU_2560:"ATmega2560",
    ROM.MCU_ESP32:"Espressif Systems ESP32",
    ROM.MCU_NRF52: "Nordic Semiconductor nRF52840",
}

models = {
    0xA4: [410000000, 525000000, 14, "410 - 525 MHz", "rnode_firmware.hex", "SX1278"],
    0xA9: [820000000, 1020000000, 17, "820 - 1020 MHz", "rnode_firmware.hex", "SX1276"],
    0xA1: [410000000, 525000000, 22, "410 - 525 MHz", "rnode_firmware_t3s3.zip", "SX1268"],
    0xA6: [820000000, 1020000000, 22, "820 - 960 MHz", "rnode_firmware_t3s3.zip", "SX1262"],
    0xA5: [410000000, 525000000, 17, "410 - 525 MHz", "rnode_firmware_t3s3_sx127x.zip", "SX1278"],
    0xAA: [820000000, 1020000000, 17, "820 - 960 MHz", "rnode_firmware_t3s3_sx127x.zip", "SX1276"],
    0xAC: [2400000000, 2500000000, 20, "2.4 - 2.5 GHz", "rnode_firmware_t3s3_sx1280_pa.zip", "SX1280"],
    0xA2: [410000000, 525000000, 17, "410 - 525 MHz", "rnode_firmware_ng21.zip", "SX1278"],
    0xA7: [820000000, 1020000000, 17, "820 - 1020 MHz", "rnode_firmware_ng21.zip", "SX1276"],
    0xA3: [410000000, 525000000, 17, "410 - 525 MHz", "rnode_firmware_ng20.zip", "SX1278"],
    0xA8: [820000000, 1020000000, 17, "820 - 1020 MHz", "rnode_firmware_ng20.zip", "SX1276"],
    0xB3: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_lora32v20.zip", "SX1278"],
    0xB8: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_lora32v20.zip", "SX1276"],
    0xB4: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_lora32v21.zip", "SX1278"],
    0xB9: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_lora32v21.zip", "SX1276"],
    0x04: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_lora32v21_tcxo.zip", "SX1278"],
    0x09: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_lora32v21_tcxo.zip", "SX1276"],
    0xBA: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_lora32v10.zip", "SX1278"],
    0xBB: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_lora32v10.zip", "SX1276"],
    0xC4: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_heltec32v2.zip", "SX1278"],
    0xC9: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_heltec32v2.zip", "SX1276"],
    0xC5: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_heltec32v3.zip", "SX1268"],
    0xCA: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_heltec32v3.zip", "SX1262"],
    0xC8: [860000000, 930000000, 28, "850 - 950 MHz", "rnode_firmware_heltec32v4pa.zip", "SX1262"],
    0xC6: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_heltec_t114.zip", "SX1268"],
    0xC7: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_heltec_t114.zip", "SX1262"],
    0xE4: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_tbeam.zip", "SX1278"],
    0xE9: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_tbeam.zip", "SX1276"],
    0xD4: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_tdeck.zip", "SX1268"],
    0xD9: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_tdeck.zip", "SX1262"],
    0xDB: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_tbeam_supreme.zip", "SX1268"],
    0xDC: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_tbeam_supreme.zip", "SX1262"],
    0xE3: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_tbeam_sx1262.zip", "SX1268"],
    0xE8: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_tbeam_sx1262.zip", "SX1262"],
    0x11: [430000000, 510000000, 22, "430 - 510 MHz", "rnode_firmware_rak4631.zip", "SX1262"],
    0x12: [779000000, 928000000, 22, "779 - 928 MHz", "rnode_firmware_rak4631.zip", "SX1262"],
    0x13: [430000000, 510000000, 22, "430 - 510 MHz", "rnode_firmware_rak4631_sx1280.zip", "SX1262 + SX1280"],
    0x14: [779000000, 928000000, 22, "779 - 928 MHz", "rnode_firmware_rak4631_sx1280.zip", "SX1262 + SX1280"],
    0x16: [779000000, 928000000, 22, "430 - 510 Mhz", "rnode_firmware_techo.zip", "SX1262"],
    0x17: [779000000, 928000000, 22, "779 - 928 Mhz", "rnode_firmware_techo.zip", "SX1262"],
    0x21: [820000000, 960000000, 22, "820 - 960 MHz", "rnode_firmware_opencom_xl.zip", "SX1262 + SX1280"],
    0xDE: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_xiao_esp32s3.zip", "SX1262"],
    0xDD: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_xiao_esp32s3.zip", "SX1262"],
    0xFE: [100000000, 1100000000, 17, "(Band capabilities unknown)", None, "Unknown"],
    0xFF: [100000000, 1100000000, 14, "(Band capabilities unknown)", None, "Unknown"],
}

CNF_DIR = None
UPD_DIR = None
FWD_DIR = None
EXT_DIR = None

try:
    CNF_DIR = os.path.expanduser("~/.config/rnodeconf")
    UPD_DIR = CNF_DIR+"/update"
    FWD_DIR = CNF_DIR+"/firmware"
    EXT_DIR = CNF_DIR+"/extracted"
    RT_PATH = CNF_DIR+"/recovery_esptool.py"
    TK_DIR  = CNF_DIR+"/trusted_keys"
    ROM_DIR = CNF_DIR+"/eeprom"

    if not os.path.isdir(CNF_DIR):
        os.makedirs(CNF_DIR)
    if not os.path.isdir(UPD_DIR):
        os.makedirs(UPD_DIR)
    if not os.path.isdir(FWD_DIR):
        os.makedirs(FWD_DIR)
    if not os.path.isdir(EXT_DIR):
        os.makedirs(EXT_DIR)
    if not os.path.isdir(TK_DIR):
        os.makedirs(TK_DIR)
    if not os.path.isdir(ROM_DIR):
        os.makedirs(ROM_DIR)

except Exception as e:
    print("No access to directory "+str(CNF_DIR)+". This utility needs file system access to store firmware and data files. Cannot continue.")
    print("The contained exception was:")
    print(str(e))
    graceful_exit(99)

squashvw = False

class RNode():
    def __init__(self, serial_instance):
        self.serial = serial_instance
        self.timeout     = 100

        self.r_frequency = None
        self.r_bandwidth = None
        self.r_txpower   = None
        self.r_sf        = None
        self.r_state     = None
        self.r_lock      = None

        self.sf = None
        self.cr = None
        self.txpower = None
        self.frequency = None
        self.bandwidth = None

        self.detected = None
        self.usb_serial_id = None

        self.platform = None
        self.mcu = None
        self.eeprom = None
        self.cfg_sector = None
        self.major_version = None
        self.minor_version = None
        self.version = None

        self.provisioned = None
        self.product = None
        self.board = None
        self.model = None
        self.hw_rev = None
        self.made = None
        self.serialno = None
        self.checksum = None
        self.device_hash = None
        self.firmware_hash = None
        self.firmware_hash_target = None
        self.signature = None
        self.signature_valid = False
        self.locally_signed = False
        self.vendor = None

        self.min_freq = None
        self.max_freq = None
        self.max_output = None

        self.configured = None
        self.conf_sf = None
        self.conf_cr = None
        self.conf_txpower = None
        self.conf_frequency = None
        self.conf_bandwidth = None

    def disconnect(self):
        self.leave()
        self.serial.close()

    def readLoop(self):
        try:
            in_frame = False
            escape = False
            command = KISS.CMD_UNKNOWN
            data_buffer = b""
            command_buffer = b""
            last_read_ms = int(time.time()*1000)

            while self.serial.is_open:
                try:
                    data_waiting = self.serial.in_waiting
                except Exception as e:
                    data_waiting = False

                if data_waiting:
                    byte = ord(self.serial.read(1))
                    last_read_ms = int(time.time()*1000)

                    if (in_frame and byte == KISS.FEND and command == KISS.CMD_ROM_READ):
                        self.eeprom = data_buffer
                        in_frame = False
                        data_buffer = b""
                        command_buffer = b""
                    elif (in_frame and byte == KISS.FEND and command == KISS.CMD_CFG_READ):
                        self.cfg_sector = data_buffer
                        in_frame = False
                        data_buffer = b""
                        command_buffer = b""
                    elif (byte == KISS.FEND):
                        in_frame = True
                        command = KISS.CMD_UNKNOWN
                        data_buffer = b""
                        command_buffer = b""
                    elif (in_frame and len(data_buffer) < 1024):
                        if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
                            command = byte
                        elif (command == KISS.CMD_ROM_READ):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                data_buffer = data_buffer+bytes([byte])
                        elif (command == KISS.CMD_CFG_READ):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                data_buffer = data_buffer+bytes([byte])
                        elif (command == KISS.CMD_DATA):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                data_buffer = data_buffer+bytes([byte])
                        elif (command == KISS.CMD_FREQUENCY):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                command_buffer = command_buffer+bytes([byte])
                                if (len(command_buffer) == 4):
                                    self.r_frequency = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3]
                                    RNS.log("Radio reporting frequency is "+str(self.r_frequency/1000000.0)+" MHz")
                                    self.updateBitrate()

                        elif (command == KISS.CMD_BANDWIDTH):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                command_buffer = command_buffer+bytes([byte])
                                if (len(command_buffer) == 4):
                                    self.r_bandwidth = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3]
                                    RNS.log("Radio reporting bandwidth is "+str(self.r_bandwidth/1000.0)+" KHz")
                                    self.updateBitrate()

                        elif (command == KISS.CMD_BT_PIN):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                command_buffer = command_buffer+bytes([byte])
                                if (len(command_buffer) == 4):
                                    self.r_bt_pin = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3]
                                    RNS.log("Bluetooth pairing PIN is: {:06d}".format(self.r_bt_pin))

                        elif (command == KISS.CMD_DEV_HASH):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                command_buffer = command_buffer+bytes([byte])
                                if (len(command_buffer) == 32):
                                    self.device_hash = command_buffer

                        elif (command == KISS.CMD_HASHES):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                command_buffer = command_buffer+bytes([byte])
                                if (len(command_buffer) == 33):
                                    if command_buffer[0] == 0x01:
                                        self.firmware_hash_target = command_buffer[1:]
                                    if command_buffer[0] == 0x02:
                                        self.firmware_hash = command_buffer[1:]

                        elif (command == KISS.CMD_FW_VERSION):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                command_buffer = command_buffer+bytes([byte])
                                if (len(command_buffer) == 2):
                                    self.major_version = command_buffer[0]
                                    self.minor_version = command_buffer[1]
                                    self.updateVersion()

                        elif (command == KISS.CMD_BOARD):
                            self.board = byte

                        elif (command == KISS.CMD_PLATFORM):
                            self.platform = byte

                        elif (command == KISS.CMD_MCU):
                            self.mcu = byte

                        elif (command == KISS.CMD_TXPOWER):
                            self.r_txpower = byte
                            RNS.log("Radio reporting TX power is "+str(self.r_txpower)+" dBm")
                        elif (command == KISS.CMD_SF):
                            self.r_sf = byte
                            RNS.log("Radio reporting spreading factor is "+str(self.r_sf))
                            self.updateBitrate()
                        elif (command == KISS.CMD_CR):
                            self.r_cr = byte
                            RNS.log("Radio reporting coding rate is "+str(self.r_cr))
                            self.updateBitrate()
                        elif (command == KISS.CMD_RADIO_STATE):
                            self.r_state = byte
                        elif (command == KISS.CMD_RADIO_LOCK):
                            self.r_lock = byte
                        elif (command == KISS.CMD_STAT_RX):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                command_buffer = command_buffer+bytes([byte])
                                if (len(command_buffer) == 4):
                                    self.r_stat_rx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3])

                        elif (command == KISS.CMD_STAT_TX):
                            if (byte == KISS.FESC):
                                escape = True
                            else:
                                if (escape):
                                    if (byte == KISS.TFEND):
                                        byte = KISS.FEND
                                    if (byte == KISS.TFESC):
                                        byte = KISS.FESC
                                    escape = False
                                command_buffer = command_buffer+bytes([byte])
                                if (len(command_buffer) == 4):
                                    self.r_stat_tx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3])
                        elif (command == KISS.CMD_STAT_RSSI):
                            self.r_stat_rssi = byte-157 # RSSI Offset
                        elif (command == KISS.CMD_STAT_SNR):
                            self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
                        elif (command == KISS.CMD_RANDOM):
                            self.r_random = byte
                        elif (command == KISS.CMD_ERROR):
                            if (byte == KISS.ERROR_INITRADIO):
                                RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")")
                            elif (byte == KISS.ERROR_TXFAILED):
                                RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")")
                            else:
                                RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")")
                        elif (command == KISS.CMD_DETECT):
                            if byte == KISS.DETECT_RESP:
                                self.detected = True
                            else:
                                self.detected = False
                        
                else:
                    time_since_last = int(time.time()*1000) - last_read_ms
                    if len(data_buffer) > 0 and time_since_last > self.timeout:
                        RNS.log(str(self)+" serial read timeout")
                        data_buffer = b""
                        in_frame = False
                        command = KISS.CMD_UNKNOWN
                        escape = False
                    sleep(0.08)

        except Exception as e:
            raise e
            graceful_exit()

    def updateBitrate(self):
        try:
            self.bitrate = self.r_sf * ( (4.0/self.r_cr) / (math.pow(2,self.r_sf)/(self.r_bandwidth/1000)) ) * 1000
            self.bitrate_kbps = round(self.bitrate/1000.0, 2)
        except Exception as e:
            self.bitrate = 0

    def updateVersion(self):
        minstr = str(self.minor_version)
        if len(minstr) == 1:
            minstr = "0"+minstr
        self.version = str(self.major_version)+"."+minstr

    def detect(self):
        kiss_command = bytes([KISS.FEND, KISS.CMD_DETECT, KISS.DETECT_REQ, KISS.FEND, KISS.CMD_FW_VERSION, 0x00, KISS.FEND, KISS.CMD_PLATFORM, 0x00, KISS.FEND, KISS.CMD_MCU, 0x00, KISS.FEND, KISS.CMD_BOARD, 0x00, KISS.FEND, KISS.CMD_DEV_HASH, 0x01, KISS.FEND, KISS.CMD_HASHES, 0x01, KISS.FEND, KISS.CMD_HASHES, 0x02, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while detecting hardware for "+self(str))

    def leave(self):
        kiss_command = bytes([KISS.FEND, KISS.CMD_LEAVE, 0xFF, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending host left command to device")
        sleep(1)

    def set_display_intensity(self, intensity):
        data = bytes([intensity & 0xFF])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DISP_INT])+data+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending display intensity command to device")

    def set_display_blanking(self, blanking_timeout):
        data = bytes([blanking_timeout & 0xFF])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DISP_BLNK])+data+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending display blanking timeout command to device")

    def set_display_rotation(self, rotation):
        data = bytes([rotation & 0xFF])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DISP_ROT])+data+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending display rotation command to device")

    def recondition_display(self):
        data = bytes([0x01])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DISP_RCND])+data+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending display recondition command to device")

    def set_disable_interference_avoidance(self, ia_disabled):
        if ia_disabled:
            data = bytes([0x01])
        else:
            data = bytes([0x00])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DIS_IA])+data+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending interference avoidance configuration command to device")

    def set_neopixel_intensity(self, intensity):
        data = bytes([intensity & 0xFF])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_NP_INT])+data+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending NeoPixel intensity command to device")

    def set_display_address(self, address):
        data = bytes([address & 0xFF])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DISP_ADR])+data+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending display address command to device")

    def enable_bluetooth(self):
        kiss_command = bytes([KISS.FEND, KISS.CMD_BT_CTRL, 0x01, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending bluetooth enable command to device")

    def disable_bluetooth(self):
        kiss_command = bytes([KISS.FEND, KISS.CMD_BT_CTRL, 0x00, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending bluetooth disable command to device")

    def bluetooth_pair(self):
        kiss_command = bytes([KISS.FEND, KISS.CMD_BT_CTRL, 0x02, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending bluetooth pair command to device")

    def store_signature(self, signature_bytes):
        data = KISS.escape(signature_bytes)
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DEV_SIG])+data+bytes([KISS.FEND])

        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending signature to device")

    def set_firmware_hash(self, hash_bytes):
        data = KISS.escape(hash_bytes)
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FW_HASH])+data+bytes([KISS.FEND])

        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending firmware hash to device")

    def indicate_firmware_update(self):
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FW_UPD])+bytes([0x01])+bytes([KISS.FEND])

        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending firmware update command to device")

    def set_wifi_mode(self, mode):
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_MODE, mode])+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending wifi mode command to device")

    def set_wifi_channel(self, channel):
        try: ch = int(channel)
        except: raise ValueError("Invalid WiFi channel")
        if ch < 1 or ch > 14: raise ValueError("Invalid WiFi channel")
        ch_data = bytes([ch])
        data = KISS.escape(ch_data)
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_CHN])+data+bytes([KISS.FEND])

        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending wifi channel to device")

    def set_wifi_ip(self, ip):
        if ip == None: ip_data = bytes([0x00, 0x00, 0x00, 0x00])
        else:
            ip_data = b""
            if not type(ip) == str: raise TypeError("Invalid IP address")
            octets = ip.split(".")
            if not len(octets) == 4: raise ValueError("Invalid IP address length")
            try:
                for i in range(0, 4):
                    octet = int(octets[i])
                    if octet < 0 or octet > 255: raise ValueError("Invalid IP octet value")
                    else: ip_data += bytes([octet])
            except Exception as e:
                raise ValueError(f"Could not decode IP address octet: {e}")
        
        data = KISS.escape(ip_data)
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_IP])+data+bytes([KISS.FEND])

        written = self.serial.write(kiss_command)
        if written != len(kiss_command): raise IOError("An IO error occurred while sending wifi IP address to device")

    def set_wifi_nm(self, nm):
        if nm == None: nm_data = bytes([0x00, 0x00, 0x00, 0x00])
        else:
            nm_data = b""
            if not type(nm) == str: raise TypeError("Invalid IP address")
            octets = nm.split(".")
            if not len(octets) == 4: raise ValueError("Invalid IP address length")
            try:
                for i in range(0, 4):
                    octet = int(octets[i])
                    if octet < 0 or octet > 255: raise ValueError("Invalid IP octet value")
                    else: nm_data += bytes([octet])
            except Exception as e:
                raise ValueError(f"Could not decode IP address octet: {e}")
        
        data = KISS.escape(nm_data)
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_NM])+data+bytes([KISS.FEND])

        written = self.serial.write(kiss_command)
        if written != len(kiss_command): raise IOError("An IO error occurred while sending wifi netmask to device")

    def set_wifi_ssid(self, ssid):
        if ssid == None: data = bytes([0x00])
        else:
            ssid_data = ssid.encode("utf-8")+bytes([0x00])
            if len(ssid_data) < 0 or len(ssid_data) > 33: raise ValueError("Invalid SSID length")
            data = KISS.escape(ssid_data)

        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_SSID])+data+bytes([KISS.FEND])

        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending wifi SSID to device")

    def set_wifi_psk(self, psk):
        if psk == None: data = bytes([0x00])
        else:
            psk_data = psk.encode("utf-8")+bytes([0x00])
            if len(psk_data) < 8 or len(psk_data) > 33: raise ValueError("Invalid psk length")
            data = KISS.escape(psk_data)
        
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_PSK])+data+bytes([KISS.FEND])

        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while sending wifi SSID to device")

    def initRadio(self):
        self.setFrequency()
        self.setBandwidth()
        self.setTXPower()
        self.setSpreadingFactor()
        self.setCodingRate()
        self.setRadioState(KISS.RADIO_STATE_ON)

    def setFrequency(self):
        c1 = self.frequency >> 24
        c2 = self.frequency >> 16 & 0xFF
        c3 = self.frequency >> 8 & 0xFF
        c4 = self.frequency & 0xFF
        data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))

        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while configuring frequency for "+self(str))

    def setBandwidth(self):
        c1 = self.bandwidth >> 24
        c2 = self.bandwidth >> 16 & 0xFF
        c3 = self.bandwidth >> 8 & 0xFF
        c4 = self.bandwidth & 0xFF
        data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))

        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while configuring bandwidth for "+self(str))

    def setTXPower(self):
        txp = bytes([self.txpower])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while configuring TX power for "+self(str))

    def setSpreadingFactor(self):
        sf = bytes([self.sf])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while configuring spreading factor for "+self(str))

    def setCodingRate(self):
        cr = bytes([self.cr])
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while configuring coding rate for "+self(str))

    def setRadioState(self, state):
        kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while configuring radio state for "+self(str))

    def setNormalMode(self):
        kiss_command = bytes([KISS.FEND, KISS.CMD_CONF_DELETE, 0x00, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while configuring device mode")

    def setTNCMode(self):
        kiss_command = bytes([KISS.FEND, KISS.CMD_CONF_SAVE, 0x00, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while configuring device mode")

        if self.platform == ROM.PLATFORM_ESP32:
            self.hard_reset()

    def wipe_eeprom(self):
        kiss_command = bytes([KISS.FEND, KISS.CMD_ROM_WIPE, 0xf8, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while wiping EEPROM")
        sleep(13);
        # Due to the current janky emulated EEPROM implementation for the
        # RAK4631, extra time must be given to allow for writing.
        if self.board == ROM.BOARD_RAK4631:
            sleep(10)

    def hard_reset(self):
        kiss_command = bytes([KISS.FEND, KISS.CMD_RESET, 0xf8, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while restarting device")
        sleep(2);

    def write_eeprom(self, addr, byte):
        write_payload = b"" + bytes([addr, byte])
        write_payload = KISS.escape(write_payload)
        kiss_command = bytes([KISS.FEND, KISS.CMD_ROM_WRITE]) + write_payload + bytes([KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while writing EEPROM")


    def download_eeprom(self):
        self.eeprom = None
        kiss_command = bytes([KISS.FEND, KISS.CMD_ROM_READ, 0x00, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while downloading EEPROM")

        sleep(0.6)
        if self.eeprom == None:
            RNS.log("Could not download EEPROM from device. Is a valid firmware installed?")
            graceful_exit()
        else:
            self.parse_eeprom()

    def download_cfg_sector(self):
        self.cfg_sector = None
        kiss_command = bytes([KISS.FEND, KISS.CMD_CFG_READ, 0x00, KISS.FEND])
        written = self.serial.write(kiss_command)
        if written != len(kiss_command):
            raise IOError("An IO error occurred while downloading config sector")

        sleep(0.6)

    def parse_eeprom(self):
        global squashvw;
        try:
            if self.eeprom[ROM.ADDR_INFO_LOCK] == ROM.INFO_LOCK_BYTE:
                from cryptography.hazmat.primitives import hashes
                from cryptography.hazmat.backends import default_backend

                self.provisioned = True

                self.product = self.eeprom[ROM.ADDR_PRODUCT]
                self.model = self.eeprom[ROM.ADDR_MODEL]
                self.hw_rev = self.eeprom[ROM.ADDR_HW_REV]
                self.serialno = bytes([self.eeprom[ROM.ADDR_SERIAL], self.eeprom[ROM.ADDR_SERIAL+1], self.eeprom[ROM.ADDR_SERIAL+2], self.eeprom[ROM.ADDR_SERIAL+3]])
                self.made = bytes([self.eeprom[ROM.ADDR_MADE], self.eeprom[ROM.ADDR_MADE+1], self.eeprom[ROM.ADDR_MADE+2], self.eeprom[ROM.ADDR_MADE+3]])
                self.checksum = b""

                try:
                    self.min_freq = models[self.model][0]
                    self.max_freq = models[self.model][1]
                    self.max_output = models[self.model][2]
                except Exception as e:
                    RNS.log("Error: Model band and output power capabilities are unknown!")
                    RNS.log("The contained exception was: "+str(e))
                    self.min_freq = 0
                    self.max_freq = 0
                    self.max_output = 0

                for i in range(0,16):
                    self.checksum = self.checksum+bytes([self.eeprom[ROM.ADDR_CHKSUM+i]])

                self.signature = b""
                for i in range(0,128):
                    self.signature = self.signature+bytes([self.eeprom[ROM.ADDR_SIGNATURE+i]])

                checksummed_info = b"" + bytes([self.product]) + bytes([self.model]) + bytes([self.hw_rev]) + self.serialno + self.made
                digest = hashes.Hash(hashes.MD5(), backend=default_backend())
                digest.update(checksummed_info)
                checksum = digest.finalize()

                if self.checksum != checksum:
                    self.provisioned = False
                    RNS.log("EEPROM checksum mismatch")
                    graceful_exit()
                else:
                    RNS.log("EEPROM checksum correct")

                    from cryptography.hazmat.primitives import serialization
                    from cryptography.hazmat.primitives.serialization import load_der_public_key
                    from cryptography.hazmat.primitives.serialization import load_der_private_key
                    from cryptography.hazmat.primitives.asymmetric import padding

                    # Try loading local signing key for 
                    # validation of self-signed devices
                    if os.path.isdir(FWD_DIR) and os.path.isfile(FWD_DIR+"/signing.key"):
                        private_bytes = None
                        try:
                            file = open(FWD_DIR+"/signing.key", "rb")
                            private_bytes = file.read()
                            file.close()
                        except Exception as e:
                            RNS.log("Could not load local signing key")

                        try:
                            private_key = serialization.load_der_private_key(
                                private_bytes,
                                password=None,
                                backend=default_backend()
                            )
                            public_key = private_key.public_key()
                            public_bytes = public_key.public_bytes(
                                encoding=serialization.Encoding.DER,
                                format=serialization.PublicFormat.SubjectPublicKeyInfo
                            )
                            public_bytes_hex = RNS.hexrep(public_bytes, delimit=False)

                            vendor_keys = []
                            for known in known_keys:
                                vendor_keys.append(known[1])

                            if not public_bytes_hex in vendor_keys:
                                local_key_entry = ["LOCAL", public_bytes_hex]
                                known_keys.append(local_key_entry)

                        except Exception as e:
                            RNS.log("Could not deserialize local signing key")
                            RNS.log(str(e))

                    # Try loading trusted signing key for 
                    # validation of devices
                    if os.path.isdir(TK_DIR):
                        for f in os.listdir(TK_DIR):
                            if os.path.isfile(TK_DIR+"/"+f) and f.endswith(".pubkey"):
                                try:
                                    file = open(TK_DIR+"/"+f, "rb")
                                    public_bytes = file.read()
                                    file.close()

                                    try:
                                        public_bytes_hex = RNS.hexrep(public_bytes, delimit=False)

                                        vendor_keys = []
                                        for known in known_keys:
                                            vendor_keys.append(known[1])

                                        if not public_bytes_hex in vendor_keys:
                                            local_key_entry = ["LOCAL", public_bytes_hex]
                                            known_keys.append(local_key_entry)

                                    except Exception as e:
                                        RNS.log("Could not deserialize trusted signing key "+str(f))
                                        RNS.log(str(e))

                                except Exception as e:
                                    RNS.log("Could not load trusted signing key"+str(f))


                    for known in known_keys:
                        vendor = known[0]
                        public_hexrep = known[1]
                        public_bytes = bytes.fromhex(public_hexrep)
                        public_key = load_der_public_key(public_bytes, backend=default_backend())
                        try:
                            public_key.verify(
                                self.signature,
                                self.checksum,
                                padding.PSS(
                                    mgf=padding.MGF1(hashes.SHA256()),
                                    salt_length=padding.PSS.MAX_LENGTH
                                ),
                                hashes.SHA256())
                            if vendor == "LOCAL":
                                self.locally_signed = True

                            self.signature_valid = True
                            self.vendor = vendor
                        except Exception as e:
                            pass

                    if self.signature_valid:
                        RNS.log("Device signature validated")
                    else:
                        RNS.log("Device signature validation failed")
                        if not squashvw:
                            print("     ")
                            print("     WARNING! This device is NOT verifiable and should NOT be trusted.")
                            print("     Someone could have added privacy-breaking or malicious code to it.")
                            print("     ")
                            print("     Please verify the signing key is present on this machine.")
                            print("     Autogenerated keys will not match another machine's signature.")
                            print("     ")
                            print("     Proceed at your own risk and responsibility! If you created this")
                            print("     device yourself, please read the documentation on how to sign your")
                            print("     device to avoid this warning.")
                            print("     ")
                            print("     Always use a firmware downloaded as binaries or compiled from source")
                            print("     from one of the following locations:")
                            print("     ")
                            print("        https://github.com/markqvist/rnode_firmware")
                            print("        https://github.com/liberatedsystems/RNode_Firmware_CE")
                            print("     ")
                            print("     You can reflash and bootstrap this device to a verifiable state")
                            print("     by using this utility. It is recommended to do so NOW!")
                            print("     ")
                            print("     To initialise this device to a verifiable state, please run:")
                            print("     ")
                            print("              rnodeconf "+str(self.serial.name)+" --autoinstall")
                            print("")



                if self.eeprom[ROM.ADDR_CONF_OK] == ROM.CONF_OK_BYTE:
                    self.configured = True
                    self.conf_sf = self.eeprom[ROM.ADDR_CONF_SF]
                    self.conf_cr = self.eeprom[ROM.ADDR_CONF_CR]
                    self.conf_txpower = self.eeprom[ROM.ADDR_CONF_TXP]
                    self.conf_frequency = self.eeprom[ROM.ADDR_CONF_FREQ] << 24 | self.eeprom[ROM.ADDR_CONF_FREQ+1] << 16 | self.eeprom[ROM.ADDR_CONF_FREQ+2] << 8 | self.eeprom[ROM.ADDR_CONF_FREQ+3]
                    self.conf_bandwidth = self.eeprom[ROM.ADDR_CONF_BW] << 24 | self.eeprom[ROM.ADDR_CONF_BW+1] << 16 | self.eeprom[ROM.ADDR_CONF_BW+2] << 8 | self.eeprom[ROM.ADDR_CONF_BW+3]
                else:
                    self.configured = False
            else:
                self.provisioned = False
        except Exception as e:
            self.provisioned = False
            RNS.log("Invalid EEPROM data, could not parse device EEPROM.")
            RNS.log("The contained exception was: "+str(e))


    def device_probe(self):
        sleep(2.5)
        self.detect()
        sleep(0.75)
        if self.detected == True:
            RNS.log("Device connected")
            RNS.log("Current firmware version: "+self.version)
            return True
        else:
            raise IOError("Got invalid response while detecting device")

selected_version = None
selected_hash = None
firmware_version_url          = "https://github.com/markqvist/rnode_firmware/releases/latest/download/release.json"
fallback_firmware_version_url = "https://github.com/markqvist/rnode_firmware/releases/latest/download/release.json"
def ensure_firmware_file(fw_filename):
    global selected_version, selected_hash, upd_nocheck
    if fw_filename == "extracted_rnode_firmware.zip":
        vfpath = EXT_DIR+"/extracted_rnode_firmware.version"
        if os.path.isfile(vfpath):
            required_files = [
                "extracted_console_image.bin",
                "extracted_rnode_firmware.bin",
                "extracted_rnode_firmware.boot_app0",
                "extracted_rnode_firmware.bootloader",
                "extracted_rnode_firmware.partitions",
            ]
            parts_missing = False
            for rf in required_files:
                if not os.path.isfile(EXT_DIR+"/"+rf):
                    parts_missing = True

            if parts_missing:
                RNS.log("One or more required firmware files are missing from the extracted RNode")
                RNS.log("Firmware archive. Installation cannot continue. Please try extracting the")
                RNS.log("firmware again with the --extract-firmware option.")
                graceful_exit(184)

            vf = open(vfpath, "rb")
            release_info = vf.read().decode("utf-8").strip()
            selected_version = release_info.split()[0]
            selected_hash = release_info.split()[1]
            RNS.log("Using existing firmware file: "+fw_filename+" for version "+selected_version)
        else:
            RNS.log("No extracted firmware is available, cannot continue.")
            RNS.log("Extract a firmware from an existing RNode first, using the --extract-firmware option.")
            graceful_exit(183)

    else:
        try:
            if not upd_nocheck:
                try:
                    # if custom firmware url, download latest release
                    if selected_version == None and fw_url == None:
                        urlretrieve(firmware_version_url, UPD_DIR+"/release_info.json")
                        import json
                        with open(UPD_DIR+"/release_info.json", "rb") as rif:
                            rdat = json.loads(rif.read())
                            variant = rdat[fw_filename]
                            with open(UPD_DIR+"/"+fw_filename+".version.latest", "wb") as verf:
                                inf_str = str(variant["version"])+" "+str(variant["hash"])
                                verf.write(inf_str.encode("utf-8"))

                    else:
                        if fw_url != None:
                            if selected_version == None:
                                version_url = fw_url+"latest/download/release.json"
                            else:
                                version_url = fw_url+"download/"+selected_version+"/release.json"
                        else:
                                version_url = firmware_update_url+selected_version+"/release.json"
                        try:
                            RNS.log("Retrieving specified version info from "+version_url)
                            urlretrieve(version_url, UPD_DIR+"/version_release_info.json")
                            import json
                            with open(UPD_DIR+"/version_release_info.json", "rb") as rif:
                                rdat = json.loads(rif.read())
                                variant = rdat[fw_filename]
                                with open(UPD_DIR+"/"+fw_filename+".version.latest", "wb") as verf:
                                    inf_str = str(variant["version"])+" "+str(variant["hash"])
                                    verf.write(inf_str.encode("utf-8"))
                        except Exception as e:
                            RNS.log("Failed to retrive version information for your board.")
                            RNS.log("Check your internet connection and try again.")
                            RNS.log("If you don't have Internet access currently, use the --fw-version option to manually specify a version.")
                            RNS.log("You can also use --extract to copy the firmware from a known-good RNode of the same model.")
                            graceful_exit()
                except Exception as e:
                    # if custom firmware url, don't fallback
                    if fw_url != None:
                        RNS.log("Failed to retrive version information for your board from the specified url.")
                        RNS.log("Check your internet connection and try again.")
                        RNS.log("If you don't have Internet access currently, use the --fw-version option to manually specify a version.")
                        RNS.log("You can also use --extract to copy the firmware from a known-good RNode of the same model.")
                        graceful_exit()

                    RNS.log("")
                    RNS.log("WARNING!")
                    RNS.log("Failed to retrieve latest version information for your board from the default server.")
                    RNS.log("Will retry using the following fallback URL: "+fallback_firmware_version_url)
                    RNS.log("")
                    RNS.log("Hit enter if you want to proceed")
                    input()
                    try:
                        urlretrieve(fallback_firmware_version_url, UPD_DIR+"/fallback_release_info.json")
                        import json
                        with open(UPD_DIR+"/fallback_release_info.json", "rb") as rif:
                            rdat = json.loads(rif.read())
                            variant = rdat[fw_filename]
                            with open(UPD_DIR+"/"+fw_filename+".version.latest", "wb") as verf:
                                inf_str = str(variant["version"])+" "+str(variant["hash"])
                                verf.write(inf_str.encode("utf-8"))

                    except Exception as e:
                        RNS.log("Error while trying fallback URL: "+str(e))
                        raise e

                import shutil
                file = open(UPD_DIR+"/"+fw_filename+".version.latest", "rb")
                release_info = file.read().decode("utf-8").strip()
                selected_version = release_info.split()[0]
                if selected_version == "not":
                    RNS.log("No valid version found for this board, exiting.")
                    graceful_exit(199)

                selected_hash = release_info.split()[1]
                if not os.path.isdir(UPD_DIR+"/"+selected_version):
                    os.makedirs(UPD_DIR+"/"+selected_version)
                shutil.copy(UPD_DIR+"/"+fw_filename+".version.latest", UPD_DIR+"/"+selected_version+"/"+fw_filename+".version")
                RNS.log("The selected firmware for this board is version "+selected_version)

            else:
                if selected_version == None:
                    RNS.log("Online firmware version check was disabled, but no firmware version specified for install.")
                    RNS.log("use the --fw-version option to manually specify a version.")
                    graceful_exit(98)

            # if custom firmware url, use it
            if fw_url != None:
                update_target_url = fw_url+"download/"+selected_version+"/"+fw_filename
                RNS.log("Retrieving firmware from custom url "+update_target_url)
            else:
                update_target_url = firmware_update_url+selected_version+"/"+fw_filename

            try:
                if not os.path.isdir(UPD_DIR+"/"+selected_version):
                    os.makedirs(UPD_DIR+"/"+selected_version)

                if not os.path.isfile(UPD_DIR+"/"+selected_version+"/"+fw_filename):
                    RNS.log("Firmware "+UPD_DIR+"/"+selected_version+"/"+fw_filename+" not found.")
                    RNS.log("Downloading missing firmware file: "+fw_filename+" for version "+selected_version)
                    urlretrieve(update_target_url, UPD_DIR+"/"+selected_version+"/"+fw_filename)
                    RNS.log("Firmware file downloaded")
                else:
                    RNS.log("Using existing firmware file: "+fw_filename+" for version "+selected_version)

                try:
                    if selected_hash == None:
                        try:
                            file = open(UPD_DIR+"/"+selected_version+"/"+fw_filename+".version", "rb")
                            release_info = file.read().decode("utf-8").strip()
                            selected_hash = release_info.split()[1]
                        except Exception as e:
                            RNS.log("Could not read locally cached release information.")
                            RNS.log("Ensure "+UPD_DIR+"/"+selected_version+"/"+fw_filename+".version exists and has the correct format and hash.")
                            RNS.log("You can clear the cache with the --clear-cache option and try again.")

                        if selected_hash == None:
                            RNS.log("No release hash found for "+fw_filename+". The firmware integrity could not be verified.")
                            graceful_exit(97)

                    RNS.log("Verifying firmware integrity...")
                    fw_file = open(UPD_DIR+"/"+selected_version+"/"+fw_filename, "rb")
                    expected_hash = bytes.fromhex(selected_hash)
                    file_hash = hashlib.sha256(fw_file.read()).hexdigest()
                    if file_hash == selected_hash:
                        pass
                    else:
                        RNS.log("")
                        RNS.log(f"Firmware hash {file_hash} but should be {selected_hash}, possibly due to download corruption.")
                        RNS.log("Firmware corrupt. Try clearing the local firmware cache with: rnodeconf --clear-cache")
                        graceful_exit(96)

                except Exception as e:
                    RNS.log("An error occurred while checking firmware file integrity. The contained exception was:")
                    RNS.log(str(e))
                    graceful_exit(95)

            except Exception as e:
                RNS.log("Could not download required firmware file: ")
                RNS.log(str(update_target_url))
                RNS.log("The contained exception was:")
                RNS.log(str(e))
                graceful_exit()

        except Exception as e:
            RNS.log("An error occurred while reading version information for "+str(fw_filename)+". The contained exception was:")
            RNS.log(str(e))
            graceful_exit()

def rnode_open_serial(port):
    import serial
    return serial.Serial(
        port = port,
        baudrate = rnode_baudrate,
        bytesize = 8,
        parity = serial.PARITY_NONE,
        stopbits = 1,
        xonxoff = False,
        rtscts = False,
        timeout = 0,
        inter_byte_timeout = None,
        write_timeout = None,
        dsrdtr = False
    )
    
    
def graceful_exit(C=0):
    if RNS.vendor.platformutils.is_windows():
        RNS.log("Windows detected; delaying DTR",RNS.LOG_VERBOSE) 
        if rnode:
            RNS.log("Sending \"Leave\" to Rnode",RNS.LOG_VERBOSE)
            rnode.leave() # Leave has wait built in
        elif rnode_serial:
            RNS.log("Closing raw serial",RNS.LOG_VERBOSE)
            sleep(1) # Wait for MCU to complete operation before DTR goes false
            rnode_serial.close()
    RNS.log("Exiting: Code "+str(C),RNS.LOG_INFO)
    exit(C)


device_signer = None
force_update = False
upd_nocheck = False
def main():
    global mapped_product, mapped_model, fw_filename, fw_url, selected_version, force_update, upd_nocheck, device_signer

    try:
        if not util.find_spec("serial"):
            raise ImportError("Serial module could not be found")
    except ImportError:
        print("")
        print("RNode Config Utility needs pyserial to work.")
        print("You can install it with: pip3 install pyserial")
        print("")
        graceful_exit()

    try:
        if not util.find_spec("cryptography"):
            raise ImportError("Cryptography module could not be found")
    except ImportError:
        print("")
        print("RNode Config Utility needs the cryptography module to work.")
        print("You can install it with: pip3 install cryptography")
        print("")
        graceful_exit()

    import serial
    from serial.tools import list_ports

    try:
        parser = argparse.ArgumentParser(description="RNode Configuration and firmware utility. This program allows you to change various settings and startup modes of RNode. It can also install, flash and update the firmware on supported devices.")
        parser.add_argument("-i", "--info", action="store_true", help="Show device info")
        parser.add_argument("-a", "--autoinstall", action="store_true", help="Automatic installation on various supported devices")
        parser.add_argument("-u", "--update", action="store_true", help="Update firmware to the latest version")
        parser.add_argument("-U", "--force-update", action="store_true", help="Update to specified firmware even if version matches or is older than installed version")
        parser.add_argument("--fw-version", action="store", metavar="version", default=None, help="Use a specific firmware version for update or autoinstall")
        parser.add_argument("--fw-url", action="store", metavar="url", default=None, help="Use an alternate firmware download URL")
        parser.add_argument("--nocheck", action="store_true", help="Don't check for firmware updates online")
        parser.add_argument("-e", "--extract", action="store_true", help="Extract firmware from connected RNode for later use")
        parser.add_argument("-E", "--use-extracted", action="store_true", help="Use the extracted firmware for autoinstallation or update")
        parser.add_argument("-C", "--clear-cache", action="store_true", help="Clear locally cached firmware files")
        parser.add_argument("--baud-flash", action="store", metavar="baud_flash", type=str, default="921600", help="Set specific baud rate when flashing device. Default is 921600")

        parser.add_argument("-N", "--normal", action="store_true", help="Switch device to normal mode")
        parser.add_argument("-T", "--tnc", action="store_true", help="Switch device to TNC mode")

        parser.add_argument("-b", "--bluetooth-on", action="store_true", help="Turn device bluetooth on")
        parser.add_argument("-B", "--bluetooth-off", action="store_true", help="Turn device bluetooth off")
        parser.add_argument("-p", "--bluetooth-pair", action="store_true", help="Put device into bluetooth pairing mode")

        parser.add_argument("-w", "--wifi", action="store", metavar="mode", default=None, help="Set WiFi mode (OFF, AP or STATION)")
        parser.add_argument("--channel", action="store", metavar="channel", default=None, help="Set WiFi channel")
        parser.add_argument("--ssid", action="store", metavar="ssid", default=None, help="Set WiFi SSID (NONE to delete)")
        parser.add_argument("--psk", action="store", metavar="psk", default=None, help="Set WiFi PSK (NONE to delete)")
        parser.add_argument("--show-psk", action="store_true", default=False, help="Display stored WiFi PSK")
        parser.add_argument("--ip", action="store", metavar="ip", default=None, help="Set static WiFi IP address (NONE for DHCP)")
        parser.add_argument("--nm", action="store", metavar="nm", default=None, help="Set static WiFi network mask (NONE for DHCP)")

        parser.add_argument("-D", "--display", action="store", metavar="i", type=int, default=None, help="Set display intensity (0-255)")
        parser.add_argument("-t", "--timeout", action="store", metavar="s", type=int, default=None, help="Set display timeout in seconds, 0 to disable")
        parser.add_argument("-R", "--rotation", action="store", metavar="rotation", type=int, default=None, help="Set display rotation, valid values are 0 through 3")
        parser.add_argument("--display-addr", action="store", metavar="byte", type=str, default=None, help="Set display address as hex byte (00 - FF)")
        parser.add_argument("--recondition-display", action="store_true", help="Start display reconditioning")

        parser.add_argument("--np", action="store", metavar="i", type=int, default=None, help="Set NeoPixel intensity (0-255)")

        parser.add_argument("--freq", action="store", metavar="Hz", type=int, default=None, help="Frequency in Hz for TNC mode")
        parser.add_argument("--bw", action="store", metavar="Hz", type=int, default=None, help="Bandwidth in Hz for TNC mode")
        parser.add_argument("--txp", action="store", metavar="dBm", type=int, default=None, help="TX power in dBm for TNC mode")
        parser.add_argument("--sf", action="store", metavar="factor", type=int, default=None, help="Spreading factor for TNC mode (7 - 12)")
        parser.add_argument("--cr", action="store", metavar="rate", type=int, default=None, help="Coding rate for TNC mode (5 - 8)")

        parser.add_argument("-x", "--ia-enable", action="store_true", help="Enable interference avoidance")
        parser.add_argument("-X", "--ia-disable", action="store_true", help="Disable interference avoidance")

        parser.add_argument("-c", "--config", action="store_true", help="Print device configuration")

        parser.add_argument("--eeprom-backup", action="store_true", help="Backup EEPROM to file")
        parser.add_argument("--eeprom-dump", action="store_true", help="Dump EEPROM to console")
        parser.add_argument("--eeprom-wipe", action="store_true", help="Unlock and wipe EEPROM")

        parser.add_argument("-P", "--public", action="store_true", help="Display public part of signing key")
        parser.add_argument("--trust-key", action="store", metavar="hexbytes", type=str, default=None, help="Public key to trust for device verification")

        parser.add_argument("--version", action="store_true", help="Print program version and exit")

        parser.add_argument("-f", "--flash", action="store_true", help="Flash firmware and bootstrap EEPROM")
        parser.add_argument("-r", "--rom", action="store_true", help="Bootstrap EEPROM without flashing firmware")
        parser.add_argument("-k", "--key", action="store_true", help="Generate a new signing key and exit") # 
        parser.add_argument("-S", "--sign", action="store_true", help="Display public part of signing key")
        parser.add_argument("-H", "--firmware-hash", action="store", help="Set installed firmware hash")
        parser.add_argument("-K", "--get-target-firmware-hash", action="store_true", help=argparse.SUPPRESS) # Get target firmware hash from device
        parser.add_argument("-L", "--get-firmware-hash", action="store_true", help=argparse.SUPPRESS) # Get calculated firmware hash from device
        parser.add_argument("--platform", action="store", metavar="platform", type=str, default=None, help="Platform specification for device bootstrap")
        parser.add_argument("--product", action="store", metavar="product", type=str, default=None, help="Product specification for device bootstrap") # 
        parser.add_argument("--model", action="store", metavar="model", type=str, default=None, help="Model code for device bootstrap")
        parser.add_argument("--hwrev", action="store", metavar="revision", type=int, default=None, help="Hardware revision for device bootstrap")

        parser.add_argument("port", nargs="?", default=None, help="serial port where RNode is attached", type=str)
        args = parser.parse_args()

        def print_donation_block():
            print("  Ethereum  : "+eth_addr)
            print("  Bitcoin   : "+btc_addr)
            print("  Monero    : "+xmr_addr)
            print("  Ko-Fi     : https://ko-fi.com/markqvist")
            print("  LiberaPay : https://liberapay.com/reticulum")
            print("")
            print("  Info      : https://reticulum.network")
            print("  Code      : https://github.com/markqvist")

        if args.version:
            print("rnodeconf "+program_version)
            graceful_exit(0)

        if args.clear_cache:
            RNS.log("Clearing local firmware cache...")
            import shutil
            shutil.rmtree(UPD_DIR)
            RNS.log("Done")
            graceful_exit(0)

        if args.fw_version != None:
            selected_version = args.fw_version
            try: 
                check_float = float(selected_version)
            except ValueError:
                RNS.log("Selected version \""+selected_version+"\" does not appear to be a number.")
                graceful_exit()

        if args.fw_url != None:
            fw_url = args.fw_url

        if args.force_update:
            force_update = True

        if args.nocheck:
            upd_nocheck = True
            
        if args.public or args.key or args.flash or args.rom or args.autoinstall or args.trust_key:
            from cryptography.hazmat.primitives import hashes
            from cryptography.hazmat.backends import default_backend
            from cryptography.hazmat.primitives import serialization
            from cryptography.hazmat.primitives.serialization import load_der_public_key
            from cryptography.hazmat.primitives.serialization import load_der_private_key
            from cryptography.hazmat.primitives.asymmetric import rsa
            from cryptography.hazmat.primitives.asymmetric import padding

        clear = lambda: os.system('clear')

        if args.trust_key:
            try:
                public_bytes = bytes.fromhex(args.trust_key)
                try:
                    public_key = load_der_public_key(public_bytes, backend=default_backend())
                    key_hash = hashlib.sha256(public_bytes).hexdigest()
                    RNS.log("Trusting key: "+str(key_hash))
                    f = open(TK_DIR+"/"+str(key_hash)+".pubkey", "wb")
                    f.write(public_bytes)
                    f.close()

                except Exception as e:
                    RNS.log("Could not create public key from supplied data. Check that the key format is valid.")
                    RNS.log(str(e))

            except Exception as e:
                RNS.log("Invalid key data supplied")
            graceful_exit(0)

        if args.use_extracted and ((args.update and args.port != None) or args.autoinstall):
            print("")
            print("You have specified that rnodeconf should use a firmware extracted")
            print("from another device. Please note that this *only* works if you are")
            print("targeting a device of the same type that the firmware came from!")
            print("")
            print("Flashing this firmware to a device it was not created for will most")
            print("likely result in it being inoperable until it is updated with the")
            print("correct firmware. Hit enter to continue.")
            input()

        if args.extract:
            # clear()
            print("")
            print("RNode Firmware Extraction")
            print("")
            if not args.port:
                ports = list_ports.comports()
                portlist = []
                for port in ports:
                    portlist.insert(0, port) 
                
                pi = 1
                print("Detected serial ports:")
                for port in portlist:
                    print("  ["+str(pi)+"] "+str(port.device)+" ("+str(port.product)+", "+str(port.serial_number)+")")
                    pi += 1

                print("\nEnter the number of the serial port your device is connected to:\n? ", end="")
                try:
                    c_port = int(input())
                    if c_port < 1 or c_port > len(ports):
                        raise ValueError()

                    selected_port = portlist[c_port-1]
                except Exception as e:
                    print("That port does not exist, exiting now.")
                    graceful_exit()

                if selected_port == None:
                    print("Could not select port, exiting now.")
                    graceful_exit()

                port_path = selected_port.device
                port_product = selected_port.product
                port_serialno = selected_port.serial_number

                print("\nOk, using device on "+str(port_path)+" ("+str(port_product)+", "+str(port_serialno)+")")

            else:
                ports = list_ports.comports()

                for port in ports:
                    if port.device == args.port:
                        selected_port = port

                if selected_port == None:
                    print("Could not find specified port "+str(args.port)+", exiting now")
                    graceful_exit()

                port_path = selected_port.device
                port_product = selected_port.product
                port_serialno = selected_port.serial_number

                print("\nUsing device on "+str(port_path))

            print("\nProbing device...")

            try:
                rnode_serial = rnode_open_serial(port_path)
            except Exception as e:
                RNS.log("Could not open the specified serial port. The contained exception was:")
                RNS.log(str(e))
                graceful_exit()

            rnode = RNode(rnode_serial)
            rnode.usb_serial_id = port_serialno
            thread = threading.Thread(target=rnode.readLoop, daemon=True).start()
            try:
                rnode.device_probe()
            except Exception as e:
                RNS.log("No answer from device")

            if rnode.detected:
                RNS.log("Trying to read EEPROM...")
                rnode.download_eeprom()
            else:
                RNS.log("Could not detect a connected RNode")

            if rnode.platform == ROM.PLATFORM_ESP32:
                if rnode.provisioned:
                    if not rnode.signature_valid:
                        print("\nThe device signature in this RNode is unknown and cannot be verified. It is still")
                        print("possible to extract the firmware from it, but you should make absolutely sure that")
                        print("it comes from a trusted source. It is possible that someone could have modified the")
                        print("firmware. If that is the case, these modifications will propagate to any new RNodes")
                        print("descendent from this one!")
                        print("\nHit enter if you are sure you want to continue.")
                        input()

                    if rnode.firmware_hash != None:
                        extracted_hash = rnode.firmware_hash
                        extracted_version = rnode.version
                        rnode.disconnect()
                        v_str = str(extracted_version)+" "+RNS.hexrep(extracted_hash, delimit=False)
                        print("\nFound RNode Firmvare v"+v_str)

                        print("\nReady to extract firmware images from the RNode")
                        print("Press enter to start the extraction process")
                        input()
                        extract_recovery_esptool()

                        hash_f = open(EXT_DIR+"/extracted_rnode_firmware.version", "wb")
                        hash_f.write(v_str.encode("utf-8"))
                        hash_f.close()

                        extraction_parts = [
                            ("bootloader", "python \""+CNF_DIR+"/recovery_esptool.py\" --chip esp32 --port "+port_path+" --baud "+args.baud_flash+" --before default_reset --after hard_reset read_flash 0x1000 0x4650 \""+EXT_DIR+"/extracted_rnode_firmware.bootloader\""),
                            ("partition table", "python \""+CNF_DIR+"/recovery_esptool.py\" --chip esp32 --port "+port_path+" --baud "+args.baud_flash+" --before default_reset --after hard_reset read_flash 0x8000 0xC00 \""+EXT_DIR+"/extracted_rnode_firmware.partitions\""),
                            ("app boot", "python \""+CNF_DIR+"/recovery_esptool.py\" --chip esp32 --port "+port_path+" --baud "+args.baud_flash+" --before default_reset --after hard_reset read_flash 0xe000 0x2000 \""+EXT_DIR+"/extracted_rnode_firmware.boot_app0\""),
                            ("application image", "python \""+CNF_DIR+"/recovery_esptool.py\" --chip esp32 --port "+port_path+" --baud "+args.baud_flash+" --before default_reset --after hard_reset read_flash 0x10000 0x200000 \""+EXT_DIR+"/extracted_rnode_firmware.bin\""),
                            ("console image", "python \""+CNF_DIR+"/recovery_esptool.py\" --chip esp32 --port "+port_path+" --baud "+args.baud_flash+" --before default_reset --after hard_reset read_flash 0x210000 0x1F0000 \""+EXT_DIR+"/extracted_console_image.bin\""),
                        ]
                        import subprocess, shlex
                        for part, command in extraction_parts:
                            print("Extracting "+part+"...")
                            if subprocess.call(shlex.split(command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
                                print("The extraction failed, the following command did not complete successfully:\n"+command)
                                exit(182)

                        print("\nFirmware successfully extracted!")
                        print("\nYou can now use this firmware to update or autoinstall other RNodes")
                        exit()
                    else:
                        print("Could not read firmware information from device")

                print("\nRNode firmware extraction failed")
                graceful_exit(180)
            else:
                print("\nFirmware extraction is currently only supported on ESP32-based RNodes.")
                graceful_exit(170)

        if args.autoinstall:
            clear()
            if not args.port:
                print("\nHello!\n\nThis guide will help you install the RNode firmware on supported")
                print("and homebrew devices. Please connect the device you wish to set\nup now. Hit enter when it is connected.")
                input()

            global squashvw
            squashvw = True

            selected_port = None
            if not args.port:
                ports = list_ports.comports()
                portlist = []
                for port in ports:
                    portlist.insert(0, port) 
                
                pi = 1
                print("Detected serial ports:")
                for port in portlist:
                    print("  ["+str(pi)+"] "+str(port.device)+" ("+str(port.product)+", "+str(port.serial_number)+")")
                    pi += 1

                print("\nEnter the number of the serial port your device is connected to:\n? ", end="")
                try:
                    c_port = int(input())
                    if c_port < 1 or c_port > len(ports):
                        raise ValueError()

                    selected_port = portlist[c_port-1]
                except Exception as e:
                    print("That port does not exist, exiting now.")
                    graceful_exit()

                if selected_port == None:
                    print("Could not select port, exiting now.")
                    graceful_exit()

                port_path = selected_port.device
                port_product = selected_port.product
                port_serialno = selected_port.serial_number

                clear()
                print("\nOk, using device on "+str(port_path)+" ("+str(port_product)+", "+str(port_serialno)+")")

            else:
                ports = list_ports.comports()

                for port in ports:
                    if port.device == args.port:
                        selected_port = port

                if selected_port == None:
                    print("Could not find specified port "+str(args.port)+", exiting now")
                    graceful_exit()

                port_path = selected_port.device
                port_product = selected_port.product
                port_serialno = selected_port.serial_number

                print("\nUsing device on "+str(port_path))

            print("\nProbing device...")

            try:
                rnode_serial = rnode_open_serial(port_path)
            except Exception as e:
                RNS.log("Could not open the specified serial port. The contained exception was:")
                RNS.log(str(e))
                graceful_exit()

            rnode = RNode(rnode_serial)
            rnode.usb_serial_id = port_serialno
            thread = threading.Thread(target=rnode.readLoop, daemon=True).start()
            try:
                rnode.device_probe()
            except Exception as e:
                RNS.log("No answer from device")

            if rnode.detected:
                RNS.log("Trying to read EEPROM...")
                rnode.download_eeprom()

            if rnode.provisioned and rnode.signature_valid:
                print("\nThis device is already installed and provisioned. No further action will")
                print("be taken. If you wish to completely reinstall this device, you must first")
                print("wipe the current EEPROM. See the help for more info.\n\nExiting now.")
                graceful_exit()

            print("\n---------------------------------------------------------------------------")
            print("                               Device Selection")
            if rnode.detected:
                print("\nThe device seems to have an RNode firmware installed, but it was not")
                print("provisioned correctly, or it is corrupt. We are going to reinstall the")
                print("correct firmware and provision it.")
            else:
                print("\nIt looks like this is a fresh device with no RNode firmware.")
                
            print("")
            print("What kind of device is this?\n")
            print("       |   Select this option if you have an RNode of a specific")
            print("      \\ /  type, built from a recipe or bought from a vendor.")
            print("       '")
            print("[1]  A specific kind of RNode")
            print("")
            print("       |   Select this option if you have put together an RNode")
            print("      \\ /  of your own design, or if you are prototyping one.")
            print("       '")
            print("[2]  Homebrew RNode")
            print("")
            print("       |   Select one of these options if you want to easily turn")
            print("      \\ /  a supported development board into an RNode.")
            print("       '")
            print("[3]  LilyGO LoRa32 v2.1 (aka T3 v1.6 / T3 v1.6.1)")
            print("[4]  LilyGO LoRa32 v2.0")
            print("[5]  LilyGO LoRa32 v1.0")
            print("[6]  LilyGO T-Beam")
            print("[7]  Heltec LoRa32 v2")
            print("[8]  Heltec LoRa32 v3")
            print("[9]  Heltec LoRa32 v4")
            print("[10] LilyGO LoRa T3S3")
            print("[11] RAK4631")
            print("[12] LilyGo T-Echo")
            print("[13] LilyGO T-Beam Supreme")
            print("[14] LilyGO T-Deck")
            print("[15] Heltec T114")
            print("[16] Seeed XIAO ESP32S3 Wio-SX1262")
            print("")
            print("---------------------------------------------------------------------------")
            print("\nEnter the number that matches your device type:\n? ", end="")

            selected_product = None
            try:
                c_dev = int(input())
                c_mod = False
                if c_dev < 1 or c_dev > 16:
                    raise ValueError()
                elif c_dev == 1:
                    selected_product = ROM.PRODUCT_RNODE
                elif c_dev == 2:
                    selected_product = ROM.PRODUCT_HMBRW
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                        Homebrew RNode Installer")
                    print("")
                    print("This option allows you to install and provision the RNode firmware on a")
                    print("custom board design, or a custom device created by coupling a generic")
                    print("development board with a supported transceiver module.")
                    print("")
                    print("Important! Using RNode firmware on homebrew devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 6:
                    selected_product = ROM.PRODUCT_TBEAM
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                          T-Beam RNode Installer")
                    print("")
                    print("The RNode firmware can currently be installed on T-Beam devices using the")
                    print("SX1276, SX1278, SX1262 and SX1268 transceiver chips.")
                    print("")
                    print("Important! Using RNode firmware on T-Beam devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 13:
                    selected_product = ROM.PRODUCT_TBEAM_S_V1
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                       T-Beam Supreme RNode Installer")
                    print("")
                    print("The RNode firmware can currently be installed on T-Beam Supreme devices")
                    print("using the SX1262 and SX1268 transceiver chips.")
                    print("")
                    print("Important! Using RNode firmware on T-Beam devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 14:
                    selected_product = ROM.PRODUCT_TDECK
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                            T-Deck RNode Installer")
                    print("")
                    print("The RNode firmware can currently be installed on T-Deck devices using the")
                    print("SX1262 and SX1268 transceiver chips.")
                    print("")
                    print("Important! Using RNode firmware on T-Beam devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 4:
                    selected_product = ROM.PRODUCT_T32_20
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                     LilyGO LoRa32 v2.0 RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on LoRa32 devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 5:
                    selected_product = ROM.PRODUCT_T32_10
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                     LilyGO LoRa32 v1.0 RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on LoRa32 devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it.")
                    print("")
                    print("Please Note! This device is known to have a faulty battery charging circuit,")
                    print("which can result in overcharging and damaging batteries. If at all possible,")
                    print("it is recommended to avoid this device.")
                    print("")
                    print("Hit enter if you're sure you wish to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 3:
                    selected_product = ROM.PRODUCT_T32_21
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                     LilyGO LoRa32 v2.1 RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on LoRa32 devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 7:
                    selected_product = ROM.PRODUCT_H32_V2
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                     Heltec LoRa32 v2.0 RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on Heltec devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 10:
                    selected_product = ROM.PRODUCT_RNODE
                    c_mod = True
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                     LilyGO LoRa32 T3S3 RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on T3S3 devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 8:
                    selected_product = ROM.PRODUCT_H32_V3
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                     Heltec LoRa32 v3.0 RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on Heltec devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 9:
                    selected_product = ROM.PRODUCT_H32_V4
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                      Heltec LoRa32 v4 RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on Heltec devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 11:
                    selected_product = ROM.PRODUCT_RAK4631
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                     RAK4631 RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on RAKwireless devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 12:
                    selected_product = ROM.PRODUCT_TECHO
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                     LilyGo T-Echo RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on LilyGo T-Echo devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 15:
                    selected_product = ROM.PRODUCT_HELTEC_T114
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                      Heltec T114 RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on Heltec T114 devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()
                elif c_dev == 16:
                    selected_product = ROM.PRODUCT_XIAO_S3
                    clear()
                    print("")
                    print("---------------------------------------------------------------------------")
                    print("                 SeeedStudio XIAO esp32s3 wio RNode Installer")
                    print("")
                    print("Important! Using RNode firmware on SeeedStudio XIAO/wio devices should currently be")
                    print("considered experimental. It is not intended for production or critical use.")
                    print("The currently supplied firmware is provided AS-IS as a courtesy to those")
                    print("who would like to experiment with it. Hit enter to continue.")
                    print("---------------------------------------------------------------------------")
                    input()

            except Exception as e:
                print("That device type does not exist, exiting now.")
                graceful_exit()

            selected_platform = None
            selected_model = None
            selected_mcu = None

            if selected_product == ROM.PRODUCT_HMBRW:
                print("\nWhat kind of microcontroller is your board based on?\n")
                print("[1] AVR ATmega1284P")
                print("[2] AVR ATmega2560")
                print("[3] Espressif Systems ESP32")
                print("\n? ", end="")
                try:
                    c_mcu = int(input())
                    if c_mcu < 1 or c_mcu > 3:
                        raise ValueError()
                    elif c_mcu == 1:
                        selected_mcu = ROM.MCU_1284P
                        selected_platform = ROM.PLATFORM_AVR
                    elif c_mcu == 2:
                        selected_mcu = ROM.MCU_2560
                        selected_platform = ROM.PLATFORM_AVR
                    elif c_mcu == 3:
                        selected_mcu = ROM.MCU_ESP32
                        selected_platform = ROM.PLATFORM_ESP32
                    selected_model = ROM.MODEL_FF

                except Exception as e:
                    print("That MCU type does not exist, exiting now.")
                    graceful_exit()

                print("\nWhat transceiver module does your board use?\n")
                print("[1] SX1276/SX1278 with antenna port on PA_BOOST pin")
                print("[2] SX1276/SX1278 with antenna port on RFO pin")
                print("\n? ", end="")
                try:
                    c_trxm = int(input())
                    if c_trxm < 1 or c_trxm > 3:
                        raise ValueError()
                    elif c_trxm == 1:
                        selected_model = ROM.MODEL_FE
                    elif c_trxm == 2:
                        selected_model = ROM.MODEL_FF

                except Exception as e:
                    print("That transceiver type does not exist, exiting now.")
                    graceful_exit()


            elif selected_product == ROM.PRODUCT_RNODE:
                if not c_mod:
                    selected_mcu = ROM.MCU_1284P
                    print("\nWhat model is this RNode?\n")
                    print("[1] Handheld v2.1 RNode, 410 - 525 MHz")
                    print("[2] Handheld v2.1 RNode, 820 - 1020 MHz")
                    print("")
                    print("[3] Original v1.x RNode, 410 - 525 MHz")
                    print("[4] Original v1.x RNode, 820 - 1020 MHz")
                    print("")
                    print("[5] Prototype v2.2 RNode, 410 - 525 MHz")
                    print("[6] Prototype v2.2 RNode, 820 - 1020 MHz")
                    print("\n? ", end="")
                    try:
                        c_model = int(input())
                        if c_model < 1 or c_model > 6:
                            raise ValueError()
                        elif c_model == 1:
                            selected_model = ROM.MODEL_A2
                            selected_mcu = ROM.MCU_ESP32
                            selected_platform = ROM.PLATFORM_ESP32
                        elif c_model == 2:
                            selected_model = ROM.MODEL_A7
                            selected_mcu = ROM.MCU_ESP32
                            selected_platform = ROM.PLATFORM_ESP32
                        elif c_model == 3:
                            selected_model = ROM.MODEL_A4
                            selected_platform = ROM.PLATFORM_AVR
                        elif c_model == 4:
                            selected_model = ROM.MODEL_A9
                            selected_platform = ROM.PLATFORM_AVR
                        elif c_model == 5:
                            selected_model = ROM.MODEL_A1
                            selected_mcu = ROM.MCU_ESP32
                            selected_platform = ROM.PLATFORM_ESP32
                        elif c_model == 6:
                            selected_model = ROM.MODEL_A6
                            selected_mcu = ROM.MCU_ESP32
                            selected_platform = ROM.PLATFORM_ESP32
                        # elif c_model == 5:
                        #     selected_model = ROM.MODEL_A3
                        #     selected_mcu = ROM.MCU_ESP32
                        #     selected_platform = ROM.PLATFORM_ESP32
                        # elif c_model == 6:
                        #     selected_model = ROM.MODEL_A8
                        #     selected_mcu = ROM.MCU_ESP32
                        #     selected_platform = ROM.PLATFORM_ESP32
                    except Exception as e:
                        print("That model does not exist, exiting now.")
                        graceful_exit()
                else:
                    print("\nWhat band is this T3S3 for?\n")
                    print("[1] 433 MHz         (with SX1278 chip)")
                    print("[2] 868/915/923 MHz (with SX1276 chip)")
                    print("");
                    print("[3] 433 MHz         (with SX1268 chip)")
                    print("[4] 868/915/923 MHz (with SX1262 chip)")
                    print("");
                    print("[5] 2.4 GHz         (with SX1280 chip and PA)")
                    print("\n? ", end="")
                    try:
                        c_model = int(input())
                        if c_model < 1 or c_model > 5:
                            raise ValueError()
                        elif c_model == 1:
                            selected_model = ROM.MODEL_A5
                            selected_mcu = ROM.MCU_ESP32
                            selected_platform = ROM.PLATFORM_ESP32
                        elif c_model == 2:
                            selected_model = ROM.MODEL_AA
                            selected_mcu = ROM.MCU_ESP32
                            selected_platform = ROM.PLATFORM_ESP32
                        elif c_model == 3:
                            selected_model = ROM.MODEL_A1
                            selected_mcu = ROM.MCU_ESP32
                            selected_platform = ROM.PLATFORM_ESP32
                        elif c_model == 4:
                            selected_model = ROM.MODEL_A6
                            selected_mcu = ROM.MCU_ESP32
                            selected_platform = ROM.PLATFORM_ESP32
                        elif c_model == 5:
                            selected_model = ROM.MODEL_AC
                            selected_mcu = ROM.MCU_ESP32
                            selected_platform = ROM.PLATFORM_ESP32
                    except Exception as e:
                        print("That model does not exist, exiting now.")
                        graceful_exit()

            elif selected_product == ROM.PRODUCT_TBEAM:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this T-Beam for?\n")
                print("[1] 433 MHz         (with SX1278 chip)")
                print("[2] 868/915/923 MHz (with SX1276 chip)")
                print("");
                print("[3] 433 MHz         (with SX1268 chip)")
                print("[4] 868/915/923 MHz (with SX1262 chip)")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 4:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_E4
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model == 2:
                        selected_model = ROM.MODEL_E9
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model == 3:
                        selected_model = ROM.MODEL_E3
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model == 4:
                        selected_model = ROM.MODEL_E8
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    graceful_exit()

            elif selected_product == ROM.PRODUCT_TBEAM_S_V1:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this T-Beam Supreme for?\n")
                print("[1] 433 MHz         (with SX1268 chip)")
                print("[2] 868/915/923 MHz (with SX1262 chip)")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 2:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_DB
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model == 2:
                        selected_model = ROM.MODEL_DC
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    graceful_exit()

            elif selected_product == ROM.PRODUCT_TDECK:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this T-Deck for?\n")
                print("[1] 433 MHz         (with SX1268 chip)")
                print("[2] 868/915/923 MHz (with SX1262 chip)")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 2:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_D4
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model == 2:
                        selected_model = ROM.MODEL_D9
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    graceful_exit()

            elif selected_product == ROM.PRODUCT_T32_10:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this LoRa32 for?\n")
                print("[1] 433 MHz")
                print("[2] 868 MHz")
                print("[3] 915 MHz")
                print("[4] 923 MHz")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 4:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_BA
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model > 1:
                        selected_model = ROM.MODEL_BB
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    graceful_exit()

            elif selected_product == ROM.PRODUCT_T32_20:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this LoRa32 for?\n")
                print("[1] 433 MHz")
                print("[2] 868 MHz")
                print("[3] 915 MHz")
                print("[4] 923 MHz")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 4:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_B3
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model > 1:
                        selected_model = ROM.MODEL_B8
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    graceful_exit()

            elif selected_product == ROM.PRODUCT_T32_21:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this LoRa32 for?\n")
                print("[1] 433 MHz")
                print("[2] 868/915/923 MHz")
                print("[3] 433 MHz, with TCXO")
                print("[4] 868/915/923 MHz, with TCXO")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 4:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_B4
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model == 2:
                        selected_model = ROM.MODEL_B9
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model == 3:
                        selected_model = ROM.MODEL_B4_TCXO
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model == 4:
                        selected_model = ROM.MODEL_B9_TCXO
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    graceful_exit()

            elif selected_product == ROM.PRODUCT_H32_V2:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this Heltec LoRa32 V2 for?\n")
                print("[1] 433 MHz")
                print("[2] 868 MHz")
                print("[3] 915 MHz")
                print("[4] 923 MHz")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 4:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_C4
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model > 1:
                        selected_model = ROM.MODEL_C9
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    graceful_exit()

            elif selected_product == ROM.PRODUCT_H32_V3:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this Heltec LoRa32 V3 for?\n")
                print("[1] 433 MHz")
                print("[2] 868 MHz")
                print("[3] 915 MHz")
                print("[4] 923 MHz")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 4:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_C5
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model > 1:
                        selected_model = ROM.MODEL_CA
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    exit()
            
            elif selected_product == ROM.PRODUCT_H32_V4:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this Heltec LoRa32 V4 for?\n")
                print("[1] 868 MHz (28 dBm output)")
                print("[2] 915 MHz (28 dBm output)")
                print("[3] 923 MHz (28 dBm output)")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 3:
                        raise ValueError()
                    else:
                        selected_model = ROM.MODEL_C8
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    exit()
            
            elif selected_product == ROM.PRODUCT_HELTEC_T114:
                selected_mcu = ROM.MCU_NRF52
                print("\nWhat band is this Heltec T114 for?\n")
                print("[1] 433 MHz")
                print("[2] 868 MHz")
                print("[3] 915 MHz")
                print("[4] 923 MHz")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 4:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_C6
                        selected_platform = ROM.PLATFORM_NRF52
                    elif c_model > 1:
                        selected_model = ROM.MODEL_C7
                        selected_platform = ROM.PLATFORM_NRF52
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    exit()

            elif selected_product == ROM.PRODUCT_XIAO_S3:
                selected_mcu = ROM.MCU_ESP32
                print("\nWhat band is this XIAO esp32s3 wio module for?\n")
                print("[1] 433 MHz")
                print("[2] 868 MHz")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 2:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_DE
                        selected_platform = ROM.PLATFORM_ESP32
                    elif c_model == 2:
                        selected_model = ROM.MODEL_DD
                        selected_platform = ROM.PLATFORM_ESP32
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    exit()

            elif selected_product == ROM.PRODUCT_RAK4631:
                selected_mcu = ROM.MCU_NRF52
                print("\nWhat band is this RAK4631 for?\n")
                print("[1] 433 MHz")
                print("[2] 868 MHz")
                print("[3] 915 MHz")
                print("[4] 923 MHz")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 4:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_11
                        selected_platform = ROM.PLATFORM_NRF52
                    elif c_model > 1:
                        selected_model = ROM.MODEL_12
                        selected_platform = ROM.PLATFORM_NRF52
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    graceful_exit()
            elif selected_product == ROM.PRODUCT_TECHO:
                selected_mcu = ROM.MCU_NRF52
                print("\nWhat band is this T-Echo for?\n")
                print("[1] 433 MHz")
                print("[2] 868 MHz")
                print("[3] 915 MHz")
                print("[4] 923 MHz")
                print("\n? ", end="")
                try:
                    c_model = int(input())
                    if c_model < 1 or c_model > 4:
                        raise ValueError()
                    elif c_model == 1:
                        selected_model = ROM.MODEL_16
                        selected_platform = ROM.PLATFORM_NRF52
                    elif c_model > 1:
                        selected_model = ROM.MODEL_17
                        selected_platform = ROM.PLATFORM_NRF52
                except Exception as e:
                    print("That band does not exist, exiting now.")
                    graceful_exit()

            if selected_model != ROM.MODEL_FF and selected_model != ROM.MODEL_FE:
                fw_filename = models[selected_model][4]

            else:
                if selected_platform == ROM.PLATFORM_AVR:
                    if selected_mcu == ROM.MCU_1284P:
                        fw_filename = "rnode_firmware.hex"
                    elif selected_mcu == ROM.MCU_2560:
                        fw_filename = "rnode_firmware_m2560.hex"
                
                elif selected_platform == ROM.PLATFORM_ESP32:
                    fw_filename = None
                    print("\nWhat kind of ESP32 board is this?\n")
                    print("[1] Adafruit Feather ESP32 (HUZZAH32)")
                    print("[2] Generic ESP32 board")
                    print("\n? ", end="")
                    try:
                        c_eboard = int(input())
                        if c_eboard < 1 or c_eboard > 2:
                            raise ValueError()
                        elif c_eboard == 1:
                            fw_filename = "rnode_firmware_featheresp32.zip"
                        elif c_eboard == 2:
                            fw_filename = "rnode_firmware_esp32_generic.zip"
                    except Exception as e:
                        print("That ESP32 board does not exist, exiting now.")
                        graceful_exit()

            if fw_filename == None:
                print("")
                print("Sorry, no firmware for your board currently exists.")
                print("Help making it a reality by contributing code or by")
                print("donating to the project.")
                print("")
                print_donation_block()
                print("")
                graceful_exit()

            if args.use_extracted:
                fw_filename = "extracted_rnode_firmware.zip"

            clear()
            print("")
            print("------------------------------------------------------------------------------")
            print("                               Installer Ready")
            print("")
            print("Ok, that should be all the information we need. Please confirm the following")
            print("summary before proceeding. In the next step, the device will be flashed and")
            print("provisioned, so make sure that you are satisfied with your choices.\n")

            print("Serial port     : "+str(selected_port.device))
            print("Device type     : "+str(products[selected_product])+" "+str(models[selected_model][3]))
            print("Platform        : "+str(platforms[selected_platform]))
            print("Device MCU      : "+str(mcus[selected_mcu]))
            print("Firmware file   : "+str(fw_filename))

            print("")
            print("------------------------------------------------------------------------------")

            print("\nIs the above correct? [y/N] ", end="")
            try:
                c_ok = input().lower()
                if c_ok != "y":
                    raise ValueError()
            except Exception as e:
                print("OK, aborting now.")
                graceful_exit()

            args.key = True
            args.port = selected_port.device
            args.platform = selected_platform
            args.hwrev = 1
            mapped_model = selected_model
            mapped_product = selected_product
            args.update = False
            args.flash = True

            try:
                RNS.log("Checking firmware file availability...")
                ensure_firmware_file(fw_filename)
            except Exception as e:
                RNS.log("Could not obain firmware package for your board")
                RNS.log("The contained exception was: "+str(e))
                graceful_exit()

            rnode.disconnect()

        if args.public:
            private_bytes = None
            try:
                file = open(FWD_DIR+"/signing.key", "rb")
                private_bytes = file.read()
                file.close()
            except Exception as e:
                RNS.log("Could not load EEPROM signing key")

            try:
                private_key = serialization.load_der_private_key(
                    private_bytes,
                    password=None,
                    backend=default_backend()
                )
                public_key = private_key.public_key()
                public_bytes = public_key.public_bytes(
                    encoding=serialization.Encoding.DER,
                    format=serialization.PublicFormat.SubjectPublicKeyInfo
                )
                RNS.log("EEPROM Signing Public key:")
                RNS.log(RNS.hexrep(public_bytes, delimit=False))

            except Exception as e:
                RNS.log("Could not deserialize signing key")
                RNS.log(str(e))

            try:
                device_signer = RNS.Identity.from_file(FWD_DIR+"/device.key")
                RNS.log("")
                RNS.log("Device Signing Public key:")
                RNS.log(RNS.hexrep(device_signer.get_public_key()[32:], delimit=True))

            except Exception as e:
                RNS.log("Could not load device signing key")
                

            graceful_exit()

        if args.key:
            if not os.path.isfile(FWD_DIR+"/device.key"):
                try:
                    RNS.log("Generating a new device signing key...")
                    device_signer = RNS.Identity()
                    device_signer.to_file(FWD_DIR+"/device.key")
                    RNS.log("Device signing key written to "+str(FWD_DIR+"/device.key"))
                except Exception as e:
                    RNS.log("Could not create new device signing key at "+str(FWD_DIR+"/device.key")+". The contained exception was:")
                    RNS.log(str(e))
                    RNS.log("Please ensure filesystem access and try again.")
                    graceful_exit(81)
            else:
                try:
                    device_signer = RNS.Identity.from_file(FWD_DIR+"/device.key")
                except Exception as e:
                    RNS.log("Could not load device signing key from "+str(FWD_DIR+"/device.key")+". The contained exception was:")
                    RNS.log(str(e))
                    RNS.log("Please restore or clear the key and try again.")
                    graceful_exit(82)

            if not os.path.isfile(FWD_DIR+"/signing.key"):
                RNS.log("Generating a new EEPROM signing key...")
            private_key = rsa.generate_private_key(
                public_exponent=65537,
                key_size=1024,
                backend=default_backend()
            )
            private_bytes = private_key.private_bytes(
                encoding=serialization.Encoding.DER,
                format=serialization.PrivateFormat.PKCS8,
                encryption_algorithm=serialization.NoEncryption()
            )
            public_key = private_key.public_key()
            public_bytes = public_key.public_bytes(
                encoding=serialization.Encoding.DER,
                format=serialization.PublicFormat.SubjectPublicKeyInfo
            )
            os.makedirs(FWD_DIR, exist_ok=True)
            if os.path.isdir(FWD_DIR):
                if os.path.isfile(FWD_DIR+"/signing.key"):
                    if not args.autoinstall:
                        RNS.log("EEPROM Signing key already exists, not overwriting!")
                        RNS.log("Manually delete this key to create a new one.")
                else:
                    file = open(FWD_DIR+"/signing.key", "wb")
                    file.write(private_bytes)
                    file.close()

                    if not squashvw:
                        RNS.log("Wrote signing key")
                        RNS.log("Public key:")
                        RNS.log(RNS.hexrep(public_bytes, delimit=False))
            else:
                RNS.log("The firmware directory does not exist, can't write key!")

            if not args.autoinstall:
                graceful_exit()

        def get_partition_hash(platform, partition_file):
            try:
                if platform == ROM.PLATFORM_ESP32 or platform == ROM.PLATFORM_AVR:
                    firmware_data = open(partition_file, "rb").read()
                    # Calculate the digest manually and see if it matches the
                    # SHA256 digest included in the ESP32 image.
                    calc_hash = hashlib.sha256(firmware_data[0:-32]).digest()
                    part_hash = firmware_data[-32:]

                    if calc_hash == part_hash:
                        return part_hash
                    else:
                        return None
                
                elif platform == ROM.PLATFORM_NRF52:
                    # Calculate digest manually, as it is not included in the image.
                    firmware_data = open(partition_file, "rb")
                    hash = hashlib.file_digest(firmware_data, 'sha256').digest()
                    firmware_data.close()
                    return hash
            except Exception as e:
                RNS.log("Could not calculate firmware partition hash. The contained exception was:")
                RNS.log(str(e))

        def get_flasher_call(platform, fw_filename):
            global selected_version
            from shutil import which
            if platform == "unzip":
                flasher = "unzip"
                if which(flasher) is not None:
                    return [flasher, "-o", UPD_DIR+"/"+selected_version+"/"+fw_filename, "-d", UPD_DIR+"/"+selected_version]
                else:
                    RNS.log("")
                    RNS.log("You do not currently have the \""+flasher+"\" program installed on your system.")
                    RNS.log("Unfortunately, that means we can't proceed, since it is needed to flash your")
                    RNS.log("board. You can install it via your package manager, for example:")
                    RNS.log("")
                    RNS.log("  sudo apt install "+flasher)
                    RNS.log("")
                    RNS.log("Please install \""+flasher+"\" and try again.")
                    graceful_exit()
            elif platform == ROM.PLATFORM_AVR:
                flasher = "avrdude"
                if which(flasher) is not None:
                    # avrdude -C/home/markqvist/.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17/etc/avrdude.conf -q -q -V -patmega2560 -cwiring -P/dev/ttyACM0 -b115200 -D -Uflash:w:/tmp/arduino-sketch-0E260F46C421A84A7CBAD48E859C8E64/RNode_Firmware.ino.hex:i
                    # avrdude -q -q -V -patmega2560 -cwiring -P/dev/ttyACM0 -b115200 -D -Uflash:w:/tmp/arduino-sketch-0E260F46C421A84A7CBAD48E859C8E64/RNode_Firmware.ino.hex:i
                    if fw_filename == "rnode_firmware.hex":
                        return [flasher, "-P", args.port, "-p", "m1284p", "-c", "arduino", "-b", "115200", "-U", "flash:w:"+UPD_DIR+"/"+selected_version+"/"+fw_filename+":i"]
                    elif fw_filename == "rnode_firmware_m2560.hex":
                        return [flasher, "-P", args.port, "-p", "atmega2560", "-c", "wiring", "-D", "-b", "115200", "-U", "flash:w:"+UPD_DIR+"/"+selected_version+"/"+fw_filename]
                else:
                    RNS.log("")
                    RNS.log("You do not currently have the \""+flasher+"\" program installed on your system.")
                    RNS.log("Unfortunately, that means we can't proceed, since it is needed to flash your")
                    RNS.log("board. You can install it via your package manager, for example:")
                    RNS.log("")
                    RNS.log("  sudo apt install avrdude")
                    RNS.log("")
                    RNS.log("Please install \""+flasher+"\" and try again.")
                    graceful_exit()
            elif platform == ROM.PLATFORM_ESP32:
                numeric_version = float(selected_version)
                flasher_dir = UPD_DIR+"/"+selected_version
                flasher = flasher_dir+"/esptool.py"
                if not os.path.isfile(flasher):
                    if os.path.isfile(CNF_DIR+"/recovery_esptool.py"):
                        import shutil
                        if not os.path.isdir(flasher_dir):
                            os.makedirs(flasher_dir)
                        shutil.copy(CNF_DIR+"/recovery_esptool.py", flasher)
                        RNS.log("No flasher present, using recovery flasher to write firmware to device")

                if os.path.isfile(flasher):
                    import stat
                    os.chmod(flasher, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP)

                if which(flasher) is not None:
                    if fw_filename == "rnode_firmware_tbeam.zip":
                        if numeric_version >= 1.55:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_tbeam_sx1262.zip":
                        if numeric_version >= 1.55:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam_sx1262.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam_sx1262.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam_sx1262.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam_sx1262.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_lora32v10.zip":
                        if numeric_version >= 1.59:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v10.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v10.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v10.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v10.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_lora32v20.zip":
                        if numeric_version >= 1.55:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v20.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_lora32v21.zip":
                        if numeric_version >= 1.55:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_lora32v21_tcxo.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "4MB",
                            "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21_tcxo.boot_app0",
                            "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21_tcxo.bootloader",
                            "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21_tcxo.bin",
                            "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                            "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_lora32v21_tcxo.partitions",
                        ]
                    elif fw_filename == "rnode_firmware_heltec32v2.zip":
                        if numeric_version >= 1.55:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "8MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v2.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v2.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v2.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v2.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "8MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v2.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v2.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v2.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v2.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_heltec32v3.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32-s3",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "8MB",
                            "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v3.boot_app0",
                            "0x0",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v3.bootloader",
                            "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v3.bin",
                            "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                            "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v3.partitions",
                        ]
                    elif fw_filename == "rnode_firmware_heltec32v4pa.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32-s3",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "16MB",
                            "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.boot_app0",
                            "0x0",     UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.bootloader",
                            "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.bin",
                            "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                            "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.partitions",
                        ]
                    elif fw_filename == "rnode_firmware_featheresp32.zip":
                        if numeric_version >= 1.55:
                            return [
                               sys.executable,  flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_featheresp32.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_featheresp32.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_featheresp32.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_featheresp32.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_featheresp32.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_featheresp32.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_featheresp32.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_featheresp32.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_esp32_generic.zip":
                        if numeric_version >= 1.55:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_esp32_generic.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_esp32_generic.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_esp32_generic.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_esp32_generic.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_esp32_generic.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_esp32_generic.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_esp32_generic.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_esp32_generic.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_ng20.zip":
                        if numeric_version >= 1.55:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng20.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng20.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_ng20.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng20.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng20.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng20.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_ng20.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng20.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_ng21.zip":
                        if numeric_version >= 1.55:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng21.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng21.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_ng21.bin",
                                "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng21.partitions",
                            ]
                        else:
                            return [
                                sys.executable, flasher,
                                "--chip", "esp32",
                                "--port", args.port,
                                "--baud", args.baud_flash,
                                "--before", "default_reset",
                                "--after", "hard_reset",
                                "write_flash", "-z",
                                "--flash_mode", "dio",
                                "--flash_freq", "80m",
                                "--flash_size", "4MB",
                                "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng21.boot_app0",
                                "0x1000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng21.bootloader",
                                "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_ng21.bin",
                                "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_ng21.partitions",
                            ]
                    elif fw_filename == "rnode_firmware_t3s3.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32s3",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "4MB",
                            "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3.boot_app0",
                            "0x0",  UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3.bootloader",
                            "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3.bin",
                            "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                            "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3.partitions",
                        ]
                    elif fw_filename == "rnode_firmware_t3s3_sx127x.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32s3",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "4MB",
                            "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3_sx127x.boot_app0",
                            "0x0",  UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3_sx127x.bootloader",
                            "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3_sx127x.bin",
                            "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                            "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3_sx127x.partitions",
                        ]
                    elif fw_filename == "rnode_firmware_t3s3_sx1280_pa.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32s3",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "4MB",
                            "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3_sx1280_pa.boot_app0",
                            "0x0",  UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3_sx1280_pa.bootloader",
                            "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3_sx1280_pa.bin",
                            "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                            "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_t3s3_sx1280_pa.partitions",
                        ]
                    elif fw_filename == "rnode_firmware_tbeam_supreme.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32s3",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "4MB",
                            "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam_supreme.boot_app0",
                            "0x0",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam_supreme.bootloader",
                            "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam_supreme.bin",
                            "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                            "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tbeam_supreme.partitions",
                        ]
                    elif fw_filename == "rnode_firmware_tdeck.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32s3",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "4MB",
                            "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tdeck.boot_app0",
                            "0x0",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tdeck.bootloader",
                            "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_tdeck.bin",
                            "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                            "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_tdeck.partitions",
                        ]
                    elif fw_filename == "rnode_firmware_xiao_esp32s3.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32s3",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "8MB",
                            "0xe000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_xiao_esp32s3.boot_app0",
                            "0x0",  UPD_DIR+"/"+selected_version+"/rnode_firmware_xiao_esp32s3.bootloader",
                            "0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_xiao_esp32s3.bin",
                            "0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
                            "0x8000",  UPD_DIR+"/"+selected_version+"/rnode_firmware_xiao_esp32s3.partitions",
                        ]
                    elif fw_filename == "extracted_rnode_firmware.zip":
                        return [
                            sys.executable, flasher,
                            "--chip", "esp32",
                            "--port", args.port,
                            "--baud", args.baud_flash,
                            "--before", "default_reset",
                            "--after", "hard_reset",
                            "write_flash", "-z",
                            "--flash_mode", "dio",
                            "--flash_freq", "80m",
                            "--flash_size", "4MB",
                            "0x1000",  EXT_DIR+"/extracted_rnode_firmware.bootloader",
                            "0xe000",  EXT_DIR+"/extracted_rnode_firmware.boot_app0",
                            "0x8000",  EXT_DIR+"/extracted_rnode_firmware.partitions",
                            "0x10000", EXT_DIR+"/extracted_rnode_firmware.bin",
                            "0x210000",EXT_DIR+"/extracted_console_image.bin",
                        ]
                    else:
                        RNS.log("No flasher available for this board, cannot install firmware.")
                else:
                    RNS.log("")
                    RNS.log("You do not currently have the \""+flasher+"\" program installed on your system.")
                    RNS.log("Unfortunately, that means we can't proceed, since it is needed to flash your")
                    RNS.log("board. You can install it via your package manager, for example:")
                    RNS.log("")
                    RNS.log("  sudo apt install esptool")
                    RNS.log("")
                    RNS.log("Please install \""+flasher+"\" and try again.")
                    graceful_exit()

            elif platform == ROM.PLATFORM_NRF52:
                flasher = "adafruit-nrfutil"
                if which(flasher) is not None:
                    return [flasher, "dfu", "serial", "--package", UPD_DIR+"/"+selected_version+"/"+fw_filename, "-p", args.port, "-b", "115200", "-t", "1200"]
                else:
                    RNS.log("")
                    RNS.log("You do not currently have the \""+flasher+"\" program installed on your system.")
                    RNS.log("Unfortunately, that means we can't proceed, since it is needed to flash your")
                    RNS.log("board. You can install it via your package manager, for example:")
                    RNS.log("")
                    RNS.log("  pip3 install --user adafruit-nrfutil")
                    RNS.log("")
                    RNS.log("Please install \""+flasher+"\" and try again.")
                    graceful_exit()

        if args.port:
            wants_fw_provision = False
            if args.flash:
                from subprocess import call
                
                if fw_filename == None:
                    fw_filename = "rnode_firmware.hex"

                if args.platform == None:
                    args.platform = ROM.PLATFORM_AVR

                if selected_version == None:
                    RNS.log("Missing parameters, cannot continue")
                    graceful_exit(68)

                if selected_model in ROM.MANUAL_FLASH_MODELS:
                    RNS.log("")
                    RNS.log("Please put the board into flashing mode now, by holding the BOOT or PRG button,")
                    RNS.log("while momentarily pressing the RESET button. Then release the BOOT or PRG button.")
                    RNS.log("Hit enter when this is done.")
                    input()

                if fw_filename == "extracted_rnode_firmware.zip":
                    try:
                        RNS.log("Flashing RNode firmware to device on "+args.port)
                        from subprocess import call
                        rc = get_flasher_call(args.platform, fw_filename)
                        flash_status = call(rc)
                        if flash_status == 0:
                            RNS.log("Done flashing")
                            args.rom = True
                            if args.platform == ROM.PLATFORM_ESP32:
                                wants_fw_provision = True
                                RNS.log("Waiting for ESP32 reset...")
                                time.sleep(7)
                        else:
                            graceful_exit()

                    except Exception as e:
                        RNS.log("Error while flashing")
                        RNS.log(str(e))
                        graceful_exit(1)
                
                else:
                    fw_src = UPD_DIR+"/"+selected_version+"/"
                    if os.path.isfile(fw_src+fw_filename):
                        try:
                            if fw_filename.endswith(".zip"):
                                RNS.log("Decompressing firmware...")
                                try:
                                    with zipfile.ZipFile(fw_src+fw_filename) as zip:
                                        zip.extractall(fw_src)
                                except Exception as e:
                                    RNS.log("Could not decompress firmware from downloaded zip file")
                                    graceful_exit()
                                RNS.log("Firmware decompressed")

                            RNS.log("Flashing RNode firmware to device on "+args.port)
                            from subprocess import call
                            rc = get_flasher_call(args.platform, fw_filename)
                            flash_status = call(rc)
                            if flash_status == 0:
                                RNS.log("Done flashing")
                                args.rom = True
                                if args.platform == ROM.PLATFORM_ESP32:
                                    wants_fw_provision = True
                                    RNS.log("Waiting for ESP32 reset...")
                                    time.sleep(8)
                                if args.platform == ROM.PLATFORM_NRF52:
                                    wants_fw_provision = True
                                    RNS.log("Waiting for NRF52 reset...")
                                    # Don't need to wait as long this time.
                                    time.sleep(6)
                            else:
                                RNS.log("Error from flasher ("+str(flash_status)+") while writing.")
                                RNS.log("Some boards have trouble flashing at high speeds, and you can")
                                RNS.log("try flashing with a lower baud rate, as in this example:")
                                RNS.log("rnodeconf --autoinstall --baud-flash 115200")
                                graceful_exit()

                        except Exception as e:
                            RNS.log("Error while flashing")
                            RNS.log(str(e))
                            graceful_exit(1)
                    else:
                        RNS.log("Firmware file not found")
                        graceful_exit()

                if selected_model in ROM.MANUAL_FLASH_MODELS:
                    RNS.log("")
                    RNS.log("Please take the board out of flashing mode by momentarily pressing the RESET button.")
                    RNS.log("Hit enter when this is done.")
                    input()
                    sleep(2.5)

            RNS.log("Opening serial port "+args.port+"...")
            try:
                rnode_port = args.port
                rnode_serial = rnode_open_serial(rnode_port)
            except Exception as e:
                RNS.log("Could not open the specified serial port. The contained exception was:")
                RNS.log(str(e))
                graceful_exit()

            rnode = RNode(rnode_serial)
            ports = list_ports.comports()
            for port in ports:
                if port.device == args.port:
                    rnode.usb_serial_id = port.serial_number
                    break
            thread = threading.Thread(target=rnode.readLoop, daemon=True).start()

            try:
                rnode.device_probe()
            except Exception as e:
                RNS.log("Serial port opened, but RNode did not respond. Is a valid firmware installed?")
                print(e)
                graceful_exit()

            if rnode.detected:
                if rnode.platform == None or rnode.mcu == None:
                    rnode.platform = ROM.PLATFORM_AVR
                    rnode.mcu = ROM.MCU_1284P


            if args.eeprom_wipe:
                RNS.log("WARNING: EEPROM is being wiped! Power down device NOW if you do not want this!")
                rnode.wipe_eeprom()

                if rnode.platform != ROM.PLATFORM_NRF52:
                    rnode.hard_reset()

                graceful_exit()

            RNS.log("Reading EEPROM...")
            rnode.download_eeprom()

            if rnode.provisioned:
                if rnode.model != ROM.MODEL_FF:
                    fw_filename = models[rnode.model][4]
                else:
                    if args.use_extracted:
                        fw_filename = "extracted_rnode_firmware.zip"
                    else:
                        if rnode.platform == ROM.PLATFORM_AVR:
                            if rnode.mcu == ROM.MCU_1284P:
                                fw_filename = "rnode_firmware.hex"
                            elif rnode.mcu == ROM.MCU_2560:
                                fw_filename = "rnode_firmware_m2560.hex"
                        elif rnode.platform == ROM.PLATFORM_ESP32:
                            if rnode.board == ROM.BOARD_HUZZAH32:
                                fw_filename = "rnode_firmware_featheresp32.zip"
                            elif rnode.board == ROM.BOARD_GENERIC_ESP32:
                                fw_filename = "rnode_firmware_esp32_generic.zip"
                        elif rnode.platform == ROM.PLATFORM_NRF52:
                            if rnode.board == ROM.BOARD_RAK4631:
                                fw_filename = "rnode_firmware_rak4631.zip"
                            else:
                                fw_filename = None

                                if args.update:
                                    RNS.log("ERROR: No firmware found for this board. Cannot update.")
                                    graceful_exit()

            if args.update:
                if not rnode.provisioned:
                    RNS.log("Device not provisioned. Cannot update device firmware.")
                    graceful_exit(1)

                if args.use_extracted:
                    fw_filename = "extracted_rnode_firmware.zip"

                from subprocess import call

                try:
                    RNS.log("Checking firmware file availability...")
                    fw_file_ensured = False
                    if selected_version == None:
                        ensure_firmware_file(fw_filename)
                        fw_file_ensured = True

                    if not force_update:
                        if rnode.version == selected_version:
                            if args.fw_version != None:
                                RNS.log("Specified firmware version ("+selected_version+") is already installed on this device")
                                RNS.log("Override with -U option to install anyway")
                                graceful_exit(0)
                            else:
                                RNS.log("Latest firmware version ("+selected_version+") is already installed on this device")
                                RNS.log("Override with -U option to install anyway")
                                graceful_exit(0)

                        if rnode.version > selected_version:
                            if args.fw_version != None:
                                RNS.log("Specified firmware version ("+selected_version+") is older than firmware already installed on this device")
                                RNS.log("Override with -U option to install anyway")
                                graceful_exit(0)
                            else:
                                RNS.log("Latest firmware version ("+selected_version+") is older than firmware already installed on this device")
                                RNS.log("Override with -U option to install anyway")
                                graceful_exit(0)

                    if not fw_file_ensured and selected_version != None:
                        ensure_firmware_file(fw_filename)

                    if fw_filename.endswith(".zip") and not fw_filename == "extracted_rnode_firmware.zip":
                        RNS.log("Decompressing firmware...")
                        fw_src = UPD_DIR+"/"+selected_version+"/"
                        try:
                            with zipfile.ZipFile(fw_src+fw_filename) as zip:
                                zip.extractall(fw_src)
                        except Exception as e:
                            RNS.log("Could not decompress firmware from downloaded zip file")
                            graceful_exit()
                        RNS.log("Firmware decompressed")

                except Exception as e:
                    RNS.log("Could not obtain firmware package for your board")
                    RNS.log("The contained exception was: "+str(e))
                    graceful_exit()

                if fw_filename == "extracted_rnode_firmware.zip":
                    update_full_path = EXT_DIR+"/extracted_rnode_firmware.version"
                else:
                    update_full_path = UPD_DIR+"/"+selected_version+"/"+fw_filename
                if os.path.isfile(update_full_path): 
                    try:
                        args.info = False
                        RNS.log("Updating RNode firmware for device on "+args.port)
                        if fw_filename == "extracted_rnode_firmware.zip":
                            vf = open(update_full_path, "rb")
                            release_info = vf.read().decode("utf-8").strip()
                            partition_hash = bytes.fromhex(release_info.split()[1])
                            vf.close()
                        else:
                            partition_filename = fw_filename.replace(".zip", ".bin")
                            if fw_filename == "extracted_rnode_firmware.zip":
                                partition_full_path = EXT_DIR+"/extracted_rnode_firmware.bin"
                            else:
                                partition_full_path = UPD_DIR+"/"+selected_version+"/"+partition_filename
                            partition_hash = get_partition_hash(rnode.platform, partition_full_path)
                        if partition_hash != None:
                            try:
                                rnode.indicate_firmware_update()
                            except Exception as e:
                                RNS.log("Error while indicating firmware update start to board, attempting update anyway...")
                            rnode.set_firmware_hash(partition_hash)
                            sleep(1)

                            if rnode.platform == ROM.PLATFORM_NRF52:
                                # Allow extra time for writing to EEPROM on NRF52. Current implementation is slow.
                                sleep(14)

                        try:
                            rnode.disconnect()
                        except Exception as e:
                            RNS.log("Error while gracefully disconnecting device before firmware update, attempting update anyway...")

                        if rnode.model in ROM.MANUAL_FLASH_MODELS:
                            RNS.log("")
                            RNS.log("Please put the board into flashing mode now, by holding the BOOT or PRG button,")
                            RNS.log("while momentarily pressing the RESET button. Then release the BOOT or PRG button.")
                            RNS.log("Hit enter when this is done.")
                            input()

                        flash_status = call(get_flasher_call(rnode.platform, fw_filename))
                        if flash_status == 0:
                            RNS.log("Flashing new firmware completed")
                            if rnode.model in ROM.MANUAL_FLASH_MODELS:
                                RNS.log("")
                                RNS.log("Please take the board out of flashing mode by momentarily pressing the RESET button.")
                                RNS.log("Hit enter when this is done.")
                                input()

                            RNS.log("Opening serial port "+args.port+"...")
                            try:
                                rnode_port = args.port
                                rnode_serial = rnode_open_serial(rnode_port)
                            except Exception as e:
                                RNS.log("Could not open the specified serial port. The contained exception was:")
                                RNS.log(str(e))
                                graceful_exit()

                            rnode = RNode(rnode_serial)
                            thread = threading.Thread(target=rnode.readLoop, daemon=True).start()

                            try:
                                rnode.device_probe()
                            except Exception as e:
                                RNS.log("Serial port opened, but RNode did not respond. Is a valid firmware installed?")
                                print(e)
                                graceful_exit()

                            if rnode.detected:
                                if rnode.platform == None or rnode.mcu == None:
                                    rnode.platform = ROM.PLATFORM_AVR
                                    rnode.mcu = ROM.MCU_1284P

                                RNS.log("Reading EEPROM...")
                                rnode.download_eeprom()

                                if rnode.provisioned:
                                    if rnode.model != ROM.MODEL_FF:
                                        fw_filename = models[rnode.model][4]
                                    else:
                                        fw_filename = None
                                    args.info = True
                                    if partition_hash != None:
                                        rnode.set_firmware_hash(partition_hash)

                            if args.info:
                                RNS.log("")
                                RNS.log("Firmware update completed successfully")
                        else:
                            RNS.log("An error occurred while flashing the new firmware, exiting now.")
                            graceful_exit()

                    except Exception as e:
                        RNS.log("Error while updating firmware")
                        RNS.log(str(e))
                else:
                    RNS.log("Firmware update file not found")
                    graceful_exit()

            if args.config:
                rnode.download_cfg_sector()
                eeprom_reserved = 200
                if rnode.platform == ROM.PLATFORM_ESP32: eeprom_size = 296
                elif rnode.platform == ROM.PLATFORM_NRF52: eeprom_size = 296
                else: eeprom_size = 4096

                eeprom_offset = eeprom_size-eeprom_reserved
                def ea(a): return a+eeprom_offset
                ec_bt   = rnode.eeprom[ROM.ADDR_CONF_BT]
                ec_dint = rnode.eeprom[ROM.ADDR_CONF_DINT]
                ec_dadr = rnode.eeprom[ROM.ADDR_CONF_DADR]
                ec_dblk = rnode.eeprom[ROM.ADDR_CONF_DBLK]
                ec_drot = rnode.eeprom[ROM.ADDR_CONF_DROT]
                ec_pset = rnode.eeprom[ROM.ADDR_CONF_PSET]
                ec_pint = rnode.eeprom[ROM.ADDR_CONF_PINT]
                ec_bset = rnode.eeprom[ROM.ADDR_CONF_BSET]
                ec_dia  = rnode.eeprom[ROM.ADDR_CONF_DIA]
                ec_wifi = rnode.eeprom[ROM.ADDR_CONF_WIFI]
                ec_wchn = rnode.eeprom[ROM.ADDR_CONF_WCHN]
                ec_ssid = None
                ec_psk  = None
                ec_ip   = None
                ec_nm   = None

                if ec_wchn < 1 or ec_wchn > 14: ec_wchn = 1
                if rnode.cfg_sector:
                    ssid_bytes = b""
                    for i in range(0, 32):
                        byte = rnode.cfg_sector[ROM.ADDR_CONF_SSID+i]
                        if byte == 0xFF: byte = 0x00
                        if byte == 0x00: break
                        else: ssid_bytes += bytes([byte])

                    try: ec_ssid = ssid_bytes.decode("utf-8")
                    except Exception as e: print(f"Error: Could not decode WiFi SSID read from device")

                    psk_bytes = b""
                    for i in range(0, 32):
                        byte = rnode.cfg_sector[ROM.ADDR_CONF_PSK+i]
                        if byte == 0xFF: byte = 0x00
                        if byte == 0x00: break
                        else: psk_bytes += bytes([byte])

                    ip_bytes = b""
                    for i in range(0, 4):
                        byte = rnode.cfg_sector[ROM.ADDR_CONF_IP+i]
                        ip_bytes += bytes([byte])
                    if len(ip_bytes) == 4: ec_ip = f"{int(ip_bytes[0])}.{int(ip_bytes[1])}.{int(ip_bytes[2])}.{int(ip_bytes[3])}"
                    if ec_ip == "255.255.255.255" or ec_ip == "0.0.0.0": ec_ip = None

                    nm_bytes = b""
                    for i in range(0, 4):
                        byte = rnode.cfg_sector[ROM.ADDR_CONF_NM+i]
                        nm_bytes += bytes([byte])
                    if len(nm_bytes) == 4: ec_nm = f"{int(nm_bytes[0])}.{int(nm_bytes[1])}.{int(nm_bytes[2])}.{int(nm_bytes[3])}"
                    if ec_nm == "255.255.255.255" or ec_nm == "0.0.0.0": ec_nm = None

                    if ec_wifi == 0x02:
                        ec_ip = "10.0.0.1"
                        ec_nm = "255.255.255.0"

                    try: ec_psk = psk_bytes.decode("utf-8")
                    except Exception as e: print(f"Error: Could not decode WiFi PSK read from device")
                    if not args.show_psk and ec_psk: ec_psk = "*"*len(ec_psk)

                print("\nDevice configuration:")
                if ec_bt == 0x73:       print(f"  Bluetooth              : Enabled")
                else:                   print(f"  Bluetooth              : Disabled")
                if ec_wifi == 0x01:     print(f"  WiFi                   : Enabled (Station)")
                if ec_wifi == 0x02:     print(f"  WiFi                   : Enabled (AP)")
                else:                   print(f"  WiFi                   : Disabled")
                if ec_wifi == 0x01 or ec_wifi == 0x02:
                    if not ec_wchn:     print(f"    Channel              : Unknown")
                    else:               print(f"    Channel              : {ec_wchn}")
                    if not ec_ssid:     print(f"    SSID                 : Not set")
                    else:               print(f"    SSID                 : {ec_ssid}")
                    if not ec_psk:      print(f"    PSK                  : Not set")
                    else:               print(f"    PSK                  : {ec_psk}")
                    if not ec_ip:       print(f"    IP Address           : DHCP")
                    else:               print(f"    IP Address           : {ec_ip}")
                    if ec_ip and ec_nm: print(f"    Network Mask         : {ec_nm}")
                if ec_dia == 0x00:      print(f"  Interference avoidance : Enabled")
                else:                   print(f"  Interference avoidance : Disabled")
                print(    f"  Display brightness     : {ec_dint}")
                if ec_dadr == 0xFF:     print(f"  Display address        : Default")
                else:                   print(f"  Display address        : {RNS.hexrep(ec_dadr, delimit=False)}")
                if ec_bset == 0x73 and ec_dblk != 0x00: print(f"  Display blanking       : {ec_dblk}s")
                else:                                   print(f"  Display blanking       : Disabled")
                if ec_drot != 0xFF:
                    if ec_drot == 0x00: rstr = "Landscape"
                    if ec_drot == 0x01: rstr = "Portrait"
                    if ec_drot == 0x02: rstr = "Landscape 180"
                    if ec_drot == 0x03: rstr = "Portrait 180"
                    print(f"  Display rotation       : {rstr}")
                else:
                    print(f"  Display rotation       : Default")
                if ec_pset == 0x73: print(f"  Neopixel Intensity     : {ec_pint}")
                print("")

                rnode.leave()
                graceful_exit()

            if args.eeprom_dump:
                RNS.log("EEPROM contents:")
                RNS.log(RNS.hexrep(rnode.eeprom))
                graceful_exit()

            if args.eeprom_backup:
                try:
                    timestamp = time.time()
                    filename = str(time.strftime("%Y-%m-%d_%H-%M-%S"))
                    path = ROM_DIR + filename + ".eeprom"
                    file = open(path, "wb")
                    file.write(rnode.eeprom)
                    file.close()
                    RNS.log("EEPROM backup written to: "+path)
                except Exception as e:
                    RNS.log("EEPROM was successfully downloaded from device,")
                    RNS.log("but file could not be written to disk.")
                graceful_exit()

            if isinstance(args.display, int):
                di = args.display
                if di < 0:
                    di = 0
                if di > 255:
                    di = 255
                RNS.log("Setting display intensity to "+str(di))
                rnode.set_display_intensity(di)

            if isinstance(args.timeout, int):
                di = args.timeout
                if di < 0:
                    di = 0
                if di > 255:
                    di = 255
                if di == 0:
                    RNS.log("Disabling display blanking")
                else:
                    RNS.log("Setting display timeout to "+str(di))
                rnode.set_display_blanking(di)

            if isinstance(args.rotation, int):
                dr = args.rotation
                if dr < 0:
                    dr = 0
                if dr > 3:
                    dr = 3

                RNS.log("Setting display rotation to "+str(dr))
                rnode.set_display_rotation(dr)

            if isinstance(args.recondition_display, bool):
                if args.recondition_display:
                    RNS.log("Starting display reconditioning")
                    rnode.recondition_display()

            if isinstance(args.ia_enable, bool):
                if args.ia_enable:
                    RNS.log("Enabling interference avoidance")
                    rnode.set_disable_interference_avoidance(False)

            if isinstance(args.ia_disable, bool):
                if args.ia_disable:
                    RNS.log("Disabling interference avoidance")
                    rnode.set_disable_interference_avoidance(True)

            if isinstance(args.np, int):
                di = args.np
                if di < 0:
                    di = 0
                if di > 255:
                    di = 255
                RNS.log("Setting NeoPixel intensity to "+str(di))
                rnode.set_neopixel_intensity(di)

            if isinstance(args.display_addr, str):
                set_addr = False
                try:
                    if args.display_addr.startswith("0x"):
                        args.display_addr = args.display_addr[2:]
                    da = bytes.fromhex(args.display_addr)
                    set_addr = True
                except Exception as e:
                    pass

                if set_addr and len(da) == 1:
                    RNS.log("Setting display address to "+RNS.hexrep(da, delimit=False))
                    rnode.set_display_address(ord(da))
                    rnode.hard_reset()
                    graceful_exit()
                else:
                    RNS.log("Invalid display address specified")

            if args.bluetooth_on:
                RNS.log("Enabling Bluetooth...")
                rnode.enable_bluetooth()
                rnode.leave()

            if args.bluetooth_off:
                RNS.log("Disabling Bluetooth...")
                rnode.disable_bluetooth()
                rnode.leave()

            if args.bluetooth_pair:
                RNS.log("Putting device into Bluetooth pairing mode. Press enter to exit when done.")
                rnode.bluetooth_pair()
                input()
                rnode.leave()

            if args.channel:
                try:
                    RNS.log(f"Setting WiFi channel to {args.channel}")
                    rnode.set_wifi_channel(args.channel)
                except Exception as e:
                    print(f"Could not set WiFi channel: {e}")
                    graceful_exit()

            if args.ssid:
                try:
                    if args.ssid.lower() == "none":
                        ssid_str = None
                        RNS.log(f"Deleting WiFi SSID")
                    else:
                        ssid_str = str(args.ssid)
                        RNS.log(f"Setting WiFi SSID to: {ssid_str}")
                    rnode.set_wifi_ssid(ssid_str)
                except Exception as e:
                    print(f"Could not set WiFi SSID: {e}")
                    graceful_exit()

            if args.psk:
                try:
                    if args.psk.lower() == "none":
                        psk_str = None
                        RNS.log(f"Deleting WiFi PSK")
                    else:
                        psk_str = str(args.psk)
                        RNS.log(f"Setting WiFi PSK")
                    rnode.set_wifi_psk(psk_str)
                except Exception as e:
                    print(f"Could not set WiFi PSK: {e}")
                    graceful_exit()

            if args.ip:
                try:
                    if args.ip.lower() == "none":
                        RNS.log(f"Setting WiFi IP to DHCP...")
                        rnode.set_wifi_ip(None)
                    else:
                        RNS.log(f"Setting WiFi static IP to: {args.ip}")
                        rnode.set_wifi_ip(args.ip)
                except Exception as e:
                    print(f"Could not set WiFi IP: {e}")
                    graceful_exit()

            if args.nm:
                try:
                    if args.nm.lower() == "none":
                        RNS.log(f"Deleting WiFi static netmask configuration...")
                        rnode.set_wifi_nm(None)
                    else:
                        RNS.log(f"Setting WiFi static netmask to: {args.nm}")
                        rnode.set_wifi_nm(args.nm)
                except Exception as e:
                    print(f"Could not set WiFi netmask: {e}")
                    graceful_exit()

            if args.wifi:
                try:
                    mode = 0x00
                    if   str(args.wifi).lower().startswith("sta"): mode = 0x01
                    elif str(args.wifi).lower().startswith("ap"):  mode = 0x02
                    if mode == 0x00: RNS.log(f"Disabling WiFi...")
                    elif mode == 0x01: RNS.log(f"Setting WiFi to station mode")
                    elif mode == 0x02: RNS.log(f"Setting WiFi to AP mode")
                    rnode.set_wifi_mode(mode)
                except Exception as e:
                    print(f"Could not set WiFi mode: {e}")
                    graceful_exit()

            if args.info:
                if rnode.provisioned:
                    timestamp = struct.unpack(">I", rnode.made)[0]
                    timestring = datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
                    sigstring = "Unverified"
                    if rnode.signature_valid:
                        if rnode.locally_signed:
                            sigstring = "Validated - Local signature"
                        else:
                            sigstring = "Genuine board, vendor is "+rnode.vendor

                    if rnode.board != None:
                        board_string = ":"+bytes([rnode.board]).hex()
                    else:
                        board_string = ""

                    RNS.log("")
                    RNS.log("Device info:")
                    RNS.log("\tProduct            : "+products[rnode.product]+" "+models[rnode.model][3]+" ("+bytes([rnode.product]).hex()+":"+bytes([rnode.model]).hex()+board_string+")")
                    RNS.log("\tDevice signature   : "+sigstring)
                    RNS.log("\tFirmware version   : "+rnode.version)
                    RNS.log("\tHardware revision  : "+str(int(rnode.hw_rev)))
                    RNS.log("\tSerial number      : "+RNS.hexrep(rnode.serialno))
                    RNS.log("\tModem chip         : "+str(models[rnode.model][5]))
                    RNS.log("\tFrequency range    : "+str(rnode.min_freq/1e6)+" MHz - "+str(rnode.max_freq/1e6)+" MHz")
                    RNS.log("\tMax TX power       : "+str(rnode.max_output)+" dBm")
                    RNS.log("\tManufactured       : "+timestring)

                    if rnode.configured:
                        rnode.bandwidth = rnode.conf_bandwidth
                        rnode.r_bandwidth = rnode.conf_bandwidth
                        rnode.sf = rnode.conf_sf
                        rnode.r_sf = rnode.conf_sf
                        rnode.cr = rnode.conf_cr
                        rnode.r_cr = rnode.conf_cr
                        rnode.updateBitrate()
                        txp_mw = round(pow(10, (rnode.conf_txpower/10)), 3)
                        RNS.log("");
                        RNS.log("\tDevice mode        : TNC")
                        RNS.log("\t  Frequency        : "+str((rnode.conf_frequency/1000000.0))+" MHz")
                        RNS.log("\t  Bandwidth        : "+str(rnode.conf_bandwidth/1000.0)+" KHz")
                        RNS.log("\t  TX power         : "+str(rnode.conf_txpower)+" dBm ("+str(txp_mw)+" mW)")
                        RNS.log("\t  Spreading factor : "+str(rnode.conf_sf))
                        RNS.log("\t  Coding rate      : "+str(rnode.conf_cr))
                        RNS.log("\t  On-air bitrate   : "+str(rnode.bitrate_kbps)+" kbps")
                    else:
                        RNS.log("\tDevice mode        : Normal (host-controlled)")

                    print("")
                    rnode.disconnect()
                    graceful_exit()

                else:
                    RNS.log("EEPROM is invalid, no further information available")
                    graceful_exit()

            if args.rom:
                if rnode.provisioned and not args.autoinstall:
                    RNS.log("EEPROM bootstrap was requested, but a valid EEPROM was already present.")
                    RNS.log("No changes are being made.")
                    graceful_exit()

                else:
                    if rnode.signature_valid:
                        RNS.log("EEPROM bootstrap was requested, but a valid EEPROM was already present.")
                        RNS.log("No changes are being made.")
                        graceful_exit()
                    else:
                        if args.autoinstall:
                            RNS.log("Clearing old EEPROM, this will take about 15 seconds...")
                            rnode.wipe_eeprom()
                            
                        if rnode.platform == ROM.PLATFORM_ESP32:
                            RNS.log("Waiting for ESP32 reset...")
                            time.sleep(6)
                        elif rnode.platform == ROM.PLATFORM_NRF52:
                            rnode_serial.close()
                            RNS.log("Waiting for NRF52 reset...")
                            time.sleep(18)
                            selected_port = None
                            ports = list_ports.comports()
                            for port in ports:
                                if port.serial_number == rnode.usb_serial_id:
                                    selected_port = port
                                    break
                            if selected_port is None:
                                RNS.log("Could not detect new port for NRF52...")
                            else:
                                try:
                                    rnode_serial = rnode_open_serial(selected_port.device)
                                    rnode.serial = rnode_serial
                                    thread = threading.Thread(target=rnode.readLoop, daemon=True).start()
                                except Exception as e:
                                    RNS.log("Could not open the specified serial port. The contained exception was:")
                                    RNS.log(str(e))
                                    exit()
                        else:
                            time.sleep(3)

                    counter = None
                    counter_path = FWD_DIR+"/serial.counter"
                    try:
                        if os.path.isfile(counter_path):
                            file = open(counter_path, "r")
                            counter_str = file.read()
                            counter = int(counter_str)
                            file.close()
                        else:
                            counter = 0
                    except Exception as e:
                        RNS.log("Could not create device serial number, exiting")
                        RNS.log(str(e))
                        graceful_exit()

                    serialno = counter+1
                    model = None
                    hwrev = None
                    if args.product != None:
                        if args.product == "03":
                            mapped_product = ROM.PRODUCT_RNODE
                        elif args.product == "10":
                            mapped_product = ROM.PRODUCT_RAK4631
                        elif args.product == "f0":
                            mapped_product = ROM.PRODUCT_HMBRW
                        elif args.product == "e0":
                            mapped_product = ROM.PRODUCT_TBEAM
                        else:
                            if len(args.product) == 2:
                                mapped_product = ord(bytes.fromhex(args.product))

                    if mapped_model != None:
                        if mapped_model == ROM.MODEL_B4_TCXO:
                            model = ROM.MODEL_B4
                        elif mapped_model == ROM.MODEL_B9_TCXO:
                            model = ROM.MODEL_B9
                        else:
                            model = mapped_model
                    else:
                        if args.model == "11":
                            model = ROM.MODEL_11
                        elif args.model == "12":
                            model = ROM.MODEL_12
                        elif args.model == "a4":
                            model = ROM.MODEL_A4
                        elif args.model == "a9":
                            model = ROM.MODEL_A9
                        elif args.model == "a1":
                            model = ROM.MODEL_A1
                        elif args.model == "a6":
                            model = ROM.MODEL_A6
                        elif args.model == "e4":
                            model = ROM.MODEL_E4
                        elif args.model == "e9":
                            model = ROM.MODEL_E9
                        elif args.model == "ff":
                            model = ROM.MODEL_FF
                        else:
                            if len(args.model) == 2:
                                model = ord(bytes.fromhex(args.model))

                        # Initialize selected_model from specified model
                        selected_model = model

                    if args.hwrev != None and (args.hwrev > 0 and args.hwrev < 256):
                        hwrev = chr(args.hwrev)

                    if serialno > 0 and model != None and hwrev != None:
                        try:
                            from cryptography.hazmat.primitives import hashes
                            from cryptography.hazmat.backends import default_backend

                            timestamp = int(time.time())
                            time_bytes = struct.pack(">I", timestamp)
                            serial_bytes = struct.pack(">I", serialno)
                            file = open(counter_path, "w")
                            file.write(str(serialno))
                            file.close()

                            info_chunk  = b"" + bytes([mapped_product, model, ord(hwrev)])
                            info_chunk += serial_bytes
                            info_chunk += time_bytes
                            digest = hashes.Hash(hashes.MD5(), backend=default_backend())
                            digest.update(info_chunk)
                            checksum = digest.finalize()

                            RNS.log("Loading signing key...")
                            signature = None
                            key_path = FWD_DIR+"/signing.key"
                            if os.path.isfile(key_path):
                                try:
                                    file = open(key_path, "rb")
                                    private_bytes = file.read()
                                    file.close()
                                    private_key = serialization.load_der_private_key(
                                        private_bytes,
                                        password=None,
                                        backend=default_backend()
                                    )
                                    public_key = private_key.public_key()
                                    public_bytes = public_key.public_bytes(
                                        encoding=serialization.Encoding.DER,
                                        format=serialization.PublicFormat.SubjectPublicKeyInfo
                                    )
                                    signature = private_key.sign(
                                        checksum,
                                        padding.PSS(
                                            mgf=padding.MGF1(hashes.SHA256()),
                                            salt_length=padding.PSS.MAX_LENGTH
                                        ),
                                        hashes.SHA256()
                                    )
                                except Exception as e:
                                    RNS.log("Error while signing EEPROM")
                                    RNS.log(str(e))
                            else:
                                RNS.log("No signing key found")
                                graceful_exit()

                            if selected_model in ROM.MANUAL_FLASH_MODELS:
                                rnode.serial.close()
                                RNS.log("")
                                RNS.log("Please reset the board by momentarily pressing the RESET button.")
                                RNS.log("Hit enter when this is done.")
                                input()
                                sleep(2.5)
                                rnode_serial = rnode_open_serial(rnode_port)
                                rnode = RNode(rnode_serial)

                            RNS.log("Bootstrapping device EEPROM...")

                            rnode.write_eeprom(ROM.ADDR_PRODUCT, mapped_product)
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_MODEL, model)
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_HW_REV, ord(hwrev))
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_SERIAL, serial_bytes[0])
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_SERIAL+1, serial_bytes[1])
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_SERIAL+2, serial_bytes[2])
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_SERIAL+3, serial_bytes[3])
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_MADE, time_bytes[0])
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_MADE+1, time_bytes[1])
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_MADE+2, time_bytes[2])
                            time.sleep(0.006)
                            rnode.write_eeprom(ROM.ADDR_MADE+3, time_bytes[3])
                            time.sleep(0.006)

                            for i in range(0,16):
                                rnode.write_eeprom(ROM.ADDR_CHKSUM+i, checksum[i])
                                time.sleep(0.006)

                            for i in range(0,128):
                                rnode.write_eeprom(ROM.ADDR_SIGNATURE+i, signature[i])
                                time.sleep(0.006)

                            rnode.write_eeprom(ROM.ADDR_INFO_LOCK, ROM.INFO_LOCK_BYTE)
                            if rnode.platform == ROM.PLATFORM_NRF52:
                                # Allow extra time for writing to EEPROM on NRF52. Current implementation is slow.
                                sleep(3)

                            RNS.log("EEPROM written! Validating...")

                            if wants_fw_provision:
                                partition_hash = None

                                if fw_filename == "extracted_rnode_firmware.zip":
                                    update_full_path = EXT_DIR+"/extracted_rnode_firmware.version"
                                    vf = open(update_full_path, "rb")
                                    release_info = vf.read().decode("utf-8").strip()
                                    partition_hash = bytes.fromhex(release_info.split()[1])
                                    vf.close()
                                else:
                                    partition_filename = fw_filename.replace(".zip", ".bin")
                                    partition_hash = get_partition_hash(rnode.platform, UPD_DIR+"/"+selected_version+"/"+partition_filename)

                                if partition_hash != None:
                                    time.sleep(0.75)
                                    RNS.log("Setting firmware checksum...")
                                    rnode.set_firmware_hash(partition_hash)

                            if rnode.platform == ROM.PLATFORM_ESP32:
                                rnode.hard_reset()
                                RNS.log("Waiting for ESP32 reset...")
                                time.sleep(7)
                                if selected_model in [ROM.MODEL_AC, ROM.MODEL_A6, ROM.MODEL_A1, ROM.MODEL_AA, ROM.MODEL_A5]:
                                    time.sleep(5)

                            elif rnode.platform == ROM.PLATFORM_NRF52:
                                # Wait a few seconds before hard resetting.
                                # Otherwise, macOS fails to set firmware hash on NRF52
                                if RNS.vendor.platformutils.is_darwin():
                                    time.sleep(5)

                                rnode.hard_reset()
                                # The hard reset on this platform is different
                                # to that of the ESP32 platform, it causes
                                # disruption to the serial connection.
                                # Therefore, we have to reestablish the serial
                                # connection after the reset.
                                rnode_serial.close()
                                RNS.log("Waiting for NRF52 reset...")

                                # Give plenty of time for to allow for
                                # potential e-ink display refresh too.
                                time.sleep(20)

                                # After the hard reset, the port number will
                                # change. We need to find the new port number,
                                # which can be done non-interactively by
                                # comparing the USB serial numbers of the
                                # original port and the one we are currently
                                # iterating.
                                selected_port = None
                                ports = list_ports.comports()
                                for port in ports:
                                    if port.serial_number == rnode.usb_serial_id:
                                        selected_port = port
                                        break
                                if selected_port is None:
                                    RNS.log("Could not detect new port for NRF52...")
                                else:
                                    try:
                                        rnode_serial = rnode_open_serial(selected_port.device)
                                        rnode.serial = rnode_serial
                                        thread = threading.Thread(target=rnode.readLoop, daemon=True).start()
                                    except Exception as e:
                                        RNS.log("Could not open the specified serial port. The contained exception was:")
                                        RNS.log(str(e))
                                        exit()
                            else:
                                rnode.hard_reset()

                            if selected_model in ROM.MANUAL_FLASH_MODELS:
                                rnode.serial.close()
                                RNS.log("")
                                RNS.log("Please reset the board by momentarily pressing the RESET button.")
                                RNS.log("Hit enter when this is done.")
                                input()
                                rnode.provisioned = True
                            else:
                                rnode.download_eeprom()

                            if rnode.provisioned:
                                RNS.log("EEPROM Bootstrapping successful!")
                                if not selected_model in ROM.MANUAL_FLASH_MODELS:
                                    rnode.hard_reset()

                                if args.autoinstall:
                                    print("")
                                    print("RNode Firmware autoinstallation complete!")
                                    print("")
                                    print("To use your device with Reticulum, read the documetation at:")
                                    print("")
                                    print("https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html")
                                    print("")
                                    print("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *")
                                    print("                              Important!            ")
                                    print("")
                                    print("ESP32-based RNodes are created with the RNode Bootstrap Console on-board.")
                                    print("")
                                    print("This repository is hosted directly on the RNode, and contains a wealth of")
                                    print("information, software and tools.")
                                    print("")
                                    print("The RNode Bootstrap Console also contains everything needed to build")
                                    print("and replicate RNodes, including detailed build recipes, 3D-printable")
                                    print("cases, and copies of the source code for both the RNode Firmware,")
                                    print("Reticulum and other utilities.")
                                    print("")
                                    print("To activate the RNode Bootstrap Console, power up your RNode and hold")
                                    print("down the user button for 10+ seconds, then release. The RNode will now")
                                    print("reboot into console mode, and activate a WiFi access point for you to")
                                    print("connect to. The console is then reachable at: http://10.0.0.1")
                                    print("")
                                    print("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *")
                                    print("")
                                    print("Thank you for using this utility! Please help the project by")
                                    print("contributing code and reporting bugs, or by donating!")
                                    print("")
                                    print("Your contributions and donations directly further the realisation")
                                    print("of truly open, free and resilient communications systems.")
                                    print("")
                                    print_donation_block()
                                    print("")
                                try:
                                    os.makedirs(FWD_DIR+"/device_db/", exist_ok=True)
                                    file = open(FWD_DIR+"/device_db/"+serial_bytes.hex(), "wb")
                                    written = file.write(rnode.eeprom)
                                    file.close()
                                except Exception as e:
                                    RNS.log("WARNING: Could not backup device EEPROM to disk")
                                graceful_exit()
                            else:
                                RNS.log("EEPROM was written, but validation failed. Check your settings.")
                                graceful_exit()
                        except Exception as e:
                            RNS.log("An error occurred while writing EEPROM. The contained exception was:")
                            RNS.log(str(e))
                            raise e

                    else:
                        RNS.log("Invalid data specified, cancelling EEPROM write")
                        graceful_exit()

            if args.sign:
                if rnode.provisioned:
                    try:
                        device_signer = RNS.Identity.from_file(FWD_DIR+"/device.key")
                    except Exception as e:
                        RNS.log("Could not load device signing key")

                    if rnode.device_hash == None:
                        RNS.log("No device hash present, skipping device signing")
                    else:
                        if device_signer == None:
                            RNS.log("No device signer loaded, cannot sign device")
                            graceful_exit(78)
                        else:
                            new_device_signature = device_signer.sign(rnode.device_hash)
                            rnode.store_signature(new_device_signature)
                            RNS.log("Device signed")
                else:
                    RNS.log("This device has not been provisioned yet, cannot create device signature")
                    graceful_exit(79)

            if args.firmware_hash != None:
                if rnode.provisioned:
                    try:
                        hash_data = bytes.fromhex(args.firmware_hash)
                        if len(hash_data) != 32:
                            raise ValueError("Incorrect hash length")

                        rnode.set_firmware_hash(hash_data)
                        RNS.log("Firmware hash set")
                    except Exception as e:
                        RNS.log("The provided value was not a valid SHA256 hash")
                        graceful_exit(78)

                else:
                    RNS.log("This device has not been provisioned yet, cannot set firmware hash")
                    graceful_exit(77)

            if args.get_target_firmware_hash:
                if rnode.provisioned:
                    RNS.log(f"The target firmware hash is: {rnode.firmware_hash_target.hex()}")

                else:
                    RNS.log("This device has not been provisioned yet, cannot get firmware hash")
                    exit(77)
                
            if args.get_firmware_hash:
                if rnode.provisioned:
                    RNS.log(f"The actual firmware hash is: {rnode.firmware_hash.hex()}")

                else:
                    RNS.log("This device has not been provisioned yet, cannot get firmware hash")
                    exit(77)

            if rnode.provisioned:
                if args.normal:
                    rnode.setNormalMode()
                    RNS.log("Device set to normal (host-controlled) operating mode")
                    graceful_exit()
                if args.tnc:
                    if not (args.freq and args.bw and args.txp and args.sf and args.cr):
                        RNS.log("Please input startup configuration:")

                    print("")
                    if args.freq:
                        rnode.frequency = args.freq
                    else:
                        print("Frequency in Hz:\t", end="")
                        rnode.frequency = int(input())


                    if args.bw:
                        rnode.bandwidth = args.bw
                    else:
                        print("Bandwidth in Hz:\t", end="")
                        rnode.bandwidth = int(input())

                    if args.txp != None and (args.txp >= 0 and args.txp <= 17):
                        rnode.txpower = args.txp
                    else:
                        print("TX Power in dBm:\t", end="")
                        rnode.txpower = int(input())

                    if args.sf:
                        rnode.sf = args.sf
                    else:
                        print("Spreading factor:\t", end="")
                        rnode.sf = int(input())

                    if args.cr:
                        rnode.cr = args.cr
                    else:
                        print("Coding rate:\t\t", end="")
                        rnode.cr = int(input())

                    print("")

                    rnode.initRadio()
                    sleep(0.5)
                    rnode.setTNCMode()
                    RNS.log("Device set to TNC operating mode")
                    sleep(1.0)

                    graceful_exit()
            else:
                RNS.log("This device contains a valid firmware, but EEPROM is invalid.")
                RNS.log("Probably the device has not been initialised, or the EEPROM has been erased.")
                RNS.log("Please correctly initialise the device and try again!")

        else:
            print("")
            parser.print_help()
            print("")
            graceful_exit()


    except KeyboardInterrupt:
        print("")
        graceful_exit()
        
    graceful_exit()

def extract_recovery_esptool():
    if not os.path.isfile(RT_PATH):
        try:
            RNS.log("Extracting recovery ESP-Tool...")
            import base64, gzip
            recovery_esptool = " "
            out = open(RT_PATH, "wb")
            out.write("#!/usr/bin/env python3\n\n".encode("utf-8")+gzip.decompress(base64.b64decode(recovery_esptool)))
            out.close()
        except Exception as e:
            RNS.log("Error: Could not extract recovery ESP-Tool. The contained exception was:")
            RNS.log(str(e))
            graceful_exit(181)

if __name__ == "__main__":
    main()
