Compare commits
22 Commits
8db47d4d01
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f10ee8329 | |||
| 17a99ddd54 | |||
| eb827f5a60 | |||
| bc60028852 | |||
| 0ef379595f | |||
| 185d15cbaf | |||
| b728ca3c75 | |||
| c15afa9819 | |||
| 192a008150 | |||
| c827989f43 | |||
| 6115cd6b84 | |||
|
|
c84006da8a | ||
|
|
3157b8e4a8 | ||
|
|
b3b6cd4302 | ||
|
|
7e56611fe6 | ||
|
|
56c1a6b881 | ||
|
|
79cb2d49e8 | ||
|
|
d8c925769d | ||
|
|
300676e5ae | ||
|
|
d49c55a843 | ||
|
|
85d8fc7f78 | ||
|
|
949c13c7b1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,3 +16,4 @@ Console/build
|
||||
build/*
|
||||
.pio/*
|
||||
.vscode/*
|
||||
Release/*.bin
|
||||
|
||||
77
Boards.h
77
Boards.h
@@ -122,6 +122,8 @@
|
||||
#define MODEL_FE 0xFE // Homebrew board, max 17dBm output power
|
||||
#define MODEL_FF 0xFF // Homebrew board, max 14dBm output power
|
||||
|
||||
#define BOARD_MESHADVENTURER_S3 0xF2
|
||||
|
||||
#if defined(__AVR_ATmega1284P__)
|
||||
#define PLATFORM PLATFORM_AVR
|
||||
#define MCU_VARIANT MCU_1284P
|
||||
@@ -149,6 +151,10 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define LORA_PA_UNKNOWN 0x00
|
||||
#define LORA_PA_GC1109 0x01
|
||||
#define LORA_PA_KCT8103L 0x02
|
||||
|
||||
#define HAS_DISPLAY false
|
||||
#define HAS_BLUETOOTH false
|
||||
#define HAS_BLE false
|
||||
@@ -361,7 +367,7 @@
|
||||
#define HAS_SLEEP true
|
||||
#define PIN_WAKEUP GPIO_NUM_0
|
||||
#define WAKEUP_LEVEL 0
|
||||
#define OCP_TUNED 0x18
|
||||
#define OCP_TUNED 0x28
|
||||
|
||||
const int pin_btn_usr1 = 0;
|
||||
|
||||
@@ -409,8 +415,9 @@
|
||||
#define HAS_LORA_LNA true
|
||||
#define PIN_WAKEUP GPIO_NUM_0
|
||||
#define WAKEUP_LEVEL 0
|
||||
#define OCP_TUNED 0x18
|
||||
#define OCP_TUNED 0x28
|
||||
#define Vext GPIO_NUM_36
|
||||
#define LORA_PA_MODEL LORA_PA_UNKNOWN;
|
||||
|
||||
const int pin_btn_usr1 = 0;
|
||||
|
||||
@@ -432,14 +439,17 @@
|
||||
|
||||
#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 LORA_PA_CSD 2 // Same pin on GC1109
|
||||
#define LORA_PA_CPS 46 // Same pin on GC1109
|
||||
#define LORA_PA_CTX 5 // Only used on KCT8103
|
||||
|
||||
#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
|
||||
|
||||
#define LORA_LNA_KCT8103L_GAIN 21
|
||||
const int PA_GC1109_VALUES[PA_GAIN_POINTS] = {11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7};
|
||||
const int PA_KCT8103L_VALUES[PA_GAIN_POINTS] = {13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7};
|
||||
|
||||
const int pin_cs = 8;
|
||||
const int pin_busy = 13;
|
||||
@@ -449,6 +459,50 @@
|
||||
const int pin_miso = 11;
|
||||
const int pin_sclk = 9;
|
||||
|
||||
#elif BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
#define IS_ESP32S3 true
|
||||
#define HAS_DISPLAY true
|
||||
#define HAS_NP true
|
||||
#define HAS_BLUETOOTH false
|
||||
#define HAS_BLE true
|
||||
#define HAS_WIFI true
|
||||
#define HAS_CONSOLE true
|
||||
#define HAS_EEPROM true
|
||||
#define HAS_BUSY true
|
||||
#define HAS_INPUT true
|
||||
#define HAS_TCXO true
|
||||
#define MODEM SX1262
|
||||
#define DIO2_AS_RF_SWITCH false
|
||||
#define HAS_RF_SWITCH_RX_TX true
|
||||
#define HAS_LORA_LNA true
|
||||
#define LORA_LNA_GAIN 30
|
||||
#define LORA_LNA_GVT 14
|
||||
#define PA_MAX_OUTPUT 22
|
||||
|
||||
const int pin_cs = 39;
|
||||
const int pin_sclk = 38;
|
||||
const int pin_miso = 40;
|
||||
const int pin_mosi = 18;
|
||||
const int pin_busy = 7;
|
||||
const int pin_reset = 43;
|
||||
const int pin_dio = 15;
|
||||
const int pin_txen = 9;
|
||||
const int pin_rxen = 8;
|
||||
const int pin_tcxo_enable = -1;
|
||||
|
||||
const int pin_btn_usr1 = 4;
|
||||
const int pin_np = 48;
|
||||
|
||||
#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
|
||||
|
||||
#elif BOARD_MODEL == BOARD_RNODE_NG_20
|
||||
#define HAS_DISPLAY true
|
||||
#define HAS_BLUETOOTH true
|
||||
@@ -483,13 +537,10 @@
|
||||
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_MISO = 2;
|
||||
const int SD_MOSI = 15;
|
||||
const int SD_CLK = 14;
|
||||
// CBA already defined by framework
|
||||
//const int SD_CS = 13;
|
||||
const int SD_CS = 13;
|
||||
#if HAS_NP == false
|
||||
#if defined(EXTERNAL_LEDS)
|
||||
const int pin_led_rx = 12;
|
||||
@@ -934,7 +985,7 @@
|
||||
// Default OCP value if not specified
|
||||
// in board configuration
|
||||
#ifndef OCP_TUNED
|
||||
#define OCP_TUNED 0x18
|
||||
#define OCP_TUNED 0x28
|
||||
#endif
|
||||
|
||||
#ifndef PA_MAX_OUTPUT
|
||||
|
||||
97
BoundaryConfig.h
Normal file → Executable file
97
BoundaryConfig.h
Normal file → Executable file
@@ -1,7 +1,10 @@
|
||||
// Copyright (C) 2026, Boundary Mode Extension
|
||||
// Based on microReticulum_Firmware by Mark Qvist
|
||||
//
|
||||
// BoundaryConfig.h — Captive-portal web configuration for Boundary Mode.
|
||||
// BoundaryConfig.h — Captive-portal web configuration for the legacy
|
||||
// "Boundary Mode" path. This should be renamed to "Transport Mode"
|
||||
// together with the rest of the boundary-mode terminology. In this fork,
|
||||
// transport/boundary mode is the only intended mode of operation.
|
||||
// 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,
|
||||
@@ -21,6 +24,11 @@
|
||||
#include <WebServer.h>
|
||||
#include <DNSServer.h>
|
||||
|
||||
// ─── Node hash (cached in RTC by normal boot, read here without starting RNS) ─
|
||||
#define NODE_HASH_RTC_MAGIC 0x504B4841UL
|
||||
extern uint32_t rtc_node_hash_magic;
|
||||
extern char rtc_node_hash_hex[33];
|
||||
|
||||
// ─── Config Portal State ─────────────────────────────────────────────────────
|
||||
static bool config_portal_active = false;
|
||||
static WebServer* config_server = nullptr;
|
||||
@@ -59,6 +67,16 @@ static const char* const BW_OPTIONS_LABELS[] PROGMEM = {
|
||||
};
|
||||
static const int BW_OPTIONS_COUNT = sizeof(BW_OPTIONS_HZ) / sizeof(BW_OPTIONS_HZ[0]);
|
||||
|
||||
static uint8_t config_default_display_rotation() {
|
||||
#if BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_RAK4631
|
||||
return 0;
|
||||
#elif BOARD_MODEL == BOARD_HELTEC32_V2 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TBEAM_S_V1
|
||||
return 1;
|
||||
#else
|
||||
return 3;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ─── HTML Page Generation ────────────────────────────────────────────────────
|
||||
|
||||
static void config_send_html() {
|
||||
@@ -87,10 +105,10 @@ static void config_send_html() {
|
||||
if (cur_txp == 0xFF) cur_txp = PA_MAX_OUTPUT; // Default to board max
|
||||
|
||||
// Default frequency if not set
|
||||
if (cur_freq == 0) cur_freq = 914875000; // 914.875 MHz default
|
||||
if (cur_freq == 0) cur_freq = 868825000; // 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
|
||||
if (cur_cr < 5 || cur_cr > 8) cur_cr = 7; // CR 4/5 default
|
||||
|
||||
// Build the HTML page
|
||||
String html = F(
|
||||
@@ -113,12 +131,25 @@ static void config_send_html() {
|
||||
"button:hover{background:#c73e54;}"
|
||||
".ok{background:#16213e;padding:20px;border-radius:8px;text-align:center;}"
|
||||
".ok h1{color:#0f0;}"
|
||||
".node-hash{background:#0f1a30;border:1px solid #0f3460;border-radius:6px;"
|
||||
"padding:10px 14px;margin:0 0 16px;}"
|
||||
".node-hash .nh-label{display:block;font-size:0.75em;color:#888;margin-bottom:4px;}"
|
||||
".node-hash code{font-family:monospace;font-size:0.95em;color:#7ecfff;"
|
||||
"word-break:break-all;letter-spacing:0.05em;}"
|
||||
"</style></head><body>"
|
||||
"<h1>📡 RNode Boundary Node</h1>"
|
||||
"<form method='POST' action='/save'>"
|
||||
);
|
||||
|
||||
// ── WiFi STA Section ──
|
||||
// ── Node public hash ──
|
||||
html += F("<div class='node-hash'><span class='nh-label'>🔑 Node Hash (Reticulum destination)</span><code>");
|
||||
if (rtc_node_hash_magic == NODE_HASH_RTC_MAGIC && rtc_node_hash_hex[0] != '\0') {
|
||||
html += String(rtc_node_hash_hex);
|
||||
} else {
|
||||
html += F("<span style='color:#888;font-style:italic;'>Not yet assigned — will be set on first normal boot</span>");
|
||||
}
|
||||
html += F("</code></div>");
|
||||
|
||||
html += F("<form method='POST' action='/save'>");
|
||||
html += F(
|
||||
"<h2>📶 WiFi Network</h2>"
|
||||
"<label>WiFi</label>"
|
||||
@@ -221,9 +252,9 @@ static void config_send_html() {
|
||||
}
|
||||
html += F("</select>");
|
||||
|
||||
// Spreading Factor — dropdown 6-12
|
||||
// Spreading Factor — dropdown 5-12
|
||||
html += F("<label>Spreading Factor</label><select name='sf'>");
|
||||
for (int sf = 6; sf <= 12; sf++) {
|
||||
for (int sf = 5; sf <= 12; sf++) {
|
||||
html += F("<option value='");
|
||||
html += String(sf);
|
||||
html += "'";
|
||||
@@ -299,11 +330,16 @@ static void config_send_html() {
|
||||
);
|
||||
|
||||
// Read current blanking timeout from EEPROM (stored as minutes, 0 = never)
|
||||
uint8_t cur_blank = 0;
|
||||
uint8_t cur_blank = 5;
|
||||
if (EEPROM.read(eeprom_addr(ADDR_CONF_BSET)) == CONF_OK_BYTE) {
|
||||
cur_blank = EEPROM.read(eeprom_addr(ADDR_CONF_DBLK));
|
||||
}
|
||||
|
||||
uint8_t cur_rotation = EEPROM.read(eeprom_addr(ADDR_CONF_DROT));
|
||||
if (cur_rotation > 3) {
|
||||
cur_rotation = config_default_display_rotation();
|
||||
}
|
||||
|
||||
static const uint8_t blank_vals[] = { 0, 1, 5, 10, 30, 60 };
|
||||
static const char* blank_labels[] = { "Never", "1 minute", "5 minutes", "10 minutes", "30 minutes", "60 minutes" };
|
||||
static const int blank_count = 6;
|
||||
@@ -320,6 +356,23 @@ static void config_send_html() {
|
||||
html += F("</select>");
|
||||
html += F("<p class='note'>Turn off display after inactivity to save power</p>");
|
||||
|
||||
html += F("<label>Display Orientation</label><select name='disp_rot'>");
|
||||
html += F("<option value='0'");
|
||||
if (cur_rotation == 0) html += F(" selected");
|
||||
html += F(">Landscape</option>");
|
||||
html += F("<option value='1'");
|
||||
if (cur_rotation == 1) html += F(" selected");
|
||||
html += F(">Portrait</option>");
|
||||
html += F("<option value='2'");
|
||||
if (cur_rotation == 2) html += F(" selected");
|
||||
html += F(">Landscape Flipped</option>");
|
||||
html += F("<option value='3'");
|
||||
if (cur_rotation == 3) html += F(" selected");
|
||||
html += F(">Portrait Flipped</option>");
|
||||
html += F("</select>");
|
||||
html += F("<p class='note'>Choose the orientation that matches your OLED mounting. "
|
||||
"Landscape modes place the two status panes side by side; portrait modes stack them.</p>");
|
||||
|
||||
// ── Submit ──
|
||||
html += F(
|
||||
"<button type='submit'>Save & Reboot</button>"
|
||||
@@ -353,6 +406,14 @@ static void config_handle_save() {
|
||||
// Set WiFi mode to STA
|
||||
EEPROM.write(eeprom_addr(ADDR_CONF_WIFI), WR_WIFI_STA);
|
||||
|
||||
// Boundary mode always uses DHCP on the STA interface. Clear the legacy
|
||||
// static IP and netmask slots so stale values from older firmware or tools
|
||||
// cannot force a persistent static address.
|
||||
for (int i = 0; i < 4; i++) {
|
||||
EEPROM.write(config_addr(ADDR_CONF_IP + i), 0x00);
|
||||
EEPROM.write(config_addr(ADDR_CONF_NM + i), 0x00);
|
||||
}
|
||||
|
||||
// ── WiFi enable setting ──
|
||||
boundary_state.wifi_enabled = (config_server->arg("wifi_en").toInt() == 1);
|
||||
|
||||
@@ -370,6 +431,12 @@ static void config_handle_save() {
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_DBLK), blank_val);
|
||||
}
|
||||
|
||||
int display_rotation = config_server->arg("disp_rot").toInt();
|
||||
if (display_rotation < 0 || display_rotation > 3) {
|
||||
display_rotation = config_default_display_rotation();
|
||||
}
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_DROT), (uint8_t)display_rotation);
|
||||
|
||||
// ── 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;
|
||||
@@ -421,7 +488,7 @@ static void config_handle_save() {
|
||||
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;
|
||||
if (sf_val >= 5 && 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;
|
||||
@@ -481,6 +548,14 @@ static void config_handle_redirect() {
|
||||
|
||||
// ─── Check if config is needed ───────────────────────────────────────────────
|
||||
bool boundary_needs_config() {
|
||||
// If the RTNode app marker is missing, this node was either never
|
||||
// configured by RTNode or was flashed from a different firmware family
|
||||
// such as stock RNode. Force the portal so RTNode can claim and rewrite
|
||||
// its persisted settings explicitly.
|
||||
if (!boundary_app_marker_valid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if WiFi SSID is configured
|
||||
char ssid[33];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
@@ -557,6 +632,10 @@ void config_portal_start() {
|
||||
display.display();
|
||||
}
|
||||
#endif
|
||||
// Headless: LED ramp will be driven from the WCC portal loop
|
||||
if (headless_mode) {
|
||||
Serial.println("[Config] Headless mode — LED will breathe during config portal");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stop Config Portal ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// 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.
|
||||
// BoundaryMode.h — Configuration and runtime state for the legacy
|
||||
// "Boundary Mode" firmware variant. Going forward this should be renamed
|
||||
// "Transport Mode". It is the only intended operating mode for this fork;
|
||||
// the old multi-mode split is kept only for compatibility while the codebase
|
||||
// is being simplified.
|
||||
//
|
||||
// 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
|
||||
@@ -15,8 +17,19 @@
|
||||
|
||||
#ifdef BOUNDARY_MODE
|
||||
|
||||
// Compatibility alias for the planned rename from Boundary Mode to
|
||||
// Transport Mode. New code should prefer the transport terminology even
|
||||
// while the legacy BOUNDARY_MODE compile-time flag still exists.
|
||||
#ifndef TRANSPORT_MODE
|
||||
#define TRANSPORT_MODE 1
|
||||
#endif
|
||||
|
||||
// ─── Boundary Mode Configuration ────────────────────────────────────────────
|
||||
//
|
||||
// NOTE: "Boundary Mode" is the legacy name. This should be relabeled
|
||||
// "Transport Mode" once the remaining non-transport code paths are removed.
|
||||
// In practice this is the only supported mode in this firmware branch.
|
||||
//
|
||||
// The boundary node operates with TWO RNS interfaces:
|
||||
//
|
||||
// 1. LoRaInterface (MODE_GATEWAY) — radio side, handles LoRa mesh
|
||||
@@ -72,10 +85,16 @@
|
||||
#define ADDR_CONF_IFAC_EN 0xD6 // IFAC enable flag (1 byte, 0x73 = enabled)
|
||||
#define ADDR_CONF_IFAC_NAME 0xD7 // Network name (33 bytes, null-terminated)
|
||||
#define ADDR_CONF_IFAC_PASS 0xF8 // Passphrase (33 bytes, null-terminated)
|
||||
// Total: 0x119 (281 bytes — extends beyond 256-byte CONFIG area into
|
||||
#define ADDR_CONF_APP_MARKER0 0x119 // RTNode app marker byte 0
|
||||
#define ADDR_CONF_APP_MARKER1 0x11A // RTNode app marker byte 1
|
||||
#define ADDR_CONF_APP_VERSION 0x11B // RTNode app config version
|
||||
// Total: 0x11C (284 bytes — extends beyond 256-byte CONFIG area into
|
||||
// unused EEPROM gap; safe on ESP32 where EEPROM starts at 824)
|
||||
|
||||
#define BOUNDARY_ENABLE_BYTE 0x73
|
||||
#define BOUNDARY_APP_MARKER0 0x52
|
||||
#define BOUNDARY_APP_MARKER1 0x54
|
||||
#define BOUNDARY_APP_VERSION 0x01
|
||||
|
||||
// ─── Boundary Mode Runtime State ─────────────────────────────────────────────
|
||||
#ifndef BOUNDARY_STATE_DEFINED
|
||||
@@ -115,6 +134,24 @@ extern BoundaryState boundary_state;
|
||||
|
||||
// ─── Boundary Mode EEPROM Load/Save ─────────────────────────────────────────
|
||||
|
||||
inline bool boundary_app_marker_valid() {
|
||||
return EEPROM.read(config_addr(ADDR_CONF_APP_MARKER0)) == BOUNDARY_APP_MARKER0 &&
|
||||
EEPROM.read(config_addr(ADDR_CONF_APP_MARKER1)) == BOUNDARY_APP_MARKER1;
|
||||
}
|
||||
|
||||
inline bool boundary_app_version_matches() {
|
||||
return boundary_app_marker_valid() &&
|
||||
EEPROM.read(config_addr(ADDR_CONF_APP_VERSION)) == BOUNDARY_APP_VERSION;
|
||||
}
|
||||
|
||||
inline void boundary_clear_app_marker() {
|
||||
EEPROM.write(config_addr(ADDR_CONF_APP_MARKER0), 0xFF);
|
||||
EEPROM.write(config_addr(ADDR_CONF_APP_MARKER1), 0xFF);
|
||||
EEPROM.write(config_addr(ADDR_CONF_APP_VERSION), 0xFF);
|
||||
EEPROM.write(config_addr(ADDR_CONF_BMODE), 0xFF);
|
||||
EEPROM.commit();
|
||||
}
|
||||
|
||||
inline void boundary_load_config() {
|
||||
// Check if boundary mode is configured
|
||||
uint8_t bmode = EEPROM.read(config_addr(ADDR_CONF_BMODE));
|
||||
@@ -258,6 +295,9 @@ inline void boundary_save_config() {
|
||||
EEPROM.write(config_addr(ADDR_CONF_IFAC_PASS + i), boundary_state.ifac_passphrase[i]);
|
||||
}
|
||||
EEPROM.write(config_addr(ADDR_CONF_IFAC_PASS + 32), 0x00);
|
||||
EEPROM.write(config_addr(ADDR_CONF_APP_MARKER0), BOUNDARY_APP_MARKER0);
|
||||
EEPROM.write(config_addr(ADDR_CONF_APP_MARKER1), BOUNDARY_APP_MARKER1);
|
||||
EEPROM.write(config_addr(ADDR_CONF_APP_VERSION), BOUNDARY_APP_VERSION);
|
||||
|
||||
EEPROM.commit();
|
||||
}
|
||||
|
||||
12
Config.h
12
Config.h
@@ -20,7 +20,7 @@
|
||||
#define CONFIG_H
|
||||
|
||||
#define MAJ_VERS 0x01
|
||||
#define MIN_VERS 0x55
|
||||
#define MIN_VERS 0x56
|
||||
|
||||
#define MODE_HOST 0x11
|
||||
#define MODE_TNC 0x12
|
||||
@@ -114,7 +114,14 @@
|
||||
#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
|
||||
|
||||
// Increase threshold for specific boards
|
||||
#if BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
#define CSMA_INFR_THRESHOLD_DB 14
|
||||
#else
|
||||
#define CSMA_INFR_THRESHOLD_DB 11
|
||||
#endif
|
||||
|
||||
#define CSMA_RFENV_RECAL_MS 2500
|
||||
#define CSMA_RFENV_RECAL_LIMIT_DB -83
|
||||
bool interference_detected = false;
|
||||
@@ -145,6 +152,7 @@
|
||||
bool hw_ready = false;
|
||||
bool radio_error = false;
|
||||
bool disp_ready = false;
|
||||
bool headless_mode = false;
|
||||
bool pmu_ready = false;
|
||||
bool promisc = false;
|
||||
bool implicit = false;
|
||||
|
||||
10
Display.h
10
Display.h
@@ -97,6 +97,11 @@ extern BoundaryState boundary_state;
|
||||
#define DISP_ADDR 0x3C
|
||||
#define SCL_OLED 18
|
||||
#define SDA_OLED 17
|
||||
#elif BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
#define DISP_RST -1
|
||||
#define DISP_ADDR 0x3C
|
||||
#define SCL_OLED 0
|
||||
#define SDA_OLED 42
|
||||
#elif BOARD_MODEL == BOARD_RAK4631
|
||||
// RAK1921/SSD1306
|
||||
#define DISP_RST -1
|
||||
@@ -334,6 +339,8 @@ bool display_init() {
|
||||
digitalWrite(pin_display_en, HIGH);
|
||||
delay(50);
|
||||
Wire.begin(SDA_OLED, SCL_OLED);
|
||||
#elif BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
Wire.begin(SDA_OLED, SCL_OLED);
|
||||
#elif BOARD_MODEL == BOARD_LORA32_V1_0
|
||||
int pin_display_en = 16;
|
||||
digitalWrite(pin_display_en, LOW);
|
||||
@@ -455,6 +462,9 @@ bool display_init() {
|
||||
#elif BOARD_MODEL == BOARD_HELTEC32_V4
|
||||
disp_mode = DISP_MODE_PORTRAIT;
|
||||
display.setRotation(1);
|
||||
#elif BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
disp_mode = DISP_MODE_LANDSCAPE;
|
||||
display.setRotation(0);
|
||||
#elif BOARD_MODEL == BOARD_HELTEC_T114
|
||||
disp_mode = DISP_MODE_PORTRAIT;
|
||||
display.setRotation(1);
|
||||
|
||||
3
Input.h
3
Input.h
@@ -96,6 +96,9 @@
|
||||
display.display();
|
||||
}
|
||||
#endif
|
||||
headless_led_fast_blink();
|
||||
} else if (display_lock_white) {
|
||||
headless_led_fast_blink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
Power.h
17
Power.h
@@ -147,6 +147,23 @@ float pmu_temperature = PMU_TEMP_MIN-1;
|
||||
bool bat_voltage_dropping = false;
|
||||
float bat_delay_v = 0;
|
||||
float bat_state_change_v = 0;
|
||||
#elif BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
#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
|
||||
|
||||
16
README.md
16
README.md
@@ -2,6 +2,8 @@
|
||||
|
||||
A custom firmware for the **Heltec WiFi LoRa 32 V4** (ESP32-S3 + SX1262) that operates as a **Transport Node** — bridging a local LoRa radio network with a remote TCP/IP backbone (such as [rmap.world](https://rmap.world)) over WiFi.
|
||||
|
||||
This project was primarily developed with the use of AI assistance.
|
||||
|
||||
```
|
||||
Android / Sideband Remote
|
||||
┌──────────┐ ┌────────────┐ Reticulum
|
||||
@@ -52,25 +54,27 @@ This firmware was designed for the **Heltec WiFi LoRa 32 V4**. This board was ch
|
||||
The easiest way to flash a pre-built firmware. You only need Python 3 and a USB cable.
|
||||
|
||||
```bash
|
||||
# Install esptool (one time)
|
||||
pip install esptool
|
||||
|
||||
# Clone this repo (or download just flash.py + the firmware binary)
|
||||
git clone https://github.com/jrl290/RTNode-HeltecV4.git
|
||||
cd RTNode-HeltecV4
|
||||
|
||||
# Download latest firmware from GitHub Releases and flash
|
||||
# (auto-detects V3 vs V4 from flash size)
|
||||
python flash.py --download
|
||||
python flash.py
|
||||
|
||||
# Optional: use your machine's installed esptool instead of the bundled copy
|
||||
python flash.py --use-system-esptool
|
||||
|
||||
# Or specify board explicitly
|
||||
python flash.py --download --board v3
|
||||
python flash.py --download --board v4
|
||||
python flash.py --board v3
|
||||
python flash.py --board v4
|
||||
|
||||
# Or flash a local binary
|
||||
python flash.py --file rtnode_heltec_v4.bin
|
||||
```
|
||||
|
||||
By default, `flash.py` uses the bundled `Release/esptool/esptool.py` for reproducible flashing. Only use `--use-system-esptool` if you explicitly want to override that with a host-installed esptool.
|
||||
|
||||
The flash utility auto-detects whether a V3 or V4 is connected by querying the flash size (8MB = V3, 16MB = V4). You can override with `--board v3` or `--board v4`. It will list all available serial ports and prompt you to choose one. If no ports are detected, you may need to hold the **BOOT** button while pressing **RESET** to enter download mode.
|
||||
|
||||
### Option B: Build from Source (PlatformIO)
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
#include "Utilities.h"
|
||||
|
||||
// CBA Boundary Mode
|
||||
// NOTE: Boundary Mode is the legacy name. This firmware branch intends to
|
||||
// converge on a single Transport Mode, with the BOUNDARY_MODE symbol kept
|
||||
// temporarily as a compatibility shim during cleanup.
|
||||
#ifdef BOUNDARY_MODE
|
||||
#include "BoundaryMode.h"
|
||||
#include "TcpInterface.h"
|
||||
@@ -243,6 +246,9 @@ TcpInterface* local_tcp_interface_ptr = nullptr;
|
||||
// RTC memory flag — survives software reset but not power cycle
|
||||
RTC_NOINIT_ATTR uint32_t boundary_config_request;
|
||||
#define BOUNDARY_CONFIG_MAGIC 0xC0F19A7E
|
||||
// RTC flag to skip config portal on next boot (set when user powers off from WCC)
|
||||
RTC_NOINIT_ATTR uint32_t boundary_skip_config;
|
||||
#define BOUNDARY_SKIP_MAGIC 0x5E1FC0F0
|
||||
|
||||
// Bootloop detection: count rapid reboots in RTC memory.
|
||||
// After BOOTLOOP_THRESHOLD consecutive reboots within BOOTLOOP_WINDOW_MS,
|
||||
@@ -253,6 +259,13 @@ RTC_NOINIT_ATTR uint32_t boundary_config_request;
|
||||
RTC_NOINIT_ATTR uint32_t bootloop_magic;
|
||||
RTC_NOINIT_ATTR uint32_t bootloop_count;
|
||||
RTC_NOINIT_ATTR uint32_t bootloop_first_boot_ms;
|
||||
|
||||
// Node public hash — cached in RTC so the config portal can display it without
|
||||
// needing to start RNS. Populated after the transport destination is created
|
||||
// on a normal boot; survives software reboots into the captive portal.
|
||||
#define NODE_HASH_RTC_MAGIC 0x504B4841UL // "PKHA"
|
||||
RTC_NOINIT_ATTR uint32_t rtc_node_hash_magic;
|
||||
RTC_NOINIT_ATTR char rtc_node_hash_hex[33]; // 32 hex chars + NUL
|
||||
#endif
|
||||
|
||||
#endif // HAS_RNS
|
||||
@@ -350,7 +363,7 @@ void setup() {
|
||||
boot_seq();
|
||||
#endif
|
||||
|
||||
#if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4 && BOARD_MODEL != BOARD_HELTEC32_V3
|
||||
#if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4 && BOARD_MODEL != BOARD_HELTEC32_V3 && BOARD_MODEL != BOARD_MESHADVENTURER_S3
|
||||
// Some boards need to wait until the hardware UART is set up before booting
|
||||
// the full firmware. In the case of the RAK4631, Heltec T114, and Heltec V3,
|
||||
// the line below will wait until a serial connection is actually established
|
||||
@@ -398,7 +411,7 @@ void setup() {
|
||||
#if MODEM == SX1276 || MODEM == SX1278
|
||||
LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy);
|
||||
#elif MODEM == SX1262
|
||||
LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen);
|
||||
LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen, pin_txen);
|
||||
#elif MODEM == SX1280
|
||||
LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen, pin_txen);
|
||||
#endif
|
||||
@@ -473,7 +486,17 @@ void setup() {
|
||||
|
||||
display_unblank();
|
||||
disp_ready = display_init();
|
||||
update_display();
|
||||
if (disp_ready) {
|
||||
update_display();
|
||||
} else {
|
||||
headless_mode = true;
|
||||
Serial.println("[Headless] No display detected — running in headless mode");
|
||||
}
|
||||
#endif
|
||||
|
||||
// LED solid on at boot for V3/V4 boards (with or without display)
|
||||
#if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3
|
||||
headless_led_solid();
|
||||
#endif
|
||||
|
||||
// ── Boundary Mode: check if config portal is needed ──
|
||||
@@ -512,22 +535,66 @@ void setup() {
|
||||
|
||||
// Enter config mode if: first boot with no config, OR button-triggered reboot,
|
||||
// OR bootloop detected
|
||||
bool app_marker_missing = !boundary_app_marker_valid();
|
||||
bool need_config = boundary_needs_config();
|
||||
bool config_requested = (boundary_config_request == BOUNDARY_CONFIG_MAGIC);
|
||||
bool skip_config = (boundary_skip_config == BOUNDARY_SKIP_MAGIC);
|
||||
boundary_config_request = 0; // Clear flag immediately
|
||||
boundary_skip_config = 0; // Clear skip flag immediately
|
||||
|
||||
// Skip flag only suppresses a button-triggered re-entry, not a genuinely
|
||||
// unconfigured device. If there's no config saved, always show the portal.
|
||||
if (skip_config && config_requested) {
|
||||
Serial.println("[Boundary] Skipping config portal — user requested normal boot");
|
||||
config_requested = false;
|
||||
}
|
||||
|
||||
if (need_config || config_requested || bootloop_detected) {
|
||||
if (bootloop_detected) {
|
||||
Serial.println("[Boundary] Entering config portal due to bootloop recovery");
|
||||
} else if (config_requested) {
|
||||
Serial.println("[Boundary] Config mode requested via button hold");
|
||||
} else if (app_marker_missing) {
|
||||
Serial.println("[Boundary] RTNode app marker missing — previous firmware was not RTNode or config is unclaimed");
|
||||
Serial.println("[Boundary] Starting config portal to migrate settings into RTNode");
|
||||
} else {
|
||||
Serial.println("[Boundary] No configuration found — starting config portal");
|
||||
}
|
||||
config_portal_start();
|
||||
// Block here: only run the config portal until user saves and device reboots
|
||||
// Track button state for "off" action (1-3s press = sleep)
|
||||
bool wcc_btn_down = false;
|
||||
uint32_t wcc_btn_down_at = 0;
|
||||
while (config_portal_is_active()) {
|
||||
config_portal_loop();
|
||||
|
||||
// Headless LED: slow ramp breathe effect during WCC mode
|
||||
headless_led_ramp();
|
||||
|
||||
// Button handling: allow 1-3s press to turn off (deep sleep)
|
||||
// Next power-on boots to normal mode since boundary_config_request is cleared
|
||||
#if HAS_INPUT
|
||||
{
|
||||
int btn = digitalRead(pin_btn_usr1);
|
||||
if (btn == LOW && !wcc_btn_down) {
|
||||
wcc_btn_down = true;
|
||||
wcc_btn_down_at = millis();
|
||||
} else if (btn == HIGH && wcc_btn_down) {
|
||||
uint32_t held = millis() - wcc_btn_down_at;
|
||||
wcc_btn_down = false;
|
||||
if (held >= 700 && held <= 5000) {
|
||||
Serial.println("[Boundary] Button press in WCC mode — powering off");
|
||||
boundary_skip_config = BOUNDARY_SKIP_MAGIC; // Skip config on next boot
|
||||
headless_led_off();
|
||||
config_portal_stop();
|
||||
#if HAS_SLEEP
|
||||
sleep_now();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MCU_VARIANT == MCU_ESP32
|
||||
esp_task_wdt_reset();
|
||||
#endif
|
||||
@@ -867,6 +934,17 @@ void setup() {
|
||||
*/
|
||||
RNS::Destination destination(RNS::Transport::identity(), RNS::Type::Destination::IN, RNS::Type::Destination::SINGLE, "rnstransport", "local");
|
||||
|
||||
// Cache this node's destination hash in RTC memory so the captive-portal
|
||||
// config page can show it without needing RNS to be running.
|
||||
{
|
||||
std::string h = destination.hash().toHex();
|
||||
size_t len = h.length();
|
||||
if (len > 32) len = 32;
|
||||
memcpy(rtc_node_hash_hex, h.c_str(), len);
|
||||
rtc_node_hash_hex[len] = '\0';
|
||||
rtc_node_hash_magic = NODE_HASH_RTC_MAGIC;
|
||||
}
|
||||
|
||||
HEAD("RNS is READY!", RNS::LOG_TRACE);
|
||||
#ifdef BOUNDARY_MODE
|
||||
HEAD("*** BOUNDARY MODE ACTIVE ***", RNS::LOG_TRACE);
|
||||
@@ -1508,7 +1586,7 @@ void serial_callback(uint8_t sbyte) {
|
||||
if (txp > 13) txp = 13;
|
||||
#endif
|
||||
#else
|
||||
if (txp > 17) txp = 17;
|
||||
if (txp > 20) txp = 20;
|
||||
#endif
|
||||
|
||||
lora_txp = txp;
|
||||
@@ -1681,6 +1759,9 @@ void serial_callback(uint8_t sbyte) {
|
||||
eeprom_conf_save();
|
||||
} else if (command == CMD_CONF_DELETE) {
|
||||
eeprom_conf_delete();
|
||||
#ifdef BOUNDARY_MODE
|
||||
boundary_clear_app_marker();
|
||||
#endif
|
||||
} else if (command == CMD_FB_EXT) {
|
||||
#if HAS_DISPLAY == true
|
||||
if (sbyte == 0xFF) {
|
||||
@@ -2511,6 +2592,13 @@ void loop() {
|
||||
if (disp_ready && !display_updating) update_display();
|
||||
#endif
|
||||
|
||||
// LED solid when operational on V3/V4 boards (yield to fast blink during white screen)
|
||||
#if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3
|
||||
if (radio_online && !display_lock_white) {
|
||||
headless_led_solid();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAS_PMU
|
||||
if (pmu_ready) update_pmu();
|
||||
#endif
|
||||
@@ -2558,7 +2646,15 @@ void sleep_now() {
|
||||
#endif
|
||||
#endif
|
||||
#if BOARD_MODEL == BOARD_HELTEC32_V4
|
||||
digitalWrite(LORA_PA_CPS, LOW);
|
||||
headless_led_off();
|
||||
headless_led_detach_pwm();
|
||||
#if LORA_PA_AUTO_DETECT
|
||||
if (sx126x_modem.isKCT8103L()) {
|
||||
digitalWrite(LORA_PA_CTX, LOW);
|
||||
} else {
|
||||
digitalWrite(LORA_PA_CPS, LOW);
|
||||
}
|
||||
#endif
|
||||
digitalWrite(LORA_PA_CSD, LOW);
|
||||
digitalWrite(LORA_PA_PWR_EN, LOW);
|
||||
digitalWrite(Vext, HIGH);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
8
Remote.h
8
Remote.h
@@ -78,6 +78,13 @@ void wifi_remote_start_ap() {
|
||||
void wifi_remote_start_sta() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
#ifdef BOUNDARY_MODE
|
||||
// Boundary mode does not expose static STA addressing in its config flow.
|
||||
// Always return the station interface to DHCP so stale legacy EEPROM data
|
||||
// cannot pin the node to an unintended address.
|
||||
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
#else
|
||||
|
||||
uint8_t ip[4]; bool ip_ok = true;
|
||||
for (uint8_t i = 0; i < 4; i++) { ip[i] = EEPROM.read(config_addr(ADDR_CONF_IP+i)); }
|
||||
if (ip[0]==0x00 && ip[1]==0x00 && ip[2]==0x00 && ip[3]==0x00) { ip_ok = false; }
|
||||
@@ -97,6 +104,7 @@ void wifi_remote_start_sta() {
|
||||
IPAddress dns2(1, 1, 1, 1);
|
||||
WiFi.config(sta_ip, sta_gw, sta_nm, dns1, dns2);
|
||||
}
|
||||
#endif
|
||||
|
||||
delay(100);
|
||||
if (wr_ssid[0] != 0x00) {
|
||||
|
||||
116
Utilities.h
116
Utilities.h
@@ -72,6 +72,10 @@ uint8_t eeprom_read(uint32_t mapped_addr);
|
||||
#endif
|
||||
|
||||
#if HAS_INPUT == true
|
||||
// Forward declarations for headless LED functions (defined later in this file)
|
||||
void headless_led_fast_blink();
|
||||
void headless_led_ramp();
|
||||
void headless_led_off();
|
||||
#include "Input.h"
|
||||
#endif
|
||||
|
||||
@@ -320,6 +324,13 @@ extern RNS::Reticulum reticulum;
|
||||
void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
|
||||
void led_id_on() { }
|
||||
void led_id_off() { }
|
||||
#elif BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
|
||||
void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
|
||||
void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
|
||||
void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
|
||||
void led_id_on() { }
|
||||
void led_id_off() { }
|
||||
#elif BOARD_MODEL == BOARD_LORA32_V2_1
|
||||
void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
|
||||
void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
|
||||
@@ -382,6 +393,79 @@ extern RNS::Reticulum reticulum;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// ── Headless LED indicators (for Heltec V4 without OLED) ─────────────────
|
||||
// Uses LEDC PWM for smooth ramp effects on pin_led_tx (GPIO 35)
|
||||
#if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == MESHADVENTURER_S3
|
||||
#define HEADLESS_LED_CHANNEL 0
|
||||
bool headless_led_pwm_attached = false;
|
||||
|
||||
void headless_led_ensure_pwm() {
|
||||
if (!headless_led_pwm_attached) {
|
||||
ledcSetup(HEADLESS_LED_CHANNEL, 5000, 8); // channel 0, 5kHz, 8-bit
|
||||
ledcAttachPin(pin_led_tx, HEADLESS_LED_CHANNEL);
|
||||
headless_led_pwm_attached = true;
|
||||
}
|
||||
}
|
||||
|
||||
void headless_led_detach_pwm() {
|
||||
if (headless_led_pwm_attached) {
|
||||
ledcDetachPin(pin_led_tx);
|
||||
headless_led_pwm_attached = false;
|
||||
pinMode(pin_led_tx, OUTPUT);
|
||||
}
|
||||
}
|
||||
|
||||
// Solid ON — normal headless operation
|
||||
void headless_led_solid() {
|
||||
headless_led_ensure_pwm();
|
||||
ledcWrite(HEADLESS_LED_CHANNEL, 255);
|
||||
}
|
||||
|
||||
// Fast blink — replaces "white screen" indicator (non-blocking, call from loop)
|
||||
void headless_led_fast_blink() {
|
||||
headless_led_ensure_pwm();
|
||||
static uint32_t last_toggle = 0;
|
||||
static bool on = false;
|
||||
uint32_t now = millis();
|
||||
if (now - last_toggle >= 100) { // 5Hz blink
|
||||
last_toggle = now;
|
||||
on = !on;
|
||||
ledcWrite(HEADLESS_LED_CHANNEL, on ? 255 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Slow ramp in/out — breathe effect for WiFi Captive Configure mode
|
||||
void headless_led_ramp() {
|
||||
headless_led_ensure_pwm();
|
||||
static uint32_t last_step = 0;
|
||||
static uint8_t brightness = 0;
|
||||
static int8_t direction = 1;
|
||||
uint32_t now = millis();
|
||||
if (now - last_step >= 10) { // ~100 steps/sec, full cycle ~5 seconds
|
||||
last_step = now;
|
||||
brightness += direction;
|
||||
if (brightness >= 255) { brightness = 255; direction = -1; }
|
||||
if (brightness == 0) { direction = 1; }
|
||||
ledcWrite(HEADLESS_LED_CHANNEL, brightness);
|
||||
}
|
||||
}
|
||||
|
||||
void headless_led_off() {
|
||||
if (headless_led_pwm_attached) {
|
||||
ledcWrite(HEADLESS_LED_CHANNEL, 0);
|
||||
} else {
|
||||
digitalWrite(pin_led_tx, LOW);
|
||||
}
|
||||
}
|
||||
#else
|
||||
void headless_led_ensure_pwm() {}
|
||||
void headless_led_detach_pwm() {}
|
||||
void headless_led_solid() {}
|
||||
void headless_led_fast_blink() {}
|
||||
void headless_led_ramp() {}
|
||||
void headless_led_off() {}
|
||||
#endif
|
||||
|
||||
void hard_reset(void) {
|
||||
#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
|
||||
wdt_enable(WDTO_15MS);
|
||||
@@ -1299,11 +1383,33 @@ int getTxPower() {
|
||||
}
|
||||
|
||||
#if HAS_LORA_PA
|
||||
#if BOARD_MODEL == BOARD_HELTEC32_V4
|
||||
bool pa_values_determined = false;
|
||||
int tx_gain[PA_GAIN_POINTS] = {100};
|
||||
#else
|
||||
bool pa_values_determined = true;
|
||||
const int tx_gain[PA_GAIN_POINTS] = {PA_GAIN_VALUES};
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extern uint8_t lora_pa_model;
|
||||
void determine_pa_values() {
|
||||
#if BOARD_MODEL == BOARD_HELTEC32_V4
|
||||
if (lora_pa_model == LORA_PA_GC1109) {
|
||||
for (int i=0; i < PA_GAIN_POINTS; i++) { tx_gain[i] = PA_GC1109_VALUES[i]; }
|
||||
pa_values_determined = true;
|
||||
for (int i=0; i < PA_GAIN_POINTS; i++) { Serial.print(" "); Serial.printf("%d", tx_gain[i]); }
|
||||
} else if (lora_pa_model == LORA_PA_KCT8103L) {
|
||||
for (int i=0; i < PA_GAIN_POINTS; i++) { tx_gain[i] = PA_KCT8103L_VALUES[i]; }
|
||||
pa_values_determined = true;
|
||||
for (int i=0; i < PA_GAIN_POINTS; i++) { Serial.print(" "); Serial.printf("%d", tx_gain[i]); }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int map_target_power_to_modem_output(int target_tx_power) {
|
||||
#if HAS_LORA_PA
|
||||
if (!pa_values_determined) { determine_pa_values(); }
|
||||
int modem_output_dbm = -9;
|
||||
for (int i = 0; i < PA_GAIN_POINTS; i++) {
|
||||
int gain = tx_gain[i];
|
||||
@@ -1696,10 +1802,12 @@ bool eeprom_model_valid() {
|
||||
if (model == MODEL_C5 || model == MODEL_CA) {
|
||||
#elif BOARD_MODEL == BOARD_HELTEC32_V4
|
||||
if (model == MODEL_C8) {
|
||||
#elif BOARD_MODEL == BOARD_HELTEC_T114
|
||||
if (model == MODEL_C6 || model == MODEL_C7) {
|
||||
#elif BOARD_MODEL == BOARD_RAK4631
|
||||
if (model == MODEL_11 || model == MODEL_12) {
|
||||
#elif BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
if (model == MODEL_C8) {
|
||||
#elif BOARD_MODEL == BOARD_HELTEC_T114
|
||||
if (model == MODEL_C6 || model == MODEL_C7) {
|
||||
#elif BOARD_MODEL == BOARD_RAK4631
|
||||
if (model == MODEL_11 || model == MODEL_12) {
|
||||
#elif BOARD_MODEL == BOARD_HUZZAH32
|
||||
if (model == MODEL_FF) {
|
||||
#elif BOARD_MODEL == BOARD_GENERIC_ESP32
|
||||
|
||||
@@ -121,6 +121,8 @@ def device_provision(env):
|
||||
env.Execute("rnodeconf --product b1 --model b9 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT"))
|
||||
elif variant in ("heltec32v4", "heltec32v4_local", "heltec32v4_boundary", "heltec32v4_boundary_local"):
|
||||
env.Execute("rnodeconf --product b1 --model b9 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT"))
|
||||
elif variant in ("meshadventurer_s3_boundary"):
|
||||
env.Execute("rnodeconf --product f0 --model fe --hwrev 1 --rom " + env.subst("$UPLOAD_PORT"))
|
||||
elif variant in ("rak4631", "rak4631_local"):
|
||||
env.Execute("rnodeconf --product 10 --model 12 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT"))
|
||||
elif variant in ("heltec_t114", "heltec_t114_local"):
|
||||
|
||||
16
flash.py
16
flash.py
@@ -323,8 +323,6 @@ def find_esptool():
|
||||
usually the newest version), then fall back to the bundled script.
|
||||
"""
|
||||
# 1. pip-installed esptool on PATH
|
||||
if shutil.which("esptool.py"):
|
||||
return ["esptool.py"]
|
||||
if shutil.which("esptool"):
|
||||
return ["esptool"]
|
||||
|
||||
@@ -851,8 +849,8 @@ def flash_firmware(firmware_path, port, esptool_cmd, baud=None,
|
||||
flash_addr = f"0x{APP_ADDR:x}"
|
||||
print(f" Detected: app-only binary -> flash at {flash_addr}")
|
||||
|
||||
before_arg = "no_reset" if no_reset_before else "default_reset"
|
||||
after_arg = "no_reset" if no_hard_reset else "hard_reset"
|
||||
before_arg = "no_reset" if no_reset_before else "default-reset"
|
||||
after_arg = "no_reset" if no_hard_reset else "hard-reset"
|
||||
|
||||
cmd = esptool_cmd + [
|
||||
"--chip", CHIP,
|
||||
@@ -860,14 +858,14 @@ def flash_firmware(firmware_path, port, esptool_cmd, baud=None,
|
||||
"--baud", baud,
|
||||
"--before", before_arg,
|
||||
"--after", after_arg,
|
||||
"write_flash",
|
||||
"write-flash",
|
||||
"-z",
|
||||
"--flash_mode", mode,
|
||||
"--flash_freq", FLASH_FREQ,
|
||||
"--flash_size", flash_size,
|
||||
"--flash-mode", mode,
|
||||
"--flash-freq", FLASH_FREQ,
|
||||
"--flash-size", flash_size,
|
||||
]
|
||||
if verify:
|
||||
cmd.append("--verify")
|
||||
cmd.append("--no-diff-verify")
|
||||
cmd += [flash_addr, firmware_path]
|
||||
|
||||
print("Running: " + " ".join(cmd[-8:]))
|
||||
|
||||
0
lib/microReticulum/library.properties
Normal file → Executable file
0
lib/microReticulum/library.properties
Normal file → Executable file
168
platformio.ini
168
platformio.ini
@@ -9,7 +9,6 @@
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
; Change source and include directories to root of project since RNode places them here
|
||||
include_dir = .
|
||||
src_dir = .
|
||||
|
||||
@@ -19,37 +18,23 @@ monitor_speed = 115200
|
||||
upload_speed = 460800
|
||||
build_flags =
|
||||
-Wall
|
||||
;-Wextra
|
||||
-Wno-missing-field-initializers
|
||||
-Wno-format
|
||||
-I.
|
||||
; CBA Define following to disable DEBUG build
|
||||
;-DNDEBUG
|
||||
; CBA Define following to include RNS stack
|
||||
-DHAS_RNS
|
||||
-DRNS_USE_FS
|
||||
-DRNS_PERSIST_PATHS
|
||||
-DMSGPACK_USE_BOOST=OFF
|
||||
; CBA Define following to disable LFS asserts
|
||||
;-DLFS_NO_ASSERT
|
||||
; ???
|
||||
;-DLFS_YES_TRACE
|
||||
; ???
|
||||
;-DCORE_DEBUG_LEVEL=5
|
||||
; ??? NO
|
||||
;-DLOG_LOCAL_LEVEL=5
|
||||
;-DCONFIG_LOG_DEFAULT_LEVEL=5
|
||||
lib_deps =
|
||||
ArduinoJson@^7.4.2
|
||||
MsgPack@^0.4.2
|
||||
adafruit/Adafruit SSD1306@^2.5.9
|
||||
https://github.com/attermann/Crypto.git
|
||||
; Exclude directories in root from sources
|
||||
build_src_filter = +<*> -<variants/>
|
||||
extra_scripts = pre:extra_script.py
|
||||
|
||||
[env:rnode-ng-20]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-lora32-v2
|
||||
custom_variant = ng20
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -60,10 +45,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.12.0
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:rnode-ng-21]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-lora32-v21
|
||||
custom_variant = ng21
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -74,10 +59,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.12.0
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-t-beam]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-t-beam
|
||||
custom_variant = tbeam
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -88,9 +73,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-t-beam-sx1262]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-t-beam
|
||||
custom_variant = tbeam_sx1262
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -102,9 +88,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-t-beam-supreme]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-t-beam
|
||||
custom_variant = tbeam_supreme
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -118,9 +105,10 @@ lib_deps =
|
||||
XPowersLib@^0.2.1
|
||||
Adafruit_SH110X
|
||||
adafruit/Adafruit SH110X@^2.1.14
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:lilygo-t3-s3]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = lilygo-t3-s3
|
||||
custom_variant = t3s3
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -132,9 +120,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:lilygo-t3-s3-sx127x]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = lilygo-t3-s3
|
||||
custom_variant = t3s3_sx127x
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -146,9 +135,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:lilygo-t3-s3-sx1280-pa]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = lilygo-t3-s3
|
||||
custom_variant = t3s3_sx1280_pa
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -160,36 +150,33 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:lilygo-t-deck]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = esp32-s3-devkitc-1
|
||||
custom_variant = tdeck
|
||||
board_build.filesystem = littlefs
|
||||
; Flash / memory layout
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
board_build.partitions = default_16MB.csv
|
||||
; Enable PSRAM + correct flash mode
|
||||
board_build.flash_mode = qio
|
||||
board_build.psram_type = opi
|
||||
board_build.arduino.memory_type = qio_opi
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DBOARD_MODEL=BOARD_TDECK
|
||||
-DBOARD_HAS_PSRAM=1
|
||||
-DBOARD_HAS_PSRAM=1
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DCORE_DEBUG_LEVEL=1
|
||||
; Enable UARDUINO_ USB_ CDC_ ON_ BOOT will start printing and wait for terminal access during startup
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
; Enable UARDUINO_USB_CDC_ON_BOOT will turn off printing and will not block when using the battery
|
||||
; -UARDUINO_USB_CDC_ON_BOOT
|
||||
-DCORE_DEBUG_LEVEL=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-lora32-v1]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-lora32-v1
|
||||
custom_variant = lora32v10
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -200,9 +187,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-lora32-v2]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-lora32-v2
|
||||
custom_variant = lora32v20
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -213,9 +201,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-lora32-v2-extled]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-lora32-v2
|
||||
custom_variant = lora32v20_extled
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -227,9 +216,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-lora32-v21]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-lora32-v21
|
||||
custom_variant = lora32v21
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -240,9 +230,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-lora32-v21-extled]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-lora32-v21
|
||||
custom_variant = lora32v21_extled
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -254,9 +245,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-lora32-v21-tcxo]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-lora32-v21
|
||||
custom_variant = lora32v21_extled
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -268,9 +260,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:heltec_wifi_lora_32_V2]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = heltec_wifi_lora_32_V2
|
||||
custom_variant = heltec32v2
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -281,9 +274,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:heltec_wifi_lora_32_V2-extled]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = heltec_wifi_lora_32_V2
|
||||
custom_variant = heltec32v2_extled
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -295,9 +289,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:heltec_wifi_lora_32_V3]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = heltec_wifi_lora_32_V3
|
||||
custom_variant = heltec32v3
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -308,16 +303,13 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:heltec_V3_boundary]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = heltec_wifi_lora_32_V3
|
||||
custom_variant = heltec32v3
|
||||
board_build.filesystem = littlefs
|
||||
; Flash / memory layout for 8MB flash
|
||||
; PSRAM: V3 ESP32-S3FN8 has NO PSRAM — firmware detects this at runtime
|
||||
; and falls back to internal SRAM for TLSF pool.
|
||||
; BOARD_HAS_PSRAM tells Arduino to *attempt* psramInit(); harmless if absent.
|
||||
board_upload.flash_size = 8MB
|
||||
board_upload.maximum_size = 8388608
|
||||
board_build.partitions = default_8MB.csv
|
||||
@@ -330,21 +322,18 @@ build_flags =
|
||||
-DBOARD_MODEL=BOARD_HELTEC32_V3
|
||||
-DBOARD_HAS_PSRAM=1
|
||||
-DBOUNDARY_MODE
|
||||
;-DNDEBUG
|
||||
-DRNS_USE_TLSF=1
|
||||
-DRNS_USE_ALLOCATOR=1
|
||||
; --- Boundary mode defaults (override via EEPROM at runtime) ---
|
||||
; TCP server mode (0=server, 1=client)
|
||||
-DBOUNDARY_TCP_MODE=0
|
||||
; TCP listen/connect port
|
||||
-DBOUNDARY_TCP_PORT=4242
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:heltec_wifi_lora_32_V4]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = esp32-s3-devkitc-1
|
||||
custom_variant = heltec32v4
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -356,13 +345,41 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:meshadventurer_S3_boundary]
|
||||
platform = espressif32@6.12.0
|
||||
board = esp32-s3-devkitc-1
|
||||
custom_variant = meshadventurer_s3_boundary
|
||||
board_build.filesystem = littlefs
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
board_build.partitions = default_16MB.csv
|
||||
board_build.flash_mode = qio
|
||||
board_build.psram_type = qio
|
||||
board_build.arduino.memory_type = qio_qspi
|
||||
monitor_speed = 921600
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DBOARD_MODEL=BOARD_MESHADVENTURER_S3
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DBOARD_HAS_PSRAM=1
|
||||
-DBOUNDARY_MODE
|
||||
-DRNS_USE_TLSF=1
|
||||
-DRNS_USE_ALLOCATOR=1
|
||||
-DBOUNDARY_TCP_MODE=0
|
||||
-DBOUNDARY_TCP_PORT=4242
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:heltec_V4_boundary]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = esp32-s3-devkitc-1
|
||||
custom_variant = heltec32v4_boundary
|
||||
board_build.filesystem = littlefs
|
||||
; Flash / memory layout for 16MB flash + 2MB PSRAM
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
board_build.partitions = default_16MB.csv
|
||||
@@ -376,24 +393,18 @@ build_flags =
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-DBOARD_HAS_PSRAM=1
|
||||
-DBOUNDARY_MODE
|
||||
;-DNDEBUG
|
||||
-DRNS_USE_TLSF=1
|
||||
-DRNS_USE_ALLOCATOR=1
|
||||
; --- Boundary mode defaults (override via EEPROM at runtime) ---
|
||||
; TCP server mode (0=server, 1=client)
|
||||
-DBOUNDARY_TCP_MODE=0
|
||||
; TCP listen/connect port
|
||||
-DBOUNDARY_TCP_PORT=4242
|
||||
; Backbone host for client mode (empty = server mode)
|
||||
; -DBOUNDARY_BACKBONE_HOST=\"192.168.1.100\"
|
||||
; -DBOUNDARY_BACKBONE_PORT=4242
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:heltec_V4_boundary-local]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = esp32-s3-devkitc-1
|
||||
custom_variant = heltec32v4_boundary_local
|
||||
board_build.filesystem = littlefs
|
||||
@@ -414,10 +425,11 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:featheresp32]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = featheresp32
|
||||
custom_variant = featheresp32
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -428,9 +440,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:seeed_xiao_esp32s3]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = seeed_xiao_esp32s3
|
||||
custom_variant = xiao_esp32s3
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -441,9 +454,10 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:generic-esp32]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = esp32dev
|
||||
custom_variant = esp32_generic
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -454,6 +468,7 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:wiscore_rak4631]
|
||||
platform = nordicnrf52
|
||||
@@ -471,11 +486,10 @@ build_flags =
|
||||
-DRNS_USE_ALLOCATOR=1
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
|
||||
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:ttgo-t-beam-local]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-t-beam
|
||||
upload_speed = 460800
|
||||
custom_variant = tbeam_local
|
||||
@@ -485,16 +499,15 @@ build_flags =
|
||||
${env.build_flags}
|
||||
-fexceptions
|
||||
-DBOARD_MODEL=BOARD_TBEAM
|
||||
; CBA TEST
|
||||
;-DUSE_FLASHFS=1
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
Adafruit_SPIFlash=symlink://../Adafruit_SPIFlash
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:ttgo-lora32-v21-local]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = ttgo-lora32-v21
|
||||
upload_speed = 460800
|
||||
custom_variant = lora32v21_local
|
||||
@@ -506,10 +519,11 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:heltec_wifi_lora_32_V4-local]
|
||||
platform = espressif32
|
||||
platform = espressif32@6.12.0
|
||||
board = esp32-s3-devkitc-1
|
||||
custom_variant = heltec32v4_local
|
||||
board_build.partitions = no_ota.csv
|
||||
@@ -521,6 +535,7 @@ build_flags =
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
XPowersLib@^0.2.1
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:wiscore_rak4631-local]
|
||||
@@ -535,17 +550,15 @@ build_flags =
|
||||
-I variants/rak4630
|
||||
-fexceptions
|
||||
-DBOARD_MODEL=BOARD_RAK4631
|
||||
; CBA TEST
|
||||
-DRNS_USE_TLSF=1
|
||||
-DRNS_USE_ALLOCATOR=1
|
||||
;-DUSE_FLASHFS=1
|
||||
build_unflags = -fno-exceptions
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
Adafruit_SPIFlash=symlink://../Adafruit_SPIFlash
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
[env:heltec_t114_local]
|
||||
;upload_port = /dev/cu.usbmodem1101
|
||||
platform = nordicnrf52
|
||||
board = nrf52840_dk_adafruit
|
||||
custom_variant = heltec_t114_local
|
||||
@@ -555,11 +568,10 @@ build_flags =
|
||||
${env.build_flags}
|
||||
-fexceptions
|
||||
-DBOARD_MODEL=BOARD_HELTEC_T114
|
||||
; CBA TEST
|
||||
-DRNS_USE_TLSF=1
|
||||
-DRNS_USE_ALLOCATOR=1
|
||||
build_unflags = -fno-exceptions
|
||||
lib_deps =
|
||||
${env.lib_deps}
|
||||
https://github.com/liamcottle/esp8266-oled-ssd1306#e16cee124fe26490cb14880c679321ad8ac89c95
|
||||
adafruit/Adafruit NeoPixel@^1.12.0
|
||||
adafruit/Adafruit NeoPixel@^1.15.4
|
||||
|
||||
2841
rnodethv3.log
2841
rnodethv3.log
File diff suppressed because it is too large
Load Diff
125
sx126x.cpp
125
sx126x.cpp
@@ -96,13 +96,21 @@
|
||||
#define SPI spiModem
|
||||
#endif
|
||||
|
||||
#if HAS_LORA_PA
|
||||
uint8_t lora_pa_model = LORA_PA_MODEL;
|
||||
#endif
|
||||
|
||||
#if HAS_LORA_LNA
|
||||
int lora_lna_gain = LORA_LNA_GAIN;
|
||||
#endif
|
||||
|
||||
extern SPIClass SPI;
|
||||
|
||||
#define MAX_PKT_LENGTH 255
|
||||
|
||||
sx126x::sx126x() :
|
||||
_spiSettings(16E6, MSBFIRST, SPI_MODE0),
|
||||
_ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN), _busy(LORA_DEFAULT_BUSY_PIN), _rxen(LORA_DEFAULT_RXEN_PIN),
|
||||
_ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN), _busy(LORA_DEFAULT_BUSY_PIN), _rxen(LORA_DEFAULT_RXEN_PIN), _txen(LORA_DEFAULT_TXEN_PIN),
|
||||
_frequency(0),
|
||||
_txp(0),
|
||||
_sf(0x07),
|
||||
@@ -126,7 +134,7 @@ bool sx126x::preInit() {
|
||||
pinMode(_ss, OUTPUT);
|
||||
digitalWrite(_ss, HIGH);
|
||||
|
||||
#if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3
|
||||
#if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3 || BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs);
|
||||
#elif BOARD_MODEL == BOARD_TECHO
|
||||
SPI.setPins(pin_miso, pin_sclk, pin_mosi);
|
||||
@@ -184,6 +192,7 @@ uint8_t ISR_VECT sx126x::singleTransfer(uint8_t opcode, uint16_t address, uint8_
|
||||
|
||||
void sx126x::rxAntEnable() {
|
||||
if (_rxen != -1) { digitalWrite(_rxen, HIGH); }
|
||||
if (_txen != -1) { digitalWrite(_txen, LOW); }
|
||||
}
|
||||
|
||||
void sx126x::loraMode() {
|
||||
@@ -274,6 +283,21 @@ void sx126x::setPacketParams(long preamble_symbols, uint8_t headermode, uint8_t
|
||||
buf[7] = 0x00;
|
||||
buf[8] = 0x00;
|
||||
executeOpcode(OP_PACKET_PARAMS_6X, buf, 9);
|
||||
|
||||
// SX1262 errata section 15.4: IQ polarity is inverted compared to
|
||||
// SX1276. The SetPacketParams command resets register 0x0736 to an
|
||||
// incorrect default. For standard IQ (no inversion), bit 2 must be
|
||||
// SET after every SetPacketParams call. For inverted IQ, bit 2 must
|
||||
// be CLEARED. Without this fix, LoRa RX demodulation fails silently
|
||||
// while TX continues to work.
|
||||
uint8_t iqreg = readRegister(0x0736);
|
||||
if (buf[5] == 0x00) {
|
||||
// Standard IQ: set bit 2
|
||||
writeRegister(0x0736, iqreg | 0x04);
|
||||
} else {
|
||||
// Inverted IQ: clear bit 2
|
||||
writeRegister(0x0736, iqreg & ~0x04);
|
||||
}
|
||||
}
|
||||
|
||||
void sx126x::reset(void) {
|
||||
@@ -286,6 +310,22 @@ void sx126x::reset(void) {
|
||||
}
|
||||
}
|
||||
|
||||
void sx126x::setDCDCRegulator(void) {
|
||||
// Documentation
|
||||
// 5. Power Distribution -> 5.1 Selecting DC-DC Converter or LDO Regulation
|
||||
// 13.1.11 SetRegulatorMode
|
||||
|
||||
uint8_t mode_byte = MODE_STDBY_RC_6X;
|
||||
executeOpcode(OP_STANDBY_6X, &mode_byte, 1);
|
||||
|
||||
// Enable DC-DC regulator for high power operation
|
||||
uint8_t reg_mode = 0x01; // 0x00 = LDO, 0x01 = DC-DC
|
||||
executeOpcode(OP_REGULATOR_MODE_6X, ®_mode, 1);
|
||||
|
||||
delay(5);
|
||||
waitOnBusy();
|
||||
}
|
||||
|
||||
void sx126x::calibrate(void) {
|
||||
// Put in STDBY_RC mode before calibration
|
||||
uint8_t mode_byte = MODE_STDBY_RC_6X;
|
||||
@@ -316,10 +356,21 @@ int sx126x::begin(long frequency) {
|
||||
if (_busy != -1) { pinMode(_busy, INPUT); }
|
||||
if (!_preinit_done) { if (!preInit()) { return false; } }
|
||||
if (_rxen != -1) { pinMode(_rxen, OUTPUT); }
|
||||
if (_txen != -1) { pinMode(_txen, OUTPUT); }
|
||||
|
||||
//TODO: if it works, make it optional
|
||||
//#ifdef SX1262_USE_DCDC_REGULATOR
|
||||
setDCDCRegulator();
|
||||
//#endif
|
||||
|
||||
calibrate();
|
||||
calibrate_image(frequency);
|
||||
enableTCXO();
|
||||
#if HAS_TCXO
|
||||
enableTCXO();
|
||||
//13.1.15 SetRxTxFallbackMode to STDBY_XOSC
|
||||
uint8_t fallback_mode = 0x30; // STDBY_XOSC after TX/RX
|
||||
executeOpcode(OP_RX_TX_FALLBACK_MODE_6X, &fallback_mode, 1);
|
||||
#endif
|
||||
loraMode();
|
||||
standby();
|
||||
|
||||
@@ -336,7 +387,9 @@ int sx126x::begin(long frequency) {
|
||||
setFrequency(frequency);
|
||||
setTxPower(2);
|
||||
enableCrc();
|
||||
writeRegister(REG_LNA_6X, 0x96); // Set LNA boost
|
||||
writeRegister(REG_LNA_6X, 0x96); // Set LNA boosted gain mode
|
||||
// Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity.
|
||||
writeRegister(0x08B5, readRegister(0x08B5) | 0x01);
|
||||
uint8_t basebuf[2] = {0}; // Set base addresses
|
||||
executeOpcode(OP_BUFFER_BASE_ADDR_6X, basebuf, 2);
|
||||
|
||||
@@ -344,7 +397,22 @@ int sx126x::begin(long frequency) {
|
||||
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
|
||||
|
||||
#if HAS_LORA_PA
|
||||
#if LORA_PA_GC1109
|
||||
if (lora_pa_model == LORA_PA_UNKNOWN) {
|
||||
#if BOARD_MODEL == BOARD_HELTEC32_V4
|
||||
|
||||
pinMode(LORA_PA_PWR_EN, OUTPUT);
|
||||
pinMode(LORA_PA_CSD, INPUT);
|
||||
digitalWrite(LORA_PA_PWR_EN, HIGH); delay(5);
|
||||
if (digitalRead(LORA_PA_CSD) == HIGH) {
|
||||
lora_pa_model = LORA_PA_KCT8103L;
|
||||
lora_lna_gain = LORA_LNA_KCT8103L_GAIN;
|
||||
} else {
|
||||
lora_pa_model = LORA_PA_GC1109;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (lora_pa_model == LORA_PA_GC1109) {
|
||||
// Enable Vfem_ctl for supply to
|
||||
// PA power net.
|
||||
pinMode(LORA_PA_PWR_EN, OUTPUT);
|
||||
@@ -369,7 +437,26 @@ int sx126x::begin(long frequency) {
|
||||
// is driven by the SX1262 DIO2
|
||||
// pin directly, so we do not
|
||||
// need to manually raise this.
|
||||
#endif
|
||||
|
||||
} else if (lora_pa_model == LORA_PA_KCT8103L) {
|
||||
// Enable Vfem_ctl for supply to
|
||||
// PA power net.
|
||||
pinMode(LORA_PA_PWR_EN, OUTPUT);
|
||||
digitalWrite(LORA_PA_PWR_EN, HIGH);
|
||||
|
||||
// Enable KCT8103L chip
|
||||
pinMode(LORA_PA_CSD, OUTPUT);
|
||||
digitalWrite(LORA_PA_CSD, HIGH);
|
||||
|
||||
// Enable receive LNA
|
||||
pinMode(LORA_PA_CTX, OUTPUT);
|
||||
digitalWrite(LORA_PA_CTX, LOW);
|
||||
|
||||
// On Heltec V4.3, the PA CPS pin
|
||||
// is driven by the SX1262 DIO2
|
||||
// pin directly, so we do not
|
||||
// need to manually raise this.
|
||||
}
|
||||
#endif
|
||||
|
||||
return 1;
|
||||
@@ -379,15 +466,19 @@ void sx126x::end() { sleep(); SPI.end(); _preinit_done = false; }
|
||||
|
||||
int sx126x::beginPacket(int implicitHeader) {
|
||||
#if HAS_LORA_PA
|
||||
#if LORA_PA_GC1109
|
||||
if (lora_pa_model == LORA_PA_GC1109) {
|
||||
// Enable PA CPS for transmit
|
||||
// digitalWrite(LORA_PA_CPS, HIGH);
|
||||
// Disabled since we're keeping it
|
||||
// on permanently as long as the
|
||||
// radio is powered up.
|
||||
#endif
|
||||
} else if (lora_pa_model == LORA_PA_KCT8103L) {
|
||||
digitalWrite(LORA_PA_CTX, HIGH);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_txen != -1) { digitalWrite(_txen, HIGH); } //Set TXen high when transmitting
|
||||
|
||||
standby();
|
||||
if (implicitHeader) { implicitHeaderMode(); }
|
||||
else { explicitHeaderMode(); }
|
||||
@@ -401,6 +492,9 @@ int sx126x::beginPacket(int implicitHeader) {
|
||||
|
||||
int sx126x::endPacket() {
|
||||
setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
|
||||
|
||||
if (_rxen != -1) { digitalWrite(_rxen, LOW); } //Set RXen low when transmitting
|
||||
|
||||
uint8_t timeout[3] = {0}; // Put in single TX mode
|
||||
executeOpcode(OP_TX_6X, timeout, 3);
|
||||
|
||||
@@ -473,7 +567,7 @@ int ISR_VECT sx126x::currentRssi() {
|
||||
executeOpcodeRead(OP_CURRENT_RSSI_6X, &byte, 1);
|
||||
int rssi = -(int(byte)) / 2;
|
||||
#if HAS_LORA_LNA
|
||||
rssi -= LORA_LNA_GAIN;
|
||||
rssi -= lora_lna_gain;
|
||||
#endif
|
||||
return rssi;
|
||||
}
|
||||
@@ -489,7 +583,7 @@ int ISR_VECT sx126x::packetRssi() {
|
||||
executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3);
|
||||
int pkt_rssi = -buf[0] / 2;
|
||||
#if HAS_LORA_LNA
|
||||
pkt_rssi -= LORA_LNA_GAIN;
|
||||
pkt_rssi -= lora_lna_gain;
|
||||
#endif
|
||||
return pkt_rssi;
|
||||
}
|
||||
@@ -596,7 +690,7 @@ void sx126x::onReceive(void(*callback)(int)){
|
||||
|
||||
void sx126x::receive(int size) {
|
||||
#if HAS_LORA_PA
|
||||
#if LORA_PA_GC1109
|
||||
if (lora_pa_model == LORA_PA_GC1109) {
|
||||
// Disable PA CPS for receive
|
||||
// digitalWrite(LORA_PA_CPS, LOW);
|
||||
// That turned out to be a bad idea.
|
||||
@@ -604,7 +698,9 @@ void sx126x::receive(int size) {
|
||||
// on and off too quickly. We'll keep
|
||||
// it on permanently, as long as the
|
||||
// radio is powered up.
|
||||
#endif
|
||||
} else if (lora_pa_model == LORA_PA_KCT8103L) {
|
||||
digitalWrite(LORA_PA_CTX, LOW);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (size > 0) {
|
||||
@@ -643,6 +739,8 @@ void sx126x::enableTCXO() {
|
||||
uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
|
||||
#elif BOARD_MODEL == BOARD_HELTEC32_V4
|
||||
uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
|
||||
#elif BOARD_MODEL == BOARD_MESHADVENTURER_S3
|
||||
uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
|
||||
#endif
|
||||
executeOpcode(OP_DIO3_TCXO_CTRL_6X, buf, 4);
|
||||
#endif
|
||||
@@ -770,12 +868,13 @@ void sx126x::setSyncWord(uint16_t sw) {
|
||||
writeRegister(REG_SYNC_WORD_LSB_6X, 0x24);
|
||||
}
|
||||
|
||||
void sx126x::setPins(int ss, int reset, int dio0, int busy, int rxen) {
|
||||
void sx126x::setPins(int ss, int reset, int dio0, int busy, int rxen, int txen) {
|
||||
_ss = ss;
|
||||
_reset = reset;
|
||||
_dio0 = dio0;
|
||||
_busy = busy;
|
||||
_rxen = rxen;
|
||||
_txen = txen;
|
||||
}
|
||||
|
||||
void sx126x::dumpRegisters(Stream& out) {
|
||||
|
||||
10
sx126x.h
10
sx126x.h
@@ -74,6 +74,7 @@ public:
|
||||
void disableCrc();
|
||||
void enableTCXO();
|
||||
void disableTCXO();
|
||||
void setDCDCRegulator();
|
||||
|
||||
void rxAntEnable();
|
||||
void loraMode();
|
||||
@@ -92,11 +93,13 @@ public:
|
||||
|
||||
byte random();
|
||||
|
||||
void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN, int rxen = LORA_DEFAULT_RXEN_PIN);
|
||||
void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN, int rxen = LORA_DEFAULT_RXEN_PIN, int txen = LORA_DEFAULT_TXEN_PIN);
|
||||
void setSPIFrequency(uint32_t frequency);
|
||||
|
||||
void dumpRegisters(Stream& out);
|
||||
|
||||
bool isKCT8103L() { return _kct8103l; }
|
||||
|
||||
private:
|
||||
void explicitHeaderMode();
|
||||
void implicitHeaderMode();
|
||||
@@ -107,7 +110,8 @@ public:
|
||||
// Poll for deferred DIO0 interrupt (call from main loop)
|
||||
void pollDio0();
|
||||
|
||||
private: uint8_t readRegister(uint16_t address);
|
||||
private:
|
||||
uint8_t readRegister(uint16_t address);
|
||||
void writeRegister(uint16_t address, uint8_t value);
|
||||
uint8_t singleTransfer(uint8_t opcode, uint16_t address, uint8_t value);
|
||||
|
||||
@@ -125,6 +129,7 @@ private:
|
||||
int _reset;
|
||||
int _dio0;
|
||||
int _rxen;
|
||||
int _txen;
|
||||
int _busy;
|
||||
long _frequency;
|
||||
int _txp;
|
||||
@@ -141,6 +146,7 @@ private:
|
||||
int _fifo_rx_addr_ptr;
|
||||
uint8_t _packet[255];
|
||||
bool _preinit_done;
|
||||
bool _kct8103l;
|
||||
volatile bool _dio0_risen;
|
||||
void (*_onReceive)(int);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user