From 9bc5d9110619f266c23c980137f4e359b13b8f2a Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 1 Nov 2022 22:40:09 +0100 Subject: [PATCH] Added rnodeconf to package --- RNS/Utilities/rnodeconf.py | 2375 ++++++++++++++++++++++++++++++++++++ docs/Reticulum Manual.pdf | Bin 2368539 -> 2368546 bytes setup.py | 1 + 3 files changed, 2376 insertions(+) create mode 100644 RNS/Utilities/rnodeconf.py diff --git a/RNS/Utilities/rnodeconf.py b/RNS/Utilities/rnodeconf.py new file mode 100644 index 0000000..6ecf3a9 --- /dev/null +++ b/RNS/Utilities/rnodeconf.py @@ -0,0 +1,2375 @@ +#!python3 + +# MIT License +# +# Copyright (c) 2018-2022 Mark Qvist - unsigned.io/rnode +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from time import sleep +import argparse +import threading +import os +import os.path +import struct +import datetime +import time +import math +import hashlib +from urllib.request import urlretrieve +from importlib import util +import RNS + +RNS.logtimefmt = "%H:%M:%S" +RNS.compact_log_fmt = True + +program_version = "2.0.0" +eth_addr = "0x81F7B979fEa6134bA9FD5c701b3501A2e61E897a" +btc_addr = "3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq" +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 +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_BT_CTRL = 0x46 + CMD_BOARD = 0x47 + CMD_PLATFORM = 0x48 + CMD_MCU = 0x49 + CMD_FW_VERSION = 0x50 + 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_FW_HASH = 0x58 + + 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 + + MCU_1284P = 0x91 + MCU_2560 = 0x92 + MCU_ESP32 = 0x81 + + PRODUCT_RNODE = 0x03 + MODEL_A4 = 0xA4 + MODEL_A9 = 0xA9 + MODEL_A3 = 0xA3 + MODEL_A8 = 0xA8 + MODEL_A2 = 0xA2 + MODEL_A7 = 0xA7 + + PRODUCT_T32_20 = 0xB0 + MODEL_B3 = 0xB3 + MODEL_B8 = 0xB8 + + PRODUCT_T32_21 = 0xB1 + MODEL_B4 = 0xB4 + MODEL_B9 = 0xB9 + + PRODUCT_H32_V2 = 0xC0 + MODEL_C4 = 0xC4 + MODEL_C9 = 0xC9 + + PRODUCT_TBEAM = 0xE0 + MODEL_E4 = 0xE4 + MODEL_E9 = 0xE9 + + 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 + + INFO_LOCK_BYTE = 0x73 + CONF_OK_BYTE = 0x73 + + BOARD_RNODE = 0x31 + BOARD_HMBRW = 0x32 + BOARD_TBEAM = 0x33 + BOARD_HUZZAH32 = 0x34 + BOARD_GENERIC_ESP32 = 0x35 + BOARD_LORA32_V2_0 = 0x36 + BOARD_LORA32_V2_1 = 0x37 + +mapped_product = ROM.PRODUCT_RNODE +products = { + ROM.PRODUCT_RNODE: "RNode", + ROM.PRODUCT_HMBRW: "Hombrew RNode", + ROM.PRODUCT_TBEAM: "LilyGO T-Beam", + ROM.PRODUCT_T32_20: "LilyGO LoRa32 v2.0", + ROM.PRODUCT_T32_21: "LilyGO LoRa32 v2.1", + ROM.PRODUCT_H32_V2: "Heltec LoRa32 v2", +} + +platforms = { + ROM.PLATFORM_AVR: "AVR", + ROM.PLATFORM_ESP32:"ESP32", +} + +mcus = { + ROM.MCU_1284P: "ATmega1284P", + ROM.MCU_2560:"ATmega2560", + ROM.MCU_ESP32:"Espressif Systems ESP32", +} + +models = { + 0xA4: [410000000, 525000000, 14, "410 - 525 MHz", "rnode_firmware.hex"], + 0xA9: [820000000, 1020000000, 17, "820 - 1020 MHz", "rnode_firmware.hex"], + 0xA2: [410000000, 525000000, 17, "410 - 525 MHz", "rnode_firmware_ng21.zip"], + 0xA7: [820000000, 1020000000, 17, "820 - 1020 MHz", "rnode_firmware_ng21.zip"], + 0xA3: [410000000, 525000000, 17, "410 - 525 MHz", "rnode_firmware_ng20.zip"], + 0xA8: [820000000, 1020000000, 17, "820 - 1020 MHz", "rnode_firmware_ng20.zip"], + 0xB3: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_lora32v20.zip"], + 0xB8: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_lora32v20.zip"], + 0xB4: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_lora32v21.zip"], + 0xB9: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_lora32v21.zip"], + 0xC4: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_heltec32v2.zip"], + 0xC9: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_heltec32v2.zip"], + 0xE4: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_tbeam.zip"], + 0xE9: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_tbeam.zip"], + 0xFE: [100000000, 1100000000, 17, "(Band capabilities unknown)", None], + 0xFF: [100000000, 1100000000, 14, "(Band capabilities unknown)", None], +} + +CNF_DIR = None +UPD_DIR = None +FWD_DIR = None + +try: + CNF_DIR = os.path.expanduser("~/.local/rnodeconf") + UPD_DIR = CNF_DIR+"/update" + FWD_DIR = CNF_DIR+"/firmware" + + 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) + +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)) + 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.platform = None + self.mcu = None + self.eeprom = 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.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 (byte == KISS.FEND): + in_frame = True + command = KISS.CMD_UNKNOWN + data_buffer = b"" + command_buffer = b"" + elif (in_frame and len(data_buffer) < 512): + 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_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_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_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-RNodeInterface.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 + 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]) + 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") + + 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 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); + + 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 configuring radio state") + + sleep(0.6) + if self.eeprom == None: + RNS.log("Could not download EEPROM from device. Is a valid firmware installed?") + exit() + else: + self.parse_eeprom() + + 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"" + + + self.min_freq = models[self.model][0] + self.max_freq = models[self.model][1] + self.max_output = models[self.model][2] + + 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("Exception") + RNS.log(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") + 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)) + + 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(" 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://unsigned.io/rnode") + print(" https://github.com/markqvist/rnode_firmware") + 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.") + + + 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://unsigned.io/firmware/latest/?variant=" +def ensure_firmware_file(fw_filename): + global selected_version, selected_hash, upd_nocheck + try: + if selected_version == None: + if not upd_nocheck: + try: + urlretrieve(firmware_version_url+fw_filename, UPD_DIR+"/"+fw_filename+".version.latest") + except Exception as e: + RNS.log("Failed to retrive latest 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.") + exit() + + 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] + 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 latest firmware for this board is version "+selected_version) + + else: + 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.") + exit(98) + + 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("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("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.") + exit(97) + + RNS.log("Veryfying 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("Firmware corrupt.") + exit(96) + + except Exception as e: + RNS.log("An error occurred while checking firmware file integrity. The contained exception was:") + RNS.log(str(e)) + 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)) + 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)) + 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 + ) + +device_signer = None +force_update = False +upd_nocheck = False +def main(): + global mapped_product, mapped_model, fw_filename, 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("") + 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("") + 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("--nocheck", action="store_true", help="Don't check for firmware updates online") + parser.add_argument("-C", "--clear-cache", action="store_true", help="Clear locally cached firmware files") + + 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("--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("--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("--version", action="store_true", help="Print program version and exit") + + parser.add_argument("-f", "--flash", action="store_true", help=argparse.SUPPRESS) # Flash firmware and bootstrap EEPROM + parser.add_argument("-r", "--rom", action="store_true", help=argparse.SUPPRESS) # Bootstrap EEPROM without flashing firmware + parser.add_argument("-k", "--key", action="store_true", help=argparse.SUPPRESS) # Generate a new signing key and exit + parser.add_argument("-P", "--public", action="store_true", help=argparse.SUPPRESS) # Display public part of signing key + parser.add_argument("-S", "--sign", action="store_true", help=argparse.SUPPRESS) # Display public part of signing key + parser.add_argument("-H", "--firmware-hash", action="store", help=argparse.SUPPRESS) # Display public part of signing key + parser.add_argument("--platform", action="store", metavar="platform", type=str, default=None, help=argparse.SUPPRESS) # Platform specification for device bootstrap + parser.add_argument("--product", action="store", metavar="product", type=str, default=None, help=argparse.SUPPRESS) # Product specification for device bootstrap + parser.add_argument("--model", action="store", metavar="model", type=str, default=None, help=argparse.SUPPRESS) # Model code for device bootstrap + parser.add_argument("--hwrev", action="store", metavar="revision", type=int, default=None, help=argparse.SUPPRESS) # 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("") + print(" Info : https://unsigned.io/") + print(" Code : https://github.com/markqvist") + + if args.version: + print("rnodeconf "+program_version) + exit(0) + + if args.clear_cache: + RNS.log("Clearing local firmware cache...") + import shutil + shutil.rmtree(UPD_DIR) + RNS.log("Done") + exit(0) + + if args.fw_version != None: + selected_version = args.fw_version + + 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: + 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 + + if args.autoinstall: + 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("\nWhat serial port is your device connected to? ", 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.") + exit() + + if selected_port == None: + print("Could not select port, exiting now.") + 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") + 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)) + exit() + + rnode = RNode(rnode_serial) + 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.") + exit() + + 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("What kind of device is this?\n") + print("[1] RNode from Unsigned.io") + print("[2] Homebrew RNode") + print("[3] LilyGO T-Beam") + print("[4] LilyGO LoRa32 v2.0") + print("[5] LilyGO LoRa32 v2.1") + print("[6] Heltec LoRa32 v2") + print("\n? ", end="") + + selected_product = None + try: + c_dev = int(input()) + if c_dev < 1 or c_dev > 6: + raise ValueError() + elif c_dev == 1: + selected_product = ROM.PRODUCT_RNODE + elif c_dev == 2: + selected_product = ROM.PRODUCT_HMBRW + print("") + 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 courtesey to those") + print("who would like to experiment with it. Hit enter to continue.") + print("---------------------------------------------------------------------------") + input() + elif c_dev == 3: + selected_product = ROM.PRODUCT_TBEAM + print("") + 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 courtesey 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 + print("") + 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 courtesey 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_21 + print("") + 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 courtesey to those") + print("who would like to experiment with it. Hit enter to continue.") + print("---------------------------------------------------------------------------") + input() + elif c_dev == 6: + selected_product = ROM.PRODUCT_H32_V2 + print("") + 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("The currently supplied firmware is provided AS-IS as a courtesey 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.") + 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.") + 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.") + exit() + + + elif selected_product == ROM.PRODUCT_RNODE: + selected_mcu = ROM.MCU_1284P + print("\nWhat model is this RNode?\n") + print("[1] Original v1.x RNode, 410 - 525 MHz") + print("[2] Original v1.x RNode, 820 - 1020 MHz") + print("[3] Prototype v2 RNode, 410 - 525 MHz") + print("[4] Prototype v2 RNode, 820 - 1020 MHz") + print("[5] RNode v2.x, 410 - 525 MHz") + print("[6] RNode v2.x, 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_A4 + selected_platform = ROM.PLATFORM_AVR + elif c_model == 2: + selected_model = ROM.MODEL_A9 + selected_platform = ROM.PLATFORM_AVR + elif c_model == 3: + selected_model = ROM.MODEL_A3 + selected_mcu = ROM.MCU_ESP32 + selected_platform = ROM.PLATFORM_ESP32 + elif c_model == 4: + selected_model = ROM.MODEL_A8 + selected_mcu = ROM.MCU_ESP32 + selected_platform = ROM.PLATFORM_ESP32 + elif c_model == 5: + selected_model = ROM.MODEL_A2 + selected_mcu = ROM.MCU_ESP32 + selected_platform = ROM.PLATFORM_ESP32 + elif c_model == 6: + selected_model = ROM.MODEL_A7 + selected_mcu = ROM.MCU_ESP32 + selected_platform = ROM.PLATFORM_ESP32 + except Exception as e: + print("That model does not exist, exiting now.") + 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") + 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_E4 + selected_platform = ROM.PLATFORM_ESP32 + elif c_model > 1: + selected_model = ROM.MODEL_E9 + selected_platform = ROM.PLATFORM_ESP32 + except Exception as e: + print("That band does not exist, exiting now.") + 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.") + 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 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_B4 + selected_platform = ROM.PLATFORM_ESP32 + elif c_model > 1: + selected_model = ROM.MODEL_B9 + selected_platform = ROM.PLATFORM_ESP32 + except Exception as e: + print("That band does not exist, exiting now.") + exit() + + elif selected_product == ROM.PRODUCT_H32_V2: + selected_mcu = ROM.MCU_ESP32 + print("\nWhat band is this Heltec 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_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.") + 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.") + 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("") + exit() + + print("\nOk, 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 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("\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.") + 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)) + 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") + + + 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.") + 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.") + 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: + exit() + + def get_partition_hash(partition_file): + try: + firmware_data = open(partition_file, "rb").read() + 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 + 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.") + 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.") + exit() + elif platform == ROM.PLATFORM_ESP32: + flasher = UPD_DIR+"/"+selected_version+"/esptool.py" + if which(flasher) is not None: + if fw_filename == "rnode_firmware_tbeam.zip": + return [ + flasher, + "--chip", "esp32", + "--port", args.port, + "--baud", "921600", + "--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_lora32v20.zip": + return [ + flasher, + "--chip", "esp32", + "--port", args.port, + "--baud", "921600", + "--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": + return [ + flasher, + "--chip", "esp32", + "--port", args.port, + "--baud", "921600", + "--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_heltec32v2.zip": + return [ + flasher, + "--chip", "esp32", + "--port", args.port, + "--baud", "921600", + "--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_featheresp32.zip": + return [ + flasher, + "--chip", "esp32", + "--port", args.port, + "--baud", "921600", + "--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": + return [ + flasher, + "--chip", "esp32", + "--port", args.port, + "--baud", "921600", + "--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": + return [ + flasher, + "--chip", "esp32", + "--port", args.port, + "--baud", "921600", + "--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": + return [ + flasher, + "--chip", "esp32", + "--port", args.port, + "--baud", "921600", + "--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", + ] + 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.") + 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") + exit(68) + + fw_src = UPD_DIR+"/"+selected_version+"/" + if os.path.isfile(fw_src+fw_filename): + try: + if fw_filename.endswith(".zip"): + RNS.log("Extracting firmware...") + unzip_status = call(get_flasher_call("unzip", fw_filename)) + if unzip_status == 0: + RNS.log("Firmware extracted") + else: + RNS.log("Could not extract firmware from downloaded zip file") + exit() + + 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: + exit() + + except Exception as e: + RNS.log("Error while flashing") + RNS.log(str(e)) + exit(1) + else: + RNS.log("Firmware file not found") + exit() + + 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)) + 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) + 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() + 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 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" + else: + fw_filename = None + if args.update: + RNS.log("ERROR: No firmware found for this board. Cannot update.") + exit() + + if args.update: + if not rnode.provisioned: + RNS.log("Device not provisioned. Cannot update device firmware.") + exit(1) + + from subprocess import call + + try: + RNS.log("Checking firmware file availability...") + if selected_version == None: + ensure_firmware_file(fw_filename) + + 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") + 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") + 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") + 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") + exit(0) + + if selected_version != None: + ensure_firmware_file(fw_filename) + + if fw_filename.endswith(".zip"): + RNS.log("Extracting firmware...") + unzip_status = call(get_flasher_call("unzip", fw_filename)) + if unzip_status == 0: + RNS.log("Firmware extracted") + else: + RNS.log("Could not extract firmware from downloaded zip file") + exit() + + except Exception as e: + RNS.log("Could not obtain firmware package for your board") + RNS.log("The contained exception was: "+str(e)) + exit() + + if os.path.isfile(UPD_DIR+"/"+selected_version+"/"+fw_filename): + try: + args.info = False + RNS.log("Updating RNode firmware for device on "+args.port) + partition_filename = fw_filename.replace(".zip", ".bin") + partition_hash = get_partition_hash(UPD_DIR+"/"+selected_version+"/"+partition_filename) + if partition_hash != None: + rnode.set_firmware_hash(partition_hash) + + rnode.disconnect() + flash_status = call(get_flasher_call(rnode.platform, fw_filename)) + if flash_status == 0: + RNS.log("Flashing new firmware completed") + 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)) + 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) + 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.") + exit() + + except Exception as e: + RNS.log("Error while updating firmware") + RNS.log(str(e)) + else: + RNS.log("Firmware update file not found") + exit() + + if args.eeprom_dump: + RNS.log("EEPROM contents:") + RNS.log(RNS.hexrep(rnode.eeprom)) + exit() + + if args.eeprom_backup: + try: + timestamp = time.time() + filename = str(time.strftime("%Y-%m-%d_%H-%M-%S")) + path = "./eeprom/"+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.") + exit() + + if args.bluetooth_on: + RNS.log("Enabling Bluetooth...") + rnode.enable_bluetooth() + + if args.bluetooth_off: + RNS.log("Disabling Bluetooth...") + rnode.disable_bluetooth() + + if args.bluetooth_pair: + RNS.log("Putting device into Bluetooth pairing mode...") + rnode.bluetooth_pair() + + 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("\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() + exit() + + else: + RNS.log("EEPROM is invalid, no further information available") + 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.") + 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.") + 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) + 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)) + exit() + + serialno = counter+1 + model = None + hwrev = None + if args.product != None: + if args.product == "03": + mapped_product = ROM.PRODUCT_RNODE + if args.product == "f0": + mapped_product = ROM.PRODUCT_HMBRW + if args.product == "e0": + mapped_product = ROM.PRODUCT_TBEAM + + if mapped_model != None: + model = mapped_model + else: + if args.model == "a4": + model = ROM.MODEL_A4 + elif args.model == "a9": + model = ROM.MODEL_A9 + elif args.model == "e4": + model = ROM.MODEL_E4 + elif args.model == "e9": + model = ROM.MODEL_E9 + elif args.model == "ff": + model = ROM.MODEL_FF + + + 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") + exit() + + + RNS.log("Bootstrapping device EEPROM...") + rnode.hard_reset() + + 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) + + RNS.log("EEPROM written! Validating...") + + if wants_fw_provision: + RNS.log("Getting partition data...") + partition_filename = fw_filename.replace(".zip", ".bin") + partition_hash = get_partition_hash(UPD_DIR+"/"+selected_version+"/"+partition_filename) + if partition_hash != None: + RNS.log("Setting firmware partition hash target") + rnode.set_firmware_hash(partition_hash) + + if rnode.platform == ROM.PLATFORM_ESP32: + RNS.log("Waiting for ESP32 reset...") + time.sleep(5) + + rnode.download_eeprom() + if rnode.provisioned: + RNS.log("EEPROM Bootstrapping successful!") + 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("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") + exit() + else: + RNS.log("EEPROM was written, but validation failed. Check your settings.") + 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") + 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") + 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") + 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") + exit(78) + + else: + RNS.log("This device has not been provisioned yet, cannot set firmware hash") + exit(77) + + if rnode.provisioned: + if args.normal: + rnode.setNormalMode() + RNS.log("Device set to normal (host-controlled) operating mode") + 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) + + 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("") + exit() + + + except KeyboardInterrupt: + print("") + exit() + +if __name__ == "__main__": + main() diff --git a/docs/Reticulum Manual.pdf b/docs/Reticulum Manual.pdf index dfd8c30ce8b2d80a594f8b8825fec0a0e08747df..0ae48180da5cc664ed99c1876ba2dd62a6386036 100644 GIT binary patch delta 4650 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