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
This commit is contained in:
James L
2026-02-22 18:25:20 -05:00
commit a746937390
134 changed files with 28567 additions and 0 deletions

11
.gitignore vendored Executable file
View File

@@ -0,0 +1,11 @@
.DS_Store
*.hex
*.pyc
TODO
Release/*.hex
Release/*.zip
Release/*.json
Console/build
build/*
.pio/*
.vscode/*

171
BLESerial.cpp Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#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

136
BLESerial.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#include "Boards.h"
#if PLATFORM != PLATFORM_NRF52
#if HAS_BLE
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>
template <size_t n>
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> 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

615
Bluetooth.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#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 <bluefruit.h>
#include <math.h>
#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

938
Boards.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#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 <variant.h>
#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

501
BoundaryConfig.h Normal file
View File

@@ -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 <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
// ─── 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(
"<!DOCTYPE html><html><head>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>RNode Boundary Setup</title>"
"<style>"
"body{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;margin:0;padding:16px;}"
"h1{color:#e94560;font-size:1.4em;margin:0 0 8px;}"
"h2{color:#0f3460;background:#e0e0e0;padding:6px 10px;margin:18px -10px 10px;font-size:1em;border-radius:4px;}"
"form{max-width:480px;margin:0 auto;}"
"label{display:block;margin:8px 0 2px;font-size:0.9em;color:#aaa;}"
"input,select{width:100%;padding:8px;margin:2px 0 6px;box-sizing:border-box;"
"background:#16213e;border:1px solid #0f3460;color:#e0e0e0;border-radius:4px;font-size:0.95em;}"
"input:focus,select:focus{border-color:#e94560;outline:none;}"
".row{display:flex;gap:10px;}.row>div{flex:1;}"
".note{font-size:0.8em;color:#666;margin:2px 0 8px;}"
"button{width:100%;padding:12px;margin:20px 0;background:#e94560;color:#fff;"
"border:none;border-radius:4px;font-size:1.1em;cursor:pointer;}"
"button:hover{background:#c73e54;}"
".ok{background:#16213e;padding:20px;border-radius:8px;text-align:center;}"
".ok h1{color:#0f0;}"
"</style></head><body>"
"<h1>&#x1f4e1; RNode Boundary Node</h1>"
"<form method='POST' action='/save'>"
);
// ── WiFi STA Section ──
html += F(
"<h2>&#x1f4f6; WiFi Network</h2>"
"<label>WiFi</label>"
"<select name='wifi_en'>"
);
html += F("<option value='1'");
if (boundary_state.wifi_enabled) html += F(" selected");
html += F(">Enabled</option>");
html += F("<option value='0'");
if (!boundary_state.wifi_enabled) html += F(" selected");
html += F(">Disabled (LoRa-only repeater)</option>");
html += F("</select>");
html += F(
"<label>SSID</label>"
"<input name='ssid' maxlength='32' placeholder='Your WiFi network' value='"
);
html += String(cur_ssid);
html += F(
"'>"
"<label>Password</label>"
"<input name='psk' type='password' maxlength='32' placeholder='WiFi password' value='"
);
html += String(cur_psk);
html += F("'>");
// ── TCP Backbone Section ──
html += F(
"<h2>&#x1f310; TCP Backbone</h2>"
"<label>Mode</label>"
"<select name='tcp_mode'>"
);
html += F("<option value='0'");
if (boundary_state.tcp_mode == 0) html += F(" selected");
html += F(">Disabled</option>");
html += F("<option value='1'");
if (boundary_state.tcp_mode == 1) html += F(" selected");
html += F(">Client (connect to backbone)</option>");
html += F("</select>");
html += F("<label>Backbone Host</label>");
html += F("<input name='bb_host' maxlength='63' placeholder='e.g. 192.168.1.100' value='");
html += String(boundary_state.backbone_host);
html += F("'>");
html += F("<label>Backbone Port</label>");
html += F("<input name='bb_port' type='number' min='1' max='65535' value='");
html += String(boundary_state.backbone_port);
html += F("'>");
// ── Local TCP Server Section ──
html += F(
"<h2>&#x1f4e1; Local TCP Server (optional)</h2>"
"<p class='note'>Run a TCP server on the same WiFi network so local devices can connect. "
"Uses Access Point mode (does not forward announces).</p>"
"<label>Local TCP Server</label>"
"<select name='ap_tcp_en'>"
);
html += F("<option value='0'");
if (!boundary_state.ap_tcp_enabled) html += F(" selected");
html += F(">Disabled</option>");
html += F("<option value='1'");
if (boundary_state.ap_tcp_enabled) html += F(" selected");
html += F(">Enabled</option>");
html += F("</select>");
html += F("<label>TCP Port</label>");
html += F("<input name='ap_tcp_port' type='number' min='1' max='65535' value='");
html += String(boundary_state.ap_tcp_port);
html += F("'>");
// ── LoRa Radio Section ──
html += F(
"<h2>&#x1f4fb; LoRa Radio</h2>"
);
// Frequency — show in MHz for human-friendliness
char freq_str[16];
dtostrf((double)cur_freq / 1000000.0, 1, 3, freq_str);
html += F("<label>Frequency (MHz)</label>");
html += F("<input name='freq' type='text' placeholder='914.875' value='");
html += String(freq_str);
html += F("'>");
html += F("<p class='note'>e.g. 914.875, 868.000, 433.000</p>");
// Bandwidth — dropdown
html += F("<label>Bandwidth</label><select name='bw'>");
for (int i = 0; i < BW_OPTIONS_COUNT; i++) {
html += F("<option value='");
html += String(BW_OPTIONS[i].hz);
html += "'";
if (BW_OPTIONS[i].hz == cur_bw) html += F(" selected");
html += ">";
html += BW_OPTIONS[i].label;
html += F("</option>");
}
html += F("</select>");
// Spreading Factor — dropdown 6-12
html += F("<label>Spreading Factor</label><select name='sf'>");
for (int sf = 6; sf <= 12; sf++) {
html += F("<option value='");
html += String(sf);
html += "'";
if (sf == cur_sf) html += F(" selected");
html += ">SF";
html += String(sf);
html += F("</option>");
}
html += F("</select>");
// Coding Rate — dropdown 5-8 (maps to 4/5 through 4/8)
html += F("<label>Coding Rate</label><select name='cr'>");
for (int cr = 5; cr <= 8; cr++) {
html += F("<option value='");
html += String(cr);
html += "'";
if (cr == cur_cr) html += F(" selected");
html += ">4/";
html += String(cr);
html += F("</option>");
}
html += F("</select>");
// TX Power
html += F("<label>TX Power (dBm)</label>");
html += F("<input name='txp' type='number' min='2' max='");
#ifdef PA_MAX_OUTPUT
html += String(PA_MAX_OUTPUT);
#else
html += "22";
#endif
html += F("' value='");
html += String(cur_txp);
html += F("'>");
#ifdef PA_MAX_OUTPUT
html += F("<p class='note'>Max output for this board: ");
html += String(PA_MAX_OUTPUT);
html += F(" dBm (with PA)</p>");
#endif
// ── Submit ──
html += F(
"<button type='submit'>Save &amp; Reboot</button>"
"</form></body></html>"
);
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(
"<!DOCTYPE html><html><head>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>Saved</title>"
"<style>"
"body{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;padding:40px;"
"display:flex;align-items:center;justify-content:center;min-height:80vh;}"
".ok{background:#16213e;padding:30px;border-radius:12px;text-align:center;max-width:400px;}"
"h1{color:#4caf50;margin-bottom:16px;}"
"p{color:#aaa;}"
"</style></head><body>"
"<div class='ok'>"
"<h1>&#x2705; Configuration Saved</h1>"
"<p>Device will reboot in 3 seconds and connect to your WiFi network.</p>"
"<p style='color:#666;font-size:0.85em;'>If the device cannot connect, hold the button for 5+ seconds to re-enter setup.</p>"
"</div></body></html>"
);
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

225
BoundaryMode.h Normal file
View File

@@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

242
Config.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#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

203
Console.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#include <FS.h>
#include <SPIFFS.h>
#include <WiFi.h>
#include <WebServer.h>
#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);
}

47
Console/Makefile Executable file
View File

@@ -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

1009
Console/assets/css/water.css Executable file

File diff suppressed because it is too large Load Diff

BIN
Console/assets/css/yond.woff2 Executable file

Binary file not shown.

BIN
Console/assets/gfx/cs.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

BIN
Console/assets/gfx/icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

BIN
Console/assets/gfx/nn.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
Console/assets/gfx/ph.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

BIN
Console/assets/gfx/rnode_iso.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
Console/assets/gfx/sideband.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
Console/assets/images/3_conv.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
Console/assets/images/an1.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
Console/assets/images/bg1ds1.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
Console/assets/images/bg1ds2.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
Console/assets/images/bg_h_1.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
Console/assets/images/bg_h_2.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
Console/assets/images/g1p.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
Console/assets/images/g2p.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
Console/assets/images/g3p.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
Console/assets/images/g4p.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
Console/assets/images/nn_an.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

373
Console/build.py Executable file
View File

@@ -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 = """
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{ASSET_PATH}css/water.css?v=4">
<link rel="shortcut icon" type="image/x-icon" href="{ASSET_PATH}gfx/icon.png">
<meta charset="utf-8"/>
<title>{PAGE_TITLE}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="load_overlay" style="background-color:#2a2a2f; position:absolute; top:0px; left:0px; width:100%; height:100%; z-index:2000;"></div>
<span class="logo">RNode Console</span>
{MENU}<hr>"""
document_end = """</body></html>"""
menu_md = """<center markdown=\"1\"><span class="menu">[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)</span></center>"""
manual_redirect = """
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url=/m/index.html">
</head>
</html>
"""
help_redirect = """
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url=/help.html">
</head>
</html>
"""
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 += "<a class=\"topic_link\" href=\""+str(topic["file"])+"\">"
md += "<span class=\"topic\">"
md += "<img class=\"topic_image\" src=\""+str(topic["image"])+"\"/>"
md += "<span class=\"topic_title\">"+str(topic["title"])+"</span>"
#md += "<span class=\"topic_date\">"+str(topic["date"])+"</span>"
md += "<span class=\"topic_excerpt\">"+str(topic["excerpt"])+"</span>"
md += "</span>"
md += "</a>"
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("<p></p>", "")
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()

8
Console/source/builds/ap.md Executable file
View File

@@ -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!

168
Console/source/builds/handheld.md Executable file
View File

@@ -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).
<img alt="Completed Handheld RNode" src="{ASSET_PATH}images/bg_h_1.webp" style="width: 100%;"/>
<center>*A completed Handheld RNode*</center>
### 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)
### <a name="prep"></a>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.
### <a name="devboard"></a>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.
<img alt="Compatible board" src="{ASSET_PATH}images/bg_h_2.webp" style="width: 100%;"/>
<center>*The correct board version for this RNode build recipe*</center>
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:
<img alt="Before desoldering" src="{ASSET_PATH}images/bg1ds1.webp" style="display: inline-block;width: 48.7%; margin-right:1%;"/>
<img alt="After desoldering" src="{ASSET_PATH}images/bg1ds2.webp" style="display: inline-block;width: 48.7%; margin-left: 1%;"/>
<center>*Before and after removing the SMA connector and moving the resistor*</center>
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.
### <a name="materials"></a>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
### <a name="parts"></a>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.
### <a name="tools"></a>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.
### <a name="firmware"></a>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`.
### <a name="assembly"></a>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!

View File

@@ -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!

View File

@@ -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!

20
Console/source/contact.md Executable file
View File

@@ -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.
<center>`3502`</center>

39
Console/source/contribute.md Executable file
View File

@@ -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<br/>
```
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
```
<br/><br/>
- Ethereum<br/>
```
0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73
```
<br/><br/>
- Bitcoin<br/>
```
35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH
```
<br/><br/>
- Ko-Fi<br/>
<a href="https://ko-fi.com/markqvist">`https://ko-fi.com/markqvist`</a>
## 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)

View File

@@ -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.)
<div class="article_date">{DATE}</div>
# 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)
<center>*Some of the device types compatible with this installation guide*</center>
## 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**.

View File

@@ -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.)
<div class="article_date">{DATE}</div>
# 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 <a5dc367015b30f2d7b59> 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:
<center><p><img src="{ASSET_PATH}images/an1.webp"/></p></center>
After a few seconds, Bobs announce shows up in the **Announce Stream** section of the Nomad Network program on Alices computer:
<center><p><img src="{ASSET_PATH}images/nn_an.webp"/></p></center>
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:
<center><p><img style="max-width: 100%; width: 300px;" src="{ASSET_PATH}images/3_conv.webp"/></p></center>
And on Alices computer running Nomad Network, it looks like this:
<center><p><img src="{ASSET_PATH}images/nn_conv.webp"/></p></center>
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.

View File

@@ -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)
<center>*A homemade RNode, based on an ESP32 board and a transceiver module, ready for use*</center>
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).

View File

@@ -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.)
<div class="article_date">{DATE}</div>
# 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.

14
Console/source/help.md Executable file
View File

@@ -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)

28
Console/source/index.md Executable file
View File

@@ -0,0 +1,28 @@
## Hello!
<table style="margin-bottom: 1.5em;">
<tbody>
<tr>
<td style="vertical-align:middle;padding-left: 0;">
You have connected to the <b>RNode Bootstrap Console</b>.<br/>
<br/>
The tools and information contained in this RNode will allow you to replicate the RNode design, build more RNodes and grow your communications ecosystems.<br/>
<br/>
This repository also contains tools, software and information necessary to bootstrap networks and communications systems based on RNodes and Reticulum.
</td>
<td width="33%" style="vertical-align:middle;padding-right: 0;">
<img src="{ASSET_PATH}gfx/rnode_iso.webp" width="100%"/></td>
</tr>
</tbody>
</table>
<hr>
<center>
<h3>What would you like to do?</h3>
<div style="width:66%">You can browse this repository freely, or jump straight into a task-oriented workflow by selecting one of the starting points below.</div>
<a href="./replicate.html"><button type="button" id="task-replicate">Create RNodes</button></a>
<a href="./software.html"><button type="button" id="task-rns">Install Software</button></a>
<a href="./learn.html"><button type="button" id="task-rns">Learn More</button></a>
<a href="./m/networks.html"><button type="button" id="task-rns">Build A Network</button></a>
<a href="./help.html"><button type="button" id="task-rns">Get Help</button></a>
<a href="https://unsigned.io/shop"><button type="button" id="task-rns">Buy an RNode</button></a>
<a href="./contribute.html"><button type="button" id="task-rns">Contribute</button></a>
</center>

14
Console/source/learn.md Executable file
View File

@@ -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}

20
Console/source/qa.md Executable file
View File

@@ -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).

5
Console/source/recipes.md Executable file
View File

@@ -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}

27
Console/source/replicate.md Executable file
View File

@@ -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.
<br/><br/>
<center>
<h3>Choose a path to get started</h3>
<br/>
<a href="{ASSET_PATH}guides/install_firmware.html"><button type="button" id="task-rns">Basic Build</button></a>
<a href="{ASSET_PATH}recipes.html"><button type="button" id="task-rns">Build Recipes</button></a>
<a href="{ASSET_PATH}guides/make_rnodes.html"><button type="button" id="task-rns">New Design</button></a>
</center>

11
Console/source/rnode.md Executable file
View File

@@ -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.

23
Console/source/s_lxmf.md Executable file
View File

@@ -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`

27
Console/source/s_nn.md Executable file
View File

@@ -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`

33
Console/source/s_rns.md Executable file
View File

@@ -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.
<p align="center"><img width="30%" src="{ASSET_PATH}m/_static/rns_logo_512.png"></p>
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).

20
Console/source/s_rnsh.md Executable file
View File

@@ -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`

12
Console/source/s_sideband.md Executable file
View File

@@ -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)

9
Console/source/sell_rnodes.md Executable file
View File

@@ -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.

23
Console/source/software.md Executable file
View File

@@ -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.
<br/><br/>
<center>
<h3>Choose a software package to get started</h3>
<br/>
<a href="./s_rns.html"><button type="button" id="task-rns">Reticulum</button></a>
<a href="./s_lxmf.html"><button type="button" id="task-rns">LXMF</button></a>
<a href="./s_nn.html"><button type="button" id="task-rns">Nomad Network</button></a>
<a href="./s_rnsh.html"><button type="button" id="task-rns">RN Shell</button></a>
<a href="./s_sideband.html"><button type="button" id="task-rns">Sideband</button></a>
</center>

17
Console/source/supported.md Executable file
View File

@@ -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.

267
Device.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#include <Ed25519.h>
#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<EEPROM_SIG_LEN; i++){dev_eeprom_signature[i]=EEPROM.read(eeprom_addr(ADDR_SIGNATURE+i));}
mbedtls_md_context_t ctx;
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
mbedtls_md_starts(&ctx);
#if HAS_BLUETOOTH == true || HAS_BLE == true
mbedtls_md_update(&ctx, dev_bt_mac, BT_DEV_ADDR_LEN);
#else
// TODO: Get from BLE stack instead
// mbedtls_md_update(&ctx, dev_bt_mac, BT_DEV_ADDR_LEN);
#endif
mbedtls_md_update(&ctx, dev_eeprom_signature, EEPROM_SIG_LEN);
mbedtls_md_finish(&ctx, dev_hash);
mbedtls_md_free(&ctx);
#elif MCU_VARIANT == MCU_NRF52
for (uint8_t i=0; i<EEPROM_SIG_LEN; i++){dev_eeprom_signature[i]=eeprom_read(eeprom_addr(ADDR_SIGNATURE+i));}
nRFCrypto.begin();
nRFCrypto_Hash hash;
hash.begin(CRYS_HASH_SHA256_mode);
#if HAS_BLUETOOTH == true || HAS_BLE == true
hash.update(dev_bt_mac, BT_DEV_ADDR_LEN);
#else
// TODO: Get from BLE stack instead
// hash.update(dev_bt_mac, BT_DEV_ADDR_LEN);
#endif
hash.update(dev_eeprom_signature, EEPROM_SIG_LEN);
hash.end(dev_hash);
#endif
device_load_signature();
device_validate_signature();
device_validate_partitions();
#if MCU_VARIANT == MCU_NRF52
nRFCrypto.end();
#endif
device_init_done = true;
return device_init_done && fw_signature_validated;
} else {
return false;
}
}
#endif

1302
Display.h Executable file

File diff suppressed because it is too large Load Diff

1
Documentation/README.md Executable file
View File

@@ -0,0 +1 @@
# RNode Documentation

BIN
Documentation/RNode_v1_Manual.pdf Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
Documentation/rnfw_1.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

35
FileStream.h Executable file
View File

@@ -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> _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

478
FileSystem.cpp Executable file
View File

@@ -0,0 +1,478 @@
#include "FileSystem.h"
#include "FileStream.h"
#include "FileSystemType.h"
#ifdef HAS_RNS
#include <Log.h>
#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<std::string> FileSystem::list_directory(const char* directory_path) {
TRACEF("list_directory: listing directory %s", directory_path);
std::list<std::string> 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

41
FileSystem.h Executable file
View File

@@ -0,0 +1,41 @@
#pragma once
#ifdef HAS_RNS
#include <FileSystem.h>
#include <FileStream.h>
#include <Bytes.h>
#include <Type.h>
#include <Stream.h>
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<std::string> list_directory(const char* directory_path);
virtual size_t storage_size();
virtual size_t storage_available();
};
#endif

51
FileSystemType.h Executable file
View File

@@ -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 <SPIFFS.h>
#define FS SPIFFS
#elif FS_TYPE == FS_TYPE_LITTLEFS
#include <LittleFS.h>
#define FS LittleFS
#elif FS_TYPE == FS_TYPE_INTERNALFS
#include <InternalFileSystem.h>
#define FS InternalFS
using namespace Adafruit_LittleFS_Namespace;
#elif FS_TYPE == FS_TYPE_FLASHFS
#include <Cached_SPIFlash.h>
#include <FlashFileSystem.h>
#define FS FlashFS
using namespace Adafruit_LittleFS_Namespace;
#else
#error "FileSystem type not specified"
#endif
#endif

131
Fonts/Org_01.h Executable file
View File

@@ -0,0 +1,131 @@
#pragma once
#include <Adafruit_GFX.h>
// 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

123
Fonts/PicoPixel.h Executable file
View File

@@ -0,0 +1,123 @@
#pragma once
#include <Adafruit_GFX.h>
// 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

124
Framing.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#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

445
Graphics.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
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
};

95
Input.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#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

674
LICENSE Executable file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <https://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/why-not-lgpl.html>.

301
MD5.cpp Executable file
View File

@@ -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;
}

52
MD5.h Executable file
View File

@@ -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 <solar at openwall.com> 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
* <scott@macvicar.net>
*/
#include <string.h>
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

518
Makefile Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
# 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

4
Modem.h Executable file
View File

@@ -0,0 +1,4 @@
#define SX1276 0x01
#define SX1278 0x02
#define SX1262 0x03
#define SX1280 0x04

38
NoopFileSystem.h Executable file
View File

@@ -0,0 +1,38 @@
#pragma once
#ifdef HAS_RNS
#include <FileSystem.h>
#include <Bytes.h>
#include <Type.h>
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<std::string> list_directory(const char* directory_path) { return std::list<std::string>(); }
virtual size_t storage_size() { return 0; }
virtual size_t storage_available() { return 0; }
};
#endif

650
Power.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#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 <XPowersLib.h>
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
}

47
Python Module/Example.py Executable file
View File

@@ -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()

536
Python Module/RNode.py Executable file
View File

@@ -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+"]"

295
README.md Normal file
View File

@@ -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 = <boundary-node-ip>
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)

2569
RNode_Firmware.ino Executable file

File diff suppressed because it is too large Load Diff

63
ROM.h Executable file
View File

@@ -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 <https://www.gnu.org/licenses/>.
#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

10
Release/README.md Executable file
View File

@@ -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
```

BIN
Release/console_image.bin Executable file

Binary file not shown.

5651
Release/esptool/esptool.py Executable file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More