commit a7469373902ec4fd387ab20de9deedf8d73ce398 Author: James L Date: Sun Feb 22 18:25:20 2026 -0500 Initial commit: RNodeTHV4 boundary mode firmware for Heltec V4 Bridges LoRa mesh and TCP/WiFi backbone networks using microReticulum. Based on microReticulum_Firmware with boundary mode additions: - BoundaryMode.h: State management and EEPROM persistence - BoundaryConfig.h: WiFi captive portal for configuration - TcpInterface.h: TCP backbone interface with HDLC framing - Display.h: Custom OLED layout with network status indicators - Transport/Identity library patches for embedded memory constraints diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..afd39f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +*.hex +*.pyc +TODO +Release/*.hex +Release/*.zip +Release/*.json +Console/build +build/* +.pio/* +.vscode/* diff --git a/BLESerial.cpp b/BLESerial.cpp new file mode 100755 index 0000000..2957755 --- /dev/null +++ b/BLESerial.cpp @@ -0,0 +1,171 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "Boards.h" + +#if PLATFORM != PLATFORM_NRF52 +#if HAS_BLE + +#include "BLESerial.h" + +uint32_t bt_passkey_callback(); +void bt_passkey_notify_callback(uint32_t passkey); +bool bt_security_request_callback(); +void bt_authentication_complete_callback(esp_ble_auth_cmpl_t auth_result); +bool bt_confirm_pin_callback(uint32_t pin); +void bt_connect_callback(BLEServer *server); +void bt_disconnect_callback(BLEServer *server); +bool bt_client_authenticated(); + +uint32_t BLESerial::onPassKeyRequest() { return bt_passkey_callback(); } +void BLESerial::onPassKeyNotify(uint32_t passkey) { bt_passkey_notify_callback(passkey); } +bool BLESerial::onSecurityRequest() { return bt_security_request_callback(); } +void BLESerial::onAuthenticationComplete(esp_ble_auth_cmpl_t auth_result) { bt_authentication_complete_callback(auth_result); } +void BLESerial::onConnect(BLEServer *server) { bt_connect_callback(server); } +void BLESerial::onDisconnect(BLEServer *server) { bt_disconnect_callback(server); ble_server->startAdvertising(); } +bool BLESerial::onConfirmPIN(uint32_t pin) { return bt_confirm_pin_callback(pin); }; +bool BLESerial::connected() { return ble_server->getConnectedCount() > 0; } + +int BLESerial::read() { + int result = this->rx_buffer.pop(); + if (result == '\n') { this->numAvailableLines--; } + return result; +} + +size_t BLESerial::readBytes(uint8_t *buffer, size_t bufferSize) { + int i = 0; + while (i < bufferSize && available()) { buffer[i] = (uint8_t)this->rx_buffer.pop(); i++; } + return i; +} + +int BLESerial::peek() { + if (this->rx_buffer.getLength() == 0) return -1; + return this->rx_buffer.get(0); +} + +int BLESerial::available() { return this->rx_buffer.getLength(); } + +size_t BLESerial::print(const char *str) { + if (ble_server->getConnectedCount() <= 0) return 0; + size_t written = 0; for (size_t i = 0; str[i] != '\0'; i++) { written += this->write(str[i]); } + flush(); + + return written; +} + +size_t BLESerial::write(const uint8_t *buffer, size_t bufferSize) { + if (ble_server->getConnectedCount() <= 0) { return 0; } else { + size_t written = 0; for (int i = 0; i < bufferSize; i++) { written += this->write(buffer[i]); } + flush(); + + return written; + } +} + +size_t BLESerial::write(uint8_t byte) { + if (bt_client_authenticated()) { + if (ble_server->getConnectedCount() <= 0) { return 0; } else { + this->transmitBuffer[this->transmitBufferLength] = byte; + this->transmitBufferLength++; + if (this->transmitBufferLength == maxTransferSize) { flush(); } + return 1; + } + } else { + return 0; + } +} + +void BLESerial::flush() { + if (this->transmitBufferLength > 0) { + TxCharacteristic->setValue(this->transmitBuffer, this->transmitBufferLength); + this->transmitBufferLength = 0; + this->lastFlushTime = millis(); + TxCharacteristic->notify(true); + } +} + +void BLESerial::disconnect() { + if (ble_server->getConnectedCount() > 0) { + uint16_t conn_id = ble_server->getConnId(); + // Serial.printf("Have connected: %d\n", conn_id); + ble_server->disconnect(conn_id); + // Serial.println("Disconnected"); + } else { + // Serial.println("No connected"); + } +} + +void BLESerial::begin(const char *name) { + ConnectedDeviceCount = 0; + BLEDevice::init(name); + + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN ,ESP_PWR_LVL_P9); + + ble_server = BLEDevice::createServer(); + ble_server->setCallbacks(this); + BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_MITM); + BLEDevice::setSecurityCallbacks(this); + + SetupSerialService(); + this->startAdvertising(); +} + +void BLESerial::startAdvertising() { + ble_adv = BLEDevice::getAdvertising(); + ble_adv->addServiceUUID(BLE_SERIAL_SERVICE_UUID); + ble_adv->setMinPreferred(0x20); + ble_adv->setMaxPreferred(0x40); + ble_adv->setScanResponse(true); + ble_adv->start(); +} + +void BLESerial::stopAdvertising() { + ble_adv = BLEDevice::getAdvertising(); + ble_adv->stop(); +} + +void BLESerial::end() { BLEDevice::deinit(); } + +void BLESerial::onWrite(BLECharacteristic *characteristic) { + if (characteristic->getUUID().toString() == BLE_RX_UUID) { + auto value = characteristic->getValue(); + for (int i = 0; i < value.length(); i++) { rx_buffer.push(value[i]); } + } +} + +void BLESerial::SetupSerialService() { + SerialService = ble_server->createService(BLE_SERIAL_SERVICE_UUID); + + RxCharacteristic = SerialService->createCharacteristic(BLE_RX_UUID, BLECharacteristic::PROPERTY_WRITE); + RxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENC_MITM); + RxCharacteristic->addDescriptor(new BLE2902()); + RxCharacteristic->setWriteProperty(true); + RxCharacteristic->setCallbacks(this); + + TxCharacteristic = SerialService->createCharacteristic(BLE_TX_UUID, BLECharacteristic::PROPERTY_NOTIFY); + TxCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM); + TxCharacteristic->addDescriptor(new BLE2902()); + TxCharacteristic->setNotifyProperty(true); + TxCharacteristic->setReadProperty(true); + + SerialService->start(); +} + +BLESerial::BLESerial() { } + +#endif +#endif \ No newline at end of file diff --git a/BLESerial.h b/BLESerial.h new file mode 100755 index 0000000..f845b56 --- /dev/null +++ b/BLESerial.h @@ -0,0 +1,136 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "Boards.h" + +#if PLATFORM != PLATFORM_NRF52 +#if HAS_BLE + +#include + +#include +#include +#include +#include + +template +class BLEFIFO { +private: + uint8_t buffer[n]; + int head = 0; + int tail = 0; + +public: + void push(uint8_t value) { + buffer[head] = value; + head = (head + 1) % n; + if (head == tail) { tail = (tail + 1) % n; } + } + + int pop() { + if (head == tail) { + return -1; + } else { + uint8_t value = buffer[tail]; + tail = (tail + 1) % n; + return value; + } + } + + void clear() { head = 0; tail = 0; } + + int get(size_t index) { + if (index >= this->getLength()) { + return -1; + } else { + return buffer[(tail + index) % n]; + } + } + + size_t getLength() { + if (head >= tail) { + return head - tail; + } else { + return n - tail + head; + } + } +}; + +#define RX_BUFFER_SIZE 6144 +#define BLE_BUFFER_SIZE 512 // Must fit in max GATT attribute length +#define MIN_MTU 50 + +class BLESerial : public BLECharacteristicCallbacks, public BLEServerCallbacks, public BLESecurityCallbacks, public Stream { +public: + BLESerial(); + + void begin(const char *name); + void end(); + void disconnect(); + void startAdvertising(); + void stopAdvertising(); + void onWrite(BLECharacteristic *characteristic); + int available(); + int peek(); + int read(); + size_t readBytes(uint8_t *buffer, size_t bufferSize); + size_t write(uint8_t byte); + size_t write(const uint8_t *buffer, size_t bufferSize); + size_t print(const char *value); + void flush(); + void onConnect(BLEServer *server); + void onDisconnect(BLEServer *server); + + uint32_t onPassKeyRequest(); + void onPassKeyNotify(uint32_t passkey); + bool onSecurityRequest(); + void onAuthenticationComplete(esp_ble_auth_cmpl_t); + bool onConfirmPIN(uint32_t pin); + + bool connected(); + + BLEServer *ble_server; + BLEAdvertising *ble_adv; + BLEService *SerialService; + BLECharacteristic *TxCharacteristic; + BLECharacteristic *RxCharacteristic; + size_t transmitBufferLength; + unsigned long long lastFlushTime; + +private: + BLESerial(BLESerial const &other) = delete; + void operator=(BLESerial const &other) = delete; + + BLEFIFO rx_buffer; + size_t numAvailableLines; + uint8_t transmitBuffer[BLE_BUFFER_SIZE]; + + int ConnectedDeviceCount; + void SetupSerialService(); + + uint16_t peerMTU; + uint16_t maxTransferSize = BLE_BUFFER_SIZE; + + bool checkMTU(); + + const char *BLE_SERIAL_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; + const char *BLE_RX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; + const char *BLE_TX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; + + bool started = false; +}; + +#endif +#endif \ No newline at end of file diff --git a/Bluetooth.h b/Bluetooth.h new file mode 100755 index 0000000..616c4b6 --- /dev/null +++ b/Bluetooth.h @@ -0,0 +1,615 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#if MCU_VARIANT == MCU_ESP32 + +#elif MCU_VARIANT == MCU_NRF52 +#endif + +#if MCU_VARIANT == MCU_ESP32 + #if HAS_BLUETOOTH == true + #include "BluetoothSerial.h" + #include "esp_bt_main.h" + #include "esp_bt_device.h" + BluetoothSerial SerialBT; + #elif HAS_BLE == true + #include "esp_bt_main.h" + #include "esp_bt_device.h" + #include "BLESerial.h" + BLESerial SerialBT; + #endif + +#elif MCU_VARIANT == MCU_NRF52 + #include + #include + #define BLE_RX_BUF 6144 + BLEUart SerialBT(BLE_RX_BUF); + BLEDis bledis; + BLEBas blebas; + bool SerialBT_init = false; +#endif + +#define BT_PAIRING_TIMEOUT 35000 +#define BLE_FLUSH_TIMEOUT 20 +uint32_t bt_pairing_started = 0; + +#define BT_DEV_ADDR_LEN 6 +#define BT_DEV_HASH_LEN 16 +uint8_t dev_bt_mac[BT_DEV_ADDR_LEN]; +char bt_da[BT_DEV_ADDR_LEN]; +char bt_dh[BT_DEV_HASH_LEN]; +char bt_devname[11]; + +#if MCU_VARIANT == MCU_ESP32 + #if HAS_BLUETOOTH == true + + void bt_confirm_pairing(uint32_t numVal) { + bt_ssp_pin = numVal; + kiss_indicate_btpin(); + if (bt_allow_pairing) { + SerialBT.confirmReply(true); + } else { + SerialBT.confirmReply(false); + } + } + + void bt_stop() { + display_unblank(); + if (bt_state != BT_STATE_OFF) { + SerialBT.end(); + bt_allow_pairing = false; + bt_state = BT_STATE_OFF; + } + } + + void bt_start() { + display_unblank(); + if (bt_state == BT_STATE_OFF) { + SerialBT.begin(bt_devname); + bt_state = BT_STATE_ON; + } + } + + void bt_enable_pairing() { + display_unblank(); + if (bt_state == BT_STATE_OFF) bt_start(); + bt_allow_pairing = true; + bt_pairing_started = millis(); + bt_state = BT_STATE_PAIRING; + } + + void bt_disable_pairing() { + display_unblank(); + bt_allow_pairing = false; + bt_ssp_pin = 0; + bt_state = BT_STATE_ON; + } + + void bt_pairing_complete(boolean success) { + display_unblank(); + if (success) { + bt_disable_pairing(); + } else { + bt_ssp_pin = 0; + } + } + + void bt_connection_callback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) { + display_unblank(); + if(event == ESP_SPP_SRV_OPEN_EVT) { + bt_state = BT_STATE_CONNECTED; + cable_state = CABLE_STATE_DISCONNECTED; + } + + if(event == ESP_SPP_CLOSE_EVT ){ + bt_state = BT_STATE_ON; + } + } + + bool bt_setup_hw() { + if (!bt_ready) { + if (EEPROM.read(eeprom_addr(ADDR_CONF_BT)) == BT_ENABLE_BYTE) { + bt_enabled = true; + } else { + bt_enabled = false; + } + if (btStart()) { + if (esp_bluedroid_init() == ESP_OK) { + if (esp_bluedroid_enable() == ESP_OK) { + const uint8_t* bda_ptr = esp_bt_dev_get_address(); + char *data = (char*)malloc(BT_DEV_ADDR_LEN+1); + for (int i = 0; i < BT_DEV_ADDR_LEN; i++) { + data[i] = bda_ptr[i]; + } + data[BT_DEV_ADDR_LEN] = EEPROM.read(eeprom_addr(ADDR_SIGNATURE)); + unsigned char *hash = MD5::make_hash(data, BT_DEV_ADDR_LEN); + memcpy(bt_dh, hash, BT_DEV_HASH_LEN); + sprintf(bt_devname, "RNode %02X%02X", bt_dh[14], bt_dh[15]); + free(data); + + SerialBT.enableSSP(); + SerialBT.onConfirmRequest(bt_confirm_pairing); + SerialBT.onAuthComplete(bt_pairing_complete); + SerialBT.register_callback(bt_connection_callback); + + bt_ready = true; + return true; + + } else { return false; } + } else { return false; } + } else { return false; } + } else { return false; } + } + + bool bt_init() { + bt_state = BT_STATE_OFF; + if (bt_setup_hw()) { + if (bt_enabled && !console_active) bt_start(); + return true; + } else { + return false; + } + } + + void update_bt() { + if (bt_allow_pairing && millis()-bt_pairing_started >= BT_PAIRING_TIMEOUT) { + bt_disable_pairing(); + } + } + + #elif HAS_BLE == true + bool bt_setup_hw(); void bt_security_setup(); + BLESecurity *ble_security = new BLESecurity(); + bool ble_authenticated = false; + uint32_t pairing_pin = 0; + + void bt_flush() { if (bt_state == BT_STATE_CONNECTED) { SerialBT.flush(); } } + + void bt_start() { + // Serial.println("BT start"); + display_unblank(); + if (bt_state == BT_STATE_OFF) { + bt_state = BT_STATE_ON; + SerialBT.begin(bt_devname); + SerialBT.setTimeout(10); + } + } + + void bt_stop() { + // Serial.println("BT stop"); + display_unblank(); + if (bt_state != BT_STATE_OFF) { + bt_allow_pairing = false; + bt_state = BT_STATE_OFF; + SerialBT.end(); + } + } + + bool bt_init() { + // Serial.println("BT init"); + bt_state = BT_STATE_OFF; + if (bt_setup_hw()) { + if (bt_enabled && !console_active) bt_start(); + return true; + } else { + return false; + } + } + + void bt_debond_all() { + // Serial.println("Debonding all"); + int dev_num = esp_ble_get_bond_device_num(); + esp_ble_bond_dev_t *dev_list = (esp_ble_bond_dev_t *)malloc(sizeof(esp_ble_bond_dev_t) * dev_num); + esp_ble_get_bond_device_list(&dev_num, dev_list); + for (int i = 0; i < dev_num; i++) { esp_ble_remove_bond_device(dev_list[i].bd_addr); } + free(dev_list); + } + + void bt_enable_pairing() { + // Serial.println("BT enable pairing"); + display_unblank(); + if (bt_state == BT_STATE_OFF) bt_start(); + + bt_security_setup(); + + bt_allow_pairing = true; + bt_pairing_started = millis(); + bt_state = BT_STATE_PAIRING; + bt_ssp_pin = pairing_pin; + } + + void bt_disable_pairing() { + // Serial.println("BT disable pairing"); + display_unblank(); + bt_allow_pairing = false; + bt_ssp_pin = 0; + bt_state = BT_STATE_ON; + } + + void bt_passkey_notify_callback(uint32_t passkey) { + // Serial.printf("Got passkey notification: %d\n", passkey); + if (bt_allow_pairing) { + bt_ssp_pin = passkey; + bt_pairing_started = millis(); + kiss_indicate_btpin(); + } else { + // Serial.println("Pairing not allowed, re-init"); + SerialBT.disconnect(); + } + } + + bool bt_confirm_pin_callback(uint32_t pin) { + // Serial.printf("Confirm PIN callback: %d\n", pin); + return true; + } + + void bt_update_passkey() { + // Serial.println("Updating passkey"); + pairing_pin = random(899999)+100000; + bt_ssp_pin = pairing_pin; + } + + uint32_t bt_passkey_callback() { + // Serial.println("API passkey request"); + if (pairing_pin == 0) { bt_update_passkey(); } + return pairing_pin; + } + + bool bt_client_authenticated() { + return ble_authenticated; + } + + bool bt_security_request_callback() { + if (bt_allow_pairing) { + // Serial.println("Accepting security request"); + return true; + } else { + // Serial.println("Rejecting security request"); + return false; + } + } + + void bt_authentication_complete_callback(esp_ble_auth_cmpl_t auth_result) { + if (auth_result.success == true) { + // Serial.println("Authentication success"); + ble_authenticated = true; + if (bt_state == BT_STATE_PAIRING) { + // Serial.println("Pairing complete, disconnecting"); + delay(2000); SerialBT.disconnect(); + } else { bt_state = BT_STATE_CONNECTED; } + } else { + // Serial.println("Authentication fail"); + ble_authenticated = false; + bt_state = BT_STATE_ON; + bt_update_passkey(); + bt_security_setup(); + } + bt_allow_pairing = false; + bt_ssp_pin = 0; + } + + void bt_connect_callback(BLEServer *server) { + uint16_t conn_id = server->getConnId(); + // Serial.printf("Connected: %d\n", conn_id); + display_unblank(); + ble_authenticated = false; + if (bt_state != BT_STATE_PAIRING) { bt_state = BT_STATE_CONNECTED; } + cable_state = CABLE_STATE_DISCONNECTED; + } + + void bt_disconnect_callback(BLEServer *server) { + uint16_t conn_id = server->getConnId(); + // Serial.printf("Disconnected: %d\n", conn_id); + display_unblank(); + ble_authenticated = false; + bt_state = BT_STATE_ON; + } + + bool bt_setup_hw() { + // Serial.println("BT setup hw"); + if (!bt_ready) { + if (EEPROM.read(eeprom_addr(ADDR_CONF_BT)) == BT_ENABLE_BYTE) { + bt_enabled = true; + } else { + bt_enabled = false; + } + if (btStart()) { + if (esp_bluedroid_init() == ESP_OK) { + if (esp_bluedroid_enable() == ESP_OK) { + const uint8_t* bda_ptr = esp_bt_dev_get_address(); + char *data = (char*)malloc(BT_DEV_ADDR_LEN+1); + for (int i = 0; i < BT_DEV_ADDR_LEN; i++) { + data[i] = bda_ptr[i]; + } + data[BT_DEV_ADDR_LEN] = EEPROM.read(eeprom_addr(ADDR_SIGNATURE)); + unsigned char *hash = MD5::make_hash(data, BT_DEV_ADDR_LEN); + memcpy(bt_dh, hash, BT_DEV_HASH_LEN); + sprintf(bt_devname, "RNode %02X%02X", bt_dh[14], bt_dh[15]); + free(data); + + bt_security_setup(); + + bt_ready = true; + return true; + + } else { return false; } + } else { return false; } + } else { return false; } + } else { return false; } + } + + void bt_security_setup() { + // Serial.println("Executing BT security setup"); + if (pairing_pin == 0) { bt_update_passkey(); } + uint32_t passkey = pairing_pin; + // Serial.printf("Passkey is %d\n", passkey); + + uint8_t key_size = 16; + uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + + esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_MITM_BOND; + uint8_t auth_option = ESP_BLE_ONLY_ACCEPT_SPECIFIED_AUTH_ENABLE; + uint8_t oob_support = ESP_BLE_OOB_DISABLE; + + esp_ble_io_cap_t iocap = ESP_IO_CAP_OUT; + + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_ONLY_ACCEPT_SPECIFIED_SEC_AUTH, &auth_option, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_OOB_SUPPORT, &oob_support, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(uint8_t)); + } + + void update_bt() { + if (bt_allow_pairing && millis()-bt_pairing_started >= BT_PAIRING_TIMEOUT) { + bt_disable_pairing(); + } + if (bt_state == BT_STATE_CONNECTED && millis()-SerialBT.lastFlushTime >= BLE_FLUSH_TIMEOUT) { + if (SerialBT.transmitBufferLength > 0) { + bt_flush(); + } + } + } + #endif + +#elif MCU_VARIANT == MCU_NRF52 + uint32_t pairing_pin = 0; + + uint8_t eeprom_read(uint32_t mapped_addr); + + void bt_stop() { + // Serial.println("BT Stop"); + if (bt_state != BT_STATE_OFF) { + bt_allow_pairing = false; + bt_state = BT_STATE_OFF; + } + } + + void bt_flush() { if (bt_state == BT_STATE_CONNECTED) { SerialBT.flushTXD(); } } + + void bt_disable_pairing() { + // Serial.println("BT Disable pairing"); + bt_allow_pairing = false; + pairing_pin = 0; + bt_ssp_pin = 0; + bt_state = BT_STATE_ON; + } + + void bt_pairing_complete(uint16_t conn_handle, uint8_t auth_status) { + // Serial.println("BT pairing complete"); + BLEConnection* connection = Bluefruit.Connection(conn_handle); + if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { + ble_gap_conn_sec_mode_t security = connection->getSecureMode(); + // Serial.println("Bonding success"); + + // On the NRF52 it is not possible with the Arduino library to reject + // requests from devices with no IO capabilities, which would allow + // bypassing pin entry through pairing using the "just works" mode. + // Therefore, we must check the security level of the connection after + // pairing to ensure "just works" has not been used. If it has, we need + // to disconnect, unpair and delete any bonding information immediately. + // Settings on the SerialBT service should prevent unauthorised access to + // the serial port anyway, but this is still wise to do regardless. + // + // Note: It may be nice to have this done in the BLESecurity class in the + // future, but as it stands right now I'd have to fork the BSP to do + // that, which I don't fancy doing. Impact on security is likely minimal. + // Requires investigation. + + if (security.sm == 1 && security.lv >= 3) { + // Serial.println("Auth level success"); + bt_state = BT_STATE_CONNECTED; + cable_state = CABLE_STATE_DISCONNECTED; + connection->disconnect(); + bt_disable_pairing(); + } else { + // Serial.println("Auth level failure, debonding"); + if (connection->bonded()) { connection->removeBondKey(); } + connection->disconnect(); + bt_disable_pairing(); + } + } else { + // Serial.println("Bonding failure"); + connection->disconnect(); + bt_disable_pairing(); + } + } + + bool bt_passkey_callback(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { + // Serial.println("Passkey callback"); + if (bt_allow_pairing) { + return true; + } + return false; + } + + void bt_connect_callback(uint16_t conn_handle) { + // Serial.println("Connect callback"); + bt_state = BT_STATE_CONNECTED; + cable_state = CABLE_STATE_DISCONNECTED; + + BLEConnection* conn = Bluefruit.Connection(conn_handle); + conn->requestPHY(BLE_GAP_PHY_2MBPS); + conn->requestMtuExchange(512+3); + conn->requestDataLengthUpdate(); + } + + void bt_disconnect_callback(uint16_t conn_handle, uint8_t reason) { + // Serial.println("Disconnect callback"); + if (reason != BLE_GAP_SEC_STATUS_SUCCESS) { + bt_state = BT_STATE_ON; + } + } + + void bt_update_passkey() { + // Serial.println("Update passkey"); + pairing_pin = random(899999)+100000; + bt_ssp_pin = pairing_pin; + } + + uint32_t bt_get_passkey() { + // Serial.println("API passkey request"); + if (pairing_pin == 0) { bt_update_passkey(); } + return pairing_pin; + } + + bool bt_setup_hw() { + // Serial.println("Setup HW"); + if (!bt_ready) { + #if HAS_EEPROM + if (EEPROM.read(eeprom_addr(ADDR_CONF_BT)) == BT_ENABLE_BYTE) { + #else + if (eeprom_read(eeprom_addr(ADDR_CONF_BT)) == BT_ENABLE_BYTE) { + #endif + bt_enabled = true; + } else { + bt_enabled = false; + } + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.autoConnLed(false); + if (Bluefruit.begin()) { + uint32_t pin = bt_get_passkey(); + char pin_char[6]; + sprintf(pin_char,"%lu", pin); + + Bluefruit.setTxPower(8); // Check bluefruit.h for supported values + Bluefruit.Security.setIOCaps(true, false, false); // display, yes; yes / no, no; keyboard, no + // This device is indeed capable of yes / no through the pairing mode + // being set, but I have chosen to set it thus to force the input of the + // pin on the device initiating the pairing. + + Bluefruit.Security.setMITM(true); + Bluefruit.Security.setPairPasskeyCallback(bt_passkey_callback); + Bluefruit.Security.setSecuredCallback(bt_connect_callback); + Bluefruit.Security.setPIN(pin_char); + Bluefruit.Periph.setDisconnectCallback(bt_disconnect_callback); + Bluefruit.Security.setPairCompleteCallback(bt_pairing_complete); + Bluefruit.Periph.setConnInterval(6, 12); // 7.5 - 15 ms + + const ble_gap_addr_t gap_addr = Bluefruit.getAddr(); + char *data = (char*)malloc(BT_DEV_ADDR_LEN+1); + for (int i = 0; i < BT_DEV_ADDR_LEN; i++) { + data[i] = gap_addr.addr[i]; + } + #if HAS_EEPROM + data[BT_DEV_ADDR_LEN] = EEPROM.read(eeprom_addr(ADDR_SIGNATURE)); + #else + data[BT_DEV_ADDR_LEN] = eeprom_read(eeprom_addr(ADDR_SIGNATURE)); + #endif + unsigned char *hash = MD5::make_hash(data, BT_DEV_ADDR_LEN); + memcpy(bt_dh, hash, BT_DEV_HASH_LEN); + sprintf(bt_devname, "RNode %02X%02X", bt_dh[14], bt_dh[15]); + free(data); + + bt_ready = true; + return true; + + } else { return false; } + } else { return false; } + } + + void bt_start() { + // Serial.println("BT Start"); + if (bt_state == BT_STATE_OFF) { + Bluefruit.setName(bt_devname); + bledis.setManufacturer(BLE_MANUFACTURER); + bledis.setModel(BLE_MODEL); + // start device information service + bledis.begin(); + blebas.begin(); + + // Guard to ensure SerialBT service is not duplicated through BT being power cycled + if (!SerialBT_init) { + SerialBT.bufferTXD(true); // enable buffering + + SerialBT.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); // enable encryption for BLE serial + SerialBT.begin(); + SerialBT_init = true; + } + + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + + // Include bleuart 128-bit uuid + Bluefruit.Advertising.addService(SerialBT); + + // There is no room for Name in Advertising packet + // Use Scan response for Name + Bluefruit.ScanResponse.addName(); + + Bluefruit.Advertising.start(0); + + bt_state = BT_STATE_ON; + } + } + + bool bt_init() { + // Serial.println("BT init"); + bt_state = BT_STATE_OFF; + if (bt_setup_hw()) { + if (bt_enabled && !console_active) bt_start(); + return true; + } else { + return false; + } + } + + void bt_enable_pairing() { + // Serial.println("BT enable pairing"); + if (bt_state == BT_STATE_OFF) bt_start(); + + uint32_t pin = bt_get_passkey(); + char pin_char[6]; + sprintf(pin_char,"%lu", pin); + Bluefruit.Security.setPIN(pin_char); + + bt_allow_pairing = true; + bt_pairing_started = millis(); + bt_state = BT_STATE_PAIRING; + kiss_indicate_btpin(); + } + + void bt_debond_all() { } + + void update_bt() { + if (bt_allow_pairing && millis()-bt_pairing_started >= BT_PAIRING_TIMEOUT) { + bt_disable_pairing(); + } + } +#endif diff --git a/Boards.h b/Boards.h new file mode 100755 index 0000000..38262d8 --- /dev/null +++ b/Boards.h @@ -0,0 +1,938 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "Modem.h" + +#ifndef BOARDS_H + #define BOARDS_H + + #define PLATFORM_AVR 0x90 + #define PLATFORM_ESP32 0x80 + #define PLATFORM_NRF52 0x70 + + #define MCU_1284P 0x91 + #define MCU_2560 0x92 + #define MCU_ESP32 0x81 + #define MCU_NRF52 0x71 + + // Products, boards and models //// + #define PRODUCT_RNODE 0x03 // RNode devices + #define BOARD_RNODE 0x31 // Original v1.0 RNode + #define MODEL_A4 0xA4 // RNode v1.0, 433 MHz + #define MODEL_A9 0xA9 // RNode v1.0, 868 MHz + + #define BOARD_RNODE_NG_20 0x40 // RNode hardware revision v2.0 + #define MODEL_A3 0xA3 // RNode v2.0, 433 MHz + #define MODEL_A8 0xA8 // RNode v2.0, 868 MHz + + #define BOARD_RNODE_NG_21 0x41 // RNode hardware revision v2.1 + #define MODEL_A2 0xA2 // RNode v2.1, 433 MHz + #define MODEL_A7 0xA7 // RNode v2.1, 868 MHz + + #define BOARD_T3S3 0x42 // T3S3 devices + #define MODEL_A1 0xA1 // T3S3, 433 MHz with SX1268 + #define MODEL_A5 0xA5 // T3S3, 433 MHz with SX1278 + #define MODEL_A6 0xA6 // T3S3, 868 MHz with SX1262 + #define MODEL_AA 0xAA // T3S3, 868 MHz with SX1276 + #define MODEL_AC 0xAC // T3S3, 2.4 GHz with SX1280 and PA + + #define PRODUCT_TBEAM 0xE0 // T-Beam devices + #define BOARD_TBEAM 0x33 + #define MODEL_E4 0xE4 // T-Beam SX1278, 433 Mhz + #define MODEL_E9 0xE9 // T-Beam SX1276, 868 Mhz + #define MODEL_E3 0xE3 // T-Beam SX1268, 433 Mhz + #define MODEL_E8 0xE8 // T-Beam SX1262, 868 Mhz + + #define PRODUCT_TDECK_V1 0xD0 + #define BOARD_TDECK 0x3B + #define MODEL_D4 0xD4 // LilyGO T-Deck, 433 MHz + #define MODEL_D9 0xD9 // LilyGO T-Deck, 868 MHz + + #define PRODUCT_TBEAM_S_V1 0xEA + #define BOARD_TBEAM_S_V1 0x3D + #define MODEL_DB 0xDB // LilyGO T-Beam Supreme, 433 MHz + #define MODEL_DC 0xDC // LilyGO T-Beam Supreme, 868 MHz + + #define PRODUCT_XIAO_S3 0xEB + #define BOARD_XIAO_S3 0x3E + #define MODEL_DE 0xDE // Xiao ESP32S3 with Wio-SX1262 module, 433 MHz + #define MODEL_DD 0xDD // Xiao ESP32S3 with Wio-SX1262 module, 868 MHz + + #define PRODUCT_T32_10 0xB2 + #define BOARD_LORA32_V1_0 0x39 + #define MODEL_BA 0xBA // LilyGO T3 v1.0, 433 MHz + #define MODEL_BB 0xBB // LilyGO T3 v1.0, 868 MHz + + #define PRODUCT_T32_20 0xB0 + #define BOARD_LORA32_V2_0 0x36 + #define MODEL_B3 0xB3 // LilyGO T3 v2.0, 433 MHz + #define MODEL_B8 0xB8 // LilyGO T3 v2.0, 868 MHz + + #define PRODUCT_T32_21 0xB1 + #define BOARD_LORA32_V2_1 0x37 + #define MODEL_B4 0xB4 // LilyGO T3 v2.1, 433 MHz + #define MODEL_B9 0xB9 // LilyGO T3 v2.1, 868 MHz + + #define PRODUCT_H32_V2 0xC0 // Board code 0x38 + #define BOARD_HELTEC32_V2 0x38 + #define MODEL_C4 0xC4 // Heltec Lora32 v2, 433 MHz + #define MODEL_C9 0xC9 // Heltec Lora32 v2, 868 MHz + + #define PRODUCT_H32_V3 0xC1 + #define BOARD_HELTEC32_V3 0x3A + #define MODEL_C5 0xC5 // Heltec Lora32 v3, 433 MHz + #define MODEL_CA 0xCA // Heltec Lora32 v3, 868 MHz + + #define PRODUCT_H32_V4 0xC3 + #define BOARD_HELTEC32_V4 0x3F + #define MODEL_C8 0xC8 // Heltec Lora32 v3, 850-950 MHz, 28dBm + + #define PRODUCT_HELTEC_T114 0xC2 // Heltec Mesh Node T114 + #define BOARD_HELTEC_T114 0x3C + #define MODEL_C6 0xC6 // Heltec Mesh Node T114, 470-510 MHz + #define MODEL_C7 0xC7 // Heltec Mesh Node T114, 863-928 MHz + + #define PRODUCT_TECHO 0x15 // LilyGO T-Echo devices + #define BOARD_TECHO 0x44 + #define MODEL_16 0x16 // T-Echo 433 MHz + #define MODEL_17 0x17 // T-Echo 868/915 MHz + + #define PRODUCT_RAK4631 0x10 + #define BOARD_RAK4631 0x51 + #define MODEL_11 0x11 // RAK4631, 433 Mhz + #define MODEL_12 0x12 // RAK4631, 868 Mhz + + #define PRODUCT_HMBRW 0xF0 + #define BOARD_HMBRW 0x32 + #define BOARD_HUZZAH32 0x34 + #define BOARD_GENERIC_ESP32 0x35 + #define BOARD_GENERIC_NRF52 0x50 + #define MODEL_FE 0xFE // Homebrew board, max 17dBm output power + #define MODEL_FF 0xFF // Homebrew board, max 14dBm output power + + #if defined(__AVR_ATmega1284P__) + #define PLATFORM PLATFORM_AVR + #define MCU_VARIANT MCU_1284P + #elif defined(__AVR_ATmega2560__) + #define PLATFORM PLATFORM_AVR + #define MCU_VARIANT MCU_2560 + #elif defined(ESP32) + #define PLATFORM PLATFORM_ESP32 + #define MCU_VARIANT MCU_ESP32 + #elif defined(NRF52840_XXAA) + #include + #define PLATFORM PLATFORM_NRF52 + #define MCU_VARIANT MCU_NRF52 + #else + #error "The firmware cannot be compiled for the selected MCU variant" + #endif + + #ifndef MODEM + #if BOARD_MODEL == BOARD_RAK4631 + #define MODEM SX1262 + #elif BOARD_MODEL == BOARD_GENERIC_NRF52 + #define MODEM SX1262 + #else + #define MODEM SX1276 + #endif + #endif + + #define HAS_DISPLAY false + #define HAS_BLUETOOTH false + #define HAS_BLE false + #define HAS_WIFI false + #define HAS_TCXO false + #define HAS_PMU false + #define HAS_NP false + #define HAS_EEPROM false + #define HAS_INPUT false + #define HAS_SLEEP false + #define HAS_LORA_PA false + #define HAS_LORA_LNA false + #define PIN_DISP_SLEEP -1 + #define VALIDATE_FIRMWARE true + + #if defined(ENABLE_TCXO) + #define HAS_TCXO true + #endif + + #if MCU_VARIANT == MCU_1284P + const int pin_cs = 4; + const int pin_reset = 3; + const int pin_dio = 2; + const int pin_led_rx = 12; + const int pin_led_tx = 13; + + #define BOARD_MODEL BOARD_RNODE + #define HAS_EEPROM true + #define CONFIG_UART_BUFFER_SIZE 6144 + #define CONFIG_QUEUE_SIZE 6144 + #define CONFIG_QUEUE_MAX_LENGTH 200 + #define EEPROM_SIZE 4096 + #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED + + #elif MCU_VARIANT == MCU_2560 + const int pin_cs = 5; + const int pin_reset = 4; + const int pin_dio = 2; + const int pin_led_rx = 12; + const int pin_led_tx = 13; + + #define BOARD_MODEL BOARD_HMBRW + #define HAS_EEPROM true + #define CONFIG_UART_BUFFER_SIZE 768 + #define CONFIG_QUEUE_SIZE 5120 + #define CONFIG_QUEUE_MAX_LENGTH 24 + #define EEPROM_SIZE 4096 + #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED + + #elif MCU_VARIANT == MCU_ESP32 + + // Board models for ESP32 based builds are + // defined by the build target in the makefile. + // If you are not using make to compile this + // firmware, you can manually define model here. + // + // #define BOARD_MODEL BOARD_GENERIC_ESP32 + #define CONFIG_UART_BUFFER_SIZE 6144 + #define CONFIG_QUEUE_SIZE 6144 + #define CONFIG_QUEUE_MAX_LENGTH 200 + + #define EEPROM_SIZE 1024 + #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED + #define CONFIG_OFFSET 0 + + #define GPS_BAUD_RATE 9600 + #define PIN_GPS_TX 12 + #define PIN_GPS_RX 34 + + #if BOARD_MODEL == BOARD_GENERIC_ESP32 + #define HAS_BLUETOOTH true + #define HAS_CONSOLE true + #define HAS_EEPROM true + const int pin_cs = 4; + const int pin_reset = 33; + const int pin_dio = 39; + const int pin_led_rx = 14; + const int pin_led_tx = 32; + + #elif BOARD_MODEL == BOARD_TBEAM + #define HAS_DISPLAY true + #define HAS_PMU true + #define HAS_BLUETOOTH true + #define HAS_CONSOLE true + #define HAS_SD false + #define HAS_EEPROM true + #define I2C_SDA 21 + #define I2C_SCL 22 + #define PMU_IRQ 35 + + #define HAS_INPUT true + const int pin_btn_usr1 = 38; + + const int pin_cs = 18; + const int pin_reset = 23; + const int pin_led_rx = 2; + const int pin_led_tx = 4; + + #if MODEM == SX1262 + #define HAS_TCXO true + #define HAS_BUSY true + #define DIO2_AS_RF_SWITCH true + #define OCP_TUNED 0x18 + const int pin_busy = 32; + const int pin_dio = 33; + const int pin_tcxo_enable = -1; + #else + const int pin_dio = 26; + #endif + + #elif BOARD_MODEL == BOARD_HUZZAH32 + #define HAS_BLUETOOTH true + #define HAS_CONSOLE true + #define HAS_EEPROM true + const int pin_cs = 4; + const int pin_reset = 33; + const int pin_dio = 39; + const int pin_led_rx = 14; + const int pin_led_tx = 32; + + #elif BOARD_MODEL == BOARD_LORA32_V1_0 + #define HAS_DISPLAY true + #define HAS_BLUETOOTH true + #define HAS_CONSOLE true + #define HAS_EEPROM true + const int pin_cs = 18; + const int pin_reset = 14; + const int pin_dio = 26; + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 25; + const int pin_led_tx = 2; + #else + const int pin_led_rx = 2; + const int pin_led_tx = 2; + #endif + + #elif BOARD_MODEL == BOARD_LORA32_V2_0 + #define HAS_DISPLAY true + #define HAS_BLUETOOTH true + #define HAS_CONSOLE true + #define HAS_EEPROM true + const int pin_cs = 18; + const int pin_reset = 12; + const int pin_dio = 26; + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 2; + const int pin_led_tx = 0; + #else + const int pin_led_rx = 22; + const int pin_led_tx = 22; + #endif + + #elif BOARD_MODEL == BOARD_LORA32_V2_1 + #define HAS_DISPLAY true + #define HAS_BLUETOOTH true + #define HAS_PMU true + #define HAS_CONSOLE true + #define HAS_EEPROM true + const int pin_cs = 18; + const int pin_reset = 23; + const int pin_dio = 26; + #if HAS_TCXO == true + const int pin_tcxo_enable = 33; + #endif + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 15; + const int pin_led_tx = 4; + #else + const int pin_led_rx = 25; + const int pin_led_tx = 25; + #endif + + #elif BOARD_MODEL == BOARD_HELTEC32_V2 + #define HAS_DISPLAY true + #define HAS_BLUETOOTH true + #define HAS_CONSOLE true + #define HAS_EEPROM true + #define HAS_INPUT true + #define HAS_SLEEP true + #define PIN_WAKEUP GPIO_NUM_0 + #define WAKEUP_LEVEL 0 + + const int pin_btn_usr1 = 0; + + const int pin_cs = 18; + const int pin_reset = 14; + const int pin_dio = 26; + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 36; + const int pin_led_tx = 37; + #else + const int pin_led_rx = 25; + const int pin_led_tx = 25; + #endif + + #elif BOARD_MODEL == BOARD_HELTEC32_V3 + #define IS_ESP32S3 true + #define HAS_DISPLAY true + #define HAS_WIFI true + #define HAS_BLUETOOTH false + #define HAS_BLE true + #define HAS_PMU true + #define HAS_CONSOLE true + #define HAS_EEPROM true + #define HAS_INPUT true + #define HAS_SLEEP true + #define PIN_WAKEUP GPIO_NUM_0 + #define WAKEUP_LEVEL 0 + #define OCP_TUNED 0x18 + + const int pin_btn_usr1 = 0; + + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 13; + const int pin_led_tx = 14; + #else + const int pin_led_rx = 35; + const int pin_led_tx = 35; + #endif + + #define MODEM SX1262 + #define HAS_TCXO true + const int pin_tcxo_enable = -1; + #define HAS_BUSY true + #define DIO2_AS_RF_SWITCH true + + // Following pins are for the SX1262 + const int pin_cs = 8; + const int pin_busy = 13; + const int pin_dio = 14; + const int pin_reset = 12; + const int pin_mosi = 10; + const int pin_miso = 11; + const int pin_sclk = 9; + + #elif BOARD_MODEL == BOARD_HELTEC32_V4 + #define IS_ESP32S3 true + #define HAS_DISPLAY true + #define HAS_BLUETOOTH false + #ifdef BOUNDARY_MODE + #define HAS_BLE false + #else + #define HAS_BLE true + #endif + #define HAS_WIFI true + #define HAS_PMU true + #define HAS_CONSOLE true + #define HAS_EEPROM true + #define HAS_INPUT true + #define HAS_SLEEP true + #define HAS_LORA_PA true + #define HAS_LORA_LNA true + #define PIN_WAKEUP GPIO_NUM_0 + #define WAKEUP_LEVEL 0 + #define OCP_TUNED 0x18 + #define Vext GPIO_NUM_36 + + const int pin_btn_usr1 = 0; + + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 13; + const int pin_led_tx = 14; + #else + const int pin_led_rx = 35; + const int pin_led_tx = 35; + #endif + + #define MODEM SX1262 + #define HAS_TCXO true + const int pin_tcxo_enable = -1; + #define HAS_BUSY true + #define DIO2_AS_RF_SWITCH true + #define LNA_GD_THRSHLD (-109) + #define LNA_GD_LIMIT (-89) + + #define LORA_LNA_GAIN 17 + #define LORA_LNA_GVT 12 + #define LORA_PA_GC1109 true + #define LORA_PA_PWR_EN 7 + #define LORA_PA_CSD 2 + #define LORA_PA_CPS 46 + + #define PA_MAX_OUTPUT 28 + #define PA_GAIN_POINTS 22 + #define PA_GAIN_VALUES 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7 + + const int pin_cs = 8; + const int pin_busy = 13; + const int pin_dio = 14; + const int pin_reset = 12; + const int pin_mosi = 10; + const int pin_miso = 11; + const int pin_sclk = 9; + + #elif BOARD_MODEL == BOARD_RNODE_NG_20 + #define HAS_DISPLAY true + #define HAS_BLUETOOTH true + #define HAS_NP true + #define HAS_CONSOLE true + #define HAS_EEPROM true + const int pin_cs = 18; + const int pin_reset = 12; + const int pin_dio = 26; + const int pin_np = 4; + #if HAS_NP == false + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 2; + const int pin_led_tx = 0; + #else + const int pin_led_rx = 22; + const int pin_led_tx = 22; + #endif + #endif + + #elif BOARD_MODEL == BOARD_RNODE_NG_21 + #define HAS_DISPLAY true + #define HAS_BLUETOOTH true + #define HAS_CONSOLE true + #define HAS_PMU true + #define HAS_NP true + #define HAS_SD false + #define HAS_EEPROM true + const int pin_cs = 18; + const int pin_reset = 23; + const int pin_dio = 26; + const int pin_np = 12; + const int pin_dac = 25; + const int pin_adc = 34; + // CBA already defined by framework + //const int SD_MISO = 2; + // CBA already defined by framework + //const int SD_MOSI = 15; + const int SD_CLK = 14; + // CBA already defined by framework + //const int SD_CS = 13; + #if HAS_NP == false + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 12; + const int pin_led_tx = 4; + #else + const int pin_led_rx = 25; + const int pin_led_tx = 25; + #endif + #endif + + #elif BOARD_MODEL == BOARD_T3S3 + #define IS_ESP32S3 true + #define HAS_DISPLAY true + #define HAS_CONSOLE true + #define HAS_WIFI true + #define HAS_BLUETOOTH false + #define HAS_BLE true + #define HAS_PMU true + #define HAS_NP false + #define HAS_SD false + #define HAS_EEPROM true + + #define HAS_INPUT true + #define HAS_SLEEP true + #define PIN_WAKEUP GPIO_NUM_0 + #define WAKEUP_LEVEL 0 + const int pin_btn_usr1 = 0; + + const int pin_cs = 7; + const int pin_reset = 8; + const int pin_sclk = 5; + const int pin_mosi = 6; + const int pin_miso = 3; + + #if MODEM == SX1262 + #define DIO2_AS_RF_SWITCH true + #define HAS_BUSY true + #define HAS_TCXO true + const int pin_busy = 34; + const int pin_dio = 33; + const int pin_tcxo_enable = -1; + #elif MODEM == SX1280 + #define CONFIG_QUEUE_SIZE 6144 + #define DIO2_AS_RF_SWITCH false + #define HAS_BUSY true + #define HAS_TCXO true + #define HAS_PA true + const int pa_max_input = 3; + + #define HAS_RF_SWITCH_RX_TX true + const int pin_rxen = 21; + const int pin_txen = 10; + + const int pin_busy = 36; + const int pin_dio = 9; + const int pin_tcxo_enable = -1; + #else + const int pin_dio = 9; + #endif + + const int pin_np = 38; + const int pin_dac = 25; + const int pin_adc = 1; + + const int SD_MISO = 2; + const int SD_MOSI = 11; + const int SD_CLK = 14; + const int SD_CS = 13; + + #if HAS_NP == false + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 37; + const int pin_led_tx = 37; + #else + const int pin_led_rx = 37; + const int pin_led_tx = 37; + #endif + #endif + + #elif BOARD_MODEL == BOARD_TDECK + #define IS_ESP32S3 true + #define MODEM SX1262 + #define DIO2_AS_RF_SWITCH true + #define HAS_BUSY true + #define HAS_TCXO true + + #define HAS_DISPLAY false + #define HAS_CONSOLE false + #define HAS_WIFI true + #define HAS_BLUETOOTH false + #define HAS_BLE true + #define HAS_PMU true + #define HAS_NP false + #define HAS_SD false + #define HAS_EEPROM true + + #define HAS_INPUT true + #define HAS_SLEEP true + #define PIN_WAKEUP GPIO_NUM_0 + #define WAKEUP_LEVEL 0 + + const int pin_poweron = 10; + const int pin_btn_usr1 = 0; + + const int pin_cs = 9; + const int pin_reset = 17; + const int pin_sclk = 40; + const int pin_mosi = 41; + const int pin_miso = 38; + const int pin_tcxo_enable = -1; + const int pin_dio = 45; + const int pin_busy = 13; + + const int SD_MISO = 38; + const int SD_MOSI = 41; + const int SD_CLK = 40; + const int SD_CS = 39; + + const int DISPLAY_DC = 11; + const int DISPLAY_CS = 12; + const int DISPLAY_MISO = 38; + const int DISPLAY_MOSI = 41; + const int DISPLAY_CLK = 40; + const int DISPLAY_BL_PIN = 42; + + #if HAS_NP == false + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 43; + const int pin_led_tx = 43; + #else + const int pin_led_rx = 43; + const int pin_led_tx = 43; + #endif + #endif + + #elif BOARD_MODEL == BOARD_TBEAM_S_V1 + #define IS_ESP32S3 true + #define MODEM SX1262 + #define DIO2_AS_RF_SWITCH true + #define HAS_BUSY true + #define HAS_TCXO true + #define OCP_TUNED 0x18 + + #define HAS_DISPLAY true + #define HAS_CONSOLE true + #define HAS_WIFI true + #define HAS_BLUETOOTH false + #define HAS_BLE true + #define HAS_PMU true + #define HAS_NP false + #define HAS_SD false + #define HAS_EEPROM true + + #define HAS_INPUT true + #define HAS_SLEEP false + + #define PMU_IRQ 40 + #define I2C_SCL 41 + #define I2C_SDA 42 + + const int pin_btn_usr1 = 0; + + const int pin_cs = 10; + const int pin_reset = 5; + const int pin_sclk = 12; + const int pin_mosi = 11; + const int pin_miso = 13; + const int pin_tcxo_enable = -1; + const int pin_dio = 1; + const int pin_busy = 4; + + const int SD_MISO = 37; + const int SD_MOSI = 35; + const int SD_CLK = 36; + const int SD_CS = 47; + + const int IMU_CS = 34; + + #if HAS_NP == false + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 43; + const int pin_led_tx = 43; + #else + const int pin_led_rx = 43; + const int pin_led_tx = 43; + #endif + #endif + + #elif BOARD_MODEL == BOARD_XIAO_S3 + #define IS_ESP32S3 true + #define MODEM SX1262 + #define DIO2_AS_RF_SWITCH true + #define HAS_BUSY true + #define HAS_TCXO true + + #define HAS_DISPLAY false + #define HAS_CONSOLE true + #define HAS_WIFI true + #define HAS_BLUETOOTH false + #define HAS_BLE true + #define HAS_NP false + #define HAS_SD false + #define HAS_EEPROM true + + #define HAS_INPUT true + #define HAS_SLEEP true + #define PIN_WAKEUP GPIO_NUM_21 + #define WAKEUP_LEVEL 0 + + const int pin_btn_usr1 = 21; + const int pin_cs = 41; + const int pin_reset = 42; + const int pin_sclk = 7; + const int pin_mosi = 9; + const int pin_miso = 8; + const int pin_tcxo_enable = -1; + const int pin_dio = 39; + const int pin_busy = 40; + + #if HAS_NP == false + #if defined(EXTERNAL_LEDS) + const int pin_led_rx = 48; + const int pin_led_tx = 48; + #else + const int pin_led_rx = 48; + const int pin_led_tx = 48; + #endif + #endif + + #else + #error An unsupported ESP32 board was selected. Cannot compile RNode firmware. + #endif + + #elif MCU_VARIANT == MCU_NRF52 + #if BOARD_MODEL == BOARD_RAK4631 + #define HAS_EEPROM false + #define HAS_DISPLAY true + #define HAS_BLUETOOTH false + #define HAS_BLE true + #define HAS_CONSOLE false + #define HAS_PMU false + #define HAS_NP false + #define HAS_SD false + #define HAS_TCXO true + #define HAS_RF_SWITCH_RX_TX true + #define HAS_BUSY true + #define HAS_INPUT true + #define DIO2_AS_RF_SWITCH true + #define CONFIG_UART_BUFFER_SIZE 6144 + #define CONFIG_QUEUE_SIZE 6144 + #define CONFIG_QUEUE_MAX_LENGTH 200 + #define EEPROM_SIZE 296 + #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED + #define BLE_MANUFACTURER "RAK Wireless" + #define BLE_MODEL "RAK4640" + + const int pin_btn_usr1 = 9; + + // Following pins are for the sx1262 + const int pin_rxen = 37; + const int pin_txen = -1; + const int pin_reset = 38; + const int pin_cs = 42; + const int pin_sclk = 43; + const int pin_mosi = 44; + const int pin_miso = 45; + const int pin_busy = 46; + const int pin_dio = 47; + const int pin_led_rx = LED_BLUE; + const int pin_led_tx = LED_GREEN; + const int pin_tcxo_enable = -1; + + #elif BOARD_MODEL == BOARD_TECHO + #define _PINNUM(port, pin) ((port) * 32 + (pin)) + #define MODEM SX1262 + #define HAS_EEPROM false + #define HAS_BLUETOOTH false + #define HAS_BLE true + #define HAS_CONSOLE false + #define HAS_PMU true + #define HAS_NP false + #define HAS_SD false + #define HAS_TCXO true + #define HAS_BUSY true + #define HAS_INPUT true + #define HAS_SLEEP true + #define BLE_MANUFACTURER "LilyGO" + #define BLE_MODEL "T-Echo" + + #define HAS_INPUT true + #define EEPROM_SIZE 296 + #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED + + #define CONFIG_UART_BUFFER_SIZE 32768 + #define CONFIG_QUEUE_SIZE 6144 + #define CONFIG_QUEUE_MAX_LENGTH 200 + + #define HAS_DISPLAY true + #define HAS_BACKLIGHT true + #define DISPLAY_SCALE 1 + + #define LED_ON LOW + #define LED_OFF HIGH + #define PIN_LED_GREEN _PINNUM(1, 1) + #define PIN_LED_RED _PINNUM(1, 3) + #define PIN_LED_BLUE _PINNUM(0, 14) + #define PIN_VEXT_EN _PINNUM(0, 12) + + const int pin_disp_cs = 30; + const int pin_disp_dc = 28; + const int pin_disp_reset = 2; + const int pin_disp_busy = 3; + const int pin_disp_en = -1; + const int pin_disp_sck = 31; + const int pin_disp_mosi = 29; + const int pin_disp_miso = -1; + const int pin_backlight = 43; + + const int pin_btn_usr1 = _PINNUM(1, 10); + const int pin_btn_touch = _PINNUM(0, 11); + + const int pin_reset = 25; + const int pin_cs = 24; + const int pin_sclk = 19; + const int pin_mosi = 22; + const int pin_miso = 23; + const int pin_busy = 17; + const int pin_dio = 20; + const int pin_tcxo_enable = 21; + const int pin_led_rx = PIN_LED_BLUE; + const int pin_led_tx = PIN_LED_RED; + + #elif BOARD_MODEL == BOARD_HELTEC_T114 + #define MODEM SX1262 + #define HAS_EEPROM false + #define HAS_DISPLAY true + #define HAS_BLUETOOTH false + #define HAS_BLE true + #define HAS_CONSOLE false + #define HAS_PMU true + #define HAS_NP true + #define HAS_SD false + #define HAS_TCXO true + #define HAS_BUSY true + #define HAS_INPUT true + #define HAS_SLEEP true + #define DIO2_AS_RF_SWITCH true + #define CONFIG_UART_BUFFER_SIZE 6144 + #define CONFIG_QUEUE_SIZE 6144 + #define CONFIG_QUEUE_MAX_LENGTH 200 + #define EEPROM_SIZE 296 + #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED + #define BLE_MANUFACTURER "Heltec" + #define BLE_MODEL "T114" + + #define PIN_T114_ADC_EN 6 + #define PIN_VEXT_EN 21 + + // LED + #define LED_T114_GREEN 3 + #define PIN_T114_LED 14 + #define NP_M 1 + const int pin_np = PIN_T114_LED; + + // SPI + #define PIN_T114_MOSI 22 + #define PIN_T114_MISO 23 + #define PIN_T114_SCK 19 + #define PIN_T114_SS 24 + + // SX1262 + #define PIN_T114_RST 25 + #define PIN_T114_DIO1 20 + #define PIN_T114_BUSY 17 + + // TFT + #define DISPLAY_SCALE 2 + #define PIN_T114_TFT_MOSI 9 + #define PIN_T114_TFT_MISO 11 // not connected + #define PIN_T114_TFT_SCK 8 + #define PIN_T114_TFT_SS 11 + #define PIN_T114_TFT_DC 12 + #define PIN_T114_TFT_RST 2 + #define PIN_T114_TFT_EN 3 + #define PIN_T114_TFT_BLGT 15 + + // pins for buttons on Heltec T114 + const int pin_btn_usr1 = 42; + + // pins for sx1262 on Heltec T114 + const int pin_reset = PIN_T114_RST; + const int pin_cs = PIN_T114_SS; + const int pin_sclk = PIN_T114_SCK; + const int pin_mosi = PIN_T114_MOSI; + const int pin_miso = PIN_T114_MISO; + const int pin_busy = PIN_T114_BUSY; + const int pin_dio = PIN_T114_DIO1; + const int pin_led_rx = 35; + const int pin_led_tx = 35; + const int pin_tcxo_enable = -1; + + // pins for ST7789 display on Heltec T114 + const int DISPLAY_DC = PIN_T114_TFT_DC; + const int DISPLAY_CS = PIN_T114_TFT_SS; + const int DISPLAY_MISO = PIN_T114_TFT_MISO; + const int DISPLAY_MOSI = PIN_T114_TFT_MOSI; + const int DISPLAY_CLK = PIN_T114_TFT_SCK; + const int DISPLAY_BL_PIN = PIN_T114_TFT_BLGT; + const int DISPLAY_RST = PIN_T114_TFT_RST; + + #else + #error An unsupported nRF board was selected. Cannot compile RNode firmware. + #endif + + #endif + + #ifndef DISPLAY_SCALE + #define DISPLAY_SCALE 1 + #endif + + #ifndef HAS_RF_SWITCH_RX_TX + const int pin_rxen = -1; + const int pin_txen = -1; + #endif + + #ifndef HAS_BUSY + const int pin_busy = -1; + #endif + + #ifndef LED_ON + #define LED_ON HIGH + #endif + + #ifndef LED_OFF + #define LED_OFF LOW + #endif + + #ifndef DIO2_AS_RF_SWITCH + #define DIO2_AS_RF_SWITCH false + #endif + + // Default OCP value if not specified + // in board configuration + #ifndef OCP_TUNED + #define OCP_TUNED 0x18 + #endif + + #ifndef NP_M + #define NP_M 0.15 + #endif + +#endif diff --git a/BoundaryConfig.h b/BoundaryConfig.h new file mode 100644 index 0000000..9cf048c --- /dev/null +++ b/BoundaryConfig.h @@ -0,0 +1,501 @@ +// Copyright (C) 2026, Boundary Mode Extension +// Based on microReticulum_Firmware by Mark Qvist +// +// BoundaryConfig.h — Captive-portal web configuration for Boundary Mode. +// When triggered (first boot with no config, or button hold >5s), +// the device starts a WiFi AP with a web form for all settings: +// WiFi STA credentials, TCP backbone params, LoRa radio params, +// and optional AP-mode TCP server. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +#ifndef BOUNDARY_CONFIG_H +#define BOUNDARY_CONFIG_H + +#ifdef BOUNDARY_MODE + +#include +#include +#include + +// ─── Config Portal State ───────────────────────────────────────────────────── +static bool config_portal_active = false; +static WebServer* config_server = nullptr; +static DNSServer* config_dns = nullptr; + +static const char CONFIG_AP_SSID[] = "RNode-Boundary-Setup"; +static const uint16_t DNS_PORT = 53; +static const uint16_t HTTP_PORT = 80; + +// Forward declarations +void config_portal_start(); +void config_portal_stop(); +void config_portal_loop(); +bool config_portal_is_active(); +bool boundary_needs_config(); + +// ─── Common bandwidth values (Hz) ─────────────────────────────────────────── +// These match Reticulum standard channel plans +struct BwOption { uint32_t hz; const char* label; }; +static const BwOption BW_OPTIONS[] = { + { 7800, "7.8 kHz" }, + { 10400, "10.4 kHz" }, + { 15600, "15.6 kHz" }, + { 20800, "20.8 kHz" }, + { 31250, "31.25 kHz" }, + { 41700, "41.7 kHz" }, + { 62500, "62.5 kHz" }, + {125000, "125 kHz" }, + {250000, "250 kHz" }, + {500000, "500 kHz" }, +}; +static const int BW_OPTIONS_COUNT = sizeof(BW_OPTIONS) / sizeof(BW_OPTIONS[0]); + +// ─── HTML Page Generation ──────────────────────────────────────────────────── + +static void config_send_html() { + // Read current values from EEPROM/globals for pre-population + char cur_ssid[33] = ""; + char cur_psk[33] = ""; + + for (int i = 0; i < 32; i++) { + cur_ssid[i] = EEPROM.read(config_addr(ADDR_CONF_SSID + i)); + if (cur_ssid[i] == (char)0xFF) cur_ssid[i] = '\0'; + } + cur_ssid[32] = '\0'; + + for (int i = 0; i < 32; i++) { + cur_psk[i] = EEPROM.read(config_addr(ADDR_CONF_PSK + i)); + if (cur_psk[i] == (char)0xFF) cur_psk[i] = '\0'; + } + cur_psk[32] = '\0'; + + // Current LoRa values (from globals, which were loaded from EEPROM) + uint32_t cur_freq = lora_freq; + uint32_t cur_bw = lora_bw; + int cur_sf = lora_sf; + int cur_cr = lora_cr; + int cur_txp = lora_txp; + if (cur_txp == 0xFF) cur_txp = 28; // Default TX power + + // Default frequency if not set + if (cur_freq == 0) cur_freq = 914875000; // 914.875 MHz default + if (cur_bw == 0) cur_bw = 125000; // 125 kHz default + if (cur_sf == 0) cur_sf = 10; // SF10 default + if (cur_cr < 5 || cur_cr > 8) cur_cr = 5; // CR 4/5 default + + // Build the HTML page + String html = F( + "" + "" + "RNode Boundary Setup" + "" + "

📡 RNode Boundary Node

" + "
" + ); + + // ── WiFi STA Section ── + html += F( + "

📶 WiFi Network

" + "" + ""); + + html += F( + "" + "" + "" + ""); + + // ── TCP Backbone Section ── + html += F( + "

🌐 TCP Backbone

" + "" + ""); + + html += F(""); + html += F(""); + + html += F(""); + html += F(""); + + // ── Local TCP Server Section ── + html += F( + "

📡 Local TCP Server (optional)

" + "

Run a TCP server on the same WiFi network so local devices can connect. " + "Uses Access Point mode (does not forward announces).

" + "" + ""); + + html += F(""); + html += F(""); + + // ── LoRa Radio Section ── + html += F( + "

📻 LoRa Radio

" + ); + + // Frequency — show in MHz for human-friendliness + char freq_str[16]; + dtostrf((double)cur_freq / 1000000.0, 1, 3, freq_str); + html += F(""); + html += F(""); + html += F("

e.g. 914.875, 868.000, 433.000

"); + + // Bandwidth — dropdown + html += F(""); + + // Spreading Factor — dropdown 6-12 + html += F(""); + + // Coding Rate — dropdown 5-8 (maps to 4/5 through 4/8) + html += F(""); + + // TX Power + html += F(""); + html += F(""); + + #ifdef PA_MAX_OUTPUT + html += F("

Max output for this board: "); + html += String(PA_MAX_OUTPUT); + html += F(" dBm (with PA)

"); + #endif + + // ── Submit ── + html += F( + "" + "
" + ); + + config_server->send(200, "text/html", html); +} + +// ─── Handle POST /save ────────────────────────────────────────────────────── + +static void config_handle_save() { + // ── WiFi STA credentials ── + String ssid = config_server->arg("ssid"); + String psk = config_server->arg("psk"); + + // Write SSID to config EEPROM area + for (int i = 0; i < 32; i++) { + uint8_t c = (i < (int)ssid.length()) ? ssid[i] : 0x00; + EEPROM.write(config_addr(ADDR_CONF_SSID + i), c); + } + EEPROM.write(config_addr(ADDR_CONF_SSID + 32), 0x00); + + // Write PSK + for (int i = 0; i < 32; i++) { + uint8_t c = (i < (int)psk.length()) ? psk[i] : 0x00; + EEPROM.write(config_addr(ADDR_CONF_PSK + i), c); + } + EEPROM.write(config_addr(ADDR_CONF_PSK + 32), 0x00); + + // Set WiFi mode to STA + EEPROM.write(eeprom_addr(ADDR_CONF_WIFI), WR_WIFI_STA); + + // ── WiFi enable setting ── + boundary_state.wifi_enabled = (config_server->arg("wifi_en").toInt() == 1); + + // ── TCP backbone settings ── + boundary_state.tcp_mode = (uint8_t)config_server->arg("tcp_mode").toInt(); // 0=disabled, 1=client + if (boundary_state.tcp_mode > 1) boundary_state.tcp_mode = 0; + boundary_state.tcp_port = (uint16_t)config_server->arg("tcp_port").toInt(); + if (boundary_state.tcp_port == 0) boundary_state.tcp_port = 4242; + + String bb_host = config_server->arg("bb_host"); + memset(boundary_state.backbone_host, 0, sizeof(boundary_state.backbone_host)); + strncpy(boundary_state.backbone_host, bb_host.c_str(), sizeof(boundary_state.backbone_host) - 1); + + boundary_state.backbone_port = (uint16_t)config_server->arg("bb_port").toInt(); + if (boundary_state.backbone_port == 0) boundary_state.backbone_port = 4242; + + // ── Local TCP server settings ── + boundary_state.ap_tcp_enabled = (config_server->arg("ap_tcp_en").toInt() == 1); + boundary_state.ap_tcp_port = (uint16_t)config_server->arg("ap_tcp_port").toInt(); + if (boundary_state.ap_tcp_port == 0) boundary_state.ap_tcp_port = 4242; + + // Save boundary config to EEPROM + boundary_save_config(); + + // ── LoRa radio settings ── + String freq_str = config_server->arg("freq"); + double freq_mhz = freq_str.toDouble(); + if (freq_mhz > 0) { + lora_freq = (uint32_t)(freq_mhz * 1000000.0); + } + + String bw_str = config_server->arg("bw"); + uint32_t bw_val = (uint32_t)bw_str.toInt(); + if (bw_val > 0) lora_bw = bw_val; + + int sf_val = config_server->arg("sf").toInt(); + if (sf_val >= 6 && sf_val <= 12) lora_sf = sf_val; + + int cr_val = config_server->arg("cr").toInt(); + if (cr_val >= 5 && cr_val <= 8) lora_cr = cr_val; + + int txp_val = config_server->arg("txp").toInt(); + if (txp_val >= 2 && txp_val <= 30) lora_txp = txp_val; + + // Save LoRa config to EEPROM (reuse existing eeprom_conf functions) + // Write directly since hw_ready may not be set yet + eeprom_update(eeprom_addr(ADDR_CONF_SF), lora_sf); + eeprom_update(eeprom_addr(ADDR_CONF_CR), lora_cr); + eeprom_update(eeprom_addr(ADDR_CONF_TXP), lora_txp); + eeprom_update(eeprom_addr(ADDR_CONF_BW) + 0, lora_bw >> 24); + eeprom_update(eeprom_addr(ADDR_CONF_BW) + 1, lora_bw >> 16); + eeprom_update(eeprom_addr(ADDR_CONF_BW) + 2, lora_bw >> 8); + eeprom_update(eeprom_addr(ADDR_CONF_BW) + 3, lora_bw); + eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 0, lora_freq >> 24); + eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 1, lora_freq >> 16); + eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 2, lora_freq >> 8); + eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 3, lora_freq); + eeprom_update(eeprom_addr(ADDR_CONF_OK), CONF_OK_BYTE); + + EEPROM.commit(); + + // ── Send confirmation page ── + String ok = F( + "" + "" + "Saved" + "" + "
" + "

✅ Configuration Saved

" + "

Device will reboot in 3 seconds and connect to your WiFi network.

" + "

If the device cannot connect, hold the button for 5+ seconds to re-enter setup.

" + "
" + ); + config_server->send(200, "text/html", ok); + + // Give the response time to send + delay(3000); + + // Reboot + ESP.restart(); +} + +// ─── Captive Portal redirect ───────────────────────────────────────────────── +static void config_handle_redirect() { + config_server->sendHeader("Location", "http://10.0.0.1/", true); + config_server->send(302, "text/plain", "Redirecting to setup..."); +} + +// ─── Check if config is needed ─────────────────────────────────────────────── +bool boundary_needs_config() { + // Check if WiFi SSID is configured + char ssid[33]; + for (int i = 0; i < 32; i++) { + ssid[i] = EEPROM.read(config_addr(ADDR_CONF_SSID + i)); + if (ssid[i] == (char)0xFF) ssid[i] = '\0'; + } + ssid[32] = '\0'; + + // Also check boundary mode enable flag + uint8_t bmode = EEPROM.read(config_addr(ADDR_CONF_BMODE)); + + // Need config if no SSID set and boundary not yet configured + if (ssid[0] == '\0' && bmode != BOUNDARY_ENABLE_BYTE) { + return true; + } + return false; +} + +// ─── Start Config Portal ───────────────────────────────────────────────────── +void config_portal_start() { + if (config_portal_active) return; + + Serial.println("[Config] Starting configuration portal..."); + + // Stop any existing WiFi + WiFi.softAPdisconnect(true); + WiFi.disconnect(true, true); + WiFi.mode(WIFI_MODE_NULL); + delay(100); + + // Start AP + WiFi.mode(WIFI_AP); + WiFi.softAP(CONFIG_AP_SSID, NULL); // Open AP for easy setup + delay(150); + + IPAddress ap_addr(10, 0, 0, 1); + IPAddress ap_mask(255, 255, 255, 0); + WiFi.softAPConfig(ap_addr, ap_addr, ap_mask); + + Serial.print("[Config] AP started: "); + Serial.println(CONFIG_AP_SSID); + Serial.print("[Config] IP: "); + Serial.println(WiFi.softAPIP()); + + // Start DNS server for captive portal (redirect all domains to us) + config_dns = new DNSServer(); + config_dns->start(DNS_PORT, "*", ap_addr); + + // Start web server + config_server = new WebServer(HTTP_PORT); + config_server->on("/", HTTP_GET, config_send_html); + config_server->on("/save", HTTP_POST, config_handle_save); + config_server->onNotFound(config_handle_redirect); // Captive portal catch-all + config_server->begin(); + + config_portal_active = true; + + Serial.println("[Config] Portal ready — connect to WiFi: " + String(CONFIG_AP_SSID)); + + #if HAS_DISPLAY + if (disp_ready) { + // Show config mode on display + stat_area.fillScreen(SSD1306_BLACK); + stat_area.setCursor(0, 0); + stat_area.println("CONFIG MODE"); + stat_area.println(""); + stat_area.println("Connect to:"); + stat_area.println(CONFIG_AP_SSID); + stat_area.println(""); + stat_area.println("Open browser"); + stat_area.println("http://10.0.0.1"); + display.clearDisplay(); + display.drawBitmap(0, 0, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK); + display.display(); + } + #endif +} + +// ─── Stop Config Portal ────────────────────────────────────────────────────── +void config_portal_stop() { + if (!config_portal_active) return; + + Serial.println("[Config] Stopping configuration portal"); + + if (config_server) { + config_server->stop(); + delete config_server; + config_server = nullptr; + } + if (config_dns) { + config_dns->stop(); + delete config_dns; + config_dns = nullptr; + } + + WiFi.softAPdisconnect(true); + WiFi.mode(WIFI_MODE_NULL); + config_portal_active = false; +} + +// ─── Portal Loop — call from main loop() ───────────────────────────────────── +void config_portal_loop() { + if (!config_portal_active) return; + if (config_dns) config_dns->processNextRequest(); + if (config_server) config_server->handleClient(); +} + +// ─── Is portal active? ────────────────────────────────────────────────────── +bool config_portal_is_active() { + return config_portal_active; +} + +#endif // BOUNDARY_MODE +#endif // BOUNDARY_CONFIG_H diff --git a/BoundaryMode.h b/BoundaryMode.h new file mode 100644 index 0000000..d7c97c3 --- /dev/null +++ b/BoundaryMode.h @@ -0,0 +1,225 @@ +// Copyright (C) 2026, Boundary Mode Extension +// Based on microReticulum_Firmware by Mark Qvist +// +// BoundaryMode.h — Configuration and runtime state for the Boundary Mode +// firmware variant. This header defines the WiFi backbone connection +// parameters and boundary-specific operational settings. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +#ifndef BOUNDARY_MODE_H +#define BOUNDARY_MODE_H + +#ifdef BOUNDARY_MODE + +// ─── Boundary Mode Configuration ──────────────────────────────────────────── +// +// The boundary node operates with TWO RNS interfaces: +// +// 1. LoRaInterface (MODE_GATEWAY) — radio side, handles LoRa mesh +// 2. TcpInterface (MODE_BOUNDARY) — WiFi side, connects to TCP backbone +// +// RNS Transport is ALWAYS enabled in boundary mode. +// Packets received on either interface are routed through Transport +// to the other interface based on path table lookups and announce rules. + +// ─── WiFi Backbone Connection ──────────────────────────────────────────────── +// These can be overridden via build flags or EEPROM at runtime. + +// Default backbone server to connect to (client mode) +// Set to empty string "" if operating in server mode +#ifndef BOUNDARY_BACKBONE_HOST +#define BOUNDARY_BACKBONE_HOST "" +#endif + +#ifndef BOUNDARY_BACKBONE_PORT +#define BOUNDARY_BACKBONE_PORT 4242 +#endif + +// TCP interface mode: 0 = disabled, 1 = client (connect out) +#ifndef BOUNDARY_TCP_MODE +#define BOUNDARY_TCP_MODE 1 +#endif + +// TCP server listen port (when in server mode) +#ifndef BOUNDARY_TCP_PORT +#define BOUNDARY_TCP_PORT 4242 +#endif + +// ─── EEPROM Extension Addresses ────────────────────────────────────────────── +// We use the CONFIG area (config_addr) for additional boundary mode settings. +// These are after the existing WiFi SSID/PSK/IP/NM fields. +// Existing layout: +// 0x00-0x20: SSID (33 bytes) +// 0x21-0x41: PSK (33 bytes) +// 0x42-0x45: IP (4 bytes) +// 0x46-0x49: NM (4 bytes) +// Our additions (config_addr space, 0x4A onwards): +#define ADDR_CONF_BMODE 0x4A // Boundary mode enabled flag (1 byte, 0x73 = enabled) +#define ADDR_CONF_BTCP_MODE 0x4B // TCP mode: 0=server, 1=client (1 byte) +#define ADDR_CONF_BTCP_PORT 0x4C // TCP port (2 bytes, big-endian) +#define ADDR_CONF_BHOST 0x4E // Backbone host (64 bytes, null-terminated) +#define ADDR_CONF_BHPORT 0x8E // Backbone target port (2 bytes, big-endian) +#define ADDR_CONF_AP_TCP_EN 0x90 // AP TCP server enable (1 byte, 0x73 = enabled) +#define ADDR_CONF_AP_TCP_PORT 0x91 // AP TCP server port (2 bytes, big-endian) +#define ADDR_CONF_AP_SSID 0x93 // AP SSID (33 bytes, null-terminated) +#define ADDR_CONF_AP_PSK 0xB4 // AP PSK (33 bytes, null-terminated) +#define ADDR_CONF_WIFI_EN 0xD5 // WiFi enable flag (1 byte, 0x73 = enabled) +// Total: 0xD6 (214 bytes used of 256 CONFIG area) + +#define BOUNDARY_ENABLE_BYTE 0x73 + +// ─── Boundary Mode Runtime State ───────────────────────────────────────────── +#ifndef BOUNDARY_STATE_DEFINED +#define BOUNDARY_STATE_DEFINED +struct BoundaryState { + bool enabled; + bool wifi_enabled; // false = LoRa-only repeater (no WiFi) + uint8_t tcp_mode; // 0=disabled, 1=client + uint16_t tcp_port; // Local port (client outbound) + char backbone_host[64]; + uint16_t backbone_port; // Target port for client mode + + // AP TCP server settings + bool ap_tcp_enabled; // Whether to run a WiFi AP with TCP server + uint16_t ap_tcp_port; // Port for the AP TCP server + char ap_ssid[33]; // AP SSID + char ap_psk[33]; // AP PSK (empty = open) + + // Runtime state + bool wifi_connected; + bool tcp_connected; // Backbone (WAN) connected + bool ap_tcp_connected; // Local TCP server (LAN) has client + bool ap_active; + uint32_t packets_bridged_lora_to_tcp; + uint32_t packets_bridged_tcp_to_lora; + uint32_t last_bridge_activity; +}; +#endif // BOUNDARY_STATE_DEFINED + +// Global boundary state instance (defined in RNode_Firmware.ino) +extern BoundaryState boundary_state; + +// ─── Boundary Mode EEPROM Load/Save ───────────────────────────────────────── + +inline void boundary_load_config() { + // Check if boundary mode is configured + uint8_t bmode = EEPROM.read(config_addr(ADDR_CONF_BMODE)); + boundary_state.enabled = (bmode == BOUNDARY_ENABLE_BYTE); + + if (!boundary_state.enabled) { + // Use compile-time defaults + boundary_state.wifi_enabled = true; + boundary_state.tcp_mode = BOUNDARY_TCP_MODE; + boundary_state.tcp_port = BOUNDARY_TCP_PORT; + strncpy(boundary_state.backbone_host, BOUNDARY_BACKBONE_HOST, + sizeof(boundary_state.backbone_host) - 1); + boundary_state.backbone_host[sizeof(boundary_state.backbone_host) - 1] = '\0'; + boundary_state.backbone_port = BOUNDARY_BACKBONE_PORT; + boundary_state.ap_tcp_enabled = false; + boundary_state.ap_tcp_port = 4242; + boundary_state.ap_ssid[0] = '\0'; + boundary_state.ap_psk[0] = '\0'; + // Mark as enabled since we're compiled with BOUNDARY_MODE + boundary_state.enabled = true; + return; + } + + // Load wifi enable flag (default to enabled if unprogrammed 0xFF) + uint8_t wifi_en_byte = EEPROM.read(config_addr(ADDR_CONF_WIFI_EN)); + boundary_state.wifi_enabled = (wifi_en_byte == BOUNDARY_ENABLE_BYTE || wifi_en_byte == 0xFF); + + // Load from EEPROM + boundary_state.tcp_mode = EEPROM.read(config_addr(ADDR_CONF_BTCP_MODE)); + if (boundary_state.tcp_mode > 1) boundary_state.tcp_mode = 0; // 0=disabled, 1=client + + boundary_state.tcp_port = + ((uint16_t)EEPROM.read(config_addr(ADDR_CONF_BTCP_PORT)) << 8) | + (uint16_t)EEPROM.read(config_addr(ADDR_CONF_BTCP_PORT + 1)); + if (boundary_state.tcp_port == 0 || boundary_state.tcp_port == 0xFFFF) { + boundary_state.tcp_port = BOUNDARY_TCP_PORT; + } + + for (int i = 0; i < 63; i++) { + boundary_state.backbone_host[i] = EEPROM.read(config_addr(ADDR_CONF_BHOST + i)); + if (boundary_state.backbone_host[i] == 0xFF) { + boundary_state.backbone_host[i] = '\0'; + } + } + boundary_state.backbone_host[63] = '\0'; + + boundary_state.backbone_port = + ((uint16_t)EEPROM.read(config_addr(ADDR_CONF_BHPORT)) << 8) | + (uint16_t)EEPROM.read(config_addr(ADDR_CONF_BHPORT + 1)); + if (boundary_state.backbone_port == 0 || boundary_state.backbone_port == 0xFFFF) { + boundary_state.backbone_port = BOUNDARY_BACKBONE_PORT; + } + + // Load AP TCP server settings + boundary_state.ap_tcp_enabled = + (EEPROM.read(config_addr(ADDR_CONF_AP_TCP_EN)) == BOUNDARY_ENABLE_BYTE); + + boundary_state.ap_tcp_port = + ((uint16_t)EEPROM.read(config_addr(ADDR_CONF_AP_TCP_PORT)) << 8) | + (uint16_t)EEPROM.read(config_addr(ADDR_CONF_AP_TCP_PORT + 1)); + if (boundary_state.ap_tcp_port == 0 || boundary_state.ap_tcp_port == 0xFFFF) { + boundary_state.ap_tcp_port = 4242; + } + + for (int i = 0; i < 32; i++) { + boundary_state.ap_ssid[i] = EEPROM.read(config_addr(ADDR_CONF_AP_SSID + i)); + if (boundary_state.ap_ssid[i] == (char)0xFF) boundary_state.ap_ssid[i] = '\0'; + } + boundary_state.ap_ssid[32] = '\0'; + + for (int i = 0; i < 32; i++) { + boundary_state.ap_psk[i] = EEPROM.read(config_addr(ADDR_CONF_AP_PSK + i)); + if (boundary_state.ap_psk[i] == (char)0xFF) boundary_state.ap_psk[i] = '\0'; + } + boundary_state.ap_psk[32] = '\0'; + + // Reset runtime state + boundary_state.packets_bridged_lora_to_tcp = 0; + boundary_state.packets_bridged_tcp_to_lora = 0; + boundary_state.last_bridge_activity = 0; + boundary_state.wifi_connected = false; + boundary_state.tcp_connected = false; + boundary_state.ap_active = false; +} + +inline void boundary_save_config() { + EEPROM.write(config_addr(ADDR_CONF_BMODE), BOUNDARY_ENABLE_BYTE); + EEPROM.write(config_addr(ADDR_CONF_WIFI_EN), + boundary_state.wifi_enabled ? BOUNDARY_ENABLE_BYTE : 0x00); + EEPROM.write(config_addr(ADDR_CONF_BTCP_MODE), boundary_state.tcp_mode); + EEPROM.write(config_addr(ADDR_CONF_BTCP_PORT), (boundary_state.tcp_port >> 8) & 0xFF); + EEPROM.write(config_addr(ADDR_CONF_BTCP_PORT + 1), boundary_state.tcp_port & 0xFF); + for (int i = 0; i < 63; i++) { + EEPROM.write(config_addr(ADDR_CONF_BHOST + i), boundary_state.backbone_host[i]); + } + EEPROM.write(config_addr(ADDR_CONF_BHOST + 63), 0x00); + EEPROM.write(config_addr(ADDR_CONF_BHPORT), (boundary_state.backbone_port >> 8) & 0xFF); + EEPROM.write(config_addr(ADDR_CONF_BHPORT + 1), boundary_state.backbone_port & 0xFF); + + // AP TCP server settings + EEPROM.write(config_addr(ADDR_CONF_AP_TCP_EN), + boundary_state.ap_tcp_enabled ? BOUNDARY_ENABLE_BYTE : 0x00); + EEPROM.write(config_addr(ADDR_CONF_AP_TCP_PORT), (boundary_state.ap_tcp_port >> 8) & 0xFF); + EEPROM.write(config_addr(ADDR_CONF_AP_TCP_PORT + 1), boundary_state.ap_tcp_port & 0xFF); + for (int i = 0; i < 32; i++) { + EEPROM.write(config_addr(ADDR_CONF_AP_SSID + i), boundary_state.ap_ssid[i]); + } + EEPROM.write(config_addr(ADDR_CONF_AP_SSID + 32), 0x00); + for (int i = 0; i < 32; i++) { + EEPROM.write(config_addr(ADDR_CONF_AP_PSK + i), boundary_state.ap_psk[i]); + } + EEPROM.write(config_addr(ADDR_CONF_AP_PSK + 32), 0x00); + + EEPROM.commit(); +} + +#endif // BOUNDARY_MODE +#endif // BOUNDARY_MODE_H diff --git a/Builds/Handheld RNode/Case_Battery_Door.stl b/Builds/Handheld RNode/Case_Battery_Door.stl new file mode 100755 index 0000000..a162035 Binary files /dev/null and b/Builds/Handheld RNode/Case_Battery_Door.stl differ diff --git a/Builds/Handheld RNode/Case_Bottom_Large_Battery.stl b/Builds/Handheld RNode/Case_Bottom_Large_Battery.stl new file mode 100755 index 0000000..9f235ca Binary files /dev/null and b/Builds/Handheld RNode/Case_Bottom_Large_Battery.stl differ diff --git a/Builds/Handheld RNode/Case_Bottom_No_Battery.stl b/Builds/Handheld RNode/Case_Bottom_No_Battery.stl new file mode 100755 index 0000000..b994b08 Binary files /dev/null and b/Builds/Handheld RNode/Case_Bottom_No_Battery.stl differ diff --git a/Builds/Handheld RNode/Case_Bottom_Small_Battery.stl b/Builds/Handheld RNode/Case_Bottom_Small_Battery.stl new file mode 100755 index 0000000..2cffacc Binary files /dev/null and b/Builds/Handheld RNode/Case_Bottom_Small_Battery.stl differ diff --git a/Builds/Handheld RNode/Case_Top.stl b/Builds/Handheld RNode/Case_Top.stl new file mode 100755 index 0000000..011f4e7 Binary files /dev/null and b/Builds/Handheld RNode/Case_Top.stl differ diff --git a/Builds/Handheld RNode/Handheld_RNode_Recipe.pdf b/Builds/Handheld RNode/Handheld_RNode_Recipe.pdf new file mode 100755 index 0000000..a1059d3 Binary files /dev/null and b/Builds/Handheld RNode/Handheld_RNode_Recipe.pdf differ diff --git a/Builds/Handheld RNode/LED_Guide.stl b/Builds/Handheld RNode/LED_Guide.stl new file mode 100755 index 0000000..c74a3af Binary files /dev/null and b/Builds/Handheld RNode/LED_Guide.stl differ diff --git a/Builds/Handheld RNode/LED_Window.stl b/Builds/Handheld RNode/LED_Window.stl new file mode 100755 index 0000000..58149da Binary files /dev/null and b/Builds/Handheld RNode/LED_Window.stl differ diff --git a/Builds/Handheld RNode/Power_Switch.stl b/Builds/Handheld RNode/Power_Switch.stl new file mode 100755 index 0000000..b71bd22 Binary files /dev/null and b/Builds/Handheld RNode/Power_Switch.stl differ diff --git a/Config.h b/Config.h new file mode 100755 index 0000000..dec0063 --- /dev/null +++ b/Config.h @@ -0,0 +1,242 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "ROM.h" +#include "Boards.h" + +#ifndef CONFIG_H + #define CONFIG_H + + #define MAJ_VERS 0x01 + #define MIN_VERS 0x55 + + #define MODE_HOST 0x11 + #define MODE_TNC 0x12 + + #define CABLE_STATE_DISCONNECTED 0x00 + #define CABLE_STATE_CONNECTED 0x01 + uint8_t cable_state = CABLE_STATE_DISCONNECTED; + + #define BT_STATE_NA 0xff + #define BT_STATE_OFF 0x00 + #define BT_STATE_ON 0x01 + #define BT_STATE_PAIRING 0x02 + #define BT_STATE_CONNECTED 0x03 + uint8_t bt_state = BT_STATE_NA; + uint32_t bt_ssp_pin = 0; + bool bt_ready = false; + bool bt_enabled = false; + bool bt_allow_pairing = false; + + #define WR_CHANNEL_DEFAULT 1 + #define WR_WIFI_OFF 0x00 + #define WR_WIFI_STA 0x01 + #define WR_WIFI_AP 0x02 + #define WR_STATE_NA 0xff + #define WR_STATE_OFF 0x00 + #define WR_STATE_ON 0x01 + #define WR_STATE_CONNECTED 0x02 + uint8_t wr_state = WR_STATE_OFF; + uint8_t wr_channel = WR_CHANNEL_DEFAULT; + + #define M_FRQ_S 27388122 + #define M_FRQ_R 27388061 + bool console_active = false; + bool modem_installed = false; + + #define MTU 508 + #define SINGLE_MTU 255 + #define HEADER_L 1 + #define MIN_L 1 + #define CMD_L 64 + + bool mw_radio_online = false; + + #define eeprom_addr(a) (a+EEPROM_OFFSET) + #define config_addr(a) (a+CONFIG_OFFSET) + + #if (MODEM == SX1262 || MODEM == SX1280) && defined(NRF52840_XXAA) + SPIClass spiModem(NRF_SPIM2, pin_miso, pin_sclk, pin_mosi); + #endif + + // MCU independent configuration parameters + const long serial_baudrate = 115200; + + // SX1276 RSSI offset to get dBm value from + // packet RSSI register + const int rssi_offset = 157; + + // Default LoRa settings + #define PHY_HEADER_LORA_SYMBOLS 20 + #define PHY_CRC_LORA_BITS 16 + #define LORA_PREAMBLE_SYMBOLS_MIN 18 + #define LORA_PREAMBLE_TARGET_MS 24 + #define LORA_PREAMBLE_FAST_DELTA 18 + #define LORA_FAST_THRESHOLD_BPS 30E3 + #define LORA_LIMIT_THRESHOLD_BPS 60E3 + #define LORA_GUARD_THRESHOLD_BPS 14E3 + #define LORA_FAST_GUARD_MS 48 + long lora_preamble_symbols = LORA_PREAMBLE_SYMBOLS_MIN; + long lora_preamble_time_ms = 0; + long lora_header_time_ms = 0; + float lora_symbol_time_ms = 0.0; + float lora_symbol_rate = 0.0; + float lora_us_per_byte = 0.0; + bool lora_low_datarate = false; + bool lora_limit_rate = false; + bool lora_guard_rate = false; + + // CSMA Parameters + #define CSMA_SIFS_MS 0 + #define CSMA_POST_TX_YIELD_SLOTS 3 + #define CSMA_SLOT_MAX_MS 100 + #define CSMA_SLOT_MIN_MS 24 + #define CSMA_SLOT_MIN_FAST_DELTA 18 + #define CSMA_SLOT_SYMBOLS 12 + #define CSMA_CW_BANDS 4 + #define CSMA_CW_MIN 0 + #define CSMA_CW_PER_BAND_WINDOWS 15 + #define CSMA_BAND_1_MAX_AIRTIME 7 + #define CSMA_BAND_N_MIN_AIRTIME 85 + #define CSMA_INFR_THRESHOLD_DB 11 + #define CSMA_RFENV_RECAL_MS 2500 + #define CSMA_RFENV_RECAL_LIMIT_DB -83 + bool interference_detected = false; + bool avoid_interference = true; + int csma_slot_ms = CSMA_SLOT_MIN_MS; + unsigned long difs_ms = CSMA_SIFS_MS + 2*csma_slot_ms; + unsigned long difs_wait_start = -1; + unsigned long cw_wait_start = -1; + unsigned long cw_wait_target = -1; + unsigned long cw_wait_passed = 0; + int csma_cw = -1; + uint8_t cw_band = 1; + uint8_t cw_min = 0; + uint8_t cw_max = CSMA_CW_PER_BAND_WINDOWS; + + // LoRa settings + int lora_sf = 0; + int lora_cr = 5; + int lora_txp = 0xFF; + uint32_t lora_bw = 0; + uint32_t lora_freq = 0; + uint32_t lora_bitrate = 0; + + // Operational variables + bool radio_locked = true; + bool radio_online = false; + bool community_fw = true; + bool hw_ready = false; + bool radio_error = false; + bool disp_ready = false; + bool pmu_ready = false; + bool promisc = false; + bool implicit = false; + bool memory_low = false; + uint8_t implicit_l = 0; + + uint8_t op_mode = MODE_HOST; + uint8_t model = 0x00; + uint8_t hwrev = 0x00; + + #define NOISE_FLOOR_SAMPLES 128 + int noise_floor = -292; + int current_rssi = -292; + int last_rssi = -292; + uint8_t last_rssi_raw = 0x00; + uint8_t last_snr_raw = 0x80; + uint8_t seq = 0xFF; + uint16_t read_len = 0; + uint16_t host_write_len = 0; + + // Incoming packet buffer + uint8_t pbuf[MTU]; + + // KISS command buffer + uint8_t cmdbuf[CMD_L]; + + // LoRa transmit buffer + uint8_t tbuf[MTU]; + + uint32_t stat_rx = 0; + uint32_t stat_tx = 0; + + #define STATUS_INTERVAL_MS 3 + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + #define DCD_SAMPLES 2500 + #define UTIL_UPDATE_INTERVAL_MS 1000 + #define UTIL_UPDATE_INTERVAL (UTIL_UPDATE_INTERVAL_MS/STATUS_INTERVAL_MS) + #define AIRTIME_LONGTERM 3600 + #define AIRTIME_LONGTERM_MS (AIRTIME_LONGTERM*1000) + #define AIRTIME_BINLEN_MS (STATUS_INTERVAL_MS*DCD_SAMPLES) + #define AIRTIME_BINS ((AIRTIME_LONGTERM*1000)/AIRTIME_BINLEN_MS) + bool util_samples[DCD_SAMPLES]; + uint16_t airtime_bins[AIRTIME_BINS]; + float longterm_bins[AIRTIME_BINS]; + int dcd_sample = 0; + float local_channel_util = 0.0; + float total_channel_util = 0.0; + float longterm_channel_util = 0.0; + float airtime = 0.0; + float longterm_airtime = 0.0; + #define current_airtime_bin(void) (millis()%AIRTIME_LONGTERM_MS)/AIRTIME_BINLEN_MS + #endif + float st_airtime_limit = 0.0; + float lt_airtime_limit = 0.0; + bool airtime_lock = false; + + bool stat_signal_detected = false; + bool stat_signal_synced = false; + bool stat_rx_ongoing = false; + bool dcd = false; + bool dcd_led = false; + bool dcd_waiting = false; + long dcd_wait_until = 0; + uint16_t dcd_count = 0; + uint16_t dcd_threshold = 2; + + uint32_t status_interval_ms = STATUS_INTERVAL_MS; + uint32_t last_status_update = 0; + uint32_t last_dcd = 0; + + // Power management + #define BATTERY_STATE_UNKNOWN 0x00 + #define BATTERY_STATE_DISCHARGING 0x01 + #define BATTERY_STATE_CHARGING 0x02 + #define BATTERY_STATE_CHARGED 0x03 + bool battery_installed = false; + bool battery_indeterminate = false; + bool external_power = false; + bool battery_ready = false; + float battery_voltage = 0.0; + float battery_percent = 0.0; + uint8_t battery_state = 0x00; + uint8_t display_intensity = 0xFF; + uint8_t display_addr = 0xFF; + volatile bool display_updating = false; + bool display_blanking_enabled = false; + bool display_diagnostics = true; + bool device_init_done = false; + bool eeprom_ok = false; + bool firmware_update_mode = false; + bool serial_in_frame = false; + + // Boot flags + #define START_FROM_BOOTLOADER 0x01 + #define START_FROM_POWERON 0x02 + #define START_FROM_BROWNOUT 0x03 + #define START_FROM_JTAG 0x04 + +#endif diff --git a/Console.h b/Console.h new file mode 100755 index 0000000..c59d348 --- /dev/null +++ b/Console.h @@ -0,0 +1,203 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include +#include +#include + +#include "SD.h" +#include "SPI.h" + +#if HAS_SD + SPIClass *spi = NULL; +#endif + + +#if CONFIG_IDF_TARGET_ESP32 +#include "esp32/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32S2 +#include "esp32s2/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32C3 +#include "esp32c3/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32S3 +#include "esp32s3/rom/rtc.h" +#else +#error Target CONFIG_IDF_TARGET is not supported +#endif + +WebServer server(80); + +void console_dbg(String msg) { + Serial.print("[Webserver] "); + Serial.println(msg); +} + +bool exists(String path){ + bool yes = false; + File file = SPIFFS.open(path, "r"); + if(!file.isDirectory()){ + yes = true; + } + file.close(); + return yes; +} + +String console_get_content_type(String filename) { + if (server.hasArg("download")) { + return "application/octet-stream"; + } else if (filename.endsWith(".htm")) { + return "text/html"; + } else if (filename.endsWith(".html")) { + return "text/html"; + } else if (filename.endsWith(".css")) { + return "text/css"; + } else if (filename.endsWith(".js")) { + return "application/javascript"; + } else if (filename.endsWith(".png")) { + return "image/png"; + } else if (filename.endsWith(".gif")) { + return "image/gif"; + } else if (filename.endsWith(".jpg")) { + return "image/jpeg"; + } else if (filename.endsWith(".ico")) { + return "image/x-icon"; + } else if (filename.endsWith(".xml")) { + return "text/xml"; + } else if (filename.endsWith(".pdf")) { + return "application/x-pdf"; + } else if (filename.endsWith(".zip")) { + return "application/x-zip"; + } else if (filename.endsWith(".gz")) { + return "application/x-gzip"; + } else if (filename.endsWith(".whl")) { + return "application/octet-stream"; + } + return "text/plain"; +} + +bool console_serve_file(String path) { + console_dbg("Request for: "+path); + if (path.endsWith("/")) { + path += "index.html"; + } + + if (path == "/r/manual/index.html") { + path = "/m.html"; + } + if (path == "/r/manual/Reticulum Manual.pdf") { + path = "/h.html"; + } + + + String content_type = console_get_content_type(path); + String pathWithGz = path + ".gz"; + if (exists(pathWithGz) || exists(path)) { + if (exists(pathWithGz)) { + path += ".gz"; + } + + File file = SPIFFS.open(path, "r"); + console_dbg("Serving file to client"); + server.streamFile(file, content_type); + file.close(); + + console_dbg("File serving done\n"); + return true; + } else { + int spos = pathWithGz.lastIndexOf('/'); + if (spos > 0) { + String remap_path = "/d"; + remap_path.concat(pathWithGz.substring(spos)); + Serial.println(remap_path); + + if (exists(remap_path)) { + File file = SPIFFS.open(remap_path, "r"); + console_dbg("Serving remapped file to client"); + server.streamFile(file, content_type); + console_dbg("Closing file"); + file.close(); + + console_dbg("File serving done\n"); + return true; + } + } + } + + console_dbg("Error: Could not open file for serving\n"); + return false; +} + +void console_register_pages() { + server.onNotFound([]() { + if (!console_serve_file(server.uri())) { + server.send(404, "text/plain", "Not Found"); + } + }); +} + +void console_start() { + Serial.println(""); + console_dbg("Starting Access Point..."); + WiFi.softAP(bt_devname); + delay(150); + IPAddress ip(10, 0, 0, 1); + IPAddress nm(255, 255, 255, 0); + WiFi.softAPConfig(ip, ip, nm); + + if(!SPIFFS.begin(true)){ + console_dbg("Error: Could not mount SPIFFS"); + return; + } else { + console_dbg("SPIFFS Ready"); + } + + #if HAS_SD + spi = new SPIClass(HSPI); + spi->begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS); + if(!SD.begin(SD_CS, *spi)){ + console_dbg("No SD card inserted"); + } else { + uint8_t cardType = SD.cardType(); + if(cardType == CARD_NONE){ + console_dbg("No SD card type"); + } else { + console_dbg("SD Card Type: "); + if(cardType == CARD_MMC){ + console_dbg("MMC"); + } else if(cardType == CARD_SD){ + console_dbg("SDSC"); + } else if(cardType == CARD_SDHC){ + console_dbg("SDHC"); + } else { + console_dbg("UNKNOWN"); + } + uint64_t cardSize = SD.cardSize() / (1024 * 1024); + Serial.printf("SD Card Size: %lluMB\n", cardSize); + } + } + #endif + + console_register_pages(); + server.begin(); + led_indicate_console(); +} + +void console_loop(){ + server.handleClient(); + // Internally, this yields the thread and allows + // other tasks to run. + delay(2); +} \ No newline at end of file diff --git a/Console/Makefile b/Console/Makefile new file mode 100755 index 0000000..f5a674a --- /dev/null +++ b/Console/Makefile @@ -0,0 +1,47 @@ +PATH_RETICULUM_WEBSITE=../../sites/reticulum.network +PATH_PACKAGES=../../dist_archive + +clean: + @echo Cleaning... + @-rm -rf ./build + +dirs: + @mkdir -p ./build + @mkdir -p ./build/3d + @mkdir -p ./build/pkg + @mkdir -p ./build/css + @mkdir -p ./build/gfx + @mkdir -p ./build/images + +pages: + python ./build.py + +pages-debug: + python ./build.py --no-gz --no-remap + +sourcepack: + @echo Packing firmware sources... + zip --junk-paths -r build/pkg/rnode_firmware.zip ../arduino-cli.yaml ../BLESerial.cpp ../BLESerial.h ../Bluetooth.h ../Boards.h ../Config.h ../Console.h ../Device.h ../Display.h ../Framing.h ../Graphics.h ../LICENSE ../Makefile ../MD5.cpp ../MD5.h ../partition_hashes ../Power.h ../README.md ../release_hashes.py ../RNode_Firmware.ino ../ROM.h ../sx126x.cpp ../sx126x.h ../sx127x.cpp ../sx127x.h ../sx128x.cpp ../sx128x.h ../Utilities.h ../esp32_btbufs.py + +data: + @echo Including assets... + @cp assets/css/* build/css/ + @cp assets/gfx/* build/gfx/ + @cp assets/images/* build/images/ + @cp assets/stl/* build/3d/ + #@cp assets/pkg/* build/pkg/ + # @cp assets/scripts/* build/scripts/ + # @cp -r ../../Reticulum/docs/manual/* build/reticulum_manual/ + # @cp -r ../../Reticulum/docs/Reticulum\ Manual.pdf build/reticulum_manual/ + +external: + make -C $(PATH_RETICULUM_WEBSITE) clean website + -rm -r $(PATH_PACKAGES)/reticulum.network + cp -r $(PATH_RETICULUM_WEBSITE)/build $(PATH_PACKAGES)/reticulum.network + +site: clean external dirs data sourcepack pages + +local: clean external dirs data sourcepack pages-debug + +serve: + python -m http.server 7777 --bind 127.0.0.1 --directory ./build diff --git a/Console/assets/css/water.css b/Console/assets/css/water.css new file mode 100755 index 0000000..d8bd13e --- /dev/null +++ b/Console/assets/css/water.css @@ -0,0 +1,1009 @@ +:root { + --background-body: #2a2a2f; + --background: #161f27; + --background-alt: #1a242f; + --selection: #1c76c5; + --text-main: #c0c6cc; + --text-bright: #dbe0e3; + /*--text-bright: #fff;*/ + --text-muted: #a9b1ba; + --links: #7eb7e1; + --focus: #0096bfab; + --border: #526980; + --code: #ffbe85; + --animation-duration: 0.1s; + --button-base: #0c151c; + --button-hover: #040a0f; + --scrollbar-thumb: var(--button-hover); + --scrollbar-thumb-hover: rgb(0, 0, 0); + --form-placeholder: #a9a9a9; + --form-text: #fff; + --variable: #d941e2; + --highlight: #efdb43; + --select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E"); +} + +@font-face { + font-family: yond; + src: url(yond.woff2); +} + +html { + text-size-adjust: none; + -webkit-text-size-adjust: none; + -moz-text-size-adjust: none; + -ms-text-size-adjust: none; +} + +body { + font-size: 1.1em; +} + +body button { + margin-bottom: 0.8em; + margin-left: 0.2em; + margin-left: 0.2em; +} + +body li { + margin-top: 0.4em; + margin-bottom: 0.4em; +} + +span.menu a { + text-decoration: none; +} + +body .logo { + font-family: "yond", "monospace"; + font-size: 5em; + text-align: center; + width: 100%; + display: block; + line-height: 0.75em; + color: #fff; + color: var(--text-bright); +} + +body a.topic_link { + color: #dbdbdb; + color: var(--text-main); +} + +body .article_date { + font-family: "yond", "monospace"; + font-size: 1.75em; + text-align: right; + line-height: 0.5em; + margin-right: -0.33em; + margin-bottom: -0.35em; +} + +body .article_photo { + width: 100%; +} + +body .topic { + display: inline-block; + vertical-align: top; + width: 48%; + margin-bottom: 2.5em; +} + +body .topic:nth-child(even) {margin-left: 2%} +body .topic:nth-child(odd) {margin-right: 2%} + +body .topic .topic_image { + display: block; + width: 100%; +} + +/*body .topic .topic_image { border: 1px solid #666; }*/ + +body .topic .topic_title { + display: block; + font-weight: 600; + font-size: 1.2em; + margin-top: 0.35em; + margin-bottom: 0.2em; +} + +body .topic .topic_date { + display: block; + font-size: 0.85em; + font-weight: 400; + font-style: oblique; + text-align: left; + margin-bottom: 1em; +} + +table#loracalc { + /*border: 1px solid #888;*/ + min-width: 50%; + width: auto; +} + +table#loracalc tbody tr td { + vertical-align: middle; + font-weight: bold; +} + +table#loracalc tbody tr td.lcfield { + padding-left: 5em; +} + +table#loracalc tbody tr:nth-child(2n) { + background-color: transparent; +} + +table#loracalc input { + display: inline-block; +} + + +html { + scrollbar-color: #040a0f #202b38; + scrollbar-color: var(--scrollbar-thumb) var(--background-body); + scrollbar-width: thin; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif; + line-height: 1.4; + max-width: 800px; + margin: 20px auto; + padding: 0 10px; + word-wrap: break-word; + color: #dbdbdb; + color: var(--text-main); + background: #202b38; + background: var(--background-body); + text-rendering: optimizeLegibility; +} + +button { + transition: + background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: + background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +input { + transition: + background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: + background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +textarea { + transition: + background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: + background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +h1 { + font-size: 1.7em; + margin-top: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: 12px; + margin-top: 24px; +} + +h1 { + color: #fff; + color: var(--text-bright); +} + +h2 { + font-size: 1.4em; + color: #fff; + color: var(--text-bright); +} + +h3 { + color: #fff; + color: var(--text-bright); +} + +h4 { + color: #fff; + color: var(--text-bright); +} + +h5 { + color: #fff; + color: var(--text-bright); +} + +h6 { + color: #fff; + color: var(--text-bright); +} + +strong { + color: #fff; + color: var(--text-bright); +} + +h1, +h2, +h3, +h4, +h5, +h6, +b, +strong, +th { + font-weight: 600; +} + +q::before { + content: none; +} + +q::after { + content: none; +} + +blockquote { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; +} + +q { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; +} + +blockquote > footer { + font-style: normal; + border: 0; +} + +blockquote cite { + font-style: normal; +} + +address { + font-style: normal; +} + +a[href^='mailto\:']::before { + content: '📧 '; +} + +a[href^='tel\:']::before { + content: '📞 '; +} + +a[href^='sms\:']::before { + content: '💬 '; +} + +mark { + background-color: #efdb43; + background-color: var(--highlight); + border-radius: 2px; + padding: 0 2px 0 2px; + color: #000; +} + +a > code, +a > strong { + color: inherit; +} + +button, +select, +input[type='submit'], +input[type='reset'], +input[type='button'], +input[type='checkbox'], +input[type='range'], +input[type='radio'] { + cursor: pointer; +} + +input, +select { + display: block; +} + +[type='checkbox'], +[type='radio'] { + display: initial; +} + +input { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +button { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +textarea { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +select { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +button { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='submit'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='reset'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='button'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +button:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='submit']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='reset']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='button']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='color'] { + min-height: 2rem; + padding: 8px; + cursor: pointer; +} + +input[type='checkbox'], +input[type='radio'] { + height: 1em; + width: 1em; +} + +input[type='radio'] { + border-radius: 100%; +} + +input { + vertical-align: top; +} + +label { + vertical-align: middle; + margin-bottom: 4px; + display: inline-block; +} + +input:not([type='checkbox']):not([type='radio']), +input[type='range'], +select, +button, +textarea { + -webkit-appearance: none; +} + +textarea { + display: block; + margin-right: 0; + box-sizing: border-box; + resize: vertical; +} + +textarea:not([cols]) { + width: 100%; +} + +textarea:not([rows]) { + min-height: 40px; + height: 140px; +} + +select { + background: #161f27 url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; + background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; + padding-right: 35px; +} + +select::-ms-expand { + display: none; +} + +select[multiple] { + padding-right: 10px; + background-image: none; + overflow-y: auto; +} + +input:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +select:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +button:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +textarea:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +input[type='checkbox']:active, +input[type='radio']:active, +input[type='submit']:active, +input[type='reset']:active, +input[type='button']:active, +input[type='range']:active, +button:active { + transform: translateY(2px); +} + +input:disabled, +select:disabled, +button:disabled, +textarea:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +::-moz-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +:-ms-input-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +::-ms-input-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +::placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +fieldset { + border: 1px #0096bfab solid; + border: 1px var(--focus) solid; + border-radius: 6px; + margin: 0; + margin-bottom: 12px; + padding: 10px; +} + +legend { + font-size: 0.9em; + font-weight: 600; +} + +input[type='range'] { + margin: 10px 0; + padding: 10px 0; + background: transparent; +} + +input[type='range']:focus { + outline: none; +} + +input[type='range']::-webkit-slider-runnable-track { + width: 100%; + height: 9.5px; + -webkit-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; +} + +input[type='range']::-webkit-slider-thumb { + box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); + -webkit-appearance: none; + margin-top: -7px; +} + +input[type='range']:focus::-webkit-slider-runnable-track { + background: #161f27; + background: var(--background); +} + +input[type='range']::-moz-range-track { + width: 100%; + height: 9.5px; + -moz-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; +} + +input[type='range']::-moz-range-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); +} + +input[type='range']::-ms-track { + width: 100%; + height: 9.5px; + background: transparent; + border-color: transparent; + border-width: 16px 0; + color: transparent; +} + +input[type='range']::-ms-fill-lower { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; +} + +input[type='range']::-ms-fill-upper { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; +} + +input[type='range']::-ms-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + border: 1px solid #000; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); +} + +input[type='range']:focus::-ms-fill-lower { + background: #161f27; + background: var(--background); +} + +input[type='range']:focus::-ms-fill-upper { + background: #161f27; + background: var(--background); +} + +a { + text-decoration: underline; + color: #41adff; + color: var(--links); +} + +a:hover { + text-decoration: underline; +} + +code { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +samp { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +time { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +pre > code { + padding: 10px; + display: block; + overflow-x: auto; +} + +var { + color: #d941e2; + color: var(--variable); + font-style: normal; + font-family: monospace; +} + +kbd { + background: #161f27; + background: var(--background); + border: 1px solid #526980; + border: 1px solid var(--border); + border-radius: 2px; + color: #dbdbdb; + color: var(--text-main); + padding: 2px 4px 2px 4px; +} + +img, +video { + max-width: 100%; + height: auto; + display: block; + margin-left: auto; + margin-right: auto; +} + +hr { + border: none; + border-top: 1px solid #526980; + border-top: 1px solid var(--border); +} + +table { + border-collapse: collapse; + margin-bottom: 10px; + width: 100%; + table-layout: fixed; +} + +table caption { + text-align: left; +} + +td, +th { + padding: 6px; + text-align: left; + vertical-align: top; + word-wrap: break-word; +} + +thead { + border-bottom: 1px solid #526980; + border-bottom: 1px solid var(--border); +} + +tfoot { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); +} + +tbody tr:nth-child(even) { + background-color: #161f27; + background-color: var(--background); +} + +tbody tr:nth-child(even) button { + background-color: #1a242f; + background-color: var(--background-alt); +} + +tbody tr:nth-child(even) button:hover { + background-color: #202b38; + background-color: var(--background-body); +} + +::-webkit-scrollbar { + height: 10px; + width: 10px; +} + +::-webkit-scrollbar-track { + background: #161f27; + background: var(--background); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb { + background: #040a0f; + background: var(--scrollbar-thumb); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgb(0, 0, 0); + background: var(--scrollbar-thumb-hover); +} + +::-moz-selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); +} + +::selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); +} + +details { + display: flex; + flex-direction: column; + align-items: flex-start; + background-color: #1a242f; + background-color: var(--background-alt); + padding: 10px 10px 0; + margin: 1em 0; + border-radius: 6px; + overflow: hidden; +} + +details[open] { + padding: 10px; +} + +details > :last-child { + margin-bottom: 0; +} + +details[open] summary { + margin-bottom: 10px; +} + +summary { + display: list-item; + background-color: #161f27; + background-color: var(--background); + padding: 10px; + margin: -10px -10px 0; + cursor: pointer; + outline: none; +} + +summary:hover, +summary:focus { + text-decoration: underline; +} + +details > :not(summary) { + margin-top: 0; +} + +summary::-webkit-details-marker { + color: #dbdbdb; + color: var(--text-main); +} + +dialog { + background-color: #1a242f; + background-color: var(--background-alt); + color: #dbdbdb; + color: var(--text-main); + border: none; + border-radius: 6px; + border-color: #526980; + border-color: var(--border); + padding: 10px 30px; +} + +dialog > header:first-child { + background-color: #161f27; + background-color: var(--background); + border-radius: 6px 6px 0 0; + margin: -10px -30px 10px; + padding: 10px; + text-align: center; +} + +dialog::-webkit-backdrop { + background: #0000009c; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); +} + +dialog::backdrop { + background: #0000009c; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); +} + +footer { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); + padding-top: 10px; + color: #a9b1ba; + color: var(--text-muted); +} + +body > footer { + margin-top: 40px; +} + +@media print { + body, + pre, + code, + summary, + details, + button, + input, + textarea { + background-color: #fff; + } + + button, + input, + textarea { + border: 1px solid #000; + } + + body, + h1, + h2, + h3, + h4, + h5, + h6, + pre, + code, + button, + input, + textarea, + footer, + summary, + strong { + color: #000; + } + + summary::marker { + color: #000; + } + + summary::-webkit-details-marker { + color: #000; + } + + tbody tr:nth-child(even) { + background-color: #f2f2f2; + } + + a { + color: #00f; + text-decoration: underline; + } +} + +#load_overlay { + display: none; +} diff --git a/Console/assets/css/yond.woff2 b/Console/assets/css/yond.woff2 new file mode 100755 index 0000000..88e8992 Binary files /dev/null and b/Console/assets/css/yond.woff2 differ diff --git a/Console/assets/gfx/cs.webp b/Console/assets/gfx/cs.webp new file mode 100755 index 0000000..efa6f74 Binary files /dev/null and b/Console/assets/gfx/cs.webp differ diff --git a/Console/assets/gfx/icon.png b/Console/assets/gfx/icon.png new file mode 100755 index 0000000..e8bf5d6 Binary files /dev/null and b/Console/assets/gfx/icon.png differ diff --git a/Console/assets/gfx/nn.webp b/Console/assets/gfx/nn.webp new file mode 100755 index 0000000..27d19d6 Binary files /dev/null and b/Console/assets/gfx/nn.webp differ diff --git a/Console/assets/gfx/ph.png b/Console/assets/gfx/ph.png new file mode 100755 index 0000000..b25b291 Binary files /dev/null and b/Console/assets/gfx/ph.png differ diff --git a/Console/assets/gfx/rnode_iso.webp b/Console/assets/gfx/rnode_iso.webp new file mode 100755 index 0000000..d69331a Binary files /dev/null and b/Console/assets/gfx/rnode_iso.webp differ diff --git a/Console/assets/gfx/sideband.webp b/Console/assets/gfx/sideband.webp new file mode 100755 index 0000000..308b9d1 Binary files /dev/null and b/Console/assets/gfx/sideband.webp differ diff --git a/Console/assets/images/3_conv.webp b/Console/assets/images/3_conv.webp new file mode 100755 index 0000000..85d0ecd Binary files /dev/null and b/Console/assets/images/3_conv.webp differ diff --git a/Console/assets/images/an1.webp b/Console/assets/images/an1.webp new file mode 100755 index 0000000..d8ae21e Binary files /dev/null and b/Console/assets/images/an1.webp differ diff --git a/Console/assets/images/bg1ds1.webp b/Console/assets/images/bg1ds1.webp new file mode 100755 index 0000000..d878ae1 Binary files /dev/null and b/Console/assets/images/bg1ds1.webp differ diff --git a/Console/assets/images/bg1ds2.webp b/Console/assets/images/bg1ds2.webp new file mode 100755 index 0000000..4e9cdc9 Binary files /dev/null and b/Console/assets/images/bg1ds2.webp differ diff --git a/Console/assets/images/bg_h_1.webp b/Console/assets/images/bg_h_1.webp new file mode 100755 index 0000000..9fa57e3 Binary files /dev/null and b/Console/assets/images/bg_h_1.webp differ diff --git a/Console/assets/images/bg_h_2.webp b/Console/assets/images/bg_h_2.webp new file mode 100755 index 0000000..3057d68 Binary files /dev/null and b/Console/assets/images/bg_h_2.webp differ diff --git a/Console/assets/images/g1p.webp b/Console/assets/images/g1p.webp new file mode 100755 index 0000000..e1465ee Binary files /dev/null and b/Console/assets/images/g1p.webp differ diff --git a/Console/assets/images/g2p.webp b/Console/assets/images/g2p.webp new file mode 100755 index 0000000..d645739 Binary files /dev/null and b/Console/assets/images/g2p.webp differ diff --git a/Console/assets/images/g3p.webp b/Console/assets/images/g3p.webp new file mode 100755 index 0000000..e9dac9f Binary files /dev/null and b/Console/assets/images/g3p.webp differ diff --git a/Console/assets/images/g4p.webp b/Console/assets/images/g4p.webp new file mode 100755 index 0000000..523c0cd Binary files /dev/null and b/Console/assets/images/g4p.webp differ diff --git a/Console/assets/images/lora_rnodes.webp b/Console/assets/images/lora_rnodes.webp new file mode 100755 index 0000000..d475b43 Binary files /dev/null and b/Console/assets/images/lora_rnodes.webp differ diff --git a/Console/assets/images/nn_an.webp b/Console/assets/images/nn_an.webp new file mode 100755 index 0000000..63c4245 Binary files /dev/null and b/Console/assets/images/nn_an.webp differ diff --git a/Console/assets/images/nn_conv.webp b/Console/assets/images/nn_conv.webp new file mode 100755 index 0000000..c66d18a Binary files /dev/null and b/Console/assets/images/nn_conv.webp differ diff --git a/Console/assets/images/nn_init.webp b/Console/assets/images/nn_init.webp new file mode 100755 index 0000000..3b3a46f Binary files /dev/null and b/Console/assets/images/nn_init.webp differ diff --git a/Console/assets/stl/Handheld_RNode_Parts.7z b/Console/assets/stl/Handheld_RNode_Parts.7z new file mode 100755 index 0000000..df861b6 Binary files /dev/null and b/Console/assets/stl/Handheld_RNode_Parts.7z differ diff --git a/Console/build.py b/Console/build.py new file mode 100755 index 0000000..a6932a8 --- /dev/null +++ b/Console/build.py @@ -0,0 +1,373 @@ +import markdown +import os +import sys +import shutil + +packages = { + "rns": "rns-1.0.3-py3-none-any.whl", + "nomadnet": "nomadnet-0.9.1-py3-none-any.whl", + "lxmf": "lxmf-0.9.3-py3-none-any.whl", + "rnsh": "rnsh-0.1.5-py3-none-any.whl", +} + +DEFAULT_TITLE = "RNode Bootstrap Console" +SOURCES_PATH="./source" +BUILD_PATH="./build" +PACKAGES_PATH = "../../dist_archive" +RNS_SOURCE_PATH = "../../Reticulum" +INPUT_ENCODING="utf-8" +OUTPUT_ENCODING="utf-8" + +LXMF_ADDRESS = "8dd57a738226809646089335a6b03695" + +document_start = """ + + + + + + +{PAGE_TITLE} + + + +
+ +{MENU}
""" + +document_end = """""" + +menu_md = """
[Start]({CONTENT_PATH}index.html) | [Replicate]({CONTENT_PATH}replicate.html) | [Software]({CONTENT_PATH}software.html) | [Learn]({CONTENT_PATH}learn.html) | [Help](help.html) | [Contribute]({CONTENT_PATH}contribute.html)
""" + +manual_redirect = """ + + + + + + +""" +help_redirect = """ + + + + + + +""" + +url_maps = [ + # { "path": "", "target": "/.md"}, +] + +def scan_pages(base_path): + files = [file for file in os.listdir(base_path) if os.path.isfile(os.path.join(base_path, file)) and file[:1] != "."] + directories = [file for file in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, file)) and file[:1] != "."] + + page_sources = [] + + for file in files: + if file.endswith(".md"): + page_sources.append(base_path+"/"+file) + + for directory in directories: + page_sources.extend(scan_pages(base_path+"/"+directory)) + + return page_sources + +def get_prop(md, prop): + try: + pt = "["+prop+"]: <> (" + pp = md.find(pt) + if pp != -1: + ps = pp+len(pt) + pe = md.find(")", ps) + return md[ps:pe] + else: + return None + + except Exception as e: + print("Error while extracting topic property: "+str(e)) + return None + +def list_topic(topic): + base_path = SOURCES_PATH+"/"+topic + files = [file for file in os.listdir(base_path) if os.path.isfile(os.path.join(base_path, file)) and file[:1] != "." and file != "index.md"] + + topic_entries = [] + for file in files: + if file.endswith(".md"): + fp = base_path+"/"+file + f = open(fp, "rb") + link_path = fp.replace(SOURCES_PATH, ".").replace(".md", ".html") + + md = f.read().decode(INPUT_ENCODING) + topic_entries.append({ + "title": get_prop(md, "title"), + "image": get_prop(md, "image"), + "date": get_prop(md, "date"), + "excerpt": get_prop(md, "excerpt"), + "md": md, + "file": link_path + }) + + topic_entries.sort(key=lambda e: e["date"], reverse=True) + return topic_entries + +def render_topic(topic_entries): + md = "" + for topic in topic_entries: + md += "" + md += "" + md += "" + md += ""+str(topic["title"])+"" + #md += ""+str(topic["date"])+"" + md += ""+str(topic["excerpt"])+"" + md += "" + md += "" + + + return md + +def generate_html(f, root_path): + md = f.read().decode(INPUT_ENCODING) + + page_title = get_prop(md, "title") + if page_title == None: + page_title = DEFAULT_TITLE + else: + page_title += " | "+DEFAULT_TITLE + + tt = "{TOPIC:" + tp = md.find(tt) + if tp != -1: + ts = tp+len(tt) + te = md.find("}", ts) + topic = md[ts:te] + + rt = tt+topic+"}" + tl = render_topic(list_topic(topic)) + print("Found topic: "+str(topic)+", rt "+str(rt)) + md = md.replace(rt, tl) + + menu_html = markdown.markdown(menu_md.replace("{CONTENT_PATH}", root_path), extensions=["md_in_html", "markdown.extensions.fenced_code", "sane_lists"]).replace("

", "") + page_html = markdown.markdown(md, extensions=["md_in_html", "markdown.extensions.fenced_code"]).replace("{ASSET_PATH}", root_path) + page_html = page_html.replace("{LXMF_ADDRESS}", LXMF_ADDRESS) + for pkg_name in packages: + page_html = page_html.replace("{PKG_"+pkg_name+"}", "pkg/"+pkg_name+".zip") + page_html = page_html.replace("{PKG_BASE_"+pkg_name+"}", pkg_name+".zip") + page_html = page_html.replace("{PKG_NAME_"+pkg_name+"}", packages[pkg_name]) + + page_date = get_prop(md, "date") + if page_date != None: + page_html = page_html.replace("{DATE}", page_date) + + return document_start.replace("{ASSET_PATH}", root_path).replace("{MENU}", menu_html).replace("{PAGE_TITLE}", page_title) + page_html + document_end + +source_files = scan_pages(SOURCES_PATH) + +mf = open(BUILD_PATH+"/m.html", "w") +mf.write(manual_redirect) +mf.close() +mf = open(BUILD_PATH+"/h.html", "w") +mf.write(help_redirect) +mf.close() + +def optimise_manual(path): + pm = 180 + scale_imgs = [ + ("_images/board_rnodev2.png", pm), + ("_images/board_rnode.png", pm), + ("_images/board_heltec32v20.png", pm), + ("_images/board_heltec32v30.png", pm), + ("_images/board_heltec32v4.png", pm), + ("_images/board_t3v21.png", pm), + ("_images/board_t3v20.png", pm), + ("_images/board_t3v10.png", pm), + ("_images/board_t3s3.png", pm), + ("_images/board_tbeam.png", pm), + ("_images/board_tdeck.png", pm), + ("_images/board_rak4631.png", pm), + ("_images/board_tbeam_supreme.png", pm), + ("_images/sideband_devices.webp", pm), + ("_images/nomadnet_3.png", pm), + ("_images/meshchat_1.webp", pm), + ("_images/radio_is5ac.png", pm), + ("_images/radio_rblhg5.png", pm), + ("_static/rns_logo_512.png", 256), + ("../images/bg_h_1.webp", pm), + ] + + import subprocess + import shlex + for i,s in scale_imgs: + fp = path+"/"+i + input_file = fp + output_file = input_file + resize = "convert "+input_file+" -quality 25 -resize "+str(s)+" "+output_file + print(resize) + subprocess.call(shlex.split(resize), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # if output_file != input_file and os.path.isfile(input_file): os.unlink(input_file) + + remove_files = [ + "objects.inv", + "Reticulum Manual.pdf", + "Reticulum Manual.epub", + "_static/styles/furo.css.map", + "_static/scripts/furo.js.map", + "_static/jquery-3.6.0.js", + "_static/jquery.js", + "static/underscore-1.13.1.js", + "_static/_sphinx_javascript_frameworks_compat.js", + "_static/scripts/furo.js.LICENSE.txt", + "_static/styles/furo-extensions.css.map", + "_images/board_rak4631.png", + "_images/board_rnodev2.png", + "_images/board_t114.png", + "_images/board_t3s3.png", + "_images/board_t3v10.png", + "_images/board_t3v20.png", + "_images/board_t3v21.png", + "_images/board_tbeam.png", + "_images/board_tdeck.png", + "_images/board_techo.png", + "_images/board_tbeam_supreme.png", + "_images/board_opencomxl.png", + "_images/board_heltec32v20.png", + "_images/board_heltec32v30.png", + # "_static/pygments.css", + # "_static/language_data.js", + # "_static/searchtools.js", + # "searchindex.js", + ] + for file in remove_files: + fp = path+"/"+file + print("Removing file: "+str(fp)) + try: + os.unlink(fp) + except Exception as e: + print("An error occurred while attempting to unlink "+str(fp)+": "+str(e)) + + remove_dirs = [ + "_sources", + ] + for d in remove_dirs: + fp = path+"/"+d + print("Removing dir: "+str(fp)) + shutil.rmtree(fp) + + shutil.move(path, BUILD_PATH+"/m") + +def fetch_reticulum_site(): + r_site_path = BUILD_PATH+"/r" + if not os.path.isdir(r_site_path): + shutil.copytree(PACKAGES_PATH+"/reticulum.network", r_site_path) + if os.path.isdir(r_site_path+"/manual"): + optimise_manual(r_site_path+"/manual") + remove_files = [ + "gfx/reticulum_logo_512.png", + ] + for file in remove_files: + fp = r_site_path+"/"+file + print("Removing file: "+str(fp)) + os.unlink(fp) + replace_paths() + +def replace_paths(): + repls = [ + ("gfx/reticulum_logo_512.png", "/m/_static/rns_logo_512.png") + ] + for root, dirs, files in os.walk(BUILD_PATH): + for file in files: + fpath = root+"/"+file + if fpath.endswith(".html"): + print("Performing replacements in "+fpath+"") + f = open(fpath, "rb") + html = f.read().decode("utf-8") + f.close() + for s,r in repls: + html = html.replace(s,r) + f = open(fpath, "wb") + f.write(html.encode("utf-8")) + f.close() + + # if not os.path.isdir(BUILD_PATH+"/d"): + # os.makedirs(BUILD_PATH+"/d") + # shutil.move(fpath, BUILD_PATH+"/d/") + + +def remap_names(): + for root, dirs, files in os.walk(BUILD_PATH): + for file in files: + fpath = root+"/"+file + spath = fpath.replace(BUILD_PATH, "") + if len(spath) > 31: + print("Path "+spath+" is too long, remapping...") + if not os.path.isdir(BUILD_PATH+"/d"): + os.makedirs(BUILD_PATH+"/d") + shutil.move(fpath, BUILD_PATH+"/d/") + + + +def gz_all(): + import gzip + for root, dirs, files in os.walk(BUILD_PATH): + for file in files: + fpath = root+"/"+file + print("Gzipping "+fpath+"...") + f = open(fpath, "rb") + g = gzip.open(fpath+".gz", "wb") + g.writelines(f) + g.close() + f.close() + os.unlink(fpath) + +from zipfile import ZipFile +for pkg_name in packages: + pkg_file = packages[pkg_name] + pkg_full_path = PACKAGES_PATH+"/"+pkg_file + if os.path.isfile(pkg_full_path): + print("Including "+pkg_file) + z = ZipFile(BUILD_PATH+"/pkg/"+pkg_name+".zip", "w") + z.write(pkg_full_path, pkg_full_path[len(PACKAGES_PATH+"/"):]) + z.close() + # shutil.copy(pkg_full_path, BUILD_PATH+"/"+pkg_name) + + else: + print("Could not find "+pkg_full_path) + exit(1) + +for um in url_maps: + with open(SOURCES_PATH+"/"+um["target"], "rb") as f: + of = BUILD_PATH+um["target"].replace(SOURCES_PATH, "").replace(".md", ".html") + root_path = "../" + html = generate_html(f, root_path) + + print("Map path : "+str(um["path"])) + print("Map target : "+str(um["target"])) + print("Mapped root path: "+str(root_path)) + + if not os.path.isdir(BUILD_PATH+"/"+um["path"]): + os.makedirs(BUILD_PATH+"/"+um["path"], exist_ok=True) + + with open(BUILD_PATH+"/"+um["path"]+"/index.html", "wb") as wf: + wf.write(html.encode(OUTPUT_ENCODING)) + +for mdf in source_files: + with open(mdf, "rb") as f: + of = BUILD_PATH+mdf.replace(SOURCES_PATH, "").replace(".md", ".html") + root_path = "../"*(len(of.replace(BUILD_PATH+"/", "").split("/"))-1) + html = generate_html(f, root_path) + + if not os.path.isdir(os.path.dirname(of)): + os.makedirs(os.path.dirname(of), exist_ok=True) + + with open(of, "wb") as wf: + wf.write(html.encode(OUTPUT_ENCODING)) + +fetch_reticulum_site() +if not "--no-gz" in sys.argv: + gz_all() + +if not "--no-remap" in sys.argv: + remap_names() diff --git a/Console/source/builds/ap.md b/Console/source/builds/ap.md new file mode 100755 index 0000000..73f5741 --- /dev/null +++ b/Console/source/builds/ap.md @@ -0,0 +1,8 @@ +[date]: <> (2023-01-12) +[title]: <> (Outdoor RNode) +[image]: <> (gfx/cs.webp) +[excerpt]: <> (An outdoor-mountable RNode suitable for Access Point or network extension operation. Also supports high-capacity batteries and solar charging.) +## Outdoor AP RNode +This RNode comes with a weather-proof case and convenient cable management options, suitable for outdoor mounting and operation. It is possible to mount this RNode directly to masts and antennas, and it supports high-capacity batteries and solar charging. + +This build recipe will be released soon. Please [support the project]({ASSET_PATH}contribute.html) to help realise it! diff --git a/Console/source/builds/handheld.md b/Console/source/builds/handheld.md new file mode 100755 index 0000000..d2c0663 --- /dev/null +++ b/Console/source/builds/handheld.md @@ -0,0 +1,168 @@ +[date]: <> (2023-01-14) +[title]: <> (Handheld RNode) +[image]: <> (gfx/rnode_iso.webp) +[excerpt]: <> (This RNode is suitable for mobile and handheld operation, and offers both wireless and wired connectivity to host devices. A good all-round unit. It is also suitable for permanent installation indoors.) +## Handheld RNode Recipe +*Version 2.1* + +This build recipe will help you create an RNode that is suitable for mobile and handheld operation, and offers both wireless and wired connectivity to host devices. It is also useful for permanent installation indoors, or even outdoors, as long as it is protected from water ingress and direct sunlight. + +Depending on the board you use, it will offer a workable frequency range between **420 and 520 MHz**, or **820 and 1020 MHz**, and a maximum TX power of **17 dBm** (50 mW). + +Completed Handheld RNode +
*A completed Handheld RNode*
+ +### Table of Contents + +1. [Preparation](#prep) +2. [Supported Boards](#devboard) +3. [Materials](#materials) +4. [Print Parts](#parts) +5. [Install Tools](#tools) +6. [Firmware Setup](#firmware) +7. [Assembly](#assembly) + + +### Step 1: Preparation + +When you have completed this recipe, you will end up with a fully-featured RNode device, similar to the one pictured above. To make it as easy as possible to complete this guide, make sure to read it all in its entirity *before* starting. I also recommend you familiarise yourself with the required materials, and the software tools needed for the setup. + +To complete this build recipe, you will need access to the following items: + +- A computer with a functional operating system, such as Linux, BSD or macOS +- One of the [supported development boards](#devboard) for this recipe +- A suitable USB cable for connecting the development board to your computer +- A 3D printer and the necessary amount of material for printing the [device parts](#parts) +- 6 pieces of M2x6mm screws to assemble the case +- A suitable antenna +- An optional NeoPixel RGB LED +- An optional [battery](#battery) + - This build can use any single-cell (3.7v) lithium battery with a 1.25mm JST connector, provided it will fit in the case. Please see [this section](#battery) for details on battery sizes. + +### Step 2: Supported Development Boards + +This RNode design is using a **LilyGO LoRa32 v2.1** board, in either the **433 MHz**, **868 MHz**, **915 MHz** or **923 MHz** variants. It seems that the 868, 915 and 923 MHz variants are in fact completely identical, and all offer a frequency range between 820 and 1020 MHz. The 433MHz variants offer a frequency range between 420 and 520 MHz. + +These boards are also sold under many different "brand" names other than LilyGO, but using the images below, you should be able to identify the correct ones. + +It is easiest to obtain the version of the board with an **u.FL** (sometimes also labeled *IPX* or *IPEX*) antenna connector, instead of the **SMA** connector. This version comes with an SMA to u.FL pigtail, which is installed into the 3D-printed case. If it is not possible to obtain this version, you can use the one with an **SMA** connector, either as is, or by removing the **SMA** connector, and using the on-board **u.FL** connector instead. + +If you do not wish to use the 3D-printable case included in this guide, it does not matter which version you get. There is **no functional difference** between the boards with **SMA** and **u.FL** connectors. + +Compatible board +
*The correct board version for this RNode build recipe*
+ +If you want to use the case provided for this build guide, and you have the version with an *SMA* connector, you will have to desolder the **SMA** connector, and activate the *u.FL* connector instead (it's already installed on all the boards, just not activated on the **SMA** connector versions). + +To activate the **u.FL** connector, you will just have to "rotate" the small resistor next to the antenna connectors by 90 degrees, so it "points" at the connector you wish to use. + +Please note that the "resistor" is actually just a zero-ohm jumper. If you don't feel like fiddling around with small components, you can simply remove it, and bridge the relevant gap with a blob of solder. + +Refer to the following two pictures to locate the resistor that needs moving: + +Before desoldering +After desoldering +
*Before and after removing the SMA connector and moving the resistor*
+ +You will also need to dismount the OLED display from the small acrylic riser on the board, and unscrew and discard the riser. Be careful not to damage the display or ribbon cable while doing this. The OLED display will be mounted directly into a matching slot in the 3D-printed case. + +As before, if you do not want to use the 3D printed case supplied here, it's probably much easier to keep the display on the board, and you can simply skip this step. + +### Step 3: Obtain Materials + +In addition to the board, you will need a few other components to build this RNode. + +- A suitable **antenna**. Most boards purchased online include a passable antenna, but you may want to upgrade it to a better one. +- 6 pieces of **M2x6mm screws** for assembling the case. Can be bought in most hardware stores or from online vendors. +- An optional **NeoPixel RGB LED** for displaying status, and TX/RX activity. If you do not want to add this, it can simply be omitted. + - The easiest way is to use the PCB-mounted NeoPixel "mini-buttons" manufactured by [adafruit.com](https://www.adafruit.com/product/1612). These fit exactly into the slot in the mounting position in the 3D-printed case, and are easy to connect cables to. +- An optional **lithium-polymer battery**. + - This RNode supports **3.7v**, **single-cell** LiPo batteries with a **1.25mm JST connector** + - The standard case can fit up to a 700mAh LP602248 battery + - Maximum battery dimensions for this case is 50mm x 25mm x 6mm + - There is a larger bottom casing available that fits 1100mAh batteries + - Maximum battery dimensions for this case is 50mm x 25mm x 12mm + +### Step 4: 3D Print Parts + +To complete the build of this RNode, you will need to 3D-print the parts for the casing. Download, extract and slice the STL files from the [parts package]({ASSET_PATH}3d/Handheld_RNode_Parts.7z) in your preferred software. + +- Two of the parts are LED light-guides, and should be printed in a semi-translucent material: + - The `LED_Window.stl` file is a light-guide for the NeoPixel LED, mounted in the circular cutout at the top of the device. + - The `LED_Guide.stl` file is a light-guide for the power and charging LEDs, mounted in the rectangular grove at the bottom of the device. +- The rest of the parts can be printed in any material, but for durability and heat-resistance, PETG is recommended. + - The `Power_Switch.stl` file is a small power-switch slider, mounted in the matching grove on the bottom-left of the device. + - The `Case_Top.stl` file is the top shell of the case. It holds the OLED display and NeoPixel RGB LED, and mounts to the bottom shell of the case with 6 M2 screws. The screw holes in both the top and bottom shells of the case are dimensioned to be self-threading when screws are inserted for the first time. Do not over-tighten. + - The `Case_Bottom_Small_Battery.stl` file is the default bottom shell of the case. It holds batteries up to approximately 700mAh. + - The `Case_Bottom_Large_Battery.stl` file is an alternative bottom shell for the case. It holds batteries up to approximately 1100mAh. + - The `Case_Bottom_No_Battery.stl` file is an alternative bottom shell for the case. It does not have space for a battery, but results in a very compact device. + - The `Case_Battery_Door.stl` file is the door for the battery compartment of the device. It snap-fits tightly into place in the bottom shell, and features a small slot for opening with a flathead screwdriver or similar. + +All files are dimensioned to fit together perfectly without any scaling on a well-tuned 3D-printer. + +The recommended layer height for all files is 0.15mm for FDM printers. + +### Step 5: Install Tools + +To install and configure the RNode Firmware on the device, you will need to install the `rnodeconf` program on your computer. This is included in the `rns` package, that can be [installed directly from this RNode]({ASSET_PATH}s_rns.html). Please carry out the installation instructions on [this page]({ASSET_PATH}s_rns.html), and continue to the next step when the `rnodeconf` program is installed. + + +### Step 6: Firmware Setup + +Once the `rnodeconf` program is installed, we will use it to install the RNode Firmware on your device, and do the initial provisioning of configuration parameters. This process can be completed automatically, by using the auto-installer. Run the `rnodeconf` auto-installer with the following command: + +``` +rnodeconf --autoinstall +``` + +1. The program will ask you to connect your device to an USB-port on your computer. Do so, and hit enter. +2. Select the serial port the device is connected as. +3. You will now be asked what device this is, select the option **A Specific Kind of RNode**. +4. The installer will ask you what model your device is. Select the **Handheld RNode v2.x** option that matches the frequency band of your device. +5. The installer will display a summary of your choices. If you are satisfied, confirm your selection. +6. The installer will now automatically install and configure the firmware and prepare the device for use. + +> **Please Note!** If you are connected to the Internet while installing, the autoinstaller will automatically download any needed firmware files to a local cache before installing. + +> If you do not have an active Internet connection while installing, you can extract and use the firmware from this device instead. This will **only** work if you are building the same type of RNode as the device you are extracting from, as the firmware has to match the targeted board and hardware configuration. + +If you need to extract the firmware from an existing RNode, run the following command: + +``` +rnodeconf --extract +``` + +If `rnodeconf` finds a working RNode, it will extract and save the firmware from the device for later use. You can then run the auto-installer with the `--use-extracted` option to use the locally extracted file: + +``` +rnodeconf --autoinstall --use-extracted +``` + +This also works for updating the firmware on existing RNodes, so you can extract a newer firmware from one RNode, and deploy it onto other RNodes using the same method. Just use the `--update` option instead of `--autoinstall`. + +### Step 7: Assembly + +With the firmware installed and configured, and the case parts printed, it's time to put it all together. + +1. Insert the **SMA to u.FL** pigtail adatper into the matching **slot** in the top part of the bottom shell. Make sure it lines up with the internal hex-nut cut-out in the bottom shell, as the hex nut of the adapter will get pulled into this cut-out, and thereby self-lock, when an antenna is connected. You can optionally mount a locking nut on the exterior thread of the SMA connector when the case has been completely assembled. +2. Thread the cable of the **SMA to u.FL** pigtail adapter into the matching grove, and run it out of the bottom opening. +3. Mount the **power-switch slider** into the matching slot, in the bottom-left part of the bottom shell. +4. With the SMA connector and power switch mounted, slide the **board** into the bottom shell, such that the **power switch** of the **board** mates with the slot in the already installed power-switch slider. Click the **board** into place in the bottom shell. +5. Optionally mount the **NeoPixel LED**: + - Measure out cables that matches lenghts between the NeoPixel mounting slot, and the corresponding pins on the board. + - Solder the **V+**, **GND** and **DATA** cables to the NeoPixel. + - Solder the **V+** cable to the **3.3v** pin on the board. + - Solder the **GND** cable to the **GND** pin on the board. + - Solder the **DATA** cable to **IO Pin 12** on the board. + - Mount the **NeoPixel** in the circular slot in the top part of the top shell. +6. Carefully mount the OLED display in the rectangular slot in the middle part of the top shell. +7. While ensuring that all internal cables stay within their routing groves, place the **top shell** on top of the **bottom shell**, making sure that the screw-mounting holes line up. +8. Mount the 6 **M2x6mm screws** into the mounting holes, until the two shells of the case are tightly and securely connected. +9. Flip over the device. +10. Connect the male **u.FL** connector to the female **u.FL** socket on the **board**. +11. Optionally, connect the male JST connector of the **battery** to the female JST connector on the **board**. +12. Fit the **battery door** into place. + +Congratulations, Your Handheld RNode is now complete! + +Flip the power switch, and start using it! \ No newline at end of file diff --git a/Console/source/builds/micropylon.md b/Console/source/builds/micropylon.md new file mode 100755 index 0000000..bf70c51 --- /dev/null +++ b/Console/source/builds/micropylon.md @@ -0,0 +1,8 @@ +[date]: <> (2023-01-09) +[title]: <> (Reticulum MicroPylon) +[image]: <> (gfx/cs.webp) +[excerpt]: <> (A powerful, solar-powered multi-transceiver RNode-based radio system for autonomous and self-configuring Reticulum network deployments.) +## Reticulum MicroPylon +This radio system is a powerful and flexible multi-transceiver radio system, designed for rapidly deploying autonomous and self-configuring Reticulum networks over wide areas. + +This build recipe will be released soon. Please [support the project]({ASSET_PATH}contribute.html) to help realise it! diff --git a/Console/source/builds/wallmount.md b/Console/source/builds/wallmount.md new file mode 100755 index 0000000..e775099 --- /dev/null +++ b/Console/source/builds/wallmount.md @@ -0,0 +1,6 @@ +[date]: <> (2023-01-10) +[title]: <> (Wall-Mount RNode) +[image]: <> (gfx/cs.webp) +[excerpt]: <> (A sleek, wall-mountable RNode, suitable for permanent installation and operation indoors, or in a semi-protected environment outdoors.) +## Wall-Mount RNode +This build recipe will be released soon. Please [support the project]({ASSET_PATH}contribute.html) to help realise it! diff --git a/Console/source/contact.md b/Console/source/contact.md new file mode 100755 index 0000000..ea366f2 --- /dev/null +++ b/Console/source/contact.md @@ -0,0 +1,20 @@ +[title]: <> (Contact) +# Contact Me + +**Hello!** I am the creator of the RNode ecosystem. + +If you have any general questions or comments about any of the projects I maintain, I encourage you to post it in one of the following places: + +- The [discussion forum](https://github.com/markqvist/Reticulum/discussions) on GitHub +- The [Reticulum Matrix Channel](#reticulum:matrix.org) at `#reticulum:matrix.org` +- The [Reticulum subreddit](https://reddit.com/r/reticulum) + +To get in touch with me personally, you can use one of the following methods, in order of preference: + +- LXMF at `8dd57a738226809646089335a6b03695` +- Matrix using `@unsignedmark:matrix.org` +- Email by using the address mark at unsigned dot io + +Please use the public forums and channels for support and help requests. I receive a lot of messages, and while I try to answer everyone (eventually), this is not always possible. + +
`3502`
\ No newline at end of file diff --git a/Console/source/contribute.md b/Console/source/contribute.md new file mode 100755 index 0000000..2f8b662 --- /dev/null +++ b/Console/source/contribute.md @@ -0,0 +1,39 @@ +[title]: <> (Donate) +## Keep Communications Free and Open +Please take part in keeping the continued development, maintenance and distribution of the RNode ecosystem possible by donating via one of the following channels: + +- Monero
+ ``` + 84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w + ``` +

+- Ethereum
+ ``` + 0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73 + ``` +

+- Bitcoin
+ ``` + 35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH + ``` +

+- Ko-Fi
+ `https://ko-fi.com/markqvist` + +## Spread Knowledge and Awareness +Another great way to contribute, is to spread awareness about the RNode project. Here's some ideas: + +- Introduce the concepts of Free & Open Communications Systems to your community +- Teach others to build and use RNodes, and how to set up resilient and private communications systems +- Learn about using Reticulum to set up resilient communications networks, and teach these skills to people in your area that need them + +## Contribute Code & Material +If you like to write, build and design, there are plenty of oppertunities to take part in the community around RNode, and the wider Reticulum community as well. + +There's always plenty of work to do, from writing code, tutorials and guides, to designing parts, devices and integrations, and translating material to other languages. + +You can find us the following places: + +- The [Reticulum Matrix Channel](element://room/!TRaVWNnQhAbvuiSnEK%3Amatrix.org?via=matrix.org) at `#reticulum:matrix.org` +- The [discussion forum](https://github.com/markqvist/Reticulum/discussions) on GitHub +- The [Reticulum subreddit](https://reddit.com/r/reticulum) \ No newline at end of file diff --git a/Console/source/guides/install_firmware.md b/Console/source/guides/install_firmware.md new file mode 100755 index 0000000..0d77695 --- /dev/null +++ b/Console/source/guides/install_firmware.md @@ -0,0 +1,123 @@ +[date]: <> (2023-01-12) +[title]: <> (Installing RNode Firmware on Supported Devices) +[image]: <> (images/g2p.webp) +[excerpt]: <> (If you have a T-Beam or LoRa32 device handy, it is very easy to get it set up for all the things that the RNode firmware allows you to do.) + +# Installing RNode Firmware on Supported Devices + +Do you have one of the devices available that the RNode Firmware supports? In that case, it is very easy to turn it into a working RNode by using the `rnodeconf` autoinstaller. + +With the firmware installed, you can use your newly created RNode as: + +- A [LoRa interface for Reticulum]({ASSET_PATH}m/interfaces.html#rnode-lora-interface) +- A LoRa packet sniffer with [LoRaMon](https://unsigned.io/loramon/) +- A Linux network interface using the [tncattach program]({ASSET_PATH}pkg/tncattach.zip) +- A LoRa-based TNC for almost any amateur radio packet application + +So let's get started! You will need either a **LilyGO T-Beam v1.1**, a **LilyGO LoRa32 v2.0**, a **LilyGO LoRa32 v2.1** or a **Heltec LoRa32 v2** device. More supported devices are added regularly, so it might be useful to check the latest [list of supported devices]({ASSET_PATH}supported.html) as well. + +It is currently recommended to use one of the following devices: A **LilyGO LoRa32 v2.1** (also known as **TTGO T3 v1.6.1**) or a **LilyGO T-Beam v1.1**. + +![Compatible LoRa devices]({ASSET_PATH}images/g2p.webp) +
*Some of the device types compatible with this installation guide*
+ +## Device Variations + +Some devices come with transceiver chips that are currently unsupported by the RNode Firmware. Currently devices with an **SX1276** or **SX1278** chip are supported. Support for **SX1262**, **SX1268** and **SX1280** is being added. Please support the development with [donations]({ASSET_PATH}donate.html), if you would like to see these chips supported. + +> **Beware!** Some devices, like the T-Beam, use SiLabs USB chips. These may need [additional drivers](https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers) to work well on macOS and Windows. Linux usually has up-to-date drivers pre-installed. The SiLabs driver may also experience conflicts with earlier, pre-installed versions of the driver, causing a *resource busy* error, which can be fixed by [removing the old driver](https://community.platformio.org/t/mac-usb-port-detected-but-won-t-upload/20663/2). + +## Preparations + +To get started, you will need to install at least version 2.1.0 of the [RNode Configuration Utility]({ASSET_PATH}m/using.html#the-rnodeconf-utility). + +The `rnodeconf` program is included in the `rns` package. Please read [these instructions]({ASSET_PATH}s_rns.html) for more information on how to install it from this repository, or from the Internet. If installation goes well, you can now move on to the next step. + +## Install The Firmware + +We are now ready to start installing the firmware. To install the RNode Firmware on your devices, run the RNode autoinstaller using this command: + +```txt +rnodeconf --autoinstall +``` + +The installer will now ask you to insert the device you want to set up, scan for connected serial ports, and ask you a number of questions regarding the device. When it has the information it needs, it will install the correct firmware and configure the necessary parameters in the device EEPROM for it to function properly. + +If the install goes well, you will be greated with a success message telling you that your device is now ready. + +> **Please Note!** If you are connected to the Internet while installing, the autoinstaller will automatically download any needed firmware files to a local cache before installing. + +> If you do not have an active Internet connection while installing, you can extract and use the firmware from this device instead. This will **only** work if you are building the same type of RNode as the device you are extracting from, as the firmware has to match the targeted board and hardware configuration. + +If you need to extract the firmware from an existing RNode, run the following command: + +``` +rnodeconf --extract +``` + +If `rnodeconf` finds a working RNode, it will extract and save the firmware from the device for later use. You can then run the auto-installer with the `--use-extracted` option to use the locally extracted file: + +``` +rnodeconf --autoinstall --use-extracted +``` + +This also works for updating the firmware on existing RNodes, so you can extract a newer firmware from one RNode, and deploy it onto other RNodes using the same method. Just use the `--update` option instead of `--autoinstall`. + +## Verify Installation +To confirm everything is OK, you can query the device info with: + +```txt +rnodeconf --info /dev/ttyUSB0 +``` + +Remember to replace `/dev/ttyUSB0` with the actual port the installer used in the previous step. You should now see `rnodeconf` connect to your device and show something like this: + +```txt +[20:11:22] Opening serial port /dev/ttyUSB0... +[20:11:25] Device connected +[20:11:25] Current firmware version: 1.26 +[20:11:25] Reading EEPROM... +[20:11:25] EEPROM checksum correct +[20:11:25] Device signature validated +[20:11:25] +[20:11:25] Device info: +[20:11:25] Product : LilyGO LoRa32 v2.0 850 - 950 MHz (b0:b8:36) +[20:11:25] Device signature : Validated - Local signature +[20:11:25] Firmware version : 1.26 +[20:11:25] Hardware revision : 1 +[20:11:25] Serial number : 00:00:00:02 +[20:11:25] Frequency range : 850.0 MHz - 950.0 MHz +[20:11:25] Max TX power : 17 dBm +[20:11:25] Manufactured : 2022-01-27 20:10:32 +[20:11:25] Device mode : Normal (host-controlled) +``` + +On the hardware side, you should see the status LED flashing briefly approximately every 2 seconds. If all of the above checks out, congratulations! Your RNode is now ready to use. If your device has a display, it should also come alive and show you various information related to the device state. + +If you want to use it with [Reticulum]({ASSET_PATH}s_rns.html), [Nomad Network]({ASSET_PATH}s_nn.html), [LoRaMon](https://unsigned.io/loramon), or other such applications, leave it in the default `Normal (host-controlled)` mode. + +If you want to use it with legacy amateur radio applications that work with KISS TNCs, you should [set it up in TNC mode]({ASSET_PATH}guides/tnc_mode.html). + +## External RGB LED +If you are using a **LilyGO LoRa32 v2.1** device, you can connect an external **NeoPixel RGB LED** for device status using the following setup: + +- Connect the NeoPixel **V+** pin to the **3.3v** pin on the board. +- Connect the NeoPixel **GND** pin to the **GND** pin on the board. +- Connect the NeoPixel **DATA** pin to **IO Pin 12** on the board. + +For the firmware to activate the NeoPixel LED, you must also make specific choices in the autoinstaller guide: + +- When asked what type of device you have, select **A specific kind of RNode**. +- When asked what model the device is, select the **Handheld v2.x RNode** that matches the frequency of your board. + +## External Display & LEDs +If you are using a **LilyGO T-Beam** device, you can connect an external **SSD1306 OLED** display using the following setup: + +- The **SSD1306**-based display must be set to use **I2C** and address `0x3D` +- Connect display **GND** to T-Beam **GND** +- Connect display **Vin** to suitable power-supplying pin on the T-Beam +- Connect display **RST** to T-Beam **Pin 13** +- Connect display **I2C CLK** to T-Beam **SCL** / **Pin 22** +- Connect display **I2C DATA** to T-Beam **SDA** / **Pin 21** + +On **T-Beam** devices, you can also connect external RX/TX LEDs to **Pin 2** and **Pin 4**. diff --git a/Console/source/guides/loracomms.md b/Console/source/guides/loracomms.md new file mode 100755 index 0000000..1b5b5b3 --- /dev/null +++ b/Console/source/guides/loracomms.md @@ -0,0 +1,237 @@ +[date]: <> (2023-01-14) +[title]: <> (Private, Secure and Uncensorable Messaging Over a LoRa Mesh) +[image]: <> (images/g1p.webp) +[excerpt]: <> (Or: How to set up a completely private, independent and encrypted communication system in half an hour, using stuff you can buy for under $100.) + +# Private, Secure and Uncensorable Messaging Over a LoRa Mesh +*Or: How to set up a completely private, independent and encrypted communication system in half an hour, using stuff you can buy for under $100.* + +![]({ASSET_PATH}images/g1p.webp) + +In this post, we will explore how two people, Alice and Bob, can set up a LoRa mesh communication system for their use that has the following characteristics: + +- Allows both real-time and asynchronous text message communication between Alice and Bob. +- Works *completely* indpendently of any infrastructure outside the control of Alice and Bob. Even if the Internet, cellular networks and the power grid fails, Alice and Bob must still be able to communicate. +- Is completely private and outside the reach of automated surveillance, and does not reveal any identifying information about Alice or Bob, nor any contents of or information about their conversations. + +In later parts of this series, we will expand the system to provide these oppertunities to an entire community, and add other mediums like Packet Radio, but for now we will focus on learning the basics by just establishing a Free Communications System between Alice and Bob. + +To accomplish this, we will be building a small and simple system based on freely available and Open Source software. To realise our system we will need the following components: + +- A networking system that can function reliably and efficiently even without any functional Internet infrastructure available. This will be provided by [Reticulum]({ASSET_PATH}r/index.html). +- Software that Alice and Bob can interact with on their computers and mobile devices to actually communicate with each other. This will be provided by the programs [Nomad Network]({ASSET_PATH}s_nn.html) and [Sideband]({ASSET_PATH}s_sideband.html). +- Radio hardware that Reticulum can use to cover the 7 kilometer distance between Bobs apartment and Alices house. This will be provided by installing the [RNode Firmware]({ASSET_PATH}guides/install_firmware.html) on a couple of small LoRa radio modules that can be purchased cheaply off Amazon or similar online vendors. + +As you might have already guessed, the "magic glue" that acutally makes this entire system possible is [Reticulum]({ASSET_PATH}r/index.html). + +Reticulum is a complete networking stack that was designed to handle challenging situations and requirements like this. Reticulum is an incredibly flexible networking platform, that can use almost anything as a carrier for digital information transfer, and it can automatically form secure mesh networks with very minimal resources, infrastructure and setup. + +Please do keep in mind though, that at the time of writing this, Reticulum is still in beta. There might be bugs and security issues that have not yet been discovered. You can keep up with such things, and get updates on the general development and releases, over on the [Reticulum GitHub page](https://github.com/markqvist/reticulum). + +The user-facing software that Alice and Bob will be installing already includes Reticulum, so there is no complicated installation and configuration setups, and getting everything up and running will be quite simple. The requirements are also very minimal, and everything can run on hardware they already have available, be that an old computer, a Raspberry Pi, or an Android phone. + +Let's get started. + +# LoRa Radio Setup +The first step is to get the LoRa radios prepared and installed. I have written in more length and details about these subjects in other posts on this site ([Installing RNode Firmware on Supported Devices]({ASSET_PATH}guides/install_firmware.html) and [How To Make Your Own RNodes]({ASSET_PATH}guides/make_rnodes.html), so this article will just quickly guide you through the basics required to get up and running. For much more information, read the above articles. + +First of all, Alice and Bob need to get a compatible piece of radio hardware to use. Had they been living closer to each other, they might have just been able to use WiFi, but they need to cover a distance of more than 7 kilometers, so they decide to go with a couple of LoRa radios. + +They take a look at the RNode Firmware [Supported Devices List]({ASSET_PATH}supported.html), and decide to go with a couple of LilyGO T-Beam devices. They could have also used others, and they don't need to choose the same device, as long as they are within the same frequency range, all compatible devices work with Reticulum and can communicate with each other, as soon as the RNode Firmware has been installed on them. + +![]({ASSET_PATH}images/lora_rnodes.webp) + +Once the devices arrive, it is time to get the firmware installed. For this they will need a computer running some sort of Linux. Alice has a computer with Ubuntu installed, so they decide to use that. Since Python3 came installed as standard with the OS, Alice can go ahead and install the RNode configuration program by simply opening a terminal and typing: + +``` +pip install rnodeconf +``` + +The above command installs the program they need to flash the LoRa radios with the right firmware. If for some reason Python3 had not already been installed on Alices computer, she would have had to install it first with the command `sudo apt install python python-pip`. + +Now that the firmware installer is ready, it is time to actually get the firmware on to the devices. Alice launches the installer with the following command: + +``` +rnodeconf --autoinstall +``` + +After this she is greated with an interactive guide that asks a few questions about the device type, grabs the latest firmware files, and installs them onto the device. After repeating with the second device, that is all there is to it, and the LoRa radios are now ready for use with Reticulum. + +# Installation at Alices House +To get a better signal, Alice mounts her LoRa radio in the attic of her house. She then runs a USB cable from the mounting location to the computer she wants to use for messaging, and plugs the cable into the computer. The LoRa radio is now directly connected to her computer via USB, and receives power from it when the computer is on. + +At her computer (running Ubuntu Linux), she installs the Nomad Network program by entering the following command in a terminal: + +``` +pip install nomadnet +``` + +After a few seconds, Nomad Network and Reticulum is installed and ready to use. She can now run the Nomad Network client by entering the following command: + +``` +nomadnet +``` + +All required directories and configuration files will now be created, and the client will start up. After a few seconds, Alice will be greeted with a screen like this: + +![]({ASSET_PATH}images/nn_init.webp) + +Confirming that everything is installed and working, it is time to add the LoRa radio as an interface that Reticulum can use. To do this, she opens up the Reticulum configuration file (located at `˜/.reticulum/config`) in a text editor. + +By referring to the [RNode LoRa Interface]({ASSET_PATH}m/interfaces.html#rnode-lora-interface) section of the [Reticulum Manual]({ASSET_PATH}m), she can just copy-and-paste in a new configuration section for the interface, and edit the radio parameters to her requirements. She ends up with a configuration file that looks like this in it's entirity: + +``` +[reticulum] + enable_transport = False + share_instance = Yes + + shared_instance_port = 37428 + instance_control_port = 37429 + + panic_on_interface_error = No + +[logging] + loglevel = 4 + +[interfaces] + [[Default Interface]] + type = AutoInterface + interface_enabled = True + + [[RNode LoRa Interface]] + type = RNodeInterface + interface_enabled = True + port = /dev/ttyUSB0 + frequency = 867200000 + bandwidth = 125000 + txpower = 7 + spreadingfactor = 8 + codingrate = 5 +``` + +*Please note that the assignment and use of radio frequency spectrum is completely outside the scope of this exploratory post. Laws and regulations about spectrum use vary greatly around the world, and you will have to do your own research for what frequencies and modes you can use in your location, and what licenses, if any, are required for any given use case.* + +Alice can now start the Nomad Network client again, and this time around it will initialise and use the LoRa radio installed in her attic. Having completed Alices part of the setup, lets move on to Bobs apartment. + +# Installation at Bobs Apartment +Bob likes his messaging to happen on a handy device like a phone, so he decides to go with the [Sideband]({ASSET_PATH}s_sideband.html) app instead of Nomad Network. He goes to the [download page](https://github.com/markqvist/Sideband/releases/latest) and installs the APK on his Android phone. He now needs a way to connect to the LoRa radio already running at Alices house to establish communication. + +Since he doesn't want to walk around with the LoRa radio constantly dangling by a USB cable from his phone, he decides to set up a Reticulum gateway in his apartment using a Raspberry Pi he had lying around. The RNode LoRa radio will connect via USB to the Raspberry Pi, and the Raspberry Pi will be connected to the WiFi network in his apartment. + +This way, any device on his WiFi network (including his Android phone) will be able to route information through the LoRa radio as well. Reticulum takes care of everything automatically, and there is no need to configure addresses, subnet, routing rules or anything. + +Both his WiFi router and the Rasperry Pi is powered by a small battery system, so even if the power goes out, the system will be able to stay on for several days on the battery, and indefinitely if he props up a solar panel on his balcony. + +Bob installs a fresh copy of Raspberry Pi OS on the small computer, and in the terminal issues the following command to install Reticulum: + +``` +pip install rns +``` + +In this case, Bob will not be running any user-facing software on the Raspberry Pi itself, so instead he starts Reticulum as a service, by running the `rnsd` program, to check that everything installed correctly: + +``` +rnsd +``` + +After a moment, the following output is shown from the `rnsd` program, signalling that everything is working properly, but that a new, default configuration file has just been created: + +``` +[2022-03-26 17:14:05] [Notice] Could not load config file, creating default configuration file... +[2022-03-26 17:14:05] [Notice] Default config file created. Make any necessary changes in /home/bob/.reticulum/config and restart Reticulum if needed. +[2022-03-26 17:14:09] [Notice] Started rnsd version 0.3.3 +``` + +Bob terminates the `rnsd` program, and then connects the LoRa radio to the Raspberry Pi with a USB cable. Since he doesn't have any particular access to the roof or attic of the building, he just sticky-tapes the LoRa radio to a window facing in the general direction of Alices house. + +He then proceeds to add the same interface configuration to his Reticulum configuration file as Alice did, so that the radio parameters of their respective LoRa radios match each other. + +To allow other devices on his network to route through his new Reticulum gateway, he also adds the line `enable_transport = yes` to his Reticulum config file, so the file in it's entirity looks like this: + +``` +[reticulum] + enable_transport = Yes + share_instance = Yes + + shared_instance_port = 37428 + instance_control_port = 37429 + + panic_on_interface_error = No + +[logging] + loglevel = 4 + +[interfaces] + [[Default Interface]] + type = AutoInterface + interface_enabled = True + + [[RNode LoRa Interface]] + type = RNodeInterface + interface_enabled = True + port = /dev/ttyUSB0 + frequency = 867200000 + bandwidth = 125000 + txpower = 7 + spreadingfactor = 8 + codingrate = 5 +``` + + After starting the program again, this time using `rnsd -vvv` to get more verbose output, he can now see that the LoRa radio is correctly configured and used by Reticulum: + +``` +[2022-03-26 18:17:43] [Debug] Bringing up system interfaces... +[2022-03-26 18:17:43] [Verbose] AutoInterface[Default Interface] discovering peers for 1.8 seconds... +[2022-03-26 18:17:45] [Notice] Opening serial port /dev/ttyUSB0... +[2022-03-26 18:17:47] [Notice] Serial port /dev/ttyUSB0 is now open +[2022-03-26 18:17:47] [Verbose] Configuring RNode interface... +[2022-03-26 18:17:47] [Verbose] Wating for radio configuration validation for RNodeInterface[RNode LoRa Interface]... +[2022-03-26 18:17:47] [Debug] RNodeInterface[RNode LoRa Interface] Radio reporting frequency is 867.2 MHz +[2022-03-26 18:17:47] [Debug] RNodeInterface[RNode LoRa Interface] Radio reporting bandwidth is 125 KHz +[2022-03-26 18:17:47] [Debug] RNodeInterface[RNode LoRa Interface] Radio reporting TX power is 7 dBm +[2022-03-26 18:17:47] [Debug] RNodeInterface[RNode LoRa Interface] Radio reporting spreading factor is 8 +[2022-03-26 18:17:47] [Debug] RNodeInterface[RNode LoRa Interface] Radio reporting coding rate is 5 +[2022-03-26 18:17:47] [Verbose] RNodeInterface[RNode LoRa Interface] On-air bitrate is now 3.1 kbps +[2022-03-26 18:17:47] [Notice] RNodeInterface[RNode LoRa Interface] is configured and powered up +[2022-03-26 18:17:48] [Debug] System interfaces are ready +[2022-03-26 18:17:48] [Verbose] Configuration loaded from /home/bob/.reticulum/config +[2022-03-26 18:17:50] [Verbose] Loaded 0 path table entries from storage +[2022-03-26 18:17:50] [Verbose] Loaded 0 tunnel table entries from storage +[2022-03-26 18:17:50] [Verbose] Transport instance started +[2022-03-26 18:17:50] [Notice] Started rnsd version 0.3.3 +``` + +Everything is ready, and when Bob launches the Sideband appplication on his phone, Alice and him will now be able to communicate securely and independently of any other infrastructure. + +# Communication +Both the [Nomad Network]({ASSET_PATH}s_nn.html) program and the [Sideband]({ASSET_PATH}s_sidband.html) application use a cryptographic message delivery system named [LXMF]({ASSET_PATH}s_lxmf.html), that in turn uses Reticulum for encryption and privacy guarantees. Both Nomad Network and Sideband are *LXMF clients*. + +Much like many different e-mail clients exist, so can many different LXMF clients, and they can all communicate with each other, which is why Alice and Bob can message each other even though they prefer to use very different kinds of user-facing software. + +An LXMF addresses consist of 32 hexadecimal characters, and are usually encapsulated in single angle quotation marks like this: `<9824f6367015b30f2d7b8a24bc6205d7>`. + +Nobody controls the allocation of addresses, and since the address space is so huge, and governed by cryptographic principles, you can create as many or as few adresses as you need. + +Since you can just create them with freely avaible software, and without any sort of permission from anyone, they are never linked to any personally identifiable information either. They are completely and truly anonymous from the beginning, and you control how much or how little of your identity you associate with them. + +For an LXMF address to be reachable for direct-delivery instant messaging on a Reticulum network, it must announce it's public keys on the network. Both Sideband and Nomad Network allows you to send an announce on the network, and both programs can be configured to do so automatically when they start. If you only want to use the system for "email-style" communication (via LXMF propagation nodes), you don't *need* to send any announces on the network, but to learn how it all works, it is a good idea to just set the programs to automatically announce at start up. + +To make sure his public cryptographic key is known by the network, Bob taps the **Announce** button in the Sideband app: + +

+ +After a few seconds, Bobs announce shows up in the **Announce Stream** section of the Nomad Network program on Alices computer: + +

+ +Using the received announce, Alice starts a conversation with Bob. Either one of them could also have started the conversation by manually typing in the others LXMF address in their program, but in many cases it can be convenient to use the announces. Now that everything is ready, they exchange a few messages to test the system. On Bobs Android phone, this looks like this: + +

+ +And on Alices computer running Nomad Network, it looks like this: + +

+ +Although pretty useful, what we have explored here does not even begin to scratch the surface of what is possible with Reticulum and associated software. I hope you will find yourself inspired to explore and read deeper into the documentation and available software. + +To learn more, take a look at the [Learn]({ASSET_PATH}learn.html) section. diff --git a/Console/source/guides/make_rnodes.md b/Console/source/guides/make_rnodes.md new file mode 100755 index 0000000..6190b3f --- /dev/null +++ b/Console/source/guides/make_rnodes.md @@ -0,0 +1,111 @@ +[date]: <> (2023-01-10) +[title]: <> (How To Make Your Own RNodes) +[image]: <> (images/g3p.webp) +[excerpt]: <> (This article will outline the general process, and provide the information you need, for building your own RNode from a few basic modules. The RNode will be functionally identical to a commercially purchased board.) +# How To Make Your Own RNodes + +This article will outline the general process, and provide the information you need, for building your own RNode from a few basic modules. The RNode will be functionally identical to a purchased device. + +Once you have learned the put together a custom RNode with your own choice of components, you can use these skills to create your own RNode designs from scratch, using either a custom-designed PCB, or simply by mounting your choice of modules in a enclosure or case. + +If you haven't already, you migh also want to check out how to [install the RNode firmware directly on pre-made LoRa development boards]({ASSET_PATH}guides/install_firmware.html). + +![A Homemade RNode]({ASSET_PATH}images/g3p.webp) +
*A homemade RNode, based on an ESP32 board and a transceiver module, ready for use*
+ +Since there is not *one right way* to cut this pie, this article will probably not give the *exact* steps for the combination of components you choose, but will instead attempt to provide you with the information you need to build RNodes from a wide variety of microcontroller boards and LoRa modules. Generally speaking, you will need three things to construct a working RNode: + +- A supported microcontroller board +- A supported transceiver module +- A way to mount and connect the two + +## Preparing the Hardware + +Currently, the RNode firmware supports a variety of different microcontrollers, and more are being added regurlarly. That means that there is a *lot* of boards to choose from. You can probably use most boards that are based on either the **ATmega1284P**, **ATmega2560** or **ESP32** microcontrollers. Regarding microcontroller boards there is a few key points to take note of: + +- You will need to connect the transceiver module over the SPI bus. This means that the board should have SPI pins for exposed for you to connect to. UART-only modules will **not** work. +- Logic voltage levels must match the transceiver module you are using, or you will have to add a voltage level converter in between the two devices, that is fast enough for the clock of the SPI bus (usually 8 or 10MHz). I recommend using a microcontroller and transceiver module with matching logic levels. Most will be 3.3 volts. +- Apart from the SPI pins for *clock*, *chip select*, *MOSI* and *MISO*, you will also need an output pin for a *reset* line to the transceiver module, and one **interrupt-capable** input pin for the interrupt signal from the transceiver module. Almost all boards should have plenty of IO available for this, but you might as well make sure before ordering anything. +- You need to choose a board that can provide enough power on it's internal regulators to power the transceiver module while it is transmitting. This can draw quite a bit of power, and some boards only have very small 3.3v regulators, which will not cut it while driving the transmitter at full tilt. + +Regarding the LoRa transceiver module, there is going to be an almost overwhelming amount of options to choose from. To narrow it down, here are the essential characteristics to look for: + +- The RNode firmware needs a module based on the **Semtech SX1276**, **Semtech SX1278**, **SX1262**, **SX1268** and **SX1280** LoRa transceiver ICs. These come in several different variants, for all frequency bands from about 150 MHz to 2500 MHz. +- The module *must* expose the direct SPI bus to the transceiver chip. UART based modules that add their own communications layer will not work. +- The module must also expose the *reset* line of the chip, and provide the **DIO0** (or other relevant) interrupt signal *from* the chip. +- As mentioned above, the module must be logic-level compatible with the microcontroller you are using, unless you want to add a level-shifter. Resistor divider arrays will most likely not work here, due to the bus speeds required. + +Keeping those things in mind, you should be able to select a suitable combination of microcontroller board and transceiver module. + +## Assembling the RNode + +Ok, having gone through the endless combinations and selected a board and a module, you are actually almost done. Connecting the devices together is pretty simple, and should only take a few minutes. I recommend that you place both devices in a solderless breadboard initially, to make sure everything is working as expected. Once you have a working setup, you can make it more durable and permanent by soldering it to a prototyping board, and connecting permanent lines between the devices. + +In the photo above I used an Adafruit Feather ESP32 board and a ModTronix inAir4 module. That will result in an RNode suitable for the 420 MHz to 520 MHz range. To complete the device I did the following: + +1. Connect the GND pin of the microcontroller board to the GND rail of the breadboard. +2. Connect the GND pin of the transceiver module to the GND rail of the breadboard. +3. Connect the 3.3 volt output line of the microcontroller board to the V_IN pin of the transceiver module. +4. Connect the *chip select* pin of the microcontroller board to the *chip select* pin of the transceiver module. +5. Connect the *SPI clock* pin of the microcontroller board to the *SPI clock* pin of the transceiver module. +6. Connect the *MOSI* pin of microcontroller board to the *MOSI* pin of the transceiver module. +7. Connect the *MISO* pin of the microcontroller board to the *MISO* pin of the transceiver module. +8. Connect the *transceiver reset* pin of the microcontroller board to the *reset* pin of the transceiver module. +9. Connect the *DIO0* pin of the transceiver module to the *DIO0 interrupt pin* of the microcontroller board. +10. You can optionally connect transmit and receiver LEDs to the corresponding pins of the microcontroller board. + +The pin layouts of your transceiver module and microcontroller board will vary, but you can look up the correct pin assignments for your processor type and board layout in the [Config.h](https://github.com/markqvist/RNode_Firmware/blob/master/Config.h) file of the [RNode Firmware](https://unsigned.io/rnode_firmware). + +### Loading the Firmware +Once the hardware is assembled, you are ready to load the firmware onto the board and configure the configuration parameters in the boards EEPROM. Luckily, this process is completely automated by the [RNode Configuration Utility](https://markqvist.github.io/Reticulum/manual/using.html#the-rnodeconf-utility). To prepare for loading the firmware, make sure that `python` and `pip` is installed on your system, then install the `rns` package (which includes the `rnodeconf` program) by issuing the command: + + +```txt +pip install rns +``` + +If installation goes well, you can now move on to the next step. + +> *Take Care*: A LoRa transceiver module **must** be connected to the board for the firmware to start and accept commands. If the firmware does not verify that the correct transceiver is available on the SPI bus, execution is stopped, and the board will not accept commands. If you find the board unresponsive after installing the firmware, or EEPROM configuration fails, double-check your transceiver module wiring! + +Having double-checked that everything is connected correctly, it is time to power up the board and install the firmware. Run the `rnodeconf` autoinstaller by executing the command: + +```txt +rnodeconf --autoinstall +``` + +The installer will now ask you to insert the device you want to set up, scan for connected serial ports, and ask you a number of questions regarding the device. When it has the information it needs, it will install the correct firmware and configure the necessary parameters in the device EEPROM for it to function properly. + +If the install goes well, you will be greated with a success message telling you that your device is now ready. To confirm everything is OK, you can query the device info with: + +```txt +rnodeconf --info /dev/ttyUSB0 +``` + +Remember to replace `/dev/ttyUSB0` with the actual port the installer used in the previous step. You should now see `rnodeconf` connect to your device and show something like this: + +```txt +[2022-01-27 20:11:22] Opening serial port /dev/ttyUSB0... +[2022-01-27 20:11:25] Device connected +[2022-01-27 20:11:25] Current firmware version: 1.26 +[2022-01-27 20:11:25] Reading EEPROM... +[2022-01-27 20:11:25] EEPROM checksum correct +[2022-01-27 20:11:25] Device signature validated +[2022-01-27 20:11:25] +[2022-01-27 20:11:25] Device info: +[2022-01-27 20:11:25] Product : LilyGO LoRa32 v2.0 850 - 950 MHz (b0:b8:36) +[2022-01-27 20:11:25] Device signature : Validated - Local signature +[2022-01-27 20:11:25] Firmware version : 1.26 +[2022-01-27 20:11:25] Hardware revision : 1 +[2022-01-27 20:11:25] Serial number : 00:00:00:02 +[2022-01-27 20:11:25] Frequency range : 850.0 MHz - 950.0 MHz +[2022-01-27 20:11:25] Max TX power : 17 dBm +[2022-01-27 20:11:25] Manufactured : 2022-01-27 20:10:32 +[2022-01-27 20:11:25] Device mode : Normal (host-controlled) +``` + +On the hardware side, you should see the status LED flashing briefly approximately every 2 seconds. If all of the above checks out, congratulations! Your RNode is now ready to use. + +If you want to use it with [Reticulum]({ASSET_PATH}s_rns.html), [Nomad Network]({ASSET_PATH}s_nn.html), [LoRaMon](https://unsigned.io/loramon), or other such applications, leave it in the default `Normal (host-controlled)` mode. + +If you want to use it with legacy amateur radio applications that work with KISS TNCs, you should [set it up in TNC mode]({ASSET_PATH}guides/tnc_mode.html). diff --git a/Console/source/guides/tnc_mode.md b/Console/source/guides/tnc_mode.md new file mode 100755 index 0000000..732c890 --- /dev/null +++ b/Console/source/guides/tnc_mode.md @@ -0,0 +1,22 @@ +[date]: <> (2023-01-07) +[title]: <> (Using an RNode With Amateur Radio Software) +[image]: <> (images/g4p.webp) +[excerpt]: <> (If you want to use an RNode with amateur radio applications, like APRS or a packet radio BBS, you will need to put the device into TNC Mode. In this mode, an RNode will behave exactly like a KISS-compatible TNC, which will make it usable with any amateur radio software.) + +# Using an RNode With Amateur Radio Software + +If you want to use an RNode with amateur radio applications, like APRS or a packet radio BBS, you will need to put the device into *TNC Mode*. In this mode, an RNode will behave exactly like a KISS-compatible TNC, which will make it usable with any amateur radio software that can talk to a KISS TNC over a serial port. + +You can use the [RNode Configuration Utility]({ASSET_PATH}m/using.html#the-rnodeconf-utility) to change settings on your device, including putting it into TNC mode. + +The `rnodeconf` program is included in the `rns` package. Please read [these instructions]({ASSET_PATH}s_rns.html) for more information on how to install it from this repository, or from the Internet. + +With the `rnodeconf` program installed, you can put your RNode into TNC mode simply by entering the command: + +``` +rnodeconf -T /dev/ttyUSB0 +``` + +Remember to replace `/dev/ttyUSB0` with the actual port your RNode is connected to. The program will now ask you for the channel configuration parameters, like frequency, bandwidth, transmission power and so on. It is also possible to specify all the parameters at once on the command line, see the `rnodeconf --help` for information on how to do this. + +That's all there is to it! Your RNode is now configured in TNC mode, and ready for use with amateur radio applications. diff --git a/Console/source/help.md b/Console/source/help.md new file mode 100755 index 0000000..d6dd72d --- /dev/null +++ b/Console/source/help.md @@ -0,0 +1,14 @@ +[title]: <> (Get Help) +## Get Help +If you are having trouble, or if something is not working, this RNode contains a number of useful resources. + +- Read [Questions & Answers](qa.html) section +- Read the [Reticulum Manual](m/index.html) stored on this RNode +- Browse a copy of the [Reticulum Website](r/index.html) stored on this RNode + +## Community & Support +If things still aren't working as expected here are some great places to ask for help: + +- The [discussion forum](https://github.com/markqvist/Reticulum/discussions) on GitHub +- The [Reticulum Matrix Channel](element://room/!TRaVWNnQhAbvuiSnEK%3Amatrix.org?via=matrix.org) at `#reticulum:matrix.org` +- The [Reticulum subreddit](https://reddit.com/r/reticulum) diff --git a/Console/source/index.md b/Console/source/index.md new file mode 100755 index 0000000..e51055d --- /dev/null +++ b/Console/source/index.md @@ -0,0 +1,28 @@ +## Hello! + + + + + + + +
+You have connected to the RNode Bootstrap Console.
+
+The tools and information contained in this RNode will allow you to replicate the RNode design, build more RNodes and grow your communications ecosystems.
+
+This repository also contains tools, software and information necessary to bootstrap networks and communications systems based on RNodes and Reticulum. +
+
+
+
+

What would you like to do?

+
You can browse this repository freely, or jump straight into a task-oriented workflow by selecting one of the starting points below.
+ + + + + + + +
diff --git a/Console/source/learn.md b/Console/source/learn.md new file mode 100755 index 0000000..53eda53 --- /dev/null +++ b/Console/source/learn.md @@ -0,0 +1,14 @@ +[title]: <> (Learn More) +## Learn More +This RNode contains a selection of tutorials and guides on setting up communications, creating RNodes, building networks and using Reticulum. You can learn more by: + +- Reading the [What is an RNode?](rnode.html) page +- Checking the [Questions & Answers](qa.html) section +- Reading the [Reticulum Manual](m/index.html) stored on this RNode +- Browsing a copy of the [Reticulum Website]({ASSET_PATH}r/index.html) stored on this RNode +- Visiting the [unsigned.io](https://unsigned.io/) website +- You can also find **unsigned.io** on Nomad Network, at `ec58b0e430cd9628907383954feea068` + +## Guides + +{TOPIC:guides} diff --git a/Console/source/qa.md b/Console/source/qa.md new file mode 100755 index 0000000..ec83bcd --- /dev/null +++ b/Console/source/qa.md @@ -0,0 +1,20 @@ +[title]: <> (Questions & Answers) +## Questions & Answers +This section contains a list of common questions, and associated answers. + +- **What are the system requirements for running Reticulum?** +Practically any system that can run Python3 can also run Reticulum. Any computer made since the early 2000's should work, provided it has a reasonably up-to-date operating system installed. Even low-power embedded devices with 256 megabytes of RAM will run Reticulum. +- **Does Reticulum work without the Internet?** +Yes. Reticulum *is* itself both a networking, and an inter-net protocol. A key difference between Reticulum and IPv4/v6, however, is that Reticulum does not require any central coordination or authority to work. As soon as two devices running Reticulum can talk to each other, they form a network. That network can dynamically grow to planetary-scale nets, split up, re-connect and heal in any number of ways, while still continuing to function. As long as there is *some sort of physical way* for two or more devices to communicate, Reticulum will allow them to form a secure and reliable network. +- **Who owns and controls the addresses I use on a Reticulum network?** +You do. Every address is in complete ownership and control of the person that created it. +- **If nobody centrally controls the addresses, will my address still be globally reachable?** +Yes. Reticulum ensures end-to-end connectivity. All addresses are globally and directly reachable. Reticulum has no concept of "private address spaces" and NAT, as you might be suffering from with IPv4. +- **Is communication over Reticulum encrypted?** +Yes. All traffic is end-to-end encrypted. Reticulum *is fundamentally unable to route unencrypted traffic*. Links established over Reticulum networks offer forward secrecy, by using ephemeral encryption keys. +- **Could you build a global Internet with Reticulum instead of IP?** +Yes. In theory this is completely possible, but it will take a lot of refinement, development, hardware support and adoption to transition the global base-layer for communication to Reticulum. Please [help us]({ASSET_PATH}contribute.html) towards this goal! +- **Is Reticulum as fast and optimised as my favorite TCP/IP stack?** +Currently not, but we are working towards being much faster than IP. The primary focus of Reticulum has been to build an understandable and well-documented *reference implementation*, that works exceptionally well over medium-bandwidth to extremely low-bandwidth forms of communication. This focus is very valuable, since it allows people to build secure communications networks that span vast areas, with very simple hardware, and very little cost. +- **Who created all of this?** +The Reticulum protocol, and the RNode system was created by [Mark Qvist]({ASSET_PATH}contact.html), of [unsigned.io](https://unsigned.io). \ No newline at end of file diff --git a/Console/source/recipes.md b/Console/source/recipes.md new file mode 100755 index 0000000..690602a --- /dev/null +++ b/Console/source/recipes.md @@ -0,0 +1,5 @@ +[title]: <> (RNode Recipes) +## RNode Build Recipes +This section contains a library of build recipes for various types of RNodes. All the recipes contain necessary plans, instructions and 3D-printable files for completing the build. + +{TOPIC:builds} diff --git a/Console/source/replicate.md b/Console/source/replicate.md new file mode 100755 index 0000000..5d43ebb --- /dev/null +++ b/Console/source/replicate.md @@ -0,0 +1,27 @@ +[title]: <> (Replicate) +## Create RNodes +This section contains the tools and guides necessary to create more RNodes. Creating any number of RNodes is **completely free and unrestricted** for all personal, non-commercial and humanitarian purposes. If doing so provides value to you or your community, you are encouraged to [contribute](./contribute.html) whatever you find to be reasonable. + +If you want to create RNodes for sale or commercial purposes, please read the [selling RNodes]({ASSET_PATH}sell_rnodes.html) section for more details. + +### Firmware Source Code +If you would like to inspect or compile the RNode Firmware source code yourself, you can download a copy of the [RNode Firmware source-code]({ASSET_PATH}pkg/rnode_firmware.zip) stored in this RNode. + +### Getting Started +To create your own RNodes, there are generally three distinct paths you can take: + +- The first, and easiest option, is to [create a basic RNode]({ASSET_PATH}guides/install_firmware.html) from one of the supported development boards. This option allows you to simply acquire a board from any online or local vendor that sells them, and then use the `rnodeconf` program to automatically turn it into an RNode. Such an RNode will be functionally equivalent to the other options, but might lack some niceties. +- The second option is to use one of the [RNode Build Recipes]({ASSET_PATH}recipes.html) included here. These recipes contain all the resources needed to build a specific type of RNode, such as a handheld device, or an outdoor-mountable, solar-powered access point. +- The third option is to [create your own RNode design]({ASSET_PATH}guides/make_rnodes.html) from scratch. This offers unlimited flexibility, but is a bit more involved. + +If you already have some experience with 3D-printing and electronics projects, the recommended path is to pick a [build recipe]({ASSET_PATH}recipes.html) for the RNode type you want. That way, you will get a neat and portable unit that's ready for real-world use. + +If you are just getting started, it might be nice to get a working "proof-of-concept" with minimal effort first, though. In such a case, [creating a basic RNode]({ASSET_PATH}guides/install_firmware.html) is a good starting point. +

+
+

Choose a path to get started

+
+ + + +
diff --git a/Console/source/rnode.md b/Console/source/rnode.md new file mode 100755 index 0000000..a34782e --- /dev/null +++ b/Console/source/rnode.md @@ -0,0 +1,11 @@ +[title]: <> (What is an RNode?) +## What is an RNode? +An RNode is an open, free and unrestricted digital radio transceiver. It enables anyone to send and receive any kind of data over both short and very long distances. RNodes can be used with many different kinds of programs and systems, but they are especially well suited for use with Reticulum. + +RNode is not a product, and not any one specific device in particular. It is a system that is easy to replicate across space and time, that produces highly functional communications tools, which respects user autonomy and empowers individuals and communities to protect their sovereignty and privacy. + +The RNode system is primarily software, which *transforms* available hardware devices into functional, physical RNodes, which can then be used to solve a wide range of communications tasks. Such RNodes can be modified and build to suit the specific time, locale and environment they need to exist in. + +If you notice the presence of a circularity in the naming of the system as a whole, and the physical devices, it is no coincidence. Every RNode contains the seeds necessary to reproduce the system, and create more RNodes, and even to bootstrap entire communications networks, completely independently of existing infrastructure, or the lack thereof. + +The production of one particular RNode device is not an end, but the potential starting point of a new branch of devices on the tree of the RNode system as a whole. This tree fits into the larger biome of Free & Open Communications Systems, which I hope that you - by using communications tools like RNode - will help grow and prosper. diff --git a/Console/source/s_lxmf.md b/Console/source/s_lxmf.md new file mode 100755 index 0000000..7171208 --- /dev/null +++ b/Console/source/s_lxmf.md @@ -0,0 +1,23 @@ +[title]: <> (LXMF) +## LXMF +LXMF is a simple and flexible messaging format and delivery protocol that allows a wide variety of implementations, while using as little bandwidth as possible. It is built on top of [Reticulum](https://reticulum.network) and offers zero-conf message routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports. + +LXMF is efficient enough that it can deliver messages over extremely low-bandwidth systems such as packet radio or LoRa. Encrypted LXMF messages can also be encoded as QR-codes or text-based URIs, allowing completely analog *paper message* transport. + +Installing this LXMF library allows other programs on your system, like Nomad Network, to use the LXMF messaging system. It also includes the `lxmd` program that you can use to run LXMF propagation nodes on your network. + +**Local Installation** + +If you do not have access to the Internet, or would prefer to install LXMF directly from this RNode, you can use the following instructions. + +- If you do not have an Internet connection while installing make sure to install the [Reticulum](./s_rns.html) package first +- Download the [{PKG_BASE_lxmf}]({ASSET_PATH}{PKG_lxmf}) package from this RNode and unzip it +- Install it with the command `pip install ./{PKG_NAME_lxmf}` +- Verify the installed Reticulum version by running `lxmd --version` + +**Online Installation** + +If you are connected to the Internet, you can try to install the latest version of LXMF via the `pip` package manager. + +- Install Nomad Network by running the command `pip install lxmf` +- Verify the installed Reticulum version by running `lxmd --version` diff --git a/Console/source/s_nn.md b/Console/source/s_nn.md new file mode 100755 index 0000000..f54e68d --- /dev/null +++ b/Console/source/s_nn.md @@ -0,0 +1,27 @@ +[title]: <> (Nomad Network) +## Nomad Network +Off-grid, resilient mesh communication with strong encryption, forward secrecy and extreme privacy. + +Nomad Network Allows you to build private and resilient communications platforms that are in complete control and ownership of the people that use them. No signups, no agreements, no handover of any data, no permissions and gatekeepers. + +![Screenshot]({ASSET_PATH}gfx/nn.webp) + +Nomad Network is build on [LXMF](lxmf.html) and [Reticulum]({ASSET_PATH}r/), which together provides the cryptographic mesh functionality and peer-to-peer message routing that Nomad Network relies on. This foundation also makes it possible to use the program over a very wide variety of communication mediums, from packet radio to fiber optics. + +Nomad Network does not need any connections to the public internet to work. In fact, it doesn't even need an IP or Ethernet network. You can use it entirely over packet radio, LoRa or even serial lines. But if you wish, you can bridge islanded networks over the Internet or private ethernet networks, or you can build networks running completely over the Internet. The choice is yours. + +### Local Installation + +If you do not have access to the Internet, or would prefer to install Nomad Network directly from this RNode, you can use the following instructions. + +- If you do not have an Internet connection while installing make sure to install the [Reticulum](./s_rns.html) and [LXMF](./s_lxmf.html) packages first +- Download the [{PKG_BASE_nomadnet}]({ASSET_PATH}{PKG_nomadnet}) package from this RNode and unzip it +- Install it with the command `pip install ./{PKG_NAME_nomadnet}` +- Verify the installed Nomad Network version by running `nomadnet --version` + +### Online Installation + +If you are connected to the Internet, you can try to install the latest version of Nomad Network via the `pip` package manager. + +- Install Nomad Network by running the command `pip install nomadnet` +- Verify the installed Nomad Network version by running `nomadnet --version` diff --git a/Console/source/s_rns.md b/Console/source/s_rns.md new file mode 100755 index 0000000..7c33a69 --- /dev/null +++ b/Console/source/s_rns.md @@ -0,0 +1,33 @@ +[title]: <> (Reticulum) +## Reticulum +The cryptographic networking stack for building resilient networks anywhere. The vision of Reticulum is to allow anyone to operate their own sovereign communication networks, and to make it cheap and easy to cover vast areas with a myriad of independent, interconnectable and autonomous networks. Reticulum is Unstoppable Networks for The People. + +

+ +This packages requires you have `python` and `pip` installed on your computer. This should come as standard on most operating systems released since 2020. + +### Local Installation +If you do not have access to the Internet, or would prefer to install Reticulum directly from this RNode, you can use the following instructions. + +- Download the [{PKG_BASE_rns}]({ASSET_PATH}{PKG_rns}) package from this RNode and unzip it +- Install it with the command `pip install ./{PKG_NAME_rns}` +- Verify the installed Reticulum version by running `rnstatus --version` + +### Online Installation +If you are connected to the Internet, you can try to install the latest version of Reticulum via the `pip` package manager. + +- Install Reticulum by running the command `pip install rns` +- Verify the installed Reticulum version by running `rnstatus --version` + +### Dependencies +If the installation has problems resolving dependencies, first try installing the `python-cryptography`, `python-netifaces` and `python-pyserial` packages from your systems package manager. + +If this fails, or is simply not possible in your situation, you can make the installation of Reticulum ignore the resolution of dependencies using the command: + +`pip install --no-dependencies ./{PKG_NAME_rns}` + +This will allow you to install Reticulum on systems, or in circumstances, where one or more dependencies cannot be resolved. This will most likely mean that some functionality will not be available, which may be a worthwhile tradeoff in some situations. + +If you use this method of installation, it is essential to read the [Pure-Python Reticulum]({ASSET_PATH}m/gettingstartedfast.html#pure-python-reticulum) section of the Reticulum Manual, and to understand the potential security implications of this installation method. + +For more detailed information, please read the entire [Getting Started section of the Reticulum Manual]({ASSET_PATH}m/gettingstartedfast.html). diff --git a/Console/source/s_rnsh.md b/Console/source/s_rnsh.md new file mode 100755 index 0000000..deb5adb --- /dev/null +++ b/Console/source/s_rnsh.md @@ -0,0 +1,20 @@ +[title]: <> (Shell Over Reticulum) +## Shell Over Reticulum + +The `rnsh` program lets you establish fully interactive remote shell sessions over Reticulum. It also allows you to pipe any program to or from a remote system, and is similar to how the ``ssh`` program works. + +### Local Installation + +If you do not have access to the Internet, or would prefer to install `rnsh` directly from this RNode, you can use the following instructions. + +- If you do not have an Internet connection while installing make sure to install the [Reticulum](./s_rns.html) package first +- Download the [{PKG_BASE_rnsh}]({ASSET_PATH}{PKG_rnsh}) package from this RNode and unzip it +- Install it with the command `pip install ./{PKG_NAME_rnsh}` +- Verify the installed `rnsh` version by running `rnsh --version` + +### Online Installation + +If you are connected to the Internet, you can try to install the latest version of `rnsh` via the `pip` package manager. + +- Install `rnsh` by running the command `pip install rnsh` +- Verify the installed `rnsh` version by running `rnsh --version` diff --git a/Console/source/s_sideband.md b/Console/source/s_sideband.md new file mode 100755 index 0000000..5b06d2f --- /dev/null +++ b/Console/source/s_sideband.md @@ -0,0 +1,12 @@ +[title]: <> (Sideband) +## Sideband +Sideband is an LXMF client for Android, Linux, Windows and macOS. It has built-in support for communicating over RNodes, and many other mediums, such as Packet Radio, WiFi, I2P, or anything else Reticulum supports. + +Sideband also supports voice calls, file transfers, and exchanging messages through encrypted QR-codes on paper, or through messages embedded directly in lxm:// links. + +![Screenshot]({ASSET_PATH}gfx/sideband.webp) + +The installation files for the Sideband program is too large to be included on this RNode, but downloads for Linux, Android and macOS can be obtained from following sources: + +- The [Sideband page](https://unsigned.io/sideband/) on [unsigned.io](https://unsigned.io/) +- The [GitHub release page for Sideband](https://github.com/markqvist/Sideband/releases/latest) diff --git a/Console/source/sell_rnodes.md b/Console/source/sell_rnodes.md new file mode 100755 index 0000000..31e2cdb --- /dev/null +++ b/Console/source/sell_rnodes.md @@ -0,0 +1,9 @@ +[title]: <> (Sell RNodes) +## Build & Sell RNodes +Creating any number of RNodes is completely free and unrestricted for all personal, non-commercial and humanitarian purposes. Feel free to use all the resources provided here, and on the [unsigned.io](https://unsigned.io/) website. If doing so provides value to you or your community, you are encouraged to [contribute]({ASSET_PATH}contribute.html) whatever you find to be reasonable. + +The RNode Ecosystem is free and non-proprietary, and actively seeks to distribute it's ownership and control. If you want to build RNodes for commercial purposes, including selling them, you must do so adhering to the Open Source licenses that the various parts of the RNode project is released under, and under your own responsibility. + +The RNode Firmware is released under GPLv3, and basing commercial works on it means (among other things), that you must also make your derivatives open source and available under the same terms. + +In practice, this means that you can use the firmware commercially, but you should understand your obligation to provide all future users of the system the same rights that you have been provided by the GPLv3. \ No newline at end of file diff --git a/Console/source/software.md b/Console/source/software.md new file mode 100755 index 0000000..a3bca99 --- /dev/null +++ b/Console/source/software.md @@ -0,0 +1,23 @@ +[title]: <> (Software) +## Software +This RNode contains a repository of downloadable software and utilities, that are useful for bootstrapping communications networks, and for replicating RNodes. + +**Please Note!** Whenever you install software onto your computer, there is a risk that someone modified this software to include malicious code. Be extra careful installing anything from this RNode, if you did not get it from a source you trust, or if there is a risk it was modified in transit. + +If possible, you can check that the `SHA-256` hashes of any downloaded files correspond to the list of release hashes published on the [Reticulum Release page](https://github.com/markqvist/Reticulum/releases). + +**You Have The Source!** Due to the size limitations of shipping all this software within an RNode, we don't include separate source-code archives for the below programs, but *all the source code is included within the Python .whl files*! + +You can simply unzip any of them with any program that understands `zip` files, and you will find the source code inside the unzipped directory (for some zip programs, you may need to change the file ending to `.zip`). + +You can also download the copy of the [RNode Firmware source-code]({ASSET_PATH}pkg/rnode_firmware.zip) that is stored in this RNode. +

+
+

Choose a software package to get started

+
+ + + + + +
\ No newline at end of file diff --git a/Console/source/supported.md b/Console/source/supported.md new file mode 100755 index 0000000..7be87b9 --- /dev/null +++ b/Console/source/supported.md @@ -0,0 +1,17 @@ +[title]: <> (Supported Hardware) +## Supported Boards & Devices +The RNode Firmware supports the following boards: + +- Handheld v2.x RNodes from [unsigned.io](https://unsigned.io/shop/product/handheld-rnode) +- Original v1.x RNodes from [unsigned.io](https://unsigned.io/shop/product/rnode) +- LilyGO T-Beam v1.1 devices +- LilyGO LoRa32 v2.0 devices +- LilyGO LoRa32 v2.1 devices +- Heltec LoRa32 v2 devices +- Homebrew RNodes based on ATmega1284p boards +- Homebrew RNodes based on ATmega2560 boards +- Homebrew RNodes based on Adafruit Feather ESP32 boards +- Homebrew RNodes based on generic ESP32 boards + +## Supported Transceiver Modules +The RNode Firmware supports all transceiver modules based on **Semtech SX1276** or **Semtech SX1278** chips, that have an **SPI interface** and expose the **DIO_0** interrupt pin from the chip. \ No newline at end of file diff --git a/Device.h b/Device.h new file mode 100755 index 0000000..e2a1d06 --- /dev/null +++ b/Device.h @@ -0,0 +1,267 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +#if MCU_VARIANT == MCU_ESP32 +#include "mbedtls/md.h" +#include "esp_ota_ops.h" +#include "esp_flash_partitions.h" +#include "esp_partition.h" + +#elif MCU_VARIANT == MCU_NRF52 +#include "Adafruit_nRFCrypto.h" + +// size of chunk to retrieve from flash sector +#define CHUNK_SIZE 128 + +#define END_SECTION_SIZE 256 + +#if defined(NRF52840_XXAA) +// https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather/hathach-memory-map +// each section follows along from one another, in this order +// this is always at the start of the memory map +#define APPLICATION_START 0x26000 + +#define USER_DATA_START 0xED000 + +#define IMG_SIZE_START 0xFF008 +#endif + +#endif + +// Forward declaration from Utilities.h +void eeprom_update(int mapped_addr, uint8_t byte); +uint8_t eeprom_read(uint32_t addr); +void hard_reset(void); + +#if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 + void eeprom_flush(); +#endif + +const uint8_t dev_keys [] PROGMEM = { + 0x0f, 0x15, 0x86, 0x74, 0xa0, 0x7d, 0xf2, 0xde, 0x32, 0x11, 0x29, 0xc1, 0x0d, 0xda, 0xcc, 0xc3, + 0xe1, 0x9b, 0xac, 0xf2, 0x27, 0x06, 0xee, 0x89, 0x1f, 0x7a, 0xfc, 0xc3, 0x6a, 0xf5, 0x38, 0x08 +}; + +#define DEV_SIG_LEN 64 +uint8_t dev_sig[DEV_SIG_LEN]; + +#define DEV_KEY_LEN 32 +uint8_t dev_k_prv[DEV_KEY_LEN]; +uint8_t dev_k_pub[DEV_KEY_LEN]; + +#define DEV_HASH_LEN 32 +uint8_t dev_hash[DEV_HASH_LEN]; +uint8_t dev_partition_table_hash[DEV_HASH_LEN]; +uint8_t dev_bootloader_hash[DEV_HASH_LEN]; +uint8_t dev_firmware_hash[DEV_HASH_LEN]; +uint8_t dev_firmware_hash_target[DEV_HASH_LEN]; + +#define EEPROM_SIG_LEN 128 +uint8_t dev_eeprom_signature[EEPROM_SIG_LEN]; + +bool dev_signature_validated = false; +bool fw_signature_validated = true; + +#define DEV_SIG_OFFSET EEPROM_SIZE-EEPROM_RESERVED-DEV_SIG_LEN +#define dev_sig_addr(a) (a+DEV_SIG_OFFSET) + +#define DEV_FWHASH_OFFSET EEPROM_SIZE-EEPROM_RESERVED-DEV_SIG_LEN-DEV_HASH_LEN +#define dev_fwhash_addr(a) (a+DEV_FWHASH_OFFSET) + +bool device_signatures_ok() { + return dev_signature_validated && fw_signature_validated; +} + +void device_validate_signature() { + int n_keys = sizeof(dev_keys)/DEV_KEY_LEN; + bool valid_signature_found = false; + for (int i = 0; i < n_keys; i++) { + memcpy(dev_k_pub, dev_keys+DEV_KEY_LEN*i, DEV_KEY_LEN); + if (Ed25519::verify(dev_sig, dev_k_pub, dev_hash, DEV_HASH_LEN)) { + valid_signature_found = true; + } + } + + if (valid_signature_found) { + dev_signature_validated = true; + } else { + dev_signature_validated = false; + } +} + +void device_save_signature() { + device_validate_signature(); + if (dev_signature_validated) { + for (uint8_t i = 0; i < DEV_SIG_LEN; i++) { + eeprom_update(dev_sig_addr(i), dev_sig[i]); + } + } +} + +void device_load_signature() { + for (uint8_t i = 0; i < DEV_SIG_LEN; i++) { + #if HAS_EEPROM + dev_sig[i] = EEPROM.read(dev_sig_addr(i)); + #elif MCU_VARIANT == MCU_NRF52 + dev_sig[i] = eeprom_read(dev_sig_addr(i)); + #endif + } +} + +void device_load_firmware_hash() { + for (uint8_t i = 0; i < DEV_HASH_LEN; i++) { + #if HAS_EEPROM + dev_firmware_hash_target[i] = EEPROM.read(dev_fwhash_addr(i)); + #elif MCU_VARIANT == MCU_NRF52 + dev_firmware_hash_target[i] = eeprom_read(dev_fwhash_addr(i)); + #endif + } +} + +void device_save_firmware_hash() { + for (uint8_t i = 0; i < DEV_HASH_LEN; i++) { + eeprom_update(dev_fwhash_addr(i), dev_firmware_hash_target[i]); + } + #if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 + eeprom_flush(); + #endif + if (!fw_signature_validated) hard_reset(); +} + +#if MCU_VARIANT == MCU_NRF52 +uint32_t retrieve_application_size() { + uint8_t bytes[4]; + memcpy(bytes, (const void*)IMG_SIZE_START, 4); + uint32_t fw_len = bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24; + return fw_len; +} + +void calculate_region_hash(unsigned long long start, unsigned long long end, uint8_t* return_hash) { + // this function calculates the hash digest of a region of memory, + // currently it is only designed to work for the application region + uint8_t chunk[CHUNK_SIZE] = {0}; + + // to store potential last chunk of program + uint8_t chunk_next[CHUNK_SIZE] = {0}; + nRFCrypto_Hash hash; + + hash.begin(CRYS_HASH_SHA256_mode); + + uint8_t size; + + while (start < end ) { + const void* src = (const void*)start; + if (start + CHUNK_SIZE >= end) { + size = end - start; + } + else { + size = CHUNK_SIZE; + } + + memcpy(chunk, src, CHUNK_SIZE); + + hash.update(chunk, size); + + start += CHUNK_SIZE; + } + hash.end(return_hash); +} +#endif + +void device_validate_partitions() { + device_load_firmware_hash(); + #if MCU_VARIANT == MCU_ESP32 + esp_partition_t partition; + partition.address = ESP_PARTITION_TABLE_OFFSET; + partition.size = ESP_PARTITION_TABLE_MAX_LEN; + partition.type = ESP_PARTITION_TYPE_DATA; + esp_partition_get_sha256(&partition, dev_partition_table_hash); + partition.address = ESP_BOOTLOADER_OFFSET; + partition.size = ESP_PARTITION_TABLE_OFFSET; + partition.type = ESP_PARTITION_TYPE_APP; + esp_partition_get_sha256(&partition, dev_bootloader_hash); + esp_partition_get_sha256(esp_ota_get_running_partition(), dev_firmware_hash); + #elif MCU_VARIANT == MCU_NRF52 + // todo, add bootloader, partition table, or softdevice? + calculate_region_hash(APPLICATION_START, APPLICATION_START+retrieve_application_size(), dev_firmware_hash); + #endif + #if VALIDATE_FIRMWARE + for (uint8_t i = 0; i < DEV_HASH_LEN; i++) { + if (dev_firmware_hash_target[i] != dev_firmware_hash[i]) { + fw_signature_validated = false; + break; + } + } + #endif +} + +bool device_firmware_ok() { + return fw_signature_validated; +} + +#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 +bool device_init() { + if (bt_ready) { + #if MCU_VARIANT == MCU_ESP32 + for (uint8_t i=0; i. + +#include "Graphics.h" +#include + +#if BOARD_MODEL != BOARD_TECHO + #if BOARD_MODEL == BOARD_TDECK + #include + #elif BOARD_MODEL == BOARD_HELTEC_T114 + #include "ST7789.h" + #define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)) + #elif BOARD_MODEL == BOARD_TBEAM_S_V1 + #include + #else + #include + #include + #endif + +#else + void (*display_callback)(); + void display_add_callback(void (*callback)()) { display_callback = callback; } + void busyCallback(const void* p) { display_callback(); } + #define SSD1306_BLACK GxEPD_BLACK + #define SSD1306_WHITE GxEPD_WHITE + #include + #include +#endif + +#include "Fonts/Org_01.h" + +// Forward declaration for boundary mode display +#ifdef BOUNDARY_MODE +#ifndef BOUNDARY_STATE_DEFINED +#define BOUNDARY_STATE_DEFINED +struct BoundaryState { + bool enabled; + bool wifi_enabled; + uint8_t tcp_mode; // 0=disabled, 1=client + uint16_t tcp_port; + char backbone_host[64]; + uint16_t backbone_port; + bool ap_tcp_enabled; + uint16_t ap_tcp_port; + char ap_ssid[33]; + char ap_psk[33]; + bool wifi_connected; + bool tcp_connected; // Backbone (WAN) connected + bool ap_tcp_connected; // Local TCP server (LAN) has client + bool ap_active; + uint32_t packets_bridged_lora_to_tcp; + uint32_t packets_bridged_tcp_to_lora; + uint32_t last_bridge_activity; +}; +#endif // BOUNDARY_STATE_DEFINED +extern BoundaryState boundary_state; +#endif + +#define DISP_W 128 +#define DISP_H 64 + +#if BOARD_MODEL == BOARD_RNODE_NG_20 || BOARD_MODEL == BOARD_LORA32_V2_0 + #define DISP_RST -1 + #define DISP_ADDR 0x3C +#elif BOARD_MODEL == BOARD_TBEAM + #define DISP_RST 13 + #define DISP_ADDR 0x3C + #define DISP_CUSTOM_ADDR true +#elif BOARD_MODEL == BOARD_HELTEC32_V2 || BOARD_MODEL == BOARD_LORA32_V1_0 + #define DISP_RST 16 + #define DISP_ADDR 0x3C + #define SCL_OLED 15 + #define SDA_OLED 4 +#elif BOARD_MODEL == BOARD_HELTEC32_V3 + #define DISP_RST 21 + #define DISP_ADDR 0x3C + #define SCL_OLED 18 + #define SDA_OLED 17 +#elif BOARD_MODEL == BOARD_HELTEC32_V4 + #define DISP_RST 21 + #define DISP_ADDR 0x3C + #define SCL_OLED 18 + #define SDA_OLED 17 +#elif BOARD_MODEL == BOARD_RAK4631 + // RAK1921/SSD1306 + #define DISP_RST -1 + #define DISP_ADDR 0x3C + #define SCL_OLED 14 + #define SDA_OLED 13 +#elif BOARD_MODEL == BOARD_RNODE_NG_21 + #define DISP_RST -1 + #define DISP_ADDR 0x3C +#elif BOARD_MODEL == BOARD_T3S3 + #define DISP_RST 21 + #define DISP_ADDR 0x3C + #define SCL_OLED 17 + #define SDA_OLED 18 +#elif BOARD_MODEL == BOARD_TECHO + SPIClass displaySPI = SPIClass(NRF_SPIM0, pin_disp_miso, pin_disp_sck, pin_disp_mosi); + #define DISP_W 128 + #define DISP_H 64 + #define DISP_ADDR -1 +#elif BOARD_MODEL == BOARD_TBEAM_S_V1 + #define DISP_RST -1 + #define DISP_ADDR 0x3C + #define SCL_OLED 18 + #define SDA_OLED 17 + #define DISP_CUSTOM_ADDR false +#elif BOARD_MODEL == BOARD_XIAO_S3 + #define DISP_RST -1 + #define DISP_ADDR 0x3C + #define SCL_OLED 6 + #define SDA_OLED 5 + #define DISP_CUSTOM_ADDR true +#else + #define DISP_RST -1 + #define DISP_ADDR 0x3C + #define DISP_CUSTOM_ADDR true +#endif + +#define SMALL_FONT &Org_01 + +#if BOARD_MODEL == BOARD_TDECK + Adafruit_ST7789 display = Adafruit_ST7789(DISPLAY_CS, DISPLAY_DC, -1); + #define SSD1306_WHITE ST77XX_WHITE + #define SSD1306_BLACK ST77XX_BLACK +#elif BOARD_MODEL == BOARD_HELTEC_T114 + ST7789Spi display(&SPI1, DISPLAY_RST, DISPLAY_DC, DISPLAY_CS); + #define SSD1306_WHITE ST77XX_WHITE + #define SSD1306_BLACK ST77XX_BLACK +#elif BOARD_MODEL == BOARD_TBEAM_S_V1 + Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1); + #define SSD1306_WHITE SH110X_WHITE + #define SSD1306_BLACK SH110X_BLACK +#elif BOARD_MODEL == BOARD_TECHO + GxEPD2_BW display(GxEPD2_154_D67(pin_disp_cs, pin_disp_dc, pin_disp_reset, pin_disp_busy)); + uint32_t last_epd_refresh = 0; + uint32_t last_epd_full_refresh = 0; + #define REFRESH_PERIOD 300000 +#else + Adafruit_SSD1306 display(DISP_W, DISP_H, &Wire, DISP_RST); +#endif + +float disp_target_fps = 7; +float epd_update_fps = 0.5; + +#define DISP_MODE_UNKNOWN 0x00 +#define DISP_MODE_LANDSCAPE 0x01 +#define DISP_MODE_PORTRAIT 0x02 +#define DISP_PIN_SIZE 6 +#define DISPLAY_BLANKING_TIMEOUT 15*1000 +uint8_t disp_mode = DISP_MODE_UNKNOWN; +uint8_t disp_ext_fb = false; +unsigned char fb[512]; +uint32_t last_disp_update = 0; +uint32_t last_unblank_event = 0; +uint32_t display_blanking_timeout = DISPLAY_BLANKING_TIMEOUT; +uint8_t display_unblank_intensity = display_intensity; +bool display_blanked = false; +bool display_tx = false; +bool recondition_display = false; +int disp_update_interval = 1000/disp_target_fps; +int epd_update_interval = 1000/disp_target_fps; +uint32_t last_page_flip = 0; +int page_interval = 4000; +bool device_signatures_ok(); +bool device_firmware_ok(); + +#define WATERFALL_SIZE 46 +int waterfall[WATERFALL_SIZE]; +int waterfall_meta[WATERFALL_SIZE]; +int waterfall_head = 0; + +int p_ad_x = 0; +int p_ad_y = 0; +int p_as_x = 0; +int p_as_y = 0; + +GFXcanvas1 stat_area(64, 64); +GFXcanvas1 disp_area(64, 64); + +static const uint8_t one_counts[256] = { + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 1, 1, 1, + 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, + 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 +}; + +void fillRect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t colour); + +void update_area_positions() { + #if BOARD_MODEL == BOARD_HELTEC_T114 + if (disp_mode == DISP_MODE_PORTRAIT) { + p_ad_x = 16; + p_ad_y = 64; + p_as_x = 16; + p_as_y = p_ad_y+126; + } else if (disp_mode == DISP_MODE_LANDSCAPE) { + p_ad_x = 0; + p_ad_y = 96; + p_as_x = 126; + p_as_y = p_ad_y; + } + #elif BOARD_MODEL == BOARD_TECHO + if (disp_mode == DISP_MODE_PORTRAIT) { + p_ad_x = 61; + p_ad_y = 36; + p_as_x = 64; + p_as_y = 64+36; + } else if (disp_mode == DISP_MODE_LANDSCAPE) { + p_ad_x = 0; + p_ad_y = 0; + p_as_x = 64; + p_as_y = 0; + } + #else + if (disp_mode == DISP_MODE_PORTRAIT) { + p_ad_x = 0 * DISPLAY_SCALE; + p_ad_y = 0 * DISPLAY_SCALE; + p_as_x = 0 * DISPLAY_SCALE; + p_as_y = 64 * DISPLAY_SCALE; + } else if (disp_mode == DISP_MODE_LANDSCAPE) { + p_ad_x = 0 * DISPLAY_SCALE; + p_ad_y = 0 * DISPLAY_SCALE; + p_as_x = 64 * DISPLAY_SCALE; + p_as_y = 0 * DISPLAY_SCALE; + } + #endif +} + +uint8_t display_contrast = 0x00; +#if BOARD_MODEL == BOARD_TBEAM_S_V1 + void set_contrast(Adafruit_SH1106G *display, uint8_t value) { + } +#elif BOARD_MODEL == BOARD_HELTEC_T114 + void set_contrast(ST7789Spi *display, uint8_t value) { } +#elif BOARD_MODEL == BOARD_TECHO + void set_contrast(void *display, uint8_t value) { + if (value == 0) { analogWrite(pin_backlight, 0); } + else { analogWrite(pin_backlight, value); } + } +#elif BOARD_MODEL == BOARD_TDECK + void set_contrast(Adafruit_ST7789 *display, uint8_t value) { + static uint8_t level = 0; + static uint8_t steps = 16; + if (value > 15) value = 15; + if (value == 0) { + digitalWrite(DISPLAY_BL_PIN, 0); + delay(3); + level = 0; + return; + } + if (level == 0) { + digitalWrite(DISPLAY_BL_PIN, 1); + level = steps; + delayMicroseconds(30); + } + int from = steps - level; + int to = steps - value; + int num = (steps + to - from) % steps; + for (int i = 0; i < num; i++) { + digitalWrite(DISPLAY_BL_PIN, 0); + digitalWrite(DISPLAY_BL_PIN, 1); + } + level = value; + } +#else + void set_contrast(Adafruit_SSD1306 *display, uint8_t contrast) { + display->ssd1306_command(SSD1306_SETCONTRAST); + display->ssd1306_command(contrast); + } +#endif + +bool display_init() { + #if HAS_DISPLAY + #if BOARD_MODEL == BOARD_RNODE_NG_20 || BOARD_MODEL == BOARD_LORA32_V2_0 + int pin_display_en = 16; + digitalWrite(pin_display_en, LOW); + delay(50); + digitalWrite(pin_display_en, HIGH); + #elif BOARD_MODEL == BOARD_T3S3 + Wire.begin(SDA_OLED, SCL_OLED); + #elif BOARD_MODEL == BOARD_HELTEC32_V2 + Wire.begin(SDA_OLED, SCL_OLED); + #elif BOARD_MODEL == BOARD_HELTEC32_V3 + // enable vext / pin 36 + pinMode(Vext, OUTPUT); + digitalWrite(Vext, LOW); + delay(50); + int pin_display_en = 21; + pinMode(pin_display_en, OUTPUT); + digitalWrite(pin_display_en, LOW); + delay(50); + digitalWrite(pin_display_en, HIGH); + delay(50); + Wire.begin(SDA_OLED, SCL_OLED); + #elif BOARD_MODEL == BOARD_HELTEC32_V4 + // enable vext / pin 36 + pinMode(Vext, OUTPUT); + digitalWrite(Vext, LOW); + delay(50); + int pin_display_en = 21; + pinMode(pin_display_en, OUTPUT); + digitalWrite(pin_display_en, LOW); + delay(50); + digitalWrite(pin_display_en, HIGH); + delay(50); + Wire.begin(SDA_OLED, SCL_OLED); + #elif BOARD_MODEL == BOARD_LORA32_V1_0 + int pin_display_en = 16; + digitalWrite(pin_display_en, LOW); + delay(50); + digitalWrite(pin_display_en, HIGH); + Wire.begin(SDA_OLED, SCL_OLED); + #elif BOARD_MODEL == BOARD_HELTEC_T114 + pinMode(PIN_T114_TFT_EN, OUTPUT); + digitalWrite(PIN_T114_TFT_EN, LOW); + #elif BOARD_MODEL == BOARD_TECHO + display.init(0, true, 10, false, displaySPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); + display.setPartialWindow(0, 0, DISP_W, DISP_H); + display.epd2.setBusyCallback(busyCallback); + #if HAS_BACKLIGHT + pinMode(pin_backlight, OUTPUT); + analogWrite(pin_backlight, 0); + #endif + #elif BOARD_MODEL == BOARD_TBEAM_S_V1 + Wire.begin(SDA_OLED, SCL_OLED); + #elif BOARD_MODEL == BOARD_XIAO_S3 + Wire.begin(SDA_OLED, SCL_OLED); + #endif + + #if HAS_EEPROM + uint8_t display_rotation = EEPROM.read(eeprom_addr(ADDR_CONF_DROT)); + #elif MCU_VARIANT == MCU_NRF52 + uint8_t display_rotation = eeprom_read(eeprom_addr(ADDR_CONF_DROT)); + #endif + if (display_rotation < 0 or display_rotation > 3) display_rotation = 0xFF; + + #if DISP_CUSTOM_ADDR == true + #if HAS_EEPROM + uint8_t display_address = EEPROM.read(eeprom_addr(ADDR_CONF_DADR)); + #elif MCU_VARIANT == MCU_NRF52 + uint8_t display_address = eeprom_read(eeprom_addr(ADDR_CONF_DADR)); + #endif + if (display_address == 0xFF) display_address = DISP_ADDR; + #else + uint8_t display_address = DISP_ADDR; + #endif + + #if HAS_EEPROM + if (EEPROM.read(eeprom_addr(ADDR_CONF_BSET)) == CONF_OK_BYTE) { + uint8_t db_timeout = EEPROM.read(eeprom_addr(ADDR_CONF_DBLK)); + if (db_timeout == 0x00) { + display_blanking_enabled = false; + } else { + display_blanking_enabled = true; + display_blanking_timeout = db_timeout*1000; + } + } + #elif MCU_VARIANT == MCU_NRF52 + if (eeprom_read(eeprom_addr(ADDR_CONF_BSET)) == CONF_OK_BYTE) { + uint8_t db_timeout = eeprom_read(eeprom_addr(ADDR_CONF_DBLK)); + if (db_timeout == 0x00) { + display_blanking_enabled = false; + } else { + display_blanking_enabled = true; + display_blanking_timeout = db_timeout*1000; + } + } + #endif + + #if BOARD_MODEL == BOARD_TECHO + // Don't check if display is actually connected + if(false) { + #elif BOARD_MODEL == BOARD_TDECK + display.init(240, 320); + display.setSPISpeed(80e6); + #elif BOARD_MODEL == BOARD_HELTEC_T114 + display.init(); + // set white as default pixel colour for Heltec T114 + display.setRGB(COLOR565(0xFF, 0xFF, 0xFF)); + if (false) { + #elif BOARD_MODEL == BOARD_TBEAM_S_V1 + if (!display.begin(display_address, true)) { + #else + if (!display.begin(SSD1306_SWITCHCAPVCC, display_address)) { + #endif + return false; + } else { + set_contrast(&display, display_contrast); + if (display_rotation != 0xFF) { + if (display_rotation == 0 || display_rotation == 2) { + disp_mode = DISP_MODE_LANDSCAPE; + } else { + disp_mode = DISP_MODE_PORTRAIT; + } + display.setRotation(display_rotation); + } else { + #if BOARD_MODEL == BOARD_RNODE_NG_20 + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(3); + #elif BOARD_MODEL == BOARD_RNODE_NG_21 + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(3); + #elif BOARD_MODEL == BOARD_LORA32_V1_0 + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(3); + #elif BOARD_MODEL == BOARD_LORA32_V2_0 + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(3); + #elif BOARD_MODEL == BOARD_LORA32_V2_1 + disp_mode = DISP_MODE_LANDSCAPE; + display.setRotation(0); + #elif BOARD_MODEL == BOARD_TBEAM + disp_mode = DISP_MODE_LANDSCAPE; + display.setRotation(0); + #elif BOARD_MODEL == BOARD_TBEAM_S_V1 + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(1); + #elif BOARD_MODEL == BOARD_HELTEC32_V2 + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(1); + #elif BOARD_MODEL == BOARD_HELTEC32_V3 + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(1); + #elif BOARD_MODEL == BOARD_HELTEC32_V4 + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(1); + #elif BOARD_MODEL == BOARD_HELTEC_T114 + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(1); + #elif BOARD_MODEL == BOARD_RAK4631 + disp_mode = DISP_MODE_LANDSCAPE; + display.setRotation(0); + #elif BOARD_MODEL == BOARD_TDECK + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(3); + #elif BOARD_MODEL == BOARD_TECHO + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(3); + #else + disp_mode = DISP_MODE_PORTRAIT; + display.setRotation(3); + #endif + } + + update_area_positions(); + + for (int i = 0; i < WATERFALL_SIZE; i++) { waterfall[i] = 0; } + + last_page_flip = millis(); + + stat_area.cp437(true); + disp_area.cp437(true); + + #if BOARD_MODEL != BOARD_HELTEC_T114 + display.cp437(true); + #endif + + #if HAS_EEPROM + display_intensity = EEPROM.read(eeprom_addr(ADDR_CONF_DINT)); + #elif MCU_VARIANT == MCU_NRF52 + display_intensity = eeprom_read(eeprom_addr(ADDR_CONF_DINT)); + #endif + display_unblank_intensity = display_intensity; + + #if BOARD_MODEL == BOARD_TECHO + #if HAS_BACKLIGHT + if (display_intensity == 0) { analogWrite(pin_backlight, 0); } + else { analogWrite(pin_backlight, display_intensity); } + #endif + #endif + + #if BOARD_MODEL == BOARD_TDECK + display.fillScreen(SSD1306_BLACK); + #endif + + #if BOARD_MODEL == BOARD_HELTEC_T114 + // Enable backlight led (display is always black without this) + fillRect(p_ad_x, p_ad_y, 128, 128, SSD1306_BLACK); + fillRect(p_as_x, p_as_y, 128, 128, SSD1306_BLACK); + pinMode(PIN_T114_TFT_BLGT, OUTPUT); + digitalWrite(PIN_T114_TFT_BLGT, LOW); + #endif + + return true; + } + #else + return false; + #endif +} + +// Draws a line on the screen +void drawLine(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t colour) { + #if BOARD_MODEL == BOARD_HELTEC_T114 + if(colour == SSD1306_WHITE){ + display.setColor(WHITE); + } else if(colour == SSD1306_BLACK) { + display.setColor(BLACK); + } + display.drawLine(x, y, width, height); + #else + display.drawLine(x, y, width, height, colour); + #endif +} + +// Draws a filled rectangle on the screen +void fillRect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t colour) { + #if BOARD_MODEL == BOARD_HELTEC_T114 + if(colour == SSD1306_WHITE){ + display.setColor(WHITE); + } else if(colour == SSD1306_BLACK) { + display.setColor(BLACK); + } + display.fillRect(x, y, width, height); + #else + display.fillRect(x, y, width, height, colour); + #endif +} + +// Draws a bitmap to the display and auto scales it based on the boards configured DISPLAY_SCALE +void drawBitmap(int16_t startX, int16_t startY, const uint8_t* bitmap, int16_t bitmapWidth, int16_t bitmapHeight, uint16_t foregroundColour, uint16_t backgroundColour) { + #if DISPLAY_SCALE == 1 + display.drawBitmap(startX, startY, bitmap, bitmapWidth, bitmapHeight, foregroundColour, backgroundColour); + #else + for(int16_t row = 0; row < bitmapHeight; row++){ + for(int16_t col = 0; col < bitmapWidth; col++){ + + // determine index and bitmask + int16_t index = row * ((bitmapWidth + 7) / 8) + (col / 8); + uint8_t bitmask = 1 << (7 - (col % 8)); + + // check if the current pixel is set in the bitmap + if(bitmap[index] & bitmask){ + // draw a scaled rectangle for the foreground pixel + fillRect(startX + col * DISPLAY_SCALE, startY + row * DISPLAY_SCALE, DISPLAY_SCALE, DISPLAY_SCALE, foregroundColour); + } else { + // draw a scaled rectangle for the background pixel + fillRect(startX + col * DISPLAY_SCALE, startY + row * DISPLAY_SCALE, DISPLAY_SCALE, DISPLAY_SCALE, backgroundColour); + } + + } + } + #endif +} + +extern uint8_t wifi_mode; +extern bool wifi_is_connected(); +extern bool wifi_host_is_connected(); +void draw_cable_icon(int px, int py) { + #if HAS_WIFI + if (wifi_mode == WR_WIFI_OFF) { + if (cable_state == CABLE_STATE_DISCONNECTED) { stat_area.drawBitmap(px, py, bm_cable+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); } + else if (cable_state == CABLE_STATE_CONNECTED) { stat_area.drawBitmap(px, py, bm_cable+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); } + } else { + if (wifi_mode == WR_WIFI_STA) { + if (wifi_is_connected()) { + stat_area.drawBitmap(px, py, bm_wifi+3*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + if (!wifi_host_is_connected()) { stat_area.fillRect(px+5, py+12, 6, 3, SSD1306_BLACK); } + } else { stat_area.drawBitmap(px, py, bm_wifi+2*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); } + + } else if (wifi_mode == WR_WIFI_AP) { + if (wifi_host_is_connected()) { stat_area.drawBitmap(px, py, bm_wifi+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); } + else { stat_area.drawBitmap(px, py, bm_wifi+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); } + + } else { + if (cable_state == CABLE_STATE_DISCONNECTED) { stat_area.drawBitmap(px, py, bm_cable+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); } + else if (cable_state == CABLE_STATE_CONNECTED) { stat_area.drawBitmap(px, py, bm_cable+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); } + } + } + + #else + if (cable_state == CABLE_STATE_DISCONNECTED) { stat_area.drawBitmap(px, py, bm_cable+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); } + else if (cable_state == CABLE_STATE_CONNECTED) { stat_area.drawBitmap(px, py, bm_cable+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); } + #endif +} + +void draw_bt_icon(int px, int py) { + if (bt_state == BT_STATE_OFF) { + stat_area.drawBitmap(px, py, bm_bt+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + } else if (bt_state == BT_STATE_ON) { + stat_area.drawBitmap(px, py, bm_bt+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + } else if (bt_state == BT_STATE_PAIRING) { + stat_area.drawBitmap(px, py, bm_bt+2*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + } else if (bt_state == BT_STATE_CONNECTED) { + stat_area.drawBitmap(px, py, bm_bt+3*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + } else { + stat_area.drawBitmap(px, py, bm_bt+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + } +} + +void draw_lora_icon(int px, int py) { + if (radio_online) { + stat_area.drawBitmap(px, py, bm_rf+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + } else { + stat_area.drawBitmap(px, py, bm_rf+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + } +} + +void draw_mw_icon(int px, int py) { + if (mw_radio_online) { + stat_area.drawBitmap(px, py, bm_rf+3*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + } else { + stat_area.drawBitmap(px, py, bm_rf+2*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); + } +} + +uint8_t charge_tick = 0; +void draw_battery_bars(int px, int py) { + if (pmu_ready) { + if (battery_ready) { + if (battery_installed) { + float battery_value = battery_percent; + + // Disable charging state display for now, since + // boards without dedicated PMU are completely + // unreliable for determining actual charging state. + bool disable_charge_status = false; + if (battery_indeterminate && battery_state == BATTERY_STATE_CHARGING) { + disable_charge_status = true; + } + + if (battery_state == BATTERY_STATE_CHARGING && !disable_charge_status) { + float battery_prog = battery_percent; + if (battery_prog > 85) { battery_prog = 84; } + if (charge_tick < battery_prog ) { charge_tick = battery_prog; } + battery_value = charge_tick; + charge_tick += 3; + if (charge_tick > 100) charge_tick = 0; + } + + if (battery_indeterminate && battery_state == BATTERY_STATE_CHARGING && !disable_charge_status) { + stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK); + stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK); + } else { + if (battery_state == BATTERY_STATE_CHARGED) { + stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK); + stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK); + } else { + // stat_area.fillRect(px, py, 14, 3, SSD1306_BLACK); + stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK); + stat_area.drawRect(px-2, py-2, 17, 7, SSD1306_WHITE); + stat_area.drawLine(px+15, py, px+15, py+3, SSD1306_WHITE); + if (battery_value > 7) stat_area.drawLine(px, py, px, py+2, SSD1306_WHITE); + if (battery_value > 20) stat_area.drawLine(px+1*2, py, px+1*2, py+2, SSD1306_WHITE); + if (battery_value > 33) stat_area.drawLine(px+2*2, py, px+2*2, py+2, SSD1306_WHITE); + if (battery_value > 46) stat_area.drawLine(px+3*2, py, px+3*2, py+2, SSD1306_WHITE); + if (battery_value > 59) stat_area.drawLine(px+4*2, py, px+4*2, py+2, SSD1306_WHITE); + if (battery_value > 72) stat_area.drawLine(px+5*2, py, px+5*2, py+2, SSD1306_WHITE); + if (battery_value > 85) stat_area.drawLine(px+6*2, py, px+6*2, py+2, SSD1306_WHITE); + } + } + } else { + stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK); + stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK); + } + } + } else { + stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK); + stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK); + } +} + +#define Q_SNR_STEP 2.0 +#define Q_SNR_MIN_BASE -9.0 +#define Q_SNR_MAX 6.0 +void draw_quality_bars(int px, int py) { + stat_area.fillRect(px, py, 13, 7, SSD1306_BLACK); + if (radio_online) { + signed char t_snr = (signed int)last_snr_raw; + int snr_int = (int)t_snr; + float snr_min = Q_SNR_MIN_BASE-(int)lora_sf*Q_SNR_STEP; + float snr_span = (Q_SNR_MAX-snr_min); + float snr = ((int)snr_int) * 0.25; + float quality = ((snr-snr_min)/(snr_span))*100; + if (quality > 100.0) quality = 100.0; + if (quality < 0.0) quality = 0.0; + + // Serial.printf("Last SNR: %.2f\n, quality: %.2f\n", snr, quality); + if (quality > 0) stat_area.drawLine(px+0*2, py+7, px+0*2, py+6, SSD1306_WHITE); + if (quality > 15) stat_area.drawLine(px+1*2, py+7, px+1*2, py+5, SSD1306_WHITE); + if (quality > 30) stat_area.drawLine(px+2*2, py+7, px+2*2, py+4, SSD1306_WHITE); + if (quality > 45) stat_area.drawLine(px+3*2, py+7, px+3*2, py+3, SSD1306_WHITE); + if (quality > 60) stat_area.drawLine(px+4*2, py+7, px+4*2, py+2, SSD1306_WHITE); + if (quality > 75) stat_area.drawLine(px+5*2, py+7, px+5*2, py+1, SSD1306_WHITE); + if (quality > 90) stat_area.drawLine(px+6*2, py+7, px+6*2, py+0, SSD1306_WHITE); + } +} + +#if MODEM == SX1280 + #define S_RSSI_MIN -105.0 + #define S_RSSI_MAX -65.0 +#else + #define S_RSSI_MIN -135.0 + #define S_RSSI_MAX -75.0 +#endif +#define S_RSSI_SPAN (S_RSSI_MAX-S_RSSI_MIN) +void draw_signal_bars(int px, int py) { + stat_area.fillRect(px, py, 13, 7, SSD1306_BLACK); + + if (radio_online) { + int rssi_val = last_rssi; + if (rssi_val < S_RSSI_MIN) rssi_val = S_RSSI_MIN; + if (rssi_val > S_RSSI_MAX) rssi_val = S_RSSI_MAX; + int signal = ((rssi_val - S_RSSI_MIN)*(1.0/S_RSSI_SPAN))*100.0; + + if (signal > 100.0) signal = 100.0; + if (signal < 0.0) signal = 0.0; + + // Serial.printf("Last SNR: %.2f\n, quality: %.2f\n", snr, quality); + if (signal > 85) stat_area.drawLine(px+0*2, py+7, px+0*2, py+0, SSD1306_WHITE); + if (signal > 72) stat_area.drawLine(px+1*2, py+7, px+1*2, py+1, SSD1306_WHITE); + if (signal > 59) stat_area.drawLine(px+2*2, py+7, px+2*2, py+2, SSD1306_WHITE); + if (signal > 46) stat_area.drawLine(px+3*2, py+7, px+3*2, py+3, SSD1306_WHITE); + if (signal > 33) stat_area.drawLine(px+4*2, py+7, px+4*2, py+4, SSD1306_WHITE); + if (signal > 20) stat_area.drawLine(px+5*2, py+7, px+5*2, py+5, SSD1306_WHITE); + if (signal > 7) stat_area.drawLine(px+6*2, py+7, px+6*2, py+6, SSD1306_WHITE); + } +} + +#if MODEM == SX1280 + #define WF_TX_SIZE 5 +#else + #define WF_TX_SIZE 5 +#endif +#define WF_RSSI_MAX -60 +#define WF_RSSI_MIN -135 +#define WF_RSSI_SPAN (WF_RSSI_MAX-WF_RSSI_MIN) +#define WF_PIXEL_WIDTH 10 +#define WF_M_RX 0x00 +#define WF_M_TX 0x01 +#define WF_M_NTFR 0x02 +void draw_waterfall(int px, int py) { + int rssi_val = current_rssi; + if (rssi_val < WF_RSSI_MIN) rssi_val = WF_RSSI_MIN; + if (rssi_val > WF_RSSI_MAX) rssi_val = WF_RSSI_MAX; + int rssi_normalised = ((rssi_val - WF_RSSI_MIN)*(1.0/WF_RSSI_SPAN))*WF_PIXEL_WIDTH; + if (display_tx) { + for (uint8_t i = 0; i < WF_TX_SIZE; i++) { + waterfall_meta[waterfall_head] = WF_M_TX; + waterfall[waterfall_head++] = -1; + if (waterfall_head >= WATERFALL_SIZE) waterfall_head = 0; + } + display_tx = false; + } else { + if (interference_detected) { waterfall_meta[waterfall_head] = WF_M_NTFR; } + else { waterfall_meta[waterfall_head] = WF_M_RX; } + waterfall[waterfall_head++] = rssi_normalised; + if (waterfall_head >= WATERFALL_SIZE) waterfall_head = 0; + } + + stat_area.fillRect(px,py,WF_PIXEL_WIDTH, WATERFALL_SIZE, SSD1306_BLACK); + for (int i = 0; i < WATERFALL_SIZE; i++){ + int wi = (waterfall_head+i)%WATERFALL_SIZE; + int ws = waterfall[wi]; + int wm = waterfall_meta[wi]; + if (ws > 0) { + if (wm == WF_M_RX) { stat_area.drawLine(px, py+i, px+ws-1, py+i, SSD1306_WHITE); } + else if (wm == WF_M_NTFR) { + uint8_t o = 0; + for (uint8_t ti = 0; ti < WF_PIXEL_WIDTH/2; ti++) { stat_area.drawPixel(px+ti*2+o, py+i, SSD1306_WHITE); } + } + } else if (ws == -1) { + uint8_t o = i%2; + for (uint8_t ti = 0; ti < WF_PIXEL_WIDTH/2; ti++) { + stat_area.drawPixel(px+ti*2+o, py+i, SSD1306_WHITE); + } + } + } +} + +bool stat_area_intialised = false; +void draw_stat_area() { + if (device_init_done) { +#ifdef BOUNDARY_MODE + // ── Boundary Mode: 4 status indicators + battery ── + // LORA / WIFI / WAN (backbone) / LAN (local TCP) + stat_area.fillRect(0, 0, 64, 64, SSD1306_BLACK); + stat_area.setFont(SMALL_FONT); + stat_area.setTextColor(SSD1306_WHITE); + stat_area.setTextSize(1); + + // Row 1 — LORA + stat_area.fillCircle(4, 3, 3, radio_online ? SSD1306_WHITE : SSD1306_BLACK); + stat_area.drawCircle(4, 3, 3, SSD1306_WHITE); + stat_area.setCursor(10, 6); + stat_area.print(radio_online ? "LORA" : "lora"); + + // Row 2 — WIFI + if (!boundary_state.wifi_enabled) { + stat_area.drawCircle(4, 13, 3, SSD1306_WHITE); + stat_area.setCursor(10, 16); + stat_area.print("wifi"); + } else if (wifi_is_connected()) { + stat_area.fillCircle(4, 13, 3, SSD1306_WHITE); + stat_area.setCursor(10, 16); + stat_area.print("WIFI"); + } else { + stat_area.drawCircle(4, 13, 3, SSD1306_WHITE); + stat_area.setCursor(10, 16); + stat_area.print("wifi"); + } + + // Row 3 — WAN / backbone TCP + if (boundary_state.tcp_mode == 0) { + stat_area.drawCircle(4, 23, 3, SSD1306_WHITE); + stat_area.setCursor(10, 26); + stat_area.print("wan"); + } else if (boundary_state.tcp_connected) { + stat_area.fillCircle(4, 23, 3, SSD1306_WHITE); + stat_area.setCursor(10, 26); + stat_area.print("WAN"); + } else { + stat_area.drawCircle(4, 23, 3, SSD1306_WHITE); + stat_area.setCursor(10, 26); + stat_area.print("wan"); + } + + // Row 4 — LAN / local TCP server + if (!boundary_state.ap_tcp_enabled) { + stat_area.drawCircle(4, 33, 3, SSD1306_WHITE); + stat_area.setCursor(10, 36); + stat_area.print("LAN"); + } else if (boundary_state.ap_tcp_connected) { + stat_area.fillCircle(4, 33, 3, SSD1306_WHITE); + stat_area.setCursor(10, 36); + stat_area.print("LAN"); + } else { + stat_area.drawCircle(4, 33, 3, SSD1306_WHITE); + stat_area.setCursor(10, 36); + stat_area.print("LAN"); + } + + // 1px separator after LAN + stat_area.drawLine(0, 40, 63, 40, SSD1306_WHITE); + + // Airtime — below LAN + stat_area.setCursor(2, 49); + if (radio_online) { + stat_area.printf("Air:%.1f%%", airtime * 100.0); + } + + // Battery + signal — bottom + draw_battery_bars(4, 56); + if (radio_online) { + draw_quality_bars(28, 56); + draw_signal_bars(44, 56); + } +#else + if (!stat_area_intialised) { + stat_area.drawBitmap(0, 0, bm_frame, 64, 64, SSD1306_WHITE, SSD1306_BLACK); + stat_area_intialised = true; + } + + draw_cable_icon(3, 8); + draw_bt_icon(3, 30); + draw_lora_icon(45, 8); + draw_mw_icon(45, 30); + draw_battery_bars(4, 58); + draw_quality_bars(28, 56); + draw_signal_bars(44, 56); + if (radio_online) { + draw_waterfall(27, 4); + } +#endif + } +} + +void update_stat_area() { + if (eeprom_ok && !firmware_update_mode && !console_active) { + + draw_stat_area(); + if (disp_mode == DISP_MODE_PORTRAIT) { + drawBitmap(p_as_x, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK); + } else if (disp_mode == DISP_MODE_LANDSCAPE) { + drawBitmap(p_as_x+2, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK); + if (device_init_done && !disp_ext_fb) drawLine(p_as_x, 0, p_as_x, 64, SSD1306_WHITE); + } + + } else { + if (firmware_update_mode) { + drawBitmap(p_as_x, p_as_y, bm_updating, stat_area.width(), stat_area.height(), SSD1306_BLACK, SSD1306_WHITE); + } else if (console_active && device_init_done) { + drawBitmap(p_as_x, p_as_y, bm_console, stat_area.width(), stat_area.height(), SSD1306_BLACK, SSD1306_WHITE); + if (disp_mode == DISP_MODE_LANDSCAPE) { + drawLine(p_as_x, 0, p_as_x, 64, SSD1306_WHITE); + } + } + } +} + +#define START_PAGE 0 +const uint8_t pages = 3; +uint8_t disp_page = START_PAGE; +extern char bt_devname[11]; +extern char bt_dh[16]; +#if HAS_WIFI + extern IPAddress wr_device_ip; +#endif +void draw_disp_area() { + if (!device_init_done || firmware_update_mode) { + uint8_t p_by = 37; + if (disp_mode == DISP_MODE_LANDSCAPE || firmware_update_mode) { + p_by = 18; + disp_area.fillRect(0, 0, disp_area.width(), disp_area.height(), SSD1306_BLACK); + } + if (!device_init_done) disp_area.drawBitmap(0, p_by, bm_boot, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + if (firmware_update_mode) disp_area.drawBitmap(0, p_by, bm_fw_update, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + } else { + if (!disp_ext_fb or bt_ssp_pin != 0) { +#ifdef BOUNDARY_MODE + // ── Boundary Mode display: compact status page ── + disp_area.fillRect(0, 0, disp_area.width(), disp_area.height(), SSD1306_BLACK); + + // Title bar + disp_area.fillRect(0, 0, disp_area.width(), 9, SSD1306_WHITE); + disp_area.setFont(SMALL_FONT); + disp_area.setTextWrap(false); + disp_area.setTextColor(SSD1306_BLACK); + disp_area.setTextSize(1); + disp_area.setCursor(4, 7); + disp_area.print("RNodeTHV4"); + + disp_area.setTextColor(SSD1306_WHITE); + + // Radio info + disp_area.setCursor(2, 18); + if (radio_online) { + disp_area.printf("%.3fMHz", (float)lora_freq / 1000000.0); + } else { + disp_area.print("Radio OFF"); + } + + disp_area.setCursor(2, 29); + if (radio_online) { + disp_area.printf("SF%d %.0fk", lora_sf, (float)lora_bw / 1000.0); + } + + // 1px separator after SF line + disp_area.drawLine(0, 34, disp_area.width()-1, 34, SSD1306_WHITE); + + // WiFi IP address + disp_area.setCursor(2, 44); + if (boundary_state.wifi_connected) { + disp_area.print(wr_device_ip); + } else { + disp_area.print("No WiFi"); + } + + // Backbone port + disp_area.setCursor(2, 55); + disp_area.printf("Port:%u", boundary_state.tcp_port); + + // 1px separator after Port line + disp_area.drawLine(0, 60, disp_area.width()-1, 60, SSD1306_WHITE); +#else + if (radio_online && display_diagnostics) { +#ifdef HAS_RNS + // CBA + //if (reticulum && reticulum.transport_enabled()) { + if (op_mode == MODE_TNC) { + // CBA Indicate that this is a Transport node + disp_area.fillRect(0,0,disp_area.width(),7, SSD1306_WHITE); + disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_BLACK); disp_area.setTextSize(1); + disp_area.setCursor(4, 5); + disp_area.print("TRANSPORT"); + } +#endif + + disp_area.fillRect(0,8,disp_area.width(),37, SSD1306_BLACK); disp_area.fillRect(0,37,disp_area.width(),27, SSD1306_WHITE); + disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(1); + + disp_area.setCursor(2, 13); + disp_area.print("On"); + disp_area.setCursor(14, 13); + disp_area.print("@"); + disp_area.setCursor(21, 13); + disp_area.printf("%.1fKbps", (float)lora_bitrate/1000.0); + + //disp_area.setCursor(31, 23-1); + disp_area.setCursor(2, 23-1); + disp_area.print("Airtime:"); + + disp_area.setCursor(11, 33-1); + if (total_channel_util < 0.099) { + //disp_area.printf("%.1f%%", total_channel_util*100.0); + disp_area.printf("%.1f%%", airtime*100.0); + } else { + //disp_area.printf("%.0f%%", total_channel_util*100.0); + disp_area.printf("%.0f%%", airtime*100.0); + } + disp_area.drawBitmap(2, 26-1, bm_hg_low, 5, 9, SSD1306_WHITE, SSD1306_BLACK); + + disp_area.setCursor(32+11, 33-1); + if (longterm_channel_util < 0.099) { + //disp_area.printf("%.1f%%", longterm_channel_util*100.0); + disp_area.printf("%.1f%%", longterm_airtime*100.0); + } else { + //disp_area.printf("%.0f%%", longterm_channel_util*100.0); + disp_area.printf("%.0f%%", longterm_airtime*100.0); + } + disp_area.drawBitmap(32+2, 26-1, bm_hg_high, 5, 9, SSD1306_WHITE, SSD1306_BLACK); + + + disp_area.setTextColor(SSD1306_BLACK); + disp_area.setCursor(2, 46); + disp_area.print("Channel"); + disp_area.setCursor(38, 46); + disp_area.print("Load:"); + + disp_area.setCursor(11, 57); + if (total_channel_util < 0.099) { + //disp_area.printf("%.1f%%", airtime*100.0); + disp_area.printf("%.1f%%", total_channel_util*100.0); + } else { + //disp_area.printf("%.0f%%", airtime*100.0); + disp_area.printf("%.0f%%", total_channel_util*100.0); + } + disp_area.drawBitmap(2, 50, bm_hg_low, 5, 9, SSD1306_BLACK, SSD1306_WHITE); + + disp_area.setCursor(32+11, 57); + if (longterm_channel_util < 0.099) { + //disp_area.printf("%.1f%%", longterm_airtime*100.0); + disp_area.printf("%.1f%%", longterm_channel_util*100.0); + } else { + //disp_area.printf("%.0f%%", longterm_airtime*100.0); + disp_area.printf("%.0f%%", longterm_channel_util*100.0); + } + disp_area.drawBitmap(32+2, 50, bm_hg_high, 5, 9, SSD1306_BLACK, SSD1306_WHITE); + + } else { + if (device_signatures_ok()) { disp_area.drawBitmap(0, 0, bm_def_lc, disp_area.width(), 23, SSD1306_WHITE, SSD1306_BLACK); } + else { disp_area.drawBitmap(0, 0, bm_def, disp_area.width(), 23, SSD1306_WHITE, SSD1306_BLACK); } + + bool display_ip = false; + #if HAS_WIFI + if (wifi_is_connected() && disp_page%2 == 1) { display_ip = true; } + #endif + if (display_ip) { + #if HAS_WIFI + uint8_t ones = 3+one_counts[wr_device_ip[0]]+one_counts[wr_device_ip[1]]+one_counts[wr_device_ip[2]]+one_counts[wr_device_ip[3]]; + uint8_t chars = 7; + for (uint8_t i = 0; i<4; i++) { if (wr_device_ip[i] > 9) { chars++; } if (wr_device_ip[i] > 99) { chars++; } } + uint8_t width = chars*6-(ones*4); + int alignment_offset = disp_area.width()-width; + int ipxpos = alignment_offset; + disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(1); + disp_area.fillRect(0, 20, disp_area.width(), 17, SSD1306_BLACK); + disp_area.setCursor(3, 34-8); disp_area.print("WiFi IP:"); + disp_area.setCursor(ipxpos, 34); disp_area.print(wr_device_ip); + #endif + } else { + disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(2); + disp_area.fillRect(0, 20, disp_area.width(), 17, SSD1306_BLACK); uint8_t ofsc = 0; + if ((bt_dh[14] & 0b00001111) == 0x01) { ofsc += 8; } + if ((bt_dh[14] >> 4) == 0x01) { ofsc += 8; } + if ((bt_dh[15] & 0b00001111) == 0x01) { ofsc += 8; } + if ((bt_dh[15] >> 4) == 0x01) { ofsc += 8; } + disp_area.setCursor(17+ofsc, 32); disp_area.printf("%02X%02X", bt_dh[14], bt_dh[15]); + } + } + + if (!hw_ready || radio_error || !device_firmware_ok()) { + if (!device_firmware_ok()) { + disp_area.drawBitmap(0, 37, bm_fw_corrupt, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + } else { + if (!modem_installed) { + disp_area.drawBitmap(0, 37, bm_no_radio, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + } else { + disp_area.drawBitmap(0, 37, bm_conf_missing, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + } + } + } else if (bt_state == BT_STATE_PAIRING and bt_ssp_pin != 0) { + char *pin_str = (char*)malloc(DISP_PIN_SIZE+1); + sprintf(pin_str, "%06d", bt_ssp_pin); + + disp_area.drawBitmap(0, 37, bm_pairing, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + for (int i = 0; i < DISP_PIN_SIZE; i++) { + uint8_t numeric = pin_str[i]-48; + uint8_t offset = numeric*5; + disp_area.drawBitmap(7+9*i, 37+16, bm_n_uh+offset, 8, 5, SSD1306_WHITE, SSD1306_BLACK); + } + free(pin_str); + } else { + if (millis()-last_page_flip >= page_interval) { + disp_page = (++disp_page%pages); + last_page_flip = millis(); + if (not community_fw and disp_page == 0) disp_page = 1; + } + + if (radio_online) { + if (!display_diagnostics) { + disp_area.drawBitmap(0, 37, bm_online, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + } + } else { + if (disp_page == 0) { + if (true || device_signatures_ok()) { + disp_area.drawBitmap(0, 37, bm_checks, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + } else { + disp_area.drawBitmap(0, 37, bm_nfr, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + } + } else if (disp_page == 1) { + if (!console_active) { + disp_area.drawBitmap(0, 37, bm_hwok, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + } else { + disp_area.drawBitmap(0, 37, bm_console_active, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + } + } else if (disp_page == 2) { + disp_area.drawBitmap(0, 37, bm_version, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK); + char *v_str = (char*)malloc(3+1); + sprintf(v_str, "%01d%02d", MAJ_VERS, MIN_VERS); + for (int i = 0; i < 3; i++) { + uint8_t numeric = v_str[i]-48; uint8_t bm_offset = numeric*5; + uint8_t dxp = 20; + if (i == 1) dxp += 9*1+4; + if (i == 2) dxp += 9*2+4; + disp_area.drawBitmap(dxp, 37+16, bm_n_uh+bm_offset, 8, 5, SSD1306_WHITE, SSD1306_BLACK); + } + free(v_str); + disp_area.drawLine(27, 37+19, 28, 37+19, SSD1306_BLACK); + disp_area.drawLine(27, 37+20, 28, 37+20, SSD1306_BLACK); + } + } + } +#endif // BOUNDARY_MODE + } else { + disp_area.drawBitmap(0, 0, fb, disp_area.width(), disp_area.height(), SSD1306_WHITE, SSD1306_BLACK); + } + } +} + +void update_disp_area() { + draw_disp_area(); + + drawBitmap(p_ad_x, p_ad_y, disp_area.getBuffer(), disp_area.width(), disp_area.height(), SSD1306_WHITE, SSD1306_BLACK); + if (disp_mode == DISP_MODE_LANDSCAPE) { + if (device_init_done && !firmware_update_mode && !disp_ext_fb) { + drawLine(0, 0, 0, 63, SSD1306_WHITE); + } + } +} + +void display_recondition() { + #if PLATFORM == PLATFORM_ESP32 + for (uint8_t iy = 0; iy < disp_area.height(); iy++) { + unsigned char rand_seg [] = {random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF)}; + stat_area.drawBitmap(0, iy, rand_seg, 64, 1, SSD1306_WHITE, SSD1306_BLACK); + disp_area.drawBitmap(0, iy, rand_seg, 64, 1, SSD1306_WHITE, SSD1306_BLACK); + } + + drawBitmap(p_ad_x, p_ad_y, disp_area.getBuffer(), disp_area.width(), disp_area.height(), SSD1306_WHITE, SSD1306_BLACK); + if (disp_mode == DISP_MODE_PORTRAIT) { + drawBitmap(p_as_x, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK); + } else if (disp_mode == DISP_MODE_LANDSCAPE) { + drawBitmap(p_as_x, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK); + } + #endif +} + +bool epd_blanked = false; +#if BOARD_MODEL == BOARD_TECHO + void epd_blank(bool full_update = true) { + display.setFullWindow(); + display.fillScreen(SSD1306_WHITE); + display.display(full_update); + } + + void epd_black(bool full_update = true) { + display.setFullWindow(); + display.fillScreen(SSD1306_BLACK); + display.display(full_update); + } +#endif + +void update_display(bool blank = false) { + display_updating = true; + if (blank == true) { + last_disp_update = millis()-disp_update_interval-1; + } else { + if (display_blanking_enabled && millis()-last_unblank_event >= display_blanking_timeout) { + blank = true; + display_blanked = true; + if (display_intensity != 0) { + display_unblank_intensity = display_intensity; + } + display_intensity = 0; + } else { + display_blanked = false; + if (display_unblank_intensity != 0x00) { + display_intensity = display_unblank_intensity; + display_unblank_intensity = 0x00; + } + } + } + + if (blank) { + if (millis()-last_disp_update >= disp_update_interval) { + if (display_contrast != display_intensity) { + display_contrast = display_intensity; + set_contrast(&display, display_contrast); + } + + #if BOARD_MODEL == BOARD_TECHO + if (!epd_blanked) { + epd_blank(); + epd_blanked = true; + } + #endif + + #if BOARD_MODEL == BOARD_HELTEC_T114 + display.clear(); + display.display(); + #elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_TECHO + display.clearDisplay(); + display.display(); + #else + // TODO: Clear screen + #endif + + last_disp_update = millis(); + } + + } else { + if (millis()-last_disp_update >= disp_update_interval) { + uint32_t current = millis(); + if (display_contrast != display_intensity) { + display_contrast = display_intensity; + set_contrast(&display, display_contrast); + } + + #if BOARD_MODEL == BOARD_HELTEC_T114 + display.clear(); + #elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_TECHO + display.clearDisplay(); + #endif + + if (recondition_display) { + disp_target_fps = 30; + disp_update_interval = 1000/disp_target_fps; + display_recondition(); + } else { + #if BOARD_MODEL == BOARD_TECHO + display.setFullWindow(); + display.fillScreen(SSD1306_WHITE); + #endif + + update_stat_area(); + update_disp_area(); + } + + #if BOARD_MODEL == BOARD_TECHO + if (current-last_epd_refresh >= epd_update_interval) { + if (current-last_epd_full_refresh >= REFRESH_PERIOD) { display.display(false); last_epd_full_refresh = millis(); } + else { display.display(true); } + last_epd_refresh = millis(); + epd_blanked = false; + } + #elif BOARD_MODEL != BOARD_TDECK + display.display(); + #endif + + last_disp_update = millis(); + } + } + display_updating = false; +} + +void display_unblank() { + last_unblank_event = millis(); +} + +void ext_fb_enable() { + disp_ext_fb = true; +} + +void ext_fb_disable() { + disp_ext_fb = false; +} diff --git a/Documentation/README.md b/Documentation/README.md new file mode 100755 index 0000000..debba1b --- /dev/null +++ b/Documentation/README.md @@ -0,0 +1 @@ +# RNode Documentation diff --git a/Documentation/RNode_v1_Manual.pdf b/Documentation/RNode_v1_Manual.pdf new file mode 100755 index 0000000..26101df Binary files /dev/null and b/Documentation/RNode_v1_Manual.pdf differ diff --git a/Documentation/images/126dcfe92fb7.webp b/Documentation/images/126dcfe92fb7.webp new file mode 100755 index 0000000..31cf5eb Binary files /dev/null and b/Documentation/images/126dcfe92fb7.webp differ diff --git a/Documentation/images/devboards_1.webp b/Documentation/images/devboards_1.webp new file mode 100755 index 0000000..0ba2311 Binary files /dev/null and b/Documentation/images/devboards_1.webp differ diff --git a/Documentation/images/rnv21_bgp.webp b/Documentation/images/rnv21_bgp.webp new file mode 100755 index 0000000..fe4196e Binary files /dev/null and b/Documentation/images/rnv21_bgp.webp differ diff --git a/Documentation/rnfw_1.jpg b/Documentation/rnfw_1.jpg new file mode 100755 index 0000000..27c9c92 Binary files /dev/null and b/Documentation/rnfw_1.jpg differ diff --git a/FileStream.h b/FileStream.h new file mode 100755 index 0000000..3e70115 --- /dev/null +++ b/FileStream.h @@ -0,0 +1,35 @@ +#pragma once + +#ifdef HAS_RNS + +#include "FileSystem.h" +#include "FileSystemType.h" + + class FileStream : public RNS::FileStreamImpl { + + private: + std::unique_ptr _file; + bool _closed = false; + + public: + FileStream(File* file) : RNS::FileStreamImpl(), _file(file) {} + virtual ~FileStream() { if (!_closed) close(); } + + public: + inline virtual const char* name() { return _file->name(); } + inline virtual size_t size() { return _file->size(); } + inline virtual void close() { _closed = true; _file->close(); } + + // Print overrides + inline virtual size_t write(uint8_t byte) { return _file->write(byte); } + inline virtual size_t write(const uint8_t *buffer, size_t size) { return _file->write(buffer, size); } + + // Stream overrides + inline virtual int available() { return _file->available(); } + inline virtual int read() { return _file->read(); } + inline virtual int peek() { return _file->peek(); } + inline virtual void flush() { _file->flush(); } + + }; + +#endif diff --git a/FileSystem.cpp b/FileSystem.cpp new file mode 100755 index 0000000..1ae4bce --- /dev/null +++ b/FileSystem.cpp @@ -0,0 +1,478 @@ +#include "FileSystem.h" +#include "FileStream.h" +#include "FileSystemType.h" + +#ifdef HAS_RNS + +#include + +#if FS_TYPE == FS_TYPE_INTERNALFS + +inline int _countLfsBlock(void *p, lfs_block_t block) { + lfs_size_t *size = (lfs_size_t*) p; + *size += 1; + return 0; +} + +lfs_ssize_t usedBlocks() { + lfs_size_t size = 0; + lfs_traverse(FS._getFS(), _countLfsBlock, &size); + return size; +} + +size_t usedBytes() { + const lfs_config* config = FS._getFS()->cfg; + const size_t usedBlockCount = usedBlocks(); + return config->block_size * usedBlockCount; +} + +size_t totalBytes() { + const lfs_config* config = FS._getFS()->cfg; + return config->block_size * config->block_count; +} + +#elif FS_TYPE == FS_TYPE_FLASHFS + +Adafruit_FlashTransport_SPI g_flashTransport(SS, SPI); + +//Flash definition structure for GD25Q16C Flash (RAK15001) +Cached_SPIFlash g_flash(&g_flashTransport); +SPIFlash_Device_t g_RAK15001 { + .total_size = (1UL << 21), + .start_up_time_us = 5000, + .manufacturer_id = 0xc8, + .memory_type = 0x40, + .capacity = 0x15, + .max_clock_speed_mhz = 15, + .quad_enable_bit_mask = 0x00, + .has_sector_protection = false, + .supports_fast_read = true, + .supports_qspi = false, + .supports_qspi_writes = false, + .write_status_register_split = false, + .single_status_byte = true, +}; + +#endif + + +bool FileSystem::init() { + TRACE("Initializing filesystem..."); + try { +#if FS_TYPE == FS_TYPE_SPIFFS + // Initialize SPIFFS + INFO("SPIFFS mounting filesystem"); + if (!SPIFFS.begin(true, "")) { + ERROR("SPIFFS filesystem mount failed"); + return false; + } + INFO("SPIFFS filesystem is ready"); +#elif FS_TYPE == FS_TYPE_LITTLEFS + // Initialize LittleFS + INFO("LittleFS mounting filesystem"); + if (!LittleFS.begin(true, "")) { + ERROR("LittleFS filesystem mount failed"); + return false; + } + DEBUG("LittleFS filesystem is ready"); +#elif FS_TYPE == FS_TYPE_INTERNALFS + // Initialize InternalFileSystem + INFO("InternalFS mounting filesystem"); + if (!InternalFS.begin()) { + ERROR("InternalFS filesystem mount failed"); + return false; + } + INFO("InternalFS filesystem is ready"); +#elif FS_TYPE == FS_TYPE_FLASHFS + // Initialize FlashFileSystem + INFO("FlashFS mounting filesystem"); + if (!g_flash.begin(&g_RAK15001)) { + ERROR("FlashFS failed to initialize"); + return false; + } + if (!FlashFS.begin(&g_flash)) { + ERROR("FlashFS filesystem mount failed"); + return false; + } +#endif + // Ensure filesystem is writable and reformat if not + RNS::Bytes test("test"); + if (write_file("/test", test) < 4) { + HEAD("Failed to write test file, filesystem is being reformatted...", RNS::LOG_CRITICAL); + //FS.format(); + reformat(); + } + else { + remove_file("/test"); + } + } + catch (std::exception& e) { + //ERROR("FileSystem init Exception: " + std::string(e.what())); + return false; + } + TRACE("Finished initializing"); + return true; +} + +bool FileSystem::format() { + INFO("Formatting filesystem..."); + try { + if (!FS.format()) { + ERROR("Format failed!"); + return false; + } + return true; + } + catch (std::exception& e) { + ERROR("FileSystem reformat Exception: " + std::string(e.what())); + } + return false; +} + +bool FileSystem::reformat() { + INFO("Reformatting filesystem..."); + try { + RNS::Bytes eeprom; + read_file("/eeprom", eeprom); + RNS::Bytes transport_identity; + read_file("/transport_identity", transport_identity); + //RNS::Bytes time_offset; + //read_file("/time_offset", time_offset); + if (!FS.format()) { + ERROR("Format failed!"); + return false; + } + if (eeprom) { + write_file("/eeprom", eeprom); + } + if (transport_identity) { + write_file("/transport_identity", transport_identity); + } + //if (time_offset) { + // write_file("/time_offset", time_offset); + //} + return true; + } + catch (std::exception& e) { + ERROR("FileSystem reformat Exception: " + std::string(e.what())); + } + return false; +} + +#ifndef NDEBUG + +void FileSystem::listDir(const char* dir, const char* prefix /*= ""*/) { + Serial.print(prefix); + std::string full_dir(dir); + if (full_dir.compare("/") != 0) { + full_dir += "/"; + } + Serial.println(full_dir.c_str()); + std::string pre(prefix); + pre.append(" "); + try { + File root = FS.open(dir); + if (!root) { + Serial.print(pre.c_str()); + Serial.println("(failed to open directory)"); + return; + } + File file = root.openNextFile(); + while (file) { + char* name = (char*)file.name(); + std::string recurse_dir(full_dir); + if (file.isDirectory()) { + recurse_dir += name; + listDir(recurse_dir.c_str(), pre.c_str()); + } + else { + Serial.print(pre.c_str()); + //Serial.print("FILE: "); + Serial.print(name); + Serial.print(" ("); + Serial.print(file.size()); + Serial.println(" bytes)"); + } + file.close(); + file = root.openNextFile(); + } + root.close(); + } + catch (std::exception& e) { + Serial.print("listDir Exception: "); + Serial.println(e.what()); + } +} + +void FileSystem::dumpDir(const char* dir) { + Serial.print("DIR: "); + std::string full_dir(dir); + if (full_dir.compare("/") != 0) { + full_dir += "/"; + } + Serial.println(full_dir.c_str()); + try { + File root = FS.open(dir); + if (!root) { + Serial.println("(failed to open directory)"); + return; + } + File file = root.openNextFile(); + while (file) { + char* name = (char*)file.name(); + if (file.isDirectory()) { + std::string recurse_dir(full_dir); + recurse_dir += name; + dumpDir(recurse_dir.c_str()); + } + else { + Serial.print("\nFILE: "); + Serial.print(name); + Serial.print(" ("); + Serial.print(file.size()); + Serial.println(" bytes)"); + char data[4096]; + size_t size = file.size(); + size_t read = file.readBytes(data, (size < sizeof(data)) ? size : sizeof(data)); + Serial.write(data, read); + Serial.println(""); + } + file.close(); + file = root.openNextFile(); + } + root.close(); + } + catch (std::exception& e) { + Serial.print("dumpDir Exception: "); + Serial.println(e.what()); + } +} + +#endif + + +/*virtua*/ bool FileSystem::file_exists(const char* file_path) { + TRACEF("file_exists: checking for existence of file %s", file_path); +/* +#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS + File file(FS); + if (file.open(file_path, FILE_O_READ)) { +#else + File file = FS.open(file_path, FILE_READ); + if (file) { +#endif + bool is_directory = file.isDirectory(); + file.close(); + return !is_directory; + } + return false; +*/ + return FS.exists(file_path); +} + +/*virtua*/ size_t FileSystem::read_file(const char* file_path, RNS::Bytes& data) { + TRACEF("read_file: reading from file %s", file_path); + size_t read = 0; +#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS + File file(FS); + if (file.open(file_path, FILE_O_READ)) { +#else + File file = FS.open(file_path, FILE_READ); + if (file) { +#endif + size_t size = file.size(); + read = file.readBytes((char*)data.writable(size), size); + TRACEF("read_file: read %u bytes from file %s", read, file_path); + if (read != size) { + ERRORF("read_file: failed to read file %s", file_path); + data.resize(read); + } + //TRACE("read_file: closing input file"); + file.close(); + } + else { + ERRORF("read_file: failed to open input file %s", file_path); + } + return read; +} + +/*virtua*/ size_t FileSystem::write_file(const char* file_path, const RNS::Bytes& data) { + TRACEF("write_file: writing to file %s", file_path); + // CBA TODO Replace remove with working truncation + if (FS.exists(file_path)) { + FS.remove(file_path); + } + size_t wrote = 0; +#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS + File file(FS); + if (file.open(file_path, FILE_O_WRITE)) { +#else + File file = FS.open(file_path, FILE_WRITE); + if (file) { +#endif + // Seek to beginning to overwrite + //file.seek(0); + //file.truncate(0); + wrote = file.write(data.data(), data.size()); + TRACEF("write_file: wrote %u bytes to file %s", wrote, file_path); + if (wrote < data.size()) { + WARNINGF("write_file: not all data was written to file %s", file_path); + } + //TRACE("write_file: closing output file"); + file.close(); + } + else { + ERRORF("write_file: failed to open output file %s", file_path); + } + return wrote; +} + +/*virtual*/ RNS::FileStream FileSystem::open_file(const char* file_path, RNS::FileStream::MODE file_mode) { + TRACEF("open_file: opening file %s", file_path); +#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS + int mode; + if (file_mode == RNS::FileStream::MODE_READ) { + mode = FILE_O_READ; + } + else if (file_mode == RNS::FileStream::MODE_WRITE) { + mode = FILE_O_WRITE; + // CBA TODO Replace remove with working truncation + if (FS.exists(file_path)) { + FS.remove(file_path); + } + } + else if (file_mode == RNS::FileStream::MODE_APPEND) { + // CBA This is the default write mode for nrf52 littlefs + mode = FILE_O_WRITE; + } + else { + ERRORF("open_file: unsupported mode %d", file_mode); + return {RNS::Type::NONE}; + } + File* file = new File(FS); + if (!file->open(file_path, mode)) { + ERRORF("open_file: failed to open output file %s", file_path); + return {RNS::Type::NONE}; + } + // Seek to beginning to overwrite (this is failing on nrf52) + //if (file_mode == RNS::FileStream::MODE_WRITE) { + // file->seek(0); + // file->truncate(0); + //} + TRACEF("open_file: successfully opened file %s", file_path); + return RNS::FileStream(new FileStream(file)); +#else + const char* mode; + if (file_mode == RNS::FileStream::MODE_READ) { + mode = FILE_READ; + } + else if (file_mode == RNS::FileStream::MODE_WRITE) { + mode = FILE_WRITE; + } + else if (file_mode == RNS::FileStream::MODE_APPEND) { + mode = FILE_APPEND; + } + else { + ERRORF("open_file: unsupported mode %d", file_mode); + return {RNS::Type::NONE}; + } + TRACEF("open_file: opening file %s in mode %s", file_path, mode); + // CBA Using copy constructor to obtain File* + File* file = new File(FS.open(file_path, mode)); + if (file == nullptr || !(*file)) { + ERRORF("open_file: failed to open output file %s", file_path); + return {RNS::Type::NONE}; + } + TRACEF("open_file: successfully opened file %s", file_path); + return RNS::FileStream(new FileStream(file)); +#endif +} + +/*virtua*/ bool FileSystem::remove_file(const char* file_path) { + TRACEF("remove_file: removing file %s", file_path); + return FS.remove(file_path); +} + +/*virtua*/ bool FileSystem::rename_file(const char* from_file_path, const char* to_file_path) { + TRACEF("rename_file: renaming file %s to %s", from_file_path, to_file_path); + return FS.rename(from_file_path, to_file_path); +} + +/*virtua*/ bool FileSystem::directory_exists(const char* directory_path) { + TRACEF("directory_exists: checking for existence of directory %s", directory_path); +#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS + File file(FS); + if (file.open(directory_path, FILE_O_READ)) { +#else + File file = FS.open(directory_path, FILE_READ); + if (file) { +#endif + bool is_directory = file.isDirectory(); + file.close(); + return is_directory; + } + return false; +} + +/*virtua*/ bool FileSystem::create_directory(const char* directory_path) { + TRACEF("create_directory: creating directory %s", directory_path); + if (!FS.mkdir(directory_path)) { + ERROR("create_directory: failed to create directory " + std::string(directory_path)); + return false; + } + return true; +} + +/*virtua*/ bool FileSystem::remove_directory(const char* directory_path) { + TRACEF("remove_directory: removing directory %s", directory_path); +#if FS_TYPE == FS_TYPE_INTERNALFS || FS_TYPE == FS_TYPE_FLASHFS + if (!FS.rmdir_r(directory_path)) { +#else + if (!FS.rmdir(directory_path)) { +#endif + ERROR("remove_directory: failed to remove directory " + std::string(directory_path)); + return false; + } + return true; +} + +/*virtua*/ std::list FileSystem::list_directory(const char* directory_path) { + TRACEF("list_directory: listing directory %s", directory_path); + std::list files; + File root = FS.open(directory_path); + if (!root) { + ERROR("list_directory: failed to open directory " + std::string(directory_path)); + return files; + } + File file = root.openNextFile(); + while (file) { + if (!file.isDirectory()) { + char* name = (char*)file.name(); + files.push_back(name); + } + // CBA Following close required to avoid leaking memory + file.close(); + file = root.openNextFile(); + } + root.close(); + TRACE("list_directory: returning directory listing"); + return files; +} + +/*virtual*/ size_t FileSystem::storage_size() { +#if FS_TYPE == FS_TYPE_INTERNALFS + return totalBytes(); +#else + return FS.totalBytes(); +#endif +} + +/*virtual*/ size_t FileSystem::storage_available() { +#if FS_TYPE == FS_TYPE_INTERNALFS + return (totalBytes() - usedBytes()); +#else + return (FS.totalBytes() - FS.usedBytes()); +#endif +} + +#endif diff --git a/FileSystem.h b/FileSystem.h new file mode 100755 index 0000000..65017a2 --- /dev/null +++ b/FileSystem.h @@ -0,0 +1,41 @@ +#pragma once + +#ifdef HAS_RNS + +#include +#include +#include +#include + +#include + +class FileSystem : public RNS::FileSystemImpl { + +public: + FileSystem() {} + + bool init(); + bool format(); + bool reformat(); + + // CBA Debug + static void listDir(const char* dir, const char* prefix = ""); + static void dumpDir(const char* dir); + +public: + virtual bool file_exists(const char* file_path); + virtual size_t read_file(const char* file_path, RNS::Bytes& data); + virtual size_t write_file(const char* file_path, const RNS::Bytes& data); + virtual RNS::FileStream open_file(const char* file_path, RNS::FileStream::MODE file_mode); + virtual bool remove_file(const char* file_path); + virtual bool rename_file(const char* from_file_path, const char* to_file_path); + virtual bool directory_exists(const char* directory_path); + virtual bool create_directory(const char* directory_path); + virtual bool remove_directory(const char* directory_path); + virtual std::list list_directory(const char* directory_path); + virtual size_t storage_size(); + virtual size_t storage_available(); + +}; + +#endif diff --git a/FileSystemType.h b/FileSystemType.h new file mode 100755 index 0000000..7dcc5e9 --- /dev/null +++ b/FileSystemType.h @@ -0,0 +1,51 @@ +#pragma once + +#ifdef HAS_RNS + +// CBA This header file was required to break-out defined and includes here that could +// not be include in FileSystem.h due to conlficts with SPIFFS in Console.h + +#include "Boards.h" + +#define FS_TYPE_SPIFFS 0 +#define FS_TYPE_LITTLEFS 1 +#define FS_TYPE_INTERNALFS 2 +#define FS_TYPE_FLASHFS 3 + +#if MCU_VARIANT == MCU_ESP32 + #if defined(USE_FLASHFS) + #define FS_TYPE FS_TYPE_FLASHFS + #else + //#define FS_TYPE FS_TYPE_SPIFFS + #define FS_TYPE FS_TYPE_LITTLEFS + #endif +#elif MCU_VARIANT == MCU_NRF52 + #if defined(USE_FLASHFS) + #define FS_TYPE FS_TYPE_FLASHFS + #else + #define FS_TYPE FS_TYPE_INTERNALFS + #endif +#else + #define FS_TYPE FS_TYPE_SPIFFS +#endif + +#if FS_TYPE == FS_TYPE_SPIFFS +#include +#define FS SPIFFS +#elif FS_TYPE == FS_TYPE_LITTLEFS +#include +#define FS LittleFS +#elif FS_TYPE == FS_TYPE_INTERNALFS +#include +#define FS InternalFS +using namespace Adafruit_LittleFS_Namespace; +#elif FS_TYPE == FS_TYPE_FLASHFS +#include +#include +#define FS FlashFS +using namespace Adafruit_LittleFS_Namespace; +#else +#error "FileSystem type not specified" +#endif + +#endif diff --git a/Fonts/Org_01.h b/Fonts/Org_01.h new file mode 100755 index 0000000..9b80258 --- /dev/null +++ b/Fonts/Org_01.h @@ -0,0 +1,131 @@ +#pragma once +#include + +// Org_v01 by Orgdot (www.orgdot.com/aliasfonts). A tiny, +// stylized font with all characters within a 6 pixel height. + +const uint8_t Org_01Bitmaps[] PROGMEM = { + 0xE8, 0xA0, 0x57, 0xD5, 0xF5, 0x00, 0xFD, 0x3E, 0x5F, 0x80, 0x88, 0x88, + 0x88, 0x80, 0xF4, 0xBF, 0x2E, 0x80, 0x80, 0x6A, 0x40, 0x95, 0x80, 0xAA, + 0x80, 0x5D, 0x00, 0xC0, 0xF0, 0x80, 0x08, 0x88, 0x88, 0x00, 0xFC, 0x63, + 0x1F, 0x80, 0xF8, 0xF8, 0x7F, 0x0F, 0x80, 0xF8, 0x7E, 0x1F, 0x80, 0x8C, + 0x7E, 0x10, 0x80, 0xFC, 0x3E, 0x1F, 0x80, 0xFC, 0x3F, 0x1F, 0x80, 0xF8, + 0x42, 0x10, 0x80, 0xFC, 0x7F, 0x1F, 0x80, 0xFC, 0x7E, 0x1F, 0x80, 0x90, + 0xB0, 0x2A, 0x22, 0xF0, 0xF0, 0x88, 0xA8, 0xF8, 0x4E, 0x02, 0x00, 0xFD, + 0x6F, 0x0F, 0x80, 0xFC, 0x7F, 0x18, 0x80, 0xF4, 0x7D, 0x1F, 0x00, 0xFC, + 0x21, 0x0F, 0x80, 0xF4, 0x63, 0x1F, 0x00, 0xFC, 0x3F, 0x0F, 0x80, 0xFC, + 0x3F, 0x08, 0x00, 0xFC, 0x2F, 0x1F, 0x80, 0x8C, 0x7F, 0x18, 0x80, 0xF9, + 0x08, 0x4F, 0x80, 0x78, 0x85, 0x2F, 0x80, 0x8D, 0xB1, 0x68, 0x80, 0x84, + 0x21, 0x0F, 0x80, 0xFD, 0x6B, 0x5A, 0x80, 0xFC, 0x63, 0x18, 0x80, 0xFC, + 0x63, 0x1F, 0x80, 0xFC, 0x7F, 0x08, 0x00, 0xFC, 0x63, 0x3F, 0x80, 0xFC, + 0x7F, 0x29, 0x00, 0xFC, 0x3E, 0x1F, 0x80, 0xF9, 0x08, 0x42, 0x00, 0x8C, + 0x63, 0x1F, 0x80, 0x8C, 0x62, 0xA2, 0x00, 0xAD, 0x6B, 0x5F, 0x80, 0x8A, + 0x88, 0xA8, 0x80, 0x8C, 0x54, 0x42, 0x00, 0xF8, 0x7F, 0x0F, 0x80, 0xEA, + 0xC0, 0x82, 0x08, 0x20, 0x80, 0xD5, 0xC0, 0x54, 0xF8, 0x80, 0xF1, 0xFF, + 0x8F, 0x99, 0xF0, 0xF8, 0x8F, 0x1F, 0x99, 0xF0, 0xFF, 0x8F, 0x6B, 0xA4, + 0xF9, 0x9F, 0x10, 0x8F, 0x99, 0x90, 0xF0, 0x55, 0xC0, 0x8A, 0xF9, 0x90, + 0xF8, 0xFD, 0x63, 0x10, 0xF9, 0x99, 0xF9, 0x9F, 0xF9, 0x9F, 0x80, 0xF9, + 0x9F, 0x20, 0xF8, 0x88, 0x47, 0x1F, 0x27, 0xC8, 0x42, 0x00, 0x99, 0x9F, + 0x99, 0x97, 0x8C, 0x6B, 0xF0, 0x96, 0x69, 0x99, 0x9F, 0x10, 0x2E, 0x8F, + 0x2B, 0x22, 0xF8, 0x89, 0xA8, 0x0F, 0xE0}; + +const GFXglyph Org_01Glyphs[] PROGMEM = {{0, 0, 0, 6, 0, 1}, // 0x20 ' ' + {0, 1, 5, 2, 0, -4}, // 0x21 '!' + {1, 3, 1, 4, 0, -4}, // 0x22 '"' + {2, 5, 5, 6, 0, -4}, // 0x23 '#' + {6, 5, 5, 6, 0, -4}, // 0x24 '$' + {10, 5, 5, 6, 0, -4}, // 0x25 '%' + {14, 5, 5, 6, 0, -4}, // 0x26 '&' + {18, 1, 1, 2, 0, -4}, // 0x27 ''' + {19, 2, 5, 3, 0, -4}, // 0x28 '(' + {21, 2, 5, 3, 0, -4}, // 0x29 ')' + {23, 3, 3, 4, 0, -3}, // 0x2A '*' + {25, 3, 3, 4, 0, -3}, // 0x2B '+' + {27, 1, 2, 2, 0, 0}, // 0x2C ',' + {28, 4, 1, 5, 0, -2}, // 0x2D '-' + {29, 1, 1, 2, 0, 0}, // 0x2E '.' + {30, 5, 5, 6, 0, -4}, // 0x2F '/' + {34, 5, 5, 6, 0, -4}, // 0x30 '0' + {38, 1, 5, 2, 0, -4}, // 0x31 '1' + {39, 5, 5, 6, 0, -4}, // 0x32 '2' + {43, 5, 5, 6, 0, -4}, // 0x33 '3' + {47, 5, 5, 6, 0, -4}, // 0x34 '4' + {51, 5, 5, 6, 0, -4}, // 0x35 '5' + {55, 5, 5, 6, 0, -4}, // 0x36 '6' + {59, 5, 5, 6, 0, -4}, // 0x37 '7' + {63, 5, 5, 6, 0, -4}, // 0x38 '8' + {67, 5, 5, 6, 0, -4}, // 0x39 '9' + {71, 1, 4, 2, 0, -3}, // 0x3A ':' + {72, 1, 4, 2, 0, -3}, // 0x3B ';' + {73, 3, 5, 4, 0, -4}, // 0x3C '<' + {75, 4, 3, 5, 0, -3}, // 0x3D '=' + {77, 3, 5, 4, 0, -4}, // 0x3E '>' + {79, 5, 5, 6, 0, -4}, // 0x3F '?' + {83, 5, 5, 6, 0, -4}, // 0x40 '@' + {87, 5, 5, 6, 0, -4}, // 0x41 'A' + {91, 5, 5, 6, 0, -4}, // 0x42 'B' + {95, 5, 5, 6, 0, -4}, // 0x43 'C' + {99, 5, 5, 6, 0, -4}, // 0x44 'D' + {103, 5, 5, 6, 0, -4}, // 0x45 'E' + {107, 5, 5, 6, 0, -4}, // 0x46 'F' + {111, 5, 5, 6, 0, -4}, // 0x47 'G' + {115, 5, 5, 6, 0, -4}, // 0x48 'H' + {119, 5, 5, 6, 0, -4}, // 0x49 'I' + {123, 5, 5, 6, 0, -4}, // 0x4A 'J' + {127, 5, 5, 6, 0, -4}, // 0x4B 'K' + {131, 5, 5, 6, 0, -4}, // 0x4C 'L' + {135, 5, 5, 6, 0, -4}, // 0x4D 'M' + {139, 5, 5, 6, 0, -4}, // 0x4E 'N' + {143, 5, 5, 6, 0, -4}, // 0x4F 'O' + {147, 5, 5, 6, 0, -4}, // 0x50 'P' + {151, 5, 5, 6, 0, -4}, // 0x51 'Q' + {155, 5, 5, 6, 0, -4}, // 0x52 'R' + {159, 5, 5, 6, 0, -4}, // 0x53 'S' + {163, 5, 5, 6, 0, -4}, // 0x54 'T' + {167, 5, 5, 6, 0, -4}, // 0x55 'U' + {171, 5, 5, 6, 0, -4}, // 0x56 'V' + {175, 5, 5, 6, 0, -4}, // 0x57 'W' + {179, 5, 5, 6, 0, -4}, // 0x58 'X' + {183, 5, 5, 6, 0, -4}, // 0x59 'Y' + {187, 5, 5, 6, 0, -4}, // 0x5A 'Z' + {191, 2, 5, 3, 0, -4}, // 0x5B '[' + {193, 5, 5, 6, 0, -4}, // 0x5C '\' + {197, 2, 5, 3, 0, -4}, // 0x5D ']' + {199, 3, 2, 4, 0, -4}, // 0x5E '^' + {200, 5, 1, 6, 0, 1}, // 0x5F '_' + {201, 1, 1, 2, 0, -4}, // 0x60 '`' + {202, 4, 4, 5, 0, -3}, // 0x61 'a' + {204, 4, 5, 5, 0, -4}, // 0x62 'b' + {207, 4, 4, 5, 0, -3}, // 0x63 'c' + {209, 4, 5, 5, 0, -4}, // 0x64 'd' + {212, 4, 4, 5, 0, -3}, // 0x65 'e' + {214, 3, 5, 4, 0, -4}, // 0x66 'f' + {216, 4, 5, 5, 0, -3}, // 0x67 'g' + {219, 4, 5, 5, 0, -4}, // 0x68 'h' + {222, 1, 4, 2, 0, -3}, // 0x69 'i' + {223, 2, 5, 3, 0, -3}, // 0x6A 'j' + {225, 4, 5, 5, 0, -4}, // 0x6B 'k' + {228, 1, 5, 2, 0, -4}, // 0x6C 'l' + {229, 5, 4, 6, 0, -3}, // 0x6D 'm' + {232, 4, 4, 5, 0, -3}, // 0x6E 'n' + {234, 4, 4, 5, 0, -3}, // 0x6F 'o' + {236, 4, 5, 5, 0, -3}, // 0x70 'p' + {239, 4, 5, 5, 0, -3}, // 0x71 'q' + {242, 4, 4, 5, 0, -3}, // 0x72 'r' + {244, 4, 4, 5, 0, -3}, // 0x73 's' + {246, 5, 5, 6, 0, -4}, // 0x74 't' + {250, 4, 4, 5, 0, -3}, // 0x75 'u' + {252, 4, 4, 5, 0, -3}, // 0x76 'v' + {254, 5, 4, 6, 0, -3}, // 0x77 'w' + {257, 4, 4, 5, 0, -3}, // 0x78 'x' + {259, 4, 5, 5, 0, -3}, // 0x79 'y' + {262, 4, 4, 5, 0, -3}, // 0x7A 'z' + {264, 3, 5, 4, 0, -4}, // 0x7B '{' + {266, 1, 5, 2, 0, -4}, // 0x7C '|' + {267, 3, 5, 4, 0, -4}, // 0x7D '}' + {269, 5, 3, 6, 0, -3}}; // 0x7E '~' + +const GFXfont Org_01 PROGMEM = {(uint8_t *)Org_01Bitmaps, + (GFXglyph *)Org_01Glyphs, 0x20, 0x7E, 7}; + +// Approx. 943 bytes diff --git a/Fonts/PicoPixel.h b/Fonts/PicoPixel.h new file mode 100755 index 0000000..01c79d5 --- /dev/null +++ b/Fonts/PicoPixel.h @@ -0,0 +1,123 @@ +#pragma once +#include + +// Picopixel by Sebastian Weber. A tiny font +// with all characters within a 6 pixel height. + +const uint8_t PicopixelBitmaps[] PROGMEM = { + 0xE8, 0xB4, 0x57, 0xD5, 0xF5, 0x00, 0x4E, 0x3E, 0x80, 0xA5, 0x4A, 0x4A, + 0x5A, 0x50, 0xC0, 0x6A, 0x40, 0x95, 0x80, 0xAA, 0x80, 0x5D, 0x00, 0x60, + 0xE0, 0x80, 0x25, 0x48, 0x56, 0xD4, 0x75, 0x40, 0xC5, 0x4E, 0xC5, 0x1C, + 0x97, 0x92, 0xF3, 0x1C, 0x53, 0x54, 0xE5, 0x48, 0x55, 0x54, 0x55, 0x94, + 0xA0, 0x46, 0x64, 0xE3, 0x80, 0x98, 0xC5, 0x04, 0x56, 0xC6, 0x57, 0xDA, + 0xD7, 0x5C, 0x72, 0x46, 0xD6, 0xDC, 0xF3, 0xCE, 0xF3, 0x48, 0x72, 0xD4, + 0xB7, 0xDA, 0xF8, 0x24, 0xD4, 0xBB, 0x5A, 0x92, 0x4E, 0x8E, 0xEB, 0x58, + 0x80, 0x9D, 0xB9, 0x90, 0x56, 0xD4, 0xD7, 0x48, 0x56, 0xD4, 0x40, 0xD7, + 0x5A, 0x71, 0x1C, 0xE9, 0x24, 0xB6, 0xD4, 0xB6, 0xA4, 0x8C, 0x6B, 0x55, + 0x00, 0xB5, 0x5A, 0xB5, 0x24, 0xE5, 0x4E, 0xEA, 0xC0, 0x91, 0x12, 0xD5, + 0xC0, 0x54, 0xF0, 0x90, 0xC7, 0xF0, 0x93, 0x5E, 0x71, 0x80, 0x25, 0xDE, + 0x5E, 0x30, 0x6E, 0x80, 0x77, 0x9C, 0x93, 0x5A, 0xB8, 0x45, 0x60, 0x92, + 0xEA, 0xAA, 0x40, 0xD5, 0x6A, 0xD6, 0x80, 0x55, 0x00, 0xD7, 0x40, 0x75, + 0x90, 0xE8, 0x71, 0xE0, 0xBA, 0x40, 0xB5, 0x80, 0xB5, 0x00, 0x8D, 0x54, + 0xAA, 0x80, 0xAC, 0xE0, 0xE5, 0x70, 0x6A, 0x26, 0xFC, 0xC8, 0xAC, 0x5A}; + +const GFXglyph PicopixelGlyphs[] PROGMEM = {{0, 0, 0, 2, 0, 1}, // 0x20 ' ' + {0, 1, 5, 2, 0, -4}, // 0x21 '!' + {1, 3, 2, 4, 0, -4}, // 0x22 '"' + {2, 5, 5, 6, 0, -4}, // 0x23 '#' + {6, 3, 6, 4, 0, -4}, // 0x24 '$' + {9, 3, 5, 4, 0, -4}, // 0x25 '%' + {11, 4, 5, 5, 0, -4}, // 0x26 '&' + {14, 1, 2, 2, 0, -4}, // 0x27 ''' + {15, 2, 5, 3, 0, -4}, // 0x28 '(' + {17, 2, 5, 3, 0, -4}, // 0x29 ')' + {19, 3, 3, 4, 0, -3}, // 0x2A '*' + {21, 3, 3, 4, 0, -3}, // 0x2B '+' + {23, 2, 2, 3, 0, 0}, // 0x2C ',' + {24, 3, 1, 4, 0, -2}, // 0x2D '-' + {25, 1, 1, 2, 0, 0}, // 0x2E '.' + {26, 3, 5, 4, 0, -4}, // 0x2F '/' + {28, 3, 5, 4, 0, -4}, // 0x30 '0' + {30, 2, 5, 3, 0, -4}, // 0x31 '1' + {32, 3, 5, 4, 0, -4}, // 0x32 '2' + {34, 3, 5, 4, 0, -4}, // 0x33 '3' + {36, 3, 5, 4, 0, -4}, // 0x34 '4' + {38, 3, 5, 4, 0, -4}, // 0x35 '5' + {40, 3, 5, 4, 0, -4}, // 0x36 '6' + {42, 3, 5, 4, 0, -4}, // 0x37 '7' + {44, 3, 5, 4, 0, -4}, // 0x38 '8' + {46, 3, 5, 4, 0, -4}, // 0x39 '9' + {48, 1, 3, 2, 0, -3}, // 0x3A ':' + {49, 2, 4, 3, 0, -3}, // 0x3B ';' + {50, 2, 3, 3, 0, -3}, // 0x3C '<' + {51, 3, 3, 4, 0, -3}, // 0x3D '=' + {53, 2, 3, 3, 0, -3}, // 0x3E '>' + {54, 3, 5, 4, 0, -4}, // 0x3F '?' + {56, 3, 5, 4, 0, -4}, // 0x40 '@' + {58, 3, 5, 4, 0, -4}, // 0x41 'A' + {60, 3, 5, 4, 0, -4}, // 0x42 'B' + {62, 3, 5, 4, 0, -4}, // 0x43 'C' + {64, 3, 5, 4, 0, -4}, // 0x44 'D' + {66, 3, 5, 4, 0, -4}, // 0x45 'E' + {68, 3, 5, 4, 0, -4}, // 0x46 'F' + {70, 3, 5, 4, 0, -4}, // 0x47 'G' + {72, 3, 5, 4, 0, -4}, // 0x48 'H' + {74, 1, 5, 2, 0, -4}, // 0x49 'I' + {75, 3, 5, 4, 0, -4}, // 0x4A 'J' + {77, 3, 5, 4, 0, -4}, // 0x4B 'K' + {79, 3, 5, 4, 0, -4}, // 0x4C 'L' + {81, 5, 5, 6, 0, -4}, // 0x4D 'M' + {85, 4, 5, 5, 0, -4}, // 0x4E 'N' + {88, 3, 5, 4, 0, -4}, // 0x4F 'O' + {90, 3, 5, 4, 0, -4}, // 0x50 'P' + {92, 3, 6, 4, 0, -4}, // 0x51 'Q' + {95, 3, 5, 4, 0, -4}, // 0x52 'R' + {97, 3, 5, 4, 0, -4}, // 0x53 'S' + {99, 3, 5, 4, 0, -4}, // 0x54 'T' + {101, 3, 5, 4, 0, -4}, // 0x55 'U' + {103, 3, 5, 4, 0, -4}, // 0x56 'V' + {105, 5, 5, 6, 0, -4}, // 0x57 'W' + {109, 3, 5, 4, 0, -4}, // 0x58 'X' + {111, 3, 5, 4, 0, -4}, // 0x59 'Y' + {113, 3, 5, 4, 0, -4}, // 0x5A 'Z' + {115, 2, 5, 3, 0, -4}, // 0x5B '[' + {117, 3, 5, 4, 0, -4}, // 0x5C '\' + {119, 2, 5, 3, 0, -4}, // 0x5D ']' + {121, 3, 2, 4, 0, -4}, // 0x5E '^' + {122, 4, 1, 4, 0, 1}, // 0x5F '_' + {123, 2, 2, 3, 0, -4}, // 0x60 '`' + {124, 3, 4, 4, 0, -3}, // 0x61 'a' + {126, 3, 5, 4, 0, -4}, // 0x62 'b' + {128, 3, 3, 4, 0, -2}, // 0x63 'c' + {130, 3, 5, 4, 0, -4}, // 0x64 'd' + {132, 3, 4, 4, 0, -3}, // 0x65 'e' + {134, 2, 5, 3, 0, -4}, // 0x66 'f' + {136, 3, 5, 4, 0, -3}, // 0x67 'g' + {138, 3, 5, 4, 0, -4}, // 0x68 'h' + {140, 1, 5, 2, 0, -4}, // 0x69 'i' + {141, 2, 6, 3, 0, -4}, // 0x6A 'j' + {143, 3, 5, 4, 0, -4}, // 0x6B 'k' + {145, 2, 5, 3, 0, -4}, // 0x6C 'l' + {147, 5, 3, 6, 0, -2}, // 0x6D 'm' + {149, 3, 3, 4, 0, -2}, // 0x6E 'n' + {151, 3, 3, 4, 0, -2}, // 0x6F 'o' + {153, 3, 4, 4, 0, -2}, // 0x70 'p' + {155, 3, 4, 4, 0, -2}, // 0x71 'q' + {157, 2, 3, 3, 0, -2}, // 0x72 'r' + {158, 3, 4, 4, 0, -3}, // 0x73 's' + {160, 2, 5, 3, 0, -4}, // 0x74 't' + {162, 3, 3, 4, 0, -2}, // 0x75 'u' + {164, 3, 3, 4, 0, -2}, // 0x76 'v' + {166, 5, 3, 6, 0, -2}, // 0x77 'w' + {168, 3, 3, 4, 0, -2}, // 0x78 'x' + {170, 3, 4, 4, 0, -2}, // 0x79 'y' + {172, 3, 4, 4, 0, -3}, // 0x7A 'z' + {174, 3, 5, 4, 0, -4}, // 0x7B '{' + {176, 1, 6, 2, 0, -4}, // 0x7C '|' + {177, 3, 5, 4, 0, -4}, // 0x7D '}' + {179, 4, 2, 5, 0, -3}}; // 0x7E '~' + +const GFXfont Picopixel PROGMEM = {(uint8_t *)PicopixelBitmaps, + (GFXglyph *)PicopixelGlyphs, 0x20, 0x7E, 7}; + +// Approx. 852 bytes \ No newline at end of file diff --git a/Framing.h b/Framing.h new file mode 100755 index 0000000..634fd22 --- /dev/null +++ b/Framing.h @@ -0,0 +1,124 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef FRAMING_H + #define FRAMING_H + + #define FEND 0xC0 + #define FESC 0xDB + #define TFEND 0xDC + #define TFESC 0xDD + + #define CMD_UNKNOWN 0xFE + #define CMD_DATA 0x00 + #define CMD_FREQUENCY 0x01 + #define CMD_BANDWIDTH 0x02 + #define CMD_TXPOWER 0x03 + #define CMD_SF 0x04 + #define CMD_CR 0x05 + #define CMD_RADIO_STATE 0x06 + #define CMD_RADIO_LOCK 0x07 + #define CMD_DETECT 0x08 + #define CMD_IMPLICIT 0x09 + #define CMD_LEAVE 0x0A + #define CMD_ST_ALOCK 0x0B + #define CMD_LT_ALOCK 0x0C + #define CMD_PROMISC 0x0E + #define CMD_READY 0x0F + + #define CMD_STAT_RX 0x21 + #define CMD_STAT_TX 0x22 + #define CMD_STAT_RSSI 0x23 + #define CMD_STAT_SNR 0x24 + #define CMD_STAT_CHTM 0x25 + #define CMD_STAT_PHYPRM 0x26 + #define CMD_STAT_BAT 0x27 + #define CMD_STAT_CSMA 0x28 + #define CMD_STAT_TEMP 0x29 + #define CMD_BLINK 0x30 + #define CMD_RANDOM 0x40 + + #define CMD_FB_EXT 0x41 + #define CMD_FB_READ 0x42 + #define CMD_FB_WRITE 0x43 + #define CMD_FB_READL 0x44 + #define CMD_DISP_READ 0x66 + #define CMD_DISP_INT 0x45 + #define CMD_DISP_ADDR 0x63 + #define CMD_DISP_BLNK 0x64 + #define CMD_DISP_ROT 0x67 + #define CMD_DISP_RCND 0x68 + #define CMD_NP_INT 0x65 + #define CMD_BT_CTRL 0x46 + #define CMD_BT_UNPAIR 0x70 + #define CMD_BT_PIN 0x62 + #define CMD_DIS_IA 0x69 + #define CMD_WIFI_MODE 0x6A + #define CMD_WIFI_SSID 0x6B + #define CMD_WIFI_PSK 0x6C + #define CMD_WIFI_CHN 0x6E + #define CMD_WIFI_IP 0x84 + #define CMD_WIFI_NM 0x85 + + #define CMD_BOARD 0x47 + #define CMD_PLATFORM 0x48 + #define CMD_MCU 0x49 + #define CMD_FW_VERSION 0x50 + #define CMD_CFG_READ 0x6D + #define CMD_ROM_READ 0x51 + #define CMD_ROM_WRITE 0x52 + #define CMD_CONF_SAVE 0x53 + #define CMD_CONF_DELETE 0x54 + #define CMD_DEV_HASH 0x56 + #define CMD_DEV_SIG 0x57 + #define CMD_FW_HASH 0x58 + #define CMD_HASHES 0x60 + #define CMD_FW_UPD 0x61 + #define CMD_UNLOCK_ROM 0x59 + #define ROM_UNLOCK_BYTE 0xF8 + #define CMD_RESET 0x55 + #define CMD_RESET_BYTE 0xF8 + + #define CMD_LOG 0x80 + #define CMD_TIME 0x81 + #define CMD_MUX_CHAIN 0x82 + #define CMD_MUX_DSCVR 0x83 + + #define DETECT_REQ 0x73 + #define DETECT_RESP 0x46 + + #define RADIO_STATE_OFF 0x00 + #define RADIO_STATE_ON 0x01 + + #define NIBBLE_SEQ 0xF0 + #define NIBBLE_FLAGS 0x0F + #define FLAG_SPLIT 0x01 + #define SEQ_UNSET 0xFF + + #define CMD_ERROR 0x90 + #define ERROR_INITRADIO 0x01 + #define ERROR_TXFAILED 0x02 + #define ERROR_EEPROM_LOCKED 0x03 + #define ERROR_QUEUE_FULL 0x04 + #define ERROR_MEMORY_LOW 0x05 + #define ERROR_MODEM_TIMEOUT 0x06 + + // Serial framing variables + size_t frame_len; + bool IN_FRAME = false; + bool ESCAPE = false; + uint8_t command = CMD_UNKNOWN; + +#endif \ No newline at end of file diff --git a/Graphics.h b/Graphics.h new file mode 100755 index 0000000..6eb00e0 --- /dev/null +++ b/Graphics.h @@ -0,0 +1,445 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const unsigned char bm_cable [] PROGMEM = { + 0x00, 0x00, 0x00, 0x1c, 0x00, 0x38, 0x07, 0xfc, 0x08, 0x38, 0x10, 0x1c, 0x10, 0x00, 0x08, 0x00, + 0x07, 0xc0, 0x00, 0x20, 0x00, 0x10, 0x00, 0x10, 0x00, 0x20, 0x07, 0xc0, 0x08, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x04, 0x80, 0x04, 0x43, 0x08, 0x46, + 0xf1, 0x8f, 0x02, 0x16, 0x02, 0x23, 0x01, 0x20, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +const unsigned char bm_rf [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0xc4, + 0x4a, 0xaa, 0x4a, 0xce, 0x6e, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xe0, 0x08, 0x10, 0x13, 0xc8, 0x04, 0x20, 0x01, 0x80, 0x00, 0x00, 0x4e, 0xc4, + 0x4a, 0xaa, 0x4a, 0xce, 0x6e, 0xaa, 0x00, 0x00, 0x01, 0x80, 0x04, 0x20, 0x03, 0xc0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x4e, + 0x31, 0x48, 0x61, 0xca, 0x74, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xe0, 0x08, 0x10, 0x13, 0xc8, 0x04, 0x20, 0x01, 0x80, 0x00, 0x00, 0x71, 0x4e, + 0x31, 0x48, 0x61, 0xca, 0x74, 0x4e, 0x00, 0x00, 0x01, 0x80, 0x04, 0x20, 0x03, 0xc0, 0x00, 0x00 +}; + +const unsigned char bm_wifi [] PROGMEM = { + 0x00, 0x00, 0x07, 0xe0, 0x08, 0x10, 0x13, 0xc8, 0x14, 0x28, 0x01, 0x80, 0x00, 0x00, 0x04, 0x60, + 0x0a, 0x50, 0x0e, 0x60, 0x0a, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xe0, 0x08, 0x10, 0x13, 0xc8, 0x14, 0x28, 0x01, 0x80, 0x00, 0x00, 0x04, 0x60, + 0x0a, 0x50, 0x0e, 0x60, 0x0a, 0x40, 0x00, 0x00, 0x01, 0x80, 0x04, 0x20, 0x03, 0xc0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x74, + 0x54, 0x40, 0x55, 0x64, 0x29, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xe0, 0x08, 0x10, 0x13, 0xc8, 0x14, 0x28, 0x01, 0x80, 0x00, 0x00, 0x45, 0x74, + 0x54, 0x40, 0x55, 0x64, 0x29, 0x44, 0x00, 0x00, 0x01, 0x80, 0x04, 0x20, 0x03, 0xc0, 0x00, 0x00 +}; + +const unsigned char bm_bt [] PROGMEM = { + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x11, 0x40, 0x00, 0x00, 0x05, 0x10, 0x00, 0x00, 0x01, 0x40, + 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x05, 0x10, 0x00, 0x00, 0x11, 0x40, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x01, 0x40, 0x09, 0x20, 0x05, 0x10, 0x03, 0x20, 0x01, 0x40, + 0x01, 0x80, 0x01, 0x40, 0x03, 0x20, 0x05, 0x10, 0x09, 0x20, 0x01, 0x40, 0x01, 0x80, 0x01, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x01, 0x40, 0x09, 0x20, 0x05, 0x10, 0x03, 0x20, 0x01, 0x40, + 0x29, 0x94, 0x01, 0x40, 0x03, 0x20, 0x05, 0x10, 0x09, 0x20, 0x01, 0x40, 0x01, 0x80, 0x01, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x01, 0x40, 0x09, 0x20, 0x05, 0x10, 0x03, 0x20, 0x11, 0x48, + 0x29, 0x94, 0x11, 0x48, 0x03, 0x20, 0x05, 0x10, 0x09, 0x20, 0x01, 0x40, 0x01, 0x80, 0x01, 0x00 +}; + +const unsigned char bm_boot [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x38, 0x66, 0x67, 0x1c, 0x3f, 0xff, 0xff, 0xfc, 0x99, 0xe6, 0x66, 0x4c, 0xff, 0xff, + 0xff, 0xfc, 0x98, 0x70, 0xe6, 0x7c, 0x3f, 0xff, 0xff, 0xfc, 0x99, 0xf0, 0xe6, 0x4c, 0xff, 0xff, + 0xff, 0xfc, 0x38, 0x79, 0xe7, 0x1c, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x0c, 0x38, 0xe1, 0xc3, 0x33, 0x38, 0x7f, 0xfe, 0x7e, 0x72, 0x64, 0xe7, 0x31, 0x33, 0xff, + 0xff, 0x1e, 0x70, 0x61, 0xe7, 0x30, 0x32, 0x7f, 0xff, 0xce, 0x72, 0x61, 0xe7, 0x32, 0x32, 0x7f, + 0xfe, 0x1e, 0x72, 0x64, 0xe7, 0x33, 0x38, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_fw_update [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x98, 0x70, 0xf1, 0xc3, 0x33, 0x38, 0x7f, 0xfc, 0x99, 0x32, 0x64, 0xe7, 0x31, 0x33, 0xff, + 0xfc, 0x98, 0x72, 0x60, 0xe7, 0x30, 0x32, 0x7f, 0xfc, 0x99, 0xf2, 0x64, 0xe7, 0x32, 0x32, 0x7f, + 0xfe, 0x39, 0xf0, 0xe4, 0xe7, 0x33, 0x38, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf8, 0x66, 0x1c, 0xe6, 0x73, 0x8e, 0x1c, 0x3f, 0xf9, 0xe6, 0x4c, 0x46, 0x53, 0x26, 0x4c, 0xff, + 0xf8, 0x66, 0x1c, 0x06, 0x53, 0x06, 0x1c, 0x3f, 0xf9, 0xe6, 0x1c, 0xa6, 0x03, 0x26, 0x1c, 0xff, + 0xf9, 0xe6, 0x4c, 0xe7, 0x27, 0x26, 0x4c, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_console_active [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0x8e, 0x67, 0x0e, 0x39, 0xe1, 0xff, + 0xff, 0x93, 0x26, 0x26, 0x7c, 0x99, 0xe7, 0xff, 0xff, 0x9f, 0x26, 0x07, 0x1c, 0x99, 0xe1, 0xff, + 0xff, 0x93, 0x26, 0x47, 0xcc, 0x99, 0xe7, 0xff, 0xff, 0xc7, 0x8e, 0x66, 0x1e, 0x38, 0x61, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3c, 0x70, 0xcc, 0xcc, 0x3f, 0xff, + 0xff, 0xfc, 0x99, 0x39, 0xcc, 0xcc, 0xff, 0xff, 0xff, 0xfc, 0x19, 0xf9, 0xce, 0x1c, 0x3f, 0xff, + 0xff, 0xfc, 0x99, 0x39, 0xce, 0x1c, 0xff, 0xff, 0xff, 0xfc, 0x9c, 0x79, 0xcf, 0x3c, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_updating [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xff, 0x71, 0x7f, 0xff, + 0xff, 0xff, 0x7f, 0xf5, 0xff, 0x75, 0x7f, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xff, 0x71, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x17, 0xd4, 0x7f, 0x44, 0x7f, 0xff, + 0xff, 0xff, 0x57, 0xd5, 0x7f, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x17, 0xd4, 0x7f, 0x44, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x47, 0x44, 0x71, 0x51, 0x7f, 0xff, + 0xff, 0xff, 0x57, 0x55, 0x75, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x47, 0x44, 0x71, 0x51, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x44, 0x51, 0x51, 0x51, 0x7f, 0xff, + 0xff, 0xff, 0x55, 0x55, 0x55, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x44, 0x51, 0x51, 0x51, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x14, 0x54, 0x45, 0x44, 0x7f, 0xff, + 0xff, 0xff, 0x55, 0x55, 0x55, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x14, 0x54, 0x45, 0x44, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x45, 0x44, 0x51, 0x51, 0x7f, 0xff, + 0xff, 0xff, 0x55, 0x55, 0x55, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x45, 0x44, 0x51, 0x51, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x01, 0xff, 0xff, + 0xff, 0xff, 0x60, 0x00, 0x00, 0x03, 0x7f, 0xff, 0xff, 0xff, 0x30, 0x00, 0x00, 0x07, 0x7f, 0xff, + 0xff, 0xff, 0xf8, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x5c, 0x00, 0x00, 0x1c, 0x7f, 0xff, + 0xff, 0xff, 0x56, 0x00, 0x00, 0x35, 0x7f, 0xff, 0xff, 0xff, 0x57, 0x00, 0x00, 0x74, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0x44, 0xc0, 0x01, 0xd1, 0x7f, 0xff, + 0xff, 0xff, 0x55, 0x60, 0x03, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x44, 0x70, 0x07, 0x51, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x14, 0x5c, 0x1d, 0x44, 0x7f, 0xff, + 0xff, 0xff, 0x55, 0x56, 0x35, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x14, 0x57, 0xe5, 0x44, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x45, 0x44, 0x51, 0x51, 0x7f, 0xff, + 0xff, 0xff, 0x55, 0x55, 0x55, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x45, 0x44, 0x51, 0x51, 0x7f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, + 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_version [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x99, 0x86, 0x1e, 0x19, 0xc7, 0x33, 0xff, 0xff, 0x99, 0x9e, 0x4c, 0xf9, 0x93, 0x13, 0xff, + 0xff, 0xc3, 0x86, 0x1e, 0x39, 0x93, 0x03, 0xff, 0xff, 0xc3, 0x9e, 0x1f, 0x99, 0x93, 0x23, 0xff, + 0xff, 0xe7, 0x86, 0x4c, 0x39, 0xc7, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_fw_corrupt [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0x30, 0xe7, 0x33, 0x9c, 0x70, 0xe1, 0xff, + 0xcf, 0x32, 0x62, 0x32, 0x99, 0x32, 0x67, 0xff, 0xc3, 0x30, 0xe0, 0x32, 0x98, 0x30, 0xe1, 0xff, + 0xcf, 0x30, 0xe5, 0x30, 0x19, 0x30, 0xe7, 0xff, 0xcf, 0x32, 0x67, 0x39, 0x39, 0x32, 0x61, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xc7, 0x0e, 0x1c, 0x98, 0x70, 0xfc, 0xff, + 0xc9, 0x93, 0x26, 0x4c, 0x99, 0x39, 0xfb, 0x7f, 0xcf, 0x93, 0x0e, 0x1c, 0x98, 0x79, 0xfb, 0x7f, + 0xc9, 0x93, 0x0e, 0x1c, 0x99, 0xf9, 0xf7, 0xbf, 0xe3, 0xc7, 0x26, 0x4e, 0x39, 0xf9, 0xf4, 0xbf, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xec, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xec, 0xdf, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdc, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdc, 0xef, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbc, 0xf7, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7c, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xfb, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +static unsigned char bm_def[] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb4, 0x61, 0x10, 0x8c, 0x23, 0xc4, 0x3f, 0xff, + 0xb5, 0xa7, 0xb7, 0xb5, 0xed, 0xed, 0xbf, 0xff, 0xb5, 0xb9, 0xb4, 0xb4, 0x6d, 0xed, 0xbf, 0xff, + 0x85, 0xa1, 0x10, 0xb4, 0x21, 0x44, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe7, 0x1c, 0xfe, 0x7f, 0x8f, 0xf0, 0x00, + 0x1f, 0xf7, 0x9d, 0xff, 0x7f, 0x9f, 0xf0, 0x00, 0x1c, 0x77, 0xfd, 0xc7, 0x73, 0xdc, 0x00, 0x00, + 0x1f, 0xe7, 0xfd, 0xc7, 0x71, 0xdf, 0x00, 0x00, 0x1f, 0xe7, 0x7d, 0xc7, 0x71, 0xdf, 0x00, 0x00, + 0x1c, 0x77, 0x3d, 0xc7, 0x73, 0xdc, 0x00, 0x00, 0x1c, 0x77, 0x1d, 0xff, 0x7f, 0x9f, 0xf0, 0x00, + 0x1c, 0x77, 0x1c, 0xfe, 0x7f, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x54, + 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x62, 0x24, 0x49, 0x22, 0x4e, 0x44, + 0x00, 0x24, 0x93, 0x66, 0xc9, 0x32, 0x44, 0x28, 0x00, 0x20, 0x92, 0xa5, 0x49, 0x2a, 0x44, 0x10, + 0x00, 0x24, 0x92, 0x24, 0x49, 0x26, 0x44, 0x10, 0x00, 0x18, 0x62, 0x24, 0x46, 0x22, 0x44, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1c, 0x9c, 0x44, 0x88, 0xc7, 0x1c, 0x00, 0x00, 0x10, 0x92, 0x6c, 0xa9, 0x24, 0x90, + 0x00, 0x00, 0x1c, 0x9c, 0x54, 0xa9, 0xe7, 0x1c, 0x00, 0x00, 0x10, 0x94, 0x44, 0xa9, 0x25, 0x10, + 0x00, 0x00, 0x10, 0x92, 0x44, 0x51, 0x24, 0x9c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +const unsigned char bm_def_lc [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb4, 0x61, 0x10, 0x8c, 0x23, 0xc4, 0x3f, 0xff, + 0xb5, 0xa7, 0xb7, 0xb5, 0xed, 0xed, 0xbf, 0xff, 0xb5, 0xb9, 0xb4, 0xb4, 0x6d, 0xed, 0xbf, 0xff, + 0x85, 0xa1, 0x10, 0xb4, 0x21, 0x44, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe7, 0x1c, 0xfe, 0x7f, 0x8f, 0xf0, 0x00, + 0x1f, 0xf7, 0x9d, 0xff, 0x7f, 0x9f, 0xf0, 0x00, 0x1c, 0x77, 0xfd, 0xc7, 0x73, 0xdc, 0x00, 0x00, + 0x1f, 0xe7, 0xfd, 0xc7, 0x71, 0xdf, 0x00, 0x00, 0x1f, 0xe7, 0x7d, 0xc7, 0x71, 0xdf, 0x00, 0x00, + 0x1c, 0x77, 0x3d, 0xc7, 0x73, 0xdc, 0x00, 0x00, 0x1c, 0x77, 0x1d, 0xff, 0x7f, 0x9f, 0xf0, 0x00, + 0x1c, 0x77, 0x1c, 0xfe, 0x7f, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x54, + 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8e, 0x39, 0x10, 0x61, 0x88, 0x91, 0x1c, + 0x02, 0x49, 0x21, 0x90, 0x92, 0x4d, 0x9b, 0x20, 0x02, 0x4e, 0x39, 0x50, 0x82, 0x4a, 0x95, 0x18, + 0x02, 0x48, 0x21, 0x30, 0x92, 0x48, 0x91, 0x04, 0x01, 0x88, 0x39, 0x10, 0x61, 0x88, 0x91, 0x38, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0xc8, 0x8e, 0x73, 0x91, 0x1c, 0x00, 0x00, 0x02, 0x05, 0x10, 0x22, 0x1b, 0x20, + 0x00, 0x00, 0x01, 0x82, 0x0c, 0x23, 0x95, 0x18, 0x00, 0x00, 0x00, 0x42, 0x02, 0x22, 0x11, 0x04, + 0x00, 0x00, 0x03, 0x82, 0x1c, 0x23, 0x91, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +const unsigned char bm_frame [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x40, 0x02, 0x0f, 0xff, 0xfc, + 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, + 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, + 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, + 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, + 0x20, 0x00, 0x1e, 0x40, 0x02, 0x78, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, + 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, + 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, + 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, + 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x3f, 0xff, 0xf2, 0x40, 0x02, 0x4f, 0xff, 0xfc, + 0x00, 0x00, 0x02, 0x40, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x03, 0xc0, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x40, 0x02, 0x40, 0x00, 0x00, 0x3f, 0xff, 0xf2, 0x40, 0x02, 0x4f, 0xff, 0xfc, + 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, + 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, + 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, + 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, + 0x20, 0x00, 0x1e, 0x40, 0x02, 0x78, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, + 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, + 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, + 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, + 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x3f, 0xff, 0xf0, 0x40, 0x02, 0x0f, 0xff, 0xfc, + 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x1c, + 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x38, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xaa, 0x8a, 0xaa, 0x80 +}; + +const unsigned char bm_console [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x0f, 0xff, 0xff, + 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x1f, 0xcf, 0xff, 0xff, 0xf3, 0xf8, 0xff, + 0xfe, 0x3f, 0x9f, 0xff, 0xff, 0xf9, 0xfc, 0x7f, 0xfc, 0x7f, 0x99, 0xe6, 0x61, 0x99, 0xfe, 0x3f, + 0xf8, 0xe7, 0x99, 0x26, 0x67, 0x99, 0xe7, 0x1f, 0xf9, 0xc7, 0x99, 0x26, 0x61, 0x99, 0xe3, 0x9f, + 0xf1, 0x8f, 0x98, 0x06, 0x67, 0x99, 0xf1, 0x8f, 0xf3, 0x9f, 0x9c, 0xce, 0x67, 0x99, 0xf9, 0xcf, + 0xf3, 0x99, 0x9f, 0xff, 0xff, 0xf9, 0x99, 0xcf, 0xf3, 0x99, 0x9f, 0xff, 0xff, 0xf9, 0x99, 0xcf, + 0xf3, 0x9f, 0x9f, 0xe3, 0x83, 0xf9, 0xf9, 0xcf, 0xf1, 0x8f, 0x9f, 0xc9, 0x93, 0xf9, 0xf1, 0x8f, + 0xf9, 0xc7, 0x9f, 0xc1, 0x83, 0xf9, 0xe3, 0x9f, 0xf8, 0xe7, 0x9f, 0xc9, 0x9f, 0xf9, 0xe7, 0x1f, + 0xfc, 0x7f, 0x9f, 0xc9, 0x9f, 0xf9, 0xfe, 0x3f, 0xfe, 0x3f, 0x9f, 0xff, 0xff, 0xf9, 0xfc, 0x7f, + 0xff, 0x1f, 0xcf, 0xff, 0xff, 0xf3, 0xf8, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xf0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, + 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfd, 0xff, 0x7f, 0xdf, 0xf7, 0xfd, 0xff, 0x7f, + 0xfd, 0xff, 0x7f, 0xdf, 0xf7, 0xfd, 0xff, 0x7f, 0xfd, 0xff, 0x7f, 0xdf, 0xf7, 0xfd, 0xff, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x78, 0x1e, 0x07, 0x81, 0xe0, 0x78, 0x1f, + 0xef, 0xbb, 0xee, 0xfb, 0xbe, 0xef, 0xbb, 0xef, 0xe8, 0xda, 0xb6, 0x9d, 0xb3, 0x6d, 0xda, 0x37, + 0xef, 0xda, 0xf6, 0xb5, 0xad, 0x6c, 0xdb, 0xf7, 0xe8, 0x5a, 0x36, 0x95, 0xad, 0x6c, 0xda, 0x97, + 0xef, 0xdb, 0xf6, 0x85, 0xb3, 0x6c, 0xda, 0x97, 0xea, 0x5a, 0x36, 0xb5, 0xb3, 0x6c, 0xdb, 0xf7, + 0xef, 0xda, 0xf6, 0xa5, 0xad, 0x6f, 0xda, 0x57, 0xe8, 0x5a, 0xb6, 0x85, 0xad, 0x6c, 0xda, 0x57, + 0xef, 0xdb, 0xf6, 0xfd, 0xbf, 0x6f, 0xdb, 0xf7, 0xe0, 0x18, 0x06, 0x01, 0x80, 0x60, 0x18, 0x07, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x42, 0x7c, 0x60, 0xf0, 0x78, 0x3c, 0x7f, 0xfe, 0x4a, 0x7c, 0x64, 0xf2, 0x79, 0x3c, 0x7f, + 0xfe, 0x43, 0xfe, 0x64, 0xf2, 0x79, 0x3e, 0x7f, 0xfe, 0x4e, 0x7e, 0x64, 0x92, 0x49, 0x26, 0x7f, + 0xfe, 0x4e, 0x7e, 0x60, 0x90, 0x48, 0x26, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + + +const unsigned char bm_checks [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x87, 0x0c, 0x99, 0xc7, 0x0f, + 0xe6, 0x00, 0x7f, 0x93, 0x3c, 0x99, 0x93, 0x3f, 0xe6, 0x00, 0x7f, 0x93, 0x0e, 0x39, 0x9f, 0x0f, + 0xff, 0xff, 0xff, 0x93, 0x3e, 0x39, 0x93, 0x3f, 0xff, 0xff, 0xff, 0x87, 0x0f, 0x79, 0xc7, 0x0f, + 0xe6, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe6, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x39, 0x30, 0xe3, 0x93, 0x87, + 0xe6, 0x00, 0x7c, 0x99, 0x33, 0xc9, 0x87, 0x3f, 0xe6, 0x00, 0x7c, 0xf8, 0x30, 0xcf, 0x8f, 0x8f, + 0xff, 0xff, 0xfc, 0x99, 0x33, 0xc9, 0x87, 0xe7, 0xff, 0xff, 0xfe, 0x39, 0x30, 0xe3, 0x93, 0x0f, + 0xe6, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe6, 0x00, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x9c, 0x3c, 0x78, 0x70, 0xc3, 0x0f, + 0xe6, 0x0d, 0x3c, 0x99, 0x33, 0xe7, 0xcf, 0x27, 0xe6, 0x04, 0x7c, 0x38, 0x38, 0xf1, 0xc3, 0x27, + 0xff, 0xfe, 0xfc, 0xf9, 0x3e, 0x7c, 0xcf, 0x27, 0xff, 0xff, 0xfc, 0xf9, 0x30, 0xe1, 0xc3, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_hwfail [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe4, 0xe3, 0x87, 0x0e, 0x73, 0x8e, 0x1c, 0x3f, + 0xe4, 0xc9, 0x93, 0x26, 0x53, 0x26, 0x4c, 0xff, 0xe0, 0xc1, 0x87, 0x26, 0x53, 0x06, 0x1c, 0x3f, + 0xe4, 0xc9, 0x87, 0x26, 0x03, 0x26, 0x1c, 0xff, 0xe4, 0xc9, 0x93, 0x0f, 0x27, 0x26, 0x4c, 0x3f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xe1, 0xc7, 0x33, 0xc9, 0x87, 0x0f, 0xf9, 0xff, 0xe7, 0x93, 0x33, 0xc9, 0x93, 0x3f, 0xf6, 0xff, + 0xe1, 0x83, 0x33, 0xc9, 0x87, 0x0f, 0xf6, 0xff, 0xe7, 0x93, 0x33, 0xc9, 0x87, 0x3f, 0xef, 0x7f, + 0xe7, 0x93, 0x30, 0xe3, 0x93, 0x0f, 0xe9, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd9, 0xbf, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd9, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb9, 0xdf, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb9, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xef, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x79, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xf9, 0xf7, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_conf_missing [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0x33, 0x87, 0x0c, 0xcc, 0xe1, 0xff, 0xff, + 0xe2, 0x33, 0x3e, 0x7c, 0xc4, 0xcf, 0xff, 0xff, 0xe0, 0x33, 0x8f, 0x1c, 0xc0, 0xc9, 0xff, 0xff, + 0xe5, 0x33, 0xe7, 0xcc, 0xc8, 0xc9, 0xff, 0xff, 0xe7, 0x33, 0x0e, 0x1c, 0xcc, 0xe3, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xf1, 0xe3, 0x99, 0x86, 0x70, 0xff, 0xf6, 0xff, + 0xe4, 0xc9, 0x89, 0x9e, 0x67, 0xff, 0xf6, 0xff, 0xe7, 0xc9, 0x81, 0x86, 0x64, 0xff, 0xef, 0x7f, + 0xe4, 0xc9, 0x91, 0x9e, 0x64, 0xff, 0xe9, 0x7f, 0xf1, 0xe3, 0x99, 0x9e, 0x71, 0xff, 0xd9, 0xbf, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd9, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb9, 0xdf, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb9, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xef, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x79, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xf9, 0xf7, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_no_radio [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xc7, 0x0e, 0x71, 0xfc, 0xce, 0x38, 0x7f, + 0xc9, 0x93, 0x26, 0x64, 0xfc, 0x4c, 0x9c, 0xff, 0xc3, 0x83, 0x26, 0x64, 0xfc, 0x0c, 0x9c, 0xff, + 0xc3, 0x93, 0x26, 0x64, 0xfc, 0x8c, 0x9c, 0xff, 0xc9, 0x93, 0x0e, 0x71, 0xfc, 0xce, 0x3c, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0x8e, 0x4c, 0xcc, 0x3f, 0xff, 0xfc, 0xff, + 0xcf, 0x26, 0x4c, 0x4c, 0x9f, 0xff, 0xfb, 0x7f, 0xc3, 0x26, 0x4c, 0x0c, 0x9f, 0xff, 0xfb, 0x7f, + 0xcf, 0x26, 0x4c, 0x8c, 0x9f, 0xff, 0xf7, 0xbf, 0xcf, 0x8f, 0x1c, 0xcc, 0x3f, 0xff, 0xf4, 0xbf, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xec, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xec, 0xdf, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdc, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdc, 0xef, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbc, 0xf7, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7c, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xfb, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_hwok [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf2, 0x71, 0xc3, 0x87, 0x39, 0xc7, 0x0e, 0x1f, 0xf2, 0x64, 0xc9, 0x93, 0x29, 0x93, 0x26, 0x7f, + 0xf0, 0x60, 0xc3, 0x93, 0x29, 0x83, 0x0e, 0x1f, 0xf2, 0x64, 0xc3, 0x93, 0x01, 0x93, 0x0e, 0x7f, + 0xf2, 0x64, 0xc9, 0x87, 0x93, 0x93, 0x26, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x33, 0x30, 0xff, 0x8e, 0x4f, 0xff, + 0xff, 0xf3, 0x13, 0x39, 0xff, 0x26, 0x1f, 0xff, 0xff, 0xf3, 0x03, 0x39, 0xff, 0x26, 0x3f, 0xff, + 0xff, 0xf3, 0x23, 0x39, 0xff, 0x26, 0x1f, 0xff, 0xff, 0xf3, 0x33, 0x39, 0xff, 0x8e, 0x4f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc3, 0x0e, 0x67, 0xf8, 0x70, 0xe3, 0x87, 0x33, 0xe7, 0x27, 0x0f, 0xf9, 0x33, 0xc9, 0x93, 0x87, + 0xe7, 0x0f, 0x9f, 0xf8, 0x70, 0xc1, 0x93, 0xcf, 0xe7, 0x0f, 0x0f, 0xf8, 0x73, 0xc9, 0x93, 0xcf, + 0xe7, 0x26, 0x67, 0xf9, 0x30, 0xc9, 0x87, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_nfr [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x38, 0x66, 0x67, 0x1c, 0x3f, 0xff, 0xff, 0xfc, 0x99, 0xe6, 0x66, 0x4c, 0xff, + 0xff, 0x9f, 0xfc, 0x98, 0x70, 0xe6, 0x7c, 0x3f, 0xff, 0x6f, 0xfc, 0x99, 0xf0, 0xe6, 0x4c, 0xff, + 0xff, 0x6f, 0xfc, 0x38, 0x79, 0xe7, 0x1c, 0x3f, 0xfe, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x97, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x9b, 0xe6, 0x71, 0xc3, 0xe1, 0xc7, 0x0f, + 0xfd, 0x9b, 0xe2, 0x64, 0xe7, 0xe7, 0x93, 0x27, 0xfb, 0x9d, 0xe0, 0x64, 0xe7, 0xe1, 0x93, 0x0f, + 0xfb, 0x9d, 0xe4, 0x64, 0xe7, 0xe7, 0x93, 0x0f, 0xf7, 0xfe, 0xe6, 0x71, 0xe7, 0xe7, 0xc7, 0x27, + 0xf7, 0x9e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x9f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xef, 0xff, 0x7f, 0xf8, 0x71, 0xcf, 0x0f, 0xff, 0xf0, 0x00, 0xff, 0xf3, 0xe4, 0xcf, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0xe0, 0xcf, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x64, 0xcf, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xf0, 0xe4, 0xc3, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_online [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc2, 0x1c, 0x66, 0x61, 0x8c, 0x24, 0x90, 0x87, + 0xe6, 0x49, 0x22, 0x4f, 0x24, 0xe4, 0x93, 0x93, 0xe6, 0x18, 0x20, 0x63, 0x3c, 0x26, 0x30, 0x87, + 0xe6, 0x19, 0x24, 0x79, 0x24, 0xe6, 0x33, 0x87, 0xe6, 0x49, 0x26, 0x43, 0x8c, 0x27, 0x70, 0x93, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe6, 0x73, 0xe7, 0x33, 0x87, 0xff, + 0xff, 0xe4, 0xe2, 0x73, 0xe7, 0x13, 0x9f, 0xff, 0xff, 0xe4, 0xe0, 0x73, 0xe7, 0x03, 0x87, 0xff, + 0xff, 0xe4, 0xe4, 0x73, 0xe7, 0x23, 0x9f, 0xff, 0xff, 0xf1, 0xe6, 0x70, 0xe7, 0x33, 0x87, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_pairing [] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf0, 0xe3, 0x98, 0x73, 0x33, 0x87, 0xff, 0xff, 0xf2, 0xc9, 0x99, 0x33, 0x13, 0x3f, 0xff, + 0xff, 0xf0, 0xc1, 0x98, 0x73, 0x03, 0x27, 0xff, 0xff, 0xf3, 0xc9, 0x98, 0x73, 0x23, 0x27, 0xff, + 0xff, 0xf3, 0xc9, 0x99, 0x33, 0x33, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +const unsigned char bm_n_uh [] PROGMEM = { + 0x07, 0x27, 0x27, 0x27, 0x07, 0x8f, 0x8f, 0xcf, 0xcf, 0xcf, 0x07, 0xe7, 0x07, 0x3f, 0x07, 0x07, + 0xe7, 0xc7, 0xe7, 0x07, 0x27, 0x27, 0x07, 0xe7, 0xe7, 0x07, 0x3f, 0x07, 0xe7, 0x07, 0x07, 0x3f, + 0x07, 0x27, 0x07, 0x07, 0xc7, 0xcf, 0x9f, 0x1f, 0x07, 0x27, 0x07, 0x27, 0x07, 0x07, 0x27, 0x07, + 0xe7, 0xe7 +}; + +const unsigned char bm_plug [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x7f, 0x80, 0x55, 0xfc, 0x00, 0xaa, 0xfc, 0x00, 0x00, + 0x7f, 0x80, 0x00, 0x1c, 0x00 +}; + +const unsigned char bm_hg_low [] PROGMEM = { + 0xf8, 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0xf8, 0xf8 +}; + +const unsigned char bm_hg_high [] PROGMEM = { + 0xf8, 0x88, 0xf8, 0x70, 0x20, 0x70, 0xf8, 0xf8, 0xf8 +}; \ No newline at end of file diff --git a/Input.h b/Input.h new file mode 100755 index 0000000..996a09c --- /dev/null +++ b/Input.h @@ -0,0 +1,95 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef INPUT_H + #define INPUT_H + + #define PIN_BUTTON pin_btn_usr1 + + #define PRESSED LOW + #define RELEASED HIGH + + #define EVENT_ALL 0x00 + #define EVENT_CLICKS 0x01 + #define EVENT_BUTTON_DOWN 0x11 + #define EVENT_BUTTON_UP 0x12 + #define EVENT_BUTTON_CLICK 0x13 + #define EVENT_BUTTON_DOUBLE_CLICK 0x14 + #define EVENT_BUTTON_TRIPLE_CLICK 0x15 + + int button_events = EVENT_CLICKS; + int button_state = RELEASED; + int debounce_state = button_state; + unsigned long button_debounce_last = 0; + unsigned long button_debounce_delay = 25; + unsigned long button_down_last = 0; + unsigned long button_up_last = 0; + + // Forward declaration + void button_event(uint8_t event, unsigned long duration); + + void input_init() { + pinMode(PIN_BUTTON, INPUT_PULLUP); + } + + void input_get_all_events() { + button_events = EVENT_ALL; + } + + void input_get_click_events() { + button_events = EVENT_CLICKS; + } + + void input_read() { + int button_reading = digitalRead(PIN_BUTTON); + if (button_reading != debounce_state) { + button_debounce_last = millis(); + debounce_state = button_reading; + } + + if ((millis() - button_debounce_last) > button_debounce_delay) { + if (button_reading != button_state) { + // State changed + int previous_state = button_state; + button_state = button_reading; + + if (button_events == EVENT_ALL) { + if (button_state == PRESSED) { + button_event(EVENT_BUTTON_DOWN, 0); + } else if (button_state == RELEASED) { + button_event(EVENT_BUTTON_UP, 0); + } + } else if (button_events == EVENT_CLICKS) { + if (previous_state == PRESSED && button_state == RELEASED) { + button_up_last = millis(); + button_event(EVENT_BUTTON_CLICK, button_up_last-button_down_last); + } else if (previous_state == RELEASED && button_state == PRESSED) { + button_down_last = millis(); + } + } + } + } + + } + + bool button_pressed() { + if (button_state == PRESSED) { + return true; + } else { + return false; + } + } + +#endif \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/MD5.cpp b/MD5.cpp new file mode 100755 index 0000000..56daa77 --- /dev/null +++ b/MD5.cpp @@ -0,0 +1,301 @@ +#include "MD5.h" + +MD5::MD5() +{ + //nothing + return; +} + +char* MD5::make_digest(const unsigned char *digest, int len) /* {{{ */ +{ + char * md5str = (char*) malloc(sizeof(char)*(len*2+1)); + static const char hexits[17] = "0123456789abcdef"; + int i; + + for (i = 0; i < len; i++) { + md5str[i * 2] = hexits[digest[i] >> 4]; + md5str[(i * 2) + 1] = hexits[digest[i] & 0x0F]; + } + md5str[len * 2] = '\0'; + return md5str; +} + +/* + * The basic MD5 functions. + * + * E and G are optimized compared to their RFC 1321 definitions for + * architectures that lack an AND-NOT instruction, just like in Colin Plumb's + * implementation. + * E() has been used instead of F() because F() is already defined in the Arduino core + */ +#define E(x, y, z) ((z) ^ ((x) & ((y) ^ (z)))) +#define G(x, y, z) ((y) ^ ((z) & ((x) ^ (y)))) +#define H(x, y, z) ((x) ^ (y) ^ (z)) +#define I(x, y, z) ((y) ^ ((x) | ~(z))) + +/* + * The MD5 transformation for all four rounds. + */ +#define STEP(f, a, b, c, d, x, t, s) \ + (a) += f((b), (c), (d)) + (x) + (t); \ + (a) = (((a) << (s)) | (((a) & 0xffffffff) >> (32 - (s)))); \ + (a) += (b); + +/* + * SET reads 4 input bytes in little-endian byte order and stores them + * in a properly aligned word in host byte order. + * + * The check for little-endian architectures that tolerate unaligned + * memory accesses is just an optimization. Nothing will break if it + * doesn't work. + */ +#if defined(__i386__) || defined(__x86_64__) || defined(__vax__) +# define SET(n) \ + (*(MD5_u32plus *)&ptr[(n) * 4]) +# define GET(n) \ + SET(n) +#else +# define SET(n) \ + (ctx->block[(n)] = \ + (MD5_u32plus)ptr[(n) * 4] | \ + ((MD5_u32plus)ptr[(n) * 4 + 1] << 8) | \ + ((MD5_u32plus)ptr[(n) * 4 + 2] << 16) | \ + ((MD5_u32plus)ptr[(n) * 4 + 3] << 24)) +# define GET(n) \ + (ctx->block[(n)]) +#endif + +/* + * This processes one or more 64-byte data blocks, but does NOT update + * the bit counters. There are no alignment requirements. + */ +const void *MD5::body(void *ctxBuf, const void *data, size_t size) +{ + MD5_CTX *ctx = (MD5_CTX*)ctxBuf; + const unsigned char *ptr; + MD5_u32plus a, b, c, d; + MD5_u32plus saved_a, saved_b, saved_c, saved_d; + + ptr = (unsigned char*)data; + + a = ctx->a; + b = ctx->b; + c = ctx->c; + d = ctx->d; + + do { + saved_a = a; + saved_b = b; + saved_c = c; + saved_d = d; + +/* Round 1 + * E() has been used instead of F() because F() is already defined in the Arduino core + */ + STEP(E, a, b, c, d, SET(0), 0xd76aa478, 7) + STEP(E, d, a, b, c, SET(1), 0xe8c7b756, 12) + STEP(E, c, d, a, b, SET(2), 0x242070db, 17) + STEP(E, b, c, d, a, SET(3), 0xc1bdceee, 22) + STEP(E, a, b, c, d, SET(4), 0xf57c0faf, 7) + STEP(E, d, a, b, c, SET(5), 0x4787c62a, 12) + STEP(E, c, d, a, b, SET(6), 0xa8304613, 17) + STEP(E, b, c, d, a, SET(7), 0xfd469501, 22) + STEP(E, a, b, c, d, SET(8), 0x698098d8, 7) + STEP(E, d, a, b, c, SET(9), 0x8b44f7af, 12) + STEP(E, c, d, a, b, SET(10), 0xffff5bb1, 17) + STEP(E, b, c, d, a, SET(11), 0x895cd7be, 22) + STEP(E, a, b, c, d, SET(12), 0x6b901122, 7) + STEP(E, d, a, b, c, SET(13), 0xfd987193, 12) + STEP(E, c, d, a, b, SET(14), 0xa679438e, 17) + STEP(E, b, c, d, a, SET(15), 0x49b40821, 22) + +/* Round 2 */ + STEP(G, a, b, c, d, GET(1), 0xf61e2562, 5) + STEP(G, d, a, b, c, GET(6), 0xc040b340, 9) + STEP(G, c, d, a, b, GET(11), 0x265e5a51, 14) + STEP(G, b, c, d, a, GET(0), 0xe9b6c7aa, 20) + STEP(G, a, b, c, d, GET(5), 0xd62f105d, 5) + STEP(G, d, a, b, c, GET(10), 0x02441453, 9) + STEP(G, c, d, a, b, GET(15), 0xd8a1e681, 14) + STEP(G, b, c, d, a, GET(4), 0xe7d3fbc8, 20) + STEP(G, a, b, c, d, GET(9), 0x21e1cde6, 5) + STEP(G, d, a, b, c, GET(14), 0xc33707d6, 9) + STEP(G, c, d, a, b, GET(3), 0xf4d50d87, 14) + STEP(G, b, c, d, a, GET(8), 0x455a14ed, 20) + STEP(G, a, b, c, d, GET(13), 0xa9e3e905, 5) + STEP(G, d, a, b, c, GET(2), 0xfcefa3f8, 9) + STEP(G, c, d, a, b, GET(7), 0x676f02d9, 14) + STEP(G, b, c, d, a, GET(12), 0x8d2a4c8a, 20) + +/* Round 3 */ + STEP(H, a, b, c, d, GET(5), 0xfffa3942, 4) + STEP(H, d, a, b, c, GET(8), 0x8771f681, 11) + STEP(H, c, d, a, b, GET(11), 0x6d9d6122, 16) + STEP(H, b, c, d, a, GET(14), 0xfde5380c, 23) + STEP(H, a, b, c, d, GET(1), 0xa4beea44, 4) + STEP(H, d, a, b, c, GET(4), 0x4bdecfa9, 11) + STEP(H, c, d, a, b, GET(7), 0xf6bb4b60, 16) + STEP(H, b, c, d, a, GET(10), 0xbebfbc70, 23) + STEP(H, a, b, c, d, GET(13), 0x289b7ec6, 4) + STEP(H, d, a, b, c, GET(0), 0xeaa127fa, 11) + STEP(H, c, d, a, b, GET(3), 0xd4ef3085, 16) + STEP(H, b, c, d, a, GET(6), 0x04881d05, 23) + STEP(H, a, b, c, d, GET(9), 0xd9d4d039, 4) + STEP(H, d, a, b, c, GET(12), 0xe6db99e5, 11) + STEP(H, c, d, a, b, GET(15), 0x1fa27cf8, 16) + STEP(H, b, c, d, a, GET(2), 0xc4ac5665, 23) + +/* Round 4 */ + STEP(I, a, b, c, d, GET(0), 0xf4292244, 6) + STEP(I, d, a, b, c, GET(7), 0x432aff97, 10) + STEP(I, c, d, a, b, GET(14), 0xab9423a7, 15) + STEP(I, b, c, d, a, GET(5), 0xfc93a039, 21) + STEP(I, a, b, c, d, GET(12), 0x655b59c3, 6) + STEP(I, d, a, b, c, GET(3), 0x8f0ccc92, 10) + STEP(I, c, d, a, b, GET(10), 0xffeff47d, 15) + STEP(I, b, c, d, a, GET(1), 0x85845dd1, 21) + STEP(I, a, b, c, d, GET(8), 0x6fa87e4f, 6) + STEP(I, d, a, b, c, GET(15), 0xfe2ce6e0, 10) + STEP(I, c, d, a, b, GET(6), 0xa3014314, 15) + STEP(I, b, c, d, a, GET(13), 0x4e0811a1, 21) + STEP(I, a, b, c, d, GET(4), 0xf7537e82, 6) + STEP(I, d, a, b, c, GET(11), 0xbd3af235, 10) + STEP(I, c, d, a, b, GET(2), 0x2ad7d2bb, 15) + STEP(I, b, c, d, a, GET(9), 0xeb86d391, 21) + + a += saved_a; + b += saved_b; + c += saved_c; + d += saved_d; + + ptr += 64; + } while (size -= 64); + + ctx->a = a; + ctx->b = b; + ctx->c = c; + ctx->d = d; + + return ptr; +} + +void MD5::MD5Init(void *ctxBuf) +{ + MD5_CTX *ctx = (MD5_CTX*)ctxBuf; + ctx->a = 0x67452301; + ctx->b = 0xefcdab89; + ctx->c = 0x98badcfe; + ctx->d = 0x10325476; + + ctx->lo = 0; + ctx->hi = 0; + + memset(ctx->block, 0, sizeof(ctx->block)); + memset(ctx->buffer, 0, sizeof(ctx->buffer)); +} + +void MD5::MD5Update(void *ctxBuf, const void *data, size_t size) +{ + MD5_CTX *ctx = (MD5_CTX*)ctxBuf; + MD5_u32plus saved_lo; + MD5_u32plus used, free; + + saved_lo = ctx->lo; + if ((ctx->lo = (saved_lo + size) & 0x1fffffff) < saved_lo) { + ctx->hi++; + } + ctx->hi += size >> 29; + + used = saved_lo & 0x3f; + + if (used) { + free = 64 - used; + + if (size < free) { + memcpy(&ctx->buffer[used], data, size); + return; + } + + memcpy(&ctx->buffer[used], data, free); + data = (unsigned char *)data + free; + size -= free; + body(ctx, ctx->buffer, 64); + } + + if (size >= 64) { + data = body(ctx, data, size & ~(size_t)0x3f); + size &= 0x3f; + } + + memcpy(ctx->buffer, data, size); +} + +void MD5::MD5Final(unsigned char *result, void *ctxBuf) +{ + MD5_CTX *ctx = (MD5_CTX*)ctxBuf; + MD5_u32plus used, free; + + used = ctx->lo & 0x3f; + + ctx->buffer[used++] = 0x80; + + free = 64 - used; + + if (free < 8) { + memset(&ctx->buffer[used], 0, free); + body(ctx, ctx->buffer, 64); + used = 0; + free = 64; + } + + memset(&ctx->buffer[used], 0, free - 8); + + ctx->lo <<= 3; + ctx->buffer[56] = ctx->lo; + ctx->buffer[57] = ctx->lo >> 8; + ctx->buffer[58] = ctx->lo >> 16; + ctx->buffer[59] = ctx->lo >> 24; + ctx->buffer[60] = ctx->hi; + ctx->buffer[61] = ctx->hi >> 8; + ctx->buffer[62] = ctx->hi >> 16; + ctx->buffer[63] = ctx->hi >> 24; + + body(ctx, ctx->buffer, 64); + + result[0] = ctx->a; + result[1] = ctx->a >> 8; + result[2] = ctx->a >> 16; + result[3] = ctx->a >> 24; + result[4] = ctx->b; + result[5] = ctx->b >> 8; + result[6] = ctx->b >> 16; + result[7] = ctx->b >> 24; + result[8] = ctx->c; + result[9] = ctx->c >> 8; + result[10] = ctx->c >> 16; + result[11] = ctx->c >> 24; + result[12] = ctx->d; + result[13] = ctx->d >> 8; + result[14] = ctx->d >> 16; + result[15] = ctx->d >> 24; + + memset(ctx, 0, sizeof(*ctx)); +} +unsigned char* MD5::make_hash(char *arg) +{ + MD5_CTX context; + unsigned char * hash = (unsigned char *) malloc(16); + MD5Init(&context); + MD5Update(&context, arg, strlen(arg)); + MD5Final(hash, &context); + return hash; +} +unsigned char* MD5::make_hash(char *arg,size_t size) +{ + MD5_CTX context; + unsigned char * hash = (unsigned char *) malloc(16); + MD5Init(&context); + MD5Update(&context, arg, size); + MD5Final(hash, &context); + return hash; +} \ No newline at end of file diff --git a/MD5.h b/MD5.h new file mode 100755 index 0000000..3ec8d81 --- /dev/null +++ b/MD5.h @@ -0,0 +1,52 @@ +#ifndef MD5_h +#define MD5_h + +#include "Arduino.h" + +/* + * This is an OpenSSL-compatible implementation of the RSA Data Security, + * Inc. MD5 Message-Digest Algorithm (RFC 1321). + * + * Written by Solar Designer in 2001, and placed + * in the public domain. There's absolutely no warranty. + * + * This differs from Colin Plumb's older public domain implementation in + * that no 32-bit integer data type is required, there's no compile-time + * endianness configuration, and the function prototypes match OpenSSL's. + * The primary goals are portability and ease of use. + * + * This implementation is meant to be fast, but not as fast as possible. + * Some known optimizations are not included to reduce source code size + * and avoid compile-time configuration. + */ + +/* + * Updated by Scott MacVicar for arduino + * + */ + +#include + +typedef unsigned long MD5_u32plus; + +typedef struct { + MD5_u32plus lo, hi; + MD5_u32plus a, b, c, d; + unsigned char buffer[64]; + MD5_u32plus block[16]; +} MD5_CTX; + +class MD5 +{ +public: + MD5(); + static unsigned char* make_hash(char *arg); + static unsigned char* make_hash(char *arg,size_t size); + static char* make_digest(const unsigned char *digest, int len); + static const void *body(void *ctxBuf, const void *data, size_t size); + static void MD5Init(void *ctxBuf); + static void MD5Final(unsigned char *result, void *ctxBuf); + static void MD5Update(void *ctxBuf, const void *data, size_t size); +}; + +#endif \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..8ba24a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,518 @@ +# Copyright (C) 2024, Mark Qvist + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Version 2.0.17 of the Arduino ESP core is based on ESP-IDF v4.4.7 +ARDUINO_ESP_CORE_VER = 2.0.17 + +# Version 3.2.0 of the Arduino ESP core is based on ESP-IDF v5.4.1 +# ARDUINO_ESP_CORE_VER = 3.2.0 + +all: release + +clean: + -rm -r ./build + -rm ./Release/rnode_firmware* + +prep: prep-avr prep-esp32 prep-samd + +prep-avr: + arduino-cli core update-index --config-file arduino-cli.yaml + arduino-cli core install arduino:avr --config-file arduino-cli.yaml + arduino-cli core install unsignedio:avr --config-file arduino-cli.yaml + +prep-esp32: + arduino-cli core update-index --config-file arduino-cli.yaml + arduino-cli core install esp32:esp32@$(ARDUINO_ESP_CORE_VER) --config-file arduino-cli.yaml + arduino-cli lib install "Adafruit SSD1306" + arduino-cli lib install "Adafruit SH110X" + arduino-cli lib install "Adafruit ST7735 and ST7789 Library" + arduino-cli lib install "Adafruit NeoPixel" + arduino-cli lib install "XPowersLib" + arduino-cli lib install "Crypto" + +prep-samd: + arduino-cli core update-index --config-file arduino-cli.yaml + arduino-cli core install adafruit:samd --config-file arduino-cli.yaml + +prep-nrf: + arduino-cli core update-index --config-file arduino-cli.yaml + arduino-cli core install rakwireless:nrf52 --config-file arduino-cli.yaml + arduino-cli core install Heltec_nRF52:Heltec_nRF52 --config-file arduino-cli.yaml + arduino-cli core install adafruit:nrf52 --config-file arduino-cli.yaml + arduino-cli lib install "GxEPD2" + arduino-cli config set library.enable_unsafe_install true + arduino-cli lib install --git-url https://github.com/liamcottle/esp8266-oled-ssd1306#e16cee124fe26490cb14880c679321ad8ac89c95 + pip install adafruit-nrfutil --upgrade + +console-site: + make -C Console clean site + +spiffs: console-site spiffs-image + +spiffs-image: + python Release/esptool/spiffsgen.py 1966080 ./Console/build Release/console_image.bin + +upload-spiffs: + @echo Deploying SPIFFS image... + python ./Release/esptool/esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +check_bt_buffers: + @./esp32_btbufs.py ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/libraries/BluetoothSerial/src/BluetoothSerial.cpp + +firmware: + arduino-cli compile --log --fqbn unsignedio:avr:rnode + +firmware-mega2560: + arduino-cli compile --log --fqbn arduino:avr:mega + +firmware-tbeam: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:t-beam -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x33\"" + +firmware-tbeam_sx126x: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:t-beam -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x33\" \"-DMODEM=0x03\"" + +firmware-t3s3: + arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DMODEM=0x03\"" + +firmware-t3s3_sx127x: + arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DMODEM=0x01\"" + +firmware-t3s3_sx1280_pa: + arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DMODEM=0x04\"" + +firmware-tdeck: + arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3B\"" + +firmware-tbeam_supreme: + arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x3D" + +firmware-lora32_v10: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\"" + +firmware-lora32_v10_extled: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\" \"-DEXTERNAL_LEDS=true\"" + +firmware-lora32_v20: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x36\" \"-DEXTERNAL_LEDS=true\"" + +firmware-lora32_v21: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\"" + +firmware-lora32_v21_extled: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\" \"-DEXTERNAL_LEDS=true\"" + +firmware-lora32_v21_tcxo: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\" \"-DENABLE_TCXO=true\"" + +firmware-heltec32_v2: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:heltec_wifi_lora_32_V2 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\"" + +firmware-heltec32_v2_extled: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:heltec_wifi_lora_32_V2 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\" \"-DEXTERNAL_LEDS=true\"" + +firmware-heltec32_v3: + arduino-cli compile --log --fqbn esp32:esp32:heltec_wifi_lora_32_V3 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3A\"" + +firmware-heltec32_v4: + arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3F\"" + +firmware-rnode_ng_20: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x40\"" + +firmware-rnode_ng_21: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x41\"" + +firmware-featheresp32: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:featheresp32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x34\"" + +firmware-genericesp32: check_bt_buffers + arduino-cli compile --log --fqbn esp32:esp32:esp32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x35\"" + +firmware-rak4631: + arduino-cli compile --log --fqbn rakwireless:nrf52:WisCoreRAK4631Board -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x51\"" + +firmware-heltec_t114: + arduino-cli compile --log --fqbn Heltec_nRF52:Heltec_nRF52:HT-n5262 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3C\"" + +firmware-techo: + arduino-cli compile --log --fqbn adafruit:nrf52:pca10056 -e --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x44\"" + +firmware-xiao_s3: + arduino-cli compile --log --fqbn "esp32:esp32:XIAO_ESP32S3" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\"" + +upload: + arduino-cli upload -p /dev/ttyUSB0 --fqbn unsignedio:avr:rnode + +upload-mega2560: + arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega + +upload-tbeam: + arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:t-beam + @sleep 1 + rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.t-beam/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-tbeam_sx1262: + arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:t-beam + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.t-beam/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-lora32_v10: + arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:ttgo-lora32 + @sleep 1 + rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-lora32_v20: + arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:ttgo-lora32 + @sleep 1 + rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-lora32_v21: + arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:ttgo-lora32 + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-heltec32_v2: + arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:heltec_wifi_lora_32_V2 + @sleep 1 + rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB1 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-heltec32_v3: + arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:heltec_wifi_lora_32_V3 + @sleep 1 + rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.heltec_wifi_lora_32_V3/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-heltec32_v4: + arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3 + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-tdeck: + arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3 + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-tbeam_supreme: + arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3 + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-rnode_ng_20: + arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:ttgo-lora32 + @sleep 1 + rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-rnode_ng_21: + arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:ttgo-lora32 + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-t3s3: + arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3 + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-featheresp32: + arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:featheresp32 + @sleep 1 + rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.featheresp32/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +upload-rak4631: + arduino-cli upload -p /dev/ttyACM0 --fqbn rakwireless:nrf52:WisCoreRAK4631Board + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes from_device /dev/ttyACM0) + +upload-heltec_t114: + arduino-cli upload -p /dev/ttyACM0 --fqbn Heltec_nRF52:Heltec_nRF52:HT-n5262 + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes from_device /dev/ttyACM0) + +upload-techo: + arduino-cli upload -p /dev/ttyACM0 --fqbn adafruit:nrf52:pca10056 + @sleep 6 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes from_device /dev/ttyACM0) + +upload-xiao_s3: + arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:XIAO_ESP32S3 + @sleep 1 + rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware.ino.bin) + @sleep 3 + python ./Release/esptool/esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin + +release: release-all + +release-all: console-site spiffs-image release-tbeam release-tbeam_sx1262 release-lora32_v10 release-lora32_v20 release-lora32_v21 release-lora32_v10_extled release-lora32_v20_extled release-lora32_v21_extled release-lora32_v21_tcxo release-featheresp32 release-genericesp32 release-heltec32_v2 release-heltec32_v3 release-heltec32_v4 release-heltec32_v2_extled release-heltec_t114 release-techo release-rnode_ng_20 release-rnode_ng_21 release-t3s3 release-t3s3_sx127x release-t3s3_sx1280_pa release-tdeck release-tbeam_supreme release-rak4631 release-xiao_s3 release-hashes + +release-hashes: + python ./release_hashes.py > ./Release/release.json + +release-rnode: + arduino-cli compile --fqbn unsignedio:avr:rnode -e + cp build/unsignedio.avr.rnode/RNode_Firmware.ino.hex Release/rnode_firmware.hex + rm -r build + +release-tbeam: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:t-beam -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x33\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_tbeam.boot_app0 + cp build/esp32.esp32.t-beam/RNode_Firmware.ino.bin build/rnode_firmware_tbeam.bin + cp build/esp32.esp32.t-beam/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_tbeam.bootloader + cp build/esp32.esp32.t-beam/RNode_Firmware.ino.partitions.bin build/rnode_firmware_tbeam.partitions + zip --junk-paths ./Release/rnode_firmware_tbeam.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_tbeam.boot_app0 build/rnode_firmware_tbeam.bin build/rnode_firmware_tbeam.bootloader build/rnode_firmware_tbeam.partitions + rm -r build + +release-tbeam_sx1262: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:t-beam -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x33\" \"-DMODEM=0x03\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_tbeam_sx1262.boot_app0 + cp build/esp32.esp32.t-beam/RNode_Firmware.ino.bin build/rnode_firmware_tbeam_sx1262.bin + cp build/esp32.esp32.t-beam/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_tbeam_sx1262.bootloader + cp build/esp32.esp32.t-beam/RNode_Firmware.ino.partitions.bin build/rnode_firmware_tbeam_sx1262.partitions + zip --junk-paths ./Release/rnode_firmware_tbeam_sx1262.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_tbeam_sx1262.boot_app0 build/rnode_firmware_tbeam_sx1262.bin build/rnode_firmware_tbeam_sx1262.bootloader build/rnode_firmware_tbeam_sx1262.partitions + rm -r build + +release-lora32_v10: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_lora32v10.boot_app0 + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin build/rnode_firmware_lora32v10.bin + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_lora32v10.bootloader + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_lora32v10.partitions + zip --junk-paths ./Release/rnode_firmware_lora32v10.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_lora32v10.boot_app0 build/rnode_firmware_lora32v10.bin build/rnode_firmware_lora32v10.bootloader build/rnode_firmware_lora32v10.partitions + rm -r build + +release-lora32_v20: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x36\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_lora32v20.boot_app0 + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin build/rnode_firmware_lora32v20.bin + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_lora32v20.bootloader + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_lora32v20.partitions + zip --junk-paths ./Release/rnode_firmware_lora32v20.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_lora32v20.boot_app0 build/rnode_firmware_lora32v20.bin build/rnode_firmware_lora32v20.bootloader build/rnode_firmware_lora32v20.partitions + rm -r build + +release-lora32_v21: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_lora32v21.boot_app0 + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin build/rnode_firmware_lora32v21.bin + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_lora32v21.bootloader + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_lora32v21.partitions + zip --junk-paths ./Release/rnode_firmware_lora32v21.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_lora32v21.boot_app0 build/rnode_firmware_lora32v21.bin build/rnode_firmware_lora32v21.bootloader build/rnode_firmware_lora32v21.partitions + rm -r build + +release-lora32_v10_extled: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\" \"-DEXTERNAL_LEDS=true\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_lora32v10.boot_app0 + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin build/rnode_firmware_lora32v10.bin + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_lora32v10.bootloader + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_lora32v10.partitions + zip --junk-paths ./Release/rnode_firmware_lora32v10.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_lora32v10.boot_app0 build/rnode_firmware_lora32v10.bin build/rnode_firmware_lora32v10.bootloader build/rnode_firmware_lora32v10.partitions + rm -r build + +release-lora32_v20_extled: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x36\" \"-DEXTERNAL_LEDS=true\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_lora32v20.boot_app0 + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin build/rnode_firmware_lora32v20.bin + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_lora32v20.bootloader + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_lora32v20.partitions + zip --junk-paths ./Release/rnode_firmware_lora32v20_extled.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_lora32v20.boot_app0 build/rnode_firmware_lora32v20.bin build/rnode_firmware_lora32v20.bootloader build/rnode_firmware_lora32v20.partitions + rm -r build + +release-lora32_v21_extled: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\" \"-DEXTERNAL_LEDS=true\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_lora32v21.boot_app0 + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin build/rnode_firmware_lora32v21.bin + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_lora32v21.bootloader + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_lora32v21.partitions + zip --junk-paths ./Release/rnode_firmware_lora32v21_extled.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_lora32v21.boot_app0 build/rnode_firmware_lora32v21.bin build/rnode_firmware_lora32v21.bootloader build/rnode_firmware_lora32v21.partitions + rm -r build + +release-lora32_v21_tcxo: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\" \"-DENABLE_TCXO=true\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_lora32v21_tcxo.boot_app0 + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin build/rnode_firmware_lora32v21_tcxo.bin + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_lora32v21_tcxo.bootloader + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_lora32v21_tcxo.partitions + zip --junk-paths ./Release/rnode_firmware_lora32v21_tcxo.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_lora32v21_tcxo.boot_app0 build/rnode_firmware_lora32v21_tcxo.bin build/rnode_firmware_lora32v21_tcxo.bootloader build/rnode_firmware_lora32v21_tcxo.partitions + rm -r build + +release-heltec32_v2: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V2 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_heltec32v2.boot_app0 + cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.bin build/rnode_firmware_heltec32v2.bin + cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_heltec32v2.bootloader + cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.partitions.bin build/rnode_firmware_heltec32v2.partitions + zip --junk-paths ./Release/rnode_firmware_heltec32v2.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_heltec32v2.boot_app0 build/rnode_firmware_heltec32v2.bin build/rnode_firmware_heltec32v2.bootloader build/rnode_firmware_heltec32v2.partitions + rm -r build + +release-heltec32_v3: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V3 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3A\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_heltec32v3.boot_app0 + cp build/esp32.esp32.heltec_wifi_lora_32_V3/RNode_Firmware.ino.bin build/rnode_firmware_heltec32v3.bin + cp build/esp32.esp32.heltec_wifi_lora_32_V3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_heltec32v3.bootloader + cp build/esp32.esp32.heltec_wifi_lora_32_V3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_heltec32v3.partitions + zip --junk-paths ./Release/rnode_firmware_heltec32v3.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_heltec32v3.boot_app0 build/rnode_firmware_heltec32v3.bin build/rnode_firmware_heltec32v3.bootloader build/rnode_firmware_heltec32v3.partitions + rm -r build + +release-heltec32_v4: check_bt_buffers + arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3F\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_heltec32v4pa.boot_app0 + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin build/rnode_firmware_heltec32v4pa.bin + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_heltec32v4pa.bootloader + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_heltec32v4pa.partitions + zip --junk-paths ./Release/rnode_firmware_heltec32v4pa.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_heltec32v4pa.boot_app0 build/rnode_firmware_heltec32v4pa.bin build/rnode_firmware_heltec32v4pa.bootloader build/rnode_firmware_heltec32v4pa.partitions + rm -r build + +release-heltec32_v2_extled: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V2 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\" \"-DEXTERNAL_LEDS=true\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_heltec32v2.boot_app0 + cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.bin build/rnode_firmware_heltec32v2.bin + cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_heltec32v2.bootloader + cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.partitions.bin build/rnode_firmware_heltec32v2.partitions + zip --junk-paths ./Release/rnode_firmware_heltec32v2.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_heltec32v2.boot_app0 build/rnode_firmware_heltec32v2.bin build/rnode_firmware_heltec32v2.bootloader build/rnode_firmware_heltec32v2.partitions + rm -r build + +release-rnode_ng_20: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x40\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_ng20.boot_app0 + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin build/rnode_firmware_ng20.bin + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_ng20.bootloader + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_ng20.partitions + zip --junk-paths ./Release/rnode_firmware_ng20.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_ng20.boot_app0 build/rnode_firmware_ng20.bin build/rnode_firmware_ng20.bootloader build/rnode_firmware_ng20.partitions + rm -r build + +release-rnode_ng_21: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x41\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_ng21.boot_app0 + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin build/rnode_firmware_ng21.bin + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_ng21.bootloader + cp build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_ng21.partitions + zip --junk-paths ./Release/rnode_firmware_ng21.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_ng21.boot_app0 build/rnode_firmware_ng21.bin build/rnode_firmware_ng21.bootloader build/rnode_firmware_ng21.partitions + rm -r build + +release-t3s3: + arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DMODEM=0x03\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_t3s3.boot_app0 + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin build/rnode_firmware_t3s3.bin + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_t3s3.bootloader + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_t3s3.partitions + zip --junk-paths ./Release/rnode_firmware_t3s3.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_t3s3.boot_app0 build/rnode_firmware_t3s3.bin build/rnode_firmware_t3s3.bootloader build/rnode_firmware_t3s3.partitions + rm -r build + +release-t3s3_sx1280_pa: + arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DMODEM=0x04\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_t3s3_sx1280_pa.boot_app0 + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin build/rnode_firmware_t3s3_sx1280_pa.bin + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_t3s3_sx1280_pa.bootloader + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_t3s3_sx1280_pa.partitions + zip --junk-paths ./Release/rnode_firmware_t3s3_sx1280_pa.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_t3s3_sx1280_pa.boot_app0 build/rnode_firmware_t3s3_sx1280_pa.bin build/rnode_firmware_t3s3_sx1280_pa.bootloader build/rnode_firmware_t3s3_sx1280_pa.partitions + rm -r build + +release-t3s3_sx127x: + arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DMODEM=0x01\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_t3s3_sx127x.boot_app0 + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin build/rnode_firmware_t3s3_sx127x.bin + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_t3s3_sx127x.bootloader + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_t3s3_sx127x.partitions + zip --junk-paths ./Release/rnode_firmware_t3s3_sx127x.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_t3s3_sx127x.boot_app0 build/rnode_firmware_t3s3_sx127x.bin build/rnode_firmware_t3s3_sx127x.bootloader build/rnode_firmware_t3s3_sx127x.partitions + rm -r build + +release-tdeck: + arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3B\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_tdeck.boot_app0 + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin build/rnode_firmware_tdeck.bin + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_tdeck.bootloader + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_tdeck.partitions + zip --junk-paths ./Release/rnode_firmware_tdeck.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_tdeck.boot_app0 build/rnode_firmware_tdeck.bin build/rnode_firmware_tdeck.bootloader build/rnode_firmware_tdeck.partitions + rm -r build + +release-tbeam_supreme: + arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3D\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_tbeam_supreme.boot_app0 + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin build/rnode_firmware_tbeam_supreme.bin + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_tbeam_supreme.bootloader + cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_tbeam_supreme.partitions + zip --junk-paths ./Release/rnode_firmware_tbeam_supreme.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_tbeam_supreme.boot_app0 build/rnode_firmware_tbeam_supreme.bin build/rnode_firmware_tbeam_supreme.bootloader build/rnode_firmware_tbeam_supreme.partitions + rm -r build + +release-featheresp32: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:featheresp32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x34\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_featheresp32.boot_app0 + cp build/esp32.esp32.featheresp32/RNode_Firmware.ino.bin build/rnode_firmware_featheresp32.bin + cp build/esp32.esp32.featheresp32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_featheresp32.bootloader + cp build/esp32.esp32.featheresp32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_featheresp32.partitions + zip --junk-paths ./Release/rnode_firmware_featheresp32.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_featheresp32.boot_app0 build/rnode_firmware_featheresp32.bin build/rnode_firmware_featheresp32.bootloader build/rnode_firmware_featheresp32.partitions + rm -r build + +release-genericesp32: check_bt_buffers + arduino-cli compile --fqbn esp32:esp32:esp32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x35\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_esp32_generic.boot_app0 + cp build/esp32.esp32.esp32/RNode_Firmware.ino.bin build/rnode_firmware_esp32_generic.bin + cp build/esp32.esp32.esp32/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_esp32_generic.bootloader + cp build/esp32.esp32.esp32/RNode_Firmware.ino.partitions.bin build/rnode_firmware_esp32_generic.partitions + zip --junk-paths ./Release/rnode_firmware_esp32_generic.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_esp32_generic.boot_app0 build/rnode_firmware_esp32_generic.bin build/rnode_firmware_esp32_generic.bootloader build/rnode_firmware_esp32_generic.partitions + rm -r build + +release-mega2560: + arduino-cli compile --fqbn arduino:avr:mega -e --build-property "compiler.cpp.extra_flags=\"-DMODEM=0x01\"" + cp build/arduino.avr.mega/RNode_Firmware.ino.hex Release/rnode_firmware_m2560.hex + rm -r build + +release-rak4631: + arduino-cli compile --fqbn rakwireless:nrf52:WisCoreRAK4631Board -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x51\"" + cp build/rakwireless.nrf52.WisCoreRAK4631Board/RNode_Firmware.ino.hex build/rnode_firmware_rak4631.hex + adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_rak4631.hex Release/rnode_firmware_rak4631.zip + +release-heltec_t114: + arduino-cli compile --fqbn Heltec_nRF52:Heltec_nRF52:HT-n5262 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3C\"" + cp build/Heltec_nRF52.Heltec_nRF52.HT-n5262/RNode_Firmware.ino.hex build/rnode_firmware_heltec_t114.hex + adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_heltec_t114.hex Release/rnode_firmware_heltec_t114.zip + +release-techo: + arduino-cli compile --log --fqbn adafruit:nrf52:pca10056 -e --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x44\"" + cp build/adafruit.nrf52.pca10056/RNode_Firmware.ino.hex build/rnode_firmware_techo.hex + adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_techo.hex Release/rnode_firmware_techo.zip + +release-xiao_s3: + arduino-cli compile --fqbn "esp32:esp32:XIAO_ESP32S3" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\"" + cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_xiao_esp32s3.boot_app0 + cp build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware.ino.bin build/rnode_firmware_xiao_esp32s3.bin + cp build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_xiao_esp32s3.bootloader + cp build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_xiao_esp32s3.partitions + zip --junk-paths ./Release/rnode_firmware_xiao_esp32s3.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_xiao_esp32s3.boot_app0 build/rnode_firmware_xiao_esp32s3.bin build/rnode_firmware_xiao_esp32s3.bootloader build/rnode_firmware_xiao_esp32s3.partitions + rm -r build \ No newline at end of file diff --git a/Modem.h b/Modem.h new file mode 100755 index 0000000..027e314 --- /dev/null +++ b/Modem.h @@ -0,0 +1,4 @@ +#define SX1276 0x01 +#define SX1278 0x02 +#define SX1262 0x03 +#define SX1280 0x04 diff --git a/NoopFileSystem.h b/NoopFileSystem.h new file mode 100755 index 0000000..bfc8b8e --- /dev/null +++ b/NoopFileSystem.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef HAS_RNS + +#include +#include +#include + +class NoopFileSystem : public RNS::FileSystemImpl { + +public: + NoopFileSystem() {} + + bool init() { return true; } + bool format() { return false; } + bool reformat() { return false; } + +public: + // CBA Debug + static void listDir(const char* dir, const char* prefix = "") {} + static void dumpDir(const char* dir) {} + +public: + virtual bool file_exists(const char* file_path) { return false; } + virtual size_t read_file(const char* file_path, RNS::Bytes& data) { return 0; } + virtual size_t write_file(const char* file_path, const RNS::Bytes& data) { return 0; } + virtual bool remove_file(const char* file_path) { return false; } + virtual bool rename_file(const char* from_file_path, const char* to_file_path) { return false; } + virtual bool directory_exists(const char* directory_path) { return false; } + virtual bool create_directory(const char* directory_path) { return false; } + virtual bool remove_directory(const char* directory_path) { return false; } + virtual std::list list_directory(const char* directory_path) { return std::list(); } + virtual size_t storage_size() { return 0; } + virtual size_t storage_available() { return 0; } + +}; + +#endif diff --git a/Power.h b/Power.h new file mode 100755 index 0000000..7ce6d8a --- /dev/null +++ b/Power.h @@ -0,0 +1,650 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#define PMU_TEMP_MIN -30 +#define PMU_TEMP_MAX 90 +#define PMU_TEMP_OFFSET 120 +bool pmu_temp_sensor_ready = false; +float pmu_temperature = PMU_TEMP_MIN-1; + +#if BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1 + #include + XPowersLibInterface* PMU = NULL; + + #ifndef PMU_WIRE_PORT + #if BOARD_MODEL == BOARD_TBEAM_S_V1 + #define PMU_WIRE_PORT Wire1 + #else + #define PMU_WIRE_PORT Wire + #endif + #endif + + #define BAT_V_MIN 3.15 + #define BAT_V_MAX 4.14 + + void disablePeripherals() { + if (PMU) { + // GNSS RTC PowerVDD + PMU->enablePowerOutput(XPOWERS_VBACKUP); + + // LoRa VDD + PMU->disablePowerOutput(XPOWERS_ALDO2); + + // GNSS VDD + PMU->disablePowerOutput(XPOWERS_ALDO3); + } + } + + bool pmuInterrupt; + void setPmuFlag() + { + pmuInterrupt = true; + } +#elif BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 + #define BAT_V_MIN 3.15 + #define BAT_V_MAX 4.3 + #define BAT_V_CHG 4.48 + #define BAT_V_FLOAT 4.33 + #define BAT_SAMPLES 5 + const uint8_t pin_vbat = 35; + float bat_p_samples[BAT_SAMPLES]; + float bat_v_samples[BAT_SAMPLES]; + uint8_t bat_samples_count = 0; + int bat_discharging_samples = 0; + int bat_charging_samples = 0; + int bat_charged_samples = 0; + bool bat_voltage_dropping = false; + float bat_delay_v = 0; + float bat_state_change_v = 0; +#elif BOARD_MODEL == BOARD_T3S3 + #define BAT_V_MIN 3.15 + #define BAT_V_MAX 4.217 + #define BAT_V_CHG 4.48 + #define BAT_V_FLOAT 4.33 + #define BAT_SAMPLES 5 + const uint8_t pin_vbat = 1; + float bat_p_samples[BAT_SAMPLES]; + float bat_v_samples[BAT_SAMPLES]; + uint8_t bat_samples_count = 0; + int bat_discharging_samples = 0; + int bat_charging_samples = 0; + int bat_charged_samples = 0; + bool bat_voltage_dropping = false; + float bat_delay_v = 0; + float bat_state_change_v = 0; +#elif BOARD_MODEL == BOARD_TDECK + #define BAT_V_MIN 3.15 + #define BAT_V_MAX 4.3 + #define BAT_V_CHG 4.48 + #define BAT_V_FLOAT 4.33 + #define BAT_SAMPLES 5 + const uint8_t pin_vbat = 4; + float bat_p_samples[BAT_SAMPLES]; + float bat_v_samples[BAT_SAMPLES]; + uint8_t bat_samples_count = 0; + int bat_discharging_samples = 0; + int bat_charging_samples = 0; + int bat_charged_samples = 0; + bool bat_voltage_dropping = false; + float bat_delay_v = 0; + float bat_state_change_v = 0; +#elif BOARD_MODEL == BOARD_HELTEC32_V3 + // Unless we implement some real voodoo + // on these boards, we can't say with + // any certainty whether we are actually + // charging and have reached a charge + // complete state. The *only* data point + // we have to go from is the bus voltage. + // The BAT_V_CHG and BAT_V_FLOAT values + // are set high here to avoid the display + // indication confusingly flapping + // between charge completed, charging and + // discharging states. + // Update: Vodoo implemented. Hopefully + // it will work accross different boards. + #define BAT_V_MIN 3.05 + #define BAT_V_MAX 4.0 + #define BAT_V_CHG 4.48 + #define BAT_V_FLOAT 4.33 + #define BAT_SAMPLES 7 + const uint8_t pin_vbat = 1; + const uint8_t pin_ctrl = 37; + float bat_p_samples[BAT_SAMPLES]; + float bat_v_samples[BAT_SAMPLES]; + uint8_t bat_samples_count = 0; + int bat_discharging_samples = 0; + int bat_charging_samples = 0; + int bat_charged_samples = 0; + bool bat_voltage_dropping = false; + float bat_delay_v = 0; + float bat_state_change_v = 0; +#elif BOARD_MODEL == BOARD_HELTEC32_V4 + #define BAT_V_MIN 3.05 + #define BAT_V_MAX 4.0 + #define BAT_V_CHG 4.48 + #define BAT_V_FLOAT 4.33 + #define BAT_SAMPLES 7 + const uint8_t pin_vbat = 1; + const uint8_t pin_ctrl = 37; + float bat_p_samples[BAT_SAMPLES]; + float bat_v_samples[BAT_SAMPLES]; + uint8_t bat_samples_count = 0; + int bat_discharging_samples = 0; + int bat_charging_samples = 0; + int bat_charged_samples = 0; + bool bat_voltage_dropping = false; + float bat_delay_v = 0; + float bat_state_change_v = 0; +#elif BOARD_MODEL == BOARD_HELTEC_T114 + #define BAT_V_MIN 3.15 + #define BAT_V_MAX 4.165 + #define BAT_V_CHG 4.48 + #define BAT_V_FLOAT 4.33 + #define BAT_SAMPLES 7 + const uint8_t pin_vbat = 4; + const uint8_t pin_ctrl = 6; + float bat_p_samples[BAT_SAMPLES]; + float bat_v_samples[BAT_SAMPLES]; + uint8_t bat_samples_count = 0; + int bat_discharging_samples = 0; + int bat_charging_samples = 0; + int bat_charged_samples = 0; + bool bat_voltage_dropping = false; + float bat_delay_v = 0; + float bat_state_change_v = 0; +#elif BOARD_MODEL == BOARD_TECHO + #define BAT_V_MIN 3.15 + #define BAT_V_MAX 4.16 + #define BAT_V_CHG 4.48 + #define BAT_V_FLOAT 4.33 + #define BAT_SAMPLES 7 + const uint8_t pin_vbat = 4; + float bat_p_samples[BAT_SAMPLES]; + float bat_v_samples[BAT_SAMPLES]; + uint8_t bat_samples_count = 0; + int bat_discharging_samples = 0; + int bat_charging_samples = 0; + int bat_charged_samples = 0; + bool bat_voltage_dropping = false; + float bat_delay_v = 0; + float bat_state_change_v = 0; +#endif + +uint32_t last_pmu_update = 0; +uint8_t pmu_target_pps = 1; +int pmu_update_interval = 1000/pmu_target_pps; +uint8_t pmu_charged_ascertain = 0; +uint8_t pmu_rc = 0; +uint8_t pmu_sc = 0; +float bat_delay_diff = 0; +bool bat_diff_positive = false; +#define PMU_R_INTERVAL 5 +#define PMU_SCV_RESET_INTERVAL 3 +void kiss_indicate_battery(); +void kiss_indicate_temperature(); + +void measure_temperature() { + #if PLATFORM == PLATFORM_ESP32 + if (pmu_temp_sensor_ready) { pmu_temperature = temperatureRead(); } else { pmu_temperature = PMU_TEMP_MIN-1; } + #endif +} + +void measure_battery() { + #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TECHO + battery_installed = true; + #if BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 + battery_indeterminate = false; + #else + battery_indeterminate = true; + #endif + + #if BOARD_MODEL == BOARD_HELTEC32_V3 + float battery_measurement = (float)(analogRead(pin_vbat)) * 0.0041; + #elif BOARD_MODEL == BOARD_HELTEC32_V4 + float battery_measurement = (float)(analogRead(pin_vbat)) * 0.00418; + #elif BOARD_MODEL == BOARD_T3S3 + float battery_measurement = (float)(analogRead(pin_vbat)) / 4095.0*6.7828; + #elif BOARD_MODEL == BOARD_HELTEC_T114 + float battery_measurement = (float)(analogRead(pin_vbat)) * 0.017165; + #elif BOARD_MODEL == BOARD_TECHO + float battery_measurement = (float)(analogRead(pin_vbat)) * 0.007067; + #else + float battery_measurement = (float)(analogRead(pin_vbat)) / 4095.0*7.26; + #endif + + bat_v_samples[bat_samples_count%BAT_SAMPLES] = battery_measurement; + bat_p_samples[bat_samples_count%BAT_SAMPLES] = ((battery_voltage-BAT_V_MIN) / (BAT_V_MAX-BAT_V_MIN))*100.0; + + bat_samples_count++; + if (!battery_ready && bat_samples_count >= BAT_SAMPLES) { + battery_ready = true; + } + + if (battery_ready) { + + battery_percent = 0; + for (uint8_t bi = 0; bi < BAT_SAMPLES; bi++) { + battery_percent += bat_p_samples[bi]; + } + battery_percent = battery_percent/BAT_SAMPLES; + + battery_voltage = 0; + for (uint8_t bi = 0; bi < BAT_SAMPLES; bi++) { + battery_voltage += bat_v_samples[bi]; + } + battery_voltage = battery_voltage/BAT_SAMPLES; + + if (bat_delay_v == 0) bat_delay_v = battery_voltage; + if (bat_state_change_v == 0) bat_state_change_v = battery_voltage; + if (battery_percent > 100.0) battery_percent = 100.0; + if (battery_percent < 0.0) battery_percent = 0.0; + + if (bat_samples_count%BAT_SAMPLES == 0) { + pmu_sc++; + bat_delay_diff = battery_voltage-bat_state_change_v; + + if (battery_voltage < bat_delay_v && battery_voltage < BAT_V_FLOAT) { + if (bat_voltage_dropping == false) { + if (bat_delay_diff < -0.008) { + bat_voltage_dropping = true; + bat_state_change_v = battery_voltage; + } + } else { + if (pmu_sc%PMU_SCV_RESET_INTERVAL == 0) { bat_state_change_v = battery_voltage; } + } + } else { + if (bat_voltage_dropping == true) { + if (bat_delay_diff > 0.01) { + bat_voltage_dropping = false; + bat_state_change_v = battery_voltage; + } + } + } + bat_samples_count = 0; + bat_delay_v = battery_voltage; + } + + if (bat_voltage_dropping && battery_voltage < BAT_V_FLOAT) { + // if (battery_state != BATTERY_STATE_DISCHARGING) { SerialBT.printf("STATE CHANGE to DISCHARGING at delta=%.3fv. State change v is now %.3fv.\n", bat_delay_diff, bat_state_change_v); } + battery_state = BATTERY_STATE_DISCHARGING; + pmu_charged_ascertain = 0; + } else { + if (pmu_charged_ascertain < 8) { pmu_charged_ascertain++; } + else { + if (battery_percent < 100.0) { + // if (battery_state != BATTERY_STATE_CHARGING) { SerialBT.printf("STATE CHANGE to CHARGING at delta=%.3fv. State change v is now %.3fv.\n", bat_delay_diff, bat_state_change_v); } + battery_state = BATTERY_STATE_CHARGING; + } else { + // if (battery_state != BATTERY_STATE_CHARGED) { SerialBT.printf("STATE CHANGE to CHARGED at delta=%.3fv. State change v is now %.3fv.\n", bat_delay_diff, bat_state_change_v); } + battery_state = BATTERY_STATE_CHARGED; + } + } + } + + #if MCU_VARIANT == MCU_NRF52 + if (bt_state != BT_STATE_OFF) { blebas.write(battery_percent); } + #endif + + // if (bt_state == BT_STATE_CONNECTED) { + // SerialBT.printf("\nBus voltage %.3fv. Unfiltered %.3fv. Diff %.3f", battery_voltage, bat_v_samples[BAT_SAMPLES-1], bat_delay_diff); + // if (bat_voltage_dropping) { SerialBT.printf("\n Voltage is dropping. Percentage %.1f%%.", battery_percent); } + // else { SerialBT.printf("\n Voltage is not dropping. Percentage %.1f%%.", battery_percent); } + // if (battery_state == BATTERY_STATE_DISCHARGING) { SerialBT.printf("\n Battery discharging. delay_v %.3fv\nState change at %.3fv", bat_delay_v, bat_state_change_v); } + // if (battery_state == BATTERY_STATE_CHARGING) { SerialBT.printf("\n Battery charging. delay_v %.3fv\nState change at %.3fv", bat_delay_v, bat_state_change_v); } + // if (battery_state == BATTERY_STATE_CHARGED) { SerialBT.print("\n Battery is charged."); } + // SerialBT.print("\n"); + // } + } + + #elif BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1 + if (PMU) { + float discharge_current = 0; + float charge_current = 0; + float ext_voltage = 0; + float ext_current = 0; + if (PMU->getChipModel() == XPOWERS_AXP192) { + discharge_current = ((XPowersAXP192*)PMU)->getBattDischargeCurrent(); + charge_current = ((XPowersAXP192*)PMU)->getBatteryChargeCurrent(); + battery_voltage = PMU->getBattVoltage()/1000.0; + // battery_percent = PMU->getBattPercentage()*1.0; + battery_installed = PMU->isBatteryConnect(); + external_power = PMU->isVbusIn(); + ext_voltage = PMU->getVbusVoltage()/1000.0; + ext_current = ((XPowersAXP192*)PMU)->getVbusCurrent(); + } + else if (PMU->getChipModel() == XPOWERS_AXP2101) { + battery_voltage = PMU->getBattVoltage()/1000.0; + // battery_percent = PMU->getBattPercentage()*1.0; + battery_installed = PMU->isBatteryConnect(); + external_power = PMU->isVbusIn(); + ext_voltage = PMU->getVbusVoltage()/1000.0; + } + + if (battery_installed) { + if (PMU->isCharging()) { + battery_state = BATTERY_STATE_CHARGING; + battery_percent = ((battery_voltage-BAT_V_MIN) / (BAT_V_MAX-BAT_V_MIN))*100.0; + } else { + if (PMU->isDischarge()) { + battery_state = BATTERY_STATE_DISCHARGING; + battery_percent = ((battery_voltage-BAT_V_MIN) / (BAT_V_MAX-BAT_V_MIN))*100.0; + } else { + battery_state = BATTERY_STATE_CHARGED; + battery_percent = 100.0; + } + } + } else { + battery_state = BATTERY_STATE_UNKNOWN; + battery_percent = 0.0; + battery_voltage = 0.0; + } + + if (battery_percent > 100.0) battery_percent = 100.0; + if (battery_percent < 0.0) battery_percent = 0.0; + + float charge_watts = battery_voltage*(charge_current/1000.0); + float discharge_watts = battery_voltage*(discharge_current/1000.0); + float ext_watts = ext_voltage*(ext_current/1000.0); + + battery_ready = true; + + // if (bt_state == BT_STATE_CONNECTED) { + // if (battery_installed) { + // if (external_power) { + // SerialBT.printf("External power connected, drawing %.2fw, %.1fmA at %.1fV\n", ext_watts, ext_current, ext_voltage); + // } else { + // SerialBT.println("Running on battery"); + // } + // SerialBT.printf("Battery percentage %.1f%%\n", battery_percent); + // SerialBT.printf("Battery voltage %.2fv\n", battery_voltage); + // // SerialBT.printf("Temperature %.1f%\n", auxillary_temperature); + + // if (battery_state == BATTERY_STATE_CHARGING) { + // SerialBT.printf("Charging with %.2fw, %.1fmA at %.1fV\n", charge_watts, charge_current, battery_voltage); + // } else if (battery_state == BATTERY_STATE_DISCHARGING) { + // SerialBT.printf("Discharging at %.2fw, %.1fmA at %.1fV\n", discharge_watts, discharge_current, battery_voltage); + // } else if (battery_state == BATTERY_STATE_CHARGED) { + // SerialBT.printf("Battery charged\n"); + // } + // } else { + // SerialBT.println("No battery installed"); + // } + // SerialBT.println(""); + // } + } + else { + battery_ready = false; + } + #endif + + if (battery_ready) { + pmu_rc++; + if (pmu_rc%PMU_R_INTERVAL == 0) { + kiss_indicate_battery(); + if (pmu_temp_sensor_ready) { kiss_indicate_temperature(); } + } + } +} + +void update_pmu() { + if (millis()-last_pmu_update >= pmu_update_interval) { + measure_battery(); + measure_temperature(); + last_pmu_update = millis(); + } +} + +bool init_pmu() { + #if IS_ESP32S3 + pmu_temp_sensor_ready = true; + #endif + + #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_TECHO + pinMode(pin_vbat, INPUT); + return true; + #elif BOARD_MODEL == BOARD_HELTEC32_V3 + // there are three version of V3: V3, V3.1, and V3.2 + // V3 and V3.1 have a pull up on pin_ctrl and are active low + // V3.2 has a transistor and active high + // put the pin input mode and read it. if it's high, we have V3 or V3.1 + // other wise, it's a V3.2 + uint16_t pin_ctrl_value; + uint8_t pin_ctrl_active = LOW; + pinMode(pin_ctrl, INPUT); + pin_ctrl_value = digitalRead(pin_ctrl); + if(pin_ctrl_value == HIGH) { + // We have either a V3 or V3.1 + pin_ctrl_active = LOW; + } + else { + // We have a V3.2 + pin_ctrl_active = HIGH; + } + pinMode(pin_ctrl,OUTPUT); + digitalWrite(pin_ctrl, pin_ctrl_active); + return true; + #elif BOARD_MODEL == BOARD_HELTEC32_V4 + pinMode(pin_ctrl,OUTPUT); + digitalWrite(pin_ctrl, HIGH); + return true; + #elif BOARD_MODEL == BOARD_HELTEC_T114 + pinMode(pin_ctrl,OUTPUT); + digitalWrite(pin_ctrl, HIGH); + return true; + #elif BOARD_MODEL == BOARD_TBEAM + Wire.begin(I2C_SDA, I2C_SCL); + + if (!PMU) { + PMU = new XPowersAXP2101(PMU_WIRE_PORT); + if (!PMU->init()) { + delete PMU; + PMU = NULL; + } + } + + if (!PMU) { + PMU = new XPowersAXP192(PMU_WIRE_PORT); + if (!PMU->init()) { + delete PMU; + PMU = NULL; + } + } + + if (!PMU) { + return false; + } + + // Configure charging indicator + PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF); + + pinMode(PMU_IRQ, INPUT_PULLUP); + attachInterrupt(PMU_IRQ, setPmuFlag, FALLING); + + if (PMU->getChipModel() == XPOWERS_AXP192) { + + // Turn off unused power sources to save power + PMU->disablePowerOutput(XPOWERS_DCDC1); + PMU->disablePowerOutput(XPOWERS_DCDC2); + PMU->disablePowerOutput(XPOWERS_LDO2); + PMU->disablePowerOutput(XPOWERS_LDO3); + + // Set the power of LoRa and GPS module to 3.3V + // LoRa + PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300); + // GPS + PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300); + // OLED + PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); + + // Turn on LoRa + PMU->enablePowerOutput(XPOWERS_LDO2); + + // Turn on GPS + //PMU->enablePowerOutput(XPOWERS_LDO3); + + // protected oled power source + PMU->setProtectedChannel(XPOWERS_DCDC1); + // protected esp32 power source + PMU->setProtectedChannel(XPOWERS_DCDC3); + // enable oled power + PMU->enablePowerOutput(XPOWERS_DCDC1); + + PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ); + + PMU->enableIRQ(XPOWERS_AXP192_VBUS_REMOVE_IRQ | + XPOWERS_AXP192_VBUS_INSERT_IRQ | + XPOWERS_AXP192_BAT_CHG_DONE_IRQ | + XPOWERS_AXP192_BAT_CHG_START_IRQ | + XPOWERS_AXP192_BAT_REMOVE_IRQ | + XPOWERS_AXP192_BAT_INSERT_IRQ | + XPOWERS_AXP192_PKEY_SHORT_IRQ + ); + + } + else if (PMU->getChipModel() == XPOWERS_AXP2101) { + + // Turn off unused power sources to save power + PMU->disablePowerOutput(XPOWERS_DCDC2); + PMU->disablePowerOutput(XPOWERS_DCDC3); + PMU->disablePowerOutput(XPOWERS_DCDC4); + PMU->disablePowerOutput(XPOWERS_DCDC5); + PMU->disablePowerOutput(XPOWERS_ALDO1); + PMU->disablePowerOutput(XPOWERS_ALDO2); + PMU->disablePowerOutput(XPOWERS_ALDO3); + PMU->disablePowerOutput(XPOWERS_ALDO4); + PMU->disablePowerOutput(XPOWERS_BLDO1); + PMU->disablePowerOutput(XPOWERS_BLDO2); + PMU->disablePowerOutput(XPOWERS_DLDO1); + PMU->disablePowerOutput(XPOWERS_DLDO2); + PMU->disablePowerOutput(XPOWERS_VBACKUP); + + // Set the power of LoRa and GPS module to 3.3V + // LoRa + PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); + // GPS + PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); + PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); + + // ESP32 VDD + // ! No need to set, automatically open , Don't close it + // PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); + // PMU->setProtectedChannel(XPOWERS_DCDC1); + PMU->setProtectedChannel(XPOWERS_DCDC1); + + // LoRa VDD + PMU->enablePowerOutput(XPOWERS_ALDO2); + + // GNSS VDD + //PMU->enablePowerOutput(XPOWERS_ALDO3); + + // GNSS RTC PowerVDD + //PMU->enablePowerOutput(XPOWERS_VBACKUP); + } + + PMU->enableSystemVoltageMeasure(); + PMU->enableVbusVoltageMeasure(); + PMU->enableBattVoltageMeasure(); + // It is necessary to disable the detection function of the TS pin on the board + // without the battery temperature detection function, otherwise it will cause abnormal charging + PMU->disableTSPinMeasure(); + + // Set the time of pressing the button to turn off + PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S); + + return true; + #elif BOARD_MODEL == BOARD_TBEAM_S_V1 + Wire1.begin(I2C_SDA, I2C_SCL); + + if (!PMU) { + PMU = new XPowersAXP2101(PMU_WIRE_PORT); + if (!PMU->init()) { + delete PMU; + PMU = NULL; + } + } + + if (!PMU) { + return false; + } + + /** + * gnss module power channel + * The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during + * initialization + */ + PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO4); + + // lora radio power channel + PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO3); + + // m.2 interface + PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300); + PMU->enablePowerOutput(XPOWERS_DCDC3); + + /** + * ALDO2 cannot be turned off. + * It is a necessary condition for sensor communication. + * It must be turned on to properly access the sensor and screen + * It is also responsible for the power supply of PCF8563 + */ + PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO2); + + // 6-axis , magnetometer ,bme280 , oled screen power channel + PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO1); + + // sdcard power channle + PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); + PMU->enablePowerOutput(XPOWERS_BLDO1); + + // PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300); + // PMU->enablePowerOutput(XPOWERS_DCDC4); + + // not use channel + PMU->disablePowerOutput(XPOWERS_DCDC2); // not elicited + PMU->disablePowerOutput(XPOWERS_DCDC5); // not elicited + PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist + PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist + PMU->disablePowerOutput(XPOWERS_VBACKUP); + + // Configure charging + PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); + PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); + // TODO: Reset + PMU->setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG); + + // Set the time of pressing the button to turn off + PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S); + PMU->setPowerKeyPressOnTime(XPOWERS_POWERON_128MS); + + // disable all axp chip interrupt + PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); + PMU->clearIrqStatus(); + + // It is necessary to disable the detection function of the TS pin on the board + // without the battery temperature detection function, otherwise it will cause abnormal charging + PMU->disableTSPinMeasure(); + PMU->enableVbusVoltageMeasure(); + PMU->enableBattVoltageMeasure(); + + + return true; + #else + return false; + #endif +} diff --git a/Python Module/Example.py b/Python Module/Example.py new file mode 100755 index 0000000..0748736 --- /dev/null +++ b/Python Module/Example.py @@ -0,0 +1,47 @@ +# This is a short example program that +# demonstrates the bare minimum of using +# RNode in a Python program. +# +# The example and the RNode.py library is +# written for Python 3, so be sure to run +# it with: python3 Example.py + +# First we'll import the RNodeInterface class. +from RNode import RNodeInterface + +# We'll also define which serial port the +# RNode is attached to. +serialPort = "/dev/ttyUSB0" + +# This function gets called every time a +# packet is received +def gotPacket(data, rnode): + message = data.decode("utf-8") + print("Received a packet: "+message) + print("RSSI: "+str(rnode.r_stat_rssi)+" dBm") + print("SNR: "+str(rnode.r_stat_snr)+" dB") + +# Create an RNode instance. This configures +# and powers up the radio. +rnode = RNodeInterface( + callback = gotPacket, + name = "My RNode", + port = serialPort, + frequency = 868000000, + bandwidth = 125000, + txpower = 2, + sf = 7, + cr = 5, + loglevel = RNodeInterface.LOG_DEBUG) + +# Enter a loop waiting for user input. +try: + print("Waiting for packets, hit enter to send a packet, Ctrl-C to exit") + while True: + input() + message = "Hello World!" + data = message.encode("utf-8") + rnode.send(data) +except KeyboardInterrupt as e: + print("") + exit() \ No newline at end of file diff --git a/Python Module/RNode.py b/Python Module/RNode.py new file mode 100755 index 0000000..e63e214 --- /dev/null +++ b/Python Module/RNode.py @@ -0,0 +1,536 @@ +# RNode interface class for Python 3 +# +# MIT License +# +# Copyright (c) 2020 Mark Qvist - unsigned.io +# +# 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 sys +import serial +import threading +import time +import math + +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_PROMISC = 0x0E + 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_FW_VERSION = 0x50 + CMD_ROM_READ = 0x51 + + 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 RNodeInterface(): + MTU = 500 + MAX_CHUNK = 32768 + FREQ_MIN = 137000000 + FREQ_MAX = 1020000000 + + LOG_CRITICAL = 0 + LOG_ERROR = 1 + LOG_WARNING = 2 + LOG_NOTICE = 3 + LOG_INFO = 4 + LOG_VERBOSE = 5 + LOG_DEBUG = 6 + LOG_EXTREME = 7 + + FREQ_MIN = 137000000 + FREQ_MAX = 1020000000 + + RSSI_OFFSET = 157 + + CALLSIGN_MAX_LEN = 32 + + def __init__(self, callback, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, loglevel = LOG_NOTICE, flow_control = False, id_interval = None, id_callsign = None): + self.serial = None + self.loglevel = loglevel + self.callback = callback + self.name = name + self.port = port + self.speed = 115200 + self.databits = 8 + self.parity = serial.PARITY_NONE + self.stopbits = 1 + self.timeout = 100 + self.online = False + + self.frequency = frequency + self.bandwidth = bandwidth + self.txpower = txpower + self.sf = sf + self.cr = cr + self.state = KISS.RADIO_STATE_OFF + self.bitrate = 0 + + self.last_id = 0 + + self.r_frequency = None + self.r_bandwidth = None + self.r_txpower = None + self.r_sf = None + self.r_cr = None + self.r_state = None + self.r_lock = None + self.r_stat_rx = None + self.r_stat_tx = None + self.r_stat_rssi = None + self.r_stat_snr = None + self.r_random = None + + self.packet_queue = [] + self.flow_control = flow_control + self.interface_ready = False + + self.validcfg = True + if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX): + self.log("Invalid frequency configured for "+str(self), RNodeInterface.LOG_ERROR) + self.validcfg = False + + if (self.txpower < 0 or self.txpower > 17): + self.log("Invalid TX power configured for "+str(self), RNodeInterface.LOG_ERROR) + self.validcfg = False + + if (self.bandwidth < 7800 or self.bandwidth > 500000): + self.log("Invalid bandwidth configured for "+str(self), RNodeInterface.LOG_ERROR) + self.validcfg = False + + if (self.sf < 7 or self.sf > 12): + self.log("Invalid spreading factor configured for "+str(self), RNodeInterface.LOG_ERROR) + self.validcfg = False + + if (self.cr < 5 or self.cr > 8): + self.log("Invalid coding rate configured for "+str(self), RNodeInterface.LOG_ERROR) + self.validcfg = False + + if id_interval != None and id_callsign != None: + if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN): + self.should_id = True + self.id_callsign = id_callsign + self.id_interval = id_interval + else: + self.log("The encoded ID callsign for "+str(self)+" exceeds the max length of "+str(RNodeInterface.CALLSIGN_MAX_LEN)+" bytes.", RNodeInterface.LOG_ERROR) + self.validcfg = False + else: + self.id_interval = None + self.id_callsign = None + + if (not self.validcfg): + raise ValueError("The configuration for "+str(self)+" contains errors, interface is offline") + + try: + self.log("Opening serial port "+self.port+"...") + self.serial = serial.Serial( + port = self.port, + baudrate = self.speed, + bytesize = self.databits, + parity = self.parity, + stopbits = self.stopbits, + xonxoff = False, + rtscts = False, + timeout = 0, + inter_byte_timeout = None, + write_timeout = None, + dsrdtr = False, + ) + except Exception as e: + self.log("Could not open serial port for interface "+str(self), RNodeInterface.LOG_ERROR) + raise e + + if self.serial.is_open: + sleep(2.0) + thread = threading.Thread(target=self.readLoop) + thread.setDaemon(True) + thread.start() + self.online = True + self.log("Serial port "+self.port+" is now open") + self.log("Configuring RNode interface...", RNodeInterface.LOG_VERBOSE) + self.initRadio() + if (self.validateRadioState()): + self.interface_ready = True + self.log(str(self)+" is configured and powered up") + sleep(1.0) + else: + self.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNodeInterface.LOG_ERROR) + self.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNodeInterface.LOG_ERROR) + self.log("Aborting RNode startup", RNodeInterface.LOG_ERROR) + self.serial.close() + raise IOError("RNode interface did not pass validation") + else: + raise IOError("Could not open serial port") + + def log(self, message, level): + pass + + 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 validateRadioState(self): + self.log("Validating radio configuration for "+str(self)+"...", RNodeInterface.LOG_VERBOSE) + sleep(0.25); + if (self.frequency != self.r_frequency): + self.log("Frequency mismatch", RNodeInterface.LOG_ERROR) + self.validcfg = False + if (self.bandwidth != self.r_bandwidth): + self.log("Bandwidth mismatch", RNodeInterface.LOG_ERROR) + self.validcfg = False + if (self.txpower != self.r_txpower): + self.log("TX power mismatch", RNodeInterface.LOG_ERROR) + self.validcfg = False + if (self.sf != self.r_sf): + self.log("Spreading factor mismatch", RNodeInterface.LOG_ERROR) + self.validcfg = False + + if (self.validcfg): + return True + else: + return False + + def setPromiscuousMode(self, state): + if state == True: + kiss_command = bytes([KISS.FEND,KISS.CMD_PROMISC, 0x01, KISS.FEND]) + else: + kiss_command = bytes([KISS.FEND,KISS.CMD_PROMISC, 0x00, KISS.FEND]) + + written = self.serial.write(kiss_command) + if written != len(kiss_command): + raise IOError("An IO error occurred while configuring promiscuous mode for "+self(str)) + + + 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) + self.log(str(self)+" On-air bitrate is now "+str(self.bitrate_kbps)+ " kbps", RNodeInterface.LOG_DEBUG) + except: + self.bitrate = 0 + + def processIncoming(self, data): + self.callback(data, self) + + def send(self, data): + self.processOutgoing(data) + + def processOutgoing(self,data): + if self.online: + if self.interface_ready: + if self.flow_control: + self.interface_ready = False + + frame = b"" + + if self.id_interval != None and self.id_callsign != None: + if self.last_id + self.id_interval < time.time(): + self.last_id = time.time() + frame = bytes([0xc0])+bytes([0x00])+KISS.escape(self.id_callsign.encode("utf-8"))+bytes([0xc0]) + + data = KISS.escape(data) + frame += bytes([0xc0])+bytes([0x00])+data+bytes([0xc0]) + written = self.serial.write(frame) + + if written != len(frame): + raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) + else: + self.queue(data) + + def queue(self, data): + self.packet_queue.append(data) + + def process_queue(self): + if len(self.packet_queue) > 0: + data = self.packet_queue.pop(0) + self.interface_ready = True + self.processOutgoing(data) + elif len(self.packet_queue) == 0: + self.interface_ready = True + + 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: + if self.serial.in_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_DATA): + in_frame = False + self.processIncoming(data_buffer) + 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) < RNodeInterface.MTU): + if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): + command = 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] + self.log(str(self)+" Radio reporting frequency is "+str(self.r_frequency/1000000.0)+" MHz", RNodeInterface.LOG_DEBUG) + 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] + self.log(str(self)+" Radio reporting bandwidth is "+str(self.r_bandwidth/1000.0)+" KHz", RNodeInterface.LOG_DEBUG) + self.updateBitrate() + + elif (command == KISS.CMD_TXPOWER): + self.r_txpower = byte + self.log(str(self)+" Radio reporting TX power is "+str(self.r_txpower)+" dBm", RNodeInterface.LOG_DEBUG) + elif (command == KISS.CMD_SF): + self.r_sf = byte + self.log(str(self)+" Radio reporting spreading factor is "+str(self.r_sf), RNodeInterface.LOG_DEBUG) + self.updateBitrate() + elif (command == KISS.CMD_CR): + self.r_cr = byte + self.log(str(self)+" Radio reporting coding rate is "+str(self.r_cr), RNodeInterface.LOG_DEBUG) + 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): + self.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")", RNodeInterface.LOG_ERROR) + elif (byte == KISS.ERROR_INITRADIO): + self.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNodeInterface.LOG_ERROR) + else: + self.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")", RNodeInterface.LOG_ERROR) + elif (command == KISS.CMD_READY): + self.process_queue() + + else: + time_since_last = int(time.time()*1000) - last_read_ms + if len(data_buffer) > 0 and time_since_last > self.timeout: + self.log(str(self)+" serial read timeout", RNodeInterface.LOG_DEBUG) + data_buffer = b"" + in_frame = False + command = KISS.CMD_UNKNOWN + escape = False + sleep(0.08) + + except Exception as e: + self.online = False + self.log("A serial port error occurred, the contained exception was: "+str(e), RNodeInterface.LOG_ERROR) + self.log("The interface "+str(self.name)+" is now offline.", RNodeInterface.LOG_ERROR) + + def log(self, msg, level=3): + logtimefmt = "%Y-%m-%d %H:%M:%S" + if self.loglevel >= level: + timestamp = time.time() + logstring = "["+time.strftime(logtimefmt)+"] ["+self.loglevelname(level)+"] "+msg + print(logstring) + + def loglevelname(self, level): + if (level == RNodeInterface.LOG_CRITICAL): + return "Critical" + if (level == RNodeInterface.LOG_ERROR): + return "Error" + if (level == RNodeInterface.LOG_WARNING): + return "Warning" + if (level == RNodeInterface.LOG_NOTICE): + return "Notice" + if (level == RNodeInterface.LOG_INFO): + return "Info" + if (level == RNodeInterface.LOG_VERBOSE): + return "Verbose" + if (level == RNodeInterface.LOG_DEBUG): + return "Debug" + if (level == RNodeInterface.LOG_EXTREME): + return "Extra" + + def hexrep(data, delimit=True): + delimiter = ":" + if not delimit: + delimiter = "" + hexrep = delimiter.join("{:02x}".format(ord(c)) for c in data) + return hexrep + + def __str__(self): + return "RNodeInterface["+self.name+"]" + diff --git a/README.md b/README.md new file mode 100644 index 0000000..d602635 --- /dev/null +++ b/README.md @@ -0,0 +1,295 @@ +# RNodeTHV4 — Reticulum Boundary Node for Heltec WiFi LoRa 32 V4 + +A custom firmware for the **Heltec WiFi LoRa 32 V4** (ESP32-S3 + SX1262) that operates as a **Boundary Node** — bridging a local LoRa radio network with a remote TCP/IP backbone (such as [rmap.world](https://rmap.world)) over WiFi. + +``` + Android / Sideband Remote + ┌──────────┐ ┌──────────────┐ WiFi Reticulum + │ Sideband │◄── BT ──►│ RNode (V4) │◄── TCP ──────────► Backbone + │ App │ │ Boundary Mode│ ▲ (rnsd / + └──────────┘ └──────┬───────┘ │ rmap.world) + │ ┌───┴───┐ + LoRa Radio │ Router │ + │ └───────┘ + ◄── RF mesh ──► + Other RNodes +``` + +Built on [microReticulum](https://github.com/attermann/microReticulum) (a C++ port of the [Reticulum](https://reticulum.network/) network stack) and the [RNode firmware](https://github.com/markqvist/RNode_Firmware) by Mark Qvist. + +## Features + +- **Bidirectional LoRa ↔ TCP bridging** — local LoRa mesh nodes can reach the global Reticulum backbone and vice versa +- **Web-based configuration portal** — WiFi SSID/password, backbone host/port, LoRa parameters, all configurable via captive portal +- **OLED status display** — real-time status indicators for LoRa, WiFi, WAN (backbone), LAN (local TCP), plus IP address, port, and airtime +- **Optional local TCP server** — serve local devices on your WiFi in addition to the backbone connection +- **Automatic reconnection** — WiFi and TCP connections recover from drops with exponential backoff +- **ESP32 memory-optimized** — table sizes, timeouts, and caching tuned for the constrained MCU environment + +## Hardware + +| Component | Spec | +|-----------|------| +| **Board** | Heltec WiFi LoRa 32 V4 | +| **MCU** | ESP32-S3, 2MB PSRAM, 16MB Flash | +| **Radio** | SX1262 + GC1109 PA (up to 28 dBm) | +| **Display** | SSD1306 OLED 128×64 | +| **WiFi** | 2.4 GHz 802.11 b/g/n | + +## Quick Start + +### Prerequisites + +- [PlatformIO](https://platformio.org/) installed (via VS Code extension or CLI) +- Heltec WiFi LoRa 32 V4 connected via USB + +### Build & Flash + +```bash +# Clone this repo +git clone https://github.com/jrl290/RNodeTHV4.git +cd RNodeTHV4 + +# Build +pio run -e heltec_V4_boundary + +# Flash +pio run -e heltec_V4_boundary -t upload + +# Monitor serial output (optional) +pio device monitor -e heltec_V4_boundary +``` + +On first boot (or if no configuration is found), the device automatically enters the **Configuration Portal**. + +## Configuration Portal + +### Entering Config Mode + +The config portal activates automatically on: +- **First boot** — when no saved configuration exists +- **Button hold >5 seconds** — hold the PRG button for 5+ seconds, the device reboots into config mode + +When active, the device creates a WiFi access point named **`RNode-Boundary-Setup`** (open network). Connect to it and browse to `http://192.168.4.1`. + +### Config Page Options + +The web form has four sections: + +#### 📶 WiFi Network +| Field | Description | +|-------|-------------| +| **WiFi** | Enable/Disable (disable for LoRa-only repeater mode) | +| **SSID** | Your WiFi network name | +| **Password** | WiFi password | + +#### 🌐 TCP Backbone +| Field | Description | +|-------|-------------| +| **Mode** | `Disabled` or `Client (connect to backbone)` | +| **Backbone Host** | IP address or hostname of backbone server (e.g. `rmap.world`) | +| **Backbone Port** | TCP port (default: `4242`) | + +#### 📡 Local TCP Server (optional) +| Field | Description | +|-------|-------------| +| **Local TCP Server** | Enable/Disable — runs a TCP server on your WiFi for local Reticulum nodes to connect | +| **TCP Port** | Port to listen on (default: `4242`) | + +#### 📻 LoRa Radio +| Field | Description | +|-------|-------------| +| **Frequency** | e.g. `867.200` MHz — must match your other RNodes | +| **Bandwidth** | 7.8 kHz – 500 kHz (typically `125 kHz`) | +| **Spreading Factor** | SF6 – SF12 (typically `SF7` for backbone, `SF10` for long range) | +| **Coding Rate** | 4/5 – 4/8 | +| **TX Power** | 2 – 22 dBm | + +After saving, the device reboots with the new configuration applied. + +## OLED Display Layout + +The 128×64 OLED is split into two panels: + +### Left Panel — Status Indicators (64×64) + +``` + ● LORA ← filled circle = radio online + ○ wifi ← unfilled circle = WiFi disconnected + ● WAN ← filled = backbone TCP connected + ○ LAN ← unfilled = no local TCP clients + ──────────────── + Air:0.3% ← current LoRa airtime + ▓▓▓▓▓ ||||||| ← battery, signal quality +``` + +- **Filled circle (●)** = active/connected +- **Unfilled circle (○)** = inactive/disconnected +- Labels are UPPERCASE when active, lowercase when inactive (except LAN which is always uppercase) + +### Right Panel — Device Info (64×64) + +``` + ▓▓ RNodeTHV4 ▓▓ ← title bar (inverted) + 867.200MHz ← LoRa frequency + SF7 125k ← spreading factor & bandwidth + ──────────────── ← separator + 192.168.1.42 ← WiFi IP address (or "No WiFi") + Port:4242 ← backbone TCP port + ──────────────── ← separator +``` + +## Interface Modes + +The firmware runs **two RNS interfaces** simultaneously, using different interface modes to control announce propagation and routing behavior: + +### LoRa Interface — `MODE_ACCESS_POINT` + +The LoRa radio operates in **Access Point mode**. In Reticulum, this means: +- The interface broadcasts its own announces but **blocks rebroadcast of remote announces** from crossing to LoRa +- This prevents backbone announces (hundreds of remote destinations) from flooding the limited-bandwidth LoRa channel +- Local nodes discover the boundary node directly; the boundary node answers path requests for remote destinations from its cache + +### TCP Backbone Interface — `MODE_BOUNDARY` + +The TCP backbone connection uses a custom **Boundary mode** (`0x20`), a new interface mode added to microReticulum for this firmware. Boundary mode means: +- Incoming announces from the backbone are received and cached, but **not stored in the path table by default** — only stored when specifically requested via a path request from a local LoRa node +- This prevents the path table (limited to 48 entries on ESP32) from being overwhelmed by thousands of backbone destinations +- When the path table needs to be culled, **Boundary-mode paths are evicted first**, preserving locally-needed LoRa paths + +### Optional Local TCP Server — `MODE_ACCESS_POINT` + +If enabled, a TCP server on the WiFi network allows local Reticulum nodes to connect. It also uses Access Point mode, with the same announce filtering as LoRa. + +## Routing & Memory Customizations + +The ESP32-S3 has limited RAM compared to a desktop Reticulum node. Several customizations were made to the microReticulum library to operate reliably within these constraints: + +### Table Size Limits + +| Table | Default (Desktop) | RNodeTHV4 | Rationale | +|-------|-------------------|-----------|-----------| +| Path table (`_destination_table`) | Unbounded | **48 entries** | Prevents unbounded growth; boundary paths evicted first | +| Hash list (`_hashlist`) | 1,000,000 | **32** | Packet dedup list; small is fine for low-throughput LoRa | +| Path request tags (`_max_pr_tags`) | 32,000 | **32** | Pending path requests rarely exceed a few dozen | +| Known destinations | 100 | **24** | Identity cache; rarely need more on a boundary node | +| Max queued announces | 16 | **4** | Outbound announce queue; LoRa is slow, no point queuing many | +| Max receipts | 1,024 | **20** | Packet receipt tracking | + +### Timeout Reductions + +| Setting | Default | RNodeTHV4 | Rationale | +|---------|---------|-----------|-----------| +| Destination timeout | 7 days | **1 day** | Free memory faster; stale paths re-resolve automatically | +| Pathfinder expiry | 7 days | **1 day** | Same as above | +| AP path time | 24 hours | **6 hours** | AP paths go stale faster in mesh environments | +| Roaming path time | 6 hours | **1 hour** | Mobile nodes change paths frequently | +| Table cull interval | 5 seconds | **60 seconds** | Less CPU overhead on culling | +| Job/Clean/Persist intervals | 5m/15m/12h | **60s/60s/60s** | More frequent housekeeping for MCU stability | + +### Selective Backbone Caching + +The most critical optimization: **backbone announces are not stored in the path table by default**. A backbone like `rmap.world` may advertise hundreds of destinations. Storing them all would evict every local LoRa path. + +Instead: +1. Backbone announces are received and their packets cached to flash storage +2. When a local LoRa node requests a path, the boundary checks its cache and responds directly +3. Only **specifically requested** paths get a path table entry +4. Path table culling prioritizes evicting backbone entries over local ones + +### Default Route Forwarding + +When a transport-addressed packet arrives from LoRa but the boundary has no path table entry for it, the firmware: +1. Strips the transport headers (converts `HEADER_2` → `HEADER_1/BROADCAST`) +2. Forwards the raw packet to the backbone interface +3. Creates reverse-table entries so proofs can route back to the sender + +This acts as a **default route** — any packet the boundary can't route locally gets forwarded to the backbone. + +### Cached Packet Unpacking Fix + +The original microReticulum `get_cached_packet()` function called `update_hash()` after deserializing cached packets from flash. However, `update_hash()` only computes the packet hash — it does **not** parse the raw bytes into fields like `destination_hash`, `data`, `flags`, etc. + +This was changed to call `unpack()` instead, which parses all packet fields AND computes the hash. Without this fix, path responses contained empty destination hashes and were silently dropped by LoRa nodes. + +## Connecting to the Backbone + +### Example: Connect to rmap.world + +In the configuration portal: +1. Set WiFi SSID and password +2. Set TCP Backbone Mode to **Client** +3. Set Backbone Host to `rmap.world` +4. Set Backbone Port to `4242` +5. Save and reboot + +### Example: Local rnsd Server + +On your server, configure `rnsd` with a TCP Server Interface in `~/.reticulum/config`: + +```ini +[interfaces] + [[TCP Server Interface]] + type = TCPServerInterface + listen_host = 0.0.0.0 + listen_port = 4242 +``` + +Then configure the boundary node as a **Client** pointing to your server's IP. + +### Example: rnsd Connects to Boundary + +On your server, configure `rnsd` with a TCP Client Interface: + +```ini +[interfaces] + [[TCP Client to Boundary]] + type = TCPClientInterface + target_host = + target_port = 4242 +``` + +Set the boundary node's **Local TCP Server** to **Enabled** (port 4242). + +## Architecture + +### Key Files + +| File | Purpose | +|------|---------| +| `RNode_Firmware.ino` | Main firmware — boundary mode initialization, interface setup, button handling | +| `BoundaryMode.h` | Boundary state struct, EEPROM load/save, configuration defaults | +| `BoundaryConfig.h` | Web-based captive portal for configuration | +| `TcpInterface.h` | TCP backbone interface (implements `RNS::InterfaceImpl`) with HDLC framing | +| `Display.h` | OLED display layout — boundary-specific status page | +| `Boards.h` | Board variant definition for `heltec32v4_boundary` | +| `platformio.ini` | Build targets: `heltec_V4_boundary` and `heltec_V4_boundary-local` | + +### Library Patches + +The firmware depends on [microReticulum](https://github.com/attermann/microReticulum) `0.2.4`, automatically fetched by PlatformIO on first build. After the first build, the library sources under `.pio/libdeps/heltec_V4_boundary/microReticulum/src/` need the patches described in "Routing & Memory Customizations" above. Key files modified: + +| File | Changes | +|------|---------| +| `Transport.cpp` | Selective caching, default route forwarding, boundary-aware culling, `get_cached_packet()` unpack fix, memory limits | +| `Transport.h` | `MODE_BOUNDARY`, `PacketEntry`, `Callbacks`, `cull_path_table()`, configurable table sizes | +| `Identity.cpp` | `_known_destinations_maxsize` = 24, `cull_known_destinations()` | +| `Type.h` | `MODE_BOUNDARY` = 0x20, reduced `MAX_QUEUED_ANNOUNCES`, `MAX_RECEIPTS`, shorter timeouts | + +### Memory Usage (typical) + +| Resource | Used | Available | +|----------|------|-----------| +| RAM | ~21.7% | 320 KB | +| Flash | ~18.1% | 16 MB | +| PSRAM | Dynamic | 2 MB | + +## License + +This project is licensed under the **GNU General Public License v3.0** — see [LICENSE](LICENSE) for details. + +Based on: +- [RNode Firmware](https://github.com/markqvist/RNode_Firmware) by Mark Qvist (GPL-3.0) +- [microReticulum](https://github.com/attermann/microReticulum) by Chris Attermann (GPL-3.0) +- [Reticulum](https://reticulum.network/) by Mark Qvist (MIT) + diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino new file mode 100755 index 0000000..786b2ae --- /dev/null +++ b/RNode_Firmware.ino @@ -0,0 +1,2569 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// CBA Reticulum includes must come before local to avoid collision with local defines +#ifdef HAS_RNS +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include "Utilities.h" + +// CBA Boundary Mode +#ifdef BOUNDARY_MODE +#include "BoundaryMode.h" +#include "TcpInterface.h" +#include "BoundaryConfig.h" +#include "esp_bt.h" +#endif + +// CBA FileSystem +#if defined(RNS_USE_FS) +#include "FileSystem.h" +#else +#include "NoopFileSystem.h" +#endif + +// CBA SD +#if HAS_SDCARD +#include +SPIClass SDSPI(HSPI); +#endif + +#if MCU_VARIANT == MCU_ESP32 + #include +#endif + +// WDT timeout +#define WDT_TIMEOUT 60 // seconds + +FIFOBuffer serialFIFO; +uint8_t serialBuffer[CONFIG_UART_BUFFER_SIZE+1]; + +FIFOBuffer16 packet_starts; +uint16_t packet_starts_buf[CONFIG_QUEUE_MAX_LENGTH+1]; + +FIFOBuffer16 packet_lengths; +uint16_t packet_lengths_buf[CONFIG_QUEUE_MAX_LENGTH+1]; + +uint8_t packet_queue[CONFIG_QUEUE_SIZE]; + +volatile uint8_t queue_height = 0; +volatile uint16_t queued_bytes = 0; +volatile uint16_t queue_cursor = 0; +volatile uint16_t current_packet_start = 0; +volatile bool serial_buffering = false; +#if HAS_BLUETOOTH || HAS_BLE == true + bool bt_init_ran = false; +#endif + +#if HAS_CONSOLE + #include "Console.h" +#endif + +#if PLATFORM == PLATFORM_ESP32 || PLATFORM == PLATFORM_NRF52 + #define MODEM_QUEUE_SIZE 8 + typedef struct { + size_t len; + int rssi; + int snr_raw; + uint8_t data[]; + } modem_packet_t; + static xQueueHandle modem_packet_queue = NULL; +#endif + +char sbuf[128]; + +#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + bool packet_ready = false; +#endif + +#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 +void update_csma_parameters(); +#endif + +#ifdef HAS_RNS +// CBA LoRa interface +class LoRaInterface : public RNS::InterfaceImpl { +public: + LoRaInterface() : RNS::InterfaceImpl("LoRaInterface") { + _IN = true; + _OUT = true; + _HW_MTU = 508; + } + LoRaInterface(const char *name) : RNS::InterfaceImpl(name) { + _IN = true; + _OUT = true; + _HW_MTU = 508; + } + virtual ~LoRaInterface() { + _name = "deleted"; + } +protected: + virtual void handle_incoming(const RNS::Bytes& data) { + TRACEF("LoRaInterface.handle_incoming: (%u bytes) data: %s", data.size(), data.toHex().c_str()); + TRACE("LoRaInterface.handle_incoming: sending packet to rns..."); + InterfaceImpl::handle_incoming(data); + } + virtual void send_outgoing(const RNS::Bytes& data) { + // CBA NOTE header will be addded later by transmit function + TRACEF("LoRaInterface.send_outgoing: (%u bytes) data: %s", data.size(), data.toHex().c_str()); + TRACE("LoRaInterface.send_outgoing: adding packet to outgoing queue..."); + for (size_t i = 0; i < data.size(); i++) { + if (queue_height < CONFIG_QUEUE_MAX_LENGTH && queued_bytes < CONFIG_QUEUE_SIZE) { + queued_bytes++; + packet_queue[queue_cursor++] = data.data()[i]; + if (queue_cursor == CONFIG_QUEUE_SIZE) queue_cursor = 0; + } + } + if (!fifo16_isfull(&packet_starts) && queued_bytes < CONFIG_QUEUE_SIZE) { + uint16_t s = current_packet_start; + int16_t e = queue_cursor-1; if (e == -1) e = CONFIG_QUEUE_SIZE-1; + uint16_t l; + + if (s != e) { + l = (s < e) ? e - s + 1 : CONFIG_QUEUE_SIZE - s + e + 1; + } else { + l = 1; + } + + if (l >= MIN_L) { + queue_height++; + + fifo16_push(&packet_starts, s); + fifo16_push(&packet_lengths, l); + + current_packet_start = queue_cursor; + } + + } + // Perform post-send housekeeping + InterfaceImpl::handle_outgoing(data); + } +}; + +// CBA logger callback +void on_log(const char* msg, RNS::LogLevel level) { +/* + Serial.print(RNS::getTimeString()); + Serial.print(" ["); + Serial.print(RNS::getLevelName(level)); + Serial.print("] "); + Serial.println(msg); + Serial.flush(); +*/ + String line = RNS::getTimeString() + String(" [") + RNS::getLevelName(level) + "] " + msg + "\n"; + Serial.print(line); + Serial.flush(); + +#ifdef HAS_SDCARD + File file = SD.open("/logfile.txt", FILE_APPEND); + if (file) { + file.write((uint8_t*)line.c_str(), line.length()); + file.close(); + } +#endif // HAS_SDCARD +} + +// CBA receive packet callback +void on_receive_packet(const RNS::Bytes& raw, const RNS::Interface& interface) { +#ifdef HAS_SDCARD + TRACE("Logging receive packet to SD"); + String line = RNS::getTimeString() + String(" recv: ") + String(raw.toHex().c_str()) + "\n"; + File file = SD.open("/tracefile.txt", FILE_APPEND); + if (file) { + file.write((uint8_t*)line.c_str(), line.length()); + file.close(); + } + RNS::Packet packet({RNS::Type::NONE}, raw); + if (packet.unpack()) { + String line = RNS::getTimeString() + String(" recv: ") + String(packet.dumpString().c_str()) + "\n"; + File file = SD.open("/tracedetails.txt", FILE_APPEND); + if (file) { + file.write((uint8_t*)line.c_str(), line.length()); + file.close(); + } + } +#endif // HAS_SDCARD +} + +// CBA transmit packet callback +void on_transmit_packet(const RNS::Bytes& raw, const RNS::Interface& interface) { +#ifdef HAS_SDCARD + TRACE("Logging transmit packet to SD"); + String line = RNS::getTimeString() + String(" send: ") + String(raw.toHex().c_str()) + "\n"; + File file = SD.open("/tracefile.txt", FILE_APPEND); + if (file) { + file.write((uint8_t*)line.c_str(), line.length()); + file.close(); + } + RNS::Packet packet({RNS::Type::NONE}, raw); + if (packet.unpack()) { + String line = RNS::getTimeString() + String(" send: ") + String(packet.dumpString().c_str()) + "\n"; + File file = SD.open("/tracedetails.txt", FILE_APPEND); + if (file) { + file.write((uint8_t*)line.c_str(), line.length()); + file.close(); + } + } +#endif // HAS_SDCARD +} + +// CBA RNS +RNS::Reticulum reticulum(RNS::Type::NONE); +RNS::Interface lora_interface(RNS::Type::NONE); +RNS::FileSystem filesystem(RNS::Type::NONE); + +#ifdef BOUNDARY_MODE +// Boundary mode: TCP backbone interface + state +BoundaryState boundary_state = {}; +RNS::Interface tcp_rns_interface(RNS::Type::NONE); +TcpInterface* tcp_interface_ptr = nullptr; +// Local TCP server (MODE_ACCESS_POINT, doesn't forward announces) +RNS::Interface local_tcp_rns_interface(RNS::Type::NONE); +TcpInterface* local_tcp_interface_ptr = nullptr; +// RTC memory flag — survives software reset but not power cycle +RTC_NOINIT_ATTR uint32_t boundary_config_request; +#define BOUNDARY_CONFIG_MAGIC 0xC0F19A7E +#endif + +#endif // HAS_RNS + +void setup() { + + // Initialise serial communication + memset(serialBuffer, 0, sizeof(serialBuffer)); + fifo_init(&serialFIFO, serialBuffer, CONFIG_UART_BUFFER_SIZE); + + Serial.begin(serial_baudrate); + + // CBA Safely wait for serial initialization + while (!Serial) { + if (millis() > 2000) { + break; + } + delay(10); + } + // CBA Test + delay(2000); + + // Configure WDT + #if MCU_VARIANT == MCU_ESP32 + esp_task_wdt_init(WDT_TIMEOUT, true); // enable panic so ESP32 restarts + esp_task_wdt_add(NULL); // add current thread to WDT watch + #elif MCU_VARIANT == MCU_NRF52 + NRF_WDT->CONFIG = 0x01; // Configure WDT to run when CPU is asleep + NRF_WDT->CRV = WDT_TIMEOUT * 32768 + 1; // set timeout + NRF_WDT->RREN = 0x01; // Enable the RR[0] reload register + NRF_WDT->TASKS_START = 1; // Start WDT + #endif + + #if MCU_VARIANT == MCU_ESP32 + boot_seq(); + EEPROM.begin(EEPROM_SIZE); + Serial.setRxBufferSize(CONFIG_UART_BUFFER_SIZE); + + #if BOARD_MODEL == BOARD_TDECK + pinMode(pin_poweron, OUTPUT); + digitalWrite(pin_poweron, HIGH); + + pinMode(SD_CS, OUTPUT); + pinMode(DISPLAY_CS, OUTPUT); + digitalWrite(SD_CS, HIGH); + digitalWrite(DISPLAY_CS, HIGH); + + pinMode(DISPLAY_BL_PIN, OUTPUT); + #endif + #endif + + #if MCU_VARIANT == MCU_NRF52 + #if BOARD_MODEL == BOARD_TECHO + delay(200); + pinMode(PIN_VEXT_EN, OUTPUT); + digitalWrite(PIN_VEXT_EN, HIGH); + pinMode(pin_btn_usr1, INPUT_PULLUP); + pinMode(pin_btn_touch, INPUT_PULLUP); + pinMode(PIN_LED_RED, OUTPUT); + pinMode(PIN_LED_GREEN, OUTPUT); + pinMode(PIN_LED_BLUE, OUTPUT); + delay(200); + #endif + + if (!eeprom_begin()) { Serial.write("EEPROM initialisation failed.\r\n"); } + #endif + + // Seed the PRNG for CSMA R-value selection + #if MCU_VARIANT == MCU_ESP32 + // On ESP32, get the seed value from the + // hardware RNG + unsigned long seed_val = (unsigned long)esp_random(); + #elif MCU_VARIANT == MCU_NRF52 + // On nRF, get the seed value from the + // hardware RNG + unsigned long seed_val = get_rng_seed(); + #else + // Otherwise, get a pseudo-random seed + // value from an unconnected analog pin + // + // CAUTION! If you are implementing the + // firmware on a platform that does not + // have a hardware RNG, you MUST take + // care to get a seed value with enough + // entropy at each device reset! + unsigned long seed_val = analogRead(0); + #endif + randomSeed(seed_val); + + #if HAS_NP + led_init(); + #endif + + #if MCU_VARIANT == MCU_NRF52 && HAS_NP == true + boot_seq(); + #endif + + #if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4 + // Some boards need to wait until the hardware UART is set up before booting + // the full firmware. In the case of the RAK4631 and Heltec T114, the line below will wait + // until a serial connection is actually established with a master. Thus, it + // is disabled on this platform. + while (!Serial); + #endif + + serial_interrupt_init(); + + // Configure input and output pins + #if HAS_INPUT + input_init(); + #endif + + #if HAS_NP == false + pinMode(pin_led_rx, OUTPUT); + pinMode(pin_led_tx, OUTPUT); + #endif + + #if HAS_TCXO == true + if (pin_tcxo_enable != -1) { + pinMode(pin_tcxo_enable, OUTPUT); + digitalWrite(pin_tcxo_enable, HIGH); + } + #endif + + // Initialise buffers + memset(pbuf, 0, sizeof(pbuf)); + memset(cmdbuf, 0, sizeof(cmdbuf)); + + memset(packet_queue, 0, sizeof(packet_queue)); + + memset(packet_starts_buf, 0, sizeof(packet_starts_buf)); + fifo16_init(&packet_starts, packet_starts_buf, CONFIG_QUEUE_MAX_LENGTH); + + memset(packet_lengths_buf, 0, sizeof(packet_starts_buf)); + fifo16_init(&packet_lengths, packet_lengths_buf, CONFIG_QUEUE_MAX_LENGTH); + + #if PLATFORM == PLATFORM_ESP32 || PLATFORM == PLATFORM_NRF52 + modem_packet_queue = xQueueCreate(MODEM_QUEUE_SIZE, sizeof(modem_packet_t*)); + #endif + + // Set chip select, reset and interrupt + // pins for the LoRa module + #if MODEM == SX1276 || MODEM == SX1278 + LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy); + #elif MODEM == SX1262 + LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen); + #elif MODEM == SX1280 + LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen, pin_txen); + #endif + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + init_channel_stats(); + + #if BOARD_MODEL == BOARD_T3S3 + #if MODEM == SX1280 + delay(300); + LoRa->reset(); + delay(100); + #endif + #endif + + #if BOARD_MODEL == BOARD_XIAO_S3 + // Improve wakeup from sleep + delay(300); + LoRa->reset(); + delay(100); + #endif + + // Check installed transceiver chip and + // probe boot parameters. + if (LoRa->preInit()) { + modem_installed = true; + + #if HAS_INPUT + // Skip quick-reset console activation + #else + uint32_t lfr = LoRa->getFrequency(); + if (lfr == 0) { + // Normal boot + } else if (lfr == M_FRQ_R) { + // Quick reboot + #if HAS_CONSOLE + if (rtc_get_reset_reason(0) == POWERON_RESET) { + console_active = true; + } + #endif + } else { + // Unknown boot + } + LoRa->setFrequency(M_FRQ_S); + #endif + + } else { + modem_installed = false; + } + #else + // Older variants only came with SX1276/78 chips, + // so assume that to be the case for now. + modem_installed = true; + #endif + + #if HAS_DISPLAY + #if HAS_EEPROM + if (EEPROM.read(eeprom_addr(ADDR_CONF_DSET)) != CONF_OK_BYTE) { + #elif MCU_VARIANT == MCU_NRF52 + if (eeprom_read(eeprom_addr(ADDR_CONF_DSET)) != CONF_OK_BYTE) { + #endif + eeprom_update(eeprom_addr(ADDR_CONF_DSET), CONF_OK_BYTE); + #if BOARD_MODEL == BOARD_TECHO + eeprom_update(eeprom_addr(ADDR_CONF_DINT), 0x03); + #else + eeprom_update(eeprom_addr(ADDR_CONF_DINT), 0xFF); + #endif + } + #if BOARD_MODEL == BOARD_TECHO + display_add_callback(work_while_waiting); + #endif + + display_unblank(); + disp_ready = display_init(); + update_display(); + #endif + + // ── Boundary Mode: check if config portal is needed ── + #ifdef BOUNDARY_MODE + { + // Load LoRa config from EEPROM so the portal can show current values + eeprom_conf_load(); + + // Enter config mode if: first boot with no config, OR button-triggered reboot + bool need_config = boundary_needs_config(); + bool config_requested = (boundary_config_request == BOUNDARY_CONFIG_MAGIC); + boundary_config_request = 0; // Clear flag immediately + + if (need_config || config_requested) { + if (config_requested) { + Serial.println("[Boundary] Config mode requested via button hold"); + } else { + Serial.println("[Boundary] No configuration found — starting config portal"); + } + config_portal_start(); + // Block here: only run the config portal until user saves and device reboots + while (config_portal_is_active()) { + config_portal_loop(); + #if MCU_VARIANT == MCU_ESP32 + esp_task_wdt_reset(); + #endif + delay(1); + } + // If we exit (shouldn't normally), reboot anyway + ESP.restart(); + } + } + #endif + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + #if HAS_PMU == true + pmu_ready = init_pmu(); + #endif + + #if HAS_BLUETOOTH || HAS_BLE == true + #ifndef BOUNDARY_MODE + bt_init(); + bt_init_ran = true; + #else + // Boundary mode: release BT controller memory (~70KB) + btStop(); + esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); + #endif + #endif + + if (console_active) { + #if HAS_CONSOLE + console_start(); + #else + kiss_indicate_reset(); + #endif + } else { + #if HAS_WIFI + wifi_mode = EEPROM.read(eeprom_addr(ADDR_CONF_WIFI)); + if (wifi_mode == WR_WIFI_STA || wifi_mode == WR_WIFI_AP) { wifi_remote_init(); } + #endif + kiss_indicate_reset(); + } + #endif + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + #if MODEM == SX1280 + avoid_interference = false; + #else + #if HAS_EEPROM + uint8_t ia_conf = EEPROM.read(eeprom_addr(ADDR_CONF_DIA)); + if (ia_conf == 0x00) { avoid_interference = true; } + else { avoid_interference = false; } + #elif MCU_VARIANT == MCU_NRF52 + uint8_t ia_conf = eeprom_read(eeprom_addr(ADDR_CONF_DIA)); + if (ia_conf == 0x00) { avoid_interference = true; } + else { avoid_interference = false; } + #endif + #endif + #endif + + // Validate board health, EEPROM and config + validate_status(); + + if (op_mode != MODE_TNC) LoRa->setFrequency(0); + + // CBA SD +#ifdef HAS_SDCARD + pinMode(SDCARD_MISO, INPUT_PULLUP); + SDSPI.begin(SDCARD_SCLK, SDCARD_MISO, SDCARD_MOSI, SDCARD_CS); + if (!SD.begin(SDCARD_CS, SDSPI)) { + Serial.println("setupSDCard FAIL"); + } else { + uint32_t cardSize = SD.cardSize() / (1024 * 1024); + Serial.print("setupSDCard PASS . SIZE = "); + Serial.print(cardSize / 1024.0); + Serial.println(" GB"); + SD.remove("/logfile"); + SD.remove("/logfile.txt"); + SD.remove("/tracefile"); + SD.remove("/tracedetails"); + SD.remove("/tracefile.txt"); + SD.remove("/tracedetails.txt"); + Serial.println("DIR: /"); + File root = SD.open("/"); + File file = root.openNextFile(); + while(file){ + Serial.print(" FILE: "); + Serial.println(file.name()); + file = root.openNextFile(); + } + } + delay(3000); +#endif + +#ifdef HAS_RNS + try { + // CBA Init filesystem +#if defined(RNS_USE_FS) + filesystem = new FileSystem(); + ((FileSystem*)filesystem.get())->init(); +#else + filesystem = new NoopFileSystem(); + ((FileSystem*)filesystem.get())->init(); +#endif + + HEAD("Registering filesystem...", RNS::LOG_TRACE); + RNS::Utilities::OS::register_filesystem(filesystem); + +#ifndef NDEBUG + //filesystem.remove_directory("/cache"); + //filesystem.remove_file("/destination_table"); + //filesystem.reformat(); + TRACE("Listing filesystem..."); +#if defined(RNS_USE_FS) + //FileSystem::listDir("/"); +#endif + TRACE("Finished listing"); + //TRACE("Dumping filesystem..."); + //FileSystem::dumpDir("/"); + //TRACE("Finished dumping"); + //reticulum.clear_caches(); + + // CBA DEBUG +/* + std::list files = filesystem.list_directory("/cache"); + for (auto& file : files) { + Serial.print(" FILE: "); + Serial.println(file.c_str()); + //RNS::Bytes content = filesystem.read_file(file.c_str()); + //DEBUG(std::string("FILE: ") + file); + //DEBUG(content.toString()); + } +*/ + TRACE("FILE: destination_table"); + RNS::Bytes content; + if (filesystem.read_file("/destination_table", content) > 0) { + TRACE(content.toString() + "\r\n"); + } +#endif // NDEBUG + + // CBA Start RNS + if (hw_ready) { + RNS::setLogCallback(&on_log); + RNS::Transport::set_receive_packet_callback(on_receive_packet); + RNS::Transport::set_transmit_packet_callback(on_transmit_packet); + + Serial.write("Starting RNS...\r\n"); + RNS::loglevel(RNS::LOG_TRACE); + //RNS::loglevel(RNS::LOG_MEM); + + HEAD("Registering LoRA Interface...", RNS::LOG_TRACE); + lora_interface = new LoRaInterface(); + lora_interface.mode(RNS::Type::Interface::MODE_ACCESS_POINT); + RNS::Transport::register_interface(lora_interface); + +#ifdef BOUNDARY_MODE + // ── Boundary Mode: Load config and optionally set up WiFi + TCP ── + HEAD("Boundary Mode: Initializing...", RNS::LOG_TRACE); + + // Reduce table sizes to conserve heap on ESP32. + // Default 100 entries for each table fragments heap to critical levels. + // 48 entries gives enough room for local paths plus some backbone paths. + // cull_path_table() is patched to evict backbone paths first, preserving + // local (LoRa / local-TCP) paths needed for inbound message delivery. + RNS::Transport::path_table_maxsize(48); + RNS::Transport::path_table_maxpersist(16); + boundary_load_config(); + + // Start WiFi if enabled + if (boundary_state.wifi_enabled) { + if (!wifi_initialized) { + if (wifi_mode != WR_WIFI_STA && wifi_mode != WR_WIFI_AP) { + wifi_mode = WR_WIFI_STA; + EEPROM.write(eeprom_addr(ADDR_CONF_WIFI), wifi_mode); + EEPROM.commit(); + } + wifi_remote_init(); + } + } else { + HEAD("Boundary Mode: WiFi DISABLED (LoRa-only repeater)", RNS::LOG_TRACE); + } + + // Register TCP backbone interface if enabled (mode 1 = client) + if (boundary_state.wifi_enabled && boundary_state.tcp_mode == 1) { + tcp_interface_ptr = new TcpInterface( + TCP_IF_MODE_CLIENT, + boundary_state.tcp_port, + boundary_state.backbone_host, + boundary_state.backbone_port + ); + tcp_rns_interface = tcp_interface_ptr; + tcp_rns_interface.mode(RNS::Type::Interface::MODE_BOUNDARY); + RNS::Transport::register_interface(tcp_rns_interface); + + { + char _bm_msg[128]; + snprintf(_bm_msg, sizeof(_bm_msg), "TCP backbone: client -> %s:%d", + boundary_state.backbone_host, boundary_state.backbone_port); + HEAD(_bm_msg, RNS::LOG_TRACE); + } + } else if (boundary_state.tcp_mode == 0) { + HEAD("Boundary Mode: TCP backbone DISABLED", RNS::LOG_TRACE); + } + + // Register local TCP server if enabled (MODE_ACCESS_POINT — no announce forwarding) + if (boundary_state.wifi_enabled && boundary_state.ap_tcp_enabled) { + local_tcp_interface_ptr = new TcpInterface( + TCP_IF_MODE_SERVER, + boundary_state.ap_tcp_port, + "", // no target host for server mode + 0 + ); + local_tcp_rns_interface = local_tcp_interface_ptr; + local_tcp_rns_interface.mode(RNS::Type::Interface::MODE_ACCESS_POINT); + RNS::Transport::register_interface(local_tcp_rns_interface); + + { + char _bm_msg[128]; + snprintf(_bm_msg, sizeof(_bm_msg), "Local TCP server: port %d (ACCESS_POINT mode)", + boundary_state.ap_tcp_port); + HEAD(_bm_msg, RNS::LOG_TRACE); + } + } +#endif + + HEAD("Creating Reticulum instance...", RNS::LOG_TRACE); + reticulum = RNS::Reticulum(); +#ifdef BOUNDARY_MODE + // In boundary mode, transport is ALWAYS enabled + reticulum.transport_enabled(true); +#else + reticulum.transport_enabled(op_mode == MODE_TNC); +#endif + reticulum.probe_destination_enabled(true); + reticulum.start(); + +#ifdef BOUNDARY_MODE + // Start TCP interfaces after Reticulum is running + if (boundary_state.wifi_enabled && (wifi_is_connected() || wifi_mode == WR_WIFI_AP)) { + if (tcp_interface_ptr) { + tcp_interface_ptr->start(); + HEAD("Boundary Mode: TCP backbone started", RNS::LOG_TRACE); + } + if (local_tcp_interface_ptr) { + local_tcp_interface_ptr->start(); + HEAD("Boundary Mode: Local TCP server started", RNS::LOG_TRACE); + } + } else if (boundary_state.wifi_enabled) { + HEAD("Boundary Mode: Waiting for WiFi before starting TCP interfaces", RNS::LOG_WARNING); + } +#endif + + // CBA load/create local destination for admin node +/* + RNS::Identity identity = {RNS::Type::NONE}; + std::string local_identity_path = RNS::Reticulum::_storagepath + "/local_identity"; + if (RNS::Utilities::OS::file_exists(local_identity_path.c_str())) { + identity = RNS::Identity::from_file(local_identity_path.c_str()); + } + if (!identity) { + RNS::verbose("No valid local identity in storage, creating..."); + identity = RNS::Identity(); + identity.to_file(local_identity_path.c_str()); + } + else { + RNS::verbose("Loaded local identity from storage"); + } + RNS::Destination destination(identity, RNS::Type::Destination::IN, RNS::Type::Destination::SINGLE, "rnstransport", "local"); +*/ + RNS::Destination destination(RNS::Transport::identity(), RNS::Type::Destination::IN, RNS::Type::Destination::SINGLE, "rnstransport", "local"); + + HEAD("RNS is READY!", RNS::LOG_TRACE); +#ifdef BOUNDARY_MODE + HEAD("*** BOUNDARY MODE ACTIVE ***", RNS::LOG_TRACE); + HEAD("RNS transport mode is ENABLED (boundary)", RNS::LOG_TRACE); + HEAD("LoRa Interface: MODE_ACCESS_POINT", RNS::LOG_TRACE); + { + char _bm_info[128]; + if (boundary_state.tcp_mode == 1) { + snprintf(_bm_info, sizeof(_bm_info), "TCP Backbone: client -> %s:%d", + boundary_state.backbone_host, boundary_state.backbone_port); + HEAD(_bm_info, RNS::LOG_TRACE); + } else { + HEAD("TCP Backbone: DISABLED", RNS::LOG_TRACE); + } + if (boundary_state.ap_tcp_enabled) { + snprintf(_bm_info, sizeof(_bm_info), "Local TCP Server: port %d (MODE_ACCESS_POINT)", + boundary_state.ap_tcp_port); + HEAD(_bm_info, RNS::LOG_TRACE); + } + if (!boundary_state.wifi_enabled) { + HEAD("WiFi: DISABLED (LoRa-only repeater)", RNS::LOG_TRACE); + } + } +#endif + if (op_mode == MODE_TNC) { + HEAD("RNS transport mode is ENABLED", RNS::LOG_TRACE); + TRACEF("Frequency: %d Hz", lora_freq); + TRACEF("Bandwidth: %d Hz", lora_bw); + TRACEF("TX Power: %d dBm", lora_txp); + TRACEF("Spreading Factor: %d", lora_sf); + TRACEF("Coding Rate: %d", lora_cr); + } + else { + HEAD("RNS transport mode is DISABLED", RNS::LOG_INFO); + HEAD("Configure TNC mode with radio configuration to enable RNS transport", RNS::LOG_INFO); + } + //RNS::loglevel(RNS::LOG_NONE); + } + else { + HEAD("RNS is inoperable because hardware is not ready!", RNS::LOG_ERROR); + HEAD("Check firmware signature and eeprom provisioning", RNS::LOG_ERROR); + // CBA Clear cached files just in case cached files are responsible for failure + //reticulum.clear_caches(); + } + } + catch (std::exception& e) { + ERROR("RNS startup failed: " + std::string(e.what())); + } +#endif // HAS_RNS +} + +void lora_receive() { + if (!implicit) { + LoRa->receive(); + } else { + LoRa->receive(implicit_l); + } +} + +inline void kiss_write_packet() { + +#ifdef HAS_RNS + TRACEF("Received %d byte packet", host_write_len); + // CBA send packet received over LoRa to RNS in addition to connected client + // CBA RESERVE + //RNS::Bytes data(); + RNS::Bytes data(512); + for (uint16_t i = 0; i < host_write_len; i++) { + #if MCU_VARIANT == MCU_NRF52 + portENTER_CRITICAL(); + uint8_t byte = pbuf[i]; + portEXIT_CRITICAL(); + #else + uint8_t byte = pbuf[i]; + #endif + data << byte; + } + lora_interface.handle_incoming(data); +#endif + + serial_write(FEND); + serial_write(CMD_DATA); + + for (uint16_t i = 0; i < host_write_len; i++) { + #if MCU_VARIANT == MCU_NRF52 + portENTER_CRITICAL(); + uint8_t byte = pbuf[i]; + portEXIT_CRITICAL(); + #else + uint8_t byte = pbuf[i]; + #endif + + if (byte == FEND) { serial_write(FESC); byte = TFEND; } + if (byte == FESC) { serial_write(FESC); byte = TFESC; } + serial_write(byte); + } + + serial_write(FEND); + host_write_len = 0; + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + packet_ready = false; + #endif + + #if MCU_VARIANT == MCU_ESP32 + #if HAS_BLE + bt_flush(); + #endif + #endif +} + +inline void getPacketData(uint16_t len) { + #if MCU_VARIANT != MCU_NRF52 + while (len-- && read_len < MTU) { + pbuf[read_len++] = LoRa->read(); + } + #else + BaseType_t int_mask = taskENTER_CRITICAL_FROM_ISR(); + while (len-- && read_len < MTU) { + pbuf[read_len++] = LoRa->read(); + } + taskEXIT_CRITICAL_FROM_ISR(int_mask); + #endif +} + +void ISR_VECT receive_callback(int packet_size) { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + BaseType_t int_mask; + #endif + + if (!promisc) { + // The standard operating mode allows large + // packets with a payload up to 500 bytes, + // by combining two raw LoRa packets. + // We read the 1-byte header and extract + // packet sequence number and split flags + uint8_t header = LoRa->read(); packet_size--; + uint8_t sequence = packetSequence(header); + bool ready = false; + + if (isSplitPacket(header) && seq == SEQ_UNSET) { + // This is the first part of a split + // packet, so we set the seq variable + // and add the data to the buffer + #if MCU_VARIANT == MCU_NRF52 + int_mask = taskENTER_CRITICAL_FROM_ISR(); read_len = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask); + #else + read_len = 0; + #endif + + seq = sequence; + + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + last_rssi = LoRa->packetRssi(); + last_snr_raw = LoRa->packetSnrRaw(); + #endif + + getPacketData(packet_size); + + } else if (isSplitPacket(header) && seq == sequence) { + // This is the second part of a split + // packet, so we add it to the buffer + // and set the ready flag. + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + last_rssi = (last_rssi+LoRa->packetRssi())/2; + last_snr_raw = (last_snr_raw+LoRa->packetSnrRaw())/2; + #endif + + getPacketData(packet_size); + seq = SEQ_UNSET; + ready = true; + + } else if (isSplitPacket(header) && seq != sequence) { + // This split packet does not carry the + // same sequence id, so we must assume + // that we are seeing the first part of + // a new split packet. + #if MCU_VARIANT == MCU_NRF52 + int_mask = taskENTER_CRITICAL_FROM_ISR(); read_len = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask); + #else + read_len = 0; + #endif + seq = sequence; + + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + last_rssi = LoRa->packetRssi(); + last_snr_raw = LoRa->packetSnrRaw(); + #endif + + getPacketData(packet_size); + + } else if (!isSplitPacket(header)) { + // This is not a split packet, so we + // just read it and set the ready + // flag to true. + + if (seq != SEQ_UNSET) { + // If we already had part of a split + // packet in the buffer, we clear it. + #if MCU_VARIANT == MCU_NRF52 + int_mask = taskENTER_CRITICAL_FROM_ISR(); read_len = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask); + #else + read_len = 0; + #endif + seq = SEQ_UNSET; + } + + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + last_rssi = LoRa->packetRssi(); + last_snr_raw = LoRa->packetSnrRaw(); + #endif + + getPacketData(packet_size); + ready = true; + } + + if (ready) { + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + // We first signal the RSSI of the + // recieved packet to the host. + kiss_indicate_stat_rssi(); + kiss_indicate_stat_snr(); + + // And then write the entire packet + host_write_len = read_len; + kiss_write_packet(); read_len = 0; + + #else + // Allocate packet struct, but abort if there + // is not enough memory available. + modem_packet_t *modem_packet = (modem_packet_t*)malloc(sizeof(modem_packet_t) + read_len); + if(!modem_packet) { memory_low = true; return; } + + // Get packet RSSI and SNR + #if MCU_VARIANT == MCU_ESP32 + modem_packet->snr_raw = LoRa->packetSnrRaw(); + modem_packet->rssi = LoRa->packetRssi(modem_packet->snr_raw); + #endif + + // Send packet to event queue, but free the + // allocated memory again if the queue is + // unable to receive the packet. + modem_packet->len = read_len; + memcpy(modem_packet->data, pbuf, read_len); read_len = 0; + if (!modem_packet_queue || xQueueSendFromISR(modem_packet_queue, &modem_packet, NULL) != pdPASS) { + free(modem_packet); + } + #endif + } + } else { + // In promiscuous mode, raw packets are + // output directly to the host + read_len = 0; + + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + last_rssi = LoRa->packetRssi(); + last_snr_raw = LoRa->packetSnrRaw(); + getPacketData(packet_size); + + // We first signal the RSSI of the + // recieved packet to the host. + kiss_indicate_stat_rssi(); + kiss_indicate_stat_snr(); + + // And then write the entire packet + kiss_write_packet(); + + #else + getPacketData(packet_size); + packet_ready = true; + #endif + } +} + +bool startRadio() { + update_radio_lock(); + if (!radio_online && !console_active) { + if (!radio_locked && hw_ready) { + if (!LoRa->begin(lora_freq)) { + // The radio could not be started. + // Indicate this failure over both the + // serial port and with the onboard LEDs + radio_error = true; + kiss_indicate_error(ERROR_INITRADIO); + led_indicate_error(0); + return false; + } else { + radio_online = true; + + init_channel_stats(); + + setTXPower(); + setBandwidth(); + setSpreadingFactor(); + setCodingRate(); + getFrequency(); + + LoRa->enableCrc(); + LoRa->onReceive(receive_callback); + lora_receive(); + + // Flash an info pattern to indicate + // that the radio is now on + kiss_indicate_radiostate(); + led_indicate_info(3); + return true; + } + + } else { + // Flash a warning pattern to indicate + // that the radio was locked, and thus + // not started + radio_online = false; + kiss_indicate_radiostate(); + led_indicate_warning(3); + return false; + } + } else { + // If radio is already on, we silently + // ignore the request. + kiss_indicate_radiostate(); + return true; + } +} + +void stopRadio() { + LoRa->end(); + radio_online = false; +} + +void update_radio_lock() { + if (lora_freq != 0 && lora_bw != 0 && lora_txp != 0xFF && lora_sf != 0) { + radio_locked = false; + } else { + radio_locked = true; + } +} + +bool queue_full() { return (queue_height >= CONFIG_QUEUE_MAX_LENGTH || queued_bytes >= CONFIG_QUEUE_SIZE); } + +volatile bool queue_flushing = false; +void flush_queue(void) { + if (!queue_flushing) { + queue_flushing = true; + led_tx_on(); + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + while (!fifo16_isempty(&packet_starts)) { + #else + while (!fifo16_isempty_locked(&packet_starts)) { + #endif + + uint16_t start = fifo16_pop(&packet_starts); + uint16_t length = fifo16_pop(&packet_lengths); + + if (length >= MIN_L && length <= MTU) { + for (uint16_t i = 0; i < length; i++) { + uint16_t pos = (start+i)%CONFIG_QUEUE_SIZE; + tbuf[i] = packet_queue[pos]; + } + + transmit(length); + } + } + + lora_receive(); led_tx_off(); + } + + queue_height = 0; + queued_bytes = 0; + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + update_airtime(); + #endif + + queue_flushing = false; + + #if HAS_DISPLAY + display_tx = true; + #endif +} + +void pop_queue() { + if (!queue_flushing) { + queue_flushing = true; led_tx_on(); + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + if (!fifo16_isempty(&packet_starts)) { + #else + if (!fifo16_isempty_locked(&packet_starts)) { + #endif + + uint16_t start = fifo16_pop(&packet_starts); + uint16_t length = fifo16_pop(&packet_lengths); + if (length >= MIN_L && length <= MTU) { + for (uint16_t i = 0; i < length; i++) { + uint16_t pos = (start+i)%CONFIG_QUEUE_SIZE; + tbuf[i] = packet_queue[pos]; + } + + transmit(length); + } + queue_height -= 1; + queued_bytes -= length; + } + + lora_receive(); led_tx_off(); + } + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + update_airtime(); + #endif + + queue_flushing = false; + + #if HAS_DISPLAY + display_tx = true; + #endif +} + +void add_airtime(uint16_t written) { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + float lora_symbols = 0; + float packet_cost_ms = 0.0; + int ldr_opt = 0; if (lora_low_datarate) ldr_opt = 1; + + #if MODEM == SX1276 || MODEM == SX1278 + lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + 8 + PHY_HEADER_LORA_SYMBOLS); + lora_symbols /= 4*(lora_sf-2*ldr_opt); + lora_symbols *= lora_cr; + lora_symbols += lora_preamble_symbols + 0.25 + 8; + packet_cost_ms += lora_symbols * lora_symbol_time_ms; + + #elif MODEM == SX1262 || MODEM == SX1280 + if (lora_sf < 7) { + lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + PHY_HEADER_LORA_SYMBOLS); + lora_symbols /= 4*lora_sf; + lora_symbols *= lora_cr; + lora_symbols += lora_preamble_symbols + 2.25 + 8; + packet_cost_ms += lora_symbols * lora_symbol_time_ms; + + } else { + lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + 8 + PHY_HEADER_LORA_SYMBOLS); + lora_symbols /= 4*(lora_sf-2*ldr_opt); + lora_symbols *= lora_cr; + lora_symbols += lora_preamble_symbols + 0.25 + 8; + packet_cost_ms += lora_symbols * lora_symbol_time_ms; + } + + #endif + + uint16_t cb = current_airtime_bin(); + uint16_t nb = cb+1; if (nb == AIRTIME_BINS) { nb = 0; } + airtime_bins[cb] += packet_cost_ms; + airtime_bins[nb] = 0; + + #endif +} + +void update_airtime() { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + uint16_t cb = current_airtime_bin(); + uint16_t pb = cb-1; if (cb-1 < 0) { pb = AIRTIME_BINS-1; } + uint16_t nb = cb+1; if (nb == AIRTIME_BINS) { nb = 0; } + airtime_bins[nb] = 0; airtime = (float)(airtime_bins[cb]+airtime_bins[pb])/(2.0*AIRTIME_BINLEN_MS); + + uint32_t longterm_airtime_sum = 0; + for (uint16_t bin = 0; bin < AIRTIME_BINS; bin++) { longterm_airtime_sum += airtime_bins[bin]; } + longterm_airtime = (float)longterm_airtime_sum/(float)AIRTIME_LONGTERM_MS; + + float longterm_channel_util_sum = 0.0; + for (uint16_t bin = 0; bin < AIRTIME_BINS; bin++) { longterm_channel_util_sum += longterm_bins[bin]; } + longterm_channel_util = (float)longterm_channel_util_sum/(float)AIRTIME_BINS; + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + update_csma_parameters(); + #endif + + kiss_indicate_channel_stats(); + #endif +} + +void transmit(uint16_t size) { + if (radio_online) { + if (!promisc) { + uint16_t written = 0; + uint8_t header = random(256) & 0xF0; + if (size > SINGLE_MTU - HEADER_L) { header = header | FLAG_SPLIT; } + + LoRa->beginPacket(); + LoRa->write(header); written++; + + for (uint16_t i=0; i < size; i++) { + LoRa->write(tbuf[i]); written++; + + if (written == 255 && isSplitPacket(header)) { + if (!LoRa->endPacket()) { + kiss_indicate_error(ERROR_MODEM_TIMEOUT); + kiss_indicate_error(ERROR_TXFAILED); + led_indicate_error(5); + hard_reset(); + } + + add_airtime(written); + LoRa->beginPacket(); + LoRa->write(header); + written = 1; + } + } + + if (!LoRa->endPacket()) { + kiss_indicate_error(ERROR_MODEM_TIMEOUT); + kiss_indicate_error(ERROR_TXFAILED); + led_indicate_error(5); + hard_reset(); + } + + add_airtime(written); + + } else { + led_tx_on(); uint16_t written = 0; + if (size > SINGLE_MTU) { size = SINGLE_MTU; } + if (!implicit) { LoRa->beginPacket(); } + else { LoRa->beginPacket(size); } + for (uint16_t i=0; i < size; i++) { LoRa->write(tbuf[i]); written++; } + LoRa->endPacket(); add_airtime(written); + } + + } else { kiss_indicate_error(ERROR_TXFAILED); led_indicate_error(5); } +} + +void serial_callback(uint8_t sbyte) { + if (IN_FRAME && sbyte == FEND && command == CMD_DATA) { + IN_FRAME = false; + + if (!fifo16_isfull(&packet_starts) && queued_bytes < CONFIG_QUEUE_SIZE) { + uint16_t s = current_packet_start; + int16_t e = queue_cursor-1; if (e == -1) e = CONFIG_QUEUE_SIZE-1; + uint16_t l; + + if (s != e) { l = (s < e) ? e - s + 1 : CONFIG_QUEUE_SIZE - s + e + 1; } + else { l = 1; } + + if (l >= MIN_L) { + queue_height++; + fifo16_push(&packet_starts, s); + fifo16_push(&packet_lengths, l); + current_packet_start = queue_cursor; + } + } + + } else if (sbyte == FEND) { + IN_FRAME = true; + command = CMD_UNKNOWN; + frame_len = 0; + } else if (IN_FRAME && frame_len < MTU) { + // Have a look at the command byte first + if (frame_len == 0 && command == CMD_UNKNOWN) { + command = sbyte; + } else if (command == CMD_DATA) { + if (bt_state != BT_STATE_CONNECTED) { + cable_state = CABLE_STATE_CONNECTED; + } + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (queue_height < CONFIG_QUEUE_MAX_LENGTH && queued_bytes < CONFIG_QUEUE_SIZE) { + queued_bytes++; + packet_queue[queue_cursor++] = sbyte; + if (queue_cursor == CONFIG_QUEUE_SIZE) queue_cursor = 0; + } + } + } else if (command == CMD_FREQUENCY) { + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; + } + + if (frame_len == 4) { + uint32_t freq = (uint32_t)cmdbuf[0] << 24 | (uint32_t)cmdbuf[1] << 16 | (uint32_t)cmdbuf[2] << 8 | (uint32_t)cmdbuf[3]; + + if (freq == 0) { + kiss_indicate_frequency(); + } else { + lora_freq = freq; + if (op_mode == MODE_HOST) setFrequency(); + kiss_indicate_frequency(); + } + } + } else if (command == CMD_BANDWIDTH) { + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; + } + + if (frame_len == 4) { + uint32_t bw = (uint32_t)cmdbuf[0] << 24 | (uint32_t)cmdbuf[1] << 16 | (uint32_t)cmdbuf[2] << 8 | (uint32_t)cmdbuf[3]; + + if (bw == 0) { + kiss_indicate_bandwidth(); + } else { + lora_bw = bw; + if (op_mode == MODE_HOST) setBandwidth(); + kiss_indicate_bandwidth(); + } + } + } else if (command == CMD_TXPOWER) { + if (sbyte == 0xFF) { + kiss_indicate_txpower(); + } else { + int txp = sbyte; + #if MODEM == SX1262 + #if HAS_LORA_PA + if (txp > PA_MAX_OUTPUT) txp = PA_MAX_OUTPUT; + #else + if (txp > 22) txp = 22; + #endif + #elif MODEM == SX1280 + #if HAS_PA + if (txp > 20) txp = 20; + #else + if (txp > 13) txp = 13; + #endif + #else + if (txp > 17) txp = 17; + #endif + + lora_txp = txp; + if (op_mode == MODE_HOST) setTXPower(); + kiss_indicate_txpower(); + } + } else if (command == CMD_SF) { + if (sbyte == 0xFF) { + kiss_indicate_spreadingfactor(); + } else { + int sf = sbyte; + if (sf < 5) sf = 5; + if (sf > 12) sf = 12; + + lora_sf = sf; + if (op_mode == MODE_HOST) setSpreadingFactor(); + kiss_indicate_spreadingfactor(); + } + } else if (command == CMD_CR) { + if (sbyte == 0xFF) { + kiss_indicate_codingrate(); + } else { + int cr = sbyte; + if (cr < 5) cr = 5; + if (cr > 8) cr = 8; + + lora_cr = cr; + if (op_mode == MODE_HOST) setCodingRate(); + kiss_indicate_codingrate(); + } + } else if (command == CMD_IMPLICIT) { + set_implicit_length(sbyte); + kiss_indicate_implicit_length(); + } else if (command == CMD_LEAVE) { + if (sbyte == 0xFF) { + display_unblank(); + cable_state = CABLE_STATE_DISCONNECTED; + current_rssi = -292; + last_rssi = -292; + last_rssi_raw = 0x00; + last_snr_raw = 0x80; + } + } else if (command == CMD_RADIO_STATE) { + if (bt_state != BT_STATE_CONNECTED) { + cable_state = CABLE_STATE_CONNECTED; + display_unblank(); + } + if (sbyte == 0xFF) { + kiss_indicate_radiostate(); + } else if (sbyte == 0x00) { + stopRadio(); + kiss_indicate_radiostate(); + } else if (sbyte == 0x01) { + startRadio(); + kiss_indicate_radiostate(); + } + } else if (command == CMD_ST_ALOCK) { + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; + } + + if (frame_len == 2) { + uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1]; + + if (at == 0) { + st_airtime_limit = 0.0; + } else { + st_airtime_limit = (float)at/(100.0*100.0); + if (st_airtime_limit >= 1.0) { st_airtime_limit = 0.0; } + } + kiss_indicate_st_alock(); + } + } else if (command == CMD_LT_ALOCK) { + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; + } + + if (frame_len == 2) { + uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1]; + + if (at == 0) { + lt_airtime_limit = 0.0; + } else { + lt_airtime_limit = (float)at/(100.0*100.0); + if (lt_airtime_limit >= 1.0) { lt_airtime_limit = 0.0; } + } + kiss_indicate_lt_alock(); + } + } else if (command == CMD_STAT_RX) { + kiss_indicate_stat_rx(); + } else if (command == CMD_STAT_TX) { + kiss_indicate_stat_tx(); + } else if (command == CMD_STAT_RSSI) { + kiss_indicate_stat_rssi(); + } else if (command == CMD_RADIO_LOCK) { + update_radio_lock(); + kiss_indicate_radio_lock(); + } else if (command == CMD_BLINK) { + led_indicate_info(sbyte); + } else if (command == CMD_RANDOM) { + kiss_indicate_random(getRandom()); + } else if (command == CMD_DETECT) { + if (sbyte == DETECT_REQ) { + if (bt_state != BT_STATE_CONNECTED) cable_state = CABLE_STATE_CONNECTED; + kiss_indicate_detect(); + } + } else if (command == CMD_PROMISC) { + if (sbyte == 0x01) { + promisc_enable(); + } else if (sbyte == 0x00) { + promisc_disable(); + } + kiss_indicate_promisc(); + } else if (command == CMD_READY) { + if (!queue_full()) { + kiss_indicate_ready(); + } else { + kiss_indicate_not_ready(); + } + } else if (command == CMD_UNLOCK_ROM) { + if (sbyte == ROM_UNLOCK_BYTE) { + unlock_rom(); + } + } else if (command == CMD_RESET) { + if (sbyte == CMD_RESET_BYTE) { + hard_reset(); + } + } else if (command == CMD_ROM_READ) { + kiss_dump_eeprom(); + } else if (command == CMD_CFG_READ) { + kiss_dump_config(); + } else if (command == CMD_ROM_WRITE) { + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; + } + + if (frame_len == 2) { + eeprom_write(cmdbuf[0], cmdbuf[1]); + } + } else if (command == CMD_FW_VERSION) { + kiss_indicate_version(); + } else if (command == CMD_PLATFORM) { + kiss_indicate_platform(); + } else if (command == CMD_MCU) { + kiss_indicate_mcu(); + } else if (command == CMD_BOARD) { + kiss_indicate_board(); + } else if (command == CMD_CONF_SAVE) { + eeprom_conf_save(); + } else if (command == CMD_CONF_DELETE) { + eeprom_conf_delete(); + } else if (command == CMD_FB_EXT) { + #if HAS_DISPLAY == true + if (sbyte == 0xFF) { + kiss_indicate_fbstate(); + } else if (sbyte == 0x00) { + ext_fb_disable(); + kiss_indicate_fbstate(); + } else if (sbyte == 0x01) { + ext_fb_enable(); + kiss_indicate_fbstate(); + } + #endif + } else if (command == CMD_FB_WRITE) { + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; + } + #if HAS_DISPLAY + if (frame_len == 9) { + uint8_t line = cmdbuf[0]; + if (line > 63) line = 63; + int fb_o = line*8; + memcpy(fb+fb_o, cmdbuf+1, 8); + } + #endif + } else if (command == CMD_FB_READ) { + if (sbyte != 0x00) { kiss_indicate_fb(); } + } else if (command == CMD_DISP_READ) { + if (sbyte != 0x00) { kiss_indicate_disp(); } + } else if (command == CMD_DEV_HASH) { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + if (sbyte != 0x00) { + kiss_indicate_device_hash(); + } + #endif + } else if (command == CMD_DEV_SIG) { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; + } + + if (frame_len == DEV_SIG_LEN) { + memcpy(dev_sig, cmdbuf, DEV_SIG_LEN); + device_save_signature(); + } + #endif + } else if (command == CMD_FW_UPD) { + if (sbyte == 0x01) { + firmware_update_mode = true; + } else { + firmware_update_mode = false; + } + } else if (command == CMD_HASHES) { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + if (sbyte == 0x01) { + kiss_indicate_target_fw_hash(); + } else if (sbyte == 0x02) { + kiss_indicate_fw_hash(); + } else if (sbyte == 0x03) { + kiss_indicate_bootloader_hash(); + } else if (sbyte == 0x04) { + kiss_indicate_partition_table_hash(); + } + #endif + } else if (command == CMD_FW_HASH) { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; + } + + if (frame_len == DEV_HASH_LEN) { + memcpy(dev_firmware_hash_target, cmdbuf, DEV_HASH_LEN); + device_save_firmware_hash(); + } + #endif + } else if (command == CMD_WIFI_CHN) { + #if HAS_WIFI + if (sbyte > 0 && sbyte < 14) { eeprom_update(eeprom_addr(ADDR_CONF_WCHN), sbyte); } + #endif + } else if (command == CMD_WIFI_MODE) { + #if HAS_WIFI + if (sbyte == WR_WIFI_OFF || sbyte == WR_WIFI_STA || sbyte == WR_WIFI_AP) { + wr_conf_save(sbyte); + wifi_mode = sbyte; + wifi_remote_init(); + } + #endif + } else if (command == CMD_WIFI_SSID) { + #if HAS_WIFI + if (sbyte == FESC) { ESCAPE = true; } + else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte; + } + + if (sbyte == 0x00) { + for (uint8_t i = 0; i<33; i++) { + if (i 0x00) recondition_display = true; + } + #endif + } else if (command == CMD_NP_INT) { + #if HAS_NP + if (sbyte == FESC) { + ESCAPE = true; + } else { + if (ESCAPE) { + if (sbyte == TFEND) sbyte = FEND; + if (sbyte == TFESC) sbyte = FESC; + ESCAPE = false; + } + sbyte; + led_set_intensity(sbyte); + np_int_conf_save(sbyte); + } + + #endif + } + } +} + +#if MCU_VARIANT == MCU_ESP32 + portMUX_TYPE update_lock = portMUX_INITIALIZER_UNLOCKED; +#endif + +bool medium_free() { + update_modem_status(); + if (avoid_interference && interference_detected) { return false; } + return !dcd; +} + +bool noise_floor_sampled = false; +int noise_floor_sample = 0; +int noise_floor_buffer[NOISE_FLOOR_SAMPLES] = {0}; +void update_noise_floor() { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + if (!dcd) { + #if BOARD_MODEL != BOARD_HELTEC32_V4 + if (!noise_floor_sampled || current_rssi < noise_floor + CSMA_INFR_THRESHOLD_DB) { + #else + if ((!noise_floor_sampled || current_rssi < noise_floor + CSMA_INFR_THRESHOLD_DB) || (noise_floor_sampled && (noise_floor < LNA_GD_THRSHLD && current_rssi <= LNA_GD_LIMIT))) { + #endif + #if HAS_LORA_LNA + // Discard invalid samples due to gain variance + // during LoRa LNA re-calibration + if (current_rssi < noise_floor-LORA_LNA_GVT) { return; } + #endif + bool sum_noise_floor = false; + noise_floor_buffer[noise_floor_sample] = current_rssi; + noise_floor_sample = noise_floor_sample+1; + if (noise_floor_sample >= NOISE_FLOOR_SAMPLES) { + noise_floor_sample %= NOISE_FLOOR_SAMPLES; + noise_floor_sampled = true; + sum_noise_floor = true; + } + + if (noise_floor_sampled && sum_noise_floor) { + noise_floor = 0; + for (int ni = 0; ni < NOISE_FLOOR_SAMPLES; ni++) { noise_floor += noise_floor_buffer[ni]; } + noise_floor /= NOISE_FLOOR_SAMPLES; + } + } + } + #endif +} + +#define LED_ID_TRIG 16 +uint8_t led_id_filter = 0; +uint32_t interference_start = 0; +bool interference_persists = false; +void update_modem_status() { + #if MCU_VARIANT == MCU_ESP32 + portENTER_CRITICAL(&update_lock); + #elif MCU_VARIANT == MCU_NRF52 + portENTER_CRITICAL(); + #endif + + bool carrier_detected = LoRa->dcd(); + current_rssi = LoRa->currentRssi(); + last_status_update = millis(); + + #if MCU_VARIANT == MCU_ESP32 + portEXIT_CRITICAL(&update_lock); + #elif MCU_VARIANT == MCU_NRF52 + portEXIT_CRITICAL(); + #endif + + #if BOARD_MODEL == BOARD_HELTEC32_V4 + if (noise_floor > LNA_GD_THRSHLD) { interference_detected = !carrier_detected && (current_rssi > (noise_floor+CSMA_INFR_THRESHOLD_DB)); } + else { interference_detected = !carrier_detected && (current_rssi > LNA_GD_LIMIT); } + #else + interference_detected = !carrier_detected && (current_rssi > (noise_floor+CSMA_INFR_THRESHOLD_DB)); + #endif + + if (interference_detected) { if (led_id_filter < LED_ID_TRIG) { led_id_filter += 1; } } + else { if (led_id_filter > 0) {led_id_filter -= 1; } } + + // Handle potential false interference detection due to + // LNA recalibration, antenna swap, moving into new RF + // environment or similar. + if (interference_detected && current_rssi < CSMA_RFENV_RECAL_LIMIT_DB) { + if (!interference_persists) { interference_persists = true; interference_start = millis(); } + else { + if (millis()-interference_start >= CSMA_RFENV_RECAL_MS) { noise_floor_sampled = false; interference_persists = false; } + } + } else { interference_persists = false; } + + if (carrier_detected) { dcd = true; } else { dcd = false; } + + dcd_led = dcd; + if (dcd_led) { led_rx_on(); } + else { + if (interference_detected) { + if (led_id_filter >= LED_ID_TRIG && noise_floor_sampled) { led_id_on(); } + } else { + if (airtime_lock) { led_indicate_airtime_lock(); } + else { led_rx_off(); led_id_off(); } + } + } +} + +void check_modem_status() { + if (millis()-last_status_update >= status_interval_ms) { + update_modem_status(); + update_noise_floor(); + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + util_samples[dcd_sample] = dcd; + dcd_sample = (dcd_sample+1)%DCD_SAMPLES; + if (dcd_sample % UTIL_UPDATE_INTERVAL == 0) { + int util_count = 0; + for (int ui = 0; ui < DCD_SAMPLES; ui++) { + if (util_samples[ui]) util_count++; + } + local_channel_util = (float)util_count / (float)DCD_SAMPLES; + total_channel_util = local_channel_util + airtime; + if (total_channel_util > 1.0) total_channel_util = 1.0; + + int16_t cb = current_airtime_bin(); + uint16_t nb = cb+1; if (nb == AIRTIME_BINS) { nb = 0; } + if (total_channel_util > longterm_bins[cb]) longterm_bins[cb] = total_channel_util; + longterm_bins[nb] = 0.0; + + update_airtime(); + } + #endif + } +} + +void validate_status() { + #if MCU_VARIANT == MCU_1284P + uint8_t boot_flags = OPTIBOOT_MCUSR; + uint8_t F_POR = PORF; + uint8_t F_BOR = BORF; + uint8_t F_WDR = WDRF; + #elif MCU_VARIANT == MCU_2560 + uint8_t boot_flags = OPTIBOOT_MCUSR; + if (boot_flags == 0x00) boot_flags = 0x03; + uint8_t F_POR = PORF; + uint8_t F_BOR = BORF; + uint8_t F_WDR = WDRF; + #elif MCU_VARIANT == MCU_ESP32 + // TODO: Get ESP32 boot flags + uint8_t boot_flags = 0x02; + uint8_t F_POR = 0x00; + uint8_t F_BOR = 0x00; + uint8_t F_WDR = 0x01; + #elif MCU_VARIANT == MCU_NRF52 + // TODO: Get NRF52 boot flags + uint8_t boot_flags = 0x02; + uint8_t F_POR = 0x00; + uint8_t F_BOR = 0x00; + uint8_t F_WDR = 0x01; + #endif + + if (hw_ready || device_init_done) { + hw_ready = false; + Serial.write("Error, invalid hardware check state\r\n"); + #if HAS_DISPLAY + if (disp_ready) { + device_init_done = true; + update_display(); + } + #endif + led_indicate_boot_error(); + } + + if (boot_flags & (1< CSMA_CW_BANDS) { new_cw_band = CSMA_CW_BANDS; } + if (new_cw_band != cw_band) { + cw_band = (uint8_t)(new_cw_band); + cw_min = (cw_band-1) * CSMA_CW_PER_BAND_WINDOWS; + cw_max = (cw_band) * CSMA_CW_PER_BAND_WINDOWS - 1; + kiss_indicate_csma_stats(); + } + } +#endif + +void tx_queue_handler() { + if (!airtime_lock && queue_height > 0) { + if (csma_cw == -1) { + csma_cw = random(cw_min, cw_max); + cw_wait_target = csma_cw * csma_slot_ms; + } + + if (difs_wait_start == -1) { // DIFS wait not yet started + if (medium_free()) { difs_wait_start = millis(); return; } // Set DIFS wait start time + else { return; } } // Medium not yet free, continue waiting + + else { // We are waiting for DIFS or CW to pass + if (!medium_free()) { difs_wait_start = -1; cw_wait_start = -1; return; } // Medium became occupied while in DIFS wait, restart waiting when free again + else { // Medium is free, so continue waiting + if (millis() < difs_wait_start+difs_ms) { return; } // DIFS has not yet passed, continue waiting + else { // DIFS has passed, and we are now in CW wait + if (cw_wait_start == -1) { cw_wait_start = millis(); return; } // If we haven't started counting CW wait time, do it from now + else { // If we are already counting CW wait time, add it to the counter + cw_wait_passed += millis()-cw_wait_start; cw_wait_start = millis(); + if (cw_wait_passed < cw_wait_target) { return; } // Contention window wait time has not yet passed, continue waiting + else { // Wait time has passed, flush the queue + bool should_flush = !lora_limit_rate && !lora_guard_rate; + if (should_flush) { flush_queue(); } else { pop_queue(); } + cw_wait_passed = 0; csma_cw = -1; difs_wait_start = -1; } + } + } + } + } + } +} + +void work_while_waiting() { loop(); } + +void loop() { + +#ifdef HAS_RNS + // CBA + if (reticulum) { + reticulum.loop(); + } + +#ifdef BOUNDARY_MODE + // Boundary Mode: poll TCP interfaces for incoming data + if (boundary_state.wifi_enabled) { + // Start TCP interfaces if WiFi just connected and not yet started + if (wifi_is_connected()) { + if (tcp_interface_ptr && !tcp_interface_ptr->isStarted()) { + tcp_interface_ptr->start(); + Serial.println("[Boundary] WiFi connected, TCP backbone started"); + } + if (local_tcp_interface_ptr && !local_tcp_interface_ptr->isStarted()) { + local_tcp_interface_ptr->start(); + Serial.println("[Boundary] WiFi connected, local TCP server started"); + } + } + if (tcp_interface_ptr) { + tcp_interface_ptr->loop(); + } + if (local_tcp_interface_ptr) { + local_tcp_interface_ptr->loop(); + } + boundary_state.tcp_connected = (tcp_interface_ptr && tcp_interface_ptr->isConnected()); + boundary_state.ap_tcp_connected = (local_tcp_interface_ptr && local_tcp_interface_ptr->isConnected()); + boundary_state.wifi_connected = wifi_is_connected(); + } +#endif + +#endif + + if (radio_online) { + // Poll for deferred DIO0 interrupt (SPI work moved out of ISR) + LoRa->pollDio0(); + + #if MCU_VARIANT == MCU_ESP32 + modem_packet_t *modem_packet = NULL; + if(modem_packet_queue && xQueueReceive(modem_packet_queue, &modem_packet, 0) == pdTRUE && modem_packet) { + host_write_len = modem_packet->len; + last_rssi = modem_packet->rssi; + last_snr_raw = modem_packet->snr_raw; + memcpy(&pbuf, modem_packet->data, modem_packet->len); + free(modem_packet); + modem_packet = NULL; + + kiss_indicate_stat_rssi(); + kiss_indicate_stat_snr(); + kiss_write_packet(); + } + + airtime_lock = false; + if (st_airtime_limit != 0.0 && airtime >= st_airtime_limit) airtime_lock = true; + if (lt_airtime_limit != 0.0 && longterm_airtime >= lt_airtime_limit) airtime_lock = true; + + #elif MCU_VARIANT == MCU_NRF52 + modem_packet_t *modem_packet = NULL; + if(modem_packet_queue && xQueueReceive(modem_packet_queue, &modem_packet, 0) == pdTRUE && modem_packet) { + memcpy(&pbuf, modem_packet->data, modem_packet->len); + host_write_len = modem_packet->len; + free(modem_packet); + modem_packet = NULL; + + portENTER_CRITICAL(); + last_rssi = LoRa->packetRssi(); + last_snr_raw = LoRa->packetSnrRaw(); + portEXIT_CRITICAL(); + kiss_indicate_stat_rssi(); + kiss_indicate_stat_snr(); + kiss_write_packet(); + } + + airtime_lock = false; + if (st_airtime_limit != 0.0 && airtime >= st_airtime_limit) airtime_lock = true; + if (lt_airtime_limit != 0.0 && longterm_airtime >= lt_airtime_limit) airtime_lock = true; + + #endif + + tx_queue_handler(); + check_modem_status(); + + } else { + if (hw_ready) { + if (console_active) { + #if HAS_CONSOLE + console_loop(); + #endif + } else { + led_indicate_standby(); + } + } else { + + led_indicate_not_ready(); + stopRadio(); + } + } + + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + buffer_serial(); + if (!fifo_isempty(&serialFIFO)) serial_poll(); + #else + if (!fifo_isempty_locked(&serialFIFO)) serial_poll(); + #endif + + #if HAS_DISPLAY + if (disp_ready && !display_updating) update_display(); + #endif + + #if HAS_PMU + if (pmu_ready) update_pmu(); + #endif + + #if HAS_BLUETOOTH || HAS_BLE == true + if (!console_active && bt_ready) update_bt(); + #endif + + #if HAS_WIFI + if (wifi_initialized) update_wifi(); + #endif + + #if HAS_INPUT + input_read(); + #endif + + // Feed WDT +#if MCU_VARIANT == MCU_ESP32 + esp_task_wdt_reset(); +#elif MCU_VARIANT == MCU_NRF52 + NRF_WDT->RR[0] = WDT_RR_RR_Reload; +#endif + + if (memory_low) { + #if PLATFORM == PLATFORM_ESP32 + if (esp_get_free_heap_size() < 8192) { + kiss_indicate_error(ERROR_MEMORY_LOW); memory_low = false; + } else { + memory_low = false; + } + #else + kiss_indicate_error(ERROR_MEMORY_LOW); memory_low = false; + #endif + } +} + +void sleep_now() { + #if HAS_SLEEP == true + stopRadio(); // TODO: Check this on all platforms + #if PLATFORM == PLATFORM_ESP32 + #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_XIAO_S3 + #if HAS_DISPLAY + display_intensity = 0; + update_display(true); + #endif + #endif + #if BOARD_MODEL == BOARD_HELTEC32_V4 + digitalWrite(LORA_PA_CPS, LOW); + digitalWrite(LORA_PA_CSD, LOW); + digitalWrite(LORA_PA_PWR_EN, LOW); + digitalWrite(Vext, HIGH); + #endif + #if PIN_DISP_SLEEP >= 0 + pinMode(PIN_DISP_SLEEP, OUTPUT); + digitalWrite(PIN_DISP_SLEEP, DISP_SLEEP_LEVEL); + #endif + #if HAS_BLUETOOTH + if (bt_state == BT_STATE_CONNECTED) { + bt_stop(); + delay(100); + } + #endif + esp_sleep_enable_ext0_wakeup(PIN_WAKEUP, WAKEUP_LEVEL); + esp_deep_sleep_start(); + #elif PLATFORM == PLATFORM_NRF52 + #if BOARD_MODEL == BOARD_HELTEC_T114 + npset(0,0,0); + digitalWrite(PIN_VEXT_EN, LOW); + digitalWrite(PIN_T114_TFT_BLGT, HIGH); + digitalWrite(PIN_T114_TFT_EN, HIGH); + #elif BOARD_MODEL == BOARD_TECHO + for (uint8_t i = display_intensity; i > 0; i--) { analogWrite(pin_backlight, i-1); delay(1); } + epd_black(true); delay(300); epd_black(true); delay(300); epd_black(false); + delay(2000); + analogWrite(PIN_VEXT_EN, 0); + delay(100); + #endif + sd_power_gpregret_set(0, 0x6d); + nrf_gpio_cfg_sense_input(pin_btn_usr1, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + NRF_POWER->SYSTEMOFF = 1; + #endif + #endif +} + +void button_event(uint8_t event, unsigned long duration) { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + if (display_blanked) { + display_unblank(); + } else { + #ifdef BOUNDARY_MODE + // Boundary Mode button mapping: + // >5s = reboot into config mode (clean restart) + // >700ms = sleep + // short = display unblank + if (duration > 5000) { + Serial.println("[Boundary] Button hold >5s — rebooting into config mode"); + boundary_config_request = BOUNDARY_CONFIG_MAGIC; + delay(100); + ESP.restart(); + } else if (duration > 700) { + #if HAS_SLEEP + sleep_now(); + #endif + } else { + display_unblank(); + } + #else + // Standard RNode button mapping + if (duration > 10000) { + #if HAS_CONSOLE + #if HAS_BLUETOOTH || HAS_BLE + bt_stop(); + #endif + console_active = true; + console_start(); + #endif + } else if (duration > 5000) { + #if HAS_BLUETOOTH || HAS_BLE + if (bt_state != BT_STATE_CONNECTED) { bt_enable_pairing(); } + #endif + } else if (duration > 700) { + #if HAS_SLEEP + sleep_now(); + #endif + } else { + #if HAS_BLUETOOTH || HAS_BLE + if (bt_state != BT_STATE_CONNECTED) { + if (bt_state == BT_STATE_OFF) { + bt_start(); + bt_conf_save(true); + } else { + bt_stop(); + bt_conf_save(false); + } + } + #endif + } + #endif // BOUNDARY_MODE + } + #endif +} + +volatile bool serial_polling = false; +void serial_poll() { + serial_polling = true; + + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + while (!fifo_isempty_locked(&serialFIFO)) { + #else + while (!fifo_isempty(&serialFIFO)) { + #endif + char sbyte = fifo_pop(&serialFIFO); + serial_callback(sbyte); + } + + serial_polling = false; +} + +#if MCU_VARIANT != MCU_ESP32 + #define MAX_CYCLES 20 +#else + #define MAX_CYCLES 10 +#endif +void buffer_serial() { + if (!serial_buffering) { + serial_buffering = true; + + uint8_t c = 0; + + #if HAS_BLUETOOTH || HAS_BLE == true + while ( + c < MAX_CYCLES && + #if HAS_WIFI + ( (bt_state != BT_STATE_CONNECTED && Serial.available()) || (bt_state == BT_STATE_CONNECTED && SerialBT.available()) || (wr_state >= WR_STATE_ON && wifi_remote_available()) ) + #else + ( (bt_state != BT_STATE_CONNECTED && Serial.available()) || (bt_state == BT_STATE_CONNECTED && SerialBT.available()) ) + #endif + ) + #else + while (c < MAX_CYCLES && Serial.available()) + #endif + { + c++; + + #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + if (!fifo_isfull_locked(&serialFIFO)) { fifo_push_locked(&serialFIFO, Serial.read()); } + #elif HAS_BLUETOOTH || HAS_BLE == true || HAS_WIFI + #if HAS_BLUETOOTH || HAS_BLE == true + if (bt_state == BT_STATE_CONNECTED) { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, SerialBT.read()); } } + #if HAS_WIFI + else if (wifi_host_is_connected()) { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, wifi_remote_read()); } } + #endif + else { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, Serial.read()); } } + #elif HAS_WIFI + if (wifi_host_is_connected()) { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, wifi_remote_read()); } } + else { if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, Serial.read()); } } + #endif + #else + if (!fifo_isfull(&serialFIFO)) { fifo_push(&serialFIFO, Serial.read()); } + #endif + } + + serial_buffering = false; + } +} + +void serial_interrupt_init() { + #if MCU_VARIANT == MCU_1284P + TCCR3A = 0; + TCCR3B = _BV(CS10) | + _BV(WGM33)| + _BV(WGM32); + + // Buffer incoming frames every 1ms + ICR3 = 16000; + TIMSK3 = _BV(ICIE3); + + #elif MCU_VARIANT == MCU_2560 + // TODO: This should probably be updated for + // atmega2560 support. Might be source of + // reported issues from snh. + TCCR3A = 0; + TCCR3B = _BV(CS10) | + _BV(WGM33)| + _BV(WGM32); + + // Buffer incoming frames every 1ms + ICR3 = 16000; + TIMSK3 = _BV(ICIE3); + + #elif MCU_VARIANT == MCU_ESP32 + // No interrupt-based polling on ESP32 + #endif + +} + +#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + ISR(TIMER3_CAPT_vect) { buffer_serial(); } +#endif diff --git a/ROM.h b/ROM.h new file mode 100755 index 0000000..c883434 --- /dev/null +++ b/ROM.h @@ -0,0 +1,63 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef ROM_H + #define ROM_H + #define CHECKSUMMED_SIZE 0x0B + + // ROM address map /////////////// + #define ADDR_PRODUCT 0x00 + #define ADDR_MODEL 0x01 + #define ADDR_HW_REV 0x02 + #define ADDR_SERIAL 0x03 + #define ADDR_MADE 0x07 + #define ADDR_CHKSUM 0x0B + #define ADDR_SIGNATURE 0x1B + #define ADDR_INFO_LOCK 0x9B + + #define ADDR_CONF_SF 0x9C + #define ADDR_CONF_CR 0x9D + #define ADDR_CONF_TXP 0x9E + #define ADDR_CONF_BW 0x9F + #define ADDR_CONF_FREQ 0xA3 + #define ADDR_CONF_OK 0xA7 + + #define ADDR_CONF_BT 0xB0 + #define ADDR_CONF_DSET 0xB1 + #define ADDR_CONF_DINT 0xB2 + #define ADDR_CONF_DADR 0xB3 + #define ADDR_CONF_DBLK 0xB4 + #define ADDR_CONF_DROT 0xB8 + #define ADDR_CONF_PSET 0xB5 + #define ADDR_CONF_PINT 0xB6 + #define ADDR_CONF_BSET 0xB7 + #define ADDR_CONF_DIA 0xB9 + #define ADDR_CONF_WIFI 0xBA + #define ADDR_CONF_WCHN 0xBB + + #define INFO_LOCK_BYTE 0x73 + #define CONF_OK_BYTE 0x73 + #define BT_ENABLE_BYTE 0x73 + + #define EEPROM_RESERVED 200 + + #define CONFIG_SIZE 256 + #define ADDR_CONF_SSID 0x00 + #define ADDR_CONF_PSK 0x21 + #define ADDR_CONF_IP 0x42 + #define ADDR_CONF_NM 0x46 + ////////////////////////////////// + +#endif diff --git a/Release/README.md b/Release/README.md new file mode 100755 index 0000000..bb773ca --- /dev/null +++ b/Release/README.md @@ -0,0 +1,10 @@ +# Precompiled Firmware +You can download and flash the firmware to supported boards using the [RNode Config Utility](https://github.com/markqvist/rnodeconfigutil). All firmware releases are now handled and installed directly through `rnodeconf`, which is inclueded in the `rns` package. It can be installed via `pip`: + +``` +# Install rnodeconf via rns package +pip install rns --upgrade + +# Install the firmware on a board with the install guide +rnodeconf --autoinstall +``` diff --git a/Release/console_image.bin b/Release/console_image.bin new file mode 100755 index 0000000..32aa935 Binary files /dev/null and b/Release/console_image.bin differ diff --git a/Release/esptool/esptool.py b/Release/esptool/esptool.py new file mode 100755 index 0000000..d1d62b4 --- /dev/null +++ b/Release/esptool/esptool.py @@ -0,0 +1,5651 @@ +#!/usr/bin/env python +# +# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) CO LTD, other contributors as noted. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +from __future__ import division, print_function + +import argparse +import base64 +import binascii +import copy +import hashlib +import inspect +import io +import itertools +import os +import re +import shlex +import string +import struct +import sys +import time +import zlib + +try: + import serial +except ImportError: + print("Pyserial is not installed for %s. Check the README for installation instructions." % (sys.executable)) + raise + +# check 'serial' is 'pyserial' and not 'serial' https://github.com/espressif/esptool/issues/269 +try: + if "serialization" in serial.__doc__ and "deserialization" in serial.__doc__: + raise ImportError(""" +esptool.py depends on pyserial, but there is a conflict with a currently installed package named 'serial'. + +You may be able to work around this by 'pip uninstall serial; pip install pyserial' \ +but this may break other installed Python software that depends on 'serial'. + +There is no good fix for this right now, apart from configuring virtualenvs. \ +See https://github.com/espressif/esptool/issues/269#issuecomment-385298196 for discussion of the underlying issue(s).""") +except TypeError: + pass # __doc__ returns None for pyserial + +try: + import serial.tools.list_ports as list_ports +except ImportError: + print("The installed version (%s) of pyserial appears to be too old for esptool.py (Python interpreter %s). " + "Check the README for installation instructions." % (sys.VERSION, sys.executable)) + raise +except Exception: + if sys.platform == "darwin": + # swallow the exception, this is a known issue in pyserial+macOS Big Sur preview ref https://github.com/espressif/esptool/issues/540 + list_ports = None + else: + raise + + +__version__ = "3.3.3" + +MAX_UINT32 = 0xffffffff +MAX_UINT24 = 0xffffff + +DEFAULT_TIMEOUT = 3 # timeout for most flash operations +START_FLASH_TIMEOUT = 20 # timeout for starting flash (may perform erase) +CHIP_ERASE_TIMEOUT = 120 # timeout for full chip erase +MAX_TIMEOUT = CHIP_ERASE_TIMEOUT * 2 # longest any command can run +SYNC_TIMEOUT = 0.1 # timeout for syncing with bootloader +MD5_TIMEOUT_PER_MB = 8 # timeout (per megabyte) for calculating md5sum +ERASE_REGION_TIMEOUT_PER_MB = 30 # timeout (per megabyte) for erasing a region +ERASE_WRITE_TIMEOUT_PER_MB = 40 # timeout (per megabyte) for erasing and writing data +MEM_END_ROM_TIMEOUT = 0.05 # special short timeout for ESP_MEM_END, as it may never respond +DEFAULT_SERIAL_WRITE_TIMEOUT = 10 # timeout for serial port write +DEFAULT_CONNECT_ATTEMPTS = 7 # default number of times to try connection +WRITE_BLOCK_ATTEMPTS = 3 # number of times to try writing a data block + +SUPPORTED_CHIPS = ['esp8266', 'esp32', 'esp32s2', 'esp32s3beta2', 'esp32s3', 'esp32c3', 'esp32c6beta', 'esp32h2beta1', 'esp32h2beta2', 'esp32c2'] + + +def timeout_per_mb(seconds_per_mb, size_bytes): + """ Scales timeouts which are size-specific """ + result = seconds_per_mb * (size_bytes / 1e6) + if result < DEFAULT_TIMEOUT: + return DEFAULT_TIMEOUT + return result + + +def _chip_to_rom_loader(chip): + return { + 'esp8266': ESP8266ROM, + 'esp32': ESP32ROM, + 'esp32s2': ESP32S2ROM, + 'esp32s3beta2': ESP32S3BETA2ROM, + 'esp32s3': ESP32S3ROM, + 'esp32c3': ESP32C3ROM, + 'esp32c6beta': ESP32C6BETAROM, + 'esp32h2beta1': ESP32H2BETA1ROM, + 'esp32h2beta2': ESP32H2BETA2ROM, + 'esp32c2': ESP32C2ROM, + }[chip] + + +def get_default_connected_device(serial_list, port, connect_attempts, initial_baud, chip='auto', trace=False, + before='default_reset'): + _esp = None + for each_port in reversed(serial_list): + print("Serial port %s" % each_port) + try: + if chip == 'auto': + _esp = ESPLoader.detect_chip(each_port, initial_baud, before, trace, + connect_attempts) + else: + chip_class = _chip_to_rom_loader(chip) + _esp = chip_class(each_port, initial_baud, trace) + _esp.connect(before, connect_attempts) + break + except (FatalError, OSError) as err: + if port is not None: + raise + print("%s failed to connect: %s" % (each_port, err)) + if _esp and _esp._port: + _esp._port.close() + _esp = None + return _esp + + +DETECTED_FLASH_SIZES = { + 0x12: "256KB", + 0x13: "512KB", + 0x14: "1MB", + 0x15: "2MB", + 0x16: "4MB", + 0x17: "8MB", + 0x18: "16MB", + 0x19: "32MB", + 0x1A: "64MB", + 0x1B: "128MB", + 0x1C: "256MB", + 0x20: "64MB", + 0x21: "128MB", + 0x22: "256MB", + 0x32: "256KB", + 0x33: "512KB", + 0x34: "1MB", + 0x35: "2MB", + 0x36: "4MB", + 0x37: "8MB", + 0x38: "16MB", + 0x39: "32MB", + 0x3A: "64MB", +} + + +def check_supported_function(func, check_func): + """ + Decorator implementation that wraps a check around an ESPLoader + bootloader function to check if it's supported. + + This is used to capture the multidimensional differences in + functionality between the ESP8266 & ESP32 (and later chips) ROM loaders, and the + software stub that runs on these. Not possible to do this cleanly + via inheritance alone. + """ + def inner(*args, **kwargs): + obj = args[0] + if check_func(obj): + return func(*args, **kwargs) + else: + raise NotImplementedInROMError(obj, func) + return inner + + +def esp8266_function_only(func): + """ Attribute for a function only supported on ESP8266 """ + return check_supported_function(func, lambda o: o.CHIP_NAME == "ESP8266") + + +def stub_function_only(func): + """ Attribute for a function only supported in the software stub loader """ + return check_supported_function(func, lambda o: o.IS_STUB) + + +def stub_and_esp32_function_only(func): + """ Attribute for a function only supported by software stubs or ESP32 and later chips ROM """ + return check_supported_function(func, lambda o: o.IS_STUB or isinstance(o, ESP32ROM)) + + +def esp32s3_or_newer_function_only(func): + """ Attribute for a function only supported by ESP32S3 and later chips ROM """ + return check_supported_function(func, lambda o: isinstance(o, ESP32S3ROM) or isinstance(o, ESP32C3ROM)) + + +PYTHON2 = sys.version_info[0] < 3 # True if on pre-Python 3 + +# Function to return nth byte of a bitstring +# Different behaviour on Python 2 vs 3 +if PYTHON2: + def byte(bitstr, index): + return ord(bitstr[index]) +else: + def byte(bitstr, index): + return bitstr[index] + +# Provide a 'basestring' class on Python 3 +try: + basestring +except NameError: + basestring = str + + +def print_overwrite(message, last_line=False): + """ Print a message, overwriting the currently printed line. + + If last_line is False, don't append a newline at the end (expecting another subsequent call will overwrite this one.) + + After a sequence of calls with last_line=False, call once with last_line=True. + + If output is not a TTY (for example redirected a pipe), no overwriting happens and this function is the same as print(). + """ + if sys.stdout.isatty(): + print("\r%s" % message, end='\n' if last_line else '') + else: + print(message) + + +def _mask_to_shift(mask): + """ Return the index of the least significant bit in the mask """ + shift = 0 + while mask & 0x1 == 0: + shift += 1 + mask >>= 1 + return shift + + +class ESPLoader(object): + """ Base class providing access to ESP ROM & software stub bootloaders. + Subclasses provide ESP8266 & ESP32 Family specific functionality. + + Don't instantiate this base class directly, either instantiate a subclass or + call ESPLoader.detect_chip() which will interrogate the chip and return the + appropriate subclass instance. + + """ + CHIP_NAME = "Espressif device" + IS_STUB = False + + FPGA_SLOW_BOOT = False + + DEFAULT_PORT = "/dev/ttyUSB0" + + USES_RFC2217 = False + + # Commands supported by ESP8266 ROM bootloader + ESP_FLASH_BEGIN = 0x02 + ESP_FLASH_DATA = 0x03 + ESP_FLASH_END = 0x04 + ESP_MEM_BEGIN = 0x05 + ESP_MEM_END = 0x06 + ESP_MEM_DATA = 0x07 + ESP_SYNC = 0x08 + ESP_WRITE_REG = 0x09 + ESP_READ_REG = 0x0a + + # Some comands supported by ESP32 and later chips ROM bootloader (or -8266 w/ stub) + ESP_SPI_SET_PARAMS = 0x0B + ESP_SPI_ATTACH = 0x0D + ESP_READ_FLASH_SLOW = 0x0e # ROM only, much slower than the stub flash read + ESP_CHANGE_BAUDRATE = 0x0F + ESP_FLASH_DEFL_BEGIN = 0x10 + ESP_FLASH_DEFL_DATA = 0x11 + ESP_FLASH_DEFL_END = 0x12 + ESP_SPI_FLASH_MD5 = 0x13 + + # Commands supported by ESP32-S2 and later chips ROM bootloader only + ESP_GET_SECURITY_INFO = 0x14 + + # Some commands supported by stub only + ESP_ERASE_FLASH = 0xD0 + ESP_ERASE_REGION = 0xD1 + ESP_READ_FLASH = 0xD2 + ESP_RUN_USER_CODE = 0xD3 + + # Flash encryption encrypted data command + ESP_FLASH_ENCRYPT_DATA = 0xD4 + + # Response code(s) sent by ROM + ROM_INVALID_RECV_MSG = 0x05 # response if an invalid message is received + + # Maximum block sized for RAM and Flash writes, respectively. + ESP_RAM_BLOCK = 0x1800 + + FLASH_WRITE_SIZE = 0x400 + + # Default baudrate. The ROM auto-bauds, so we can use more or less whatever we want. + ESP_ROM_BAUD = 115200 + + # First byte of the application image + ESP_IMAGE_MAGIC = 0xe9 + + # Initial state for the checksum routine + ESP_CHECKSUM_MAGIC = 0xef + + # Flash sector size, minimum unit of erase. + FLASH_SECTOR_SIZE = 0x1000 + + UART_DATE_REG_ADDR = 0x60000078 + + CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000 # This ROM address has a different value on each chip model + + UART_CLKDIV_MASK = 0xFFFFF + + # Memory addresses + IROM_MAP_START = 0x40200000 + IROM_MAP_END = 0x40300000 + + # The number of bytes in the UART response that signify command status + STATUS_BYTES_LENGTH = 2 + + # Response to ESP_SYNC might indicate that flasher stub is running instead of the ROM bootloader + sync_stub_detected = False + + # Device PIDs + USB_JTAG_SERIAL_PID = 0x1001 + + # Chip IDs that are no longer supported by esptool + UNSUPPORTED_CHIPS = {6: "ESP32-S3(beta 3)"} + + def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): + """Base constructor for ESPLoader bootloader interaction + + Don't call this constructor, either instantiate ESP8266ROM + or ESP32ROM, or use ESPLoader.detect_chip(). + + This base class has all of the instance methods for bootloader + functionality supported across various chips & stub + loaders. Subclasses replace the functions they don't support + with ones which throw NotImplementedInROMError(). + + """ + self.secure_download_mode = False # flag is set to True if esptool detects the ROM is in Secure Download Mode + self.stub_is_disabled = False # flag is set to True if esptool detects conditions which require the stub to be disabled + + if isinstance(port, basestring): + self._port = serial.serial_for_url(port) + else: + self._port = port + self._slip_reader = slip_reader(self._port, self.trace) + # setting baud rate in a separate step is a workaround for + # CH341 driver on some Linux versions (this opens at 9600 then + # sets), shouldn't matter for other platforms/drivers. See + # https://github.com/espressif/esptool/issues/44#issuecomment-107094446 + self._set_port_baudrate(baud) + self._trace_enabled = trace_enabled + # set write timeout, to prevent esptool blocked at write forever. + try: + self._port.write_timeout = DEFAULT_SERIAL_WRITE_TIMEOUT + except NotImplementedError: + # no write timeout for RFC2217 ports + # need to set the property back to None or it will continue to fail + self._port.write_timeout = None + + @property + def serial_port(self): + return self._port.port + + def _set_port_baudrate(self, baud): + try: + self._port.baudrate = baud + except IOError: + raise FatalError("Failed to set baud rate %d. The driver may not support this rate." % baud) + + @staticmethod + def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_reset', trace_enabled=False, + connect_attempts=DEFAULT_CONNECT_ATTEMPTS): + """ Use serial access to detect the chip type. + + First, get_security_info command is sent to detect the ID of the chip + (supported only by ESP32-C3 and later, works even in the Secure Download Mode). + If this fails, we reconnect and fall-back to reading the magic number. + It's mapped at a specific ROM address and has a different value on each chip model. + This way we can use one memory read and compare it to the magic number for each chip type. + + This routine automatically performs ESPLoader.connect() (passing + connect_mode parameter) as part of querying the chip. + """ + inst = None + detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled) + if detect_port.serial_port.startswith("rfc2217:"): + detect_port.USES_RFC2217 = True + detect_port.connect(connect_mode, connect_attempts, detecting=True) + try: + print('Detecting chip type...', end='') + res = detect_port.check_command('get security info', ESPLoader.ESP_GET_SECURITY_INFO, b'') + res = struct.unpack(" self.STATUS_BYTES_LENGTH: + return data[:-self.STATUS_BYTES_LENGTH] + else: # otherwise, just return the 'val' field which comes from the reply header (this is used by read_reg) + return val + + def flush_input(self): + self._port.flushInput() + self._slip_reader = slip_reader(self._port, self.trace) + + def sync(self): + val, _ = self.command(self.ESP_SYNC, b'\x07\x07\x12\x20' + 32 * b'\x55', + timeout=SYNC_TIMEOUT) + + # ROM bootloaders send some non-zero "val" response. The flasher stub sends 0. If we receive 0 then it + # probably indicates that the chip wasn't or couldn't be reseted properly and esptool is talking to the + # flasher stub. + self.sync_stub_detected = val == 0 + + for _ in range(7): + val, _ = self.command() + self.sync_stub_detected &= val == 0 + + def _setDTR(self, state): + self._port.setDTR(state) + + def _setRTS(self, state): + self._port.setRTS(state) + # Work-around for adapters on Windows using the usbser.sys driver: + # generate a dummy change to DTR so that the set-control-line-state + # request is sent with the updated RTS state and the same DTR state + self._port.setDTR(self._port.dtr) + + def _get_pid(self): + if list_ports is None: + print("\nListing all serial ports is currently not available. Can't get device PID.") + return + active_port = self._port.port + + # Pyserial only identifies regular ports, URL handlers are not supported + if not active_port.lower().startswith(("com", "/dev/")): + print("\nDevice PID identification is only supported on COM and /dev/ serial ports.") + return + # Return the real path if the active port is a symlink + if active_port.startswith("/dev/") and os.path.islink(active_port): + active_port = os.path.realpath(active_port) + + # The "cu" (call-up) device has to be used for outgoing communication on MacOS + if sys.platform == "darwin" and "tty" in active_port: + active_port = [active_port, active_port.replace("tty", "cu")] + ports = list_ports.comports() + for p in ports: + if p.device in active_port: + return p.pid + print("\nFailed to get PID of a device on {}, using standard reset sequence.".format(active_port)) + + def bootloader_reset(self, usb_jtag_serial=False, extra_delay=False): + """ Issue a reset-to-bootloader, with USB-JTAG-Serial custom reset sequence option + """ + # RTS = either CH_PD/EN or nRESET (both active low = chip in reset) + # DTR = GPIO0 (active low = boot to flasher) + # + # DTR & RTS are active low signals, + # ie True = pin @ 0V, False = pin @ VCC. + if usb_jtag_serial: + # Custom reset sequence, which is required when the device + # is connecting via its USB-JTAG-Serial peripheral + self._setRTS(False) + self._setDTR(False) # Idle + time.sleep(0.1) + self._setDTR(True) # Set IO0 + self._setRTS(False) + time.sleep(0.1) + self._setRTS(True) # Reset. Note dtr/rts calls inverted so we go through (1,1) instead of (0,0) + self._setDTR(False) + self._setRTS(True) # Extra RTS set for RTS as Windows only propagates DTR on RTS setting + time.sleep(0.1) + self._setDTR(False) + self._setRTS(False) + else: + # This fpga delay is for Espressif internal use + fpga_delay = True if self.FPGA_SLOW_BOOT and os.environ.get("ESPTOOL_ENV_FPGA", "").strip() == "1" else False + delay = 7 if fpga_delay else 0.5 if extra_delay else 0.05 # 0.5 needed for ESP32 rev0 and rev1 + + self._setDTR(False) # IO0=HIGH + self._setRTS(True) # EN=LOW, chip in reset + time.sleep(0.1) + self._setDTR(True) # IO0=LOW + self._setRTS(False) # EN=HIGH, chip out of reset + time.sleep(delay) + self._setDTR(False) # IO0=HIGH, done + + def _connect_attempt(self, mode='default_reset', usb_jtag_serial=False, extra_delay=False): + """ A single connection attempt """ + last_error = None + boot_log_detected = False + download_mode = False + + # If we're doing no_sync, we're likely communicating as a pass through + # with an intermediate device to the ESP32 + if mode == "no_reset_no_sync": + return last_error + + if mode != 'no_reset': + if not self.USES_RFC2217: # Might block on rfc2217 ports + self._port.reset_input_buffer() # Empty serial buffer to isolate boot log + self.bootloader_reset(usb_jtag_serial, extra_delay) + + # Detect the ROM boot log and check actual boot mode (ESP32 and later only) + waiting = self._port.inWaiting() + read_bytes = self._port.read(waiting) + data = re.search(b'boot:(0x[0-9a-fA-F]+)(.*waiting for download)?', read_bytes, re.DOTALL) + if data is not None: + boot_log_detected = True + boot_mode = data.group(1) + download_mode = data.group(2) is not None + + for _ in range(5): + try: + self.flush_input() + self._port.flushOutput() + self.sync() + return None + except FatalError as e: + print('.', end='') + sys.stdout.flush() + time.sleep(0.05) + last_error = e + + if boot_log_detected: + last_error = FatalError("Wrong boot mode detected ({})! The chip needs to be in download mode.".format(boot_mode.decode("utf-8"))) + if download_mode: + last_error = FatalError("Download mode successfully detected, but getting no sync reply: The serial TX path seems to be down.") + return last_error + + def get_memory_region(self, name): + """ Returns a tuple of (start, end) for the memory map entry with the given name, or None if it doesn't exist + """ + try: + return [(start, end) for (start, end, n) in self.MEMORY_MAP if n == name][0] + except IndexError: + return None + + def connect(self, mode='default_reset', attempts=DEFAULT_CONNECT_ATTEMPTS, detecting=False, warnings=True): + """ Try connecting repeatedly until successful, or giving up """ + if warnings and mode in ['no_reset', 'no_reset_no_sync']: + print('WARNING: Pre-connection option "{}" was selected.'.format(mode), + 'Connection may fail if the chip is not in bootloader or flasher stub mode.') + print('Connecting...', end='') + sys.stdout.flush() + last_error = None + + usb_jtag_serial = (mode == 'usb_reset') or (self._get_pid() == self.USB_JTAG_SERIAL_PID) + + try: + for _, extra_delay in zip(range(attempts) if attempts > 0 else itertools.count(), itertools.cycle((False, True))): + last_error = self._connect_attempt(mode=mode, usb_jtag_serial=usb_jtag_serial, extra_delay=extra_delay) + if last_error is None: + break + finally: + print('') # end 'Connecting...' line + + if last_error is not None: + raise FatalError('Failed to connect to {}: {}' + '\nFor troubleshooting steps visit: ' + 'https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html'.format(self.CHIP_NAME, last_error)) + + if not detecting: + try: + # check the date code registers match what we expect to see + chip_magic_value = self.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR) + if chip_magic_value not in self.CHIP_DETECT_MAGIC_VALUE: + actually = None + for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3ROM, + ESP32C3ROM, ESP32H2BETA1ROM, ESP32H2BETA2ROM, ESP32C2ROM, ESP32C6BETAROM]: + if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE: + actually = cls + break + if warnings and actually is None: + print(("WARNING: This chip doesn't appear to be a %s (chip magic value 0x%08x). " + "Probably it is unsupported by this version of esptool.") % (self.CHIP_NAME, chip_magic_value)) + else: + raise FatalError("This chip is %s not %s. Wrong --chip argument?" % (actually.CHIP_NAME, self.CHIP_NAME)) + except UnsupportedCommandError: + self.secure_download_mode = True + self._post_connect() + self.check_chip_id() + + def _post_connect(self): + """ + Additional initialization hook, may be overridden by the chip-specific class. + Gets called after connect, and after auto-detection. + """ + pass + + def read_reg(self, addr, timeout=DEFAULT_TIMEOUT): + """ Read memory address in target """ + # we don't call check_command here because read_reg() function is called + # when detecting chip type, and the way we check for success (STATUS_BYTES_LENGTH) is different + # for different chip types (!) + val, data = self.command(self.ESP_READ_REG, struct.pack(' 0: + # add a dummy write to a date register as an excuse to have a delay + command += struct.pack(' start: + raise FatalError(("Software loader is resident at 0x%08x-0x%08x. " + "Can't load binary at overlapping address range 0x%08x-0x%08x. " + "Either change binary loading address, or use the --no-stub " + "option to disable the software loader.") % (start, end, load_start, load_end)) + + return self.check_command("enter RAM download mode", self.ESP_MEM_BEGIN, + struct.pack(' length: + raise FatalError('Read more than expected') + + digest_frame = self.read() + if len(digest_frame) != 16: + raise FatalError('Expected digest, got: %s' % hexify(digest_frame)) + expected_digest = hexify(digest_frame).upper() + digest = hashlib.md5(data).hexdigest().upper() + if digest != expected_digest: + raise FatalError('Digest mismatch: expected %s, got %s' % (expected_digest, digest)) + return data + + def flash_spi_attach(self, hspi_arg): + """Send SPI attach command to enable the SPI flash pins + + ESP8266 ROM does this when you send flash_begin, ESP32 ROM + has it as a SPI command. + """ + # last 3 bytes in ESP_SPI_ATTACH argument are reserved values + arg = struct.pack(' 0: + self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1) + if miso_bits > 0: + self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1) + flags = 0 + if dummy_len > 0: + flags |= (dummy_len - 1) + if addr_len > 0: + flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT + if flags: + self.write_reg(SPI_USR1_REG, flags) + else: + def set_data_lengths(mosi_bits, miso_bits): + SPI_DATA_LEN_REG = SPI_USR1_REG + SPI_MOSI_BITLEN_S = 17 + SPI_MISO_BITLEN_S = 8 + mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1) + miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1) + flags = (miso_mask << SPI_MISO_BITLEN_S) | (mosi_mask << SPI_MOSI_BITLEN_S) + if dummy_len > 0: + flags |= (dummy_len - 1) + if addr_len > 0: + flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT + self.write_reg(SPI_DATA_LEN_REG, flags) + + # SPI peripheral "command" bitmasks for SPI_CMD_REG + SPI_CMD_USR = (1 << 18) + + # shift values + SPI_USR2_COMMAND_LEN_SHIFT = 28 + SPI_USR_ADDR_LEN_SHIFT = 26 + + if read_bits > 32: + raise FatalError("Reading more than 32 bits back from a SPI flash operation is unsupported") + if len(data) > 64: + raise FatalError("Writing more than 64 bytes of data with one SPI command is unsupported") + + data_bits = len(data) * 8 + old_spi_usr = self.read_reg(SPI_USR_REG) + old_spi_usr2 = self.read_reg(SPI_USR2_REG) + flags = SPI_USR_COMMAND + if read_bits > 0: + flags |= SPI_USR_MISO + if data_bits > 0: + flags |= SPI_USR_MOSI + if addr_len > 0: + flags |= SPI_USR_ADDR + if dummy_len > 0: + flags |= SPI_USR_DUMMY + set_data_lengths(data_bits, read_bits) + self.write_reg(SPI_USR_REG, flags) + self.write_reg(SPI_USR2_REG, + (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command) + if addr and addr_len > 0: + self.write_reg(SPI_ADDR_REG, addr) + if data_bits == 0: + self.write_reg(SPI_W0_REG, 0) # clear data register before we read it + else: + data = pad_to(data, 4, b'\00') # pad to 32-bit multiple + words = struct.unpack("I" * (len(data) // 4), data) + next_reg = SPI_W0_REG + for word in words: + self.write_reg(next_reg, word) + next_reg += 4 + self.write_reg(SPI_CMD_REG, SPI_CMD_USR) + + def wait_done(): + for _ in range(10): + if (self.read_reg(SPI_CMD_REG) & SPI_CMD_USR) == 0: + return + raise FatalError("SPI command did not complete in time") + wait_done() + + status = self.read_reg(SPI_W0_REG) + # restore some SPI controller registers + self.write_reg(SPI_USR_REG, old_spi_usr) + self.write_reg(SPI_USR2_REG, old_spi_usr2) + return status + + def read_spiflash_sfdp(self, addr, read_bits): + CMD_RDSFDP = 0x5A + return self.run_spiflash_command(CMD_RDSFDP, read_bits=read_bits, addr=addr, addr_len=24, dummy_len=8) + + def read_status(self, num_bytes=2): + """Read up to 24 bits (num_bytes) of SPI flash status register contents + via RDSR, RDSR2, RDSR3 commands + + Not all SPI flash supports all three commands. The upper 1 or 2 + bytes may be 0xFF. + """ + SPIFLASH_RDSR = 0x05 + SPIFLASH_RDSR2 = 0x35 + SPIFLASH_RDSR3 = 0x15 + + status = 0 + shift = 0 + for cmd in [SPIFLASH_RDSR, SPIFLASH_RDSR2, SPIFLASH_RDSR3][0:num_bytes]: + status += self.run_spiflash_command(cmd, read_bits=8) << shift + shift += 8 + return status + + def write_status(self, new_status, num_bytes=2, set_non_volatile=False): + """Write up to 24 bits (num_bytes) of new status register + + num_bytes can be 1, 2 or 3. + + Not all flash supports the additional commands to write the + second and third byte of the status register. When writing 2 + bytes, esptool also sends a 16-byte WRSR command (as some + flash types use this instead of WRSR2.) + + If the set_non_volatile flag is set, non-volatile bits will + be set as well as volatile ones (WREN used instead of WEVSR). + + """ + SPIFLASH_WRSR = 0x01 + SPIFLASH_WRSR2 = 0x31 + SPIFLASH_WRSR3 = 0x11 + SPIFLASH_WEVSR = 0x50 + SPIFLASH_WREN = 0x06 + SPIFLASH_WRDI = 0x04 + + enable_cmd = SPIFLASH_WREN if set_non_volatile else SPIFLASH_WEVSR + + # try using a 16-bit WRSR (not supported by all chips) + # this may be redundant, but shouldn't hurt + if num_bytes == 2: + self.run_spiflash_command(enable_cmd) + self.run_spiflash_command(SPIFLASH_WRSR, struct.pack(">= 8 + + self.run_spiflash_command(SPIFLASH_WRDI) + + def get_crystal_freq(self): + # Figure out the crystal frequency from the UART clock divider + # Returns a normalized value in integer MHz (40 or 26 are the only supported values) + # + # The logic here is: + # - We know that our baud rate and the ESP UART baud rate are roughly the same, or we couldn't communicate + # - We can read the UART clock divider register to know how the ESP derives this from the APB bus frequency + # - Multiplying these two together gives us the bus frequency which is either the crystal frequency (ESP32) + # or double the crystal frequency (ESP8266). See the self.XTAL_CLK_DIVIDER parameter for this factor. + uart_div = self.read_reg(self.UART_CLKDIV_REG) & self.UART_CLKDIV_MASK + est_xtal = (self._port.baudrate * uart_div) / 1e6 / self.XTAL_CLK_DIVIDER + norm_xtal = 40 if est_xtal > 33 else 26 + if abs(norm_xtal - est_xtal) > 1: + print("WARNING: Detected crystal freq %.2fMHz is quite different to normalized freq %dMHz. Unsupported crystal in use?" % (est_xtal, norm_xtal)) + return norm_xtal + + def hard_reset(self): + print('Hard resetting via RTS pin...') + self._setRTS(True) # EN->LOW + time.sleep(0.1) + self._setRTS(False) + + def soft_reset(self, stay_in_bootloader): + if not self.IS_STUB: + if stay_in_bootloader: + return # ROM bootloader is already in bootloader! + else: + # 'run user code' is as close to a soft reset as we can do + self.flash_begin(0, 0) + self.flash_finish(False) + else: + if stay_in_bootloader: + # soft resetting from the stub loader + # will re-load the ROM bootloader + self.flash_begin(0, 0) + self.flash_finish(True) + elif self.CHIP_NAME != "ESP8266": + raise FatalError("Soft resetting is currently only supported on ESP8266") + else: + # running user code from stub loader requires some hacks + # in the stub loader + self.command(self.ESP_RUN_USER_CODE, wait_response=False) + + def check_chip_id(self): + try: + chip_id = self.get_chip_id() + if chip_id != self.IMAGE_CHIP_ID: + print("WARNING: Chip ID {} ({}) doesn't match expected Chip ID {}. esptool may not work correctly." + .format(chip_id, self.UNSUPPORTED_CHIPS.get(chip_id, 'Unknown'), self.IMAGE_CHIP_ID)) + # Try to flash anyways by disabling stub + self.stub_is_disabled = True + except NotImplementedInROMError: + pass + + +class ESP8266ROM(ESPLoader): + """ Access class for ESP8266 ROM bootloader + """ + CHIP_NAME = "ESP8266" + IS_STUB = False + + CHIP_DETECT_MAGIC_VALUE = [0xfff0c101] + + # OTP ROM addresses + ESP_OTP_MAC0 = 0x3ff00050 + ESP_OTP_MAC1 = 0x3ff00054 + ESP_OTP_MAC3 = 0x3ff0005c + + SPI_REG_BASE = 0x60000200 + SPI_USR_OFFS = 0x1c + SPI_USR1_OFFS = 0x20 + SPI_USR2_OFFS = 0x24 + SPI_MOSI_DLEN_OFFS = None + SPI_MISO_DLEN_OFFS = None + SPI_W0_OFFS = 0x40 + + UART_CLKDIV_REG = 0x60000014 + + XTAL_CLK_DIVIDER = 2 + + FLASH_SIZES = { + '512KB': 0x00, + '256KB': 0x10, + '1MB': 0x20, + '2MB': 0x30, + '4MB': 0x40, + '2MB-c1': 0x50, + '4MB-c1': 0x60, + '8MB': 0x80, + '16MB': 0x90, + } + + FLASH_FREQUENCY = { + '80m': 0xf, + '40m': 0x0, + '26m': 0x1, + '20m': 0x2, + } + + BOOTLOADER_FLASH_OFFSET = 0 + + MEMORY_MAP = [[0x3FF00000, 0x3FF00010, "DPORT"], + [0x3FFE8000, 0x40000000, "DRAM"], + [0x40100000, 0x40108000, "IRAM"], + [0x40201010, 0x402E1010, "IROM"]] + + def get_efuses(self): + # Return the 128 bits of ESP8266 efuse as a single Python integer + result = self.read_reg(0x3ff0005c) << 96 + result |= self.read_reg(0x3ff00058) << 64 + result |= self.read_reg(0x3ff00054) << 32 + result |= self.read_reg(0x3ff00050) + return result + + def _get_flash_size(self, efuses): + # rX_Y = EFUSE_DATA_OUTX[Y] + r0_4 = (efuses & (1 << 4)) != 0 + r3_25 = (efuses & (1 << 121)) != 0 + r3_26 = (efuses & (1 << 122)) != 0 + r3_27 = (efuses & (1 << 123)) != 0 + + if r0_4 and not r3_25: + if not r3_27 and not r3_26: + return 1 + elif not r3_27 and r3_26: + return 2 + if not r0_4 and r3_25: + if not r3_27 and not r3_26: + return 2 + elif not r3_27 and r3_26: + return 4 + return -1 + + def get_chip_description(self): + efuses = self.get_efuses() + is_8285 = (efuses & ((1 << 4) | 1 << 80)) != 0 # One or the other efuse bit is set for ESP8285 + if is_8285: + flash_size = self._get_flash_size(efuses) + max_temp = (efuses & (1 << 5)) != 0 # This efuse bit identifies the max flash temperature + chip_name = { + 1: "ESP8285H08" if max_temp else "ESP8285N08", + 2: "ESP8285H16" if max_temp else "ESP8285N16" + }.get(flash_size, "ESP8285") + return chip_name + return "ESP8266EX" + + def get_chip_features(self): + features = ["WiFi"] + if "ESP8285" in self.get_chip_description(): + features += ["Embedded Flash"] + return features + + def flash_spi_attach(self, hspi_arg): + if self.IS_STUB: + super(ESP8266ROM, self).flash_spi_attach(hspi_arg) + else: + # ESP8266 ROM has no flash_spi_attach command in serial protocol, + # but flash_begin will do it + self.flash_begin(0, 0) + + def flash_set_parameters(self, size): + # not implemented in ROM, but OK to silently skip for ROM + if self.IS_STUB: + super(ESP8266ROM, self).flash_set_parameters(size) + + def chip_id(self): + """ Read Chip ID from efuse - the equivalent of the SDK system_get_chip_id() function """ + id0 = self.read_reg(self.ESP_OTP_MAC0) + id1 = self.read_reg(self.ESP_OTP_MAC1) + return (id0 >> 24) | ((id1 & MAX_UINT24) << 8) + + def read_mac(self): + """ Read MAC from OTP ROM """ + mac0 = self.read_reg(self.ESP_OTP_MAC0) + mac1 = self.read_reg(self.ESP_OTP_MAC1) + mac3 = self.read_reg(self.ESP_OTP_MAC3) + if (mac3 != 0): + oui = ((mac3 >> 16) & 0xff, (mac3 >> 8) & 0xff, mac3 & 0xff) + elif ((mac1 >> 16) & 0xff) == 0: + oui = (0x18, 0xfe, 0x34) + elif ((mac1 >> 16) & 0xff) == 1: + oui = (0xac, 0xd0, 0x74) + else: + raise FatalError("Unknown OUI") + return oui + ((mac1 >> 8) & 0xff, mac1 & 0xff, (mac0 >> 24) & 0xff) + + def get_erase_size(self, offset, size): + """ Calculate an erase size given a specific size in bytes. + + Provides a workaround for the bootloader erase bug.""" + + sectors_per_block = 16 + sector_size = self.FLASH_SECTOR_SIZE + num_sectors = (size + sector_size - 1) // sector_size + start_sector = offset // sector_size + + head_sectors = sectors_per_block - (start_sector % sectors_per_block) + if num_sectors < head_sectors: + head_sectors = num_sectors + + if num_sectors < 2 * head_sectors: + return (num_sectors + 1) // 2 * sector_size + else: + return (num_sectors - head_sectors) * sector_size + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("Overriding VDDSDIO setting only applies to ESP32") + + +class ESP8266StubLoader(ESP8266ROM): + """ Access class for ESP8266 stub loader, runs on top of ROM. + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + def get_erase_size(self, offset, size): + return size # stub doesn't have same size bug as ROM loader + + +ESP8266ROM.STUB_CLASS = ESP8266StubLoader + + +class ESP32ROM(ESPLoader): + """Access class for ESP32 ROM bootloader + + """ + CHIP_NAME = "ESP32" + IMAGE_CHIP_ID = 0 + IS_STUB = False + + FPGA_SLOW_BOOT = True + + CHIP_DETECT_MAGIC_VALUE = [0x00f01d83] + + IROM_MAP_START = 0x400d0000 + IROM_MAP_END = 0x40400000 + + DROM_MAP_START = 0x3F400000 + DROM_MAP_END = 0x3F800000 + + # ESP32 uses a 4 byte status reply + STATUS_BYTES_LENGTH = 4 + + SPI_REG_BASE = 0x3ff42000 + SPI_USR_OFFS = 0x1c + SPI_USR1_OFFS = 0x20 + SPI_USR2_OFFS = 0x24 + SPI_MOSI_DLEN_OFFS = 0x28 + SPI_MISO_DLEN_OFFS = 0x2c + EFUSE_RD_REG_BASE = 0x3ff5a000 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + 0x18 + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = (1 << 7) # EFUSE_RD_DISABLE_DL_ENCRYPT + + DR_REG_SYSCON_BASE = 0x3ff66000 + APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE + 0x7C + APB_CTL_DATE_V = 0x1 + APB_CTL_DATE_S = 31 + + SPI_W0_OFFS = 0x80 + + UART_CLKDIV_REG = 0x3ff40014 + + XTAL_CLK_DIVIDER = 1 + + FLASH_SIZES = { + '1MB': 0x00, + '2MB': 0x10, + '4MB': 0x20, + '8MB': 0x30, + '16MB': 0x40, + '32MB': 0x50, + '64MB': 0x60, + '128MB': 0x70 + } + + FLASH_FREQUENCY = { + '80m': 0xf, + '40m': 0x0, + '26m': 0x1, + '20m': 0x2, + } + + BOOTLOADER_FLASH_OFFSET = 0x1000 + + OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"] + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3F400000, 0x3F800000, "DROM"], + [0x3F800000, 0x3FC00000, "EXTRAM_DATA"], + [0x3FF80000, 0x3FF82000, "RTC_DRAM"], + [0x3FF90000, 0x40000000, "BYTE_ACCESSIBLE"], + [0x3FFAE000, 0x40000000, "DRAM"], + [0x3FFE0000, 0x3FFFFFFC, "DIRAM_DRAM"], + [0x40000000, 0x40070000, "IROM"], + [0x40070000, 0x40078000, "CACHE_PRO"], + [0x40078000, 0x40080000, "CACHE_APP"], + [0x40080000, 0x400A0000, "IRAM"], + [0x400A0000, 0x400BFFFC, "DIRAM_IRAM"], + [0x400C0000, 0x400C2000, "RTC_IRAM"], + [0x400D0000, 0x40400000, "IROM"], + [0x50000000, 0x50002000, "RTC_DATA"]] + + FLASH_ENCRYPTED_WRITE_ALIGN = 32 + + """ Try to read the BLOCK1 (encryption key) and check if it is valid """ + + def is_flash_encryption_key_valid(self): + + """ Bit 0 of efuse_rd_disable[3:0] is mapped to BLOCK1 + this bit is at position 16 in EFUSE_BLK0_RDATA0_REG """ + word0 = self.read_efuse(0) + rd_disable = (word0 >> 16) & 0x1 + + # reading of BLOCK1 is NOT ALLOWED so we assume valid key is programmed + if rd_disable: + return True + else: + # reading of BLOCK1 is ALLOWED so we will read and verify for non-zero. + # When ESP32 has not generated AES/encryption key in BLOCK1, the contents will be readable and 0. + # If the flash encryption is enabled it is expected to have a valid non-zero key. We break out on + # first occurance of non-zero value + key_word = [0] * 7 + for i in range(len(key_word)): + key_word[i] = self.read_efuse(14 + i) + # key is non-zero so break & return + if key_word[i] != 0: + return True + return False + + def get_flash_crypt_config(self): + """ For flash encryption related commands we need to make sure + user has programmed all the relevant efuse correctly so before + writing encrypted write_flash_encrypt esptool will verify the values + of flash_crypt_config to be non zero if they are not read + protected. If the values are zero a warning will be printed + + bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config + this bit is at position 19 in EFUSE_BLK0_RDATA0_REG """ + word0 = self.read_efuse(0) + rd_disable = (word0 >> 19) & 0x1 + + if rd_disable == 0: + """ we can read the flash_crypt_config efuse value + so go & read it (EFUSE_BLK0_RDATA5_REG[31:28]) """ + word5 = self.read_efuse(5) + word5 = (word5 >> 28) & 0xF + return word5 + else: + # if read of the efuse is disabled we assume it is set correctly + return 0xF + + def get_encrypted_download_disabled(self): + if self.read_reg(self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG) & self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT: + return True + else: + return False + + def get_pkg_version(self): + word3 = self.read_efuse(3) + pkg_version = (word3 >> 9) & 0x07 + pkg_version += ((word3 >> 2) & 0x1) << 3 + return pkg_version + + # Returns new version format based on major and minor versions + def get_chip_full_revision(self): + return self.get_major_chip_version() * 100 + self.get_minor_chip_version() + + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return self.get_major_chip_version() + + def get_minor_chip_version(self): + return (self.read_efuse(5) >> 24) & 0x3 + + def get_major_chip_version(self): + rev_bit0 = (self.read_efuse(3) >> 15) & 0x1 + rev_bit1 = (self.read_efuse(5) >> 20) & 0x1 + apb_ctl_date = self.read_reg(self.APB_CTL_DATE_ADDR) + rev_bit2 = (apb_ctl_date >> self.APB_CTL_DATE_S) & self.APB_CTL_DATE_V + combine_value = (rev_bit2 << 2) | (rev_bit1 << 1) | rev_bit0 + + revision = { + 0: 0, + 1: 1, + 3: 2, + 7: 3, + }.get(combine_value, 0) + return revision + + def get_chip_description(self): + pkg_version = self.get_pkg_version() + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + rev3 = major_rev == 3 + single_core = self.read_efuse(3) & (1 << 0) # CHIP_VER DIS_APP_CPU + + chip_name = { + 0: "ESP32-S0WDQ6" if single_core else "ESP32-D0WDQ6", + 1: "ESP32-S0WD" if single_core else "ESP32-D0WD", + 2: "ESP32-D2WD", + 4: "ESP32-U4WDH", + 5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4", + 6: "ESP32-PICO-V3-02", + 7: "ESP32-D0WDR2-V3", + }.get(pkg_version, "unknown ESP32") + + # ESP32-D0WD-V3, ESP32-D0WDQ6-V3 + if chip_name.startswith("ESP32-D0WD") and rev3: + chip_name += "-V3" + + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) + + def get_chip_features(self): + features = ["WiFi"] + word3 = self.read_efuse(3) + + # names of variables in this section are lowercase + # versions of EFUSE names as documented in TRM and + # ESP-IDF efuse_reg.h + + chip_ver_dis_bt = word3 & (1 << 1) + if chip_ver_dis_bt == 0: + features += ["BT"] + + chip_ver_dis_app_cpu = word3 & (1 << 0) + if chip_ver_dis_app_cpu: + features += ["Single Core"] + else: + features += ["Dual Core"] + + chip_cpu_freq_rated = word3 & (1 << 13) + if chip_cpu_freq_rated: + chip_cpu_freq_low = word3 & (1 << 12) + if chip_cpu_freq_low: + features += ["160MHz"] + else: + features += ["240MHz"] + + pkg_version = self.get_pkg_version() + if pkg_version in [2, 4, 5, 6]: + features += ["Embedded Flash"] + + if pkg_version == 6: + features += ["Embedded PSRAM"] + + word4 = self.read_efuse(4) + adc_vref = (word4 >> 8) & 0x1F + if adc_vref: + features += ["VRef calibration in efuse"] + + blk3_part_res = word3 >> 14 & 0x1 + if blk3_part_res: + features += ["BLK3 partially reserved"] + + word6 = self.read_efuse(6) + coding_scheme = word6 & 0x3 + features += ["Coding Scheme %s" % { + 0: "None", + 1: "3/4", + 2: "Repeat (UNSUPPORTED)", + 3: "Invalid"}[coding_scheme]] + + return features + + def read_efuse(self, n): + """ Read the nth word of the ESP3x EFUSE region. """ + return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n)) + + def chip_id(self): + raise NotSupportedError(self, "chip_id") + + def read_mac(self): + """ Read MAC from EFUSE region """ + words = [self.read_efuse(2), self.read_efuse(1)] + bitstring = struct.pack(">II", *words) + bitstring = bitstring[2:8] # trim the 2 byte CRC + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_erase_size(self, offset, size): + return size + + def override_vddsdio(self, new_voltage): + new_voltage = new_voltage.upper() + if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES: + raise FatalError("The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'") + RTC_CNTL_SDIO_CONF_REG = 0x3ff48074 + RTC_CNTL_XPD_SDIO_REG = (1 << 31) + RTC_CNTL_DREFH_SDIO_M = (3 << 29) + RTC_CNTL_DREFM_SDIO_M = (3 << 27) + RTC_CNTL_DREFL_SDIO_M = (3 << 25) + # RTC_CNTL_SDIO_TIEH = (1 << 23) # not used here, setting TIEH=1 would set 3.3V output, not safe for esptool.py to do + RTC_CNTL_SDIO_FORCE = (1 << 22) + RTC_CNTL_SDIO_PD_EN = (1 << 21) + + reg_val = RTC_CNTL_SDIO_FORCE # override efuse setting + reg_val |= RTC_CNTL_SDIO_PD_EN + if new_voltage != "OFF": + reg_val |= RTC_CNTL_XPD_SDIO_REG # enable internal LDO + if new_voltage == "1.9V": + reg_val |= (RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M) # boost voltage + self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val) + print("VDDSDIO regulator set to %s" % new_voltage) + + def read_flash_slow(self, offset, length, progress_fn): + BLOCK_LEN = 64 # ROM read limit per command (this limit is why it's so slow) + + data = b'' + while len(data) < length: + block_len = min(BLOCK_LEN, length - len(data)) + r = self.check_command("read flash block", self.ESP_READ_FLASH_SLOW, + struct.pack('> 0) & 0x0F + + def get_minor_chip_version(self): + hi_num_word = 3 + hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 20) & 0x01 + low_num_word = 4 + low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 4) & 0x07 + return (hi << 3) + low + + def get_major_chip_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x03 + + def get_flash_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F + + def get_psram_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 28) & 0x0F + + def get_block2_version(self): + # BLK_VERSION_MINOR + num_word = 4 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 4) & 0x07 + + def get_chip_description(self): + chip_name = { + 0: "ESP32-S2", + 1: "ESP32-S2FH2", + 2: "ESP32-S2FH4", + 102: "ESP32-S2FNR2", + 100: "ESP32-S2R2", + }.get(self.get_flash_version() + self.get_psram_version() * 100, "unknown ESP32-S2") + + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) + + def get_chip_features(self): + features = ["WiFi"] + + if self.secure_download_mode: + features += ["Secure Download Mode Enabled"] + + flash_version = { + 0: "No Embedded Flash", + 1: "Embedded Flash 2MB", + 2: "Embedded Flash 4MB", + }.get(self.get_flash_version(), "Unknown Embedded Flash") + features += [flash_version] + + psram_version = { + 0: "No Embedded PSRAM", + 1: "Embedded PSRAM 2MB", + 2: "Embedded PSRAM 4MB", + }.get(self.get_psram_version(), "Unknown Embedded PSRAM") + features += [psram_version] + + block2_version = { + 0: "No calibration in BLK2 of efuse", + 1: "ADC and temperature sensor calibration in BLK2 of efuse V1", + 2: "ADC and temperature sensor calibration in BLK2 of efuse V2", + }.get(self.get_block2_version(), "Unknown Calibration in BLK2") + features += [block2_version] + + return features + + def get_crystal_freq(self): + # ESP32-S2 XTAL is fixed to 40MHz + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-S2") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-S2 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see either an AES-128 key or two AES-256 keys + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes): + return True + + return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) \ + and any(p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes) + + def uses_usb(self, _cache=[]): + if self.secure_download_mode: + return False # can't detect native USB in secure download mode + if not _cache: + buf_no = self.read_reg(self.UARTDEV_BUF_NO) & 0xff + _cache.append(buf_no == self.UARTDEV_BUF_NO_USB) + return _cache[0] + + def _post_connect(self): + if self.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + + def _check_if_can_reset(self): + """ + Check the strapping register to see if we can reset out of download mode. + """ + if os.getenv("ESPTOOL_TESTING") is not None: + print("ESPTOOL_TESTING is set, ignoring strapping mode check") + # Esptool tests over USB CDC run with GPIO0 strapped low, don't complain in this case. + return + strap_reg = self.read_reg(self.GPIO_STRAP_REG) + force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG) + if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0: + print("WARNING: {} chip was placed into download mode using GPIO0.\n" + "esptool.py can not exit the download mode over USB. " + "To run the app, reset the chip manually.\n" + "To suppress this note, set --after option to 'no_reset'.".format(self.get_chip_description())) + raise SystemExit(1) + + def hard_reset(self): + if self.uses_usb(): + self._check_if_can_reset() + + print('Hard resetting via RTS pin...') + self._setRTS(True) # EN->LOW + if self.uses_usb(): + # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions + time.sleep(0.2) + self._setRTS(False) + time.sleep(0.2) + else: + time.sleep(0.1) + self._setRTS(False) + + +class ESP32S3ROM(ESP32ROM): + CHIP_NAME = "ESP32-S3" + + IMAGE_CHIP_ID = 9 + + CHIP_DETECT_MAGIC_VALUE = [0x9] + + BOOTLOADER_FLASH_OFFSET = 0x0 + + FPGA_SLOW_BOOT = False + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x44000000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3e000000 + + UART_DATE_REG_ADDR = 0x60000080 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1c + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + # todo: use espefuse APIs to get this info + EFUSE_BASE = 0x60007000 # BLOCK0 read base address + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x44 + EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x5C + EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + + EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY0_SHIFT = 24 + EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY1_SHIFT = 28 + EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY2_SHIFT = 0 + EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY3_SHIFT = 4 + EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY4_SHIFT = 8 + EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY5_SHIFT = 12 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20 + + PURPOSE_VAL_XTS_AES256_KEY_1 = 2 + PURPOSE_VAL_XTS_AES256_KEY_2 = 3 + PURPOSE_VAL_XTS_AES128_KEY = 4 + + UARTDEV_BUF_NO = 0x3fcef14c # Variable in ROM .bss which indicates the port in use + UARTDEV_BUF_NO_USB = 3 # Value of the above variable indicating that USB is in use + + USB_RAM_BLOCK = 0x800 # Max block size USB CDC is used + + GPIO_STRAP_REG = 0x60004038 + GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode + RTC_CNTL_OPTION1_REG = 0x6000812C + RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB? + + UART_CLKDIV_REG = 0x60000014 + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3C000000, 0x3D000000, "DROM"], + [0x3D000000, 0x3E000000, "EXTRAM_DATA"], + [0x600FE000, 0x60100000, "RTC_DRAM"], + [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"], + [0x3FC88000, 0x403E2000, "MEM_INTERNAL"], + [0x3FC88000, 0x3FD00000, "DRAM"], + [0x40000000, 0x4001A100, "IROM_MASK"], + [0x40370000, 0x403E0000, "IRAM"], + [0x600FE000, 0x60100000, "RTC_IRAM"], + [0x42000000, 0x42800000, "IROM"], + [0x50000000, 0x50002000, "RTC_DATA"]] + + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return self.get_minor_chip_version() + + def get_pkg_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07 + + def is_eco0(self, minor_raw): + # Workaround: The major version field was allocated to other purposes + # when block version is v1.1. + # Luckily only chip v0.0 have this kind of block version and efuse usage. + return ( + (minor_raw & 0x7) == 0 and self.get_blk_version_major() == 1 and self.get_blk_version_minor() == 1 + ) + + def get_minor_chip_version(self): + minor_raw = self.get_raw_minor_chip_version() + if self.is_eco0(minor_raw): + return 0 + return minor_raw + + def get_raw_minor_chip_version(self): + hi_num_word = 5 + hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01 + low_num_word = 3 + low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07 + return (hi << 3) + low + + def get_blk_version_major(self): + num_word = 4 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 0) & 0x03 + + def get_blk_version_minor(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x07 + + def get_major_chip_version(self): + minor_raw = self.get_raw_minor_chip_version() + if self.is_eco0(minor_raw): + return 0 + return self.get_raw_major_chip_version() + + def get_raw_major_chip_version(self): + num_word = 5 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03 + + def get_chip_description(self): + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (self.CHIP_NAME, major_rev, minor_rev) + + def get_chip_features(self): + return ["WiFi", "BLE"] + + def get_crystal_freq(self): + # ESP32S3 XTAL is fixed to 40MHz + return 40 + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-S3 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see either an AES-128 key or two AES-256 keys + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes): + return True + + return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) \ + and any(p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes) + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-S3") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def uses_usb(self, _cache=[]): + if self.secure_download_mode: + return False # can't detect native USB in secure download mode + if not _cache: + buf_no = self.read_reg(self.UARTDEV_BUF_NO) & 0xff + _cache.append(buf_no == self.UARTDEV_BUF_NO_USB) + return _cache[0] + + def _post_connect(self): + if self.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + + def _check_if_can_reset(self): + """ + Check the strapping register to see if we can reset out of download mode. + """ + if os.getenv("ESPTOOL_TESTING") is not None: + print("ESPTOOL_TESTING is set, ignoring strapping mode check") + # Esptool tests over USB CDC run with GPIO0 strapped low, don't complain in this case. + return + strap_reg = self.read_reg(self.GPIO_STRAP_REG) + force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG) + if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0: + print("WARNING: {} chip was placed into download mode using GPIO0.\n" + "esptool.py can not exit the download mode over USB. " + "To run the app, reset the chip manually.\n" + "To suppress this note, set --after option to 'no_reset'.".format(self.get_chip_description())) + raise SystemExit(1) + + def hard_reset(self): + if self.uses_usb(): + self._check_if_can_reset() + + print('Hard resetting via RTS pin...') + self._setRTS(True) # EN->LOW + if self.uses_usb(): + # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions + time.sleep(0.2) + self._setRTS(False) + time.sleep(0.2) + else: + time.sleep(0.1) + self._setRTS(False) + + +class ESP32S3BETA2ROM(ESP32S3ROM): + CHIP_NAME = "ESP32-S3(beta2)" + IMAGE_CHIP_ID = 4 + + CHIP_DETECT_MAGIC_VALUE = [0xeb004136] + + EFUSE_BASE = 0x6001A000 # BLOCK0 read base address + + def get_chip_description(self): + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (self.CHIP_NAME, major_rev, minor_rev) + + +class ESP32C3ROM(ESP32ROM): + CHIP_NAME = "ESP32-C3" + IMAGE_CHIP_ID = 5 + + FPGA_SLOW_BOOT = False + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x42800000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3c800000 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1C + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + BOOTLOADER_FLASH_OFFSET = 0x0 + + # Magic value for ESP32C3 eco 1+2 and ESP32C3 eco3 respectivly + CHIP_DETECT_MAGIC_VALUE = [0x6921506f, 0x1b31506f] + + UART_DATE_REG_ADDR = 0x60000000 + 0x7c + + EFUSE_BASE = 0x60008800 + EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044 + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + + EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + + EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY0_SHIFT = 24 + EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY1_SHIFT = 28 + EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY2_SHIFT = 0 + EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY3_SHIFT = 4 + EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY4_SHIFT = 8 + EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY5_SHIFT = 12 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20 + + PURPOSE_VAL_XTS_AES128_KEY = 4 + + GPIO_STRAP_REG = 0x3f404038 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3C000000, 0x3C800000, "DROM"], + [0x3FC80000, 0x3FCE0000, "DRAM"], + [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"], + [0x3FF00000, 0x3FF20000, "DROM_MASK"], + [0x40000000, 0x40060000, "IROM_MASK"], + [0x42000000, 0x42800000, "IROM"], + [0x4037C000, 0x403E0000, "IRAM"], + [0x50000000, 0x50002000, "RTC_IRAM"], + [0x50000000, 0x50002000, "RTC_DRAM"], + [0x600FE000, 0x60100000, "MEM_INTERNAL2"]] + + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return self.get_minor_chip_version() + + def get_pkg_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07 + + def get_minor_chip_version(self): + hi_num_word = 5 + hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01 + low_num_word = 3 + low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07 + return (hi << 3) + low + + def get_major_chip_version(self): + num_word = 5 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03 + + def get_chip_description(self): + chip_name = { + 0: "ESP32-C3", + }.get(self.get_pkg_version(), "unknown ESP32-C3") + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) + + def get_chip_features(self): + return ["Wi-Fi"] + + def get_crystal_freq(self): + # ESP32C3 XTAL is fixed to 40MHz + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-C3") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-C3 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see an AES-128 key + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes) + + +class ESP32H2BETA1ROM(ESP32ROM): + CHIP_NAME = "ESP32-H2(beta1)" + IMAGE_CHIP_ID = 10 + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x42800000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3c800000 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1C + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + BOOTLOADER_FLASH_OFFSET = 0x0 + + CHIP_DETECT_MAGIC_VALUE = [0xca26cc22] + + UART_DATE_REG_ADDR = 0x60000000 + 0x7c + + EFUSE_BASE = 0x6001A000 + EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044 + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + + EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + + EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY0_SHIFT = 24 + EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY1_SHIFT = 28 + EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY2_SHIFT = 0 + EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY3_SHIFT = 4 + EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY4_SHIFT = 8 + EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY5_SHIFT = 12 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20 + + PURPOSE_VAL_XTS_AES128_KEY = 4 + + GPIO_STRAP_REG = 0x3f404038 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + MEMORY_MAP = [] + + FLASH_FREQUENCY = { + '48m': 0xf, + '24m': 0x0, + '16m': 0x1, + '12m': 0x2, + } + + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return 0 + + def get_pkg_version(self): + num_word = 4 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07 + + def get_minor_chip_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x07 + + def get_major_chip_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x03 + + def get_chip_description(self): + chip_name = { + 0: "ESP32-H2", + }.get(self.get_pkg_version(), "unknown ESP32-H2") + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) + + def get_chip_features(self): + return ["BLE/802.15.4"] + + def get_crystal_freq(self): + return 32 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-H2") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-H2 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see an AES-128 key + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes) + + +class ESP32H2BETA2ROM(ESP32H2BETA1ROM): + CHIP_NAME = "ESP32-H2(beta2)" + IMAGE_CHIP_ID = 14 + + def get_chip_description(self): + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (self.CHIP_NAME, major_rev, minor_rev) + + +class ESP32C2ROM(ESP32C3ROM): + CHIP_NAME = "ESP32-C2" + IMAGE_CHIP_ID = 12 + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x42400000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3c400000 + + # Magic value for ESP32C2 ECO0 and ECO1 respectively + CHIP_DETECT_MAGIC_VALUE = [0x6F51306F, 0x7c41a06f] + + EFUSE_BASE = 0x60008800 + EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x040 + MAC_EFUSE_REG = EFUSE_BASE + 0x040 + + FLASH_FREQUENCY = { + '60m': 0xf, + '30m': 0x0, + '20m': 0x1, + '15m': 0x2, + } + + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return self.get_major_chip_version() + + def get_pkg_version(self): + num_word = 1 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 22) & 0x07 + + def get_chip_description(self): + chip_name = { + 0: "ESP32-C2", + 1: "ESP32-C2", + }.get(self.get_pkg_version(), "unknown ESP32-C2") + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) + + def get_minor_chip_version(self): + num_word = 1 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 16) & 0xF + + def get_major_chip_version(self): + num_word = 1 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 20) & 0x3 + + def _post_connect(self): + # ESP32C2 ECO0 is no longer supported by the flasher stub + if self.get_chip_revision() == 0: + self.stub_is_disabled = True + self.IS_STUB = False + + +class ESP32C6BETAROM(ESP32C3ROM): + CHIP_NAME = "ESP32-C6(beta)" + IMAGE_CHIP_ID = 7 + + CHIP_DETECT_MAGIC_VALUE = [0x0da1806f] + + UART_DATE_REG_ADDR = 0x00000500 + + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return 0 + + def get_pkg_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 29) & 0x07 + + def get_minor_chip_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x0F + + def get_major_chip_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 22) & 0x03 + + def get_chip_description(self): + chip_name = { + 0: "ESP32-C6", + }.get(self.get_pkg_version(), "unknown ESP32-C6") + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) + + +class ESP32StubLoader(ESP32ROM): + """ Access class for ESP32 stub loader, runs on top of ROM. + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32ROM.STUB_CLASS = ESP32StubLoader + + +class ESP32S2StubLoader(ESP32S2ROM): + """ Access class for ESP32-S2 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + if rom_loader.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK + + +ESP32S2ROM.STUB_CLASS = ESP32S2StubLoader + + +class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM): + """ Access class for ESP32S3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader + + +class ESP32S3StubLoader(ESP32S3ROM): + """ Access class for ESP32S3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + if rom_loader.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK + + +ESP32S3ROM.STUB_CLASS = ESP32S3StubLoader + + +class ESP32C3StubLoader(ESP32C3ROM): + """ Access class for ESP32C3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader + + +class ESP32H2BETA1StubLoader(ESP32H2BETA1ROM): + """ Access class for ESP32H2BETA1 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32H2BETA1ROM.STUB_CLASS = ESP32H2BETA1StubLoader + + +class ESP32H2BETA2StubLoader(ESP32H2BETA2ROM): + """ Access class for ESP32H2BETA2 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32H2BETA2ROM.STUB_CLASS = ESP32H2BETA2StubLoader + + +class ESP32C2StubLoader(ESP32C2ROM): + """ Access class for ESP32C2 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32C2ROM.STUB_CLASS = ESP32C2StubLoader + + +class ESPBOOTLOADER(object): + """ These are constants related to software ESP8266 bootloader, working with 'v2' image files """ + + # First byte of the "v2" application image + IMAGE_V2_MAGIC = 0xea + + # First 'segment' value in a "v2" application image, appears to be a constant version value? + IMAGE_V2_SEGMENT = 4 + + +def LoadFirmwareImage(chip, filename): + """ Load a firmware image. Can be for any supported SoC. + + ESP8266 images will be examined to determine if they are original ROM firmware images (ESP8266ROMFirmwareImage) + or "v2" OTA bootloader images. + + Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1) or ESP8266V2FirmwareImage (v2). + """ + chip = re.sub(r"[-()]", "", chip.lower()) + with open(filename, 'rb') as f: + if chip == 'esp32': + return ESP32FirmwareImage(f) + elif chip == "esp32s2": + return ESP32S2FirmwareImage(f) + elif chip == "esp32s3beta2": + return ESP32S3BETA2FirmwareImage(f) + elif chip == "esp32s3": + return ESP32S3FirmwareImage(f) + elif chip == 'esp32c3': + return ESP32C3FirmwareImage(f) + elif chip == 'esp32c6beta': + return ESP32C6BETAFirmwareImage(f) + elif chip == 'esp32h2beta1': + return ESP32H2BETA1FirmwareImage(f) + elif chip == 'esp32h2beta2': + return ESP32H2BETA2FirmwareImage(f) + elif chip == 'esp32c2': + return ESP32C2FirmwareImage(f) + else: # Otherwise, ESP8266 so look at magic to determine the image type + magic = ord(f.read(1)) + f.seek(0) + if magic == ESPLoader.ESP_IMAGE_MAGIC: + return ESP8266ROMFirmwareImage(f) + elif magic == ESPBOOTLOADER.IMAGE_V2_MAGIC: + return ESP8266V2FirmwareImage(f) + else: + raise FatalError("Invalid image magic number: %d" % magic) + + +class ImageSegment(object): + """ Wrapper class for a segment in an ESP image + (very similar to a section in an ELFImage also) """ + def __init__(self, addr, data, file_offs=None): + self.addr = addr + self.data = data + self.file_offs = file_offs + self.include_in_checksum = True + if self.addr != 0: + self.pad_to_alignment(4) # pad all "real" ImageSegments 4 byte aligned length + + def copy_with_new_addr(self, new_addr): + """ Return a new ImageSegment with same data, but mapped at + a new address. """ + return ImageSegment(new_addr, self.data, 0) + + def split_image(self, split_len): + """ Return a new ImageSegment which splits "split_len" bytes + from the beginning of the data. Remaining bytes are kept in + this segment object (and the start address is adjusted to match.) """ + result = copy.copy(self) + result.data = self.data[:split_len] + self.data = self.data[split_len:] + self.addr += split_len + self.file_offs = None + result.file_offs = None + return result + + def __repr__(self): + r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr) + if self.file_offs is not None: + r += " file_offs 0x%08x" % (self.file_offs) + return r + + def get_memory_type(self, image): + """ + Return a list describing the memory type(s) that is covered by this + segment's start address. + """ + return [map_range[2] for map_range in image.ROM_LOADER.MEMORY_MAP if map_range[0] <= self.addr < map_range[1]] + + def pad_to_alignment(self, alignment): + self.data = pad_to(self.data, alignment, b'\x00') + + +class ELFSection(ImageSegment): + """ Wrapper class for a section in an ELF image, has a section + name as well as the common properties of an ImageSegment. """ + def __init__(self, name, addr, data): + super(ELFSection, self).__init__(addr, data) + self.name = name.decode("utf-8") + + def __repr__(self): + return "%s %s" % (self.name, super(ELFSection, self).__repr__()) + + +class BaseFirmwareImage(object): + SEG_HEADER_LEN = 8 + SHA256_DIGEST_LEN = 32 + + """ Base class with common firmware image functions """ + def __init__(self): + self.segments = [] + self.entrypoint = 0 + self.elf_sha256 = None + self.elf_sha256_offset = 0 + self.pad_to_size = 0 + + def load_common_header(self, load_file, expected_magic): + (magic, segments, self.flash_mode, self.flash_size_freq, self.entrypoint) = struct.unpack(' 16: + raise FatalError('Invalid segment count %d (max 16). Usually this indicates a linker script problem.' % len(self.segments)) + + def load_segment(self, f, is_irom_segment=False): + """ Load the next segment from the image file """ + file_offs = f.tell() + (offset, size) = struct.unpack(' 0x40200000 or offset < 0x3ffe0000 or size > 65536: + print('WARNING: Suspicious segment 0x%x, length %d' % (offset, size)) + + def maybe_patch_segment_data(self, f, segment_data): + """If SHA256 digest of the ELF file needs to be inserted into this segment, do so. Returns segment data.""" + segment_len = len(segment_data) + file_pos = f.tell() # file_pos is position in the .bin file + if self.elf_sha256_offset >= file_pos and self.elf_sha256_offset < file_pos + segment_len: + # SHA256 digest needs to be patched into this binary segment, + # calculate offset of the digest inside the binary segment. + patch_offset = self.elf_sha256_offset - file_pos + # Sanity checks + if patch_offset < self.SEG_HEADER_LEN or patch_offset + self.SHA256_DIGEST_LEN > segment_len: + raise FatalError('Cannot place SHA256 digest on segment boundary' + '(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)' % + (self.elf_sha256_offset, file_pos, segment_len)) + # offset relative to the data part + patch_offset -= self.SEG_HEADER_LEN + if segment_data[patch_offset:patch_offset + self.SHA256_DIGEST_LEN] != b'\x00' * self.SHA256_DIGEST_LEN: + raise FatalError('Contents of segment at SHA256 digest offset 0x%x are not all zero. Refusing to overwrite.' % + self.elf_sha256_offset) + assert len(self.elf_sha256) == self.SHA256_DIGEST_LEN + segment_data = segment_data[0:patch_offset] + self.elf_sha256 + \ + segment_data[patch_offset + self.SHA256_DIGEST_LEN:] + return segment_data + + def save_segment(self, f, segment, checksum=None): + """ Save the next segment to the image file, return next checksum value if provided """ + segment_data = self.maybe_patch_segment_data(f, segment.data) + f.write(struct.pack(' 0: + if len(irom_segments) != 1: + raise FatalError('Found %d segments that could be irom0. Bad ELF file?' % len(irom_segments)) + return irom_segments[0] + return None + + def get_non_irom_segments(self): + irom_segment = self.get_irom_segment() + return [s for s in self.segments if s != irom_segment] + + def merge_adjacent_segments(self): + if not self.segments: + return # nothing to merge + + segments = [] + # The easiest way to merge the sections is the browse them backward. + for i in range(len(self.segments) - 1, 0, -1): + # elem is the previous section, the one `next_elem` may need to be + # merged in + elem = self.segments[i - 1] + next_elem = self.segments[i] + if all((elem.get_memory_type(self) == next_elem.get_memory_type(self), + elem.include_in_checksum == next_elem.include_in_checksum, + next_elem.addr == elem.addr + len(elem.data))): + # Merge any segment that ends where the next one starts, without spanning memory types + # + # (don't 'pad' any gaps here as they may be excluded from the image due to 'noinit' + # or other reasons.) + elem.data += next_elem.data + else: + # The section next_elem cannot be merged into the previous one, + # which means it needs to be part of the final segments. + # As we are browsing the list backward, the elements need to be + # inserted at the beginning of the final list. + segments.insert(0, next_elem) + + # The first segment will always be here as it cannot be merged into any + # "previous" section. + segments.insert(0, self.segments[0]) + + # note: we could sort segments here as well, but the ordering of segments is sometimes + # important for other reasons (like embedded ELF SHA-256), so we assume that the linker + # script will have produced any adjacent sections in linear order in the ELF, anyhow. + self.segments = segments + + def set_mmu_page_size(self, size): + """ If supported, this should be overridden by the chip-specific class. Gets called in elf2image. """ + print('WARNING: Changing MMU page size is not supported on {}! Defaulting to 64KB.'.format(self.ROM_LOADER.CHIP_NAME)) + + +class ESP8266ROMFirmwareImage(BaseFirmwareImage): + """ 'Version 1' firmware image, segments loaded directly by the ROM bootloader. """ + + ROM_LOADER = ESP8266ROM + + def __init__(self, load_file=None): + super(ESP8266ROMFirmwareImage, self).__init__() + self.flash_mode = 0 + self.flash_size_freq = 0 + self.version = 1 + + if load_file is not None: + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) + + for _ in range(segments): + self.load_segment(load_file) + self.checksum = self.read_checksum(load_file) + + self.verify() + + def default_output_name(self, input_file): + """ Derive a default output name from the ELF name. """ + return input_file + '-' + + def save(self, basename): + """ Save a set of V1 images for flashing. Parameter is a base filename. """ + # IROM data goes in its own plain binary file + irom_segment = self.get_irom_segment() + if irom_segment is not None: + with open("%s0x%05x.bin" % (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START), "wb") as f: + f.write(irom_segment.data) + + # everything but IROM goes at 0x00000 in an image file + normal_segments = self.get_non_irom_segments() + with open("%s0x00000.bin" % basename, 'wb') as f: + self.write_common_header(f, normal_segments) + checksum = ESPLoader.ESP_CHECKSUM_MAGIC + for segment in normal_segments: + checksum = self.save_segment(f, segment, checksum) + self.append_checksum(f, checksum) + + +ESP8266ROM.BOOTLOADER_IMAGE = ESP8266ROMFirmwareImage + + +class ESP8266V2FirmwareImage(BaseFirmwareImage): + """ 'Version 2' firmware image, segments loaded by software bootloader stub + (ie Espressif bootloader or rboot) + """ + + ROM_LOADER = ESP8266ROM + + def __init__(self, load_file=None): + super(ESP8266V2FirmwareImage, self).__init__() + self.version = 2 + if load_file is not None: + segments = self.load_common_header(load_file, ESPBOOTLOADER.IMAGE_V2_MAGIC) + if segments != ESPBOOTLOADER.IMAGE_V2_SEGMENT: + # segment count is not really segment count here, but we expect to see '4' + print('Warning: V2 header has unexpected "segment" count %d (usually 4)' % segments) + + # irom segment comes before the second header + # + # the file is saved in the image with a zero load address + # in the header, so we need to calculate a load address + irom_segment = self.load_segment(load_file, True) + irom_segment.addr = 0 # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_addr + 8 + irom_segment.include_in_checksum = False + + first_flash_mode = self.flash_mode + first_flash_size_freq = self.flash_size_freq + first_entrypoint = self.entrypoint + # load the second header + + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) + + if first_flash_mode != self.flash_mode: + print('WARNING: Flash mode value in first header (0x%02x) disagrees with second (0x%02x). Using second value.' + % (first_flash_mode, self.flash_mode)) + if first_flash_size_freq != self.flash_size_freq: + print('WARNING: Flash size/freq value in first header (0x%02x) disagrees with second (0x%02x). Using second value.' + % (first_flash_size_freq, self.flash_size_freq)) + if first_entrypoint != self.entrypoint: + print('WARNING: Entrypoint address in first header (0x%08x) disagrees with second header (0x%08x). Using second value.' + % (first_entrypoint, self.entrypoint)) + + # load all the usual segments + for _ in range(segments): + self.load_segment(load_file) + self.checksum = self.read_checksum(load_file) + + self.verify() + + def default_output_name(self, input_file): + """ Derive a default output name from the ELF name. """ + irom_segment = self.get_irom_segment() + if irom_segment is not None: + irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START + else: + irom_offs = 0 + return "%s-0x%05x.bin" % (os.path.splitext(input_file)[0], + irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) + + def save(self, filename): + with open(filename, 'wb') as f: + # Save first header for irom0 segment + f.write(struct.pack(b' 0: + last_addr = flash_segments[0].addr + for segment in flash_segments[1:]: + if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: + raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " + "Can't generate binary. Suggest changing linker script or ELF to merge sections.") % + (segment.addr, last_addr)) + last_addr = segment.addr + + def get_alignment_data_needed(segment): + # Actual alignment (in data bytes) required for a segment header: positioned so that + # after we write the next 8 byte header, file_offs % IROM_ALIGN == segment.addr % IROM_ALIGN + # + # (this is because the segment's vaddr may not be IROM_ALIGNed, more likely is aligned + # IROM_ALIGN+0x18 to account for the binary file header + align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN + pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past + if pad_len == 0 or pad_len == self.IROM_ALIGN: + return 0 # already aligned + + # subtract SEG_HEADER_LEN a second time, as the padding block has a header as well + pad_len -= self.SEG_HEADER_LEN + if pad_len < 0: + pad_len += self.IROM_ALIGN + return pad_len + + # try to fit each flash segment on a 64kB aligned boundary + # by padding with parts of the non-flash segments... + while len(flash_segments) > 0: + segment = flash_segments[0] + pad_len = get_alignment_data_needed(segment) + if pad_len > 0: # need to pad + if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN: + pad_segment = ram_segments[0].split_image(pad_len) + if len(ram_segments[0].data) == 0: + ram_segments.pop(0) + else: + pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + else: + # write the flash segment + assert (f.tell() + 8) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN + checksum = self.save_flash_segment(f, segment, checksum) + flash_segments.pop(0) + total_segments += 1 + + # flash segments all written, so write any remaining RAM segments + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + + if self.secure_pad: + # pad the image so that after signing it will end on a a 64KB boundary. + # This ensures all mapped flash content will be verified. + if not self.append_digest: + raise FatalError("secure_pad only applies if a SHA-256 digest is also appended to the image") + align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN + # 16 byte aligned checksum (force the alignment to simplify calculations) + checksum_space = 16 + if self.secure_pad == '1': + # after checksum: SHA-256 digest + (to be added by signing process) version, signature + 12 trailing bytes due to alignment + space_after_checksum = 32 + 4 + 64 + 12 + elif self.secure_pad == '2': # Secure Boot V2 + # after checksum: SHA-256 digest + signature sector, but we place signature sector after the 64KB boundary + space_after_checksum = 32 + pad_len = (self.IROM_ALIGN - align_past - checksum_space - space_after_checksum) % self.IROM_ALIGN + pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) + + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + + # done writing segments + self.append_checksum(f, checksum) + image_length = f.tell() + + if self.secure_pad: + assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0 + + # kinda hacky: go back to the initial header and write the new segment count + # that includes padding segments. This header is not checksummed + f.seek(1) + try: + f.write(chr(total_segments)) + except TypeError: # Python 3 + f.write(bytes([total_segments])) + + if self.append_digest: + # calculate the SHA256 of the whole file and append it + f.seek(0) + digest = hashlib.sha256() + digest.update(f.read(image_length)) + f.write(digest.digest()) + + if self.pad_to_size: + image_length = f.tell() + if image_length % self.pad_to_size != 0: + pad_by = self.pad_to_size - (image_length % self.pad_to_size) + f.write(b"\xff" * pad_by) + + with open(filename, 'wb') as real_file: + real_file.write(f.getvalue()) + + def save_flash_segment(self, f, segment, checksum=None): + """ Save the next segment to the image file, return next checksum value if provided """ + segment_end_pos = f.tell() + len(segment.data) + self.SEG_HEADER_LEN + segment_len_remainder = segment_end_pos % self.IROM_ALIGN + if segment_len_remainder < 0x24: + # Work around a bug in ESP-IDF 2nd stage bootloader, that it didn't map the + # last MMU page, if an IROM/DROM segment was < 0x24 bytes over the page boundary. + segment.data += b'\x00' * (0x24 - segment_len_remainder) + return self.save_segment(f, segment, checksum) + + def load_extended_header(self, load_file): + def split_byte(n): + return (n & 0x0F, (n >> 4) & 0x0F) + + fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) + + self.wp_pin = fields[0] + + # SPI pin drive stengths are two per byte + self.clk_drv, self.q_drv = split_byte(fields[1]) + self.d_drv, self.cs_drv = split_byte(fields[2]) + self.hd_drv, self.wp_drv = split_byte(fields[3]) + + chip_id = fields[4] + if chip_id != self.ROM_LOADER.IMAGE_CHIP_ID: + print(("Unexpected chip id in image. Expected %d but value was %d. " + "Is this image for a different chip model?") % (self.ROM_LOADER.IMAGE_CHIP_ID, chip_id)) + + self.min_rev = fields[5] + self.min_rev_full = fields[6] + self.max_rev_full = fields[7] + + # reserved fields in the middle should all be zero + if any(f for f in fields[8:-1] if f != 0): + print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?") + + append_digest = fields[-1] # last byte is append_digest + if append_digest in [0, 1]: + self.append_digest = (append_digest == 1) + else: + raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", append_digest) + + def save_extended_header(self, save_file): + def join_byte(ln, hn): + return (ln & 0x0F) + ((hn & 0x0F) << 4) + + append_digest = 1 if self.append_digest else 0 + + fields = [self.wp_pin, + join_byte(self.clk_drv, self.q_drv), + join_byte(self.d_drv, self.cs_drv), + join_byte(self.hd_drv, self.wp_drv), + self.ROM_LOADER.IMAGE_CHIP_ID, + self.min_rev, + self.min_rev_full, + self.max_rev_full] + fields += [0] * 4 # padding + fields += [append_digest] + + packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields) + save_file.write(packed) + + +class ESP8266V3FirmwareImage(ESP32FirmwareImage): + """ ESP8266 V3 firmware image is very similar to ESP32 image + """ + + EXTENDED_HEADER_STRUCT_FMT = "B" * 16 + + def is_flash_addr(self, addr): + return (addr > ESP8266ROM.IROM_MAP_START) + + def save(self, filename): + total_segments = 0 + with io.BytesIO() as f: # write file to memory first + self.write_common_header(f, self.segments) + + checksum = ESPLoader.ESP_CHECKSUM_MAGIC + + # split segments into flash-mapped vs ram-loaded, and take copies so we can mutate them + flash_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if self.is_flash_addr(s.addr) and len(s.data)] + ram_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if not self.is_flash_addr(s.addr) and len(s.data)] + + # check for multiple ELF sections that are mapped in the same flash mapping region. + # this is usually a sign of a broken linker script, but if you have a legitimate + # use case then let us know + if len(flash_segments) > 0: + last_addr = flash_segments[0].addr + for segment in flash_segments[1:]: + if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: + raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " + "Can't generate binary. Suggest changing linker script or ELF to merge sections.") % + (segment.addr, last_addr)) + last_addr = segment.addr + + # try to fit each flash segment on a 64kB aligned boundary + # by padding with parts of the non-flash segments... + while len(flash_segments) > 0: + segment = flash_segments[0] + # remove 8 bytes empty data for insert segment header + if segment.name == '.flash.rodata': + segment.data = segment.data[8:] + # write the flash segment + checksum = self.save_segment(f, segment, checksum) + flash_segments.pop(0) + total_segments += 1 + + # flash segments all written, so write any remaining RAM segments + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + + # done writing segments + self.append_checksum(f, checksum) + image_length = f.tell() + + # kinda hacky: go back to the initial header and write the new segment count + # that includes padding segments. This header is not checksummed + f.seek(1) + try: + f.write(chr(total_segments)) + except TypeError: # Python 3 + f.write(bytes([total_segments])) + + if self.append_digest: + # calculate the SHA256 of the whole file and append it + f.seek(0) + digest = hashlib.sha256() + digest.update(f.read(image_length)) + f.write(digest.digest()) + + with open(filename, 'wb') as real_file: + real_file.write(f.getvalue()) + + def load_extended_header(self, load_file): + def split_byte(n): + return (n & 0x0F, (n >> 4) & 0x0F) + + fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) + + self.wp_pin = fields[0] + + # SPI pin drive stengths are two per byte + self.clk_drv, self.q_drv = split_byte(fields[1]) + self.d_drv, self.cs_drv = split_byte(fields[2]) + self.hd_drv, self.wp_drv = split_byte(fields[3]) + + if fields[15] in [0, 1]: + self.append_digest = (fields[15] == 1) + else: + raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", fields[15]) + + # remaining fields in the middle should all be zero + if any(f for f in fields[4:15] if f != 0): + print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?") + + +ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage + + +class ESP32S2FirmwareImage(ESP32FirmwareImage): + """ ESP32S2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S2ROM + + +ESP32S2ROM.BOOTLOADER_IMAGE = ESP32S2FirmwareImage + + +class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage): + """ ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S3BETA2ROM + + +ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage + + +class ESP32S3FirmwareImage(ESP32FirmwareImage): + """ ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S3ROM + + +ESP32S3ROM.BOOTLOADER_IMAGE = ESP32S3FirmwareImage + + +class ESP32C3FirmwareImage(ESP32FirmwareImage): + """ ESP32C3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32C3ROM + + +ESP32C3ROM.BOOTLOADER_IMAGE = ESP32C3FirmwareImage + + +class ESP32C6BETAFirmwareImage(ESP32FirmwareImage): + """ ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32C6BETAROM + + +ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage + + +class ESP32H2BETA1FirmwareImage(ESP32FirmwareImage): + """ ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32H2BETA1ROM + + +ESP32H2BETA1ROM.BOOTLOADER_IMAGE = ESP32H2BETA1FirmwareImage + + +class ESP32H2BETA2FirmwareImage(ESP32FirmwareImage): + """ ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32H2BETA2ROM + + +ESP32H2BETA2ROM.BOOTLOADER_IMAGE = ESP32H2BETA2FirmwareImage + + +class ESP32C2FirmwareImage(ESP32FirmwareImage): + """ ESP32C2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32C2ROM + + def set_mmu_page_size(self, size): + if size not in [16384, 32768, 65536]: + raise FatalError("{} is not a valid page size.".format(size)) + self.IROM_ALIGN = size + + +ESP32C2ROM.BOOTLOADER_IMAGE = ESP32C2FirmwareImage + + +class ELFFile(object): + SEC_TYPE_PROGBITS = 0x01 + SEC_TYPE_STRTAB = 0x03 + SEC_TYPE_INITARRAY = 0x0e + SEC_TYPE_FINIARRAY = 0x0f + + PROG_SEC_TYPES = (SEC_TYPE_PROGBITS, SEC_TYPE_INITARRAY, SEC_TYPE_FINIARRAY) + + LEN_SEC_HEADER = 0x28 + + SEG_TYPE_LOAD = 0x01 + LEN_SEG_HEADER = 0x20 + + def __init__(self, name): + # Load sections from the ELF file + self.name = name + with open(self.name, 'rb') as f: + self._read_elf_file(f) + + def get_section(self, section_name): + for s in self.sections: + if s.name == section_name: + return s + raise ValueError("No section %s in ELF file" % section_name) + + def _read_elf_file(self, f): + # read the ELF file header + LEN_FILE_HEADER = 0x34 + try: + (ident, _type, machine, _version, + self.entrypoint, _phoff, shoff, _flags, + _ehsize, _phentsize, _phnum, shentsize, + shnum, shstrndx) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) + except struct.error as e: + raise FatalError("Failed to read a valid ELF header from %s: %s" % (self.name, e)) + + if byte(ident, 0) != 0x7f or ident[1:4] != b'ELF': + raise FatalError("%s has invalid ELF magic header" % self.name) + if machine not in [0x5e, 0xf3]: + raise FatalError("%s does not appear to be an Xtensa or an RISCV ELF file. e_machine=%04x" % (self.name, machine)) + if shentsize != self.LEN_SEC_HEADER: + raise FatalError("%s has unexpected section header entry size 0x%x (not 0x%x)" % (self.name, shentsize, self.LEN_SEC_HEADER)) + if shnum == 0: + raise FatalError("%s has 0 section headers" % (self.name)) + self._read_sections(f, shoff, shnum, shstrndx) + self._read_segments(f, _phoff, _phnum, shstrndx) + + def _read_sections(self, f, section_header_offs, section_header_count, shstrndx): + f.seek(section_header_offs) + len_bytes = section_header_count * self.LEN_SEC_HEADER + section_header = f.read(len_bytes) + if len(section_header) == 0: + raise FatalError("No section header found at offset %04x in ELF file." % section_header_offs) + if len(section_header) != (len_bytes): + raise FatalError("Only read 0x%x bytes from section header (expected 0x%x.) Truncated ELF file?" % (len(section_header), len_bytes)) + + # walk through the section header and extract all sections + section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER) + + def read_section_header(offs): + name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from(" 0] + self.sections = prog_sections + + def _read_segments(self, f, segment_header_offs, segment_header_count, shstrndx): + f.seek(segment_header_offs) + len_bytes = segment_header_count * self.LEN_SEG_HEADER + segment_header = f.read(len_bytes) + if len(segment_header) == 0: + raise FatalError("No segment header found at offset %04x in ELF file." % segment_header_offs) + if len(segment_header) != (len_bytes): + raise FatalError("Only read 0x%x bytes from segment header (expected 0x%x.) Truncated ELF file?" % (len(segment_header), len_bytes)) + + # walk through the segment header and extract all segments + segment_header_offsets = range(0, len(segment_header), self.LEN_SEG_HEADER) + + def read_segment_header(offs): + seg_type, seg_offs, _vaddr, lma, size, _memsize, _flags, _align = struct.unpack_from(" 0] + self.segments = prog_segments + + def sha256(self): + # return SHA256 hash of the input ELF file + sha256 = hashlib.sha256() + with open(self.name, 'rb') as f: + sha256.update(f.read()) + return sha256.digest() + + +def slip_reader(port, trace_function): + """Generator to read SLIP packets from a serial port. + Yields one full SLIP packet at a time, raises exception on timeout or invalid data. + + Designed to avoid too many calls to serial.read(1), which can bog + down on slow systems. + """ + partial_packet = None + in_escape = False + successful_slip = False + while True: + waiting = port.inWaiting() + read_bytes = port.read(1 if waiting == 0 else waiting) + if read_bytes == b'': + if partial_packet is None: # fail due to no data + msg = "Serial data stream stopped: Possible serial noise or corruption." if successful_slip else "No serial data received." + else: # fail during packet transfer + msg = "Packet content transfer stopped (received {} bytes)".format(len(partial_packet)) + trace_function(msg) + raise FatalError(msg) + trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes)) + for b in read_bytes: + if type(b) is int: + b = bytes([b]) # python 2/3 compat + + if partial_packet is None: # waiting for packet header + if b == b'\xc0': + partial_packet = b"" + else: + trace_function("Read invalid data: %s", HexFormatter(read_bytes)) + trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting()))) + raise FatalError('Invalid head of packet (0x%s): Possible serial noise or corruption.' % hexify(b)) + elif in_escape: # part-way through escape sequence + in_escape = False + if b == b'\xdc': + partial_packet += b'\xc0' + elif b == b'\xdd': + partial_packet += b'\xdb' + else: + trace_function("Read invalid data: %s", HexFormatter(read_bytes)) + trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting()))) + raise FatalError('Invalid SLIP escape (0xdb, 0x%s)' % (hexify(b))) + elif b == b'\xdb': # start of escape sequence + in_escape = True + elif b == b'\xc0': # end of packet + trace_function("Received full packet: %s", HexFormatter(partial_packet)) + yield partial_packet + partial_packet = None + successful_slip = True + else: # normal byte in packet + partial_packet += b + + +def arg_auto_int(x): + return int(x, 0) + + +def format_chip_name(c): + """ Normalize chip name from user input """ + c = c.lower().replace('-', '') + if c == 'esp8684': # TODO: Delete alias, ESPTOOL-389 + print('WARNING: Chip name ESP8684 is deprecated in favor of ESP32-C2 and will be removed in a future release. Using ESP32-C2 instead.') + return 'esp32c2' + return c + + +def div_roundup(a, b): + """ Return a/b rounded up to nearest integer, + equivalent result to int(math.ceil(float(int(a)) / float(int(b))), only + without possible floating point accuracy errors. + """ + return (int(a) + int(b) - 1) // int(b) + + +def align_file_position(f, size): + """ Align the position in the file to the next block of specified size """ + align = (size - 1) - (f.tell() % size) + f.seek(align, 1) + + +def flash_size_bytes(size): + """ Given a flash size of the type passed in args.flash_size + (ie 512KB or 1MB) then return the size in bytes. + """ + if "MB" in size: + return int(size[:size.index("MB")]) * 1024 * 1024 + elif "KB" in size: + return int(size[:size.index("KB")]) * 1024 + else: + raise FatalError("Unknown size %s" % size) + + +def hexify(s, uppercase=True): + format_str = '%02X' if uppercase else '%02x' + if not PYTHON2: + return ''.join(format_str % c for c in s) + else: + return ''.join(format_str % ord(c) for c in s) + + +class HexFormatter(object): + """ + Wrapper class which takes binary data in its constructor + and returns a hex string as it's __str__ method. + + This is intended for "lazy formatting" of trace() output + in hex format. Avoids overhead (significant on slow computers) + of generating long hex strings even if tracing is disabled. + + Note that this doesn't save any overhead if passed as an + argument to "%", only when passed to trace() + + If auto_split is set (default), any long line (> 16 bytes) will be + printed as separately indented lines, with ASCII decoding at the end + of each line. + """ + def __init__(self, binary_string, auto_split=True): + self._s = binary_string + self._auto_split = auto_split + + def __str__(self): + if self._auto_split and len(self._s) > 16: + result = "" + s = self._s + while len(s) > 0: + line = s[:16] + ascii_line = "".join(c if (c == ' ' or (c in string.printable and c not in string.whitespace)) + else '.' for c in line.decode('ascii', 'replace')) + s = s[16:] + result += "\n %-16s %-16s | %s" % (hexify(line[:8], False), hexify(line[8:], False), ascii_line) + return result + else: + return hexify(self._s, False) + + +def pad_to(data, alignment, pad_character=b'\xFF'): + """ Pad to the next alignment boundary """ + pad_mod = len(data) % alignment + if pad_mod != 0: + data += pad_character * (alignment - pad_mod) + return data + + +class FatalError(RuntimeError): + """ + Wrapper class for runtime errors that aren't caused by internal bugs, but by + ESP ROM responses or input content. + """ + def __init__(self, message): + RuntimeError.__init__(self, message) + + @staticmethod + def WithResult(message, result): + """ + Return a fatal error object that appends the hex values of + 'result' and its meaning as a string formatted argument. + """ + + err_defs = { + 0x101: 'Out of memory', + 0x102: 'Invalid argument', + 0x103: 'Invalid state', + 0x104: 'Invalid size', + 0x105: 'Requested resource not found', + 0x106: 'Operation or feature not supported', + 0x107: 'Operation timed out', + 0x108: 'Received response was invalid', + 0x109: 'CRC or checksum was invalid', + 0x10A: 'Version was invalid', + 0x10B: 'MAC address was invalid', + # Flasher stub error codes + 0xC000: 'Bad data length', + 0xC100: 'Bad data checksum', + 0xC200: 'Bad blocksize', + 0xC300: 'Invalid command', + 0xC400: 'Failed SPI operation', + 0xC500: 'Failed SPI unlock', + 0xC600: 'Not in flash mode', + 0xC700: 'Inflate error', + 0xC800: 'Not enough data', + 0xC900: 'Too much data', + 0xFF00: 'Command not implemented', + } + + err_code = struct.unpack(">H", result[:2]) + message += " (result was {}: {})".format(hexify(result), err_defs.get(err_code[0], 'Unknown result')) + return FatalError(message) + + +class NotImplementedInROMError(FatalError): + """ + Wrapper class for the error thrown when a particular ESP bootloader function + is not implemented in the ROM bootloader. + """ + def __init__(self, bootloader, func): + FatalError.__init__(self, "%s ROM does not support function %s." % (bootloader.CHIP_NAME, func.__name__)) + + +class NotSupportedError(FatalError): + def __init__(self, esp, function_name): + FatalError.__init__(self, "Function %s is not supported for %s." % (function_name, esp.CHIP_NAME)) + +# "Operation" commands, executable at command line. One function each +# +# Each function takes either two args (, ) or a single +# argument. + + +class UnsupportedCommandError(RuntimeError): + """ + Wrapper class for when ROM loader returns an invalid command response. + + Usually this indicates the loader is running in Secure Download Mode. + """ + def __init__(self, esp, op): + if esp.secure_download_mode: + msg = "This command (0x%x) is not supported in Secure Download Mode" % op + else: + msg = "Invalid (unsupported) command 0x%x" % op + RuntimeError.__init__(self, msg) + + +def load_ram(esp, args): + image = LoadFirmwareImage(esp.CHIP_NAME, args.filename) + + print('RAM boot...') + for seg in image.segments: + size = len(seg.data) + print('Downloading %d bytes at %08x...' % (size, seg.addr), end=' ') + sys.stdout.flush() + esp.mem_begin(size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, seg.addr) + + seq = 0 + while len(seg.data) > 0: + esp.mem_block(seg.data[0:esp.ESP_RAM_BLOCK], seq) + seg.data = seg.data[esp.ESP_RAM_BLOCK:] + seq += 1 + print('done!') + + print('All segments done, executing at %08x' % image.entrypoint) + esp.mem_finish(image.entrypoint) + + +def read_mem(esp, args): + print('0x%08x = 0x%08x' % (args.address, esp.read_reg(args.address))) + + +def write_mem(esp, args): + esp.write_reg(args.address, args.value, args.mask, 0) + print('Wrote %08x, mask %08x to %08x' % (args.value, args.mask, args.address)) + + +def dump_mem(esp, args): + with open(args.filename, 'wb') as f: + for i in range(args.size // 4): + d = esp.read_reg(args.address + (i * 4)) + f.write(struct.pack(b'> 16 + args.flash_size = DETECTED_FLASH_SIZES.get(size_id) + if args.flash_size is None: + print('Warning: Could not auto-detect Flash size (FlashID=0x%x, SizeID=0x%x), defaulting to 4MB' % (flash_id, size_id)) + args.flash_size = '4MB' + else: + print('Auto-detected Flash size:', args.flash_size) + + +def _update_image_flash_params(esp, address, args, image): + """ Modify the flash mode & size bytes if this looks like an executable bootloader image """ + if len(image) < 8: + return image # not long enough to be a bootloader image + + # unpack the (potential) image header + magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4]) + if address != esp.BOOTLOADER_FLASH_OFFSET: + return image # not flashing bootloader offset, so don't modify this + + if (args.flash_mode, args.flash_freq, args.flash_size) == ('keep',) * 3: + return image # all settings are 'keep', not modifying anything + + # easy check if this is an image: does it start with a magic byte? + if magic != esp.ESP_IMAGE_MAGIC: + print("Warning: Image file at 0x%x doesn't look like an image file, so not changing any flash settings." % address) + return image + + # make sure this really is an image, and not just data that + # starts with esp.ESP_IMAGE_MAGIC (mostly a problem for encrypted + # images that happen to start with a magic byte + try: + test_image = esp.BOOTLOADER_IMAGE(io.BytesIO(image)) + test_image.verify() + except Exception: + print("Warning: Image file at 0x%x is not a valid %s image, so not changing any flash settings." % (address, esp.CHIP_NAME)) + return image + + if args.flash_mode != 'keep': + flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] + + flash_freq = flash_size_freq & 0x0F + if args.flash_freq != 'keep': + flash_freq = esp.parse_flash_freq_arg(args.flash_freq) + + flash_size = flash_size_freq & 0xF0 + if args.flash_size != 'keep': + flash_size = esp.parse_flash_size_arg(args.flash_size) + + flash_params = struct.pack(b'BB', flash_mode, flash_size + flash_freq) + if flash_params != image[2:4]: + print('Flash params set to 0x%04x' % struct.unpack(">H", flash_params)) + image = image[0:2] + flash_params + image[4:] + return image + + +def write_flash(esp, args): + # set args.compress based on default behaviour: + # -> if either --compress or --no-compress is set, honour that + # -> otherwise, set --compress unless --no-stub is set + if args.compress is None and not args.no_compress: + args.compress = not args.no_stub + + # In case we have encrypted files to write, we first do few sanity checks before actual flash + if args.encrypt or args.encrypt_files is not None: + do_write = True + + if not esp.secure_download_mode: + if esp.get_encrypted_download_disabled(): + raise FatalError("This chip has encrypt functionality in UART download mode disabled. " + "This is the Flash Encryption configuration for Production mode instead of Development mode.") + + crypt_cfg_efuse = esp.get_flash_crypt_config() + + if crypt_cfg_efuse is not None and crypt_cfg_efuse != 0xF: + print('Unexpected FLASH_CRYPT_CONFIG value: 0x%x' % (crypt_cfg_efuse)) + do_write = False + + enc_key_valid = esp.is_flash_encryption_key_valid() + + if not enc_key_valid: + print('Flash encryption key is not programmed') + do_write = False + + # Determine which files list contain the ones to encrypt + files_to_encrypt = args.addr_filename if args.encrypt else args.encrypt_files + + for address, argfile in files_to_encrypt: + if address % esp.FLASH_ENCRYPTED_WRITE_ALIGN: + print("File %s address 0x%x is not %d byte aligned, can't flash encrypted" % + (argfile.name, address, esp.FLASH_ENCRYPTED_WRITE_ALIGN)) + do_write = False + + if not do_write and not args.ignore_flash_encryption_efuse_setting: + raise FatalError("Can't perform encrypted flash write, consult Flash Encryption documentation for more information") + + # verify file sizes fit in flash + if args.flash_size != 'keep': # TODO: check this even with 'keep' + flash_end = flash_size_bytes(args.flash_size) + for address, argfile in args.addr_filename: + argfile.seek(0, os.SEEK_END) + if address + argfile.tell() > flash_end: + raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. " + "Use --flash_size argument, or change flashing address.") + % (argfile.name, argfile.tell(), address, flash_end)) + argfile.seek(0) + + if args.erase_all: + erase_flash(esp, args) + else: + for address, argfile in args.addr_filename: + argfile.seek(0, os.SEEK_END) + write_end = address + argfile.tell() + argfile.seek(0) + bytes_over = address % esp.FLASH_SECTOR_SIZE + if bytes_over != 0: + print("WARNING: Flash address {:#010x} is not aligned to a {:#x} byte flash sector. " + "{:#x} bytes before this address will be erased." + .format(address, esp.FLASH_SECTOR_SIZE, bytes_over)) + # Print the address range of to-be-erased flash memory region + print("Flash will be erased from {:#010x} to {:#010x}..." + .format(address - bytes_over, div_roundup(write_end, esp.FLASH_SECTOR_SIZE) * esp.FLASH_SECTOR_SIZE - 1)) + + """ Create a list describing all the files we have to flash. Each entry holds an "encrypt" flag + marking whether the file needs encryption or not. This list needs to be sorted. + + First, append to each entry of our addr_filename list the flag args.encrypt + For example, if addr_filename is [(0x1000, "partition.bin"), (0x8000, "bootloader")], + all_files will be [(0x1000, "partition.bin", args.encrypt), (0x8000, "bootloader", args.encrypt)], + where, of course, args.encrypt is either True or False + """ + all_files = [(offs, filename, args.encrypt) for (offs, filename) in args.addr_filename] + + """Now do the same with encrypt_files list, if defined. + In this case, the flag is True + """ + if args.encrypt_files is not None: + encrypted_files_flag = [(offs, filename, True) for (offs, filename) in args.encrypt_files] + + # Concatenate both lists and sort them. + # As both list are already sorted, we could simply do a merge instead, + # but for the sake of simplicity and because the lists are very small, + # let's use sorted. + all_files = sorted(all_files + encrypted_files_flag, key=lambda x: x[0]) + + for address, argfile, encrypted in all_files: + compress = args.compress + + # Check whether we can compress the current file before flashing + if compress and encrypted: + print('\nWARNING: - compress and encrypt options are mutually exclusive ') + print('Will flash %s uncompressed' % argfile.name) + compress = False + + if args.no_stub: + print('Erasing flash...') + image = pad_to(argfile.read(), esp.FLASH_ENCRYPTED_WRITE_ALIGN if encrypted else 4) + if len(image) == 0: + print('WARNING: File %s is empty' % argfile.name) + continue + image = _update_image_flash_params(esp, address, args, image) + calcmd5 = hashlib.md5(image).hexdigest() + uncsize = len(image) + if compress: + uncimage = image + image = zlib.compress(uncimage, 9) + # Decompress the compressed binary a block at a time, to dynamically calculate the + # timeout based on the real write size + decompress = zlib.decompressobj() + blocks = esp.flash_defl_begin(uncsize, len(image), address) + else: + blocks = esp.flash_begin(uncsize, address, begin_rom_encrypted=encrypted) + argfile.seek(0) # in case we need it again + seq = 0 + bytes_sent = 0 # bytes sent on wire + bytes_written = 0 # bytes written to flash + t = time.time() + + timeout = DEFAULT_TIMEOUT + + while len(image) > 0: + print_overwrite('Writing at 0x%08x... (%d %%)' % (address + bytes_written, 100 * (seq + 1) // blocks)) + sys.stdout.flush() + block = image[0:esp.FLASH_WRITE_SIZE] + if compress: + # feeding each compressed block into the decompressor lets us see block-by-block how much will be written + block_uncompressed = len(decompress.decompress(block)) + bytes_written += block_uncompressed + block_timeout = max(DEFAULT_TIMEOUT, timeout_per_mb(ERASE_WRITE_TIMEOUT_PER_MB, block_uncompressed)) + if not esp.IS_STUB: + timeout = block_timeout # ROM code writes block to flash before ACKing + esp.flash_defl_block(block, seq, timeout=timeout) + if esp.IS_STUB: + timeout = block_timeout # Stub ACKs when block is received, then writes to flash while receiving the block after it + else: + # Pad the last block + block = block + b'\xff' * (esp.FLASH_WRITE_SIZE - len(block)) + if encrypted: + esp.flash_encrypt_block(block, seq) + else: + esp.flash_block(block, seq) + bytes_written += len(block) + bytes_sent += len(block) + image = image[esp.FLASH_WRITE_SIZE:] + seq += 1 + + if esp.IS_STUB: + # Stub only writes each block to flash after 'ack'ing the receive, so do a final dummy operation which will + # not be 'ack'ed until the last block has actually been written out to flash + esp.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR, timeout=timeout) + + t = time.time() - t + speed_msg = "" + if compress: + if t > 0.0: + speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000) + print_overwrite('Wrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s...' % (uncsize, + bytes_sent, + address, t, speed_msg), last_line=True) + else: + if t > 0.0: + speed_msg = " (%.1f kbit/s)" % (bytes_written / t * 8 / 1000) + print_overwrite('Wrote %d bytes at 0x%08x in %.1f seconds%s...' % (bytes_written, address, t, speed_msg), last_line=True) + + if not encrypted and not esp.secure_download_mode: + try: + res = esp.flash_md5sum(address, uncsize) + if res != calcmd5: + print('File md5: %s' % calcmd5) + print('Flash md5: %s' % res) + print('MD5 of 0xFF is %s' % (hashlib.md5(b'\xFF' * uncsize).hexdigest())) + raise FatalError("MD5 of file does not match data in flash!") + else: + print('Hash of data verified.') + except NotImplementedInROMError: + pass + + print('\nLeaving...') + + if esp.IS_STUB: + # skip sending flash_finish to ROM loader here, + # as it causes the loader to exit and run user code + esp.flash_begin(0, 0) + + # Get the "encrypted" flag for the last file flashed + # Note: all_files list contains triplets like: + # (address: Integer, filename: String, encrypted: Boolean) + last_file_encrypted = all_files[-1][2] + + # Check whether the last file flashed was compressed or not + if args.compress and not last_file_encrypted: + esp.flash_defl_finish(False) + else: + esp.flash_finish(False) + + if args.verify: + print('Verifying just-written flash...') + print('(This option is deprecated, flash contents are now always read back after flashing.)') + # If some encrypted files have been flashed print a warning saying that we won't check them + if args.encrypt or args.encrypt_files is not None: + print('WARNING: - cannot verify encrypted files, they will be ignored') + # Call verify_flash function only if there at least one non-encrypted file flashed + if not args.encrypt: + verify_flash(esp, args) + + +def image_info(args): + if args.chip == "auto": + print("WARNING: --chip not specified, defaulting to ESP8266.") + image = LoadFirmwareImage(args.chip, args.filename) + print('Image version: %d' % image.version) + if args.chip != 'auto' and args.chip != 'esp8266': + print( + "Minimal chip revision:", + "v{}.{},".format(image.min_rev_full // 100, image.min_rev_full % 100), + "(legacy min_rev = {})".format(image.min_rev) + ) + print( + "Maximal chip revision:", + "v{}.{}".format(image.max_rev_full // 100, image.max_rev_full % 100), + ) + print('Entry point: %08x' % image.entrypoint if image.entrypoint != 0 else 'Entry point not set') + print('%d segments' % len(image.segments)) + print() + idx = 0 + for seg in image.segments: + idx += 1 + segs = seg.get_memory_type(image) + seg_name = ",".join(segs) + print('Segment %d: %r [%s]' % (idx, seg, seg_name)) + calc_checksum = image.calculate_checksum() + print('Checksum: %02x (%s)' % (image.checksum, + 'valid' if image.checksum == calc_checksum else 'invalid - calculated %02x' % calc_checksum)) + try: + digest_msg = 'Not appended' + if image.append_digest: + is_valid = image.stored_digest == image.calc_digest + digest_msg = "%s (%s)" % (hexify(image.calc_digest).lower(), + "valid" if is_valid else "invalid") + print('Validation Hash: %s' % digest_msg) + except AttributeError: + pass # ESP8266 image has no append_digest field + + +def make_image(args): + image = ESP8266ROMFirmwareImage() + if len(args.segfile) == 0: + raise FatalError('No segments specified') + if len(args.segfile) != len(args.segaddr): + raise FatalError('Number of specified files does not match number of specified addresses') + for (seg, addr) in zip(args.segfile, args.segaddr): + with open(seg, 'rb') as f: + data = f.read() + image.segments.append(ImageSegment(addr, data)) + image.entrypoint = args.entrypoint + image.save(args.output) + + +def elf2image(args): + e = ELFFile(args.input) + if args.chip == 'auto': # Default to ESP8266 for backwards compatibility + args.chip = 'esp8266' + + print("Creating {} image...".format(args.chip)) + + if args.chip == 'esp32': + image = ESP32FirmwareImage() + if args.secure_pad: + image.secure_pad = '1' + elif args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32s2': + image = ESP32S2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32s3beta2': + image = ESP32S3BETA2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32s3': + image = ESP32S3FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32c3': + image = ESP32C3FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32c6beta': + image = ESP32C6BETAFirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32h2beta1': + image = ESP32H2BETA1FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32h2beta2': + image = ESP32H2BETA2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32c2': + image = ESP32C2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.version == '1': # ESP8266 + image = ESP8266ROMFirmwareImage() + elif args.version == '2': + image = ESP8266V2FirmwareImage() + else: + image = ESP8266V3FirmwareImage() + image.entrypoint = e.entrypoint + image.flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] + + if args.chip != 'esp8266': + image.min_rev = args.min_rev + image.min_rev_full = args.min_rev_full + image.max_rev_full = args.max_rev_full + + if args.flash_mmu_page_size: + image.set_mmu_page_size(flash_size_bytes(args.flash_mmu_page_size)) + + # ELFSection is a subclass of ImageSegment, so can use interchangeably + image.segments = e.segments if args.use_segments else e.sections + + if args.pad_to_size: + image.pad_to_size = flash_size_bytes(args.pad_to_size) + + image.flash_size_freq = image.ROM_LOADER.parse_flash_size_arg(args.flash_size) + image.flash_size_freq += image.ROM_LOADER.parse_flash_freq_arg(args.flash_freq) + + if args.elf_sha256_offset: + image.elf_sha256 = e.sha256() + image.elf_sha256_offset = args.elf_sha256_offset + + before = len(image.segments) + image.merge_adjacent_segments() + if len(image.segments) != before: + delta = before - len(image.segments) + print("Merged %d ELF section%s" % (delta, "s" if delta > 1 else "")) + + image.verify() + + if args.output is None: + args.output = image.default_output_name(args.input) + image.save(args.output) + + print("Successfully created {} image.".format(args.chip)) + + +def read_mac(esp, args): + mac = esp.read_mac() + + def print_mac(label, mac): + print('%s: %s' % (label, ':'.join(map(lambda x: '%02x' % x, mac)))) + print_mac("MAC", mac) + + +def chip_id(esp, args): + try: + chipid = esp.chip_id() + print('Chip ID: 0x%08x' % chipid) + except NotSupportedError: + print('Warning: %s has no Chip ID. Reading MAC instead.' % esp.CHIP_NAME) + read_mac(esp, args) + + +def erase_flash(esp, args): + print('Erasing flash (this may take a while)...') + t = time.time() + esp.erase_flash() + print('Chip erase completed successfully in %.1fs' % (time.time() - t)) + + +def erase_region(esp, args): + print('Erasing region (may be slow depending on size)...') + t = time.time() + esp.erase_region(args.address, args.size) + print('Erase completed successfully in %.1f seconds.' % (time.time() - t)) + + +def run(esp, args): + esp.run() + + +def flash_id(esp, args): + flash_id = esp.flash_id() + print('Manufacturer: %02x' % (flash_id & 0xff)) + flid_lowbyte = (flash_id >> 16) & 0xFF + print('Device: %02x%02x' % ((flash_id >> 8) & 0xff, flid_lowbyte)) + print('Detected flash size: %s' % (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))) + + +def read_flash(esp, args): + if args.no_progress: + flash_progress = None + else: + def flash_progress(progress, length): + msg = '%d (%d %%)' % (progress, progress * 100.0 / length) + padding = '\b' * len(msg) + if progress == length: + padding = '\n' + sys.stdout.write(msg + padding) + sys.stdout.flush() + t = time.time() + data = esp.read_flash(args.address, args.size, flash_progress) + t = time.time() - t + print_overwrite('Read %d bytes at 0x%x in %.1f seconds (%.1f kbit/s)...' + % (len(data), args.address, t, len(data) / t * 8 / 1000), last_line=True) + with open(args.filename, 'wb') as f: + f.write(data) + + +def verify_flash(esp, args): + differences = False + + for address, argfile in args.addr_filename: + image = pad_to(argfile.read(), 4) + argfile.seek(0) # rewind in case we need it again + + image = _update_image_flash_params(esp, address, args, image) + + image_size = len(image) + print('Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s...' % (image_size, image_size, address, argfile.name)) + # Try digest first, only read if there are differences. + digest = esp.flash_md5sum(address, image_size) + expected_digest = hashlib.md5(image).hexdigest() + if digest == expected_digest: + print('-- verify OK (digest matched)') + continue + else: + differences = True + if getattr(args, 'diff', 'no') != 'yes': + print('-- verify FAILED (digest mismatch)') + continue + + flash = esp.read_flash(address, image_size) + assert flash != image + diff = [i for i in range(image_size) if flash[i] != image[i]] + print('-- verify FAILED: %d differences, first @ 0x%08x' % (len(diff), address + diff[0])) + for d in diff: + flash_byte = flash[d] + image_byte = image[d] + if PYTHON2: + flash_byte = ord(flash_byte) + image_byte = ord(image_byte) + print(' %08x %02x %02x' % (address + d, flash_byte, image_byte)) + if differences: + raise FatalError("Verify failed.") + + +def read_flash_status(esp, args): + print('Status value: 0x%04x' % esp.read_status(args.bytes)) + + +def write_flash_status(esp, args): + fmt = "0x%%0%dx" % (args.bytes * 2) + args.value = args.value & ((1 << (args.bytes * 8)) - 1) + print(('Initial flash status: ' + fmt) % esp.read_status(args.bytes)) + print(('Setting flash status: ' + fmt) % args.value) + esp.write_status(args.value, args.bytes, args.non_volatile) + print(('After flash status: ' + fmt) % esp.read_status(args.bytes)) + + +def get_security_info(esp, args): + si = esp.get_security_info() + # TODO: better display and tests + print('Flags: {:#010x} ({})'.format(si["flags"], bin(si["flags"]))) + print('Flash_Crypt_Cnt: {:#x}'.format(si["flash_crypt_cnt"])) + print('Key_Purposes: {}'.format(si["key_purposes"])) + if si["chip_id"] is not None and si["api_version"] is not None: + print('Chip_ID: {}'.format(si["chip_id"])) + print('Api_Version: {}'.format(si["api_version"])) + + +def merge_bin(args): + try: + chip_class = _chip_to_rom_loader(args.chip) + except KeyError: + msg = "Please specify the chip argument" if args.chip == "auto" else "Invalid chip choice: '{}'".format(args.chip) + msg = msg + " (choose from {})".format(', '.join(SUPPORTED_CHIPS)) + raise FatalError(msg) + + # sort the files by offset. The AddrFilenamePairAction has already checked for overlap + input_files = sorted(args.addr_filename, key=lambda x: x[0]) + if not input_files: + raise FatalError("No input files specified") + first_addr = input_files[0][0] + if first_addr < args.target_offset: + raise FatalError("Output file target offset is 0x%x. Input file offset 0x%x is before this." % (args.target_offset, first_addr)) + + if args.format != 'raw': + raise FatalError("This version of esptool only supports the 'raw' output format") + + with open(args.output, 'wb') as of: + def pad_to(flash_offs): + # account for output file offset if there is any + of.write(b'\xFF' * (flash_offs - args.target_offset - of.tell())) + for addr, argfile in input_files: + pad_to(addr) + image = argfile.read() + image = _update_image_flash_params(chip_class, addr, args, image) + of.write(image) + if args.fill_flash_size: + pad_to(flash_size_bytes(args.fill_flash_size)) + print("Wrote 0x%x bytes to file %s, ready to flash to offset 0x%x" % (of.tell(), args.output, args.target_offset)) + + +def version(args): + print(__version__) + +# +# End of operations functions +# + + +def main(argv=None, esp=None): + """ + Main function for esptool + + argv - Optional override for default arguments parsing (that uses sys.argv), can be a list of custom arguments + as strings. Arguments and their values need to be added as individual items to the list e.g. "-b 115200" thus + becomes ['-b', '115200']. + + esp - Optional override of the connected device previously returned by get_default_connected_device() + """ + + external_esp = esp is not None + + parser = argparse.ArgumentParser(description='esptool.py v%s - Espressif chips ROM Bootloader Utility' % __version__, prog='esptool') + + parser.add_argument('--chip', '-c', + help='Target chip type', + type=format_chip_name, # support ESP32-S2, etc. + choices=['auto'] + SUPPORTED_CHIPS, + default=os.environ.get('ESPTOOL_CHIP', 'auto')) + + parser.add_argument( + '--port', '-p', + help='Serial port device', + default=os.environ.get('ESPTOOL_PORT', None)) + + parser.add_argument( + '--baud', '-b', + help='Serial port baud rate used when flashing/reading', + type=arg_auto_int, + default=os.environ.get('ESPTOOL_BAUD', ESPLoader.ESP_ROM_BAUD)) + + parser.add_argument( + '--before', + help='What to do before connecting to the chip', + choices=['default_reset', 'usb_reset', 'no_reset', 'no_reset_no_sync'], + default=os.environ.get('ESPTOOL_BEFORE', 'default_reset')) + + parser.add_argument( + '--after', '-a', + help='What to do after esptool.py is finished', + choices=['hard_reset', 'soft_reset', 'no_reset', 'no_reset_stub'], + default=os.environ.get('ESPTOOL_AFTER', 'hard_reset')) + + parser.add_argument( + '--no-stub', + help="Disable launching the flasher stub, only talk to ROM bootloader. Some features will not be available.", + action='store_true') + + parser.add_argument( + '--trace', '-t', + help="Enable trace-level output of esptool.py interactions.", + action='store_true') + + parser.add_argument( + '--override-vddsdio', + help="Override ESP32 VDDSDIO internal voltage regulator (use with care)", + choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES, + nargs='?') + + parser.add_argument( + '--connect-attempts', + help=('Number of attempts to connect, negative or 0 for infinite. ' + 'Default: %d.' % DEFAULT_CONNECT_ATTEMPTS), + type=int, + default=os.environ.get('ESPTOOL_CONNECT_ATTEMPTS', DEFAULT_CONNECT_ATTEMPTS)) + + subparsers = parser.add_subparsers( + dest='operation', + help='Run esptool {command} -h for additional help') + + def add_spi_connection_arg(parent): + parent.add_argument('--spi-connection', '-sc', help='ESP32-only argument. Override default SPI Flash connection. ' + 'Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers to use for SPI flash (CLK,Q,D,HD,CS).', + action=SpiConnectionAction) + + parser_load_ram = subparsers.add_parser( + 'load_ram', + help='Download an image to RAM and execute') + parser_load_ram.add_argument('filename', help='Firmware image') + + parser_dump_mem = subparsers.add_parser( + 'dump_mem', + help='Dump arbitrary memory to disk') + parser_dump_mem.add_argument('address', help='Base address', type=arg_auto_int) + parser_dump_mem.add_argument('size', help='Size of region to dump', type=arg_auto_int) + parser_dump_mem.add_argument('filename', help='Name of binary dump') + + parser_read_mem = subparsers.add_parser( + 'read_mem', + help='Read arbitrary memory location') + parser_read_mem.add_argument('address', help='Address to read', type=arg_auto_int) + + parser_write_mem = subparsers.add_parser( + 'write_mem', + help='Read-modify-write to arbitrary memory location') + parser_write_mem.add_argument('address', help='Address to write', type=arg_auto_int) + parser_write_mem.add_argument('value', help='Value', type=arg_auto_int) + parser_write_mem.add_argument('mask', help='Mask of bits to write', type=arg_auto_int, nargs='?', default='0xFFFFFFFF') + + def add_spi_flash_subparsers(parent, allow_keep, auto_detect): + """ Add common parser arguments for SPI flash properties """ + extra_keep_args = ['keep'] if allow_keep else [] + + if auto_detect and allow_keep: + extra_fs_message = ", detect, or keep" + elif auto_detect: + extra_fs_message = ", or detect" + elif allow_keep: + extra_fs_message = ", or keep" + else: + extra_fs_message = "" + + parent.add_argument('--flash_freq', '-ff', help='SPI Flash frequency', + choices=extra_keep_args + ['80m', '60m', '48m', '40m', '30m', '26m', '24m', '20m', '16m', '15m', '12m'], + default=os.environ.get('ESPTOOL_FF', 'keep' if allow_keep else '40m')) + parent.add_argument('--flash_mode', '-fm', help='SPI Flash mode', + choices=extra_keep_args + ['qio', 'qout', 'dio', 'dout'], + default=os.environ.get('ESPTOOL_FM', 'keep' if allow_keep else 'qio')) + parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16MB, 32MB, 64MB, 128MB)' + ' plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)' + extra_fs_message, + action=FlashSizeAction, auto_detect=auto_detect, + default=os.environ.get('ESPTOOL_FS', 'keep' if allow_keep else '1MB')) + add_spi_connection_arg(parent) + + parser_write_flash = subparsers.add_parser( + 'write_flash', + help='Write a binary blob to flash') + + parser_write_flash.add_argument('addr_filename', metavar='
', help='Address followed by binary filename, separated by space', + action=AddrFilenamePairAction) + parser_write_flash.add_argument('--erase-all', '-e', + help='Erase all regions of flash (not just write areas) before programming', + action="store_true") + + add_spi_flash_subparsers(parser_write_flash, allow_keep=True, auto_detect=True) + parser_write_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") + parser_write_flash.add_argument('--verify', help='Verify just-written data on flash ' + '(mostly superfluous, data is read back during flashing)', action='store_true') + parser_write_flash.add_argument('--encrypt', help='Apply flash encryption when writing data (required correct efuse settings)', + action='store_true') + # In order to not break backward compatibility, our list of encrypted files to flash is a new parameter + parser_write_flash.add_argument('--encrypt-files', metavar='
', + help='Files to be encrypted on the flash. Address followed by binary filename, separated by space.', + action=AddrFilenamePairAction) + parser_write_flash.add_argument('--ignore-flash-encryption-efuse-setting', help='Ignore flash encryption efuse settings ', + action='store_true') + + compress_args = parser_write_flash.add_mutually_exclusive_group(required=False) + compress_args.add_argument('--compress', '-z', help='Compress data in transfer (default unless --no-stub is specified)', + action="store_true", default=None) + compress_args.add_argument('--no-compress', '-u', help='Disable data compression during transfer (default if --no-stub is specified)', + action="store_true") + + subparsers.add_parser( + 'run', + help='Run application code in flash') + + parser_image_info = subparsers.add_parser( + 'image_info', + help='Dump headers from an application image') + parser_image_info.add_argument('filename', help='Image file to parse') + + parser_make_image = subparsers.add_parser( + 'make_image', + help='Create an application image from binary files') + parser_make_image.add_argument('output', help='Output image file') + parser_make_image.add_argument('--segfile', '-f', action='append', help='Segment input file') + parser_make_image.add_argument('--segaddr', '-a', action='append', help='Segment base address', type=arg_auto_int) + parser_make_image.add_argument('--entrypoint', '-e', help='Address of entry point', type=arg_auto_int, default=0) + + parser_elf2image = subparsers.add_parser( + 'elf2image', + help='Create an application image from ELF file') + parser_elf2image.add_argument('input', help='Input ELF file') + parser_elf2image.add_argument('--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str) + parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1', '2', '3'], default='1') + parser_elf2image.add_argument( + # kept for compatibility + # Minimum chip revision (deprecated, consider using --min-rev-full) + "--min-rev", + "-r", + # In v3 we do not do help=argparse.SUPPRESS because + # it should remain visible. + help="Minimal chip revision (ECO version format)", + type=int, + choices=range(256), + metavar="{0, ... 255}", + default=0, + ) + parser_elf2image.add_argument( + "--min-rev-full", + help="Minimal chip revision (in format: major * 100 + minor)", + type=int, + choices=range(65536), + metavar="{0, ... 65535}", + default=0, + ) + parser_elf2image.add_argument( + "--max-rev-full", + help="Maximal chip revision (in format: major * 100 + minor)", + type=int, + choices=range(65536), + metavar="{0, ... 65535}", + default=65535, + ) + parser_elf2image.add_argument('--secure-pad', action='store_true', + help='Pad image so once signed it will end on a 64KB boundary. For Secure Boot v1 images only.') + parser_elf2image.add_argument('--secure-pad-v2', action='store_true', + help='Pad image to 64KB, so once signed its signature sector will start at the next 64K block. ' + 'For Secure Boot v2 images only.') + parser_elf2image.add_argument('--elf-sha256-offset', help='If set, insert SHA256 hash (32 bytes) of the input ELF file at specified offset in the binary.', + type=arg_auto_int, default=None) + parser_elf2image.add_argument('--use_segments', help='If set, ELF segments will be used instead of ELF sections to genereate the image.', + action='store_true') + parser_elf2image.add_argument('--flash-mmu-page-size', help="Change flash MMU page size.", choices=['64KB', '32KB', '16KB']) + parser_elf2image.add_argument( + "--pad-to-size", + help="The block size with which the final binary image after padding must be aligned to. Value 0xFF is used for padding, similar to erase_flash", + default=None, + ) + add_spi_flash_subparsers(parser_elf2image, allow_keep=False, auto_detect=False) + + subparsers.add_parser( + 'read_mac', + help='Read MAC address from OTP ROM') + + subparsers.add_parser( + 'chip_id', + help='Read Chip ID from OTP ROM') + + parser_flash_id = subparsers.add_parser( + 'flash_id', + help='Read SPI flash manufacturer and device ID') + add_spi_connection_arg(parser_flash_id) + + parser_read_status = subparsers.add_parser( + 'read_flash_status', + help='Read SPI flash status register') + + add_spi_connection_arg(parser_read_status) + parser_read_status.add_argument('--bytes', help='Number of bytes to read (1-3)', type=int, choices=[1, 2, 3], default=2) + + parser_write_status = subparsers.add_parser( + 'write_flash_status', + help='Write SPI flash status register') + + add_spi_connection_arg(parser_write_status) + parser_write_status.add_argument('--non-volatile', help='Write non-volatile bits (use with caution)', action='store_true') + parser_write_status.add_argument('--bytes', help='Number of status bytes to write (1-3)', type=int, choices=[1, 2, 3], default=2) + parser_write_status.add_argument('value', help='New value', type=arg_auto_int) + + parser_read_flash = subparsers.add_parser( + 'read_flash', + help='Read SPI flash content') + add_spi_connection_arg(parser_read_flash) + parser_read_flash.add_argument('address', help='Start address', type=arg_auto_int) + parser_read_flash.add_argument('size', help='Size of region to dump', type=arg_auto_int) + parser_read_flash.add_argument('filename', help='Name of binary dump') + parser_read_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") + + parser_verify_flash = subparsers.add_parser( + 'verify_flash', + help='Verify a binary blob against flash') + parser_verify_flash.add_argument('addr_filename', help='Address and binary file to verify there, separated by space', + action=AddrFilenamePairAction) + parser_verify_flash.add_argument('--diff', '-d', help='Show differences', + choices=['no', 'yes'], default='no') + add_spi_flash_subparsers(parser_verify_flash, allow_keep=True, auto_detect=True) + + parser_erase_flash = subparsers.add_parser( + 'erase_flash', + help='Perform Chip Erase on SPI flash') + add_spi_connection_arg(parser_erase_flash) + + parser_erase_region = subparsers.add_parser( + 'erase_region', + help='Erase a region of the flash') + add_spi_connection_arg(parser_erase_region) + parser_erase_region.add_argument('address', help='Start address (must be multiple of 4096)', type=arg_auto_int) + parser_erase_region.add_argument('size', help='Size of region to erase (must be multiple of 4096)', type=arg_auto_int) + + parser_merge_bin = subparsers.add_parser( + 'merge_bin', + help='Merge multiple raw binary files into a single file for later flashing') + + parser_merge_bin.add_argument('--output', '-o', help='Output filename', type=str, required=True) + parser_merge_bin.add_argument('--format', '-f', help='Format of the output file', choices='raw', default='raw') # for future expansion + add_spi_flash_subparsers(parser_merge_bin, allow_keep=True, auto_detect=False) + + parser_merge_bin.add_argument('--target-offset', '-t', help='Target offset where the output file will be flashed', + type=arg_auto_int, default=0) + parser_merge_bin.add_argument('--fill-flash-size', help='If set, the final binary file will be padded with FF ' + 'bytes up to this flash size.', action=FlashSizeAction) + parser_merge_bin.add_argument('addr_filename', metavar='
', + help='Address followed by binary filename, separated by space', + action=AddrFilenamePairAction) + + subparsers.add_parser('get_security_info', help='Get some security-related data') + + subparsers.add_parser('version', help='Print esptool version') + + # internal sanity check - every operation matches a module function of the same name + for operation in subparsers.choices.keys(): + assert operation in globals(), "%s should be a module function" % operation + + argv = expand_file_arguments(argv or sys.argv[1:]) + + args = parser.parse_args(argv) + print('esptool.py v%s' % __version__) + + # operation function can take 1 arg (args), 2 args (esp, arg) + # or be a member function of the ESPLoader class. + + if args.operation is None: + parser.print_help() + sys.exit(1) + + # Forbid the usage of both --encrypt, which means encrypt all the given files, + # and --encrypt-files, which represents the list of files to encrypt. + # The reason is that allowing both at the same time increases the chances of + # having contradictory lists (e.g. one file not available in one of list). + if args.operation == "write_flash" and args.encrypt and args.encrypt_files is not None: + raise FatalError("Options --encrypt and --encrypt-files must not be specified at the same time.") + + operation_func = globals()[args.operation] + + if PYTHON2: + # This function is depreciated in Python3 + operation_args = inspect.getargspec(operation_func).args + else: + operation_args = inspect.getfullargspec(operation_func).args + + if operation_args[0] == 'esp': # operation function takes an ESPLoader connection object + if args.before != "no_reset_no_sync": + initial_baud = min(ESPLoader.ESP_ROM_BAUD, args.baud) # don't sync faster than the default baud rate + else: + initial_baud = args.baud + + if args.port is None: + ser_list = get_port_list() + print("Found %d serial ports" % len(ser_list)) + else: + ser_list = [args.port] + esp = esp or get_default_connected_device(ser_list, port=args.port, connect_attempts=args.connect_attempts, + initial_baud=initial_baud, chip=args.chip, trace=args.trace, + before=args.before) + + if esp is None: + raise FatalError("Could not connect to an Espressif device on any of the %d available serial ports." % len(ser_list)) + + if esp.secure_download_mode: + print("Chip is %s in Secure Download Mode" % esp.CHIP_NAME) + else: + print("Chip is %s" % (esp.get_chip_description())) + print("Features: %s" % ", ".join(esp.get_chip_features())) + print("Crystal is %dMHz" % esp.get_crystal_freq()) + read_mac(esp, args) + + if not args.no_stub: + if esp.secure_download_mode: + print("WARNING: Stub loader is not supported in Secure Download Mode, setting --no-stub") + args.no_stub = True + elif not esp.IS_STUB and esp.stub_is_disabled: + print("WARNING: Stub loader has been disabled for compatibility, setting --no-stub") + args.no_stub = True + else: + esp = esp.run_stub() + + if args.override_vddsdio: + esp.override_vddsdio(args.override_vddsdio) + + if args.baud > initial_baud: + try: + esp.change_baud(args.baud) + except NotImplementedInROMError: + print("WARNING: ROM doesn't support changing baud rate. Keeping initial baud rate %d" % initial_baud) + + # override common SPI flash parameter stuff if configured to do so + if hasattr(args, "spi_connection") and args.spi_connection is not None: + if esp.CHIP_NAME != "ESP32": + raise FatalError("Chip %s does not support --spi-connection option." % esp.CHIP_NAME) + print("Configuring SPI flash mode...") + esp.flash_spi_attach(args.spi_connection) + elif args.no_stub: + print("Enabling default SPI flash mode...") + # ROM loader doesn't enable flash unless we explicitly do it + esp.flash_spi_attach(0) + + # XMC chip startup sequence + XMC_VENDOR_ID = 0x20 + + def is_xmc_chip_strict(): + id = esp.flash_id() + rdid = ((id & 0xff) << 16) | ((id >> 16) & 0xff) | (id & 0xff00) + + vendor_id = ((rdid >> 16) & 0xFF) + mfid = ((rdid >> 8) & 0xFF) + cpid = (rdid & 0xFF) + + if vendor_id != XMC_VENDOR_ID: + return False + + matched = False + if mfid == 0x40: + if cpid >= 0x13 and cpid <= 0x20: + matched = True + elif mfid == 0x41: + if cpid >= 0x17 and cpid <= 0x20: + matched = True + elif mfid == 0x50: + if cpid >= 0x15 and cpid <= 0x16: + matched = True + return matched + + def flash_xmc_startup(): + # If the RDID value is a valid XMC one, may skip the flow + fast_check = True + if fast_check and is_xmc_chip_strict(): + return # Successful XMC flash chip boot-up detected by RDID, skipping. + + sfdp_mfid_addr = 0x10 + mf_id = esp.read_spiflash_sfdp(sfdp_mfid_addr, 8) + if mf_id != XMC_VENDOR_ID: # Non-XMC chip detected by SFDP Read, skipping. + return + + print("WARNING: XMC flash chip boot-up failure detected! Running XMC25QHxxC startup flow") + esp.run_spiflash_command(0xB9) # Enter DPD + esp.run_spiflash_command(0x79) # Enter UDPD + esp.run_spiflash_command(0xFF) # Exit UDPD + time.sleep(0.002) # Delay tXUDPD + esp.run_spiflash_command(0xAB) # Release Power-Down + time.sleep(0.00002) + # Check for success + if not is_xmc_chip_strict(): + print("WARNING: XMC flash boot-up fix failed.") + print("XMC flash chip boot-up fix successful!") + + # Check flash chip connection + if not esp.secure_download_mode: + try: + flash_id = esp.flash_id() + if flash_id in (0xffffff, 0x000000): + print('WARNING: Failed to communicate with the flash chip, read/write operations will fail. ' + 'Try checking the chip connections or removing any other hardware connected to IOs.') + except Exception as e: + esp.trace('Unable to verify flash chip connection ({}).'.format(e)) + + # Check if XMC SPI flash chip booted-up successfully, fix if not + if not esp.secure_download_mode: + try: + flash_xmc_startup() + except Exception as e: + esp.trace('Unable to perform XMC flash chip startup sequence ({}).'.format(e)) + + if hasattr(args, "flash_size"): + print("Configuring flash size...") + detect_flash_size(esp, args) + if args.flash_size != 'keep': # TODO: should set this even with 'keep' + esp.flash_set_parameters(flash_size_bytes(args.flash_size)) + # Check if stub supports chosen flash size + if esp.IS_STUB and args.flash_size in ('32MB', '64MB', '128MB'): + print("WARNING: Flasher stub doesn't fully support flash size larger than 16MB, in case of failure use --no-stub.") + + if esp.IS_STUB and hasattr(args, "address") and hasattr(args, "size"): + if args.address + args.size > 0x1000000: + print("WARNING: Flasher stub doesn't fully support flash size larger than 16MB, in case of failure use --no-stub.") + + try: + operation_func(esp, args) + finally: + try: # Clean up AddrFilenamePairAction files + for address, argfile in args.addr_filename: + argfile.close() + except AttributeError: + pass + + # Handle post-operation behaviour (reset or other) + if operation_func == load_ram: + # the ESP is now running the loaded image, so let it run + print('Exiting immediately.') + elif args.after == 'hard_reset': + esp.hard_reset() + elif args.after == 'soft_reset': + print('Soft resetting...') + # flash_finish will trigger a soft reset + esp.soft_reset(False) + elif args.after == 'no_reset_stub': + print('Staying in flasher stub.') + else: # args.after == 'no_reset' + print('Staying in bootloader.') + if esp.IS_STUB: + esp.soft_reset(True) # exit stub back to ROM loader + + if not external_esp: + esp._port.close() + + else: + operation_func(args) + + +def get_port_list(): + if list_ports is None: + raise FatalError("Listing all serial ports is currently not available. Please try to specify the port when " + "running esptool.py or update the pyserial package to the latest version") + return sorted(ports.device for ports in list_ports.comports()) + + +def expand_file_arguments(argv): + """ Any argument starting with "@" gets replaced with all values read from a text file. + Text file arguments can be split by newline or by space. + Values are added "as-is", as if they were specified in this order on the command line. + """ + new_args = [] + expanded = False + for arg in argv: + if arg.startswith("@"): + expanded = True + with open(arg[1:], "r") as f: + for line in f.readlines(): + new_args += shlex.split(line) + else: + new_args.append(arg) + if expanded: + print("esptool.py %s" % (" ".join(new_args[1:]))) + return new_args + return argv + + +class FlashSizeAction(argparse.Action): + """ Custom flash size parser class to support backwards compatibility with megabit size arguments. + + (At next major relase, remove deprecated sizes and this can become a 'normal' choices= argument again.) + """ + def __init__(self, option_strings, dest, nargs=1, auto_detect=False, **kwargs): + super(FlashSizeAction, self).__init__(option_strings, dest, nargs, **kwargs) + self._auto_detect = auto_detect + + def __call__(self, parser, namespace, values, option_string=None): + try: + value = { + '2m': '256KB', + '4m': '512KB', + '8m': '1MB', + '16m': '2MB', + '32m': '4MB', + '16m-c1': '2MB-c1', + '32m-c1': '4MB-c1', + }[values[0]] + print("WARNING: Flash size arguments in megabits like '%s' are deprecated." % (values[0])) + print("Please use the equivalent size '%s'." % (value)) + print("Megabit arguments may be removed in a future release.") + except KeyError: + value = values[0] + + known_sizes = dict(ESP8266ROM.FLASH_SIZES) + known_sizes.update(ESP32ROM.FLASH_SIZES) + if self._auto_detect: + known_sizes['detect'] = 'detect' + known_sizes['keep'] = 'keep' + if value not in known_sizes: + raise argparse.ArgumentError(self, '%s is not a known flash size. Known sizes: %s' % (value, ", ".join(known_sizes.keys()))) + setattr(namespace, self.dest, value) + + +class SpiConnectionAction(argparse.Action): + """ Custom action to parse 'spi connection' override. Values are SPI, HSPI, or a sequence of 5 pin numbers separated by commas. + """ + def __call__(self, parser, namespace, value, option_string=None): + if value.upper() == "SPI": + value = 0 + elif value.upper() == "HSPI": + value = 1 + elif "," in value: + values = value.split(",") + if len(values) != 5: + raise argparse.ArgumentError(self, '%s is not a valid list of comma-separate pin numbers. Must be 5 numbers - CLK,Q,D,HD,CS.' % value) + try: + values = tuple(int(v, 0) for v in values) + except ValueError: + raise argparse.ArgumentError(self, '%s is not a valid argument. All pins must be numeric values' % values) + if any([v for v in values if v > 33 or v < 0]): + raise argparse.ArgumentError(self, 'Pin numbers must be in the range 0-33.') + # encode the pin numbers as a 32-bit integer with packed 6-bit values, the same way ESP32 ROM takes them + # TODO: make this less ESP32 ROM specific somehow... + clk, q, d, hd, cs = values + value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk + else: + raise argparse.ArgumentError(self, '%s is not a valid spi-connection value. ' + 'Values are SPI, HSPI, or a sequence of 5 pin numbers CLK,Q,D,HD,CS).' % value) + setattr(namespace, self.dest, value) + + +class AddrFilenamePairAction(argparse.Action): + """ Custom parser class for the address/filename pairs passed as arguments """ + def __init__(self, option_strings, dest, nargs='+', **kwargs): + super(AddrFilenamePairAction, self).__init__(option_strings, dest, nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + # validate pair arguments + pairs = [] + for i in range(0, len(values), 2): + try: + address = int(values[i], 0) + except ValueError: + raise argparse.ArgumentError(self, 'Address "%s" must be a number' % values[i]) + try: + argfile = open(values[i + 1], 'rb') + except IOError as e: + raise argparse.ArgumentError(self, e) + except IndexError: + raise argparse.ArgumentError(self, 'Must be pairs of an address and the binary filename to write there') + pairs.append((address, argfile)) + + # Sort the addresses and check for overlapping + end = 0 + for address, argfile in sorted(pairs, key=lambda x: x[0]): + argfile.seek(0, 2) # seek to end + size = argfile.tell() + argfile.seek(0) + sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1) + sector_end = ((address + size + ESPLoader.FLASH_SECTOR_SIZE - 1) & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) - 1 + if sector_start < end: + message = 'Detected overlap at address: 0x%x for file: %s' % (address, argfile.name) + raise argparse.ArgumentError(self, message) + end = sector_end + setattr(namespace, self.dest, pairs) + + +# Binary stub code (see flasher_stub dir for source & details) +ESP8266ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +5dwuzr3/OyU1/H+n+L8kk1ilxnz4f3giyVw=\ +"""))) +ESP32ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32S2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ ++rqe/Hebvj3Y4S8jQkWaGpP//j8geRE3\ +"""))) +ESP32S3BETA2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32S3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32C3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +2JPh/i7GZklhkqv/AdnBtYo=\ +"""))) +ESP32C6BETAROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +eNrNWlt7G7cR/SuyJEuJm68FyL3BiSXSpkTJtzqpY9Uu3XoX2FWdC79YpmK5Df97cebCXVIi5cc+UCKxu8Bg5syZC/a/+7P6arZ/f6van1wZO7my8VPl8Ts+5t3jyZUv4rf+5Kp0k6uCRvfiYPk8/sl+iH+SOJTF\ +//V2/OPl6YSenlw14XVOcxzGP+ZpnL8/i6PJ4eRiclWb+LU3qEY7cfZilwWoeqeTq9AbPTzdjg+atIyr9uIn3lsUg/inP9mfTDE9JruMM6TxR8N3uXweR+PidRTYuviliVd8lLxq8sk+CfXHk3hfiPdX/GzT5Pma\ +C7r0kPVC24yfEHLaJs8H4TNRWG+hufiJenPx4/v4/2gushRn2OMAwh+165j4v3BDVsHNi7rDuSzNKww6MuhK7W/rH0H7PHtc7/rUMFeSj6CeaBzX53mjOaOBnH+iK8PY8aEyPuybt7llddo+G5lvt8VJXCSM49x2\ +FCEQ4pO1qiEZ4Rqsx6tUCZvImGdYNz2EsKOu4izP77A+jcUp87iNOuMbCGEZYwK/nWMs6ThPorp4wSbAkvhf5XsLlT0kowyj3M7yFCYd41ZspwScIBtjHIsUtcxTkE63485cb7SkWlGNbsVDl8P410ZQ1SmPFpbV\ +U2Axh/ntHXgAkGQYC7itcWcHuKmdOkrn2QlVu4U7xkhUb5CRJpyx4aEN6NvmMqVtZasM/M9bmazPUIN8TY1Jx+o0HWDER7CPTDzOCOZse4vuuLHt6rBHoNVIMFoq7iZgKX99Cgj2Ky9c5UeETfED3FGkR4AbA/P7\ +Q38qO8tw97iDZOzKq4r6HQcVLgu4Gz86DPIQtwz5fiuSk+4MrmS40tuBwWXqIgNAdniRUrffndzZ81YcWj5pF4Euo64bpQ3MSTSpNlL5S8yiaDQrm1HI087X64B+C5GYBEsv3Enoi7ckCAz6A5vlpe7wlYhJNqIR\ +HMCIq/QiU4bulLUIHASZHGsoZNzIQRRj8HWs4YXCyHPeQ5d+ADiwhYCyBIDjpcoRZqPGvh7JFYQSt8eubPuvGeMUR/yJ/zFOY/TnyP8Le4VpEEceEPOeDPZEizW7pRPtgg5As+TThomk9ivhCRv2qW5yQJ4yoWcn\ +A9ZXnG5CMk/AmH3ehssHIyYgipQSGiVMdqPjRVqz32gU7wTly28lJvv68BcNnXh8xIKv2whtphKisikzRNUb8y4IuvnWAZuktt92o89YsTm4YT14f9EIt8Lre0frhNiKX8BEMbQxOWrOINTYUcFQsxf1AiJzJosd\ +Jm9FYcc/ZiwbwOq7vEaseT5nDkBeg+Dh5QqgUYT7jDrgCv8ry9Re4OZqhy8wLw4RSBxTHWviBVPljZG5DfrLsQSQMhJGejqRkvk6G2Is9Nu7Pd3drmuFVfi58w6N9VbjBZF30caJjUvadknmf2i3ytvUQxIL3uo9\ +cVrx6yqVsA/pXCJhf13ENcikjNxFASE5Qlgjqrn8AOKw84+49B4rp3tizLiFUPNGg2QtoRSJ8jmAUXJmIMQ0Y2UUmhmIsBC8KQ9YUCaZbQanctHNinq3tzqzSX9qpycGKMdDpjJOTs47wZY+kNKJmlmONfI5kW/5\ +8c5qsGe6vZnYSrMjd3h7hC9wMnwzqW+1VZWDe7BTOZpMI0k12evmDbT75nThqiBS5z/iJrXa+8dMrrBCkT6/TRBF2ArD1mFpmu3biRpBGJHGOSXd3eF7Ji/GL6TLfmRuVURrpnOj8xC27IcP84+vQARvwEb/BKQq\ +oRiKjdugwT345mPsBYGtQTkVUCT0f2KuqAWipZhVVbwkR+/PXLNUUG2VMQGBIHzvlq0XUM+5x+gnnrRIg0Kq0DpuM+S3b7eUzef/WT8nIdLfZ1IlGPrrN8eFL/gRol7NhuzpgdRs6Wqqla94EiUSVtLBgECEdZok\ +Zpum8h/Yy4hJIHKZ5hrngdV0CP9K8+cMrvJLwKVRD7M4lMvO+d/fM14q5VWH2qDpidyLK38ast0LkrH/SUpjGlmS4dXtAPfLfvKMc7AvVRjCmGc7NICLPUXki5OUuMfpleZ2OareXHIqKCVN+Edd8pO1O/B/bUGN\ +UFP3HkgRvl5WGiQk/MDZ+JRzcZI1Ba9qMbLIXFHAaeZq7ZgHoVHkGU3yf0CzhJ7ix7Tl2stvPzxY9foNGRMVwcuU/OLeS8Dw5WT6GuKf/gTWKR8/foKLT+49xcWnk+kz8PXbZ52mS5WfDU+RvXxobYDMD9uN3H8g\ +bpIJUUksLSWWgllDIbE2k1ibdL6XlB7Qs7in7nGyhdoEzwIe2B6I0Gebna30mvwVh+CzpUpIsyjNaDBWlbISZrHJXZjEHv2bU8SCtZ0yehbV2XJldef8gGC3JdmwVmRpMdA2Gr51hQDpGnusBa1adNEls+kRTSeB\ +cU3qEH90unSTyfg6OiqaUnVAW8wpgZyw9GvR05Qb6XwqWuYeCgol2okYW3Q8XbTopK8A6eB1pDfA0tJgyl+4K3K0mUBK6pSc509kW2hy2Hw5SUZ+XKTNsFFDtC0lSlSiqDMEZfwtws/5UzXrOjWXEgdcpZGKAbro\ +LOSy74L3h/BN40k7HssnLomnUhDDPOAl/MfNPteW2GSGW7GjWq6DZ7v3G/KgZ9QIqqSiyb6nn05/fo2fru60iWiRpE3ufb4rTiMVfnBSkll6Nm2fdR7JnH90wJ5rUUyU9ugEyn6gdK5zF925nW22MftbnnrhBWAn\ +lH6NSviFxcyAGxLLlcwmEu2mSpRAbyha0CtwYKLUcr+yyrUfp4qwFAq4OvFODcWGuWTALVszpeYcErr8G4ChyRihlrt24LQ88xVfiEi5oGHxElv4X+RKMpnRFYiZf0V5Txt3qGvRaIxP+EcVDnTE8UiZHrx2EgGN\ +PHkNfg9aWI38ryC1TCEBvq/zlxB+LFLW+avJhQRQJz2eRkGQ3oQIK5zHwUPQl0pGEiRXSe9+J50GImTaOTpHmMSlx7JeyjU7yMojipv0yDOZNExzDNxQnOzsKp2ieqnp2yWvXaaq15DvIRg0onFE10DXUBhC56XV\ +lRtZOes2c3pe0JBzSWF66i4jpZ51mIaG6I46JHegAABNPYzOM+zBXOwMwxs7kqgOs3YawLvIbFXKnkgJs5EBQboqXfAI+MVTyHgC452A1UZQ9GhzLl94lOoOxM3tGKncrQTKYFuNwuE81+EC4wgLurzoDno2iOq8\ +Cq21udfsF2qXKUkN6ajbPqUmO7Rlz/3PGEOBb9A2w2jckvTSCuEak37Hae1MG6CJVFCG3Rk786zYqiPKQrciX+0X2Bu12Ju2qrxMUE4U2gnfog76YneADB2BKeD6i+OLC6Hz0OlXGSaPi2X7Qnsl2ddUbRISnUHs\ +UqZdCWRmJGDouvLgYDFnInMiZvXHHCeFwTR1iU41bOveMjkVLmd66ORYSVslF3bcboHsV0sUpSdqjSEvu4wx0sh92o0hfC0IPnvXrsykfFiJarXwDlRdJ62EViV00nNKRwBF3fwmqQQ1N9R7AQ9IvZqYxP8zZqxA\ +6SM36XriqfDZi8X0v7Yd18ZIM3Fpus/9pNPpFK8iXQfhTSPorvrn7Pmqc0dpmb3LDXEf0IxswhBNLxfm3IFswlzrmDDgobqQPVZUKkkXc52ExqBVAJcOU4ZMLUJW/U93mdapvMqH79uV/FSqRTP/3I5+Bj/fsl5h\ +ZiK5e8zrNdpIXP/Y4KNEOlmrMWTw3aF0GXJyGjnJsOw3dbISWBm+6L9V4uc+MOmq07dHZ40GBH/SDSN1y0Hzgo1V5VJuNot07JgHqDuRSmPAcn5Aew0dd2Qc+KaTfcohBmi27u+IQ1XtYZc46QM9eKGDgyM2IofY\ +sYwsymMhFooGi7z8BjX/xq8F8GHfJ+EfwzI1lJO8HXcif9IimMRowlssesmzhHAyWrfSO1Qt1o/+DswmgzncWo5C9TTJ4ZzOSTrTyNNg86LRBLQQzkgnsyHbOOgpJ3RQyo+if/cxde2l/ywlDJ3gNEsgeScFQRCL\ +4d0EKoWoeV1rFk2ZwidNI+VQ3vS/0nulIow8qXil+lGBos3w2rUcS8jV89UMiU3gFGdXzh2M7KdagZs1zzWV5aDKGjmWc5ZGPKPFgxE8EFR3W5GtHLAt+h/kLnH9qaRa9B+JwuJMIEh/q1XglhyDpNJ/6/pfoQfo\ +N110pvOkPe3WP9Z7DYXJkaanUuVXxaZiaAmk9npxRBVM4DF+a+N01Q85Nb5oX0zwfT/aAkn4M35VQgvW30hdbK3atVZFy48ooNNG4t2+Zon4cUkmtAT0Ot5Gc3nLAPPQLpPrGTlCa/KWs9z1IemsfQGINwnHSPxH\ +emNnWxTfpzJjxlGypR1DpG2Fy1lVM8oZXrUHrqpCjhD+BaF1Kjwq5Y0vLxlBNHeqcxdZ20lRs62hkL9R6b98BCv6b1xXkFMhsVIO+KH05TtUEdwD3R9Sef/1ptUR07+XtypiOVd3+jXph45+1UZmtCkEP1p6SQtR\ +9Mnq6y9STJF6zqT3bQnPlwzmSl51UnBXXJ7vC69QQtCXs9q8bTjeDJFH7X4Kc7BJCYfMgJW/ufZQk4No0R9A/goE63jZsJIKq2+J/K6nvQqijF+X2NC2k7d5mgM5J9InqQRAhR8nnHJjkg8eIsHrqUjSrmDSE3n5\ +oOEs7OLkIdqU91lEFvkvMMT2uHcir7MwnIr0DylShcO1p2T10xcORiRfp00PvRc99s99yS+oSNPwQXnagGM97RcJVNU7Q5ig81Gq3/TgJZXDa9N5F8O3UYuoT15joLdSiGZKjlB1JmcAain8Dz3poTh1Tck5KLIQ\ +XVD/Vc8S8vb0x+SLkLLsoxSye1LeK9Hpk2Z9soJPdt3z1iN1LeC3ss8MAErDa0IIxaTiFgGMSb7ITwba4E+kFUUl8ovmJfD68jjGgSp7vfcGYe0N4PMPXH7SoJzPnh4/w+Vnk1mniU+UYPa/2aLXPf/1cVZe4KVP\ +a/I8sbZITLxST2cXnxeD/X6viIOhnJX6dihAFZ1pX4a7sxibJc4k8/8BksMRng==\ +"""))) +ESP32H2BETA1ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32H2BETA2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32C2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) + + +def _main(): + try: + main() + except FatalError as e: + print('\nA fatal error occurred: %s' % e) + sys.exit(2) + + +if __name__ == '__main__': + _main() diff --git a/Release/esptool/gen_esp32part.py b/Release/esptool/gen_esp32part.py new file mode 100755 index 0000000..273f4e3 --- /dev/null +++ b/Release/esptool/gen_esp32part.py @@ -0,0 +1,595 @@ +#!/usr/bin/env python +# +# ESP32 partition table generation tool +# +# Converts partition tables to/from CSV and binary formats. +# +# See https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/partition-tables.html +# for explanation of partition table structure and uses. +# +# SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import division, print_function, unicode_literals + +import argparse +import binascii +import errno +import hashlib +import os +import re +import struct +import sys + +MAX_PARTITION_LENGTH = 0xC00 # 3K for partition data (96 entries) leaves 1K in a 4K sector for signature +MD5_PARTITION_BEGIN = b'\xEB\xEB' + b'\xFF' * 14 # The first 2 bytes are like magic numbers for MD5 sum +PARTITION_TABLE_SIZE = 0x1000 # Size of partition table + +MIN_PARTITION_SUBTYPE_APP_OTA = 0x10 +NUM_PARTITION_SUBTYPE_APP_OTA = 16 + +__version__ = '1.2' + +APP_TYPE = 0x00 +DATA_TYPE = 0x01 + +TYPES = { + 'app': APP_TYPE, + 'data': DATA_TYPE, +} + + +def get_ptype_as_int(ptype): + """ Convert a string which might be numeric or the name of a partition type to an integer """ + try: + return TYPES[ptype] + except KeyError: + try: + return int(ptype, 0) + except TypeError: + return ptype + + +# Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h +SUBTYPES = { + APP_TYPE: { + 'factory': 0x00, + 'test': 0x20, + }, + DATA_TYPE: { + 'ota': 0x00, + 'phy': 0x01, + 'nvs': 0x02, + 'coredump': 0x03, + 'nvs_keys': 0x04, + 'efuse': 0x05, + 'undefined': 0x06, + 'esphttpd': 0x80, + 'fat': 0x81, + 'spiffs': 0x82, + }, +} + + +def get_subtype_as_int(ptype, subtype): + """ Convert a string which might be numeric or the name of a partition subtype to an integer """ + try: + return SUBTYPES[get_ptype_as_int(ptype)][subtype] + except KeyError: + try: + return int(subtype, 0) + except TypeError: + return subtype + + +ALIGNMENT = { + APP_TYPE: 0x10000, + DATA_TYPE: 0x1000, +} + + +def get_alignment_for_type(ptype): + return ALIGNMENT.get(ptype, ALIGNMENT[DATA_TYPE]) + + +def get_partition_type(ptype): + if ptype == 'app': + return APP_TYPE + if ptype == 'data': + return DATA_TYPE + raise InputError('Invalid partition type') + + +def add_extra_subtypes(csv): + for line_no in csv: + try: + fields = [line.strip() for line in line_no.split(',')] + for subtype, subtype_values in SUBTYPES.items(): + if (int(fields[2], 16) in subtype_values.values() and subtype == get_partition_type(fields[0])): + raise ValueError('Found duplicate value in partition subtype') + SUBTYPES[TYPES[fields[0]]][fields[1]] = int(fields[2], 16) + except InputError as err: + raise InputError('Error parsing custom subtypes: %s' % err) + + +quiet = False +md5sum = True +secure = False +offset_part_table = 0 + + +def status(msg): + """ Print status message to stderr """ + if not quiet: + critical(msg) + + +def critical(msg): + """ Print critical message to stderr """ + sys.stderr.write(msg) + sys.stderr.write('\n') + + +class PartitionTable(list): + def __init__(self): + super(PartitionTable, self).__init__(self) + + @classmethod + def from_file(cls, f): + data = f.read() + data_is_binary = data[0:2] == PartitionDefinition.MAGIC_BYTES + if data_is_binary: + status('Parsing binary partition input...') + return cls.from_binary(data), True + + data = data.decode() + status('Parsing CSV input...') + return cls.from_csv(data), False + + @classmethod + def from_csv(cls, csv_contents): + res = PartitionTable() + lines = csv_contents.splitlines() + + def expand_vars(f): + f = os.path.expandvars(f) + m = re.match(r'(? 1) + + # print sorted duplicate partitions by name + if len(duplicates) != 0: + critical('A list of partitions that have the same name:') + for p in sorted(self, key=lambda x:x.name): + if len(duplicates.intersection([p.name])) != 0: + critical('%s' % (p.to_csv())) + raise InputError('Partition names must be unique') + + # check for overlaps + last = None + for p in sorted(self, key=lambda x:x.offset): + if p.offset < offset_part_table + PARTITION_TABLE_SIZE: + raise InputError('Partition offset 0x%x is below 0x%x' % (p.offset, offset_part_table + PARTITION_TABLE_SIZE)) + if last is not None and p.offset < last.offset + last.size: + raise InputError('Partition at 0x%x overlaps 0x%x-0x%x' % (p.offset, last.offset, last.offset + last.size - 1)) + last = p + + # check that otadata should be unique + otadata_duplicates = [p for p in self if p.type == TYPES['data'] and p.subtype == SUBTYPES[DATA_TYPE]['ota']] + if len(otadata_duplicates) > 1: + for p in otadata_duplicates: + critical('%s' % (p.to_csv())) + raise InputError('Found multiple otadata partitions. Only one partition can be defined with type="data"(1) and subtype="ota"(0).') + + if len(otadata_duplicates) == 1 and otadata_duplicates[0].size != 0x2000: + p = otadata_duplicates[0] + critical('%s' % (p.to_csv())) + raise InputError('otadata partition must have size = 0x2000') + + def flash_size(self): + """ Return the size that partitions will occupy in flash + (ie the offset the last partition ends at) + """ + try: + last = sorted(self, reverse=True)[0] + except IndexError: + return 0 # empty table! + return last.offset + last.size + + def verify_size_fits(self, flash_size_bytes: int) -> None: + """ Check that partition table fits into the given flash size. + Raises InputError otherwise. + """ + table_size = self.flash_size() + if flash_size_bytes < table_size: + mb = 1024 * 1024 + raise InputError('Partitions tables occupies %.1fMB of flash (%d bytes) which does not fit in configured ' + "flash size %dMB. Change the flash size in menuconfig under the 'Serial Flasher Config' menu." % + (table_size / mb, table_size, flash_size_bytes / mb)) + + @classmethod + def from_binary(cls, b): + md5 = hashlib.md5() + result = cls() + for o in range(0,len(b),32): + data = b[o:o + 32] + if len(data) != 32: + raise InputError('Partition table length must be a multiple of 32 bytes') + if data == b'\xFF' * 32: + return result # got end marker + if md5sum and data[:2] == MD5_PARTITION_BEGIN[:2]: # check only the magic number part + if data[16:] == md5.digest(): + continue # the next iteration will check for the end marker + else: + raise InputError("MD5 checksums don't match! (computed: 0x%s, parsed: 0x%s)" % (md5.hexdigest(), binascii.hexlify(data[16:]))) + else: + md5.update(data) + result.append(PartitionDefinition.from_binary(data)) + raise InputError('Partition table is missing an end-of-table marker') + + def to_binary(self): + result = b''.join(e.to_binary() for e in self) + if md5sum: + result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest() + if len(result) >= MAX_PARTITION_LENGTH: + raise InputError('Binary partition table length (%d) longer than max' % len(result)) + result += b'\xFF' * (MAX_PARTITION_LENGTH - len(result)) # pad the sector, for signing + return result + + def to_csv(self, simple_formatting=False): + rows = ['# ESP-IDF Partition Table', + '# Name, Type, SubType, Offset, Size, Flags'] + rows += [x.to_csv(simple_formatting) for x in self] + return '\n'.join(rows) + '\n' + + +class PartitionDefinition(object): + MAGIC_BYTES = b'\xAA\x50' + + # dictionary maps flag name (as used in CSV flags list, property name) + # to bit set in flags words in binary format + FLAGS = { + 'encrypted': 0 + } + + # add subtypes for the 16 OTA slot values ("ota_XX, etc.") + for ota_slot in range(NUM_PARTITION_SUBTYPE_APP_OTA): + SUBTYPES[TYPES['app']]['ota_%d' % ota_slot] = MIN_PARTITION_SUBTYPE_APP_OTA + ota_slot + + def __init__(self): + self.name = '' + self.type = None + self.subtype = None + self.offset = None + self.size = None + self.encrypted = False + + @classmethod + def from_csv(cls, line, line_no): + """ Parse a line from the CSV """ + line_w_defaults = line + ',,,,' # lazy way to support default fields + fields = [f.strip() for f in line_w_defaults.split(',')] + + res = PartitionDefinition() + res.line_no = line_no + res.name = fields[0] + res.type = res.parse_type(fields[1]) + res.subtype = res.parse_subtype(fields[2]) + res.offset = res.parse_address(fields[3]) + res.size = res.parse_address(fields[4]) + if res.size is None: + raise InputError("Size field can't be empty") + + flags = fields[5].split(':') + for flag in flags: + if flag in cls.FLAGS: + setattr(res, flag, True) + elif len(flag) > 0: + raise InputError("CSV flag column contains unknown flag '%s'" % (flag)) + + return res + + def __eq__(self, other): + return self.name == other.name and self.type == other.type \ + and self.subtype == other.subtype and self.offset == other.offset \ + and self.size == other.size + + def __repr__(self): + def maybe_hex(x): + return '0x%x' % x if x is not None else 'None' + return "PartitionDefinition('%s', 0x%x, 0x%x, %s, %s)" % (self.name, self.type, self.subtype or 0, + maybe_hex(self.offset), maybe_hex(self.size)) + + def __str__(self): + return "Part '%s' %d/%d @ 0x%x size 0x%x" % (self.name, self.type, self.subtype, self.offset or -1, self.size or -1) + + def __cmp__(self, other): + return self.offset - other.offset + + def __lt__(self, other): + return self.offset < other.offset + + def __gt__(self, other): + return self.offset > other.offset + + def __le__(self, other): + return self.offset <= other.offset + + def __ge__(self, other): + return self.offset >= other.offset + + def parse_type(self, strval): + if strval == '': + raise InputError("Field 'type' can't be left empty.") + return parse_int(strval, TYPES) + + def parse_subtype(self, strval): + if strval == '': + if self.type == TYPES['app']: + raise InputError('App partition cannot have an empty subtype') + return SUBTYPES[DATA_TYPE]['undefined'] + return parse_int(strval, SUBTYPES.get(self.type, {})) + + def parse_address(self, strval): + if strval == '': + return None # PartitionTable will fill in default + return parse_int(strval) + + def verify(self): + if self.type is None: + raise ValidationError(self, 'Type field is not set') + if self.subtype is None: + raise ValidationError(self, 'Subtype field is not set') + if self.offset is None: + raise ValidationError(self, 'Offset field is not set') + align = get_alignment_for_type(self.type) + if self.offset % align: + raise ValidationError(self, 'Offset 0x%x is not aligned to 0x%x' % (self.offset, align)) + if self.size % align and secure and self.type == APP_TYPE: + raise ValidationError(self, 'Size 0x%x is not aligned to 0x%x' % (self.size, align)) + if self.size is None: + raise ValidationError(self, 'Size field is not set') + + if self.name in TYPES and TYPES.get(self.name, '') != self.type: + critical("WARNING: Partition has name '%s' which is a partition type, but does not match this partition's " + 'type (0x%x). Mistake in partition table?' % (self.name, self.type)) + all_subtype_names = [] + for names in (t.keys() for t in SUBTYPES.values()): + all_subtype_names += names + if self.name in all_subtype_names and SUBTYPES.get(self.type, {}).get(self.name, '') != self.subtype: + critical("WARNING: Partition has name '%s' which is a partition subtype, but this partition has " + 'non-matching type 0x%x and subtype 0x%x. Mistake in partition table?' % (self.name, self.type, self.subtype)) + + STRUCT_FORMAT = b'<2sBBLL16sL' + + @classmethod + def from_binary(cls, b): + if len(b) != 32: + raise InputError('Partition definition length must be exactly 32 bytes. Got %d bytes.' % len(b)) + res = cls() + (magic, res.type, res.subtype, res.offset, + res.size, res.name, flags) = struct.unpack(cls.STRUCT_FORMAT, b) + if b'\x00' in res.name: # strip null byte padding from name string + res.name = res.name[:res.name.index(b'\x00')] + res.name = res.name.decode() + if magic != cls.MAGIC_BYTES: + raise InputError('Invalid magic bytes (%r) for partition definition' % magic) + for flag,bit in cls.FLAGS.items(): + if flags & (1 << bit): + setattr(res, flag, True) + flags &= ~(1 << bit) + if flags != 0: + critical('WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?' % flags) + return res + + def get_flags_list(self): + return [flag for flag in self.FLAGS.keys() if getattr(self, flag)] + + def to_binary(self): + flags = sum((1 << self.FLAGS[flag]) for flag in self.get_flags_list()) + return struct.pack(self.STRUCT_FORMAT, + self.MAGIC_BYTES, + self.type, self.subtype, + self.offset, self.size, + self.name.encode(), + flags) + + def to_csv(self, simple_formatting=False): + def addr_format(a, include_sizes): + if not simple_formatting and include_sizes: + for (val, suffix) in [(0x100000, 'M'), (0x400, 'K')]: + if a % val == 0: + return '%d%s' % (a // val, suffix) + return '0x%x' % a + + def lookup_keyword(t, keywords): + for k,v in keywords.items(): + if simple_formatting is False and t == v: + return k + return '%d' % t + + def generate_text_flags(): + """ colon-delimited list of flags """ + return ':'.join(self.get_flags_list()) + + return ','.join([self.name, + lookup_keyword(self.type, TYPES), + lookup_keyword(self.subtype, SUBTYPES.get(self.type, {})), + addr_format(self.offset, False), + addr_format(self.size, True), + generate_text_flags()]) + + +def parse_int(v, keywords={}): + """Generic parser for integer fields - int(x,0) with provision for + k/m/K/M suffixes and 'keyword' value lookup. + """ + try: + for letter, multiplier in [('k', 1024), ('m', 1024 * 1024)]: + if v.lower().endswith(letter): + return parse_int(v[:-1], keywords) * multiplier + return int(v, 0) + except ValueError: + if len(keywords) == 0: + raise InputError('Invalid field value %s' % v) + try: + return keywords[v.lower()] + except KeyError: + raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ', '.join(keywords))) + + +def main(): + global quiet + global md5sum + global offset_part_table + global secure + parser = argparse.ArgumentParser(description='ESP32 partition table utility') + + parser.add_argument('--flash-size', help='Optional flash size limit, checks partition table fits in flash', + nargs='?', choices=['1MB', '2MB', '4MB', '8MB', '16MB', '32MB', '64MB', '128MB']) + parser.add_argument('--disable-md5sum', help='Disable md5 checksum for the partition table', default=False, action='store_true') + parser.add_argument('--no-verify', help="Don't verify partition table fields", action='store_true') + parser.add_argument('--verify', '-v', help='Verify partition table fields (deprecated, this behaviour is ' + 'enabled by default and this flag does nothing.', action='store_true') + parser.add_argument('--quiet', '-q', help="Don't print non-critical status messages to stderr", action='store_true') + parser.add_argument('--offset', '-o', help='Set offset partition table', default='0x8000') + parser.add_argument('--secure', help='Require app partitions to be suitable for secure boot', action='store_true') + parser.add_argument('--extra-partition-subtypes', help='Extra partition subtype entries', nargs='*') + parser.add_argument('input', help='Path to CSV or binary file to parse.', type=argparse.FileType('rb')) + parser.add_argument('output', help='Path to output converted binary or CSV file. Will use stdout if omitted.', + nargs='?', default='-') + + args = parser.parse_args() + + quiet = args.quiet + md5sum = not args.disable_md5sum + secure = args.secure + offset_part_table = int(args.offset, 0) + if args.extra_partition_subtypes: + add_extra_subtypes(args.extra_partition_subtypes) + + table, input_is_binary = PartitionTable.from_file(args.input) + + if not args.no_verify: + status('Verifying table...') + table.verify() + + if args.flash_size: + size_mb = int(args.flash_size.replace('MB', '')) + table.verify_size_fits(size_mb * 1024 * 1024) + + # Make sure that the output directory is created + output_dir = os.path.abspath(os.path.dirname(args.output)) + + if not os.path.exists(output_dir): + try: + os.makedirs(output_dir) + except OSError as exc: + if exc.errno != errno.EEXIST: + raise + + if input_is_binary: + output = table.to_csv() + with sys.stdout if args.output == '-' else open(args.output, 'w') as f: + f.write(output) + else: + output = table.to_binary() + try: + stdout_binary = sys.stdout.buffer # Python 3 + except AttributeError: + stdout_binary = sys.stdout + with stdout_binary if args.output == '-' else open(args.output, 'wb') as f: + f.write(output) + + +class InputError(RuntimeError): + def __init__(self, e): + super(InputError, self).__init__(e) + + +class ValidationError(InputError): + def __init__(self, partition, message): + super(ValidationError, self).__init__( + 'Partition %s invalid: %s' % (partition.name, message)) + + +if __name__ == '__main__': + try: + main() + except InputError as e: + print(e, file=sys.stderr) + sys.exit(2) diff --git a/Release/esptool/spiffsgen.py b/Release/esptool/spiffsgen.py new file mode 100755 index 0000000..45f8449 --- /dev/null +++ b/Release/esptool/spiffsgen.py @@ -0,0 +1,593 @@ +#!/usr/bin/env python +# +# spiffsgen is a tool used to generate a spiffs image from a directory +# +# SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import division, print_function + +import argparse +import io +import math +import os +import struct + +try: + import typing + + TSP = typing.TypeVar('TSP', bound='SpiffsObjPageWithIdx') + ObjIdsItem = typing.Tuple[int, typing.Type[TSP]] +except ImportError: + pass + + +SPIFFS_PH_FLAG_USED_FINAL_INDEX = 0xF8 +SPIFFS_PH_FLAG_USED_FINAL = 0xFC + +SPIFFS_PH_FLAG_LEN = 1 +SPIFFS_PH_IX_SIZE_LEN = 4 +SPIFFS_PH_IX_OBJ_TYPE_LEN = 1 +SPIFFS_TYPE_FILE = 1 + +# Based on typedefs under spiffs_config.h +SPIFFS_OBJ_ID_LEN = 2 # spiffs_obj_id +SPIFFS_SPAN_IX_LEN = 2 # spiffs_span_ix +SPIFFS_PAGE_IX_LEN = 2 # spiffs_page_ix +SPIFFS_BLOCK_IX_LEN = 2 # spiffs_block_ix + + +class SpiffsBuildConfig(object): + def __init__(self, + page_size, # type: int + page_ix_len, # type: int + block_size, # type: int + block_ix_len, # type: int + meta_len, # type: int + obj_name_len, # type: int + obj_id_len, # type: int + span_ix_len, # type: int + packed, # type: bool + aligned, # type: bool + endianness, # type: str + use_magic, # type: bool + use_magic_len, # type: bool + aligned_obj_ix_tables # type: bool + ): + if block_size % page_size != 0: + raise RuntimeError('block size should be a multiple of page size') + + self.page_size = page_size + self.block_size = block_size + self.obj_id_len = obj_id_len + self.span_ix_len = span_ix_len + self.packed = packed + self.aligned = aligned + self.obj_name_len = obj_name_len + self.meta_len = meta_len + self.page_ix_len = page_ix_len + self.block_ix_len = block_ix_len + self.endianness = endianness + self.use_magic = use_magic + self.use_magic_len = use_magic_len + self.aligned_obj_ix_tables = aligned_obj_ix_tables + + self.PAGES_PER_BLOCK = self.block_size // self.page_size + self.OBJ_LU_PAGES_PER_BLOCK = int(math.ceil(self.block_size / self.page_size * self.obj_id_len / self.page_size)) + self.OBJ_USABLE_PAGES_PER_BLOCK = self.PAGES_PER_BLOCK - self.OBJ_LU_PAGES_PER_BLOCK + + self.OBJ_LU_PAGES_OBJ_IDS_LIM = self.page_size // self.obj_id_len + + self.OBJ_DATA_PAGE_HEADER_LEN = self.obj_id_len + self.span_ix_len + SPIFFS_PH_FLAG_LEN + + pad = 4 - (4 if self.OBJ_DATA_PAGE_HEADER_LEN % 4 == 0 else self.OBJ_DATA_PAGE_HEADER_LEN % 4) + + self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED = self.OBJ_DATA_PAGE_HEADER_LEN + pad + self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD = pad + self.OBJ_DATA_PAGE_CONTENT_LEN = self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN + + self.OBJ_INDEX_PAGES_HEADER_LEN = (self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED + SPIFFS_PH_IX_SIZE_LEN + + SPIFFS_PH_IX_OBJ_TYPE_LEN + self.obj_name_len + self.meta_len) + if aligned_obj_ix_tables: + self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED = (self.OBJ_INDEX_PAGES_HEADER_LEN + SPIFFS_PAGE_IX_LEN - 1) & ~(SPIFFS_PAGE_IX_LEN - 1) + self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED_PAD = self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED - self.OBJ_INDEX_PAGES_HEADER_LEN + else: + self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED = self.OBJ_INDEX_PAGES_HEADER_LEN + self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED_PAD = 0 + + self.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM = (self.page_size - self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED) // self.block_ix_len + self.OBJ_INDEX_PAGES_OBJ_IDS_LIM = (self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED) // self.block_ix_len + + +class SpiffsFullError(RuntimeError): + pass + + +class SpiffsPage(object): + _endianness_dict = { + 'little': '<', + 'big': '>' + } + + _len_dict = { + 1: 'B', + 2: 'H', + 4: 'I', + 8: 'Q' + } + + def __init__(self, bix, build_config): # type: (int, SpiffsBuildConfig) -> None + self.build_config = build_config + self.bix = bix + + def to_binary(self): # type: () -> bytes + raise NotImplementedError() + + +class SpiffsObjPageWithIdx(SpiffsPage): + def __init__(self, obj_id, build_config): # type: (int, SpiffsBuildConfig) -> None + super(SpiffsObjPageWithIdx, self).__init__(0, build_config) + self.obj_id = obj_id + + def to_binary(self): # type: () -> bytes + raise NotImplementedError() + + +class SpiffsObjLuPage(SpiffsPage): + def __init__(self, bix, build_config): # type: (int, SpiffsBuildConfig) -> None + SpiffsPage.__init__(self, bix, build_config) + + self.obj_ids_limit = self.build_config.OBJ_LU_PAGES_OBJ_IDS_LIM + self.obj_ids = list() # type: typing.List[ObjIdsItem] + + def _calc_magic(self, blocks_lim): # type: (int) -> int + # Calculate the magic value mirroring computation done by the macro SPIFFS_MAGIC defined in + # spiffs_nucleus.h + magic = 0x20140529 ^ self.build_config.page_size + if self.build_config.use_magic_len: + magic = magic ^ (blocks_lim - self.bix) + # narrow the result to build_config.obj_id_len bytes + mask = (2 << (8 * self.build_config.obj_id_len)) - 1 + return magic & mask + + def register_page(self, page): # type: (TSP) -> None + if not self.obj_ids_limit > 0: + raise SpiffsFullError() + + obj_id = (page.obj_id, page.__class__) + self.obj_ids.append(obj_id) + self.obj_ids_limit -= 1 + + def to_binary(self): # type: () -> bytes + img = b'' + + for (obj_id, page_type) in self.obj_ids: + if page_type == SpiffsObjIndexPage: + obj_id ^= (1 << ((self.build_config.obj_id_len * 8) - 1)) + img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[self.build_config.obj_id_len], obj_id) + + assert len(img) <= self.build_config.page_size + + img += b'\xFF' * (self.build_config.page_size - len(img)) + + return img + + def magicfy(self, blocks_lim): # type: (int) -> None + # Only use magic value if no valid obj id has been written to the spot, which is the + # spot taken up by the last obj id on last lookup page. The parent is responsible + # for determining which is the last lookup page and calling this function. + remaining = self.obj_ids_limit + empty_obj_id_dict = { + 1: 0xFF, + 2: 0xFFFF, + 4: 0xFFFFFFFF, + 8: 0xFFFFFFFFFFFFFFFF + } + if remaining >= 2: + for i in range(remaining): + if i == remaining - 2: + self.obj_ids.append((self._calc_magic(blocks_lim), SpiffsObjDataPage)) + break + else: + self.obj_ids.append((empty_obj_id_dict[self.build_config.obj_id_len], SpiffsObjDataPage)) + self.obj_ids_limit -= 1 + + +class SpiffsObjIndexPage(SpiffsObjPageWithIdx): + def __init__(self, obj_id, span_ix, size, name, build_config + ): # type: (int, int, int, str, SpiffsBuildConfig) -> None + super(SpiffsObjIndexPage, self).__init__(obj_id, build_config) + self.span_ix = span_ix + self.name = name + self.size = size + + if self.span_ix == 0: + self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM + else: + self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_LIM + + self.pages = list() # type: typing.List[int] + + def register_page(self, page): # type: (SpiffsObjDataPage) -> None + if not self.pages_lim > 0: + raise SpiffsFullError + + self.pages.append(page.offset) + self.pages_lim -= 1 + + def to_binary(self): # type: () -> bytes + obj_id = self.obj_id ^ (1 << ((self.build_config.obj_id_len * 8) - 1)) + img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[self.build_config.obj_id_len] + + SpiffsPage._len_dict[self.build_config.span_ix_len] + + SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN], + obj_id, + self.span_ix, + SPIFFS_PH_FLAG_USED_FINAL_INDEX) + + # Add padding before the object index page specific information + img += b'\xFF' * self.build_config.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD + + # If this is the first object index page for the object, add filname, type + # and size information + if self.span_ix == 0: + img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[SPIFFS_PH_IX_SIZE_LEN] + + SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN], + self.size, + SPIFFS_TYPE_FILE) + + img += self.name.encode() + (b'\x00' * ( + (self.build_config.obj_name_len - len(self.name)) + + self.build_config.meta_len + + self.build_config.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED_PAD)) + + # Finally, add the page index of daa pages + for page in self.pages: + page = page >> int(math.log(self.build_config.page_size, 2)) + img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[self.build_config.page_ix_len], page) + + assert len(img) <= self.build_config.page_size + + img += b'\xFF' * (self.build_config.page_size - len(img)) + + return img + + +class SpiffsObjDataPage(SpiffsObjPageWithIdx): + def __init__(self, offset, obj_id, span_ix, contents, build_config + ): # type: (int, int, int, bytes, SpiffsBuildConfig) -> None + super(SpiffsObjDataPage, self).__init__(obj_id, build_config) + self.span_ix = span_ix + self.contents = contents + self.offset = offset + + def to_binary(self): # type: () -> bytes + img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + + SpiffsPage._len_dict[self.build_config.obj_id_len] + + SpiffsPage._len_dict[self.build_config.span_ix_len] + + SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN], + self.obj_id, + self.span_ix, + SPIFFS_PH_FLAG_USED_FINAL) + + img += self.contents + + assert len(img) <= self.build_config.page_size + + img += b'\xFF' * (self.build_config.page_size - len(img)) + + return img + + +class SpiffsBlock(object): + def _reset(self): # type: () -> None + self.cur_obj_index_span_ix = 0 + self.cur_obj_data_span_ix = 0 + self.cur_obj_id = 0 + self.cur_obj_idx_page = None # type: typing.Optional[SpiffsObjIndexPage] + + def __init__(self, bix, build_config): # type: (int, SpiffsBuildConfig) -> None + self.build_config = build_config + self.offset = bix * self.build_config.block_size + self.remaining_pages = self.build_config.OBJ_USABLE_PAGES_PER_BLOCK + self.pages = list() # type: typing.List[SpiffsPage] + self.bix = bix + + lu_pages = list() + for i in range(self.build_config.OBJ_LU_PAGES_PER_BLOCK): + page = SpiffsObjLuPage(self.bix, self.build_config) + lu_pages.append(page) + + self.pages.extend(lu_pages) + + self.lu_page_iter = iter(lu_pages) + self.lu_page = next(self.lu_page_iter) + + self._reset() + + def _register_page(self, page): # type: (TSP) -> None + if isinstance(page, SpiffsObjDataPage): + assert self.cur_obj_idx_page is not None + self.cur_obj_idx_page.register_page(page) # can raise SpiffsFullError + + try: + self.lu_page.register_page(page) + except SpiffsFullError: + self.lu_page = next(self.lu_page_iter) + try: + self.lu_page.register_page(page) + except AttributeError: # no next lookup page + # Since the amount of lookup pages is pre-computed at every block instance, + # this should never occur + raise RuntimeError('invalid attempt to add page to a block when there is no more space in lookup') + + self.pages.append(page) + + def begin_obj(self, obj_id, size, name, obj_index_span_ix=0, obj_data_span_ix=0 + ): # type: (int, int, str, int, int) -> None + if not self.remaining_pages > 0: + raise SpiffsFullError() + self._reset() + + self.cur_obj_id = obj_id + self.cur_obj_index_span_ix = obj_index_span_ix + self.cur_obj_data_span_ix = obj_data_span_ix + + page = SpiffsObjIndexPage(obj_id, self.cur_obj_index_span_ix, size, name, self.build_config) + self._register_page(page) + + self.cur_obj_idx_page = page + + self.remaining_pages -= 1 + self.cur_obj_index_span_ix += 1 + + def update_obj(self, contents): # type: (bytes) -> None + if not self.remaining_pages > 0: + raise SpiffsFullError() + page = SpiffsObjDataPage(self.offset + (len(self.pages) * self.build_config.page_size), + self.cur_obj_id, self.cur_obj_data_span_ix, contents, self.build_config) + + self._register_page(page) + + self.cur_obj_data_span_ix += 1 + self.remaining_pages -= 1 + + def end_obj(self): # type: () -> None + self._reset() + + def is_full(self): # type: () -> bool + return self.remaining_pages <= 0 + + def to_binary(self, blocks_lim): # type: (int) -> bytes + img = b'' + + if self.build_config.use_magic: + for (idx, page) in enumerate(self.pages): + if idx == self.build_config.OBJ_LU_PAGES_PER_BLOCK - 1: + assert isinstance(page, SpiffsObjLuPage) + page.magicfy(blocks_lim) + img += page.to_binary() + else: + for page in self.pages: + img += page.to_binary() + + assert len(img) <= self.build_config.block_size + + img += b'\xFF' * (self.build_config.block_size - len(img)) + return img + + +class SpiffsFS(object): + def __init__(self, img_size, build_config): # type: (int, SpiffsBuildConfig) -> None + if img_size % build_config.block_size != 0: + raise RuntimeError('image size should be a multiple of block size') + + self.img_size = img_size + self.build_config = build_config + + self.blocks = list() # type: typing.List[SpiffsBlock] + self.blocks_lim = self.img_size // self.build_config.block_size + self.remaining_blocks = self.blocks_lim + self.cur_obj_id = 1 # starting object id + + def _create_block(self): # type: () -> SpiffsBlock + if self.is_full(): + raise SpiffsFullError('the image size has been exceeded') + + block = SpiffsBlock(len(self.blocks), self.build_config) + self.blocks.append(block) + self.remaining_blocks -= 1 + return block + + def is_full(self): # type: () -> bool + return self.remaining_blocks <= 0 + + def create_file(self, img_path, file_path): # type: (str, str) -> None + if len(img_path) > self.build_config.obj_name_len: + raise RuntimeError("object name '%s' too long" % img_path) + + name = img_path + + with open(file_path, 'rb') as obj: + contents = obj.read() + + stream = io.BytesIO(contents) + + try: + block = self.blocks[-1] + block.begin_obj(self.cur_obj_id, len(contents), name) + except (IndexError, SpiffsFullError): + block = self._create_block() + block.begin_obj(self.cur_obj_id, len(contents), name) + + contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN) + + while contents_chunk: + try: + block = self.blocks[-1] + try: + # This can fail because either (1) all the pages in block have been + # used or (2) object index has been exhausted. + block.update_obj(contents_chunk) + except SpiffsFullError: + # If its (1), use the outer exception handler + if block.is_full(): + raise SpiffsFullError + # If its (2), write another object index page + block.begin_obj(self.cur_obj_id, len(contents), name, + obj_index_span_ix=block.cur_obj_index_span_ix, + obj_data_span_ix=block.cur_obj_data_span_ix) + continue + except (IndexError, SpiffsFullError): + # All pages in the block have been exhausted. Create a new block, copying + # the previous state of the block to a new one for the continuation of the + # current object + prev_block = block + block = self._create_block() + block.cur_obj_id = prev_block.cur_obj_id + block.cur_obj_idx_page = prev_block.cur_obj_idx_page + block.cur_obj_data_span_ix = prev_block.cur_obj_data_span_ix + block.cur_obj_index_span_ix = prev_block.cur_obj_index_span_ix + continue + + contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN) + + block.end_obj() + + self.cur_obj_id += 1 + + def to_binary(self): # type: () -> bytes + img = b'' + all_blocks = [] + for block in self.blocks: + all_blocks.append(block.to_binary(self.blocks_lim)) + bix = len(self.blocks) + if self.build_config.use_magic: + # Create empty blocks with magic numbers + while self.remaining_blocks > 0: + block = SpiffsBlock(bix, self.build_config) + all_blocks.append(block.to_binary(self.blocks_lim)) + self.remaining_blocks -= 1 + bix += 1 + else: + # Just fill remaining spaces FF's + all_blocks.append(b'\xFF' * (self.img_size - len(all_blocks) * self.build_config.block_size)) + img += b''.join([blk for blk in all_blocks]) + return img + + +class CustomHelpFormatter(argparse.HelpFormatter): + """ + Similar to argparse.ArgumentDefaultsHelpFormatter, except it + doesn't add the default value if "(default:" is already present. + This helps in the case of options with action="store_false", like + --no-magic or --no-magic-len. + """ + def _get_help_string(self, action): # type: (argparse.Action) -> str + if action.help is None: + return '' + if '%(default)' not in action.help and '(default:' not in action.help: + if action.default is not argparse.SUPPRESS: + defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE] + if action.option_strings or action.nargs in defaulting_nargs: + return action.help + ' (default: %(default)s)' + return action.help + + +def main(): # type: () -> None + parser = argparse.ArgumentParser(description='SPIFFS Image Generator', + formatter_class=CustomHelpFormatter) + + parser.add_argument('image_size', + help='Size of the created image') + + parser.add_argument('base_dir', + help='Path to directory from which the image will be created') + + parser.add_argument('output_file', + help='Created image output file path') + + parser.add_argument('--page-size', + help='Logical page size. Set to value same as CONFIG_SPIFFS_PAGE_SIZE.', + type=int, + default=256) + + parser.add_argument('--block-size', + help="Logical block size. Set to the same value as the flash chip's sector size (g_rom_flashchip.sector_size).", + type=int, + default=4096) + + parser.add_argument('--obj-name-len', + help='File full path maximum length. Set to value same as CONFIG_SPIFFS_OBJ_NAME_LEN.', + type=int, + default=32) + + parser.add_argument('--meta-len', + help='File metadata length. Set to value same as CONFIG_SPIFFS_META_LENGTH.', + type=int, + default=4) + + parser.add_argument('--use-magic', + dest='use_magic', + help='Use magic number to create an identifiable SPIFFS image. Specify if CONFIG_SPIFFS_USE_MAGIC.', + action='store_true') + + parser.add_argument('--no-magic', + dest='use_magic', + help='Inverse of --use-magic (default: --use-magic is enabled)', + action='store_false') + + parser.add_argument('--use-magic-len', + dest='use_magic_len', + help='Use position in memory to create different magic numbers for each block. Specify if CONFIG_SPIFFS_USE_MAGIC_LENGTH.', + action='store_true') + + parser.add_argument('--no-magic-len', + dest='use_magic_len', + help='Inverse of --use-magic-len (default: --use-magic-len is enabled)', + action='store_false') + + parser.add_argument('--follow-symlinks', + help='Take into account symbolic links during partition image creation.', + action='store_true') + + parser.add_argument('--big-endian', + help='Specify if the target architecture is big-endian. If not specified, little-endian is assumed.', + action='store_true') + + parser.add_argument('--aligned-obj-ix-tables', + action='store_true', + help='Use aligned object index tables. Specify if SPIFFS_ALIGNED_OBJECT_INDEX_TABLES is set.') + + parser.set_defaults(use_magic=True, use_magic_len=True) + + args = parser.parse_args() + + if not os.path.exists(args.base_dir): + raise RuntimeError('given base directory %s does not exist' % args.base_dir) + + with open(args.output_file, 'wb') as image_file: + image_size = int(args.image_size, 0) + spiffs_build_default = SpiffsBuildConfig(args.page_size, SPIFFS_PAGE_IX_LEN, + args.block_size, SPIFFS_BLOCK_IX_LEN, args.meta_len, + args.obj_name_len, SPIFFS_OBJ_ID_LEN, SPIFFS_SPAN_IX_LEN, + True, True, 'big' if args.big_endian else 'little', + args.use_magic, args.use_magic_len, args.aligned_obj_ix_tables) + + spiffs = SpiffsFS(image_size, spiffs_build_default) + + for root, dirs, files in os.walk(args.base_dir, followlinks=args.follow_symlinks): + for f in files: + full_path = os.path.join(root, f) + spiffs.create_file('/' + os.path.relpath(full_path, args.base_dir).replace('\\', '/'), full_path) + + image = spiffs.to_binary() + + image_file.write(image) + + +if __name__ == '__main__': + main() diff --git a/Remote.h b/Remote.h new file mode 100755 index 0000000..d8a0fae --- /dev/null +++ b/Remote.h @@ -0,0 +1,217 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include + +#if CONFIG_IDF_TARGET_ESP32 + #include "esp32/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32S2 + #include "esp32s2/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32C3 + #include "esp32c3/rom/rtc.h" +#elif CONFIG_IDF_TARGET_ESP32S3 + #include "esp32s3/rom/rtc.h" +#else + #error Target CONFIG_IDF_TARGET is not supported +#endif + +#define WIFI_UPDATE_INTERVAL_MS 500 +#define WR_SOCKET_TIMEOUT 6 +#define WR_READ_TIMEOUT_MS 6500 +#define WR_RECONNECT_INTERVAL_MS 10000 + +uint32_t wifi_update_interval_ms = WIFI_UPDATE_INTERVAL_MS; +uint32_t last_wifi_update = 0; +uint32_t wr_last_connect_try = 0; +uint32_t wr_last_read = 0; + +WiFiClient connection; +WiFiServer remote_listener(7633, 1); +IPAddress ap_ip(10, 0, 0, 1); +IPAddress ap_nm(255, 255, 255, 0); +IPAddress wr_device_ip; +char wr_hostname[10]; +wl_status_t wr_wifi_status = WL_IDLE_STATUS; + +uint8_t wifi_mode = WIFI_OFF; +bool wifi_init_ran = false; +bool wifi_initialized = false; + +char wr_ssid[33]; +char wr_psk[33]; + +extern void host_disconnected(); + +void wifi_dbg(String msg) { Serial.print("[WiFi] "); Serial.println(msg); } + +uint8_t wifi_remote_mode() { return wifi_mode; } + +bool wifi_is_connected() { return (wr_wifi_status == WL_CONNECTED); } +bool wifi_host_is_connected() { if (connection) { return true; } else { return false; } } + +void wifi_remote_start_ap() { + WiFi.mode(WIFI_AP); + if (wr_ssid[0] != 0x00) { + if (wr_psk[0] != 0x00) { WiFi.softAP(wr_ssid, wr_psk, wr_channel); } + else { WiFi.softAP(wr_ssid, NULL, wr_channel); } + } else { + if (wr_psk[0] != 0x00) { WiFi.softAP(bt_devname, wr_psk, wr_channel); } + else { WiFi.softAP(bt_devname, NULL, wr_channel); } + } + delay(150); + WiFi.softAPConfig(ap_ip, ap_ip, ap_nm); + wifi_initialized = true; +} + +void wifi_remote_start_sta() { + WiFi.mode(WIFI_STA); + + uint8_t ip[4]; bool ip_ok = true; + for (uint8_t i = 0; i < 4; i++) { ip[i] = EEPROM.read(config_addr(ADDR_CONF_IP+i)); } + if (ip[0]==0x00 && ip[1]==0x00 && ip[2]==0x00 && ip[3]==0x00) { ip_ok = false; } + if (ip[0]==0xFF && ip[1]==0xFF && ip[2]==0xFF && ip[3]==0xFF) { ip_ok = false; } + + uint8_t nm[4]; bool nm_ok = true; + for (uint8_t i = 0; i < 4; i++) { nm[i] = EEPROM.read(config_addr(ADDR_CONF_NM+i)); } + if (nm[0]==0x00 && nm[1]==0x00 && nm[2]==0x00 && nm[3]==0x00) { nm_ok = false; } + if (nm[0]==0xFF && nm[1]==0xFF && nm[2]==0xFF && nm[3]==0xFF) { nm_ok = false; } + + if (ip_ok && nm_ok) { + IPAddress sta_ip(ip[0], ip[1], ip[2], ip[3]); + IPAddress sta_nm(nm[0], nm[1], nm[2], nm[3]); + // Gateway = x.x.x.1 (first host on subnet), DNS = 8.8.8.8 + 1.1.1.1 + IPAddress sta_gw(ip[0], ip[1], ip[2], 1); + IPAddress dns1(8, 8, 8, 8); + IPAddress dns2(1, 1, 1, 1); + WiFi.config(sta_ip, sta_gw, sta_nm, dns1, dns2); + } + + delay(100); + if (wr_ssid[0] != 0x00) { + if (wr_psk[0] != 0x00) { WiFi.begin(wr_ssid, wr_psk); } + else { WiFi.begin(wr_ssid); } + } + + delay(500); + wr_wifi_status = WiFi.status(); + wifi_initialized = true; + wr_last_connect_try = millis(); +} + +void wifi_remote_stop() { + WiFi.softAPdisconnect(true); + WiFi.disconnect(true, true); + WiFi.mode(WIFI_MODE_NULL); + wifi_initialized = false; +} + +void wifi_remote_start() { + if (wifi_mode == WR_WIFI_AP) { wifi_remote_start_ap(); } + else if (wifi_mode == WR_WIFI_STA) { wifi_remote_start_sta(); } + else { wifi_remote_stop(); } + + if (wifi_initialized == true) { + remote_listener.begin(); + remote_listener.setTimeout(WR_SOCKET_TIMEOUT); + wr_state = WR_STATE_ON; + } else { remote_listener.end(); wr_state = WR_STATE_OFF; } +} + +void wifi_remote_init() { + memcpy(wr_hostname, bt_devname, 5); + memcpy(wr_hostname+5, bt_devname+6, 4); + wr_hostname[9] = 0x00; + WiFi.softAPdisconnect(true); + WiFi.disconnect(true, true); + WiFi.mode(WIFI_MODE_NULL); + WiFi.setHostname(wr_hostname); + + wr_ssid[32] = 0x00; wr_psk[32] = 0x00; + for (uint8_t i = 0; i < 32; i++) { wr_ssid[i] = EEPROM.read(config_addr(ADDR_CONF_SSID+i)); if (wr_ssid[i] == 0xFF) { wr_ssid[i] = 0x00; } } + for (uint8_t i = 0; i < 32; i++) { wr_psk[i] = EEPROM.read(config_addr(ADDR_CONF_PSK+i)); if (wr_psk[i] == 0xFF) { wr_psk[i] = 0x00; } } + wr_channel = EEPROM.read(eeprom_addr(ADDR_CONF_WCHN)); if (wr_channel < 1 || wr_channel > 14) { wr_channel = WR_CHANNEL_DEFAULT; } + wifi_remote_start(); + wifi_init_ran = true; +} + +void wifi_remote_close_all() { + // wifi_dbg("Close all"); // TODO: Remove debug + if (connection) { connection.stop(); } + WiFiClient client = remote_listener.available(); + while (client) { client.stop(); client = remote_listener.available(); } + wr_state = WR_STATE_ON; +} + +void wifi_remote_check_active() { + if (millis()-wr_last_read >= WR_READ_TIMEOUT_MS) { + // wifi_dbg("Connection activity timed out"); // TODO: Remove debug + if (connection && connection.connected()) { + connection.stop(); + wifi_remote_close_all(); + host_disconnected(); + } + } +} + +bool wifi_remote_available() { + if (connection) { + if (connection.connected()) { + if (connection.available()) { wr_last_read = millis(); return true; } + else { wifi_remote_check_active(); return false; } + } else { + // wifi_dbg("Client disconnected"); // TODO: Remove debug + wifi_remote_close_all(); + return false; + } + } else { + WiFiClient client = remote_listener.available(); + if (!client) { return false; } + else { + // wifi_dbg("Client connected"); // TODO: Remove debug + connection = client; + wr_state = WR_STATE_CONNECTED; + wr_last_read = millis(); + if (connection.available()) { return true; } + else { return false; } + } + } +} + +uint8_t wifi_remote_read() { + if (connection && connection.available()) { return connection.read(); } + else { + // wifi_dbg("Error: No data to read from TCP socket"); // TODO: Remove debug + if (connection) { wifi_remote_close_all(); } + return 0xC0; + } +} + +void wifi_remote_write(uint8_t byte) { if (connection) { connection.write(byte); } } + +void wifi_update_status() { + wr_wifi_status = WiFi.status(); + if (wr_wifi_status == WL_CONNECTED) { wr_device_ip = WiFi.localIP(); } + if (wifi_mode == WR_WIFI_AP && wifi_initialized) { wr_device_ip = WiFi.softAPIP(); wr_wifi_status = WL_CONNECTED; } + if (wifi_init_ran && wifi_mode == WR_WIFI_STA && wr_wifi_status != WL_CONNECTED) { + if (millis()-wr_last_connect_try >= WR_RECONNECT_INTERVAL_MS) { wifi_remote_init(); } + } +} + +void update_wifi() { + if (millis()-last_wifi_update >= wifi_update_interval_ms) { + wifi_update_status(); + last_wifi_update = millis(); + } +} \ No newline at end of file diff --git a/ST7789.h b/ST7789.h new file mode 100755 index 0000000..85012e8 --- /dev/null +++ b/ST7789.h @@ -0,0 +1,440 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * Copyright (c) 2024 by Heltec AutoMation + * 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. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef ST7789Spi_h +#define ST7789Spi_h + +#include "OLEDDisplay.h" +#include + + +#define ST_CMD_DELAY 0x80 // special signifier for command lists + +#define ST77XX_NOP 0x00 +#define ST77XX_SWRESET 0x01 +#define ST77XX_RDDID 0x04 +#define ST77XX_RDDST 0x09 + +#define ST77XX_SLPIN 0x10 +#define ST77XX_SLPOUT 0x11 +#define ST77XX_PTLON 0x12 +#define ST77XX_NORON 0x13 + +#define ST77XX_INVOFF 0x20 +#define ST77XX_INVON 0x21 +#define ST77XX_DISPOFF 0x28 +#define ST77XX_DISPON 0x29 +#define ST77XX_CASET 0x2A +#define ST77XX_RASET 0x2B +#define ST77XX_RAMWR 0x2C +#define ST77XX_RAMRD 0x2E + +#define ST77XX_PTLAR 0x30 +#define ST77XX_TEOFF 0x34 +#define ST77XX_TEON 0x35 +#define ST77XX_MADCTL 0x36 +#define ST77XX_COLMOD 0x3A + +#define ST77XX_MADCTL_MY 0x80 +#define ST77XX_MADCTL_MX 0x40 +#define ST77XX_MADCTL_MV 0x20 +#define ST77XX_MADCTL_ML 0x10 +#define ST77XX_MADCTL_RGB 0x00 + +#define ST77XX_RDID1 0xDA +#define ST77XX_RDID2 0xDB +#define ST77XX_RDID3 0xDC +#define ST77XX_RDID4 0xDD + +// Some ready-made 16-bit ('565') color settings: +#define ST77XX_BLACK 0x0000 +#define ST77XX_WHITE 0xFFFF +#define ST77XX_RED 0xF800 +#define ST77XX_GREEN 0x07E0 +#define ST77XX_BLUE 0x001F +#define ST77XX_CYAN 0x07FF +#define ST77XX_MAGENTA 0xF81F +#define ST77XX_YELLOW 0xFFE0 +#define ST77XX_ORANGE 0xFC00 + +#define LED_A_ON LOW + +#ifdef ESP_PLATFORM +#undef LED_A_ON +#define LED_A_ON HIGH +#define rtos_free free +#define rtos_malloc malloc +//SPIClass SPI1(HSPI); +#endif +class ST7789Spi : public OLEDDisplay { + private: + uint8_t _rst; + uint8_t _dc; + uint8_t _cs; + uint8_t _ledA; + int _miso; + int _mosi; + int _clk; + SPIClass * _spi; + SPISettings _spiSettings; + uint16_t _RGB=0xFFFF; + uint8_t _buffheight; + public: + /* pass _cs as -1 to indicate "do not use CS pin", for cases where it is hard wired low */ + ST7789Spi(SPIClass *spiClass,uint8_t _rst, uint8_t _dc, uint8_t _cs, OLEDDISPLAY_GEOMETRY g = GEOMETRY_RAWMODE,uint16_t width=240,uint16_t height=320,int mosi=-1,int miso=-1,int clk=-1) { + this->_spi = spiClass; + this->_rst = _rst; + this->_dc = _dc; + this->_cs = _cs; + this->_mosi=mosi; + this->_miso=miso; + this->_clk=clk; + //this->_ledA = _ledA; + _spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0); + setGeometry(g,width,height); + } + + bool connect(){ + this->_buffheight=displayHeight / 8; + this->_buffheight+=displayHeight % 8 ? 1:0; + pinMode(_cs, OUTPUT); + pinMode(_dc, OUTPUT); + //pinMode(_ledA, OUTPUT); + if (_cs != (uint8_t) -1) { + pinMode(_cs, OUTPUT); + } + pinMode(_rst, OUTPUT); + +#ifdef ESP_PLATFORM + _spi->begin(_clk,_miso,_mosi,-1); +#else + _spi->begin(); +#endif + _spi->setClockDivider (SPI_CLOCK_DIV2); + + // Pulse Reset low for 10ms + digitalWrite(_rst, HIGH); + delay(1); + digitalWrite(_rst, LOW); + delay(10); + digitalWrite(_rst, HIGH); + _spi->begin (); + //digitalWrite(_ledA, LED_A_ON); + return true; + } + + void display(void) { + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + + uint16_t minBoundY = UINT16_MAX; + uint16_t maxBoundY = 0; + + uint16_t minBoundX = UINT16_MAX; + uint16_t maxBoundX = 0; + + uint16_t x, y; + + // Calculate the Y bounding box of changes + // and copy buffer[pos] to buffer_back[pos]; + for (y = 0; y < _buffheight; y++) { + for (x = 0; x < displayWidth; x++) { + //Serial.printf("x %d y %d\r\n",x,y); + uint16_t pos = x + y * displayWidth; + if (buffer[pos] != buffer_back[pos]) { + minBoundY = min(minBoundY, y); + maxBoundY = max(maxBoundY, y); + minBoundX = min(minBoundX, x); + maxBoundX = max(maxBoundX, x); + } + buffer_back[pos] = buffer[pos]; + } + yield(); + } + + // If the minBoundY wasn't updated + // we can savely assume that buffer_back[pos] == buffer[pos] + // holdes true for all values of pos + if (minBoundY == UINT16_MAX) return; + + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + + for (y = minBoundY; y <= maxBoundY; y++) + { + for(int temp = 0; temp<8;temp++) + { + //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1); + setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1); + //setAddrWindow(y*8+temp,minBoundX,1,maxBoundX-minBoundX+1); + uint32_t const pixbufcount = maxBoundX-minBoundX+1; + uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount); + for (x = minBoundX; x <= maxBoundX; x++) + { + pixbuf[x-minBoundX] = ((buffer[x + y * displayWidth]>>temp)&0x01)==1?_RGB:0; + } +#ifdef ESP_PLATFORM + _spi->transferBytes((uint8_t *)pixbuf, NULL, 2 * pixbufcount); +#else + _spi->transfer(pixbuf, NULL, 2 * pixbufcount); +#endif + rtos_free(pixbuf); + } + } + _spi->endTransaction(); + set_CS(HIGH); + + #else + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + uint8_t x, y; + for (y = 0; y < _buffheight; y++) + { + for(int temp = 0; temp<8;temp++) + { + //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1); + //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1); + setAddrWindow(y*8+temp,0,1,displayWidth); + uint32_t const pixbufcount = displayWidth; + uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount); + for (x = 0; x < displayWidth; x++) + { + pixbuf[x] = ((buffer[x + y * displayWidth]>>temp)&0x01)==1?_RGB:0; + } +#ifdef ESP_PLATFORM + _spi->transferBytes((uint8_t *)pixbuf, NULL, 2 * pixbufcount); +#else + _spi->transfer(pixbuf, NULL, 2 * pixbufcount); +#endif + rtos_free(pixbuf); + } + } + _spi->endTransaction(); + set_CS(HIGH); + + #endif + } + + virtual void resetOrientation() { + uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX; + sendCommand(ST77XX_MADCTL); + WriteData(madctl); + delay(10); + } + + virtual void flipScreenVertically() { + uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MY; + sendCommand(ST77XX_MADCTL); + WriteData(madctl); + delay(10); + } + + virtual void mirrorScreen() { + uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX|ST77XX_MADCTL_MY; + sendCommand(ST77XX_MADCTL); + WriteData(madctl); + delay(10); + } + + virtual void setRotation(uint8_t r) { + uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX; + if (r == 1) { madctl = 0xC0; } + if (r == 2) { madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MY; } + if (r == 3) { madctl = 0x00; } + sendCommand(ST77XX_MADCTL); + WriteData(madctl); + delay(10); + } + + void setRGB(uint16_t c) + { + + this->_RGB=0x00|c>>8|c<<8&0xFF00; + } + + void displayOn(void) { + //sendCommand(DISPLAYON); + } + + void displayOff(void) { + //sendCommand(DISPLAYOFF); + } + +//#define ST77XX_MADCTL_MY 0x80 +//#define ST77XX_MADCTL_MX 0x40 +//#define ST77XX_MADCTL_MV 0x20 +//#define ST77XX_MADCTL_ML 0x10 + protected: + // Send all the init commands + virtual void sendInitCommands() + { + sendCommand(ST77XX_SWRESET); // 1: Software reset, no args, w/delay + delay(150); + + sendCommand(ST77XX_SLPOUT); // 2: Out of sleep mode, no args, w/delay + delay(10); + + sendCommand(ST77XX_COLMOD); // 3: Set color mode, 16-bit color + WriteData(0x55); + delay(10); + + sendCommand(ST77XX_MADCTL); // 4: Mem access ctrl (directions), Row/col addr, bottom-top refresh + WriteData(0x08); + + sendCommand(ST77XX_CASET); // 5: Column addr set, + WriteData(0x00); + WriteData(0x00); // XSTART = 0 + WriteData(0x00); + WriteData(240); // XEND = 240 + + sendCommand(ST77XX_RASET); // 6: Row addr set, + WriteData(0x00); + WriteData(0x00); // YSTART = 0 + WriteData(320>>8); + WriteData(320&0xFF); // YSTART = 320 + + sendCommand(ST77XX_SLPOUT); // 7: hack + delay(10); + + sendCommand(ST77XX_NORON); // 8: Normal display on, no args, w/delay + delay(10); + + sendCommand(ST77XX_DISPON); // 9: Main screen turn on, no args, delay + delay(10); + + sendCommand(ST77XX_INVON); // 10: invert + delay(10); + + //uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MX; + uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX; + sendCommand(ST77XX_MADCTL); + WriteData(madctl); + delay(10); + setRGB(ST77XX_GREEN); + } + + + private: + + void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + x += (320-displayWidth)/2; + y += (240-displayHeight)/2; + uint32_t xa = ((uint32_t)x << 16) | (x + w - 1); + uint32_t ya = ((uint32_t)y << 16) | (y + h - 1); + + writeCommand(ST77XX_CASET); // Column addr set + SPI_WRITE32(xa); + + writeCommand(ST77XX_RASET); // Row addr set + SPI_WRITE32(ya); + + writeCommand(ST77XX_RAMWR); // write to RAM + } + int getBufferOffset(void) { + return 0; + } + inline void set_CS(bool level) { + if (_cs != (uint8_t) -1) { + digitalWrite(_cs, level); + } + }; + inline void sendCommand(uint8_t com) __attribute__((always_inline)){ + set_CS(HIGH); + digitalWrite(_dc, LOW); + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + _spi->transfer(com); + _spi->endTransaction(); + set_CS(HIGH); + digitalWrite(_dc, HIGH); + } + + inline void WriteData(uint8_t data) __attribute__((always_inline)){ + digitalWrite(_cs, LOW); + _spi->beginTransaction(_spiSettings); + _spi->transfer(data); + _spi->endTransaction(); + digitalWrite(_cs, HIGH); + } + void SPI_WRITE32(uint32_t l) + { + _spi->transfer(l >> 24); + _spi->transfer(l >> 16); + _spi->transfer(l >> 8); + _spi->transfer(l); + } + void writeCommand(uint8_t cmd) { + digitalWrite(_dc, LOW); + _spi->transfer(cmd); + digitalWrite(_dc, HIGH); + } + +// Private functions + void setGeometry(OLEDDISPLAY_GEOMETRY g, uint16_t width, uint16_t height) { + this->geometry = g; + + switch (g) { + case GEOMETRY_128_128: + this->displayWidth = 128; + this->displayHeight = 128; + break; + case GEOMETRY_128_64: + this->displayWidth = 128; + this->displayHeight = 64; + break; + case GEOMETRY_128_32: + this->displayWidth = 128; + this->displayHeight = 32; + break; + case GEOMETRY_64_48: + this->displayWidth = 64; + this->displayHeight = 48; + break; + case GEOMETRY_64_32: + this->displayWidth = 64; + this->displayHeight = 32; + break; + case GEOMETRY_RAWMODE: + this->displayWidth = width > 0 ? width : 128; + this->displayHeight = height > 0 ? height : 64; + break; + } + uint8_t tmp=displayHeight % 8; + uint8_t _buffheight=displayHeight / 8; + + if(tmp!=0) + _buffheight++; + this->displayBufferSize = displayWidth * _buffheight ; + } + + + +}; + +#endif \ No newline at end of file diff --git a/TcpInterface.h b/TcpInterface.h new file mode 100644 index 0000000..a77916e --- /dev/null +++ b/TcpInterface.h @@ -0,0 +1,404 @@ +// Copyright (C) 2026, Boundary Mode Extension +// Based on microReticulum_Firmware by Mark Qvist +// +// TcpInterface — An RNS InterfaceImpl that bridges the WiFi TCP +// connection as a second RNS transport interface, enabling +// Boundary mode operation between LoRa and TCP/IP backbone. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +#ifndef TCP_INTERFACE_H +#define TCP_INTERFACE_H + +#ifdef HAS_RNS +#ifdef BOUNDARY_MODE + +#include +#include +#include +#include + +// ─── TCP Interface Configuration ───────────────────────────────────────────── +#define TCP_IF_DEFAULT_PORT 4242 +#define TCP_IF_MAX_CLIENTS 4 +#define TCP_IF_HW_MTU 1064 +#define TCP_IF_CONNECT_TIMEOUT 6000 // ms +#define TCP_IF_WRITE_TIMEOUT 2000 // ms — short to avoid WDT +#define TCP_IF_READ_TIMEOUT 120000 // ms — 2 minutes (backbone can go quiet) +#define TCP_IF_RECONNECT_MIN 10000 // ms — initial reconnect interval +#define TCP_IF_RECONNECT_MAX 120000 // ms — max backoff (2 minutes) +#define TCP_IF_KEEPALIVE_INTERVAL 30000 // ms — send empty HDLC frames to keep link alive +#define TCP_IF_POLL_INTERVAL 10 // ms + +// HDLC-like framing for TCP (matches Reticulum-rust tcp_interface) +#define HDLC_FLAG 0x7E +#define HDLC_ESC 0x7D +#define HDLC_ESC_MASK 0x20 + +// ─── TCP Interface Mode ────────────────────────────────────────────────────── +enum TcpIfMode { + TCP_IF_MODE_SERVER = 0, // Listen for incoming connections (from backbone rnsd) + TCP_IF_MODE_CLIENT = 1, // Connect out to a backbone rnsd TCP server +}; + +// ─── Client connection state ───────────────────────────────────────────────── +struct TcpClient { + WiFiClient client; + uint32_t last_activity; + bool active; + // HDLC deframe state + bool in_frame; + bool escape; + uint8_t rxbuf[TCP_IF_HW_MTU]; + uint16_t rxlen; +}; + +// ─── TcpInterface Class ───────────────────────────────────────────────────── +class TcpInterface : public RNS::InterfaceImpl { +public: + TcpInterface(TcpIfMode mode, uint16_t port = TCP_IF_DEFAULT_PORT, + const char* target_host = nullptr, uint16_t target_port = 0) + : RNS::InterfaceImpl("TcpInterface"), + _mode(mode), + _port(port), + _target_port(target_port), + _server(nullptr), + _num_clients(0), + _last_reconnect(0), + _last_keepalive(0), + _reconnect_interval(TCP_IF_RECONNECT_MIN), + _resolved_ip((uint32_t)0), + _consecutive_failures(0), + _started(false) + { + _IN = true; + _OUT = true; + _HW_MTU = TCP_IF_HW_MTU; + // Report low bitrate + small announce_cap so that Transport + // rate-limits announce forwarding through this interface. + // Without this the backbone floods the ESP32 with announces. + // 500 bps ≈ LoRa-class throughput; announce_cap = 2% max bandwidth. + _bitrate = 500; + _announce_cap = 2.0; + if (target_host != nullptr) { + strncpy(_target_host, target_host, sizeof(_target_host) - 1); + _target_host[sizeof(_target_host) - 1] = '\0'; + } else { + _target_host[0] = '\0'; + } + for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) { + _clients[i].active = false; + _clients[i].in_frame = false; + _clients[i].escape = false; + _clients[i].rxlen = 0; + _clients[i].last_activity = 0; + } + } + + virtual ~TcpInterface() { + stop(); + } + + // ─── Lifecycle ─────────────────────────────────────────────────────────── + bool start() { + if (_started) return true; + + if (_mode == TCP_IF_MODE_SERVER) { + _server = new WiFiServer(_port, TCP_IF_MAX_CLIENTS); + _server->begin(); + _server->setNoDelay(true); + Serial.printf("[TcpIF] Server listening on port %d\r\n", _port); + _started = true; + } else { + // Client mode — try initial connection + _started = true; + _connect_client(); + } + return _started; + } + + void stop() { + for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) { + if (_clients[i].active) { + _clients[i].client.stop(); + _clients[i].active = false; + } + } + if (_server) { + _server->end(); + delete _server; + _server = nullptr; + } + _started = false; + _num_clients = 0; + } + + // ─── Main loop — call from Arduino loop() ──────────────────────────────── + void loop() { + if (!_started) return; + + // Accept new connections in server mode + if (_mode == TCP_IF_MODE_SERVER && _server) { + WiFiClient newClient = _server->available(); + if (newClient) { + _accept_client(newClient); + } + } + + // Client mode reconnection (with WiFi check + exponential backoff) + if (_mode == TCP_IF_MODE_CLIENT && _num_clients == 0) { + uint32_t now = millis(); + if (now - _last_reconnect >= _reconnect_interval) { + if (WiFi.status() == WL_CONNECTED) { + _connect_client(); + } else { + // WiFi not connected — skip TCP attempt, just update timer + _last_reconnect = now; + } + } + } + + // Send keepalive (empty HDLC frames) to prevent read timeout on both sides + if (_num_clients > 0) { + uint32_t now = millis(); + if (now - _last_keepalive >= TCP_IF_KEEPALIVE_INTERVAL) { + _last_keepalive = now; + uint8_t ka[] = { HDLC_FLAG, HDLC_FLAG }; + for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) { + if (_clients[i].active && _clients[i].client.connected()) { + _clients[i].client.write(ka, 2); + } + } + } + } + + // Process incoming data from all active clients + for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) { + if (!_clients[i].active) continue; + + if (!_clients[i].client.connected()) { + Serial.printf("[TcpIF] Client %d disconnected\r\n", i); + _clients[i].client.stop(); + _clients[i].active = false; + _clients[i].in_frame = false; + _clients[i].escape = false; + _clients[i].rxlen = 0; + _num_clients--; + continue; + } + + // Check read timeout + if (_clients[i].last_activity > 0 && + (millis() - _clients[i].last_activity) > TCP_IF_READ_TIMEOUT) { + Serial.printf("[TcpIF] Client %d read timeout\r\n", i); + _clients[i].client.stop(); + _clients[i].active = false; + _clients[i].in_frame = false; + _clients[i].rxlen = 0; + _num_clients--; + continue; + } + + // Read available bytes and deframe + while (_clients[i].client.available()) { + uint8_t byte = _clients[i].client.read(); + _clients[i].last_activity = millis(); + _hdlc_deframe(i, byte); + } + } + } + + // ─── Stats ─────────────────────────────────────────────────────────────── + int clientCount() const { return _num_clients; } + bool isStarted() const { return _started; } + bool isConnected() const { return _num_clients > 0; } + +protected: + // ─── RNS InterfaceImpl: outgoing packet from RNS Transport ─────────────── + virtual void send_outgoing(const RNS::Bytes& data) override { + if (!_started || _num_clients == 0) return; + + // HDLC frame the data + uint8_t frame_buf[TCP_IF_HW_MTU * 2 + 4]; // worst case: every byte escaped + 2 flags + uint16_t flen = 0; + + frame_buf[flen++] = HDLC_FLAG; + for (size_t i = 0; i < data.size(); i++) { + uint8_t b = data.data()[i]; + if (b == HDLC_FLAG || b == HDLC_ESC) { + frame_buf[flen++] = HDLC_ESC; + frame_buf[flen++] = b ^ HDLC_ESC_MASK; + } else { + frame_buf[flen++] = b; + } + if (flen >= sizeof(frame_buf) - 4) break; // safety + } + frame_buf[flen++] = HDLC_FLAG; + + // Send to all connected clients (non-blocking: no flush) + for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) { + if (_clients[i].active && _clients[i].client.connected()) { + size_t written = _clients[i].client.write(frame_buf, flen); + if (written == 0) { + Serial.printf("[TcpIF] Write failed on client %d, dropping\r\n", i); + _clients[i].client.stop(); + _clients[i].active = false; + _clients[i].in_frame = false; + _clients[i].rxlen = 0; + _num_clients--; + } + } + } + yield(); // feed WDT between TCP writes and RNS processing + + // Post-send housekeeping + InterfaceImpl::handle_outgoing(data); + } + + // ─── RNS InterfaceImpl: incoming packet to RNS Transport ───────────────── + virtual void handle_incoming(const RNS::Bytes& data) override { + TRACEF("TcpInterface.handle_incoming: (%u bytes)", data.size()); + InterfaceImpl::handle_incoming(data); + } + +private: + // ─── HDLC byte-level deframing ────────────────────────────────────────── + void _hdlc_deframe(int idx, uint8_t byte) { + TcpClient& c = _clients[idx]; + + if (byte == HDLC_FLAG) { + if (c.in_frame && c.rxlen > 0) { + // End of frame — deliver to RNS + RNS::Bytes data(c.rxbuf, c.rxlen); + handle_incoming(data); + c.rxlen = 0; + } + c.in_frame = true; + c.escape = false; + c.rxlen = 0; + } else if (c.in_frame) { + if (c.escape) { + byte ^= HDLC_ESC_MASK; + c.escape = false; + if (c.rxlen < TCP_IF_HW_MTU) { + c.rxbuf[c.rxlen++] = byte; + } + } else if (byte == HDLC_ESC) { + c.escape = true; + } else { + if (c.rxlen < TCP_IF_HW_MTU) { + c.rxbuf[c.rxlen++] = byte; + } + } + } + } + + // ─── Accept a new server-mode client ───────────────────────────────────── + void _accept_client(WiFiClient& newClient) { + // Find a free slot + for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) { + if (!_clients[i].active) { + _clients[i].client = newClient; + _clients[i].client.setNoDelay(true); + _clients[i].client.setTimeout(TCP_IF_WRITE_TIMEOUT / 1000); + _clients[i].active = true; + _clients[i].in_frame = false; + _clients[i].escape = false; + _clients[i].rxlen = 0; + _clients[i].last_activity = millis(); + _num_clients++; + Serial.printf("[TcpIF] Client %d connected from %s\r\n", + i, _clients[i].client.remoteIP().toString().c_str()); + return; + } + } + // No free slots — reject + Serial.println("[TcpIF] Max clients reached, rejecting connection"); + newClient.stop(); + } + + // ─── Client-mode outbound connection ───────────────────────────────────── + void _connect_client() { + if (_target_host[0] == '\0') { + Serial.println("[TcpIF] No target host configured for client mode"); + return; + } + + WiFiClient client; + client.setTimeout(TCP_IF_CONNECT_TIMEOUT / 1000); + + bool connected = false; + + // Try cached IP first (avoids DNS lookup on every reconnect) + if (_resolved_ip != (uint32_t)0) { + Serial.printf("[TcpIF] Connecting to %s:%d (cached IP)...\r\n", _target_host, _target_port); + connected = client.connect(_resolved_ip, _target_port); + if (!connected) { + // Cached IP failed — clear cache and try fresh DNS + _resolved_ip = (uint32_t)0; + Serial.println("[TcpIF] Cached IP failed, retrying with DNS"); + } + } + + if (!connected) { + Serial.printf("[TcpIF] Connecting to %s:%d (DNS)...\r\n", _target_host, _target_port); + IPAddress resolved; + if (WiFi.hostByName(_target_host, resolved)) { + _resolved_ip = resolved; + Serial.printf("[TcpIF] Resolved %s -> %s\r\n", _target_host, resolved.toString().c_str()); + connected = client.connect(resolved, _target_port); + } else { + Serial.printf("[TcpIF] DNS failed for %s\r\n", _target_host); + } + } + + if (connected) { + client.setNoDelay(true); + client.setTimeout(TCP_IF_WRITE_TIMEOUT / 1000); + _clients[0].client = client; + _clients[0].active = true; + _clients[0].in_frame = false; + _clients[0].escape = false; + _clients[0].rxlen = 0; + _clients[0].last_activity = millis(); + _num_clients = 1; + _consecutive_failures = 0; + _reconnect_interval = TCP_IF_RECONNECT_MIN; + Serial.printf("[TcpIF] Connected to backbone at %s:%d\r\n", + _target_host, _target_port); + } else { + _consecutive_failures++; + // Exponential backoff: 10s -> 20s -> 40s -> 80s -> 120s (max) + _reconnect_interval = _reconnect_interval * 2; + if (_reconnect_interval > TCP_IF_RECONNECT_MAX) { + _reconnect_interval = TCP_IF_RECONNECT_MAX; + } + Serial.printf("[TcpIF] Failed to connect to %s:%d (attempt %d, next retry in %ds)\r\n", + _target_host, _target_port, _consecutive_failures, + _reconnect_interval / 1000); + } + _last_reconnect = millis(); + } + + // ─── Member variables ──────────────────────────────────────────────────── + TcpIfMode _mode; + uint16_t _port; + char _target_host[64]; + uint16_t _target_port; + WiFiServer* _server; + TcpClient _clients[TCP_IF_MAX_CLIENTS]; + int _num_clients; + uint32_t _last_reconnect; + uint32_t _last_keepalive; + uint32_t _reconnect_interval; + IPAddress _resolved_ip; + uint16_t _consecutive_failures; + bool _started; +}; + +#endif // BOUNDARY_MODE +#endif // HAS_RNS +#endif // TCP_INTERFACE_H diff --git a/Utilities.h b/Utilities.h new file mode 100755 index 0000000..944e0c9 --- /dev/null +++ b/Utilities.h @@ -0,0 +1,2069 @@ +// Copyright (C) 2024, Mark Qvist + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "Config.h" + +#if HAS_EEPROM + #include +#elif PLATFORM == PLATFORM_NRF52 + #include + #include + #include + using namespace Adafruit_LittleFS_Namespace; + #define EEPROM_FILE "eeprom" + bool file_exists = false; + int written_bytes = 4; + File file(InternalFS); +#endif +#include + +#if MODEM == SX1262 +#include "sx126x.h" +sx126x *LoRa = &sx126x_modem; +#elif MODEM == SX1276 || MODEM == SX1278 +#include "sx127x.h" +sx127x *LoRa = &sx127x_modem; +#elif MODEM == SX1280 +#include "sx128x.h" +sx128x *LoRa = &sx128x_modem; +#endif + +#include "ROM.h" +#include "Framing.h" +#include "MD5.h" + +#if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 +uint8_t eeprom_read(uint32_t mapped_addr); +#endif + +#if HAS_DISPLAY == true + #include "Display.h" +#else + void display_unblank() {} + bool display_blanked = false; +#endif + +#if HAS_BLUETOOTH == true || HAS_BLE == true + void kiss_indicate_btpin(); + #include "Bluetooth.h" +#else + // bt_devname used by Remote.h WiFi AP; provide fallback when BT disabled + char bt_devname[11] = "RNode"; +#endif + +#if HAS_WIFI == true + #include "Remote.h" +#endif + +#if HAS_PMU == true + #include "Power.h" +#endif + +#if HAS_INPUT == true + #include "Input.h" +#endif + +#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + #include "Device.h" +#endif +#if MCU_VARIANT == MCU_ESP32 + //https://github.com/espressif/esp-idf/issues/8855 + #if BOARD_MODEL == BOARD_HELTEC32_V3 + #include "hal/wdt_hal.h" + #elif BOARD_MODEL == BOARD_T3S3 + #include "hal/wdt_hal.h" + #else + #include "hal/wdt_hal.h" + #endif + #define ISR_VECT IRAM_ATTR +#else + #define ISR_VECT +#endif + +#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + #include + #include +#endif + +uint8_t boot_vector = 0x00; + +#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + uint8_t OPTIBOOT_MCUSR __attribute__ ((section(".noinit"))); + void resetFlagsInit(void) __attribute__ ((naked)) __attribute__ ((used)) __attribute__ ((section (".init0"))); + void resetFlagsInit(void) { + __asm__ __volatile__ ("sts %0, r2\n" : "=m" (OPTIBOOT_MCUSR) :); + } +#elif MCU_VARIANT == MCU_ESP32 + // TODO: Get ESP32 boot flags +#elif MCU_VARIANT == MCU_NRF52 + // TODO: Get NRF52 boot flags +#endif + +#ifdef HAS_RNS +#include +extern RNS::Reticulum reticulum; +#endif + +#if MCU_VARIANT == MCU_NRF52 + unsigned long get_rng_seed() { + nrf_rng_error_correction_enable(NRF_RNG); + nrf_rng_shorts_disable(NRF_RNG, NRF_RNG_SHORT_VALRDY_STOP_MASK); + nrf_rng_task_trigger(NRF_RNG, NRF_RNG_TASK_START); + while (!nrf_rng_event_check(NRF_RNG, NRF_RNG_EVENT_VALRDY)); + uint8_t rb_a = nrf_rng_random_value_get(NRF_RNG); + nrf_rng_event_clear(NRF_RNG, NRF_RNG_EVENT_VALRDY); + while (!nrf_rng_event_check(NRF_RNG, NRF_RNG_EVENT_VALRDY)); + uint8_t rb_b = nrf_rng_random_value_get(NRF_RNG); + nrf_rng_event_clear(NRF_RNG, NRF_RNG_EVENT_VALRDY); + while (!nrf_rng_event_check(NRF_RNG, NRF_RNG_EVENT_VALRDY)); + uint8_t rb_c = nrf_rng_random_value_get(NRF_RNG); + nrf_rng_event_clear(NRF_RNG, NRF_RNG_EVENT_VALRDY); + while (!nrf_rng_event_check(NRF_RNG, NRF_RNG_EVENT_VALRDY)); + uint8_t rb_d = nrf_rng_random_value_get(NRF_RNG); + nrf_rng_event_clear(NRF_RNG, NRF_RNG_EVENT_VALRDY); + nrf_rng_task_trigger(NRF_RNG, NRF_RNG_TASK_STOP); + return rb_a << 24 | rb_b << 16 | rb_c << 8 | rb_d; + } +#endif + +#if HAS_NP == true + #include + #define NUMPIXELS 1 + Adafruit_NeoPixel pixels(NUMPIXELS, pin_np, NEO_GRB + NEO_KHZ800); + + uint8_t npr = 0; + uint8_t npg = 0; + uint8_t npb = 0; + float npi = NP_M; + bool pixels_started = false; + + void led_set_intensity(uint8_t intensity) { + npi = (float)intensity/255.0; + } + + void led_init() { + #if BOARD_MODEL == BOARD_HELTEC_T114 + // Enable vext power supply to neopixel + pinMode(PIN_VEXT_EN, OUTPUT); + digitalWrite(PIN_VEXT_EN, HIGH); + #endif + + #if MCU_VARIANT == MCU_NRF52 + if (eeprom_read(eeprom_addr(ADDR_CONF_PSET)) == CONF_OK_BYTE) { + uint8_t int_val = eeprom_read(eeprom_addr(ADDR_CONF_PINT)); + led_set_intensity(int_val); + } + #else + if (EEPROM.read(eeprom_addr(ADDR_CONF_PSET)) == CONF_OK_BYTE) { + uint8_t int_val = EEPROM.read(eeprom_addr(ADDR_CONF_PINT)); + led_set_intensity(int_val); + } + #endif + } + + void npset(uint8_t r, uint8_t g, uint8_t b) { + if (pixels_started != true) { + pixels.begin(); + pixels_started = true; + } + + if (r != npr || g != npg || b != npb) { + npr = r; npg = g; npb = b; + pixels.setPixelColor(0, pixels.Color(npr*npi, npg*npi, npb*npi)); + pixels.show(); + } + } + + void boot_seq() { + uint8_t rs[] = { 0x00, 0x00, 0x00 }; + uint8_t gs[] = { 0x10, 0x08, 0x00 }; + uint8_t bs[] = { 0x00, 0x08, 0x10 }; + for (int i = 0; i < 1*sizeof(rs); i++) { + npset(rs[i%sizeof(rs)], gs[i%sizeof(gs)], bs[i%sizeof(bs)]); + delay(33); + npset(0x00, 0x00, 0x00); + delay(66); + } + } +#else + void boot_seq() { } +#endif + +#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } +#elif MCU_VARIANT == MCU_ESP32 + #if HAS_NP == true + void led_rx_on() { npset(0, 0, 0xFF); } + void led_rx_off() { npset(0, 0, 0); } + void led_tx_on() { npset(0xFF, 0x50, 0x00); } + void led_tx_off() { npset(0, 0, 0); } + void led_id_on() { npset(0x90, 0, 0x70); } + void led_id_off() { npset(0, 0, 0); } + #elif BOARD_MODEL == BOARD_RNODE_NG_20 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_RNODE_NG_21 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_T3S3 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_TBEAM + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, LOW); } + void led_tx_off() { digitalWrite(pin_led_tx, HIGH); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_TDECK + void led_rx_on() { } + void led_rx_off() { } + void led_tx_on() { } + void led_tx_off() { } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_TBEAM_S_V1 + void led_rx_on() { } + void led_rx_off() { } + void led_tx_on() { } + void led_tx_off() { } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_LORA32_V1_0 + #if defined(EXTERNAL_LEDS) + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #else + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #endif + #elif BOARD_MODEL == BOARD_LORA32_V2_0 + #if defined(EXTERNAL_LEDS) + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #else + void led_rx_on() { digitalWrite(pin_led_rx, LOW); } + void led_rx_off() { digitalWrite(pin_led_rx, HIGH); } + void led_tx_on() { digitalWrite(pin_led_tx, LOW); } + void led_tx_off() { digitalWrite(pin_led_tx, HIGH); } + void led_id_on() { } + void led_id_off() { } + #endif + #elif BOARD_MODEL == BOARD_HELTEC32_V2 + #if defined(EXTERNAL_LEDS) + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #else + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #endif + #elif BOARD_MODEL == BOARD_HELTEC32_V3 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_HELTEC32_V4 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_LORA32_V2_1 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_XIAO_S3 + void led_rx_on() { digitalWrite(pin_led_rx, LED_ON); } + void led_rx_off() { digitalWrite(pin_led_rx, LED_OFF); } + void led_tx_on() { digitalWrite(pin_led_tx, LED_ON); } + void led_tx_off() { digitalWrite(pin_led_tx, LED_OFF); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_HUZZAH32 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_GENERIC_ESP32 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #endif +#elif MCU_VARIANT == MCU_NRF52 + #if HAS_NP == true + void led_rx_on() { npset(0, 0, 0xFF); } + void led_rx_off() { npset(0, 0, 0); } + void led_tx_on() { npset(0xFF, 0x50, 0x00); } + void led_tx_off() { npset(0, 0, 0); } + void led_id_on() { npset(0x90, 0, 0x70); } + void led_id_off() { npset(0, 0, 0); } + #elif BOARD_MODEL == BOARD_RAK4631 + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_HELTEC_T114 + // Heltec T114 pulls pins LOW to turn on + void led_rx_on() { digitalWrite(pin_led_rx, LOW); } + void led_rx_off() { digitalWrite(pin_led_rx, HIGH); } + void led_tx_on() { digitalWrite(pin_led_tx, LOW); } + void led_tx_off() { digitalWrite(pin_led_tx, HIGH); } + void led_id_on() { } + void led_id_off() { } + #elif BOARD_MODEL == BOARD_TECHO + void led_rx_on() { digitalWrite(pin_led_rx, LED_ON); } + void led_rx_off() { digitalWrite(pin_led_rx, LED_OFF); } + void led_tx_on() { digitalWrite(pin_led_tx, LED_ON); } + void led_tx_off() { digitalWrite(pin_led_tx, LED_OFF); } + void led_id_on() { } + void led_id_off() { } + #endif +#endif + +void hard_reset(void) { + #if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + wdt_enable(WDTO_15MS); + while(true) { + led_tx_on(); led_rx_off(); + } + #elif MCU_VARIANT == MCU_ESP32 + ESP.restart(); + #elif MCU_VARIANT == MCU_NRF52 + NVIC_SystemReset(); + #endif +} + +// LED Indication: Error +void led_indicate_error(int cycles) { + #if HAS_NP == true + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + while(cycles > 0) { + npset(0xFF, 0x00, 0x00); + delay(100); + npset(0xFF, 0x50, 0x00); + delay(100); + if (!forever) cycles--; + } + npset(0,0,0); + #else + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + while(cycles > 0) { + digitalWrite(pin_led_rx, HIGH); + digitalWrite(pin_led_tx, LOW); + delay(100); + digitalWrite(pin_led_rx, LOW); + digitalWrite(pin_led_tx, HIGH); + delay(100); + if (!forever) cycles--; + } + led_rx_off(); + led_tx_off(); + #endif +} + +// LED Indication: Airtime Lock +void led_indicate_airtime_lock() { + #if HAS_NP == true + npset(32,0,2); + #endif +} + +// LED Indication: Boot Error +void led_indicate_boot_error() { + #if HAS_NP == true + while(true) { + npset(0xFF, 0xFF, 0xFF); + } + #else + while (true) { + led_tx_on(); + led_rx_off(); + delay(10); + led_rx_on(); + led_tx_off(); + delay(5); + } + #endif +} + +// LED Indication: Warning +void led_indicate_warning(int cycles) { + #if HAS_NP == true + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + while(cycles > 0) { + npset(0xFF, 0x50, 0x00); + delay(100); + npset(0x00, 0x00, 0x00); + delay(100); + if (!forever) cycles--; + } + npset(0,0,0); + #else + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + digitalWrite(pin_led_tx, HIGH); + while(cycles > 0) { + led_tx_off(); + delay(100); + led_tx_on(); + delay(100); + if (!forever) cycles--; + } + led_tx_off(); + #endif +} + +// LED Indication: Info +#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + void led_indicate_info(int cycles) { + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + while(cycles > 0) { + led_rx_off(); + delay(100); + led_rx_on(); + delay(100); + if (!forever) cycles--; + } + led_rx_off(); + } +#elif MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + #if HAS_NP == true + void led_indicate_info(int cycles) { + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + while(cycles > 0) { + npset(0x00, 0x00, 0xFF); + delay(100); + npset(0x00, 0x00, 0x00); + delay(100); + if (!forever) cycles--; + } + npset(0,0,0); + } + #elif BOARD_MODEL == BOARD_LORA32_V2_1 + void led_indicate_info(int cycles) { + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + while(cycles > 0) { + led_rx_off(); + delay(100); + led_rx_on(); + delay(100); + if (!forever) cycles--; + } + led_rx_off(); + } + #elif BOARD_MODEL == BOARD_LORA32_V2_0 + void led_indicate_info(int cycles) { + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + while(cycles > 0) { + led_rx_off(); + delay(100); + led_rx_on(); + delay(100); + if (!forever) cycles--; + } + led_rx_off(); + } + #elif BOARD_MODEL == BOARD_TECHO + void led_indicate_info(int cycles) { + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + while(cycles > 0) { + led_rx_off(); + delay(100); + led_rx_on(); + delay(100); + if (!forever) cycles--; + } + led_rx_off(); + } + #else + void led_indicate_info(int cycles) { + bool forever = (cycles == 0) ? true : false; + cycles = forever ? 1 : cycles; + while(cycles > 0) { + led_tx_off(); + delay(100); + led_tx_on(); + delay(100); + if (!forever) cycles--; + } + led_tx_off(); + } + #endif +#endif + + +unsigned long led_standby_ticks = 0; +#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + uint8_t led_standby_min = 1; + uint8_t led_standby_max = 40; + unsigned long led_standby_wait = 11000; + +#elif MCU_VARIANT == MCU_ESP32 + + #if HAS_NP == true + int led_standby_lng = 200; + int led_standby_cut = 100; + int led_standby_min = 0; + int led_standby_max = 375+led_standby_lng; + int led_notready_min = 0; + int led_notready_max = led_standby_max; + int led_notready_value = led_notready_min; + int8_t led_notready_direction = 0; + unsigned long led_notready_ticks = 0; + unsigned long led_standby_wait = 350; + unsigned long led_console_wait = 1; + unsigned long led_notready_wait = 200; + + #else + uint8_t led_standby_min = 200; + uint8_t led_standby_max = 255; + uint8_t led_notready_min = 0; + uint8_t led_notready_max = 255; + uint8_t led_notready_value = led_notready_min; + int8_t led_notready_direction = 0; + unsigned long led_notready_ticks = 0; + unsigned long led_standby_wait = 1768; + unsigned long led_notready_wait = 150; + #endif + +#elif MCU_VARIANT == MCU_NRF52 + int led_standby_lng = 200; + int led_standby_cut = 100; + uint8_t led_standby_min = 200; + uint8_t led_standby_max = 255; + uint8_t led_notready_min = 0; + uint8_t led_notready_max = 255; + uint8_t led_notready_value = led_notready_min; + int8_t led_notready_direction = 0; + unsigned long led_notready_ticks = 0; + unsigned long led_standby_wait = 1768; + unsigned long led_notready_wait = 150; +#endif + +unsigned long led_standby_value = led_standby_min; +int8_t led_standby_direction = 0; + +#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + void led_indicate_standby() { + led_standby_ticks++; + if (led_standby_ticks > led_standby_wait) { + led_standby_ticks = 0; + if (led_standby_value <= led_standby_min) { + led_standby_direction = 1; + } else if (led_standby_value >= led_standby_max) { + led_standby_direction = -1; + } + led_standby_value += led_standby_direction; + analogWrite(pin_led_rx, led_standby_value); + led_tx_off(); + } + } + +#elif MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + #if HAS_NP == true + void led_indicate_standby() { + led_standby_ticks++; + + if (led_standby_ticks > led_standby_wait) { + led_standby_ticks = 0; + + if (led_standby_value <= led_standby_min) { + led_standby_direction = 1; + } else if (led_standby_value >= led_standby_max) { + led_standby_direction = -1; + } + + uint8_t led_standby_intensity; + led_standby_value += led_standby_direction; + int led_standby_ti = led_standby_value - led_standby_lng; + + if (led_standby_ti < 0) { + led_standby_intensity = 0; + } else if (led_standby_ti > led_standby_cut) { + led_standby_intensity = led_standby_cut; + } else { + led_standby_intensity = led_standby_ti; + } + npset(led_standby_intensity/3, led_standby_intensity/3, led_standby_intensity/3); + } + } + + void led_indicate_console() { + npset(0x60, 0x00, 0x60); + // led_standby_ticks++; + + // if (led_standby_ticks > led_console_wait) { + // led_standby_ticks = 0; + + // if (led_standby_value <= led_standby_min) { + // led_standby_direction = 1; + // } else if (led_standby_value >= led_standby_max) { + // led_standby_direction = -1; + // } + + // uint8_t led_standby_intensity; + // led_standby_value += led_standby_direction; + // int led_standby_ti = led_standby_value - led_standby_lng; + + // if (led_standby_ti < 0) { + // led_standby_intensity = 0; + // } else if (led_standby_ti > led_standby_cut) { + // led_standby_intensity = led_standby_cut; + // } else { + // led_standby_intensity = led_standby_ti; + // } + // npset(led_standby_intensity, 0x00, led_standby_intensity); + // } + } + + #else + void led_indicate_standby() { + led_standby_ticks++; + if (led_standby_ticks > led_standby_wait) { + led_standby_ticks = 0; + if (led_standby_value <= led_standby_min) { + led_standby_direction = 1; + } else if (led_standby_value >= led_standby_max) { + led_standby_direction = -1; + } + led_standby_value += led_standby_direction; + if (led_standby_value > 253) { + #if BOARD_MODEL == BOARD_TECHO + led_rx_on(); + #else + led_tx_on(); + #endif + } else { + #if BOARD_MODEL == BOARD_TECHO + led_rx_off(); + #else + led_tx_off(); + #endif + } + #if BOARD_MODEL == BOARD_LORA32_V2_1 + #if defined(EXTERNAL_LEDS) + led_rx_off(); + #endif + #elif BOARD_MODEL == BOARD_LORA32_V2_0 + #if defined(EXTERNAL_LEDS) + led_rx_off(); + #endif + #else + led_rx_off(); + #endif + } + } + + void led_indicate_console() { + led_indicate_standby(); + } + #endif +#endif + +#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + void led_indicate_not_ready() { + led_standby_ticks++; + if (led_standby_ticks > led_standby_wait) { + led_standby_ticks = 0; + if (led_standby_value <= led_standby_min) { + led_standby_direction = 1; + } else if (led_standby_value >= led_standby_max) { + led_standby_direction = -1; + } + led_standby_value += led_standby_direction; + analogWrite(pin_led_tx, led_standby_value); + led_rx_off(); + } + } +#elif MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + #if HAS_NP == true + void led_indicate_not_ready() { + led_standby_ticks++; + + if (led_standby_ticks > led_notready_wait) { + led_standby_ticks = 0; + + if (led_standby_value <= led_standby_min) { + led_standby_direction = 1; + } else if (led_standby_value >= led_standby_max) { + led_standby_direction = -1; + } + + uint8_t led_standby_intensity; + led_standby_value += led_standby_direction; + int led_standby_ti = led_standby_value - led_standby_lng; + + if (led_standby_ti < 0) { + led_standby_intensity = 0; + } else if (led_standby_ti > led_standby_cut) { + led_standby_intensity = led_standby_cut; + } else { + led_standby_intensity = led_standby_ti; + } + + npset(led_standby_intensity, 0x00, 0x00); + } + } + #else + void led_indicate_not_ready() { + led_notready_ticks++; + if (led_notready_ticks > led_notready_wait) { + led_notready_ticks = 0; + if (led_notready_value <= led_notready_min) { + led_notready_direction = 1; + } else if (led_notready_value >= led_notready_max) { + led_notready_direction = -1; + } + led_notready_value += led_notready_direction; + if (led_notready_value > 128) { + led_tx_on(); + } else { + led_tx_off(); + } + #if BOARD_MODEL == BOARD_LORA32_V2_1 + #if defined(EXTERNAL_LEDS) + led_rx_off(); + #endif + #elif BOARD_MODEL == BOARD_LORA32_V2_0 + #if defined(EXTERNAL_LEDS) + led_rx_off(); + #endif + #else + led_rx_off(); + #endif + } + } + #endif +#endif + +void serial_write(uint8_t byte) { + #if HAS_BLUETOOTH || HAS_BLE == true + if (bt_state != BT_STATE_CONNECTED) { + #if HAS_WIFI + if (wifi_host_is_connected()) { wifi_remote_write(byte); } + else { Serial.write(byte); } + #else + Serial.write(byte); + #endif + } else { + SerialBT.write(byte); + #if MCU_VARIANT == MCU_NRF52 && HAS_BLE + // This ensures that the TX buffer is flushed after a frame is queued in serial. + // serial_in_frame is used to ensure that the flush only happens at the end of the frame + if (serial_in_frame && byte == FEND) { SerialBT.flushTXD(); serial_in_frame = false; } + else if (!serial_in_frame && byte == FEND) { serial_in_frame = true; } + #endif + } + #else + Serial.write(byte); + #endif +} + +void escaped_serial_write(uint8_t byte) { + if (byte == FEND) { serial_write(FESC); byte = TFEND; } + if (byte == FESC) { serial_write(FESC); byte = TFESC; } + serial_write(byte); +} + +void kiss_indicate_reset() { + serial_write(FEND); + serial_write(CMD_RESET); + serial_write(CMD_RESET_BYTE); + serial_write(FEND); +} + +void kiss_indicate_error(uint8_t error_code) { + serial_write(FEND); + serial_write(CMD_ERROR); + serial_write(error_code); + serial_write(FEND); +} + +void kiss_indicate_radiostate() { + serial_write(FEND); + serial_write(CMD_RADIO_STATE); + serial_write(radio_online); + serial_write(FEND); +} + +void kiss_indicate_stat_rx() { + serial_write(FEND); + serial_write(CMD_STAT_RX); + escaped_serial_write(stat_rx>>24); + escaped_serial_write(stat_rx>>16); + escaped_serial_write(stat_rx>>8); + escaped_serial_write(stat_rx); + serial_write(FEND); +} + +void kiss_indicate_stat_tx() { + serial_write(FEND); + serial_write(CMD_STAT_TX); + escaped_serial_write(stat_tx>>24); + escaped_serial_write(stat_tx>>16); + escaped_serial_write(stat_tx>>8); + escaped_serial_write(stat_tx); + serial_write(FEND); +} + +void kiss_indicate_stat_rssi() { + uint8_t packet_rssi_val = (uint8_t)(last_rssi+rssi_offset); + serial_write(FEND); + serial_write(CMD_STAT_RSSI); + escaped_serial_write(packet_rssi_val); + serial_write(FEND); +} + +void kiss_indicate_stat_snr() { + serial_write(FEND); + serial_write(CMD_STAT_SNR); + escaped_serial_write(last_snr_raw); + serial_write(FEND); +} + +void kiss_indicate_radio_lock() { + serial_write(FEND); + serial_write(CMD_RADIO_LOCK); + serial_write(radio_locked); + serial_write(FEND); +} + +void kiss_indicate_spreadingfactor() { + serial_write(FEND); + serial_write(CMD_SF); + serial_write((uint8_t)lora_sf); + serial_write(FEND); +} + +void kiss_indicate_codingrate() { + serial_write(FEND); + serial_write(CMD_CR); + serial_write((uint8_t)lora_cr); + serial_write(FEND); +} + +void kiss_indicate_implicit_length() { + serial_write(FEND); + serial_write(CMD_IMPLICIT); + serial_write(implicit_l); + serial_write(FEND); +} + +void kiss_indicate_txpower() { + serial_write(FEND); + serial_write(CMD_TXPOWER); + serial_write((uint8_t)lora_txp); + serial_write(FEND); +} + +void kiss_indicate_bandwidth() { + serial_write(FEND); + serial_write(CMD_BANDWIDTH); + escaped_serial_write(lora_bw>>24); + escaped_serial_write(lora_bw>>16); + escaped_serial_write(lora_bw>>8); + escaped_serial_write(lora_bw); + serial_write(FEND); +} + +void kiss_indicate_frequency() { + serial_write(FEND); + serial_write(CMD_FREQUENCY); + escaped_serial_write(lora_freq>>24); + escaped_serial_write(lora_freq>>16); + escaped_serial_write(lora_freq>>8); + escaped_serial_write(lora_freq); + serial_write(FEND); +} + +void kiss_indicate_st_alock() { + uint16_t at = (uint16_t)(st_airtime_limit*100*100); + serial_write(FEND); + serial_write(CMD_ST_ALOCK); + escaped_serial_write(at>>8); + escaped_serial_write(at); + serial_write(FEND); +} + +void kiss_indicate_lt_alock() { + uint16_t at = (uint16_t)(lt_airtime_limit*100*100); + serial_write(FEND); + serial_write(CMD_LT_ALOCK); + escaped_serial_write(at>>8); + escaped_serial_write(at); + serial_write(FEND); +} + +void kiss_indicate_channel_stats() { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + uint16_t ats = (uint16_t)(airtime*100*100); + uint16_t atl = (uint16_t)(longterm_airtime*100*100); + uint16_t cls = (uint16_t)(total_channel_util*100*100); + uint16_t cll = (uint16_t)(longterm_channel_util*100*100); + uint8_t crs = (uint8_t)(current_rssi+rssi_offset); + uint8_t nfl = (uint8_t)(noise_floor+rssi_offset); + uint8_t ntf = 0xFF; if (interference_detected) { ntf = (uint8_t)(current_rssi+rssi_offset); } + serial_write(FEND); + serial_write(CMD_STAT_CHTM); + escaped_serial_write(ats>>8); + escaped_serial_write(ats); + escaped_serial_write(atl>>8); + escaped_serial_write(atl); + escaped_serial_write(cls>>8); + escaped_serial_write(cls); + escaped_serial_write(cll>>8); + escaped_serial_write(cll); + escaped_serial_write(crs); + escaped_serial_write(nfl); + escaped_serial_write(ntf); + serial_write(FEND); + #endif +} + +void kiss_indicate_csma_stats() { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + serial_write(FEND); + serial_write(CMD_STAT_CSMA); + escaped_serial_write(cw_band); + escaped_serial_write(cw_min); + escaped_serial_write(cw_max); + serial_write(FEND); + #endif +} + +void kiss_indicate_phy_stats() { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + uint16_t lst = (uint16_t)(lora_symbol_time_ms*1000); + uint16_t lsr = (uint16_t)(lora_symbol_rate); + uint16_t prs = (uint16_t)(lora_preamble_symbols); + uint16_t prt = (uint16_t)(lora_preamble_time_ms); + uint16_t cst = (uint16_t)(csma_slot_ms); + uint16_t dft = (uint16_t)(difs_ms); + serial_write(FEND); + serial_write(CMD_STAT_PHYPRM); + escaped_serial_write(lst>>8); escaped_serial_write(lst); + escaped_serial_write(lsr>>8); escaped_serial_write(lsr); + escaped_serial_write(prs>>8); escaped_serial_write(prs); + escaped_serial_write(prt>>8); escaped_serial_write(prt); + escaped_serial_write(cst>>8); escaped_serial_write(cst); + escaped_serial_write(dft>>8); escaped_serial_write(dft); + serial_write(FEND); + #endif +} + +void kiss_indicate_battery() { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + serial_write(FEND); + serial_write(CMD_STAT_BAT); + escaped_serial_write(battery_state); + escaped_serial_write((uint8_t)int(battery_percent)); + serial_write(FEND); + #endif +} + +void kiss_indicate_temperature() { + #if HAS_PMU + #if MCU_VARIANT == MCU_ESP32 + float pmu_temp = pmu_temperature+PMU_TEMP_OFFSET; + uint8_t temp = (uint8_t)pmu_temp; + serial_write(FEND); + serial_write(CMD_STAT_TEMP); + escaped_serial_write(pmu_temp); + serial_write(FEND); + #endif + #endif +} + +void kiss_indicate_btpin() { + #if HAS_BLUETOOTH || HAS_BLE == true + serial_write(FEND); + serial_write(CMD_BT_PIN); + escaped_serial_write(bt_ssp_pin>>24); + escaped_serial_write(bt_ssp_pin>>16); + escaped_serial_write(bt_ssp_pin>>8); + escaped_serial_write(bt_ssp_pin); + serial_write(FEND); + #endif +} + +void kiss_indicate_random(uint8_t byte) { + serial_write(FEND); + serial_write(CMD_RANDOM); + serial_write(byte); + serial_write(FEND); +} + +void kiss_indicate_fbstate() { + serial_write(FEND); + serial_write(CMD_FB_EXT); + #if HAS_DISPLAY + if (disp_ext_fb) { + serial_write(0x01); + } else { + serial_write(0x00); + } + #else + serial_write(0xFF); + #endif + serial_write(FEND); +} + +#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + void kiss_indicate_device_hash() { + serial_write(FEND); + serial_write(CMD_DEV_HASH); + for (int i = 0; i < DEV_HASH_LEN; i++) { + uint8_t byte = dev_hash[i]; + escaped_serial_write(byte); + } + serial_write(FEND); + } + + void kiss_indicate_target_fw_hash() { + serial_write(FEND); + serial_write(CMD_HASHES); + serial_write(0x01); + for (int i = 0; i < DEV_HASH_LEN; i++) { + uint8_t byte = dev_firmware_hash_target[i]; + escaped_serial_write(byte); + } + serial_write(FEND); + } + + void kiss_indicate_fw_hash() { + serial_write(FEND); + serial_write(CMD_HASHES); + serial_write(0x02); + for (int i = 0; i < DEV_HASH_LEN; i++) { + uint8_t byte = dev_firmware_hash[i]; + escaped_serial_write(byte); + } + serial_write(FEND); + } + + void kiss_indicate_bootloader_hash() { + serial_write(FEND); + serial_write(CMD_HASHES); + serial_write(0x03); + for (int i = 0; i < DEV_HASH_LEN; i++) { + uint8_t byte = dev_bootloader_hash[i]; + escaped_serial_write(byte); + } + serial_write(FEND); + } + + void kiss_indicate_partition_table_hash() { + serial_write(FEND); + serial_write(CMD_HASHES); + serial_write(0x04); + for (int i = 0; i < DEV_HASH_LEN; i++) { + uint8_t byte = dev_partition_table_hash[i]; + escaped_serial_write(byte); + } + serial_write(FEND); + } +#endif + +void kiss_indicate_fb() { + serial_write(FEND); + serial_write(CMD_FB_READ); + #if HAS_DISPLAY + for (int i = 0; i < 512; i++) { + uint8_t byte = fb[i]; + escaped_serial_write(byte); + } + #else + serial_write(0xFF); + #endif + serial_write(FEND); +} + +void kiss_indicate_disp() { + serial_write(FEND); + serial_write(CMD_DISP_READ); + #if HAS_DISPLAY + uint8_t *da = disp_area.getBuffer(); + uint8_t *sa = stat_area.getBuffer(); + for (int i = 0; i < 512; i++) { escaped_serial_write(da[i]); } + for (int i = 0; i < 512; i++) { escaped_serial_write(sa[i]); } + #else + serial_write(0xFF); + #endif + serial_write(FEND); +} + +void kiss_indicate_ready() { + serial_write(FEND); + serial_write(CMD_READY); + serial_write(0x01); + serial_write(FEND); +} + +void kiss_indicate_not_ready() { + serial_write(FEND); + serial_write(CMD_READY); + serial_write(0x00); + serial_write(FEND); +} + +void kiss_indicate_promisc() { + serial_write(FEND); + serial_write(CMD_PROMISC); + if (promisc) { + serial_write(0x01); + } else { + serial_write(0x00); + } + serial_write(FEND); +} + +void kiss_indicate_detect() { + serial_write(FEND); + serial_write(CMD_DETECT); + serial_write(DETECT_RESP); + serial_write(FEND); +} + +void kiss_indicate_version() { + serial_write(FEND); + serial_write(CMD_FW_VERSION); + serial_write(MAJ_VERS); + serial_write(MIN_VERS); + serial_write(FEND); +} + +void kiss_indicate_platform() { + serial_write(FEND); + serial_write(CMD_PLATFORM); + serial_write(PLATFORM); + serial_write(FEND); +} + +void kiss_indicate_board() { + serial_write(FEND); + serial_write(CMD_BOARD); + serial_write(BOARD_MODEL); + serial_write(FEND); +} + +void kiss_indicate_mcu() { + serial_write(FEND); + serial_write(CMD_MCU); + serial_write(MCU_VARIANT); + serial_write(FEND); +} + +inline bool isSplitPacket(uint8_t header) { + return (header & FLAG_SPLIT); +} + +inline uint8_t packetSequence(uint8_t header) { + return header >> 4; +} + +void setPreamble() { + if (radio_online) LoRa->setPreambleLength(lora_preamble_symbols); + kiss_indicate_phy_stats(); +} + +void updateBitrate() { + #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52 + if (!radio_online) { lora_bitrate = 0; } + else { + lora_symbol_rate = (float)lora_bw/(float)(pow(2, lora_sf)); + lora_symbol_time_ms = (1.0/lora_symbol_rate)*1000.0; + lora_bitrate = (uint32_t)(lora_sf * ( (4.0/(float)lora_cr) / ((float)(pow(2, lora_sf))/((float)lora_bw/1000.0)) ) * 1000.0); + lora_us_per_byte = 1000000.0/((float)lora_bitrate/8.0); + + bool fast_rate = lora_bitrate > LORA_FAST_THRESHOLD_BPS; + lora_limit_rate = lora_bitrate > LORA_LIMIT_THRESHOLD_BPS; + lora_guard_rate = (!lora_limit_rate && lora_bitrate > LORA_GUARD_THRESHOLD_BPS); + + int csma_slot_min_ms = CSMA_SLOT_MIN_MS; + float lora_preamble_target_ms = LORA_PREAMBLE_TARGET_MS; + if (fast_rate) { csma_slot_min_ms -= CSMA_SLOT_MIN_FAST_DELTA; + lora_preamble_target_ms -= LORA_PREAMBLE_FAST_DELTA; } + + csma_slot_ms = lora_symbol_time_ms*CSMA_SLOT_SYMBOLS; + if (csma_slot_ms > CSMA_SLOT_MAX_MS) { csma_slot_ms = CSMA_SLOT_MAX_MS; } + if (csma_slot_ms < CSMA_SLOT_MIN_MS) { csma_slot_ms = csma_slot_min_ms; } + difs_ms = CSMA_SIFS_MS + 2*csma_slot_ms; + + float target_preamble_symbols = lora_preamble_target_ms/lora_symbol_time_ms; + if (target_preamble_symbols < LORA_PREAMBLE_SYMBOLS_MIN) { target_preamble_symbols = LORA_PREAMBLE_SYMBOLS_MIN; } + else { target_preamble_symbols = (ceil)(target_preamble_symbols); } + + lora_preamble_symbols = (long)target_preamble_symbols; setPreamble(); + lora_preamble_time_ms = (ceil)(lora_preamble_symbols * lora_symbol_time_ms); + lora_header_time_ms = (ceil)(PHY_HEADER_LORA_SYMBOLS * lora_symbol_time_ms); + } + #endif +} + +void setSpreadingFactor() { + if (radio_online) LoRa->setSpreadingFactor(lora_sf); + updateBitrate(); +} + +void setCodingRate() { + if (radio_online) LoRa->setCodingRate4(lora_cr); + updateBitrate(); +} + +void set_implicit_length(uint8_t len) { + implicit_l = len; + if (implicit_l != 0) { + implicit = true; + } else { + implicit = false; + } +} + +int getTxPower() { + uint8_t txp = LoRa->getTxPower(); + return (int)txp; +} + +#if HAS_LORA_PA + const int tx_gain[PA_GAIN_POINTS] = {PA_GAIN_VALUES}; +#endif + +int map_target_power_to_modem_output(int target_tx_power) { + #if HAS_LORA_PA + int modem_output_dbm = -9; + for (int i = 0; i < PA_GAIN_POINTS; i++) { + int gain = tx_gain[i]; + int effective_output_dbm = i + gain; + if (effective_output_dbm > target_tx_power) { + int diff = effective_output_dbm - target_tx_power; + modem_output_dbm = -1*diff; + break; + } else if (effective_output_dbm == target_tx_power) { + modem_output_dbm = i; break; + } else if (i == PA_GAIN_POINTS-1) { + int diff = target_tx_power - effective_output_dbm; + modem_output_dbm = i+diff; break; + } + } + #else + int modem_output_dbm = target_tx_power; + #endif + + return modem_output_dbm; +} + +int map_modem_output_to_target_power(int modem_output_dbm) { + #if HAS_LORA_PA + if (modem_output_dbm < 0) { modem_output_dbm = 0; } + if (modem_output_dbm >= PA_GAIN_POINTS) { modem_output_dbm = PA_GAIN_POINTS-1; } + int gain = tx_gain[modem_output_dbm]; + int target_tx_power = modem_output_dbm+gain; + #else + int target_tx_power = modem_output_dbm; + #endif + + return target_tx_power; +} + +void setTXPower() { + if (radio_online) { + int mapped_lora_txp = map_target_power_to_modem_output(lora_txp); + + #if HAS_LORA_PA + int real_lora_txp = map_modem_output_to_target_power(mapped_lora_txp); + lora_txp = real_lora_txp; + #endif + + if (model == MODEL_11) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN); + if (model == MODEL_12) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN); + + if (model == MODEL_C6) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN); + if (model == MODEL_C7) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN); + + if (model == MODEL_A1) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_A2) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_A3) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN); + if (model == MODEL_A4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN); + if (model == MODEL_A5) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_A6) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_A7) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_A8) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_A9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_AA) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_AC) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + + if (model == MODEL_BA) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_BB) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_B3) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_B4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_B8) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_B9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + + if (model == MODEL_C4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_C9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_C5) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_CA) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_C8) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + + if (model == MODEL_D4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_D9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + + if (model == MODEL_DB) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_DC) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + + if (model == MODEL_DD) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_DE) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + + if (model == MODEL_E4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_E9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_E3) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_E8) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + + if (model == MODEL_FE) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN); + if (model == MODEL_FF) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN); + } +} + + +void getBandwidth() { + if (radio_online) { + lora_bw = LoRa->getSignalBandwidth(); + } + updateBitrate(); +} + +void setBandwidth() { + if (radio_online) { + LoRa->setSignalBandwidth(lora_bw); + getBandwidth(); + } +} + +void getFrequency() { + if (radio_online) { + lora_freq = LoRa->getFrequency(); + } +} + +void setFrequency() { + if (radio_online) { + LoRa->setFrequency(lora_freq); + getFrequency(); + } +} + +uint8_t getRandom() { return random(0xFF); } + +void promisc_enable() { + promisc = true; +} + +void promisc_disable() { + promisc = false; +} + +#if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 + bool eeprom_begin() { + if (!InternalFS.begin()) { + // FileSystem couldn't be initialized so fail + return false; + } + + if (InternalFS.exists(EEPROM_FILE)) { + if (file.open(EEPROM_FILE, FILE_O_WRITE)) { + // File was successfully opened for writing + return true; + } + // File exists but couldn't be opeend for writing so reformat filesystem + if (!InternalFS.format()) { + // FileSystem format failed so fail + return false; + } + } + + // File doesn't exist at this point + if (!file.open(EEPROM_FILE, FILE_O_WRITE)) { + // New file couldn't be opeend for writing so reformat filesystem in case it wasn't done previously + if (!InternalFS.format()) { + // FileSystem format failed so fail + return false; + } + if (!file.open(EEPROM_FILE, FILE_O_WRITE)) { + // New file still couldn't be opeend for writing so fail + return false; + } + } + // New file was successfully opeend for writing so initialise with empty content + uint8_t empty_content[EEPROM_SIZE] = {0}; + if (file.write(empty_content, EEPROM_SIZE) < EEPROM_SIZE) { + // Write of content failed so fail + return false; + } + file.flush(); + // File is opened for writing and all is well + return true; + } + + uint8_t eeprom_read(uint32_t mapped_addr) { + uint8_t byte; + void* byte_ptr = &byte; + file.seek(mapped_addr); + file.read(byte_ptr, 1); + return byte; + } +#endif + +bool eeprom_info_locked() { + #if HAS_EEPROM + uint8_t lock_byte = EEPROM.read(eeprom_addr(ADDR_INFO_LOCK)); + #elif MCU_VARIANT == MCU_NRF52 + uint8_t lock_byte = eeprom_read(eeprom_addr(ADDR_INFO_LOCK)); + #endif + if (lock_byte == INFO_LOCK_BYTE) { + return true; + } else { + return false; + } +} + +void eeprom_dump_info() { + for (int addr = ADDR_PRODUCT; addr <= ADDR_INFO_LOCK; addr++) { + #if HAS_EEPROM + uint8_t byte = EEPROM.read(eeprom_addr(addr)); + #elif MCU_VARIANT == MCU_NRF52 + uint8_t byte = eeprom_read(eeprom_addr(addr)); + #endif + escaped_serial_write(byte); + } +} + +void eeprom_dump_config() { + for (int addr = ADDR_CONF_SF; addr <= ADDR_CONF_OK; addr++) { + #if HAS_EEPROM + uint8_t byte = EEPROM.read(eeprom_addr(addr)); + #elif MCU_VARIANT == MCU_NRF52 + uint8_t byte = eeprom_read(eeprom_addr(addr)); + #endif + escaped_serial_write(byte); + } +} + +void eeprom_dump_all() { + for (int addr = 0; addr < EEPROM_RESERVED; addr++) { + #if HAS_EEPROM + uint8_t byte = EEPROM.read(eeprom_addr(addr)); + #elif MCU_VARIANT == MCU_NRF52 + uint8_t byte = eeprom_read(eeprom_addr(addr)); + #endif + escaped_serial_write(byte); + } +} + +void eeprom_config_dump_all() { + #if MCU_VARIANT == MCU_ESP32 + for (int addr = 0; addr < CONFIG_SIZE; addr++) { + uint8_t byte = EEPROM.read(config_addr(addr)); + escaped_serial_write(byte); + } + #endif +} + +void kiss_dump_eeprom() { + serial_write(FEND); + serial_write(CMD_ROM_READ); + eeprom_dump_all(); + serial_write(FEND); +} + +void kiss_dump_config() { + serial_write(FEND); + serial_write(CMD_CFG_READ); + eeprom_config_dump_all(); + serial_write(FEND); +} + +#if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 +void eeprom_flush() { + file.close(); + file.open(EEPROM_FILE, FILE_O_WRITE); + written_bytes = 0; +} +#endif + +void eeprom_update(int mapped_addr, uint8_t byte) { + #if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560 + EEPROM.update(mapped_addr, byte); + #elif MCU_VARIANT == MCU_ESP32 + if (EEPROM.read(mapped_addr) != byte) { + EEPROM.write(mapped_addr, byte); + EEPROM.commit(); + } + #elif !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 + // todo: clean up this implementation, writing one byte and syncing + // each time is really slow, but this is also suboptimal + uint8_t read_byte; + void* read_byte_ptr = &read_byte; + file.seek(mapped_addr); + file.read(read_byte_ptr, 1); + file.seek(mapped_addr); + if (read_byte != byte) { + file.write(byte); + } + written_bytes++; + + if ((mapped_addr - eeprom_addr(0)) == ADDR_INFO_LOCK) { + // have to do a flush because we're only writing 1 byte and it syncs after 4 + eeprom_flush(); + } + else if ((mapped_addr - eeprom_addr(0)) == ADDR_CONF_OK) { + // have to do a flush because we're only writing 1 byte and it syncs after 4 + eeprom_flush(); + } + + if (written_bytes >= 4) { + file.close(); + file.open(EEPROM_FILE, FILE_O_WRITE); + written_bytes = 0; + } + #endif +} + +void eeprom_write(uint8_t addr, uint8_t byte) { + if (!eeprom_info_locked() && addr >= 0 && addr < EEPROM_RESERVED) { + eeprom_update(eeprom_addr(addr), byte); + } else { + kiss_indicate_error(ERROR_EEPROM_LOCKED); + } +} + +void eeprom_erase() { + #if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 + InternalFS.format(); + #else + for (int addr = 0; addr < EEPROM_RESERVED; addr++) { + eeprom_update(eeprom_addr(addr), 0xFF); + } + #endif + #ifdef HAS_RNS + reticulum.clear_caches(); + #endif + hard_reset(); +} + +bool eeprom_lock_set() { + #if HAS_EEPROM + if (EEPROM.read(eeprom_addr(ADDR_INFO_LOCK)) == INFO_LOCK_BYTE) { + #elif MCU_VARIANT == MCU_NRF52 + if (eeprom_read(eeprom_addr(ADDR_INFO_LOCK)) == INFO_LOCK_BYTE) { + #endif + return true; + } else { + return false; + } +} + +bool eeprom_product_valid() { + #if HAS_EEPROM + uint8_t rval = EEPROM.read(eeprom_addr(ADDR_PRODUCT)); + #elif MCU_VARIANT == MCU_NRF52 + uint8_t rval = eeprom_read(eeprom_addr(ADDR_PRODUCT)); + #endif + + #if PLATFORM == PLATFORM_AVR + if (rval == PRODUCT_RNODE || rval == PRODUCT_HMBRW) { + #elif PLATFORM == PLATFORM_ESP32 + if (rval == PRODUCT_RNODE || rval == BOARD_RNODE_NG_20 || rval == BOARD_RNODE_NG_21 || rval == PRODUCT_HMBRW || rval == PRODUCT_TBEAM || rval == PRODUCT_T32_10 || rval == PRODUCT_T32_20 || rval == PRODUCT_T32_21 || rval == PRODUCT_H32_V2 || rval == PRODUCT_H32_V3 || rval == PRODUCT_H32_V4 || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1 || rval == PRODUCT_XIAO_S3) { + #elif PLATFORM == PLATFORM_NRF52 + if (rval == PRODUCT_RAK4631 || rval == PRODUCT_HELTEC_T114 || rval == PRODUCT_TECHO || rval == PRODUCT_HMBRW) { + #else + if (false) { + #endif + return true; + } else { + return false; + } +} + +bool eeprom_model_valid() { + #if HAS_EEPROM + model = EEPROM.read(eeprom_addr(ADDR_MODEL)); + #elif MCU_VARIANT == MCU_NRF52 + model = eeprom_read(eeprom_addr(ADDR_MODEL)); + #endif + #if BOARD_MODEL == BOARD_RNODE + if (model == MODEL_A4 || model == MODEL_A9 || model == MODEL_FF || model == MODEL_FE) { + #elif BOARD_MODEL == BOARD_RNODE_NG_20 + if (model == MODEL_A3 || model == MODEL_A8) { + #elif BOARD_MODEL == BOARD_RNODE_NG_21 + if (model == MODEL_A2 || model == MODEL_A7) { + #elif BOARD_MODEL == BOARD_T3S3 + if (model == MODEL_A1 || model == MODEL_A6 || model == MODEL_A5 || model == MODEL_AA || model == MODEL_AC) { + #elif BOARD_MODEL == BOARD_HMBRW + if (model == MODEL_FF || model == MODEL_FE) { + #elif BOARD_MODEL == BOARD_TBEAM + if (model == MODEL_E4 || model == MODEL_E9 || model == MODEL_E3 || model == MODEL_E8) { + #elif BOARD_MODEL == BOARD_TDECK + if (model == MODEL_D4 || model == MODEL_D9) { + #elif BOARD_MODEL == BOARD_TECHO + if (model == MODEL_16 || model == MODEL_17) { + #elif BOARD_MODEL == BOARD_TBEAM_S_V1 + if (model == MODEL_DB || model == MODEL_DC) { + #elif BOARD_MODEL == BOARD_XIAO_S3 + if (model == MODEL_DD || model == MODEL_DE) { + #elif BOARD_MODEL == BOARD_LORA32_V1_0 + if (model == MODEL_BA || model == MODEL_BB) { + #elif BOARD_MODEL == BOARD_LORA32_V2_0 + if (model == MODEL_B3 || model == MODEL_B8) { + #elif BOARD_MODEL == BOARD_LORA32_V2_1 + if (model == MODEL_B4 || model == MODEL_B9) { + #elif BOARD_MODEL == BOARD_HELTEC32_V2 + if (model == MODEL_C4 || model == MODEL_C9) { + #elif BOARD_MODEL == BOARD_HELTEC32_V3 + if (model == MODEL_C5 || model == MODEL_CA) { + #elif BOARD_MODEL == BOARD_HELTEC32_V4 + if (model == MODEL_C8) { + #elif BOARD_MODEL == BOARD_HELTEC_T114 + if (model == MODEL_C6 || model == MODEL_C7) { + #elif BOARD_MODEL == BOARD_RAK4631 + if (model == MODEL_11 || model == MODEL_12) { + #elif BOARD_MODEL == BOARD_HUZZAH32 + if (model == MODEL_FF) { + #elif BOARD_MODEL == BOARD_GENERIC_ESP32 + if (model == MODEL_FF || model == MODEL_FE) { + #else + if (false) { + #endif + return true; + } else { + return false; + } +} + +bool eeprom_hwrev_valid() { + #if HAS_EEPROM + hwrev = EEPROM.read(eeprom_addr(ADDR_HW_REV)); + #elif MCU_VARIANT == MCU_NRF52 + hwrev = eeprom_read(eeprom_addr(ADDR_HW_REV)); + #endif + if (hwrev != 0x00 && hwrev != 0xFF) { + return true; + } else { + return false; + } +} + +bool eeprom_checksum_valid() { + char *data = (char*)malloc(CHECKSUMMED_SIZE); + for (uint8_t i = 0; i < CHECKSUMMED_SIZE; i++) { + #if HAS_EEPROM + char byte = EEPROM.read(eeprom_addr(i)); + #elif MCU_VARIANT == MCU_NRF52 + char byte = eeprom_read(eeprom_addr(i)); + #endif + data[i] = byte; + } + + unsigned char *hash = MD5::make_hash(data, CHECKSUMMED_SIZE); + bool checksum_valid = true; + for (uint8_t i = 0; i < 16; i++) { + #if HAS_EEPROM + uint8_t stored_chk_byte = EEPROM.read(eeprom_addr(ADDR_CHKSUM+i)); + #elif MCU_VARIANT == MCU_NRF52 + uint8_t stored_chk_byte = eeprom_read(eeprom_addr(ADDR_CHKSUM+i)); + #endif + uint8_t calced_chk_byte = (uint8_t)hash[i]; + if (stored_chk_byte != calced_chk_byte) { + checksum_valid = false; + } + } + + free(hash); + free(data); + return checksum_valid; +} + +void wr_conf_save(uint8_t mode) { + eeprom_update(eeprom_addr(ADDR_CONF_WIFI), mode); + #if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 + // have to do a flush because we're only writing 1 byte and it syncs after 8 + eeprom_flush(); + #endif +} + +void bt_conf_save(bool is_enabled) { + if (is_enabled) { + eeprom_update(eeprom_addr(ADDR_CONF_BT), BT_ENABLE_BYTE); + #if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 + // have to do a flush because we're only writing 1 byte and it syncs after 8 + eeprom_flush(); + #endif + } else { + eeprom_update(eeprom_addr(ADDR_CONF_BT), 0x00); + #if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52 + // have to do a flush because we're only writing 1 byte and it syncs after 8 + eeprom_flush(); + #endif + } +} + +void di_conf_save(uint8_t dint) { + eeprom_update(eeprom_addr(ADDR_CONF_DINT), dint); +} + +void da_conf_save(uint8_t dadr) { + eeprom_update(eeprom_addr(ADDR_CONF_DADR), dadr); +} + +void db_conf_save(uint8_t val) { + #if HAS_DISPLAY + if (val == 0x00) { + display_blanking_enabled = false; + } else { + display_blanking_enabled = true; + display_blanking_timeout = val*1000; + } + eeprom_update(eeprom_addr(ADDR_CONF_BSET), CONF_OK_BYTE); + eeprom_update(eeprom_addr(ADDR_CONF_DBLK), val); + #endif +} + +void drot_conf_save(uint8_t val) { + #if HAS_DISPLAY + if (val >= 0x00 and val <= 0x03) { + eeprom_update(eeprom_addr(ADDR_CONF_DROT), val); + hard_reset(); + } + #endif +} + +void dia_conf_save(uint8_t val) { + if (val > 0x00) { eeprom_update(eeprom_addr(ADDR_CONF_DIA), 0x01); } + else { eeprom_update(eeprom_addr(ADDR_CONF_DIA), 0x00); } + hard_reset(); +} + +void np_int_conf_save(uint8_t p_int) { + eeprom_update(eeprom_addr(ADDR_CONF_PSET), CONF_OK_BYTE); + eeprom_update(eeprom_addr(ADDR_CONF_PINT), p_int); +} + + +bool eeprom_have_conf() { + #if HAS_EEPROM + if (EEPROM.read(eeprom_addr(ADDR_CONF_OK)) == CONF_OK_BYTE) { + #elif MCU_VARIANT == MCU_NRF52 + if (eeprom_read(eeprom_addr(ADDR_CONF_OK)) == CONF_OK_BYTE) { + #endif + return true; + } else { + return false; + } +} + +void eeprom_conf_load() { + if (eeprom_have_conf()) { + #if HAS_EEPROM + lora_sf = EEPROM.read(eeprom_addr(ADDR_CONF_SF)); + lora_cr = EEPROM.read(eeprom_addr(ADDR_CONF_CR)); + lora_txp = EEPROM.read(eeprom_addr(ADDR_CONF_TXP)); + lora_freq = (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_FREQ)+0x00) << 24 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_FREQ)+0x01) << 16 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_FREQ)+0x02) << 8 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_FREQ)+0x03); + lora_bw = (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_BW)+0x00) << 24 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_BW)+0x01) << 16 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_BW)+0x02) << 8 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_BW)+0x03); + #elif MCU_VARIANT == MCU_NRF52 + lora_sf = eeprom_read(eeprom_addr(ADDR_CONF_SF)); + lora_cr = eeprom_read(eeprom_addr(ADDR_CONF_CR)); + lora_txp = eeprom_read(eeprom_addr(ADDR_CONF_TXP)); + lora_freq = (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_FREQ)+0x00) << 24 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_FREQ)+0x01) << 16 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_FREQ)+0x02) << 8 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_FREQ)+0x03); + lora_bw = (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_BW)+0x00) << 24 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_BW)+0x01) << 16 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_BW)+0x02) << 8 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_BW)+0x03); + #endif + } +} + +void eeprom_conf_save() { + if (hw_ready && radio_online) { + eeprom_update(eeprom_addr(ADDR_CONF_SF), lora_sf); + eeprom_update(eeprom_addr(ADDR_CONF_CR), lora_cr); + eeprom_update(eeprom_addr(ADDR_CONF_TXP), lora_txp); + + eeprom_update(eeprom_addr(ADDR_CONF_BW)+0x00, lora_bw>>24); + eeprom_update(eeprom_addr(ADDR_CONF_BW)+0x01, lora_bw>>16); + eeprom_update(eeprom_addr(ADDR_CONF_BW)+0x02, lora_bw>>8); + eeprom_update(eeprom_addr(ADDR_CONF_BW)+0x03, lora_bw); + + eeprom_update(eeprom_addr(ADDR_CONF_FREQ)+0x00, lora_freq>>24); + eeprom_update(eeprom_addr(ADDR_CONF_FREQ)+0x01, lora_freq>>16); + eeprom_update(eeprom_addr(ADDR_CONF_FREQ)+0x02, lora_freq>>8); + eeprom_update(eeprom_addr(ADDR_CONF_FREQ)+0x03, lora_freq); + + eeprom_update(eeprom_addr(ADDR_CONF_OK), CONF_OK_BYTE); + led_indicate_info(10); + } else { + led_indicate_warning(10); + } +} + +void eeprom_conf_delete() { + eeprom_update(eeprom_addr(ADDR_CONF_OK), 0x00); +} + +void unlock_rom() { + led_indicate_error(50); + eeprom_erase(); +} + +void init_channel_stats() { + #if MCU_VARIANT == MCU_ESP32 + for (uint16_t ai = 0; ai < DCD_SAMPLES; ai++) { util_samples[ai] = false; } + for (uint16_t ai = 0; ai < AIRTIME_BINS; ai++) { airtime_bins[ai] = 0; } + for (uint16_t ai = 0; ai < AIRTIME_BINS; ai++) { longterm_bins[ai] = 0.0; } + local_channel_util = 0.0; + total_channel_util = 0.0; + airtime = 0.0; + longterm_airtime = 0.0; + #endif +} + +typedef struct FIFOBuffer +{ + unsigned char *begin; + unsigned char *end; + unsigned char * volatile head; + unsigned char * volatile tail; +} FIFOBuffer; + +inline bool fifo_isempty(const FIFOBuffer *f) { + return f->head == f->tail; +} + +inline bool fifo_isfull(const FIFOBuffer *f) { + return ((f->head == f->begin) && (f->tail == f->end)) || (f->tail == f->head - 1); +} + +inline void fifo_push(FIFOBuffer *f, unsigned char c) { + *(f->tail) = c; + + if (f->tail == f->end) { + f->tail = f->begin; + } else { + f->tail++; + } +} + +inline unsigned char fifo_pop(FIFOBuffer *f) { + if(f->head == f->end) { + f->head = f->begin; + return *(f->end); + } else { + return *(f->head++); + } +} + +inline void fifo_flush(FIFOBuffer *f) { + f->head = f->tail; +} + +#if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + static inline bool fifo_isempty_locked(const FIFOBuffer *f) { + bool result; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + result = fifo_isempty(f); + } + return result; + } + + static inline bool fifo_isfull_locked(const FIFOBuffer *f) { + bool result; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + result = fifo_isfull(f); + } + return result; + } + + static inline void fifo_push_locked(FIFOBuffer *f, unsigned char c) { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + fifo_push(f, c); + } + } +#endif + +/* +static inline unsigned char fifo_pop_locked(FIFOBuffer *f) { + unsigned char c; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + c = fifo_pop(f); + } + return c; +} +*/ + +inline void fifo_init(FIFOBuffer *f, unsigned char *buffer, size_t size) { + f->head = f->tail = f->begin = buffer; + f->end = buffer + size; +} + +inline size_t fifo_len(FIFOBuffer *f) { + return f->end - f->begin; +} + +typedef struct FIFOBuffer16 +{ + uint16_t *begin; + uint16_t *end; + uint16_t * volatile head; + uint16_t * volatile tail; +} FIFOBuffer16; + +inline bool fifo16_isempty(const FIFOBuffer16 *f) { + return f->head == f->tail; +} + +inline bool fifo16_isfull(const FIFOBuffer16 *f) { + return ((f->head == f->begin) && (f->tail == f->end)) || (f->tail == f->head - 1); +} + +inline void fifo16_push(FIFOBuffer16 *f, uint16_t c) { + *(f->tail) = c; + + if (f->tail == f->end) { + f->tail = f->begin; + } else { + f->tail++; + } +} + +inline uint16_t fifo16_pop(FIFOBuffer16 *f) { + if(f->head == f->end) { + f->head = f->begin; + return *(f->end); + } else { + return *(f->head++); + } +} + +inline void fifo16_flush(FIFOBuffer16 *f) { + f->head = f->tail; +} + +#if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52 + static inline bool fifo16_isempty_locked(const FIFOBuffer16 *f) { + bool result; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + result = fifo16_isempty(f); + } + + return result; + } +#endif + +/* +static inline bool fifo16_isfull_locked(const FIFOBuffer16 *f) { + bool result; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + result = fifo16_isfull(f); + } + return result; +} + + +static inline void fifo16_push_locked(FIFOBuffer16 *f, uint16_t c) { + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + fifo16_push(f, c); + } +} + +static inline size_t fifo16_pop_locked(FIFOBuffer16 *f) { + size_t c; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + c = fifo16_pop(f); + } + return c; +} +*/ + +inline void fifo16_init(FIFOBuffer16 *f, uint16_t *buffer, uint16_t size) { + f->head = f->tail = f->begin = buffer; + f->end = buffer + size; +} + +inline uint16_t fifo16_len(FIFOBuffer16 *f) { + return (f->end - f->begin); +} + +extern void stopRadio(); +void host_disconnected() { + stopRadio(); + cable_state = CABLE_STATE_DISCONNECTED; + current_rssi = -292; + last_rssi = -292; + last_rssi_raw = 0x00; + last_snr_raw = 0x80; +} \ No newline at end of file diff --git a/arduino-cli.yaml b/arduino-cli.yaml new file mode 100755 index 0000000..6dd5f8d --- /dev/null +++ b/arduino-cli.yaml @@ -0,0 +1,7 @@ +board_manager: + additional_urls: + - https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + - https://raw.githubusercontent.com/RAKwireless/RAKwireless-Arduino-BSP-Index/main/package_rakwireless_index.json + - https://github.com/HelTecAutomation/Heltec_nRF52/releases/download/1.7.0/package_heltec_nrf_index.json + - https://adafruit.github.io/arduino-board-index/package_adafruit_index.json + - http://unsigned.io/arduino/package_unsignedio_UnsignedBoards_index.json diff --git a/boards/rak11200.json b/boards/rak11200.json new file mode 100755 index 0000000..614e891 --- /dev/null +++ b/boards/rak11200.json @@ -0,0 +1,39 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_ESP32_DEV", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "rak11200" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "frameworks": [ + "arduino", + "espidf" + ], + "name": "WisCore RAK11200 Board", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "protocols": [ + "esptool", + "espota", + "ftdi" + ], + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.rakwireless.com", + "vendor": "RAKwireless" +} diff --git a/boards/rak11300.json b/boards/rak11300.json new file mode 100755 index 0000000..3a8ec3f --- /dev/null +++ b/boards/rak11300.json @@ -0,0 +1,42 @@ +{ + "build": { + "core": "arduino", + "cpu": "cortex-m0plus", + "extra_flags": "-D ARDUINO_RASPBERRY_PI_PICO -DARDUINO_ARCH_RP2040", + "f_cpu": "133000000L", + "hwids": [ + [ + "0x2E8A", + "0x00C0" + ] + ], + "mcu": "rp2040", + "variant": "rak11300" + }, + "debug": { + "jlink_device": "RP2040_M0_0", + "openocd_target": "rp2040.cfg", + "svd_path": "rp2040.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "WisBlock RAK11300", + "upload": { + "maximum_ram_size": 270336, + "maximum_size": 2097152, + "require_upload_port": true, + "native_usb": true, + "use_1200bps_touch": true, + "wait_for_upload_port": false, + "protocol": "picotool", + "protocols": [ + "cmsis-dap", + "jlink", + "raspberrypi-swd", + "picotool" + ] + }, + "url": "https://docs.rakwireless.com/", + "vendor": "RAKwireless" +} \ No newline at end of file diff --git a/boards/rak3112.json b/boards/rak3112.json new file mode 100755 index 0000000..da3e863 --- /dev/null +++ b/boards/rak3112.json @@ -0,0 +1,51 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DRAK3112", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DBOARD_HAS_PSRAM" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "dio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "rak3112" + }, + "connectivity": [ + "wifi", + "bluetooth" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "RAKwireless RAK3112", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": false, + "require_upload_port": true, + "speed": 921600 + }, + "url": "http://www.rakwireless.com/", + "vendor": "RAKwireless" +} \ No newline at end of file diff --git a/boards/rak4630.json b/boards/rak4630.json new file mode 100755 index 0000000..c7ef814 --- /dev/null +++ b/boards/rak4630.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x8029" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ], + [ + "0x239A", + "0x802A" + ] + ], + "usb_product": "WisCore RAK4631 Board", + "mcu": "nrf52840", + "variant": "rak4630", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "WisCore RAK4631 Board", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.rakwireless.com", + "vendor": "RAKwireless" +} \ No newline at end of file diff --git a/boards/t-beams3-supreme.json b/boards/t-beams3-supreme.json new file mode 100755 index 0000000..95a6501 --- /dev/null +++ b/boards/t-beams3-supreme.json @@ -0,0 +1,51 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_USB_MODE=1", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "LilyGo T-Beam supreme (8MB Flash 8MB PSRAM)", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.lilygo.cc/products/t-beamsupreme-m", + "vendor": "LilyGo" +} \ No newline at end of file diff --git a/esp32_btbufs.py b/esp32_btbufs.py new file mode 100755 index 0000000..ad0ae70 --- /dev/null +++ b/esp32_btbufs.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import sys +import re + +try: + target_path = sys.argv[1] + rxbuf_size = 0; rxbuf_minsize = 6144 + txbuf_size = 0; txbuf_minsize = 384 + line_index = 0 + rx_line_index = 0 + tx_line_index = 0 + with open(target_path) as sf: + for line in sf: + line_index += 1 + if line.startswith("#define RX_QUEUE_SIZE"): + ents = re.sub(" +", " ", line).split(" ") + try: + rxbuf_size = int(ents[2]) + rx_line_index = line_index + except Exception as e: + print(f"Could not parse Bluetooth RX_QUEUE_SIZE: {e}") + + if line.startswith("#define TX_QUEUE_SIZE"): + ents = re.sub(" +", " ", line).split(" ") + try: + txbuf_size = int(ents[2]) + tx_line_index = line_index + except Exception as e: + print(f"Could not parse Bluetooth TX_QUEUE_SIZE: {e}") + + if rxbuf_size != 0 and txbuf_size != 0: + break + + if rxbuf_size < rxbuf_minsize: + print(f"Error: The configured ESP32 Bluetooth RX buffer size is too small, please set it to at least {rxbuf_minsize} and try compiling again.") + print(f"The buffer configuration can be modified in line {rx_line_index} of: {target_path}") + exit(1) + + if txbuf_size < txbuf_minsize: + print(f"Error: The configured ESP32 Bluetooth TX buffer size is too small, please set it to at least {txbuf_minsize} and try compiling again.") + print(f"The buffer configuration can be modified in line {tx_line_index} of: {target_path}") + exit(1) + + exit(0) + +except Exception as e: + print(f"Could not determine ESP32 Bluetooth buffer configuration: {e}") + print("Please fix this error and try again") \ No newline at end of file diff --git a/extra_script.py b/extra_script.py new file mode 100755 index 0000000..0ebcec2 --- /dev/null +++ b/extra_script.py @@ -0,0 +1,197 @@ +import time +import hashlib +import shutil + +Import("env") + +env.Replace(PROGNAME="rnode_firmware_%s" % env.GetProjectOption("custom_variant")) +print("PROGNAME:", env.subst("$PROGNAME")) + +# +# Custom targets +# + +def target_package(target, source, env): + print("target_package...") + print("Platform:", env.GetProjectOption("platform")) + print("Board:", env.GetProjectOption("board")) + print("Variant:", env.GetProjectOption("custom_variant")) + # do some actions + platform = env.GetProjectOption("platform") + board = env.GetProjectOption("board") + firmware_package(env) + +platform = env.GetProjectOption("platform") +print("Platform:", platform) +if (platform == "espressif32"): + env.AddCustomTarget( + name="package", + dependencies="$BUILD_DIR/${PROGNAME}.bin", + actions=[ + target_package + ], + title="Package", + description="Package esp32 firmware for delivery" + ) +elif (platform == "nordicnrf52"): + # remove --specs=nano.specs to allow exceptions to work + if '--specs=nano.specs' in env['LINKFLAGS']: + env['LINKFLAGS'].remove('--specs=nano.specs') + env.AddCustomTarget( + name="package", + dependencies="$BUILD_DIR/${PROGNAME}.zip", + actions=[ + target_package + ], + title="Package", + description="Package nrf52 firmware for delivery" + ) + +# +# Upload actions +# + +def pre_upload(source, target, env): + print("pre_upload...") + # do some actions + +def post_upload(source, target, env): + print("post_upload...") + print("Platform:", env.GetProjectOption("platform")) + print("Board:", env.GetProjectOption("board")) + print("Variant:", env.GetProjectOption("custom_variant")) + print("Serial port:", env.subst("$UPLOAD_PORT")) + # do some actions + platform = env.GetProjectOption("platform") + board = env.GetProjectOption("board") + if (platform == "espressif32"): + time.sleep(10) + # device provisioning is incomplete and only currently appropriate for 915MHz T-Beam + device_provision(env) + firmware_hash(source, env) + # firmware pacakaging is incomplete due to missing console image + #firmware_package(env) + elif (platform == "nordicnrf52"): + time.sleep(5) + # device provisioning is incomplete and only currently appropriate for 915MHz RAK4631 + device_provision(env) + firmware_hash(source, env) + # firmware pacakaging is incomplete due to missing console image + #firmware_package(env) + +def post_clean(source, target, env): + print("post_clean...") + core_dir = env.subst("$CORE_DIR") + print("core_dir:", core_dir) + packages_dir = env.subst("$PACKAGES_DIR") + print("packages_dir:", packages_dir) + project_dir = env.subst("$PROJECT_DIR") + print("project_dir:", project_dir) + #build_dir = env.subst("$BUILD_DIR").get_abspath() + build_dir = env.subst("$BUILD_DIR") + print("build_dir:", build_dir) + build_cache_dir = env.subst("$PLATFORMIO_BUILD_CACHE_DIR") + print("build_cache_dir:", build_cache_dir) + workspace_dir = env.subst("$PLATFORMIO_WORKSPACE_DIR") + print("workspace_dir:", workspace_dir) + #shutil.rmtree(directory_path) + env.Execute("rm -f " + project_dir + "/Release/" + project_dir + "/" + env.subst("$PROGNAME") + ".zip") + +env.AddPreAction("upload", pre_upload) +env.AddPostAction("upload", post_upload) +env.AddPostAction("clean", post_clean) + +def device_wipe(env): + # Device wipe + print("Wiping device...") + env.Execute("rnodeconf --eeprom-wipe " + env.subst("$UPLOAD_PORT")) + +def device_provision(env): + # Device provision + print("Provisioning device...") + platform = env.GetProjectOption("platform") + print("Platform:", platform) + board = env.GetProjectOption("board") + print("Board:", board) + variant = env.GetProjectOption("custom_variant") + print("Variant:", variant) + if variant in ("tbeam", "tbeam_local"): + env.Execute("rnodeconf --product e0 --model e9 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) + elif variant in ("lora32v21", "lora32v21_local"): + env.Execute("rnodeconf --product b1 --model b9 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) + elif variant in ("heltec32v4", "heltec32v4_local", "heltec32v4_boundary", "heltec32v4_boundary_local"): + env.Execute("rnodeconf --product b1 --model b9 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) + elif variant in ("rak4631", "rak4631_local"): + env.Execute("rnodeconf --product 10 --model 12 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) + elif variant in ("heltec_t114", "heltec_t114_local"): + env.Execute("rnodeconf --product c2 --model c7 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) + +def firmware_hash(source, env): + # Firmware hash + print("Updating firmware hash...") + source_file = source[0].get_abspath() + platform = env.GetProjectOption("platform") + print("Platform:", platform) + if (platform == "nordicnrf52"): + build_dir = env.subst("$BUILD_DIR") + env.Execute("cd " + build_dir + "; unzip -o " + source_file + " " + env.subst("$PROGNAME") + ".bin") + #source_file.replace(".zip", ".bin") + source_file = build_dir + "/" + env.subst("$PROGNAME") + ".bin"; + print("source_file:", source_file) + firmware_data = open(source_file, "rb").read() + calc_hash = hashlib.sha256(firmware_data).digest() + hex_hash = calc_hash.hex() + print("firmware_hash:", hex_hash) + env.Execute("rnodeconf --firmware-hash " + hex_hash + " " + env.subst("$UPLOAD_PORT")) + else: + print("source_file:", source_file) + firmware_data = open(source_file, "rb").read() + calc_hash = hashlib.sha256(firmware_data[0:-32]).digest() + part_hash = firmware_data[-32:] + hex_hash = calc_hash.hex() + print("firmware_hash:", hex_hash) + if (calc_hash == part_hash): + env.Execute("rnodeconf --firmware-hash " + hex_hash + " " + env.subst("$UPLOAD_PORT")) + else: + print("Calculated hash does not match!") + +def firmware_package(env): + platform = env.GetProjectOption("platform") + board = env.GetProjectOption("board") + # Firmware package + print("Building firmware package...") + platform = env.GetProjectOption("platform") + print("Platform:", platform) + board = env.GetProjectOption("board") + print("Board:", board) + variant = env.GetProjectOption("custom_variant") + print("Variant:", variant) + core_dir = env.subst("$CORE_DIR") + print("core_dir:", core_dir) + packages_dir = env.subst("$PACKAGES_DIR") + print("packages_dir:", packages_dir) + workspace_dir = env.subst("$WORKSPACE_DIR") + print("workspace_dir:", workspace_dir) + project_dir = env.subst("$PROJECT_DIR") + print("project_dir:", project_dir) + #build_dir = env.subst("$BUILD_DIR").get_abspath() + build_dir = env.subst("$BUILD_DIR") + print("build_dir:", build_dir) + if (platform == "espressif32"): + #env.Execute("cp " + packages_dir + "/framework-arduinoespressif32/tools/partitions/boot_app0.bin " + build_dir + "/rnode_firmware_" + variant + ".boot_app0") + env.Execute("cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin " + build_dir + "/rnode_firmware_" + variant + ".boot_app0") + env.Execute("cp " + build_dir + "/bootloader.bin " + build_dir + "/" + env.subst("$PROGNAME") + ".bootloader") + env.Execute("cp " + build_dir + "/partitions.bin " + build_dir + "/" + env.subst("$PROGNAME") + ".partitions") + env.Execute("rm -f " + project_dir + "/Release/" + env.subst("$PROGNAME") + ".zip") + zip_cmd = "zip --junk-paths " + zip_cmd += project_dir + "/Release/rnode_firmware_" + variant + ".zip " + zip_cmd += project_dir + "/Release/esptool/esptool.py " + zip_cmd += project_dir + "/Release/console_image.bin " + zip_cmd += build_dir + "/" + env.subst("$PROGNAME") + ".bin " + zip_cmd += build_dir + "/" + env.subst("$PROGNAME") + ".boot_app0 " + zip_cmd += build_dir + "/" + env.subst("$PROGNAME") + ".bootloader " + zip_cmd += build_dir + "/" + env.subst("$PROGNAME") + ".partitions " + env.Execute(zip_cmd) + elif (platform == "nordicnrf52"): + env.Execute("cp " + build_dir + "/" + env.subst("$PROGNAME") + ".zip " + project_dir + "/Release/.") + env.Execute("python " + project_dir + "/release_hashes.py > " + project_dir + "/Release/release.json") diff --git a/nrf52_hash.py b/nrf52_hash.py new file mode 100755 index 0000000..f75e98f --- /dev/null +++ b/nrf52_hash.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +# Copyright (C) 2023, Mark Qvist + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import RNS +import json +import hashlib + +major_version = None +minor_version = None +target_version = None + +target_file = os.path.join(sys.argv[1]) + +firmware_data = open(target_file, "rb").read() +calc_hash = hashlib.sha256(firmware_data).digest() + +print(RNS.hexrep(calc_hash, delimit=False)) diff --git a/partition_hashes b/partition_hashes new file mode 100755 index 0000000..9d8db4b --- /dev/null +++ b/partition_hashes @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +# Copyright (C) 2024, Mark Qvist + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import RNS +import json +import hashlib +import subprocess + +major_version = None +minor_version = None +target_version = None + +target_file = os.path.join(sys.argv[1]) + +if sys.argv[1] == "from_device": + from_device = True +else: + from_device = False + +if not from_device: + firmware_data = open(target_file, "rb").read() + calc_hash = hashlib.sha256(firmware_data[0:-32]).digest() + part_hash = firmware_data[-32:] + + if calc_hash == part_hash: + print(RNS.hexrep(part_hash, delimit=False)) + +else: + try: + cmdresult = subprocess.run(["rnodeconf", sys.argv[2], "-L"], stdout=subprocess.PIPE).stdout.decode('utf-8') + part_hash = cmdresult.split("The actual firmware hash is: ")[1].replace("\n", "") + print(part_hash) + except Exception as e: + print("Could not get partition hash from device: "+str(e)) diff --git a/platformio.ini b/platformio.ini new file mode 100755 index 0000000..90f8a59 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,557 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +; Change source and include directories to root of project since RNode places them here +include_dir = . +src_dir = . + +[env] +framework = arduino +monitor_speed = 115200 +upload_speed = 460800 +build_flags = + -Wall + ;-Wextra + -Wno-missing-field-initializers + -Wno-format + -I. + ; CBA Define following to disable DEBUG build + ;-DNDEBUG + ; CBA Define following to include RNS stack + -DHAS_RNS + -DRNS_USE_FS + -DRNS_PERSIST_PATHS + -DMSGPACK_USE_BOOST=OFF + ; CBA Define following to disable LFS asserts + ;-DLFS_NO_ASSERT + ; ??? + ;-DLFS_YES_TRACE + ; ??? + ;-DCORE_DEBUG_LEVEL=5 + ; ??? NO + ;-DLOG_LOCAL_LEVEL=5 + ;-DCONFIG_LOG_DEFAULT_LEVEL=5 +lib_deps = + ArduinoJson@^7.4.2 + MsgPack@^0.4.2 + adafruit/Adafruit SSD1306@^2.5.9 + https://github.com/attermann/Crypto.git +; Exclude directories in root from sources +build_src_filter = +<*> - +extra_scripts = pre:extra_script.py + +[env:rnode-ng-20] +platform = espressif32 +board = ttgo-lora32-v2 +custom_variant = ng20 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_RNODE_NG_20 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + adafruit/Adafruit NeoPixel@^1.12.0 + attermann/microReticulum + +[env:rnode-ng-21] +platform = espressif32 +board = ttgo-lora32-v21 +custom_variant = ng21 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_RNODE_NG_21 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + adafruit/Adafruit NeoPixel@^1.12.0 + attermann/microReticulum + +[env:ttgo-t-beam] +platform = espressif32 +board = ttgo-t-beam +custom_variant = tbeam +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_TBEAM +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:ttgo-t-beam-sx1262] +platform = espressif32 +board = ttgo-t-beam +custom_variant = tbeam_sx1262 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_TBEAM + -DMODEM=SX1262 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:ttgo-t-beam-supreme] +platform = espressif32 +board = ttgo-t-beam +custom_variant = tbeam_supreme +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_TBEAM_S_V1 + -DMODEM=SX1262 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + Adafruit_SH110X + adafruit/Adafruit SH110X@^2.1.14 + attermann/microReticulum + +[env:lilygo-t3-s3] +platform = espressif32 +board = lilygo-t3-s3 +custom_variant = t3s3 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_T3S3 + -DMODEM=SX1262 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:lilygo-t3-s3-sx127x] +platform = espressif32 +board = lilygo-t3-s3 +custom_variant = t3s3_sx127x +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_T3S3 + -DMODEM=SX1276 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:lilygo-t3-s3-sx1280-pa] +platform = espressif32 +board = lilygo-t3-s3 +custom_variant = t3s3_sx1280_pa +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_T3S3 + -DMODEM=SX1280 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:lilygo-t-deck] +platform = espressif32 +board = esp32-s3-devkitc-1 +custom_variant = tdeck +board_build.filesystem = littlefs +; Flash / memory layout +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_build.partitions = default_16MB.csv +; Enable PSRAM + correct flash mode +board_build.flash_mode = qio +board_build.psram_type = opi +board_build.arduino.memory_type = qio_opi +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_TDECK + -DBOARD_HAS_PSRAM=1 + -DARDUINO_USB_MODE=1 + -DCORE_DEBUG_LEVEL=1 + ; Enable UARDUINO_ USB_ CDC_ ON_ BOOT will start printing and wait for terminal access during startup + -DARDUINO_USB_CDC_ON_BOOT=1 + ; Enable UARDUINO_USB_CDC_ON_BOOT will turn off printing and will not block when using the battery + ; -UARDUINO_USB_CDC_ON_BOOT +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:ttgo-lora32-v1] +platform = espressif32 +board = ttgo-lora32-v1 +custom_variant = lora32v10 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_LORA32_V1_0 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:ttgo-lora32-v2] +platform = espressif32 +board = ttgo-lora32-v2 +custom_variant = lora32v20 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_LORA32_V2_0 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:ttgo-lora32-v2-extled] +platform = espressif32 +board = ttgo-lora32-v2 +custom_variant = lora32v20_extled +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_LORA32_V1_0 + -DEXTERNAL_LEDS=true +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:ttgo-lora32-v21] +platform = espressif32 +board = ttgo-lora32-v21 +custom_variant = lora32v21 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_LORA32_V2_1 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:ttgo-lora32-v21-extled] +platform = espressif32 +board = ttgo-lora32-v21 +custom_variant = lora32v21_extled +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_LORA32_V2_1 + -DEXTERNAL_LEDS=true +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:ttgo-lora32-v21-tcxo] +platform = espressif32 +board = ttgo-lora32-v21 +custom_variant = lora32v21_extled +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_LORA32_V2_1 + -DENABLE_TCXO=true +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:heltec_wifi_lora_32_V2] +platform = espressif32 +board = heltec_wifi_lora_32_V2 +custom_variant = heltec32v2 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_LORA32_V2_1 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:heltec_wifi_lora_32_V2-extled] +platform = espressif32 +board = heltec_wifi_lora_32_V2 +custom_variant = heltec32v2_extled +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_LORA32_V2_1 + -DEXTERNAL_LEDS=true +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:heltec_wifi_lora_32_V3] +platform = espressif32 +board = heltec_wifi_lora_32_V3 +custom_variant = heltec32v3 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_HELTEC32_V3 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:heltec_wifi_lora_32_V4] +platform = espressif32 +board = esp32-s3-devkitc-1 +custom_variant = heltec32v4 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_HELTEC32_V4 + -DARDUINO_USB_CDC_ON_BOOT=1 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:heltec_V4_boundary] +platform = espressif32 +board = esp32-s3-devkitc-1 +custom_variant = heltec32v4_boundary +board_build.filesystem = littlefs +; Flash / memory layout for 16MB flash + 2MB PSRAM +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_build.partitions = default_16MB.csv +board_build.flash_mode = qio +board_build.psram_type = qio +board_build.arduino.memory_type = qio_qspi +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_HELTEC32_V4 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DBOARD_HAS_PSRAM=1 + -DBOUNDARY_MODE + ; --- Boundary mode defaults (override via EEPROM at runtime) --- + ; TCP server mode (0=server, 1=client) + -DBOUNDARY_TCP_MODE=0 + ; TCP listen/connect port + -DBOUNDARY_TCP_PORT=4242 + ; Backbone host for client mode (empty = server mode) + ; -DBOUNDARY_BACKBONE_HOST=\"192.168.1.100\" + ; -DBOUNDARY_BACKBONE_PORT=4242 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum +monitor_filters = esp32_exception_decoder + +[env:heltec_V4_boundary-local] +platform = espressif32 +board = esp32-s3-devkitc-1 +custom_variant = heltec32v4_boundary_local +board_build.filesystem = littlefs +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_build.partitions = default_16MB.csv +board_build.flash_mode = qio +board_build.psram_type = qio +board_build.arduino.memory_type = qio_qspi +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_HELTEC32_V4 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DBOARD_HAS_PSRAM=1 + -DBOUNDARY_MODE + -DBOUNDARY_TCP_MODE=0 + -DBOUNDARY_TCP_PORT=4242 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + microReticulum=symlink://../microReticulum +monitor_filters = esp32_exception_decoder + +[env:featheresp32] +platform = espressif32 +board = featheresp32 +custom_variant = featheresp32 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_HUZZAH32 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:seeed_xiao_esp32s3] +platform = espressif32 +board = seeed_xiao_esp32s3 +custom_variant = xiao_esp32s3 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_XIAO_S3 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:generic-esp32] +platform = espressif32 +board = esp32dev +custom_variant = esp32_generic +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_GENERIC_ESP32 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + attermann/microReticulum + +[env:wiscore_rak4631] +platform = nordicnrf52 +board = rak4630 +custom_variant = rak4631 +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_src_filter = ${env.build_src_filter} + +build_flags = + ${env.build_flags} + -I variants/rak4630 + -fexceptions + -DBOARD_MODEL=BOARD_RAK4631 + -DRNS_USE_TLSF=1 + -DRNS_USE_ALLOCATOR=1 +lib_deps = + ${env.lib_deps} + attermann/microReticulum + + + +[env:ttgo-t-beam-local] +platform = espressif32 +board = ttgo-t-beam +upload_speed = 460800 +custom_variant = tbeam_local +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -fexceptions + -DBOARD_MODEL=BOARD_TBEAM + ; CBA TEST + ;-DUSE_FLASHFS=1 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + microReticulum=symlink://../microReticulum + Adafruit_SPIFlash=symlink://../Adafruit_SPIFlash +monitor_filters = esp32_exception_decoder + +[env:ttgo-lora32-v21-local] +platform = espressif32 +board = ttgo-lora32-v21 +upload_speed = 460800 +custom_variant = lora32v21_local +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_LORA32_V2_1 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + microReticulum=symlink://../microReticulum +monitor_filters = esp32_exception_decoder + +[env:heltec_wifi_lora_32_V4-local] +platform = espressif32 +board = esp32-s3-devkitc-1 +custom_variant = heltec32v4_local +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_HELTEC32_V4 + -DARDUINO_USB_CDC_ON_BOOT=1 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + microReticulum=symlink://../microReticulum +monitor_filters = esp32_exception_decoder + +[env:wiscore_rak4631-local] +platform = nordicnrf52 +board = rak4630 +custom_variant = rak4631_local +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_src_filter = ${env.build_src_filter} + +build_flags = + ${env.build_flags} + -I variants/rak4630 + -fexceptions + -DBOARD_MODEL=BOARD_RAK4631 + ; CBA TEST + -DRNS_USE_TLSF=1 + -DRNS_USE_ALLOCATOR=1 + ;-DUSE_FLASHFS=1 +build_unflags = -fno-exceptions +lib_deps = + ${env.lib_deps} + microReticulum=symlink://../microReticulum + Adafruit_SPIFlash=symlink://../Adafruit_SPIFlash + +[env:heltec_t114_local] +;upload_port = /dev/cu.usbmodem1101 +platform = nordicnrf52 +board = nrf52840_dk_adafruit +custom_variant = heltec_t114_local +board_build.partitions = no_ota.csv +board_build.filesystem = littlefs +build_flags = + ${env.build_flags} + -fexceptions + -DBOARD_MODEL=BOARD_HELTEC_T114 + ; CBA TEST + -DRNS_USE_TLSF=1 + -DRNS_USE_ALLOCATOR=1 +build_unflags = -fno-exceptions +lib_deps = + ${env.lib_deps} + https://github.com/liamcottle/esp8266-oled-ssd1306#e16cee124fe26490cb14880c679321ad8ac89c95 + adafruit/Adafruit NeoPixel@^1.12.0 + microReticulum=symlink://../microReticulum diff --git a/release_hashes.py b/release_hashes.py new file mode 100755 index 0000000..ce90963 --- /dev/null +++ b/release_hashes.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2024, Mark Qvist + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import json +import hashlib + +major_version = None +minor_version = None +target_version = None + +file = open("Config.h", "rb") +config_data = file.read().splitlines() +for line in config_data: + dline = line.decode("utf-8").strip() + components = dline.split() + if dline.startswith("#define MAJ_VERS"): + major_version = "%01d" % ord(bytes.fromhex(dline.split()[2].split("x")[1])) + if dline.startswith("#define MIN_VERS"): + minor_version = "%02d" % ord(bytes.fromhex(dline.split()[2].split("x")[1])) + +target_version = major_version+"."+minor_version + +release_hashes = {} +target_dir = "./Release" +files = os.listdir(target_dir) +for filename in files: + if os.path.isfile(os.path.join(target_dir, filename)): + if filename.startswith("rnode_firmware"): + file = open(os.path.join(target_dir, filename), "rb") + release_hashes[filename] = { + "hash": hashlib.sha256(file.read()).hexdigest(), + "version": target_version + } + +print(json.dumps(release_hashes)) diff --git a/sx126x.cpp b/sx126x.cpp new file mode 100755 index 0000000..651f52f --- /dev/null +++ b/sx126x.cpp @@ -0,0 +1,824 @@ +// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva. +// Licensed under the MIT license. + +#include "Boards.h" + +#if MODEM == SX1262 +#include "sx126x.h" + +#if MCU_VARIANT == MCU_ESP32 + #if MCU_VARIANT == MCU_ESP32 and !defined(CONFIG_IDF_TARGET_ESP32S3) + #include "soc/rtc_wdt.h" + #endif + #define ISR_VECT IRAM_ATTR +#else + #define ISR_VECT +#endif + +#define OP_RF_FREQ_6X 0x86 +#define OP_SLEEP_6X 0x84 +#define OP_STANDBY_6X 0x80 +#define OP_TX_6X 0x83 +#define OP_RX_6X 0x82 +#define OP_PA_CONFIG_6X 0x95 +#define OP_SET_IRQ_FLAGS_6X 0x08 // Also provides info such as + // preamble detection, etc for + // knowing when it's safe to switch + // antenna modes +#define OP_CLEAR_IRQ_STATUS_6X 0x02 +#define OP_GET_IRQ_STATUS_6X 0x12 +#define OP_RX_BUFFER_STATUS_6X 0x13 +#define OP_PACKET_STATUS_6X 0x14 // Get snr & rssi of last packet +#define OP_CURRENT_RSSI_6X 0x15 +#define OP_MODULATION_PARAMS_6X 0x8B // BW, SF, CR, etc. +#define OP_PACKET_PARAMS_6X 0x8C // CRC, preamble, payload length, etc. +#define OP_STATUS_6X 0xC0 +#define OP_TX_PARAMS_6X 0x8E // Set dbm, etc +#define OP_PACKET_TYPE_6X 0x8A +#define OP_BUFFER_BASE_ADDR_6X 0x8F +#define OP_READ_REGISTER_6X 0x1D +#define OP_WRITE_REGISTER_6X 0x0D +#define OP_DIO3_TCXO_CTRL_6X 0x97 +#define OP_DIO2_RF_CTRL_6X 0x9D +#define OP_CAD_PARAMS 0x88 +#define OP_CALIBRATE_6X 0x89 +#define OP_RX_TX_FALLBACK_MODE_6X 0x93 +#define OP_REGULATOR_MODE_6X 0x96 +#define OP_CALIBRATE_IMAGE_6X 0x98 + +#define MASK_CALIBRATE_ALL 0x7f + +#define IRQ_TX_DONE_MASK_6X 0x01 +#define IRQ_RX_DONE_MASK_6X 0x02 +#define IRQ_HEADER_DET_MASK_6X 0x10 +#define IRQ_PREAMBLE_DET_MASK_6X 0x04 +#define IRQ_PAYLOAD_CRC_ERROR_MASK_6X 0x40 +#define IRQ_ALL_MASK_6X 0b0100001111111111 + +#define MODE_LONG_RANGE_MODE_6X 0x01 + +#define OP_FIFO_WRITE_6X 0x0E +#define OP_FIFO_READ_6X 0x1E +#define REG_OCP_6X 0x08E7 +#define REG_LNA_6X 0x08AC // No agc in sx1262 +#define REG_SYNC_WORD_MSB_6X 0x0740 +#define REG_SYNC_WORD_LSB_6X 0x0741 +#define REG_PAYLOAD_LENGTH_6X 0x0702 // https://github.com/beegee-tokyo/SX126x-Arduino/blob/master/src/radio/sx126x/sx126x.h#L98 +#define REG_RANDOM_GEN_6X 0x0819 + +#define MODE_TCXO_3_3V_6X 0x07 +#define MODE_TCXO_3_0V_6X 0x06 +#define MODE_TCXO_2_7V_6X 0x06 +#define MODE_TCXO_2_4V_6X 0x06 +#define MODE_TCXO_2_2V_6X 0x03 +#define MODE_TCXO_1_8V_6X 0x02 +#define MODE_TCXO_1_7V_6X 0x01 +#define MODE_TCXO_1_6V_6X 0x00 + +#define MODE_STDBY_RC_6X 0x00 +#define MODE_STDBY_XOSC_6X 0x01 +#define MODE_FALLBACK_STDBY_RC_6X 0x20 +#define MODE_IMPLICIT_HEADER 0x01 +#define MODE_EXPLICIT_HEADER 0x00 + +#define SYNC_WORD_6X 0x1424 + +#define XTAL_FREQ_6X (double)32000000 +#define FREQ_DIV_6X (double)pow(2.0, 25.0) +#define FREQ_STEP_6X (double)(XTAL_FREQ_6X / FREQ_DIV_6X) + +#if BOARD_MODEL == BOARD_TECHO + SPIClass spim3 = SPIClass(NRF_SPIM3, pin_miso, pin_sclk, pin_mosi) ; + #define SPI spim3 + +#elif defined(NRF52840_XXAA) + extern SPIClass spiModem; + #define SPI spiModem +#endif + +extern SPIClass SPI; + +#define MAX_PKT_LENGTH 255 + +sx126x::sx126x() : + _spiSettings(16E6, MSBFIRST, SPI_MODE0), + _ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN), _busy(LORA_DEFAULT_BUSY_PIN), _rxen(LORA_DEFAULT_RXEN_PIN), + _frequency(0), + _txp(0), + _sf(0x07), + _bw(0x04), + _cr(0x01), + _ldro(0x00), + _packetIndex(0), + _preambleLength(18), + _implicitHeaderMode(0), + _payloadLength(255), + _crcMode(1), + _fifo_tx_addr_ptr(0), + _fifo_rx_addr_ptr(0), + _packet({0}), + _preinit_done(false), + _dio0_risen(false), + _onReceive(NULL) +{ setTimeout(0); } + +bool sx126x::preInit() { + pinMode(_ss, OUTPUT); + digitalWrite(_ss, HIGH); + + #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3 + SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs); + #elif BOARD_MODEL == BOARD_TECHO + SPI.setPins(pin_miso, pin_sclk, pin_mosi); + SPI.begin(); + #else + SPI.begin(); + #endif + + // Check version (retry for up to 2 seconds) + // TODO: Actually read version registers, not syncwords + long start = millis(); + uint8_t syncmsb; + uint8_t synclsb; + while (((millis() - start) < 2000) && (millis() >= start)) { + syncmsb = readRegister(REG_SYNC_WORD_MSB_6X); + synclsb = readRegister(REG_SYNC_WORD_LSB_6X); + if ( uint16_t(syncmsb << 8 | synclsb) == 0x1424 || uint16_t(syncmsb << 8 | synclsb) == 0x4434) { + break; + } + delay(100); + } + if ( uint16_t(syncmsb << 8 | synclsb) != 0x1424 && uint16_t(syncmsb << 8 | synclsb) != 0x4434) { + return false; + } + + _preinit_done = true; + return true; +} + +uint8_t ISR_VECT sx126x::readRegister(uint16_t address) { + return singleTransfer(OP_READ_REGISTER_6X, address, 0x00); +} + +void sx126x::writeRegister(uint16_t address, uint8_t value) { + singleTransfer(OP_WRITE_REGISTER_6X, address, value); +} + +uint8_t ISR_VECT sx126x::singleTransfer(uint8_t opcode, uint16_t address, uint8_t value) { + waitOnBusy(); + + uint8_t response; + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(opcode); + SPI.transfer((address & 0xFF00) >> 8); + SPI.transfer(address & 0x00FF); + if (opcode == OP_READ_REGISTER_6X) { SPI.transfer(0x00); } + response = SPI.transfer(value); + SPI.endTransaction(); + + digitalWrite(_ss, HIGH); + + return response; +} + +void sx126x::rxAntEnable() { + if (_rxen != -1) { digitalWrite(_rxen, HIGH); } +} + +void sx126x::loraMode() { + // Enable lora mode on the SX1262 chip + uint8_t mode = MODE_LONG_RANGE_MODE_6X; + executeOpcode(OP_PACKET_TYPE_6X, &mode, 1); +} + +void sx126x::waitOnBusy() { + unsigned long time = millis(); + if (_busy != -1) { + while (digitalRead(_busy) == HIGH) { + if (millis() >= (time + 100)) { break; } + } + } +} + +void sx126x::executeOpcode(uint8_t opcode, uint8_t *buffer, uint8_t size) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(opcode); + for (int i = 0; i < size; i++) { SPI.transfer(buffer[i]); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +void sx126x::executeOpcodeRead(uint8_t opcode, uint8_t *buffer, uint8_t size) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(opcode); + SPI.transfer(0x00); + for (int i = 0; i < size; i++) { buffer[i] = SPI.transfer(0x00); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +void sx126x::writeBuffer(const uint8_t* buffer, size_t size) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(OP_FIFO_WRITE_6X); + SPI.transfer(_fifo_tx_addr_ptr); + for (int i = 0; i < size; i++) { SPI.transfer(buffer[i]); _fifo_tx_addr_ptr++; } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +void sx126x::readBuffer(uint8_t* buffer, size_t size) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(OP_FIFO_READ_6X); + SPI.transfer(_fifo_rx_addr_ptr); + SPI.transfer(0x00); + for (int i = 0; i < size; i++) { buffer[i] = SPI.transfer(0x00); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +void sx126x::setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, int ldro) { + // Because there is no access to these registers on the sx1262, we have + // to set all these parameters at once or not at all. + uint8_t buf[8]; + buf[0] = sf; + buf[1] = bw; + buf[2] = cr; + buf[3] = ldro; // Low data rate toggle + buf[4] = 0x00; // Unused params in LoRa mode + buf[5] = 0x00; + buf[6] = 0x00; + buf[7] = 0x00; + executeOpcode(OP_MODULATION_PARAMS_6X, buf, 8); +} + +void sx126x::setPacketParams(long preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc) { + // Because there is no access to these registers on the sx1262, we have + // to set all these parameters at once or not at all. + uint8_t buf[9]; + buf[0] = uint8_t((preamble_symbols & 0xFF00) >> 8); + buf[1] = uint8_t((preamble_symbols & 0x00FF)); + buf[2] = headermode; + buf[3] = payload_length; + buf[4] = crc; + buf[5] = 0x00; // standard IQ setting (no inversion) + buf[6] = 0x00; // unused params + buf[7] = 0x00; + buf[8] = 0x00; + executeOpcode(OP_PACKET_PARAMS_6X, buf, 9); +} + +void sx126x::reset(void) { + if (_reset != -1) { + pinMode(_reset, OUTPUT); + digitalWrite(_reset, LOW); + delay(10); + digitalWrite(_reset, HIGH); + delay(10); + } +} + +void sx126x::calibrate(void) { + // Put in STDBY_RC mode before calibration + uint8_t mode_byte = MODE_STDBY_RC_6X; + executeOpcode(OP_STANDBY_6X, &mode_byte, 1); + + // Calibrate RC64k, RC13M, PLL, ADC and image + uint8_t calibrate = MASK_CALIBRATE_ALL; + executeOpcode(OP_CALIBRATE_6X, &calibrate, 1); + + delay(5); + waitOnBusy(); +} + +void sx126x::calibrate_image(long frequency) { + uint8_t image_freq[2] = {0}; + if (frequency >= 430E6 && frequency <= 440E6) { image_freq[0] = 0x6B; image_freq[1] = 0x6F; } + else if (frequency >= 470E6 && frequency <= 510E6) { image_freq[0] = 0x75; image_freq[1] = 0x81; } + else if (frequency >= 779E6 && frequency <= 787E6) { image_freq[0] = 0xC1; image_freq[1] = 0xC5; } + else if (frequency >= 863E6 && frequency <= 870E6) { image_freq[0] = 0xD7; image_freq[1] = 0xDB; } + else if (frequency >= 902E6 && frequency <= 928E6) { image_freq[0] = 0xE1; image_freq[1] = 0xE9; } // TODO: Allow higher freq calibration + executeOpcode(OP_CALIBRATE_IMAGE_6X, image_freq, 2); + waitOnBusy(); +} + +int sx126x::begin(long frequency) { + reset(); + + if (_busy != -1) { pinMode(_busy, INPUT); } + if (!_preinit_done) { if (!preInit()) { return false; } } + if (_rxen != -1) { pinMode(_rxen, OUTPUT); } + + calibrate(); + calibrate_image(frequency); + enableTCXO(); + loraMode(); + standby(); + + // Set sync word + setSyncWord(SYNC_WORD_6X); + + #if DIO2_AS_RF_SWITCH + // enable dio2 rf switch + uint8_t byte = 0x01; + executeOpcode(OP_DIO2_RF_CTRL_6X, &byte, 1); + #endif + + rxAntEnable(); + setFrequency(frequency); + setTxPower(2); + enableCrc(); + writeRegister(REG_LNA_6X, 0x96); // Set LNA boost + uint8_t basebuf[2] = {0}; // Set base addresses + executeOpcode(OP_BUFFER_BASE_ADDR_6X, basebuf, 2); + + setModulationParams(_sf, _bw, _cr, _ldro); + setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); + + #if HAS_LORA_PA + #if LORA_PA_GC1109 + // Enable Vfem_ctl for supply to + // PA power net. + pinMode(LORA_PA_PWR_EN, OUTPUT); + digitalWrite(LORA_PA_PWR_EN, HIGH); + + // Enable PA LNA and TX standby + pinMode(LORA_PA_CSD, OUTPUT); + digitalWrite(LORA_PA_CSD, HIGH); + + // Keep PA CPS low until actual + // transmit. Does it save power? + // Who knows? Will have to measure. + // Note from the future: Nope. + // Power consumption is the same, + // and turning it on and off is + // not something that it likes. + // Keeping it high for now. + pinMode(LORA_PA_CPS, OUTPUT); + digitalWrite(LORA_PA_CPS, HIGH); + + // On Heltec V4, the PA CTX pin + // is driven by the SX1262 DIO2 + // pin directly, so we do not + // need to manually raise this. + #endif + #endif + + return 1; +} + +void sx126x::end() { sleep(); SPI.end(); _preinit_done = false; } + +int sx126x::beginPacket(int implicitHeader) { + #if HAS_LORA_PA + #if LORA_PA_GC1109 + // Enable PA CPS for transmit + // digitalWrite(LORA_PA_CPS, HIGH); + // Disabled since we're keeping it + // on permanently as long as the + // radio is powered up. + #endif + #endif + + standby(); + if (implicitHeader) { implicitHeaderMode(); } + else { explicitHeaderMode(); } + + _payloadLength = 0; + _fifo_tx_addr_ptr = 0; + setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); + + return 1; +} + +int sx126x::endPacket() { + setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); + uint8_t timeout[3] = {0}; // Put in single TX mode + executeOpcode(OP_TX_6X, timeout, 3); + + uint8_t buf[2]; + buf[0] = 0x00; + buf[1] = 0x00; + executeOpcodeRead(OP_GET_IRQ_STATUS_6X, buf, 2); + + // Wait for TX done + bool timed_out = false; + uint32_t w_timeout = millis()+LORA_MODEM_TIMEOUT_MS; + while ((millis() < w_timeout) && ((buf[1] & IRQ_TX_DONE_MASK_6X) == 0)) { + buf[0] = 0x00; + buf[1] = 0x00; + executeOpcodeRead(OP_GET_IRQ_STATUS_6X, buf, 2); + yield(); + } + + if (!(millis() < w_timeout)) { timed_out = true; } + + // Clear IRQs + uint8_t mask[2]; + mask[0] = 0x00; + mask[1] = IRQ_TX_DONE_MASK_6X; + executeOpcode(OP_CLEAR_IRQ_STATUS_6X, mask, 2); + if (timed_out) { return 0; } else { return 1; } +} + +unsigned long preamble_detected_at = 0; +extern long lora_preamble_time_ms; +extern long lora_header_time_ms; +bool false_preamble_detected = false; + +bool sx126x::dcd() { + uint8_t buf[2] = {0}; executeOpcodeRead(OP_GET_IRQ_STATUS_6X, buf, 2); + uint32_t now = millis(); + + bool header_detected = false; + bool carrier_detected = false; + + if ((buf[1] & IRQ_HEADER_DET_MASK_6X) != 0) { header_detected = true; carrier_detected = true; } + else { header_detected = false; } + + if ((buf[1] & IRQ_PREAMBLE_DET_MASK_6X) != 0) { + carrier_detected = true; + if (preamble_detected_at == 0) { preamble_detected_at = now; } + if (now - preamble_detected_at > lora_preamble_time_ms + lora_header_time_ms) { + preamble_detected_at = 0; + if (!header_detected) { false_preamble_detected = true; } + uint8_t clearbuf[2] = {0}; + clearbuf[1] = IRQ_PREAMBLE_DET_MASK_6X; + executeOpcode(OP_CLEAR_IRQ_STATUS_6X, clearbuf, 2); + } + } + + // TODO: Maybe there's a way of unlatching the RSSI + // status without re-activating receive mode? + if (false_preamble_detected) { sx126x_modem.receive(); false_preamble_detected = false; } + return carrier_detected; +} + +uint8_t sx126x::currentRssiRaw() { + uint8_t byte = 0; + executeOpcodeRead(OP_CURRENT_RSSI_6X, &byte, 1); + return byte; +} + +int ISR_VECT sx126x::currentRssi() { + uint8_t byte = 0; + executeOpcodeRead(OP_CURRENT_RSSI_6X, &byte, 1); + int rssi = -(int(byte)) / 2; + #if HAS_LORA_LNA + rssi -= LORA_LNA_GAIN; + #endif + return rssi; +} + +uint8_t sx126x::packetRssiRaw() { + uint8_t buf[3] = {0}; + executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3); + return buf[2]; +} + +int ISR_VECT sx126x::packetRssi() { + uint8_t buf[3] = {0}; + executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3); + int pkt_rssi = -buf[0] / 2; + #if HAS_LORA_LNA + pkt_rssi -= LORA_LNA_GAIN; + #endif + return pkt_rssi; +} + +int ISR_VECT sx126x::packetRssi(uint8_t pkt_snr_raw) { + // TODO: May need more calculations here + uint8_t buf[3] = {0}; + executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3); + int pkt_rssi = -buf[0] / 2; + return pkt_rssi; +} + +uint8_t ISR_VECT sx126x::packetSnrRaw() { + uint8_t buf[3] = {0}; + executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3); + return buf[1]; +} + +float ISR_VECT sx126x::packetSnr() { + uint8_t buf[3] = {0}; + executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3); + return float(buf[1]) * 0.25; +} + +long sx126x::packetFrequencyError() { + // TODO: Implement this, no idea how to check it on the sx1262 + const float fError = 0.0; + return static_cast(fError); +} + +size_t sx126x::write(uint8_t byte) { return write(&byte, sizeof(byte)); } +size_t sx126x::write(const uint8_t *buffer, size_t size) { + if ((_payloadLength + size) > MAX_PKT_LENGTH) { size = MAX_PKT_LENGTH - _payloadLength; } + writeBuffer(buffer, size); + _payloadLength = _payloadLength + size; + return size; +} + +int ISR_VECT sx126x::available() { + uint8_t buf[2] = {0}; + executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, buf, 2); + return buf[0] - _packetIndex; +} + +int ISR_VECT sx126x::read(){ + if (!available()) { return -1; } + if (_packetIndex == 0) { + uint8_t rxbuf[2] = {0}; + executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, rxbuf, 2); + int size = rxbuf[0]; + _fifo_rx_addr_ptr = rxbuf[1]; + readBuffer(_packet, size); + } + + uint8_t byte = _packet[_packetIndex]; + _packetIndex++; + return byte; +} + +int sx126x::peek() { + if (!available()) { return -1; } + if (_packetIndex == 0) { + uint8_t rxbuf[2] = {0}; + executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, rxbuf, 2); + int size = rxbuf[0]; + _fifo_rx_addr_ptr = rxbuf[1]; + readBuffer(_packet, size); + } + + uint8_t b = _packet[_packetIndex]; + return b; +} + +void sx126x::flush() { } + +void sx126x::onReceive(void(*callback)(int)){ + _onReceive = callback; + + if (callback) { + pinMode(_dio0, INPUT); + uint8_t buf[8]; // Set preamble and header detection irqs, plus dio0 mask + buf[0] = 0xFF; // Set irq masks, enable all + buf[1] = 0xFF; + buf[2] = 0x00; // Set dio0 masks + buf[3] = IRQ_RX_DONE_MASK_6X; + buf[4] = 0x00; // Set dio1 masks + buf[5] = 0x00; + buf[6] = 0x00; // Set dio2 masks + buf[7] = 0x00; + executeOpcode(OP_SET_IRQ_FLAGS_6X, buf, 8); + + #ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.usingInterrupt(digitalPinToInterrupt(_dio0)); + #endif + attachInterrupt(digitalPinToInterrupt(_dio0), sx126x::onDio0Rise, RISING); + + } else { + detachInterrupt(digitalPinToInterrupt(_dio0)); + #ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0)); + #endif + } +} + +void sx126x::receive(int size) { + #if HAS_LORA_PA + #if LORA_PA_GC1109 + // Disable PA CPS for receive + // digitalWrite(LORA_PA_CPS, LOW); + // That turned out to be a bad idea. + // The LNA goes wonky if it's toggled + // on and off too quickly. We'll keep + // it on permanently, as long as the + // radio is powered up. + #endif + #endif + + if (size > 0) { + implicitHeaderMode(); + _payloadLength = size; + setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); + } else { explicitHeaderMode(); } + + if (_rxen != -1) { rxAntEnable(); } + uint8_t mode[3] = {0xFF, 0xFF, 0xFF}; // Continuous mode + executeOpcode(OP_RX_6X, mode, 3); +} + +void sx126x::standby() { + uint8_t byte = MODE_STDBY_XOSC_6X; // STDBY_XOSC + executeOpcode(OP_STANDBY_6X, &byte, 1); +} + +void sx126x::sleep() { uint8_t byte = 0x00; executeOpcode(OP_SLEEP_6X, &byte, 1); } + +void sx126x::enableTCXO() { + #if HAS_TCXO + #if BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_XIAO_S3 + uint8_t buf[4] = {MODE_TCXO_3_3V_6X, 0x00, 0x00, 0xFF}; + #elif BOARD_MODEL == BOARD_TBEAM + uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; + #elif BOARD_MODEL == BOARD_TDECK + uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; + #elif BOARD_MODEL == BOARD_TBEAM_S_V1 + uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; + #elif BOARD_MODEL == BOARD_T3S3 + uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; + #elif BOARD_MODEL == BOARD_HELTEC_T114 + uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; + #elif BOARD_MODEL == BOARD_TECHO + uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; + #elif BOARD_MODEL == BOARD_HELTEC32_V4 + uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; + #endif + executeOpcode(OP_DIO3_TCXO_CTRL_6X, buf, 4); + #endif +} + +// TODO: Once enabled, SX1262 needs a complete reset to disable TCXO +void sx126x::disableTCXO() { } + +void sx126x::setTxPower(int level, int outputPin) { + // Currently no low power mode for SX1262 implemented, assuming PA boost + + // WORKAROUND - Better Resistance of the SX1262 Tx to Antenna Mismatch, see DS_SX1261-2_V1.2 datasheet chapter 15.2 + // RegTxClampConfig = @address 0x08D8 + writeRegister(0x08D8, readRegister(0x08D8) | (0x0F << 1)); + + uint8_t pa_buf[4]; + pa_buf[0] = 0x04; // PADutyCycle needs to be 0x04 to achieve 22dBm output, but can be lowered for better efficiency at lower outputs + pa_buf[1] = 0x07; // HPMax at 0x07 is maximum supported for SX1262 + pa_buf[2] = 0x00; // DeviceSel 0x00 for SX1262 (0x01 for SX1261) + pa_buf[3] = 0x01; // PALut always 0x01 (reserved according to datasheet) + executeOpcode(OP_PA_CONFIG_6X, pa_buf, 4); // set pa_config for high power + + if (level > 22) { level = 22; } + else if (level < -9) { level = -9; } + writeRegister(REG_OCP_6X, OCP_TUNED); // Use board-specific tuned OCP + + uint8_t tx_buf[2]; + tx_buf[0] = level; + tx_buf[1] = 0x02; // PA ramping time - 40 microseconds + executeOpcode(OP_TX_PARAMS_6X, tx_buf, 2); + + _txp = level; +} + +uint8_t sx126x::getTxPower() { return _txp; } + +void sx126x::setFrequency(long frequency) { + _frequency = frequency; + uint8_t buf[4]; + uint32_t freq = (uint32_t)((double)frequency / (double)FREQ_STEP_6X); + buf[0] = ((freq >> 24) & 0xFF); + buf[1] = ((freq >> 16) & 0xFF); + buf[2] = ((freq >> 8) & 0xFF); + buf[3] = (freq & 0xFF); + executeOpcode(OP_RF_FREQ_6X, buf, 4); +} + +uint32_t sx126x::getFrequency() { + // We can't read the frequency on the sx1262 / 80 + uint32_t frequency = _frequency; + return frequency; +} + +void sx126x::setSpreadingFactor(int sf) { + if (sf < 5) { sf = 5; } + else if (sf > 12) { sf = 12; } + _sf = sf; + + handleLowDataRate(); + setModulationParams(sf, _bw, _cr, _ldro); +} + +long sx126x::getSignalBandwidth() { + int bw = _bw; + switch (bw) { + case 0x00: return 7.8E3; + case 0x01: return 15.6E3; + case 0x02: return 31.25E3; + case 0x03: return 62.5E3; + case 0x04: return 125E3; + case 0x05: return 250E3; + case 0x06: return 500E3; + case 0x08: return 10.4E3; + case 0x09: return 20.8E3; + case 0x0A: return 41.7E3; + } + return 0; +} + +extern bool lora_low_datarate; +void sx126x::handleLowDataRate() { + if ( long( (1<<_sf) / (getSignalBandwidth()/1000)) > 16) + { _ldro = 0x01; lora_low_datarate = true; } + else { _ldro = 0x00; lora_low_datarate = false; } +} + +// TODO: Check if there's anything the sx1262 can do here +void sx126x::optimizeModemSensitivity(){ } + +void sx126x::setSignalBandwidth(long sbw) { + if (sbw <= 7.8E3) { _bw = 0x00; } + else if (sbw <= 10.4E3) { _bw = 0x08; } + else if (sbw <= 15.6E3) { _bw = 0x01; } + else if (sbw <= 20.8E3) { _bw = 0x09; } + else if (sbw <= 31.25E3) { _bw = 0x02; } + else if (sbw <= 41.7E3) { _bw = 0x0A; } + else if (sbw <= 62.5E3) { _bw = 0x03; } + else if (sbw <= 125E3) { _bw = 0x04; } + else if (sbw <= 250E3) { _bw = 0x05; } + else { _bw = 0x06; } + + handleLowDataRate(); + setModulationParams(_sf, _bw, _cr, _ldro); + optimizeModemSensitivity(); +} + +void sx126x::setCodingRate4(int denominator) { + if (denominator < 5) { denominator = 5; } + else if (denominator > 8) { denominator = 8; } + int cr = denominator - 4; + _cr = cr; + setModulationParams(_sf, _bw, cr, _ldro); +} + +void sx126x::setPreambleLength(long preamble_symbols) { + _preambleLength = preamble_symbols; + setPacketParams(preamble_symbols, _implicitHeaderMode, _payloadLength, _crcMode); +} + +void sx126x::setSyncWord(uint16_t sw) { + // TODO: Why was this hardcoded instead of using the config value? + // writeRegister(REG_SYNC_WORD_MSB_6X, (sw & 0xFF00) >> 8); + // writeRegister(REG_SYNC_WORD_LSB_6X, sw & 0x00FF); + writeRegister(REG_SYNC_WORD_MSB_6X, 0x14); + writeRegister(REG_SYNC_WORD_LSB_6X, 0x24); +} + +void sx126x::setPins(int ss, int reset, int dio0, int busy, int rxen) { + _ss = ss; + _reset = reset; + _dio0 = dio0; + _busy = busy; + _rxen = rxen; +} + +void sx126x::dumpRegisters(Stream& out) { + for (int i = 0; i < 128; i++) { + out.print("0x"); + out.print(i, HEX); + out.print(": 0x"); + out.println(readRegister(i), HEX); + } +} + +void ISR_VECT sx126x::handleDio0Rise() { + // Just set flag — actual SPI work is deferred to pollDio0() + _dio0_risen = true; +} + +void sx126x::pollDio0() { + if (!_dio0_risen) return; + _dio0_risen = false; + + uint8_t buf[2]; + buf[0] = 0x00; + buf[1] = 0x00; + executeOpcodeRead(OP_GET_IRQ_STATUS_6X, buf, 2); + executeOpcode(OP_CLEAR_IRQ_STATUS_6X, buf, 2); + + if ((buf[1] & IRQ_PAYLOAD_CRC_ERROR_MASK_6X) == 0) { + _packetIndex = 0; + uint8_t rxbuf[2] = {0}; // Read packet length + executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, rxbuf, 2); + int packetLength = rxbuf[0]; + if (_onReceive) { _onReceive(packetLength); } + } +} + +void ISR_VECT sx126x::onDio0Rise() { sx126x_modem.handleDio0Rise(); } +void sx126x::setSPIFrequency(uint32_t frequency) { _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); } +void sx126x::enableCrc() { _crcMode = 1; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } +void sx126x::disableCrc() { _crcMode = 0; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } +void sx126x::explicitHeaderMode() { _implicitHeaderMode = 0; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } +void sx126x::implicitHeaderMode() { _implicitHeaderMode = 1; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } +byte sx126x::random() { return readRegister(REG_RANDOM_GEN_6X); } + +sx126x sx126x_modem; + +#endif \ No newline at end of file diff --git a/sx126x.h b/sx126x.h new file mode 100755 index 0000000..7d35227 --- /dev/null +++ b/sx126x.h @@ -0,0 +1,150 @@ +// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva. +// Licensed under the MIT license. + +#ifndef SX126X_H +#define SX126X_H + +#include +#include +#include "Modem.h" + +#define LORA_DEFAULT_SS_PIN 10 +#define LORA_DEFAULT_RESET_PIN 9 +#define LORA_DEFAULT_DIO0_PIN 2 +#define LORA_DEFAULT_RXEN_PIN -1 +#define LORA_DEFAULT_TXEN_PIN -1 +#define LORA_DEFAULT_BUSY_PIN -1 +#define LORA_MODEM_TIMEOUT_MS 20E3 + +#define PA_OUTPUT_RFO_PIN 0 +#define PA_OUTPUT_PA_BOOST_PIN 1 + +#define RSSI_OFFSET 157 + +class sx126x : public Stream { +public: + sx126x(); + + int begin(long frequency); + void end(); + + int beginPacket(int implicitHeader = false); + int endPacket(); + + int parsePacket(int size = 0); + int packetRssi(); + int packetRssi(uint8_t pkt_snr_raw); + int currentRssi(); + uint8_t packetRssiRaw(); + uint8_t currentRssiRaw(); + uint8_t packetSnrRaw(); + float packetSnr(); + long packetFrequencyError(); + + // from Print + virtual size_t write(uint8_t byte); + virtual size_t write(const uint8_t *buffer, size_t size); + + // from Stream + virtual int available(); + virtual int read(); + virtual int peek(); + virtual void flush(); + + void onReceive(void(*callback)(int)); + + void receive(int size = 0); + void standby(); + void sleep(); + void reset(void); + + bool preInit(); + uint8_t getTxPower(); + void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN); + uint32_t getFrequency(); + void setFrequency(long frequency); + void setSpreadingFactor(int sf); + long getSignalBandwidth(); + void setSignalBandwidth(long sbw); + void setCodingRate4(int denominator); + void setPreambleLength(long preamble_symbols); + void setSyncWord(uint16_t sw); + bool dcd(); + void enableCrc(); + void disableCrc(); + void enableTCXO(); + void disableTCXO(); + + void rxAntEnable(); + void loraMode(); + void waitOnBusy(); + void executeOpcode(uint8_t opcode, uint8_t *buffer, uint8_t size); + void executeOpcodeRead(uint8_t opcode, uint8_t *buffer, uint8_t size); + void writeBuffer(const uint8_t* buffer, size_t size); + void readBuffer(uint8_t* buffer, size_t size); + void setPacketParams(long preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc); + + void setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, int ldro); + + // deprecated + void crc() { enableCrc(); } + void noCrc() { disableCrc(); } + + byte random(); + + void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN, int rxen = LORA_DEFAULT_RXEN_PIN); + void setSPIFrequency(uint32_t frequency); + + void dumpRegisters(Stream& out); + +private: + void explicitHeaderMode(); + void implicitHeaderMode(); + + void handleDio0Rise(); + +public: + // Poll for deferred DIO0 interrupt (call from main loop) + void pollDio0(); + +private: uint8_t readRegister(uint16_t address); + void writeRegister(uint16_t address, uint8_t value); + uint8_t singleTransfer(uint8_t opcode, uint16_t address, uint8_t value); + + static void onDio0Rise(); + + void handleLowDataRate(); + void optimizeModemSensitivity(); + + void calibrate(void); + void calibrate_image(long frequency); + +private: + SPISettings _spiSettings; + int _ss; + int _reset; + int _dio0; + int _rxen; + int _busy; + long _frequency; + int _txp; + uint8_t _sf; + uint8_t _bw; + uint8_t _cr; + uint8_t _ldro; + int _packetIndex; + int _preambleLength; + int _implicitHeaderMode; + int _payloadLength; + int _crcMode; + int _fifo_tx_addr_ptr; + int _fifo_rx_addr_ptr; + uint8_t _packet[255]; + bool _preinit_done; + volatile bool _dio0_risen; + void (*_onReceive)(int); +}; + +extern sx126x sx126x_modem; + +#endif diff --git a/sx127x.cpp b/sx127x.cpp new file mode 100755 index 0000000..ef9038d --- /dev/null +++ b/sx127x.cpp @@ -0,0 +1,503 @@ +// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva. +// Licensed under the MIT license. + +#include "Boards.h" + +#if MODEM == SX1276 +#include "sx127x.h" + +#if MCU_VARIANT == MCU_ESP32 + #if MCU_VARIANT == MCU_ESP32 and !defined(CONFIG_IDF_TARGET_ESP32S3) + #include "hal/wdt_hal.h" + #endif + #define ISR_VECT IRAM_ATTR +#else + #define ISR_VECT +#endif + +// Registers +#define REG_FIFO_7X 0x00 +#define REG_OP_MODE_7X 0x01 +#define REG_FRF_MSB_7X 0x06 +#define REG_FRF_MID_7X 0x07 +#define REG_FRF_LSB_7X 0x08 +#define REG_PA_CONFIG_7X 0x09 +#define REG_OCP_7X 0x0b +#define REG_LNA_7X 0x0c +#define REG_FIFO_ADDR_PTR_7X 0x0d +#define REG_FIFO_TX_BASE_ADDR_7X 0x0e +#define REG_FIFO_RX_BASE_ADDR_7X 0x0f +#define REG_FIFO_RX_CURRENT_ADDR_7X 0x10 +#define REG_IRQ_FLAGS_7X 0x12 +#define REG_RX_NB_BYTES_7X 0x13 +#define REG_MODEM_STAT_7X 0x18 +#define REG_PKT_SNR_VALUE_7X 0x19 +#define REG_PKT_RSSI_VALUE_7X 0x1a +#define REG_RSSI_VALUE_7X 0x1b +#define REG_MODEM_CONFIG_1_7X 0x1d +#define REG_MODEM_CONFIG_2_7X 0x1e +#define REG_PREAMBLE_MSB_7X 0x20 +#define REG_PREAMBLE_LSB_7X 0x21 +#define REG_PAYLOAD_LENGTH_7X 0x22 +#define REG_MODEM_CONFIG_3_7X 0x26 +#define REG_FREQ_ERROR_MSB_7X 0x28 +#define REG_FREQ_ERROR_MID_7X 0x29 +#define REG_FREQ_ERROR_LSB_7X 0x2a +#define REG_RSSI_WIDEBAND_7X 0x2c +#define REG_DETECTION_OPTIMIZE_7X 0x31 +#define REG_HIGH_BW_OPTIMIZE_1_7X 0x36 +#define REG_DETECTION_THRESHOLD_7X 0x37 +#define REG_SYNC_WORD_7X 0x39 +#define REG_HIGH_BW_OPTIMIZE_2_7X 0x3a +#define REG_DIO_MAPPING_1_7X 0x40 +#define REG_VERSION_7X 0x42 +#define REG_TCXO_7X 0x4b +#define REG_PA_DAC_7X 0x4d + +// Modes +#define MODE_LONG_RANGE_MODE_7X 0x80 +#define MODE_SLEEP_7X 0x00 +#define MODE_STDBY_7X 0x01 +#define MODE_TX_7X 0x03 +#define MODE_RX_CONTINUOUS_7X 0x05 +#define MODE_RX_SINGLE_7X 0x06 + +// PA config +#define PA_BOOST_7X 0x80 + +// IRQ masks +#define IRQ_TX_DONE_MASK_7X 0x08 +#define IRQ_RX_DONE_MASK_7X 0x40 +#define IRQ_PAYLOAD_CRC_ERROR_MASK_7X 0x20 + +#define SYNC_WORD_7X 0x12 +#define MAX_PKT_LENGTH 255 + +extern SPIClass SPI; + +sx127x::sx127x() : + _spiSettings(8E6, MSBFIRST, SPI_MODE0), + _ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN), + _frequency(0), _packetIndex(0), _preinit_done(false), _onReceive(NULL) { setTimeout(0); } + +void sx127x::setSPIFrequency(uint32_t frequency) { _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); } +void sx127x::setPins(int ss, int reset, int dio0, int busy) { _ss = ss; _reset = reset; _dio0 = dio0; _busy = busy; } +uint8_t ISR_VECT sx127x::readRegister(uint8_t address) { return singleTransfer(address & 0x7f, 0x00); } +void sx127x::writeRegister(uint8_t address, uint8_t value) { singleTransfer(address | 0x80, value); } +void sx127x::standby() { writeRegister(REG_OP_MODE_7X, MODE_LONG_RANGE_MODE_7X | MODE_STDBY_7X); } +void sx127x::sleep() { writeRegister(REG_OP_MODE_7X, MODE_LONG_RANGE_MODE_7X | MODE_SLEEP_7X); } +void sx127x::setSyncWord(uint8_t sw) { writeRegister(REG_SYNC_WORD_7X, sw); } +void sx127x::enableCrc() { writeRegister(REG_MODEM_CONFIG_2_7X, readRegister(REG_MODEM_CONFIG_2_7X) | 0x04); } +void sx127x::disableCrc() { writeRegister(REG_MODEM_CONFIG_2_7X, readRegister(REG_MODEM_CONFIG_2_7X) & 0xfb); } +void sx127x::enableTCXO() { uint8_t tcxo_reg = readRegister(REG_TCXO_7X); writeRegister(REG_TCXO_7X, tcxo_reg | 0x10); } +void sx127x::disableTCXO() { uint8_t tcxo_reg = readRegister(REG_TCXO_7X); writeRegister(REG_TCXO_7X, tcxo_reg & 0xEF); } +void sx127x::explicitHeaderMode() { _implicitHeaderMode = 0; writeRegister(REG_MODEM_CONFIG_1_7X, readRegister(REG_MODEM_CONFIG_1_7X) & 0xfe); } +void sx127x::implicitHeaderMode() { _implicitHeaderMode = 1; writeRegister(REG_MODEM_CONFIG_1_7X, readRegister(REG_MODEM_CONFIG_1_7X) | 0x01); } +byte sx127x::random() { return readRegister(REG_RSSI_WIDEBAND_7X); } +void sx127x::flush() { } + +bool sx127x::preInit() { + pinMode(_ss, OUTPUT); + digitalWrite(_ss, HIGH); + + #if BOARD_MODEL == BOARD_T3S3 + SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs); + #else + SPI.begin(); + #endif + + // Check modem version + uint8_t version; + long start = millis(); + while (((millis() - start) < 500) && (millis() >= start)) { + version = readRegister(REG_VERSION_7X); + if (version == 0x12) { break; } + delay(100); + } + + if (version != 0x12) { return false; } + _preinit_done = true; + return true; +} + +uint8_t ISR_VECT sx127x::singleTransfer(uint8_t address, uint8_t value) { + uint8_t response; + + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(address); + response = SPI.transfer(value); + SPI.endTransaction(); + digitalWrite(_ss, HIGH); + + return response; +} + +int sx127x::begin(long frequency) { + if (_reset != -1) { + pinMode(_reset, OUTPUT); + digitalWrite(_reset, LOW); + delay(10); + digitalWrite(_reset, HIGH); + delay(10); + } + + if (_busy != -1) { pinMode(_busy, INPUT); } + if (!_preinit_done) { if (!preInit()) { return false; } } + + sleep(); + setFrequency(frequency); + + // Set base addresses + writeRegister(REG_FIFO_TX_BASE_ADDR_7X, 0); + writeRegister(REG_FIFO_RX_BASE_ADDR_7X, 0); + + // Set LNA boost and auto AGC + writeRegister(REG_LNA_7X, readRegister(REG_LNA_7X) | 0x03); + writeRegister(REG_MODEM_CONFIG_3_7X, 0x04); + + setSyncWord(SYNC_WORD_7X); + enableCrc(); + setTxPower(2); + + standby(); + + return 1; +} + +void sx127x::end() { sleep(); SPI.end(); _preinit_done = false; } + +int sx127x::beginPacket(int implicitHeader) { + standby(); + + if (implicitHeader) { implicitHeaderMode(); } + else { explicitHeaderMode(); } + + // Reset FIFO address and payload length + writeRegister(REG_FIFO_ADDR_PTR_7X, 0); + writeRegister(REG_PAYLOAD_LENGTH_7X, 0); + + return 1; +} + +int sx127x::endPacket() { + // Enter TX mode + writeRegister(REG_OP_MODE_7X, MODE_LONG_RANGE_MODE_7X | MODE_TX_7X); + + // Wait for TX completion + while ((readRegister(REG_IRQ_FLAGS_7X) & IRQ_TX_DONE_MASK_7X) == 0) { + yield(); + } + + // Clear TX complete IRQ + writeRegister(REG_IRQ_FLAGS_7X, IRQ_TX_DONE_MASK_7X); + return 1; +} + +bool sx127x::dcd() { + bool carrier_detected = false; + uint8_t status = readRegister(REG_MODEM_STAT_7X); + if ((status & SIG_DETECT) == SIG_DETECT) { carrier_detected = true; } + if ((status & SIG_SYNCED) == SIG_SYNCED) { carrier_detected = true; } + return carrier_detected; +} + +uint8_t sx127x::currentRssiRaw() { + uint8_t rssi = readRegister(REG_RSSI_VALUE_7X); + return rssi; +} + +int ISR_VECT sx127x::currentRssi() { + int rssi = (int)readRegister(REG_RSSI_VALUE_7X) - RSSI_OFFSET; + if (_frequency < 820E6) rssi -= 7; + return rssi; +} + +uint8_t sx127x::packetRssiRaw() { + uint8_t pkt_rssi_value = readRegister(REG_PKT_RSSI_VALUE_7X); + return pkt_rssi_value; +} + +int ISR_VECT sx127x::packetRssi(uint8_t pkt_snr_raw) { + int pkt_rssi = (int)readRegister(REG_PKT_RSSI_VALUE_7X) - RSSI_OFFSET; + int pkt_snr = ((int8_t)pkt_snr_raw)*0.25; + + if (_frequency < 820E6) pkt_rssi -= 7; + + if (pkt_snr < 0) { + pkt_rssi += pkt_snr; + } else { + // Slope correction is (16/15)*pkt_rssi, + // this estimation looses one floating point + // operation, and should be precise enough. + pkt_rssi = (int)(1.066 * pkt_rssi); + } + return pkt_rssi; +} + +int ISR_VECT sx127x::packetRssi() { + int pkt_rssi = (int)readRegister(REG_PKT_RSSI_VALUE_7X) - RSSI_OFFSET; + int pkt_snr = packetSnr(); + + if (_frequency < 820E6) pkt_rssi -= 7; + + if (pkt_snr < 0) { pkt_rssi += pkt_snr; } + else { + // Slope correction is (16/15)*pkt_rssi, + // this estimation looses one floating point + // operation, and should be precise enough. + pkt_rssi = (int)(1.066 * pkt_rssi); + } + return pkt_rssi; +} + +uint8_t ISR_VECT sx127x::packetSnrRaw() { return readRegister(REG_PKT_SNR_VALUE_7X); } + +float ISR_VECT sx127x::packetSnr() { return ((int8_t)readRegister(REG_PKT_SNR_VALUE_7X)) * 0.25; } + +long sx127x::packetFrequencyError() { + int32_t freqError = 0; + freqError = static_cast(readRegister(REG_FREQ_ERROR_MSB_7X) & B111); + freqError <<= 8L; + freqError += static_cast(readRegister(REG_FREQ_ERROR_MID_7X)); + freqError <<= 8L; + freqError += static_cast(readRegister(REG_FREQ_ERROR_LSB_7X)); + + if (readRegister(REG_FREQ_ERROR_MSB_7X) & B1000) { // Sign bit is on + freqError -= 524288; // B1000'0000'0000'0000'0000 + } + + const float fXtal = 32E6; // FXOSC: crystal oscillator (XTAL) frequency (2.5. Chip Specification, p. 14) + const float fError = ((static_cast(freqError) * (1L << 24)) / fXtal) * (getSignalBandwidth() / 500000.0f); + + return static_cast(fError); +} + +size_t sx127x::write(uint8_t byte) { return write(&byte, sizeof(byte)); } + +size_t sx127x::write(const uint8_t *buffer, size_t size) { + int currentLength = readRegister(REG_PAYLOAD_LENGTH_7X); + if ((currentLength + size) > MAX_PKT_LENGTH) { size = MAX_PKT_LENGTH - currentLength; } + + for (size_t i = 0; i < size; i++) { writeRegister(REG_FIFO_7X, buffer[i]); } + writeRegister(REG_PAYLOAD_LENGTH_7X, currentLength + size); + + return size; +} + +int ISR_VECT sx127x::available() { return (readRegister(REG_RX_NB_BYTES_7X) - _packetIndex); } + +int ISR_VECT sx127x::read() { + if (!available()) { return -1; } + _packetIndex++; + return readRegister(REG_FIFO_7X); +} + +int sx127x::peek() { + if (!available()) { return -1; } + + // Remember current FIFO address, read, and then reset address + int currentAddress = readRegister(REG_FIFO_ADDR_PTR_7X); + uint8_t b = readRegister(REG_FIFO_7X); + writeRegister(REG_FIFO_ADDR_PTR_7X, currentAddress); + + return b; +} + +void sx127x::onReceive(void(*callback)(int)) { + _onReceive = callback; + + if (callback) { + pinMode(_dio0, INPUT); + writeRegister(REG_DIO_MAPPING_1_7X, 0x00); + + #ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.usingInterrupt(digitalPinToInterrupt(_dio0)); + #endif + + attachInterrupt(digitalPinToInterrupt(_dio0), sx127x::onDio0Rise, RISING); + + } else { + detachInterrupt(digitalPinToInterrupt(_dio0)); + + #ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0)); + #endif + } +} + +void sx127x::receive(int size) { + if (size > 0) { + implicitHeaderMode(); + writeRegister(REG_PAYLOAD_LENGTH_7X, size & 0xff); + } else { explicitHeaderMode(); } + + writeRegister(REG_OP_MODE_7X, MODE_LONG_RANGE_MODE_7X | MODE_RX_CONTINUOUS_7X); +} + +void sx127x::setTxPower(int level, int outputPin) { + // Setup according to RFO or PA_BOOST output pin + if (PA_OUTPUT_RFO_PIN == outputPin) { + if (level < 0) { level = 0; } + else if (level > 14) { level = 14; } + + writeRegister(REG_PA_DAC_7X, 0x84); + writeRegister(REG_PA_CONFIG_7X, 0x70 | level); + + } else { + if (level < 2) { level = 2; } + else if (level > 17) { level = 17; } + + writeRegister(REG_PA_DAC_7X, 0x84); + writeRegister(REG_PA_CONFIG_7X, PA_BOOST_7X | (level - 2)); + } +} + +uint8_t sx127x::getTxPower() { byte txp = readRegister(REG_PA_CONFIG_7X); return txp; } + +void sx127x::setFrequency(unsigned long frequency) { + _frequency = frequency; + uint32_t frf = ((uint64_t)frequency << 19) / 32000000; + + writeRegister(REG_FRF_MSB_7X, (uint8_t)(frf >> 16)); + writeRegister(REG_FRF_MID_7X, (uint8_t)(frf >> 8)); + writeRegister(REG_FRF_LSB_7X, (uint8_t)(frf >> 0)); + + optimizeModemSensitivity(); +} + +uint32_t sx127x::getFrequency() { + uint8_t msb = readRegister(REG_FRF_MSB_7X); + uint8_t mid = readRegister(REG_FRF_MID_7X); + uint8_t lsb = readRegister(REG_FRF_LSB_7X); + + uint32_t frf = ((uint32_t)msb << 16) | ((uint32_t)mid << 8) | (uint32_t)lsb; + uint64_t frm = (uint64_t)frf*32000000; + uint32_t frequency = (frm >> 19); + + return frequency; +} + +void sx127x::setSpreadingFactor(int sf) { + if (sf < 6) { sf = 6; } + else if (sf > 12) { sf = 12; } + + if (sf == 6) { + writeRegister(REG_DETECTION_OPTIMIZE_7X, 0xc5); + writeRegister(REG_DETECTION_THRESHOLD_7X, 0x0c); + } else { + writeRegister(REG_DETECTION_OPTIMIZE_7X, 0xc3); + writeRegister(REG_DETECTION_THRESHOLD_7X, 0x0a); + } + + writeRegister(REG_MODEM_CONFIG_2_7X, (readRegister(REG_MODEM_CONFIG_2_7X) & 0x0f) | ((sf << 4) & 0xf0)); + handleLowDataRate(); +} + +long sx127x::getSignalBandwidth() { + byte bw = (readRegister(REG_MODEM_CONFIG_1_7X) >> 4); + switch (bw) { + case 0: return 7.8E3; + case 1: return 10.4E3; + case 2: return 15.6E3; + case 3: return 20.8E3; + case 4: return 31.25E3; + case 5: return 41.7E3; + case 6: return 62.5E3; + case 7: return 125E3; + case 8: return 250E3; + case 9: return 500E3; } + + return 0; +} + +void sx127x::setSignalBandwidth(long sbw) { + int bw; + if (sbw <= 7.8E3) { + bw = 0; + } else if (sbw <= 10.4E3) { + bw = 1; + } else if (sbw <= 15.6E3) { + bw = 2; + } else if (sbw <= 20.8E3) { + bw = 3; + } else if (sbw <= 31.25E3) { + bw = 4; + } else if (sbw <= 41.7E3) { + bw = 5; + } else if (sbw <= 62.5E3) { + bw = 6; + } else if (sbw <= 125E3) { + bw = 7; + } else if (sbw <= 250E3) { + bw = 8; + } else /*if (sbw <= 250E3)*/ { + bw = 9; + } + + writeRegister(REG_MODEM_CONFIG_1_7X, (readRegister(REG_MODEM_CONFIG_1_7X) & 0x0f) | (bw << 4)); + handleLowDataRate(); + optimizeModemSensitivity(); +} + +void sx127x::setCodingRate4(int denominator) { + if (denominator < 5) { denominator = 5; } + else if (denominator > 8) { denominator = 8; } + int cr = denominator - 4; + writeRegister(REG_MODEM_CONFIG_1_7X, (readRegister(REG_MODEM_CONFIG_1_7X) & 0xf1) | (cr << 1)); +} + +void sx127x::setPreambleLength(long preamble_symbols) { + long length = preamble_symbols - 4; + writeRegister(REG_PREAMBLE_MSB_7X, (uint8_t)(length >> 8)); + writeRegister(REG_PREAMBLE_LSB_7X, (uint8_t)(length >> 0)); +} + +extern bool lora_low_datarate; +void sx127x::handleLowDataRate() { + int sf = (readRegister(REG_MODEM_CONFIG_2_7X) >> 4); + if ( long( (1< 16) { + // Set auto AGC and LowDataRateOptimize + writeRegister(REG_MODEM_CONFIG_3_7X, (1<<3)|(1<<2)); + lora_low_datarate = true; + } else { + // Only set auto AGC + writeRegister(REG_MODEM_CONFIG_3_7X, (1<<2)); + lora_low_datarate = false; + } +} + +void sx127x::optimizeModemSensitivity() { + byte bw = (readRegister(REG_MODEM_CONFIG_1_7X) >> 4); + uint32_t freq = getFrequency(); + + if (bw == 9 && (410E6 <= freq) && (freq <= 525E6)) { + writeRegister(REG_HIGH_BW_OPTIMIZE_1_7X, 0x02); + writeRegister(REG_HIGH_BW_OPTIMIZE_2_7X, 0x7f); + } else if (bw == 9 && (820E6 <= freq) && (freq <= 1020E6)) { + writeRegister(REG_HIGH_BW_OPTIMIZE_1_7X, 0x02); + writeRegister(REG_HIGH_BW_OPTIMIZE_2_7X, 0x64); + } else { + writeRegister(REG_HIGH_BW_OPTIMIZE_1_7X, 0x03); + } +} + +void ISR_VECT sx127x::handleDio0Rise() { + int irqFlags = readRegister(REG_IRQ_FLAGS_7X); + + // Clear IRQs + writeRegister(REG_IRQ_FLAGS_7X, irqFlags); + if ((irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK_7X) == 0) { + _packetIndex = 0; + int packetLength = _implicitHeaderMode ? readRegister(REG_PAYLOAD_LENGTH_7X) : readRegister(REG_RX_NB_BYTES_7X); + writeRegister(REG_FIFO_ADDR_PTR_7X, readRegister(REG_FIFO_RX_CURRENT_ADDR_7X)); + if (_onReceive) { _onReceive(packetLength); } + writeRegister(REG_FIFO_ADDR_PTR_7X, 0); + } +} + +void ISR_VECT sx127x::onDio0Rise() { sx127x_modem.handleDio0Rise(); } + +sx127x sx127x_modem; + +#endif \ No newline at end of file diff --git a/sx127x.h b/sx127x.h new file mode 100755 index 0000000..7639857 --- /dev/null +++ b/sx127x.h @@ -0,0 +1,114 @@ +// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva. +// Licensed under the MIT license. + +#ifndef SX1276_H +#define SX1276_H + +#include +#include +#include "Modem.h" + +#define LORA_DEFAULT_SS_PIN 10 +#define LORA_DEFAULT_RESET_PIN 9 +#define LORA_DEFAULT_DIO0_PIN 2 +#define LORA_DEFAULT_BUSY_PIN -1 + +#define PA_OUTPUT_RFO_PIN 0 +#define PA_OUTPUT_PA_BOOST_PIN 1 + +#define RSSI_OFFSET 157 + +// Modem status flags +#define SIG_DETECT 0x01 +#define SIG_SYNCED 0x02 +#define RX_ONGOING 0x04 + +class sx127x : public Stream { +public: + sx127x(); + + int begin(long frequency); + void end(); + + int beginPacket(int implicitHeader = false); + int endPacket(); + + int parsePacket(int size = 0); + int packetRssi(); + int packetRssi(uint8_t pkt_snr_raw); + int currentRssi(); + uint8_t packetRssiRaw(); + uint8_t currentRssiRaw(); + uint8_t packetSnrRaw(); + float packetSnr(); + long packetFrequencyError(); + + // from Print + virtual size_t write(uint8_t byte); + virtual size_t write(const uint8_t *buffer, size_t size); + + // from Stream + virtual int available(); + virtual int read(); + virtual int peek(); + virtual void flush(); + + void onReceive(void(*callback)(int)); + + void receive(int size = 0); + void standby(); + void sleep(); + + bool preInit(); + uint8_t getTxPower(); + void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN); + uint32_t getFrequency(); + void setFrequency(unsigned long frequency); + void setSpreadingFactor(int sf); + long getSignalBandwidth(); + void setSignalBandwidth(long sbw); + void setCodingRate4(int denominator); + void setPreambleLength(long preamble_symbols); + void setSyncWord(uint8_t sw); + bool dcd(); + void enableCrc(); + void disableCrc(); + void enableTCXO(); + void disableTCXO(); + + byte random(); + + void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN); + void setSPIFrequency(uint32_t frequency); + +private: + void explicitHeaderMode(); + void implicitHeaderMode(); + + void handleDio0Rise(); + + uint8_t readRegister(uint8_t address); + void writeRegister(uint8_t address, uint8_t value); + uint8_t singleTransfer(uint8_t address, uint8_t value); + + static void onDio0Rise(); + + void handleLowDataRate(); + void optimizeModemSensitivity(); + +private: + SPISettings _spiSettings; + int _ss; + int _reset; + int _dio0; + int _busy; + long _frequency; + int _packetIndex; + int _implicitHeaderMode; + bool _preinit_done; + void (*_onReceive)(int); +}; + +extern sx127x sx127x_modem; + +#endif diff --git a/sx128x.cpp b/sx128x.cpp new file mode 100755 index 0000000..59c464e --- /dev/null +++ b/sx128x.cpp @@ -0,0 +1,887 @@ +// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva. +// Licensed under the MIT license. + +#include "Boards.h" + +#if MODEM == SX1280 +#include "sx128x.h" + +#define MCU_1284P 0x91 +#define MCU_2560 0x92 +#define MCU_ESP32 0x81 +#define MCU_NRF52 0x71 +#if defined(__AVR_ATmega1284P__) + #define PLATFORM PLATFORM_AVR + #define MCU_VARIANT MCU_1284P +#elif defined(__AVR_ATmega2560__) + #define PLATFORM PLATFORM_AVR + #define MCU_VARIANT MCU_2560 +#elif defined(ESP32) + #define PLATFORM PLATFORM_ESP32 + #define MCU_VARIANT MCU_ESP32 +#elif defined(NRF52840_XXAA) + #define PLATFORM PLATFORM_NRF52 + #define MCU_VARIANT MCU_NRF52 +#endif + +#ifndef MCU_VARIANT + #error No MCU variant defined, cannot compile +#endif + +#if MCU_VARIANT == MCU_ESP32 + #if MCU_VARIANT == MCU_ESP32 and !defined(CONFIG_IDF_TARGET_ESP32S3) + #include "hal/wdt_hal.h" + #endif + #define ISR_VECT IRAM_ATTR +#else + #define ISR_VECT +#endif + +// SX128x registers +#define OP_RF_FREQ_8X 0x86 +#define OP_SLEEP_8X 0x84 +#define OP_STANDBY_8X 0x80 +#define OP_TX_8X 0x83 +#define OP_RX_8X 0x82 +#define OP_SET_IRQ_FLAGS_8X 0x8D +#define OP_CLEAR_IRQ_STATUS_8X 0x97 +#define OP_GET_IRQ_STATUS_8X 0x15 +#define OP_RX_BUFFER_STATUS_8X 0x17 +#define OP_PACKET_STATUS_8X 0x1D +#define OP_CURRENT_RSSI_8X 0x1F +#define OP_MODULATION_PARAMS_8X 0x8B +#define OP_PACKET_PARAMS_8X 0x8C +#define OP_STATUS_8X 0xC0 +#define OP_TX_PARAMS_8X 0x8E +#define OP_PACKET_TYPE_8X 0x8A +#define OP_BUFFER_BASE_ADDR_8X 0x8F +#define OP_READ_REGISTER_8X 0x19 +#define OP_WRITE_REGISTER_8X 0x18 +#define IRQ_TX_DONE_MASK_8X 0x01 +#define IRQ_RX_DONE_MASK_8X 0x02 +#define IRQ_HEADER_DET_MASK_8X 0x10 +#define IRQ_HEADER_ERROR_MASK_8X 0x20 +#define IRQ_PAYLOAD_CRC_ERROR_MASK_8X 0x40 + +#define MODE_LONG_RANGE_MODE_8X 0x01 + +#define OP_FIFO_WRITE_8X 0x1A +#define OP_FIFO_READ_8X 0x1B +#define IRQ_PREAMBLE_DET_MASK_8X 0x80 + +#define REG_PACKET_SIZE 0x901 +#define REG_FIRM_VER_MSB 0x154 +#define REG_FIRM_VER_LSB 0x153 + +#define XTAL_FREQ_8X (double)52000000 +#define FREQ_DIV_8X (double)pow(2.0, 18.0) +#define FREQ_STEP_8X (double)(XTAL_FREQ_8X / FREQ_DIV_8X) + +#if defined(NRF52840_XXAA) + extern SPIClass spiModem; + #define SPI spiModem +#endif + +extern SPIClass SPI; + +#define MAX_PKT_LENGTH 255 + +sx128x::sx128x() : + _spiSettings(8E6, MSBFIRST, SPI_MODE0), + _ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN), _rxen(pin_rxen), _busy(LORA_DEFAULT_BUSY_PIN), _txen(pin_txen), + _frequency(0), _txp(0), _sf(0x05), _bw(0x34), _cr(0x01), _packetIndex(0), _implicitHeaderMode(0), _payloadLength(255), _crcMode(0), _fifo_tx_addr_ptr(0), + _fifo_rx_addr_ptr(0), _rxPacketLength(0), _preinit_done(false), _tcxo(false) { setTimeout(0); } + +bool ISR_VECT sx128x::getPacketValidity() { + uint8_t buf[2]; + buf[0] = 0x00; + buf[1] = 0x00; + executeOpcodeRead(OP_GET_IRQ_STATUS_8X, buf, 2); + executeOpcode(OP_CLEAR_IRQ_STATUS_8X, buf, 2); + if ((buf[1] & IRQ_PAYLOAD_CRC_ERROR_MASK_8X) == 0) { return true; } + else { return false; } +} + +void ISR_VECT sx128x::onDio0Rise() { + BaseType_t int_status = taskENTER_CRITICAL_FROM_ISR(); + // On the SX1280, there is a bug which can cause the busy line + // to remain high if a high amount of packets are received when + // in continuous RX mode. This is documented as Errata 16.1 in + // the SX1280 datasheet v3.2 (page 149) + // Therefore, the modem is set into receive mode each time a packet is received. + if (sx128x_modem.getPacketValidity()) { sx128x_modem.receive(); sx128x_modem.handleDio0Rise(); } + else { sx128x_modem.receive(); } + + taskEXIT_CRITICAL_FROM_ISR(int_status); +} + +void sx128x::handleDio0Rise() { + _packetIndex = 0; + uint8_t rxbuf[2] = {0}; + executeOpcodeRead(OP_RX_BUFFER_STATUS_8X, rxbuf, 2); + + // If implicit header mode is enabled, use pre-set packet length as payload length instead. + // See SX1280 datasheet v3.2, page 92 + if (_implicitHeaderMode == 0x80) { _rxPacketLength = _payloadLength; } + else { _rxPacketLength = rxbuf[0]; } + + if (_receive_callback) { _receive_callback(_rxPacketLength); } +} + +bool sx128x::preInit() { + pinMode(_ss, OUTPUT); + digitalWrite(_ss, HIGH); + + // TODO: Check if this change causes issues on any platforms + #if MCU_VARIANT == MCU_ESP32 + #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK + SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs); + #else + SPI.begin(); + #endif + #else + SPI.begin(); + #endif + + // Detect modem (retry for up to 500ms) + long start = millis(); + uint8_t version_msb; + uint8_t version_lsb; + while (((millis() - start) < 500) && (millis() >= start)) { + version_msb = readRegister(REG_FIRM_VER_MSB); + version_lsb = readRegister(REG_FIRM_VER_LSB); + if ((version_msb == 0xB7 && version_lsb == 0xA9) || (version_msb == 0xB5 && version_lsb == 0xA9)) { break; } + delay(100); + } + + if ((version_msb != 0xB7 || version_lsb != 0xA9) && (version_msb != 0xB5 || version_lsb != 0xA9)) { return false; } + _preinit_done = true; + return true; +} + +uint8_t ISR_VECT sx128x::readRegister(uint16_t address) { return singleTransfer(OP_READ_REGISTER_8X, address, 0x00); } +void sx128x::writeRegister(uint16_t address, uint8_t value) { singleTransfer(OP_WRITE_REGISTER_8X, address, value); } + +uint8_t ISR_VECT sx128x::singleTransfer(uint8_t opcode, uint16_t address, uint8_t value) { + waitOnBusy(); + uint8_t response; + digitalWrite(_ss, LOW); + + SPI.beginTransaction(_spiSettings); + SPI.transfer(opcode); + SPI.transfer((address & 0xFF00) >> 8); + SPI.transfer(address & 0x00FF); + if (opcode == OP_READ_REGISTER_8X) { SPI.transfer(0x00); } + response = SPI.transfer(value); + SPI.endTransaction(); + digitalWrite(_ss, HIGH); + + return response; +} + +void sx128x::rxAntEnable() { + if (_txen != -1) { digitalWrite(_txen, LOW); } + if (_rxen != -1) { digitalWrite(_rxen, HIGH); } +} + +void sx128x::txAntEnable() { + if (_txen != -1) { digitalWrite(_txen, HIGH); } + if (_rxen != -1) { digitalWrite(_rxen, LOW); } +} + +void sx128x::loraMode() { + uint8_t mode = MODE_LONG_RANGE_MODE_8X; + executeOpcode(OP_PACKET_TYPE_8X, &mode, 1); +} + +void sx128x::waitOnBusy() { + unsigned long time = millis(); + while (digitalRead(_busy) == HIGH) { + if (millis() >= (time + 100)) { break; } + } +} + +void sx128x::executeOpcode(uint8_t opcode, uint8_t *buffer, uint8_t size) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(opcode); + for (int i = 0; i < size; i++) { SPI.transfer(buffer[i]); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +void sx128x::executeOpcodeRead(uint8_t opcode, uint8_t *buffer, uint8_t size) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(opcode); + SPI.transfer(0x00); + for (int i = 0; i < size; i++) { buffer[i] = SPI.transfer(0x00); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +void sx128x::writeBuffer(const uint8_t* buffer, size_t size) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(OP_FIFO_WRITE_8X); + SPI.transfer(_fifo_tx_addr_ptr); + for (int i = 0; i < size; i++) { SPI.transfer(buffer[i]); _fifo_tx_addr_ptr++; } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +void sx128x::readBuffer(uint8_t* buffer, size_t size) { + waitOnBusy(); + digitalWrite(_ss, LOW); + SPI.beginTransaction(_spiSettings); + SPI.transfer(OP_FIFO_READ_8X); + SPI.transfer(_fifo_rx_addr_ptr); + SPI.transfer(0x00); + for (int i = 0; i < size; i++) { buffer[i] = SPI.transfer(0x00); } + SPI.endTransaction(); + digitalWrite(_ss, HIGH); +} + +void sx128x::setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr) { + // because there is no access to these registers on the sx1280, we have + // to set all these parameters at once or not at all. + uint8_t buf[3]; + buf[0] = sf << 4; + buf[1] = bw; + buf[2] = cr; + executeOpcode(OP_MODULATION_PARAMS_8X, buf, 3); + + if (sf <= 6) { writeRegister(0x925, 0x1E); } + else if (sf <= 8) { writeRegister(0x925, 0x37); } + else if (sf >= 9) { writeRegister(0x925, 0x32); } + writeRegister(0x093C, 0x1); +} + +uint8_t preamble_e = 0; +uint8_t preamble_m = 0; +uint32_t last_me_result_target = 0; +extern long lora_preamble_symbols; +void sx128x::setPacketParams(uint32_t target_preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc) { + if (last_me_result_target != target_preamble_symbols) { + // Calculate exponent and mantissa values for modem + if (target_preamble_symbols >= 0xF000) target_preamble_symbols = 0xF000; + uint32_t calculated_preamble_symbols; + uint8_t e = 1; + uint8_t m = 1; + while (e <= 15) { + while (m <= 15) { + calculated_preamble_symbols = m * (pow(2,e)); + if (calculated_preamble_symbols >= target_preamble_symbols-4) break; + m++; + } + + if (calculated_preamble_symbols >= target_preamble_symbols-4) break; + m = 1; e++; + } + + last_me_result_target = target_preamble_symbols; + lora_preamble_symbols = calculated_preamble_symbols+4; + _preambleLength = lora_preamble_symbols; + + preamble_e = e; + preamble_m = m; + } + + uint8_t buf[7]; + buf[0] = (preamble_e << 4) | preamble_m; + buf[1] = headermode; + buf[2] = payload_length; + buf[3] = crc; + buf[4] = 0x40; // Standard IQ setting (no inversion) + buf[5] = 0x00; // Unused params + buf[6] = 0x00; + + executeOpcode(OP_PACKET_PARAMS_8X, buf, 7); +} + +void sx128x::reset() { + if (_reset != -1) { + pinMode(_reset, OUTPUT); + digitalWrite(_reset, LOW); + delay(10); + digitalWrite(_reset, HIGH); + delay(10); + } +} + +int sx128x::begin(unsigned long frequency) { + reset(); + + if (_rxen != -1) { pinMode(_rxen, OUTPUT); } + if (_txen != -1) { pinMode(_txen, OUTPUT); } + if (_busy != -1) { pinMode(_busy, INPUT); } + + if (!_preinit_done) { + if (!preInit()) { + return false; + } + } + + standby(); + loraMode(); + rxAntEnable(); + setFrequency(frequency); + + // TODO: Implement LNA boost + //writeRegister(REG_LNA, 0x96); + + setModulationParams(_sf, _bw, _cr); + setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); + setTxPower(_txp); + + // Set base addresses + uint8_t basebuf[2] = {0}; + executeOpcode(OP_BUFFER_BASE_ADDR_8X, basebuf, 2); + + _radio_online = true; + return 1; +} + +void sx128x::end() { + sleep(); + SPI.end(); + _bitrate = 0; + _radio_online = false; + _preinit_done = false; +} + +int sx128x::beginPacket(int implicitHeader) { + standby(); + + if (implicitHeader) { implicitHeaderMode(); } + else { explicitHeaderMode(); } + + _payloadLength = 0; + _fifo_tx_addr_ptr = 0; + setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); + + return 1; +} + +int sx128x::endPacket() { + setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); + txAntEnable(); + + // Put in single TX mode + uint8_t timeout[3] = {0}; + executeOpcode(OP_TX_8X, timeout, 3); + + uint8_t buf[2]; + buf[0] = 0x00; + buf[1] = 0x00; + executeOpcodeRead(OP_GET_IRQ_STATUS_8X, buf, 2); + + // Wait for TX done + bool timed_out = false; + uint32_t w_timeout = millis()+LORA_MODEM_TIMEOUT_MS; + while ((millis() < w_timeout) && ((buf[1] & IRQ_TX_DONE_MASK_8X) == 0)) { + buf[0] = 0x00; + buf[1] = 0x00; + executeOpcodeRead(OP_GET_IRQ_STATUS_8X, buf, 2); + yield(); + } + + if (!(millis() < w_timeout)) { timed_out = true; } + + // clear IRQ's + uint8_t mask[2]; + mask[0] = 0x00; + mask[1] = IRQ_TX_DONE_MASK_8X; + executeOpcode(OP_CLEAR_IRQ_STATUS_8X, mask, 2); + + if (timed_out) { return 0; } + else { return 1; } +} + +unsigned long preamble_detected_at = 0; +extern long lora_preamble_time_ms; +extern long lora_header_time_ms; +bool false_preamble_detected = false; +bool sx128x::dcd() { + uint8_t buf[2] = {0}; executeOpcodeRead(OP_GET_IRQ_STATUS_8X, buf, 2); + uint32_t now = millis(); + + bool header_detected = false; + bool carrier_detected = false; + + if ((buf[1] & IRQ_HEADER_DET_MASK_8X) != 0) { header_detected = true; carrier_detected = true; } + else { header_detected = false; } + + if ((buf[0] & IRQ_PREAMBLE_DET_MASK_8X) != 0) { + carrier_detected = true; + if (preamble_detected_at == 0) { preamble_detected_at = now; } + if (now - preamble_detected_at > lora_preamble_time_ms + lora_header_time_ms) { + preamble_detected_at = 0; + if (!header_detected) { false_preamble_detected = true; } + uint8_t clearbuf[2] = {0}; clearbuf[0] = IRQ_PREAMBLE_DET_MASK_8X; + executeOpcode(OP_CLEAR_IRQ_STATUS_8X, clearbuf, 2); + } + } + + // TODO: Maybe there's a way of unlatching the RSSI + // status without re-activating receive mode? + if (false_preamble_detected) { sx128x_modem.receive(); false_preamble_detected = false; } + return carrier_detected; +} + + +uint8_t sx128x::currentRssiRaw() { + uint8_t byte = 0; + executeOpcodeRead(OP_CURRENT_RSSI_8X, &byte, 1); + return byte; +} + +int ISR_VECT sx128x::currentRssi() { + uint8_t byte = 0; + executeOpcodeRead(OP_CURRENT_RSSI_8X, &byte, 1); + int rssi = -byte / 2; + return rssi; +} + +uint8_t sx128x::packetRssiRaw() { + uint8_t buf[5] = {0}; + executeOpcodeRead(OP_PACKET_STATUS_8X, buf, 5); + return buf[0]; +} + +int ISR_VECT sx128x::packetRssi(uint8_t pkt_snr_raw) { + // TODO: May need more calculations here + uint8_t buf[5] = {0}; + executeOpcodeRead(OP_PACKET_STATUS_8X, buf, 5); + int pkt_rssi = -buf[0] / 2; + return pkt_rssi; +} + +uint8_t ISR_VECT sx128x::packetSnrRaw() { + uint8_t buf[5] = {0}; + executeOpcodeRead(OP_PACKET_STATUS_8X, buf, 5); + return buf[1]; +} + +float ISR_VECT sx128x::packetSnr() { + uint8_t buf[5] = {0}; + executeOpcodeRead(OP_PACKET_STATUS_8X, buf, 5); + return float(buf[1]) * 0.25; +} + +long sx128x::packetFrequencyError() { + // TODO: Implement this, page 120 of sx1280 datasheet + int32_t freqError = 0; + const float fError = 0.0; + return static_cast(fError); +} + +void sx128x::flush() { } +int ISR_VECT sx128x::available() { return _rxPacketLength - _packetIndex; } +size_t sx128x::write(uint8_t byte) { return write(&byte, sizeof(byte)); } +size_t sx128x::write(const uint8_t *buffer, size_t size) { + if ((_payloadLength + size) > MAX_PKT_LENGTH) { size = MAX_PKT_LENGTH - _payloadLength; } + writeBuffer(buffer, size); + _payloadLength = _payloadLength + size; + return size; +} + +int ISR_VECT sx128x::read() { + if (!available()) { return -1; } + + // If received new packet + if (_packetIndex == 0) { + uint8_t rxbuf[2] = {0}; + executeOpcodeRead(OP_RX_BUFFER_STATUS_8X, rxbuf, 2); + int size; + + // If implicit header mode is enabled, read packet length as payload length instead. + // See SX1280 datasheet v3.2, page 92 + if (_implicitHeaderMode == 0x80) { + size = _payloadLength; + } else { + size = rxbuf[0]; + } + + _fifo_rx_addr_ptr = rxbuf[1]; + if (size > 255) { size = 255; } + + readBuffer(_packet, size); + } + + uint8_t byte = _packet[_packetIndex]; + _packetIndex++; + return byte; +} + +int sx128x::peek() { + if (!available()) { return -1; } + uint8_t b = _packet[_packetIndex]; + return b; +} + + +void sx128x::onReceive(void(*callback)(int)) { + _receive_callback = callback; + + if (callback) { + pinMode(_dio0, INPUT); + + // Set preamble and header detection irqs, plus dio0 mask + uint8_t buf[8]; + + // Set irq masks, enable all + buf[0] = 0xFF; + buf[1] = 0xFF; + + // On the SX1280, no RxDone IRQ is generated if a packet is received with + // an invalid header, but the modem will be taken out of single RX mode. + // This can cause the modem to not receive packets until it is reset + // again. This is documented as Errata 16.2 in the SX1280 datasheet v3.2 + // (page 150) Below, the header error IRQ is mapped to dio0 so that the + // modem can be set into RX mode again on reception of a corrupted + // header. + // set dio0 masks + buf[2] = 0x00; + buf[3] = IRQ_RX_DONE_MASK_8X | IRQ_HEADER_ERROR_MASK_8X; + + // Set dio1 masks + buf[4] = 0x00; + buf[5] = 0x00; + + // Set dio2 masks + buf[6] = 0x00; + buf[7] = 0x00; + + executeOpcode(OP_SET_IRQ_FLAGS_8X, buf, 8); + + #ifdef SPI_HAS_NOTUSINGINTERRUPT + SPI.usingInterrupt(digitalPinToInterrupt(_dio0)); + #endif + + attachInterrupt(digitalPinToInterrupt(_dio0), onDio0Rise, RISING); + + } else { + detachInterrupt(digitalPinToInterrupt(_dio0)); + #ifdef SPI_HAS_NOTUSINGINTERRUPT + _spiModem->notUsingInterrupt(digitalPinToInterrupt(_dio0)); + #endif + } +} + +void sx128x::receive(int size) { + if (size > 0) { + implicitHeaderMode(); + // Tell radio payload length + //_rxPacketLength = size; + //_payloadLength = size; + //setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); + } else { + explicitHeaderMode(); + } + + rxAntEnable(); + + // On the SX1280, there is a bug which can cause the busy line + // to remain high if a high amount of packets are received when + // in continuous RX mode. This is documented as Errata 16.1 in + // the SX1280 datasheet v3.2 (page 149) + // Therefore, the modem is set to single RX mode below instead. + + // uint8_t mode[3] = {0x03, 0xFF, 0xFF}; // Countinuous RX mode + uint8_t mode[3] = {0}; // single RX mode + executeOpcode(OP_RX_8X, mode, 3); +} + +void sx128x::standby() { + uint8_t byte = 0x01; // Always use STDBY_XOSC + executeOpcode(OP_STANDBY_8X, &byte, 1); +} + +void sx128x::setPins(int ss, int reset, int dio0, int busy, int rxen, int txen) { + _ss = ss; + _reset = reset; + _dio0 = dio0; + _busy = busy; + _rxen = rxen; + _txen = txen; +} + +void sx128x::setTxPower(int level, int outputPin) { + uint8_t tx_buf[2]; + + // RAK4631 with WisBlock SX1280 module (LIBSYS002) + #if BOARD_VARIANT == MODEL_13 || BOARD_VARIANT == MODEL_21 + if (level > 27) { level = 27; } + else if (level < 0) { level = 0; } + + _txp = level; + int reg_value; + switch (level) { + case 0: + reg_value = -18; + break; + case 1: + reg_value = -16; + break; + case 2: + reg_value = -15; + break; + case 3: + reg_value = -14; + break; + case 4: + reg_value = -13; + break; + case 5: + reg_value = -12; + break; + case 6: + reg_value = -11; + break; + case 7: + reg_value = -9; + break; + case 8: + reg_value = -8; + break; + case 9: + reg_value = -7; + break; + case 10: + reg_value = -6; + break; + case 11: + reg_value = -5; + break; + case 12: + reg_value = -4; + break; + case 13: + reg_value = -3; + break; + case 14: + reg_value = -2; + break; + case 15: + reg_value = -1; + break; + case 16: + reg_value = 0; + break; + case 17: + reg_value = 1; + break; + case 18: + reg_value = 2; + break; + case 19: + reg_value = 3; + break; + case 20: + reg_value = 4; + break; + case 21: + reg_value = 5; + break; + case 22: + reg_value = 6; + break; + case 23: + reg_value = 7; + break; + case 24: + reg_value = 8; + break; + case 25: + reg_value = 9; + break; + case 26: + reg_value = 12; + break; + case 27: + reg_value = 13; + break; + default: + reg_value = 0; + break; + } + + tx_buf[0] = reg_value + 18; + tx_buf[1] = 0xE0; // Ramping time, 20 microseconds + executeOpcode(OP_TX_PARAMS_8X, tx_buf, 2); + + // T3S3 SX1280 PA + #elif BOARD_VARIANT == MODEL_AC + if (level > 20) { level = 20; } + else if (level < 0) { level = 0; } + + _txp = level; + int reg_value; + switch (level) { + case 0: + reg_value = -18; + break; + case 1: + reg_value = -17; + break; + case 2: + reg_value = -16; + break; + case 3: + reg_value = -15; + break; + case 4: + reg_value = -14; + break; + case 5: + reg_value = -13; + break; + case 6: + reg_value = -12; + break; + case 7: + reg_value = -10; + break; + case 8: + reg_value = -9; + break; + case 9: + reg_value = -8; + break; + case 10: + reg_value = -7; + break; + case 11: + reg_value = -6; + break; + case 12: + reg_value = -5; + break; + case 13: + reg_value = -4; + break; + case 14: + reg_value = -3; + break; + case 15: + reg_value = -2; + break; + case 16: + reg_value = -1; + break; + case 17: + reg_value = 0; + break; + case 18: + reg_value = 1; + break; + case 19: + reg_value = 2; + break; + case 20: + reg_value = 3; + break; + default: + reg_value = 0; + break; + } + tx_buf[0] = reg_value; + tx_buf[1] = 0xE0; // Ramping time, 20 microseconds + + // For SX1280 boards with no specific PA requirements + #else + if (level > 13) { level = 13; } + else if (level < -18) { level = -18; } + _txp = level; + tx_buf[0] = level + 18; + tx_buf[1] = 0xE0; // Ramping time, 20 microseconds + #endif + + executeOpcode(OP_TX_PARAMS_8X, tx_buf, 2); +} + +void sx128x::setFrequency(uint32_t frequency) { + _frequency = frequency; + uint8_t buf[3]; + uint32_t freq = (uint32_t)((double)frequency / (double)FREQ_STEP_8X); + buf[0] = ((freq >> 16) & 0xFF); + buf[1] = ((freq >> 8) & 0xFF); + buf[2] = (freq & 0xFF); + + executeOpcode(OP_RF_FREQ_8X, buf, 3); +} + +uint32_t sx128x::getFrequency() { + // We can't read the frequency on the sx1280 + uint32_t frequency = _frequency; + return frequency; +} + +void sx128x::setSpreadingFactor(int sf) { + if (sf < 5) { sf = 5; } + else if (sf > 12) { sf = 12; } + _sf = sf; + + setModulationParams(sf, _bw, _cr); + handleLowDataRate(); +} + +uint32_t sx128x::getSignalBandwidth() { + int bw = _bw; + switch (bw) { + case 0x34: return 203.125E3; + case 0x26: return 406.25E3; + case 0x18: return 812.5E3; + case 0x0A: return 1625E3; + } + + return 0; +} + +void sx128x::setSignalBandwidth(uint32_t sbw) { + if (sbw <= 203.125E3) { _bw = 0x34; } + else if (sbw <= 406.25E3) { _bw = 0x26; } + else if (sbw <= 812.5E3) { _bw = 0x18; } + else { _bw = 0x0A; } + + setModulationParams(_sf, _bw, _cr); + handleLowDataRate(); + optimizeModemSensitivity(); +} + +// TODO: add support for new interleaving scheme, see page 117 of sx1280 datasheet +void sx128x::setCodingRate4(int denominator) { + if (denominator < 5) { denominator = 5; } + else if (denominator > 8) { denominator = 8; } + _cr = denominator - 4; + setModulationParams(_sf, _bw, _cr); +} + +extern bool lora_low_datarate; +void sx128x::handleLowDataRate() { + if (_sf > 10) { lora_low_datarate = true; } + else { lora_low_datarate = false; } +} + +void sx128x::optimizeModemSensitivity() { } // TODO: Check if there's anything the sx1280 can do here +uint8_t sx128x::getCodingRate4() { return _cr + 4; } +void sx128x::setPreambleLength(long preamble_symbols) { setPacketParams(preamble_symbols, _implicitHeaderMode, _payloadLength, _crcMode); } +void sx128x::setSyncWord(int sw) { } // TODO: Implement +void sx128x::enableTCXO() { } // TODO: Need to check how to implement on sx1280 +void sx128x::disableTCXO() { } // TODO: Need to check how to implement on sx1280 +void sx128x::sleep() { uint8_t byte = 0x00; executeOpcode(OP_SLEEP_8X, &byte, 1); } +uint8_t sx128x::getTxPower() { return _txp; } +uint8_t sx128x::getSpreadingFactor() { return _sf; } +void sx128x::enableCrc() { _crcMode = 0x20; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } +void sx128x::disableCrc() { _crcMode = 0; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } +void sx128x::setSPIFrequency(uint32_t frequency) { _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); } +void sx128x::explicitHeaderMode() { _implicitHeaderMode = 0; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } +void sx128x::implicitHeaderMode() { _implicitHeaderMode = 0x80; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); } +void sx128x::dumpRegisters(Stream& out) { for (int i = 0; i < 128; i++) { out.print("0x"); out.print(i, HEX); out.print(": 0x"); out.println(readRegister(i), HEX); } } + +sx128x sx128x_modem; +#endif \ No newline at end of file diff --git a/sx128x.h b/sx128x.h new file mode 100755 index 0000000..ccaeb04 --- /dev/null +++ b/sx128x.h @@ -0,0 +1,146 @@ +// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva. +// Licensed under the MIT license. + +#ifndef SX128X_H +#define SX128X_H + +#include +#include +#include "Modem.h" + +#define LORA_DEFAULT_SS_PIN 10 +#define LORA_DEFAULT_RESET_PIN 9 +#define LORA_DEFAULT_DIO0_PIN 2 +#define LORA_DEFAULT_RXEN_PIN -1 +#define LORA_DEFAULT_TXEN_PIN -1 +#define LORA_DEFAULT_BUSY_PIN -1 +#define LORA_MODEM_TIMEOUT_MS 15E3 +#define PA_OUTPUT_RFO_PIN 0 +#define PA_OUTPUT_PA_BOOST_PIN 1 +#define RSSI_OFFSET 157 + +class sx128x : public Stream { +public: + sx128x(); + + int begin(unsigned long frequency); + void end(); + void reset(); + + int beginPacket(int implicitHeader = false); + int endPacket(); + + int parsePacket(int size = 0); + int packetRssi(); + int packetRssi(uint8_t pkt_snr_raw); + int currentRssi(); + uint8_t packetRssiRaw(); + uint8_t currentRssiRaw(); + uint8_t packetSnrRaw(); + float packetSnr(); + long packetFrequencyError(); + + // from Print + virtual size_t write(uint8_t byte); + virtual size_t write(const uint8_t *buffer, size_t size); + + // from Stream + virtual int available(); + virtual int read(); + virtual int peek(); + virtual void flush(); + + void onReceive(void(*callback)(int)); + + void receive(int size = 0); + void standby(); + void sleep(); + + bool preInit(); + uint8_t getTxPower(); + void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN); + uint32_t getFrequency(); + void setFrequency(uint32_t frequency); + void setSpreadingFactor(int sf); + uint8_t getSpreadingFactor(); + uint32_t getSignalBandwidth(); + void setSignalBandwidth(uint32_t sbw); + void setCodingRate4(int denominator); + uint8_t getCodingRate4(); + void setPreambleLength(long preamble_symbols); + void setSyncWord(int sw); + bool dcd(); + void clearIRQStatus(); + void enableCrc(); + void disableCrc(); + void enableTCXO(); + void disableTCXO(); + + void txAntEnable(); + void rxAntEnable(); + void loraMode(); + void waitOnBusy(); + void executeOpcode(uint8_t opcode, uint8_t *buffer, uint8_t size); + void executeOpcodeRead(uint8_t opcode, uint8_t *buffer, uint8_t size); + void writeBuffer(const uint8_t* buffer, size_t size); + void readBuffer(uint8_t* buffer, size_t size); + void setPacketParams(uint32_t target_preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc); + void setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr); + + void crc() { enableCrc(); } + void noCrc() { disableCrc(); } + + void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN, int rxen = LORA_DEFAULT_RXEN_PIN, int txen = LORA_DEFAULT_TXEN_PIN); + void setSPIFrequency(uint32_t frequency); + + void dumpRegisters(Stream& out); + +private: + void explicitHeaderMode(); + void implicitHeaderMode(); + + bool getPacketValidity(); + void handleDio0Rise(); + + uint8_t readRegister(uint16_t address); + void writeRegister(uint16_t address, uint8_t value); + uint8_t singleTransfer(uint8_t opcode, uint16_t address, uint8_t value); + + static void onDio0Rise(); + + void handleLowDataRate(); + void optimizeModemSensitivity(); + +private: + SPISettings _spiSettings; + int _ss; + int _reset; + int _dio0; + int _rxen; + int _txen; + int _busy; + int _modem; + unsigned long _frequency; + int _txp; + uint8_t _sf; + uint8_t _bw; + uint8_t _cr; + int _packetIndex; + uint32_t _preambleLength; + int _implicitHeaderMode; + int _payloadLength; + int _crcMode; + int _fifo_tx_addr_ptr; + int _fifo_rx_addr_ptr; + uint8_t _packet[256]; + bool _preinit_done; + bool _tcxo; + bool _radio_online; + int _rxPacketLength; + uint32_t _bitrate; + void (*_receive_callback)(int); +}; + +extern sx128x sx128x_modem; + +#endif diff --git a/variants/rak11200/pins_arduino.h b/variants/rak11200/pins_arduino.h new file mode 100755 index 0000000..2692bd0 --- /dev/null +++ b/variants/rak11200/pins_arduino.h @@ -0,0 +1,64 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#ifndef _VARIANT_RAK11200_ +#define _VARIANT_RAK11200_ +#endif + +#include + +// #ifndef EXTERNAL_NUM_INTERRUPTS +// #define EXTERNAL_NUM_INTERRUPTS 16 +// #endif +// #ifndef NUM_DIGITAL_PINS +// #define NUM_DIGITAL_PINS 40 +// #endif +// #ifndef NUM_ANALOG_INPUTS +// #define NUM_ANALOG_INPUTS 16 +// #endif +// #ifndef analogInputToDigitalPin +// #define analogInputToDigitalPin(p) (((p) < 20) ? (esp32_adc2gpio[(p)]) : -1) +// #endif +// #ifndef digitalPinToInterrupt +// #define digitalPinToInterrupt(p) (((p) < 40) ? (p) : -1) +// #endif +// #ifndef digitalPinHasPWM +// #define digitalPinHasPWM(p) (p < 34) +// #endif + +#define LED_GREEN 12 +#define LED_BLUE 2 + +#ifdef LED_BUILTIN +#undef LED_BUILTIN +#endif +#define LED_BUILTIN LED_GREEN + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +#define TX1 21 +#define RX1 19 + +#define WB_IO1 14 +#define WB_IO2 27 +#define WB_IO3 26 +#define WB_IO4 23 +#define WB_IO5 13 +#define WB_IO6 22 +#define WB_SW1 34 +#define WB_A0 36 +#define WB_A1 39 +#define WB_CS 32 +#define WB_LED1 12 +#define WB_LED2 2 + +static const uint8_t SDA = 4; +static const uint8_t SCL = 5; + +static const uint8_t SS = 32; +static const uint8_t MOSI = 25; +static const uint8_t MISO = 35; +static const uint8_t SCK = 33; + +#endif /* Pins_Arduino_h */ diff --git a/variants/rak11200/variant.h b/variants/rak11200/variant.h new file mode 100755 index 0000000..9ce419b --- /dev/null +++ b/variants/rak11200/variant.h @@ -0,0 +1 @@ +// Just for consistency, for RAK11200 ESP32 Wrover all definitions are in the pins_arduino.h \ No newline at end of file diff --git a/variants/rak11300/rak_variant.cpp b/variants/rak11300/rak_variant.cpp new file mode 100755 index 0000000..9dacc9c --- /dev/null +++ b/variants/rak11300/rak_variant.cpp @@ -0,0 +1 @@ +#include "rak_variant.h" \ No newline at end of file diff --git a/variants/rak11300/rak_variant.h b/variants/rak11300/rak_variant.h new file mode 100755 index 0000000..7938d65 --- /dev/null +++ b/variants/rak11300/rak_variant.h @@ -0,0 +1,6 @@ +#pragma once +#include +#ifndef _VARIANT_RAK11300_ +#define _VARIANT_RAK11300_ + +#endif // #define _VARIANT_RAK11300_ diff --git a/variants/rak11300/variant.h b/variants/rak11300/variant.h new file mode 100755 index 0000000..1a26835 --- /dev/null +++ b/variants/rak11300/variant.h @@ -0,0 +1,2 @@ +// Just for consistency, for RAK11310 all definitions are in the pins_arduino.h +#include "rak_variant.h" \ No newline at end of file diff --git a/variants/rak3112/pins_arduino.h b/variants/rak3112/pins_arduino.h new file mode 100755 index 0000000..45698fd --- /dev/null +++ b/variants/rak3112/pins_arduino.h @@ -0,0 +1,97 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#ifndef _VARIANT_RAK3112_ +#define _VARIANT_RAK3112_ +#endif + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// GPIO's +#define WB_IO1 21 +#define WB_IO2 2 +// #define WB_IO2 14 +#define WB_IO3 41 +#define WB_IO4 42 +#define WB_IO5 38 +#define WB_IO6 39 +// #define WB_SW1 35 NC +#define WB_A0 1 +#define WB_A1 2 +#define WB_CS 12 +#define WB_LED1 46 +#define WB_LED2 45 + +// LEDs +#define PIN_LED1 (46) +#define PIN_LED2 (45) +#define LED_BLUE PIN_LED2 +#define LED_GREEN PIN_LED1 +#define LED_BUILTIN LED_GREEN +#define LED_CONN PIN_LED2 +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Analog pins + */ +#define PIN_A0 (21) +#define PIN_A1 (14) + +/* + * Serial interfaces + */ +// TXD1 RXD1 on Base Board +#define PIN_SERIAL1_RX (44) +#define PIN_SERIAL1_TX (43) + +// TXD0 RXD0 on Base Board (NC due to large flash chip) +// #define PIN_SERIAL2_RX (19) +// #define PIN_SERIAL2_TX (20) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (10) +#define PIN_SPI_MOSI (11) +#define PIN_SPI_SCK (13) +#define SPI_CS (12) + +static const uint8_t SS = SPI_CS; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +// Internal SPI to LoRa transceiver +#define LORA_SX126X_SCK 5 +#define LORA_SX126X_MISO 3 +#define LORA_SX126X_MOSI 6 +#define LORA_SX126X_CS 7 +#define LORA_SX126X_RESET 8 +#define LORA_SX126X_DIO1 47 +#define LORA_SX126X_BUSY 48 +#define LORA_SX126X_DIO2_AS_RF_SWITCH 1 +#define LORA_SX126X_DIO3_TCXO_VOLTAGE 1.8 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 2 + +#ifndef PIN_WIRE_SDA +#define PIN_WIRE_SDA (9) +#endif +#ifndef PIN_WIRE_SCL +#define PIN_WIRE_SCL (40) +#endif +#define SDA PIN_WIRE_SDA +#define SCL PIN_WIRE_SCL + +#define PIN_WIRE1_SDA (17) +#define PIN_WIRE1_SCL (18) + +#endif /* Pins_Arduino_h */ diff --git a/variants/rak3112/variant.h b/variants/rak3112/variant.h new file mode 100755 index 0000000..d85019c --- /dev/null +++ b/variants/rak3112/variant.h @@ -0,0 +1,5 @@ +// Just for consistency, for RAK3112 ESP32-S3 all definitions are in the pins_arduino.h + +#ifndef _VARIANT_RAK3112_ +#define _VARIANT_RAK3113_ +#endif \ No newline at end of file diff --git a/variants/rak4630/WVariant.h b/variants/rak4630/WVariant.h new file mode 100755 index 0000000..500fc6b --- /dev/null +++ b/variants/rak4630/WVariant.h @@ -0,0 +1,36 @@ +/* + Copyright (c) 2015 Arduino LLC. All right reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern const uint32_t g_ADigitalPinMap[] ; + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/variants/rak4630/variant.cpp b/variants/rak4630/variant.cpp new file mode 100755 index 0000000..196812b --- /dev/null +++ b/variants/rak4630/variant.cpp @@ -0,0 +1,54 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +#ifdef __cplusplus +extern "C" +{ +#endif // __cplusplus + + const uint32_t g_ADigitalPinMap[] = + { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47}; + + void initVariant() + { + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + } +#ifdef __cplusplus +} +#endif diff --git a/variants/rak4630/variant.h b/variants/rak4630/variant.h new file mode 100755 index 0000000..28738c8 --- /dev/null +++ b/variants/rak4630/variant.h @@ -0,0 +1,165 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#ifndef _VARIANT_RAK4630_ +#define _VARIANT_RAK4630_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ +#include + +#ifdef __cplusplus +extern "C" +{ +#endif // __cplusplus + + /* + * WisBlock Base GPIO definitions + */ + static const uint8_t WB_IO1 = 17; // SLOT_A SLOT_B + static const uint8_t WB_IO2 = 34; // SLOT_A SLOT_B + static const uint8_t WB_IO3 = 21; // SLOT_C + static const uint8_t WB_IO4 = 4; // SLOT_C + static const uint8_t WB_IO5 = 9; // SLOT_D + static const uint8_t WB_IO6 = 10; // SLOT_D + static const uint8_t WB_SW1 = 33; // IO_SLOT + static const uint8_t WB_A0 = 5; // IO_SLOT + static const uint8_t WB_A1 = 31; // IO_SLOT + static const uint8_t WB_I2C1_SDA = 13; // SENSOR_SLOT IO_SLOT + static const uint8_t WB_I2C1_SCL = 14; // SENSOR_SLOT IO_SLOT + static const uint8_t WB_I2C2_SDA = 24; // IO_SLOT + static const uint8_t WB_I2C2_SCL = 25; // IO_SLOT + static const uint8_t WB_SPI_CS = 26; // IO_SLOT + static const uint8_t WB_SPI_CLK = 3; // IO_SLOT + static const uint8_t WB_SPI_MISO = 29; // IO_SLOT + static const uint8_t WB_SPI_MOSI = 30; // IO_SLOT + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Analog pins + */ +#define PIN_A0 (5) //(3) +#define PIN_A1 (31) //(4) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + + static const uint8_t A0 = PIN_A0; + static const uint8_t A1 = PIN_A1; + static const uint8_t A2 = PIN_A2; + static const uint8_t A3 = PIN_A3; + static const uint8_t A4 = PIN_A4; + static const uint8_t A5 = PIN_A5; + static const uint8_t A6 = PIN_A6; + static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + + static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +// TXD1 RXD1 on Base Board +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// TXD0 RXD0 on Base Board +#define PIN_SERIAL2_RX (19) +#define PIN_SERIAL2_TX (20) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (29) +#define PIN_SPI_MOSI (30) +#define PIN_SPI_SCK (3) + + static const uint8_t SS = 26; + static const uint8_t MOSI = PIN_SPI_MOSI; + static const uint8_t MISO = PIN_SPI_MISO; + static const uint8_t SCK = PIN_SPI_SCK; + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 2 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) +#define PIN_WIRE1_SDA (24) +#define PIN_WIRE1_SCL (25) + +// QSPI Pins +#define PIN_QSPI_SCK 3 // 19 +#define PIN_QSPI_CS 26 // 17 +#define PIN_QSPI_IO0 30 // 20 +#define PIN_QSPI_IO1 29 // 21 +#define PIN_QSPI_IO2 28 // 22 +#define PIN_QSPI_IO3 2 // 23 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif