Merge pull request 'Refactor/lora cli' (#5) from refactor/lora-cli into main
Build of FoxMesh / build-and-test (push) Successful in 2m45s

Reviewed-on: #5
Dieser Commit wurde in Pull Request #5 zusammengeführt.
Dieser Commit ist enthalten in:
tom committed 2026-10-21 00:29:14 +02:00
2 Ursprung 1d22bcb4ce 1a56c456f6
Commit b54a9fc714
9 Dateien geändert
+468 -105

Keine Dateien entsprechen Ihrer Suche

+6
Datei anzeigen
@@ -25,6 +25,11 @@ build_flags =
; -isystem /home/tom/.platformio/packages/framework-espidf/components/esp_libc/platform_include
-Wno-include-next
; --- STRICT MODE FOR PROJECT APPLICATION CODE ONLY ---
#-DGIT_HASH=\"\"${sysenv.GIT_HASH}\"\"
!echo -DGIT_HASH=\\\"$(git rev-parse --short HEAD)\\\"
!echo -DGIT_TAG=\\\"$(git describe --tags --always)\\\"
; These flags are strictly applied ONLY to C++ files inside the src/ directory.
build_src_flags =
; Enables all standard compiler warning flags
@@ -43,3 +48,4 @@ build_src_flags =
-Wno-include-next
; Promotes all warnings to hard compilation errors to guarantee clean code
;-Werror
;-D MATH_COMPARE=1
+26 -1
Datei anzeigen
@@ -19,6 +19,7 @@
#include "esp_log.h"
#include "config.hpp"
#include "lora.hpp"
#include "display.hpp"
static const char* TAG = "CLI";
static i2c* i2c_bus = nullptr;
@@ -91,9 +92,11 @@ void process_command(const char* cmd) {
printf(" freq <hz> - Set dynamic radio frequency (433 / 868 MHz bounds)\r\n");
printf(" bw <62|125|250|500>- Set dynamic LoRa bandwidth in kHz\r\n");
printf(" i2c - Scan the configured I2C bus infrastructure\r\n");
printf(" test-display - Run a comprehensive visual test suite\r\n");
}
else if (strcmp(cmd, "version") == 0) {
printf("%s System - Running Version: %s\r\n", Config::PROJECT_TITLE, Config::PROJECT_VERSION);
printf("%s System - Running Version: %s Tag: " GIT_TAG "\r\n", Config::PROJECT_TITLE, Config::PROJECT_VERSION);
printf("Build Date: " __DATE__ ", Git-Hash: " GIT_HASH "\r\n");
}
else if (strcmp(cmd, "status") == 0) {
printf("--- %s Status ---\r\n", Config::PROJECT_TITLE);
@@ -101,6 +104,23 @@ void process_command(const char* cmd) {
printf("Mute Timer Period : %lu seconds\r\n", (unsigned long)current_mute_time_sec);
printf("Target CPU Board : ESP32\r\n");
printf("ESP-IDF Version : %s\r\n", esp_get_idf_version());
if (lora_system != nullptr) {
// atomar, thread-safe copy of telemtry data (stack allocation)
LoraStateSnapshot snapshot = lora_system->getSnapshot();
printf("LoRa Hardware : %s\r\n", snapshot.isInitialized ? "INITIALIZED" : "FAILED / UNINITIALIZED");
printf("LoRa Transceiver : Mode [%s]\r\n", snapshot.currentModeStr);
printf("LoRa Frequency : %lu Hz\r\n", (unsigned long)snapshot.frequencyHz);
printf("LoRa Bandwidth : %lu kHz\r\n", (unsigned long)snapshot.bandwidthKhz);
printf("LoRa SpreadingFact: SF%u\r\n", snapshot.spreadingFactor);
printf("LoRa Coding Rate : 4/%u\r\n", snapshot.codingRate);
printf("Packets Received : %lu\r\n", (unsigned long)snapshot.totalPacketsReceived);
printf("Packets Transmit : %lu\r\n", (unsigned long)snapshot.totalPacketsTransmitted);
printf("History In Memory : %zu Packets\r\n", snapshot.currentHistoryCount);
} else {
printf("LoRa Snapshot : Error (LoraController Handle uninitialized)\r\n");
}
}
else if (strncmp(cmd, "mute ", 5) == 0) {
int parsed_time = atoi(cmd + 5);
@@ -220,6 +240,11 @@ void process_command(const char* cmd) {
printf("Error: I2C Bus not initialized in CLI!\r\n");
}
}
else if (strcmp(cmd, "test-display") == 0) {
printf("[CLI] Triggering asynchronous OLED font verification...\n");
run_display_font_test(oled_display);
printf("[CLI] Test completed.\n");
}
else {
printf("Unknown command: '%s'. Type 'help' for options.\r\n", cmd);
}
+1 -7
Datei anzeigen
@@ -22,7 +22,7 @@
namespace Config {
// Compile-time constants with strict type enforcement
constexpr const char* PROJECT_TITLE = "FoxMesh";
constexpr const char* PROJECT_VERSION = "1.0.3";
constexpr const char* PROJECT_VERSION = "0.1";
constexpr const char* PROJECT_AUTHORS = "Tom OE3TKT";
// CLI Mute Configuration
@@ -72,12 +72,6 @@ namespace Config {
constexpr uint8_t LORA_PREAMBLE_LENGTH_MSB = 0x00;
constexpr uint8_t LORA_PREAMBLE_LENGTH_LSB = 0x0C; // Semtech Factor Default is 0x08, MeshCom with 0x0C, i.e. 12 symbols
// Modem Config 1: BW 250kHz (0x80), CR 4/6 (0x04), Explicit Header (0x00) -> 0x84
// constexpr uint8_t LORA_MODEM_CONFIG_1 = 0x84; // @TODO: will be removed in v1.1
// Modem Config 2: SF11 (0xB0), TX Continuous off (0x00), CRC On (0x04) -> 0xB4
// constexpr uint8_t LORA_MODEM_CONFIG_2 = 0xB4; // @TODO: will be removed in v1.1
// --- DYNAMIC COMPILE-TIME BIT-SHIFTING EXPRESSIONS ---
// Helper to map bandwidth kHz to Semtech Register Bits (7-4 of RegModemConfig1)
+193 -52
Datei anzeigen
@@ -116,26 +116,139 @@ static const uint8_t font_5x7[][5] = {
{0x00, 0x00, 0x77, 0x00, 0x00}, // 124 |
{0x00, 0x41, 0x36, 0x08, 0x00}, // 125 }
{0x08, 0x08, 0x2a, 0x1c, 0x08}, // 126 ~
// German Umlaute mappings
// {0x7d, 0x11, 0x11, 0x11, 0x7d}, // 127 Ä
// {0x3c, 0x41, 0x41, 0x41, 0x3c}, // 128 Ö
// {0x3d, 0x40, 0x40, 0x40, 0x3d}, // 129 Ü
// {0x20, 0x55, 0x54, 0x54, 0x78}, // 130 ä
// {0x38, 0x54, 0x54, 0x54, 0x38}, // 131 ö
// {0x3c, 0x40, 0x40, 0x20, 0x7c}, // 132 ü
// {0x7f, 0x49, 0x49, 0x36, 0x20} // 133 ß
// new German Umlaute mapping (5x7/5x8 Font Matrix)
{0,0,0,0,0}, //127 not allowed
{0x7d, 0x13, 0x13, 0x13, 0x7d}, // 128 Ä
{0x3c, 0x43, 0x43, 0x43, 0x3c}, // 129 Ö
{0x3d, 0x42, 0x42, 0x42, 0x3d}, // 130 Ü
{0x20, 0x55, 0x54, 0x54, 0x78}, // 131 ä
{0x38, 0x55, 0x55, 0x55, 0x38}, // 132 ö
{0x3c, 0x42, 0x42, 0x22, 0x7c}, // 133 ü
{0x7f, 0x49, 0x49, 0x36, 0x00}, // 134 ß (Letzte Spalte auf 0x00 bereinigt)
// {0x3c, 0x42, 0x51, 0x4a, 0x34} // 134 © Copyright
// {0x3c, 0x42, 0x4d, 0x4d, 0x32} // 135
{0x3c, 0x42, 0x95, 0x95, 0x52}
{0x00, 0x00, 0x00, 0x00, 0x00}, // 127 DEL (Control character)
// --- UTF-8 0xC2 RANGE (ISO-8859-1 Indizes 128 to 191) ---
{0x00, 0x00, 0x00, 0x00, 0x00}, // 128 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 129 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 130 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 131 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 132 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 133 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 134 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 135 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 136 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 137 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 138 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 139 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 140 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 141 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 142 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 143 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 144 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 145 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 146 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 147 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 148 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 149 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 150 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 151 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 152 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 153 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 154 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 155 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 156 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 157 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 158 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 159 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 160 Non-breaking space
{0x00, 0x00, 0x2f, 0x00, 0x00}, // 161 Inverted exclamation mark (¡)
{0x43, 0x4c, 0x78, 0x4c, 0x43}, // 162 Cent sign (¢)
{0x18, 0x7c, 0x14, 0x3c, 0x54}, // 163 Pound sign (£)
{0x6c, 0x6c, 0x6c, 0x6c, 0x6c}, // 164 Currency sign (¤)
{0x30, 0x78, 0x4d, 0x78, 0x30}, // 165 Yen sign (¥)
{0x00, 0x00, 0x77, 0x00, 0x00}, // 166 Broken bar (¦)
{0x00, 0x4d, 0x4d, 0x6c, 0x00}, // 167 Section sign / Paragraph (§)
{0x00, 0x00, 0x01, 0x00, 0x00}, // 168 Diaeresis (¨)
{0x00, 0x15, 0x16, 0x7c, 0x14}, // 169 Copyright sign (©)
{0x00, 0x42, 0x7b, 0x42, 0x42}, // 170 Feminine ordinal indicator (ª)
{0x08, 0x14, 0x2a, 0x14, 0x22}, // 171 Left-pointing double-angle quotation mark («)
{0x04, 0x04, 0x04, 0x04, 0x1c}, // 172 Not sign (¬)
{0x00, 0x08, 0x08, 0x08, 0x00}, // 173 Soft hyphen
{0x00, 0x14, 0x14, 0x14, 0x14}, // 174 Registered sign (®)
{0x00, 0x02, 0x02, 0x02, 0x02}, // 175 Macron (¯)
{0x00, 0x03, 0x03, 0x00, 0x00}, // 176 Degree sign (°)
{0x44, 0x44, 0x5f, 0x44, 0x44}, // 177 Plus-minus sign (±)
{0x00, 0x00, 0x07, 0x04, 0x04}, // 178 Superscript two (²)
{0x00, 0x00, 0x05, 0x05, 0x07}, // 179 Superscript three (³)
{0x02, 0x01, 0x02, 0x04, 0x00}, // 180 Acute accent (´)
{0x30, 0x48, 0x30, 0x08, 0x06}, // 181 Micro sign (µ)
{0x00, 0x6e, 0x6e, 0x6e, 0x00}, // 182 Pilcrow sign / Paragraph sign (¶)
{0x00, 0x00, 0x04, 0x00, 0x00}, // 183 Middle dot (·)
{0x00, 0x40, 0x44, 0x40, 0x40}, // 184 Cedilla (¸)
{0x00, 0x00, 0x01, 0x00, 0x00}, // 185 Superscript one (¹)
{0x00, 0x44, 0x7d, 0x40, 0x00}, // 186 Masculine ordinal indicator (º)
{0x22, 0x14, 0x2a, 0x14, 0x08}, // 187 Right-pointing double-angle quotation mark (»)
{0x1c, 0x22, 0x49, 0x49, 0x30}, // 188 Vulgar fraction one quarter (¼)
{0x38, 0x44, 0x54, 0x24, 0x18}, // 189 Vulgar fraction one half (½)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 190 Vulgar fraction three quarters (¾)
{0x40, 0x00, 0x45, 0x06, 0x00}, // 191 Inverted question mark (¿)
// --- UTF-8 0xC3 RANGE (ISO-8859-1 Indizes 192 to 255) ---
{0x7c, 0x12, 0x11, 0x12, 0x7c}, // 192 Capital A with grave (À)
{0x7c, 0x11, 0x12, 0x14, 0x78}, // 193 Capital A with acute (Á)
{0x7c, 0x15, 0x16, 0x14, 0x7c}, // 194 Capital A with circumflex (Â)
{0x7c, 0x16, 0x19, 0x16, 0x7c}, // 195 Capital A with tilde (Ã)
{0x7c, 0x11, 0x13, 0x11, 0x7c}, // 196 Capital A with umlaut (Ä)
{0x7c, 0x11, 0x15, 0x11, 0x7c}, // 197 Capital A with ring above (Å)
{0x7e, 0x51, 0x7f, 0x41, 0x73}, // 198 Capital AE ligature (Æ)
{0x3e, 0x41, 0x49, 0x22, 0x10}, // 199 Capital C with cedilla (Ç)
{0x7f, 0x4a, 0x49, 0x4a, 0x41}, // 200 Capital E with grave (È)
{0x7f, 0x49, 0x4a, 0x4c, 0x40}, // 201 Capital E with acute (É)
{0x7f, 0x4d, 0x4e, 0x4d, 0x41}, // 202 Capital E with circumflex (Ê)
{0x7f, 0x49, 0x4b, 0x49, 0x41}, // 203 Capital E with umlaut (Ë)
{0x00, 0x42, 0x7d, 0x40, 0x00}, // 204 Capital I with grave (Ì)
{0x00, 0x40, 0x7d, 0x42, 0x00}, // 205 Capital I with acute (Í)
{0x00, 0x46, 0x7d, 0x46, 0x00}, // 206 Capital I with circumflex (Î)
{0x00, 0x45, 0x7d, 0x45, 0x00}, // 207 Capital I with umlaut (Ï)
{0x7f, 0x41, 0x41, 0x22, 0x1c}, // 208 Capital Eth (Ð)
{0x7f, 0x05, 0x06, 0x0c, 0x7f}, // 209 Capital N with tilde (Ñ)
{0x3e, 0x42, 0x41, 0x42, 0x3e}, // 210 Capital O with grave (Ò)
{0x3e, 0x40, 0x41, 0x42, 0x3c}, // 211 Capital O with acute (Ó)
{0x3e, 0x44, 0x45, 0x44, 0x3e}, // 212 Capital O with circumflex (Ô)
{0x3e, 0x45, 0x46, 0x45, 0x3e}, // 213 Capital O with tilde (Õ)
{0x3e, 0x41, 0x43, 0x41, 0x3e}, // 214 Capital O with umlaut (Ö)
{0x22, 0x14, 0x08, 0x14, 0x22}, // 215 Multiplication sign (×)
{0x2e, 0x41, 0x41, 0x41, 0x3a}, // 216 Capital O with stroke (Ø)
{0x3f, 0x42, 0x41, 0x40, 0x3f}, // 217 Capital U with grave (Ù)
{0x3f, 0x40, 0x41, 0x42, 0x3e}, // 218 Capital U with acute (Ú)
{0x3f, 0x44, 0x45, 0x44, 0x3f}, // 219 Capital U with circumflex (Û)
{0x3f, 0x41, 0x43, 0x41, 0x3f}, // 220 Capital U with umlaut (Ü)
{0x03, 0x08, 0x70, 0x0c, 0x03}, // 221 Capital Y with acute (Ý)
{0x7f, 0x49, 0x09, 0x09, 0x06}, // 222 Capital Thorn (Þ)
{0x7f, 0x49, 0x49, 0x36, 0x00}, // 223 Small sharp s / Eszett (ß)
{0x20, 0x56, 0x55, 0x56, 0x78}, // 224 Small a with grave (à)
{0x20, 0x55, 0x56, 0x58, 0x70}, // 225 Small a with acute (á)
{0x20, 0x59, 0x5a, 0x58, 0x78}, // 226 Small a with circumflex (â)
{0x20, 0x5a, 0x5d, 0x5a, 0x78}, // 227 Small a with tilde (ã)
{0x20, 0x55, 0x57, 0x55, 0x78}, // 228 Small a with umlaut (ä)
{0x20, 0x55, 0x59, 0x55, 0x78}, // 229 Small a with ring above (å)
{0x24, 0x54, 0x78, 0x54, 0x5c}, // 230 Small ae ligature (æ)
{0x38, 0x44, 0x4c, 0x54, 0x24}, // 231 Small c with cedilla (ç)
{0x38, 0x56, 0x55, 0x56, 0x18}, // 232 Small e with grave (è)
{0x38, 0x55, 0x56, 0x58, 0x10}, // 233 Small e with acute (é)
{0x38, 0x59, 0x5a, 0x58, 0x18}, // 234 Small e with circumflex (ê)
{0x38, 0x55, 0x57, 0x55, 0x18}, // 235 Small e with umlaut (ë)
{0x00, 0x45, 0x7e, 0x40, 0x00}, // 236 Small i with grave (ì)
{0x00, 0x40, 0x7e, 0x45, 0x00}, // 237 Small i with acute (í)
{0x00, 0x49, 0x7e, 0x49, 0x00}, // 238 Small i with circumflex (î)
{0x00, 0x46, 0x7e, 0x46, 0x00}, // 239 Small i with umlaut (ï)
{0x38, 0x44, 0x44, 0x48, 0x7f}, // 240 Small eth (ð)
{0x7c, 0x09, 0x0a, 0x04, 0x78}, // 241 Small n with tilde (ñ)
{0x38, 0x46, 0x45, 0x46, 0x38}, // 242 Small o with grave (ò)
{0x38, 0x45, 0x46, 0x48, 0x30}, // 243 Small o with acute (ó)
{0x38, 0x4a, 0x4b, 0x4a, 0x38}, // 244 Small o with circumflex (ô)
{0x38, 0x4b, 0x4c, 0x4b, 0x38}, // 245 Small o with tilde (õ)
{0x38, 0x45, 0x47, 0x45, 0x38}, // 246 Small o with umlaut (ö)
{0x08, 0x08, 0x2a, 0x08, 0x08}, // 247 Division sign (÷)
{0x38, 0x44, 0x44, 0x44, 0x38}, // 248 Small o with stroke (ø)
{0x3c, 0x42, 0x41, 0x20, 0x7c}, // 249 Small u with grave (ù)
{0x3c, 0x40, 0x41, 0x22, 0x78}, // 250 Small u with acute (ú)
{0x3c, 0x44, 0x45, 0x24, 0x7c}, // 251 Small u with circumflex (û)
{0x3c, 0x41, 0x43, 0x21, 0x7c}, // 252 Small u with umlaut (ü)
{0x0c, 0x50, 0x52, 0x50, 0x3c}, // 253 Small y with acute (ý)
{0x7f, 0x14, 0x14, 0x08, 0x04}, // 254 Small thorn (þ)
{0x0c, 0x51, 0x53, 0x51, 0x3c} // 255 Small y with umlaut (ÿ)
};
/**
@@ -335,56 +448,48 @@ void DisplayController::run() {
bool validCharacter = false;
const size_t current_char_idx = i; // Store original index for connection logic
// Basic UTF-8 multi-byte decoder for native German Umlaute translation
// Multi-byte UTF-8 decoder targeting the complete ISO-8859-1 (Latin-1) spectrum
if ((c_primary == 0xC3 || c_primary == 0xC2) && (i + 1) < textLength) {
const uint8_t c_secondary = static_cast<uint8_t>(incoming.content[++i]);
switch (c_secondary) {
// 0xC3 Multi-byte compound tokens (fontIndex 128 - 133)
case 0x84: fontIndex = 128; validCharacter = true; break; // Ä
case 0x96: fontIndex = 129; validCharacter = true; break; // Ö
case 0x9C: fontIndex = 130; validCharacter = true; break; // Ü
case 0xA4: fontIndex = 131; validCharacter = true; break; // ä
case 0xB6: fontIndex = 132; validCharacter = true; break; // ö
case 0xBC: fontIndex = 133; validCharacter = true; break; // ü
// 0xC2 Multi-byte compound tokens (fontIndex 134 - 135)
case 0x9F: fontIndex = 134; validCharacter = true; break; // ß
case 0xA9: fontIndex = 135; validCharacter = true; break; // ©
default: i--; // Rollback pointer if multi-byte token was invalid
// Mathematical reconstruction of the raw Latin-1 byte position
// 0xC3 sequence maps to 0xC0-0xFF (c_secondary + 0x40)
// 0xC2 sequence maps directly to 0x80-0xBF
uint8_t iso_char = (c_primary == 0xC3) ? (c_secondary + 0x40) : c_secondary;
// Subtract table offset since font array starts at index 32 (Space)
fontIndex = static_cast<size_t>(iso_char - 32);
validCharacter = true;
}
} else if (c_primary >= 32 && c_primary <= 126) {
// Handle standard printable 7-bit ASCII characters
else if (c_primary >= 32 && c_primary <= 126) {
fontIndex = static_cast<size_t>(c_primary - 32);
validCharacter = true;
}
if (validCharacter && totalBytesWritten < 122) {
uint8_t txBuffer[7];
// copy all 5 font columns in a single, hardware-accelerated burst
txBuffer[0] = 0x40; // Data identifier
txBuffer[1] = font_5x7[fontIndex][0];
txBuffer[2] = font_5x7[fontIndex][1];
txBuffer[3] = font_5x7[fontIndex][2];
txBuffer[4] = font_5x7[fontIndex][3];
txBuffer[5] = font_5x7[fontIndex][4];
// txBuffer[1] = font_5x7[fontIndex][0];
// txBuffer[2] = font_5x7[fontIndex][1];
// txBuffer[3] = font_5x7[fontIndex][2];
// txBuffer[4] = font_5x7[fontIndex][3];
// txBuffer[5] = font_5x7[fontIndex][4];
std::memcpy(&txBuffer[1], font_5x7[fontIndex], 5);
// Check if current character is a potential line segment
if (c_primary == '-' || c_primary == '_' || c_primary == '=') {
bool connect = false;
// Look behind: Direct boolean evaluation in a single line
bool connect = (current_char_idx > 0 && incoming.content[current_char_idx - 1] == static_cast<char>(c_primary));
// Look behind: Check if preceding byte was an identical line segment
if (current_char_idx > 0) {
const char prev = incoming.content[current_char_idx - 1];
if (prev == static_cast<char>(c_primary)) connect = true;
}
// Look ahead: Check if succeeding byte is an identical line segment
// Since 'i' was updated by multi-byte parsing, we look ahead from the current 'i'
if (!connect && (i + 1) < textLength) {
const char next = incoming.content[i + 1];
if (next == static_cast<char>(c_primary)) connect = true;
// Look ahead: Only checks if the look-behind didn't already establish a connection
if (!connect && (i + 1) < textLength && incoming.content[i + 1] == static_cast<char>(c_primary)) {
connect = true;
}
// Seamlessly bridge the 1-pixel gap if part of a multi-character line sequence
txBuffer[6] = connect ? font_5x7[fontIndex][4] : 0x00;
txBuffer[6] = connect ? txBuffer[5] : 0x00;
} else {
txBuffer[6] = 0x00; // Standard 1-pixel spacing for regular text elements
}
@@ -411,3 +516,39 @@ void DisplayController::run() {
}
}
}
/**
* @brief Run a comprehensive visual test suite and clear the screen after 5 seconds.
* @param display Pointer to the active DisplayController instance.
*/
void run_display_font_test(DisplayController* display) {
if (display == nullptr) return;
// Test 1: Standard printable ASCII characters (Lines 0 and 1)
display->printMessage(0, "=== FONT TEST ===", 12);
display->printMessage(1, "ASCII: abcXYZ 123!?", 0);
// Test 2: Native German Umlaute via multi-byte UTF-8 (Lines 2, 3 and 4)
display->printMessage(2, "Umlaute: AE-OE-UE-ss", 0);
display->printMessage(3, "Outputs: Ä Ö Ü ß", 0);
display->printMessage(4, "Outputs: ä ö ü", 0);
// Test 3: Newly supported Latin-1 symbols (Line 5)
display->printMessage(5, "Symbols: 23°C © 2026", 0);
// Test 4: Dynamic line-segment connection logic (Lines 6 and 7)
display->printMessage(6, "--------------------", 0);
display->printMessage(7, "Line test: ___ === _", 0);
// Let the user look at the test pattern for 60 seconds
vTaskDelay(pdMS_TO_TICKS(60000));
// Print a quick shutdown notice to the CLI console before restarting
printf("\r\n[SYSTEM] Visual test completed. Triggering hardware reset now...\n");
vTaskDelay(pdMS_TO_TICKS(1000));
// Perform a clean software-controlled hardware restart of the ESP32
esp_restart();
// Clear all lines by printing empty strings (triggers Auto-Clear)
// for (uint8_t line = 0; line < 8; line++) {
// display->printMessage(line, "", 0);
// }
}
+8
Datei anzeigen
@@ -48,6 +48,7 @@ public:
printMessage(static_cast<uint8_t>(line | 0x80), text, startX);
}
private:
// Internal structure mapping the safe asynchronous data pipeline
struct DisplayMessage {
@@ -67,3 +68,10 @@ private:
esp_err_t sendCommand(uint8_t cmd) const;
esp_err_t initializeHardware();
};
/**
* @brief Run a comprehensive visual test suite and clear the screen after 5 seconds.
* Note: This is a global helper function decoupled from the main class interface.
* @param display Pointer to the active DisplayController instance.
*/
void run_display_font_test(DisplayController* display);
+192 -41
Datei anzeigen
@@ -141,31 +141,35 @@ esp_err_t LoraController::initializeHardware() {
return err;
}
// Configure interrupt triggering on the rising edge (PosEdge)
err = gpio_set_intr_type(Config::LORA_IRQ_PIN, GPIO_INTR_POSEDGE);
err = gpio_isr_handler_add(Config::LORA_IRQ_PIN, LoraController::gpioIsrHandler, m_isrSemaphore);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set GPIO interrupt type: %s", esp_err_to_name(err));
return err;
}
// Bind the static ISR handler to the physical pin context
err = gpio_isr_handler_add(Config::LORA_IRQ_PIN, LoraController::gpioIsrHandler, m_isrSemaphore);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to register GPIO ISR handler: %s", esp_err_to_name(err));
return err;
}
// MARK STATE MATRIX READY (Thread-Safe Status Update)
// We use a safe timeout here. If the mutex is permanently blocked during boot,
// we must escalate this as a system fault instead of returning ESP_OK.
if (xSemaphoreTake(m_stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
m_state.isInitialized = true;
m_state.frequencyHz = Config::LORA_DEFAULT_FREQ_HZ;
xSemaphoreGive(m_stateMutex);
} else {
ESP_LOGE(TAG, "Fatal: State mutex acquisition timed out during hardware registration.");
return ESP_ERR_TIMEOUT; // Escalate the mutex block as a hard boot failure
}
ESP_LOGI(TAG, "SX1276 LoRa transceiver attached successfully via SPI (Version: 0x%02X).", version);
return ESP_OK;
}
/*
void LoraController::setModeReceiver() {
writeRegister(0x01, 0x80U); // Sleep + LoRa Mode
vTaskDelay(pdMS_TO_TICKS(10));
// Apply MeshCom network profile: 433.175 MHz, BW 250kHz, SF11, CR 4/6
writeRegister(0x06, Config::LORA_FRF_MSB);
writeRegister(0x07, Config::LORA_FRF_MID);
writeRegister(0x08, Config::LORA_FRF_LSB);
writeRegister(0x1D, Config::LORA_MODEM_CONFIG_1); // RegModemConfig1
writeRegister(0x1E, Config::LORA_MODEM_CONFIG_2); // RegModemConfig2
writeRegister(0x40, 0x00U); // DIO0 map onto RxDone
writeRegister(0x01, 0x85U); // RXCONTINUOUS
ESP_LOGI(TAG, "Radio state changed to RXCONTINUOUS");
}
*/
void LoraController::setModeReceiver() {
// DIO0 auf RxDone mappen (Bit 7-6 auf 00)
writeRegister(0x40, 0x00U);
@@ -185,14 +189,18 @@ void LoraController::applyModemConfig() {
writeRegister(0x01, 0x80U);
vTaskDelay(pdMS_TO_TICKS(10));
// Apply RF carrier center frequency (e.g., MeshCom 433.175 MHz)
writeRegister(0x06, Config::LORA_FRF_MSB);
writeRegister(0x07, Config::LORA_FRF_MID);
writeRegister(0x08, Config::LORA_FRF_LSB);
// just write the m_state frequency....
m_state.frequencyHz = Config::LORA_DEFAULT_FREQ_HZ; // @TODO: remove this line after implementation of the doubly linked list
// Configure modem bandwidth, spreading factor, and coding rate
writeRegister(0x1D, Config::LORA_MODEM_CONFIG_1);
writeRegister(0x1E, Config::LORA_MODEM_CONFIG_2);
@@ -211,27 +219,76 @@ void LoraController::applyModemConfig() {
ESP_LOGI(TAG, "Radio profile applied: 433.175 MHz | SF11 | BW 250kHz | SyncWord 0x34");
}
/*
void LoraController::setFrequency(uint8_t msb, uint8_t mid, uint8_t lsb) {
if (m_spiHandle == nullptr) return;
// Safely force transceiver to Standby to update hardware registers safely
writeRegister(0x01, 0x81U);
vTaskDelay(pdMS_TO_TICKS(5));
#ifndef MATH_COMPARE
#define MATH_COMPARE 0
#endif
writeRegister(0x06, msb);
writeRegister(0x07, mid);
writeRegister(0x08, lsb);
// Re-engage Continous Reception Mode
writeRegister(0x01, 0x85U);
ESP_LOGI(TAG, "Frequency dynamically shifted to: 0x%02X%02X%02X", msb, mid, lsb);
}
*/
void LoraController::setFrequency(uint32_t freq_hz) {
// @TODO
if (m_spiHandle == nullptr) return;
if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) {
m_state.frequencyHz = freq_hz;
// 1. Fetch current mode and shift transceiver into Standby (0x01)
// Changing RF carrier registers is strictly prohibited while actively radiating or sampling
const uint8_t previousMode = readRegister(0x01); // RegOpMode
writeRegister(0x01, static_cast<uint8_t>((previousMode & 0xF8U) | 0x01U));
vTaskDelay(pdMS_TO_TICKS(2));
// 2. High-speed fractional frequency calculation
// Formula optimized from: (freq_hz * 2^19) / 32,000,000 b/c 32,000,000 can be seen as 2^11 * 5^6:
// we see freq_hz * 2^(19-11)/5^6 which is: freq_hz * 2^8 / 15625, i.e.
// reduced to exact fraction: 256 / 15,625
// or even more using shift operation, we can switch on a compare of all three methods with #MATH_COMPARE 1
#if (MATH_COMPARE == 1)
// =================================================================
// BENCHMARK MODE (Runs all 3 variants and measures CPU cycles)
// =================================================================
uint32_t start_cycles = esp_cpu_get_cycle_count();
[[maybe_unused]] volatile uint32_t frf_long = static_cast<uint32_t>((static_cast<uint64_t>(freq_hz) << 19) / 32000000ULL);
uint32_t cycles_long = esp_cpu_get_cycle_count() - start_cycles;
start_cycles = esp_cpu_get_cycle_count();
[[maybe_unused]] volatile uint32_t frf_64bit = static_cast<uint32_t>((static_cast<uint64_t>(freq_hz) * 256ULL) / 15625ULL);
uint32_t cycles_64bit = esp_cpu_get_cycle_count() - start_cycles;
start_cycles = esp_cpu_get_cycle_count();
volatile uint32_t frf_shift = static_cast<uint32_t>((static_cast<uint64_t>(freq_hz) * 1125899907ULL) >> 36);
uint32_t cycles_shift = esp_cpu_get_cycle_count() - start_cycles;
printf("[MATH_COMPARE] Freq: %lu Hz -> long 64bit: %lu cycles | 64bit: %lu cycles | Shift: %lu cycles\n",
freq_hz, cycles_long, cycles_64bit, cycles_shift);
const uint32_t frf = frf_shift; // Fallback to the safe bit-shift variant
#else
// =================================================================
// PRODUCTION MODE (Highly optimized, zero benchmark overhead)
// =================================================================
const uint32_t frf = static_cast<uint32_t>((static_cast<uint64_t>(freq_hz) * 1125899907ULL) >> 36);
#endif
// 3. Shift the 24-bit frequency tracking token across the three consecutive hardware registers
writeRegister(0x06, static_cast<uint8_t>((frf >> 16) & 0xFFU)); // RegFrfMsb
writeRegister(0x07, static_cast<uint8_t>((frf >> 8) & 0xFFU)); // RegFrfMid
writeRegister(0x08, static_cast<uint8_t>(frf & 0xFFU)); // RegFrfLsb
// 4. Restore previous operational transceiver path configuration
writeRegister(0x01, previousMode);
xSemaphoreGive(m_stateMutex);
// 5. Force state synchronization and PLL locking verification
setModeReceiver();
// High-priority diagnosis logging bypasses CLI lock buffers uncensored
printf("\r\n[LoRa HW] Frequency successfully shifted to %lu Hz (FRF Word: 0x%06X)\r\n",
(unsigned long)freq_hz, (unsigned int)(frf & 0xFFFFFFU));
fflush(stdout);
}
}
void LoraController::setBandwidth(uint32_t bw_khz) {
if (m_spiHandle == nullptr) return;
@@ -316,6 +373,18 @@ void LoraController::calculateTelemetry(uint8_t rawRssi, uint8_t rawSnr, float&
}
void LoraController::run() {
// Lambda Object:
auto utf8_advance = [](const std::string& str, size_t start_byte, size_t num_chars) -> size_t {
size_t i = start_byte;
size_t chars_counted = 0;
while (i < str.length() && chars_counted < num_chars) {
if (((unsigned char)str[i] & 0xC0) != 0x80) chars_counted++;
i++;
}
while (i < str.length() && ((unsigned char)str[i] & 0xC0) == 0x80) i++;
return i;
};
if (initializeHardware() != ESP_OK) {
ESP_LOGE(TAG, "Critical: LoRa init failed. Thread aborted.");
return;
@@ -328,7 +397,9 @@ void LoraController::run() {
ESP_LOGI(TAG, "LoRa RX was initialized and is running");
while (true) {
if (xSemaphoreTake(m_isrSemaphore, portMAX_DELAY) == pdTRUE) {
// CHANGE: wait a maximum of 500 ms instead of infinity long
if (xSemaphoreTake(m_isrSemaphore, pdMS_TO_TICKS(500)) == pdTRUE) {
// if (xSemaphoreTake(m_isrSemaphore, portMAX_DELAY) == pdTRUE) {
uint32_t irqFlags = readRegister(0x12);
writeRegister(0x12, static_cast<uint8_t>(irqFlags));
@@ -368,7 +439,6 @@ void LoraController::run() {
printf("%c", c);
else
printf("~"); // not printable chars
}
printf("\"\r\n Hex : ");
for (uint8_t i = 0; i < packet.length; i++)
@@ -380,12 +450,40 @@ void LoraController::run() {
// FIXED: Uses the new parameter order (line first, text second)
if (display != nullptr) {
char displayBuffer[16];
snprintf(displayBuffer, sizeof(displayBuffer), "LoRa rx #%03u", (unsigned int)(packetCounter % 1000));
char displayBuffer[85]; // worst case 21 chars with each 4 bytes + Nullbyte.
snprintf(displayBuffer, sizeof(displayBuffer), "#%03u %4.1fdBm %2.1fdB",
(unsigned int)(packetCounter % 1000),
packet.rssi,
packet.snr);
display->printMessage(2, displayBuffer);
// we don't care about UTF-8 for now
std::string text(reinterpret_cast<const char*>(packet.payload), static_cast<size_t>(packet.length));
size_t byte_pos = 0;
uint8_t linenr = 3; // start at line 3
while ( byte_pos < text.length() && linenr < 8) {
// The '::' ensures that the global/local function is called.
size_t next_byte_pos = utf8_advance(text, byte_pos, 21);
std::string line = text.substr(byte_pos, next_byte_pos - byte_pos);
std::strncpy(displayBuffer, line.c_str(), sizeof(displayBuffer ) - 1);
displayBuffer[sizeof(displayBuffer) - 1] ='\0';
display->printMessage(linenr++, displayBuffer);
byte_pos = next_byte_pos;
}
}
}
// New: after work or after 500ms delay, the chip has usually changed its mode, we update the state for the m_state:
uint8_t currentHardwareMode = readRegister(0x01);
if (xSemaphoreTake(m_stateMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
m_state.currentMode = currentHardwareMode;
xSemaphoreGive(m_stateMutex);
}
}
}
}
@@ -400,3 +498,56 @@ void LoraController::startTask(uint32_t priority) {
1
);
}
/**
* @brief Generates an atomic, high-performance runtime snapshot of configuration parameters and metrics.
*/
LoraStateSnapshot LoraController::getSnapshot() {
LoraStateSnapshot snapshot = {};
// Sichere die thread-übergreifende Konsistenz mittels Mutex ab
if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) {
// Kopiere die primitiven Werte aus dem internen Zustandstabellen-Struct (m_state)
snapshot.frequencyHz = m_state.frequencyHz;
snapshot.bandwidthKhz = m_state.bandwidthKhz;
snapshot.totalPacketsReceived = m_state.totalPacketsReceived;
snapshot.totalPacketsTransmitted = m_state.totalPacketsTransmitted;
snapshot.spreadingFactor = m_state.spreadingFactor;
snapshot.codingRate = m_state.codingRate;
snapshot.isInitialized = m_state.isInitialized;
// Zähle die Elemente in der Liste, ohne die schweren Payload-Vektoren zu kopieren
// TODO install the package list:
// snapshot.currentHistoryCount = m_state.packetList.size();
// Read Register 0x01 ONLY ONCE to avoid SPI race conditions
// uint8_t rohofmode = readRegister(0x01);
// uint8_t rawMode = rohofmode & 0x07U;
// use the cached mode from RAM instead live register read
uint8_t rawMode = m_state.currentMode & 0x07U;
// Print the real, unmanipulated hex value safely
//printf("Readregister actual raw hex: 0x%02X\r\n", rohofmode);
//fflush(stdout);
static const char* const MODE_STRINGS[] = {
"SLEEP", // 0x00
"STANDBY", // 0x01
"FSTX", // 0x02
"TX", // 0x03
"FSRX", // 0x04
"RXCONTINUOUS", // 0x05
"RXSINGLE", // 0x06
"CAD" // 0x07
};
strncpy(snapshot.currentModeStr, MODE_STRINGS[rawMode], sizeof(snapshot.currentModeStr) - 1);
snapshot.currentModeStr[sizeof(snapshot.currentModeStr) - 1] = '\0'; // guarantee NULL termination
// Release mutex immediately
xSemaphoreGive(m_stateMutex);
}
return snapshot;
}
+16
Datei anzeigen
@@ -16,6 +16,9 @@
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "lora_types.hpp"
#include <iostream>
#include <string>
#include <cstring>
/**
@@ -76,6 +79,17 @@ public:
*/
void setBandwidth(uint32_t bw_khz);
/**
* @brief Generates an atomic, high-performance runtime snapshot of configuration parameters and metrics.
* @details This routine performs a rapid, thread-safe memory copy of the volatile tracking state variables.
* Unlike getPacketListCopy(), this method explicitly excludes the heavy heap-allocated packet list data,
* making it exceptionally lightweight and highly suitable for high-frequency UI display refreshes,
* CLI updates, or periodic remote health diagnostics.
* @note Acquires the internal synchronization mutex lock momentarily to guarantee structural telemetry alignment.
* @return A self-contained snapshot structure mapping the current system hardware configurations and operational counts.
*/
LoraStateSnapshot getSnapshot();
private:
static const char* TAG;
@@ -110,4 +124,6 @@ private:
*/
void calculateTelemetry(uint8_t rawRssi, uint8_t rawSnr, float& outRssi, float& outSnr);
size_t utf8_advance(const std::string& str, size_t start_byte, size_t num_chars);
};
+19
Datei anzeigen
@@ -20,9 +20,28 @@ struct LoraStateTable {
uint32_t totalPacketsTransmitted = 0; ///< Accumulator metric track-counting egress frames transmitted. (4 Bytes)
uint32_t nextDatabaseId = 1; ///< Identity sequencer pointer controlling the next node's databaseId field. (4 Bytes)
uint32_t bandwidthKhz = 250; ///< Modem bandwidth performance target configured in kHz. (4 Bytes)
uint8_t currentMode = 0; ///< Current operationl modem mode (1 Byte)
uint8_t spreadingFactor = 11; ///< LoRa Spreading Factor configuration (typically ranges between 6 and 12). (1 Byte)
uint8_t codingRate = 6; ///< Coding Rate denominator setting (e.g., value 6 yields a 4/6 coding protection overhead). (1 Byte)
bool isInitialized = false; ///< True if hardware was successfully validated and attached during initialization. (1 Byte)
// The three 1-byte variables at the bottom align perfectly into a 32-bit slot, leaving minimal trailing padding.
};
/**
* @brief Thread-isolated telemetry snapshot for UI displays and remote diagnostics.
* @note Optimized using "largest-to-smallest" data type ordering to minimize memory padding.
*/
struct LoraStateSnapshot {
size_t currentHistoryCount; ///< Soft-limit threshold capping the maximum number of history records stored before auto-purge. (4 or 8 Bytes)
uint32_t frequencyHz; ///< RF carrier operating frequency configured in Hertz. (4 Bytes)
uint32_t bandwidthKhz; ///< Modem bandwidth performance target configured in kHz. (4 Bytes)
uint32_t totalPacketsReceived; ///< Accumulator metric track-counting successful ingress frames. (4 Bytes)
uint32_t totalPacketsTransmitted; ///< Accumulator metric track-counting egress frames transmitted. (4 Bytes)
char currentModeStr[16]; ///< Human-readable string representation of the current operational modem mode. (16 Bytes)
uint8_t spreadingFactor; ///< LoRa Spreading Factor configuration (typically ranges between 6 and 12). (1 Byte)
uint8_t codingRate; ///< Coding Rate denominator setting (e.g., value 6 yields a 4/6 coding protection overhead). (1 Byte)
bool isInitialized; ///< True if hardware was successfully validated and attached during initialization. (1 Byte)
// Elements are strictly organized by size alignment requirements to completely prevent intermittent alignment gaps.
};
+7 -4
Datei anzeigen
@@ -8,6 +8,10 @@
* For the full license terms, see the LICENSE file in the project root.
*/
#ifndef GIT_TAG
#define GIT_TAG "unknown"
#endif
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -127,12 +131,10 @@ extern "C" {
// Trigger synchronous welcome splash screen frame
if (display != nullptr) {
char msg[32];
//display->printMessage(0, "FoxMesh " Config::PROJECT_VERSION " \x86" "OE3TKT");
snprintf(msg, sizeof(msg), "FoxMesh %s", Config::PROJECT_VERSION);
snprintf(msg, sizeof(msg), "FoxMesh " GIT_TAG);
display->printMessage(0, msg);
display->appendMessage(0, "OE3TKT", 15*6);
// display->appendMessage(0, GIT_TAG, 12*6);
display->printMessage(1, "----------------------");
//display->printMessage(2, "Bärbel frühstückt ©");
}
// Main system watchdog and status monitor loop
while (true) {
@@ -140,6 +142,7 @@ extern "C" {
Config::PROJECT_TITLE,
Config::PROJECT_VERSION,
(unsigned long)esp_get_free_heap_size());
// ESP_LOGI(TAG, "Git-Hash: %s", GIT_HASH);
vTaskDelay(pdMS_TO_TICKS(5000));
}