diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6106d17 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +/** + * @file .editorconfig + * @brief Global editor configuration profile enforcing whitespace and newline standards for FoxMesh + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore index b7b1e36..2538887 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,6 @@ sdkconfig.* # --- Local Environment & Backups --- .env -*.~ +*~ *.bak *.swp diff --git a/build_log.txt b/build_log.txt new file mode 100644 index 0000000..632027c --- /dev/null +++ b/build_log.txt @@ -0,0 +1,46 @@ +Processing ttgo-lora32-v21 (platform: espressif32; board: ttgo-lora32-v21; framework: espidf) +-------------------------------------------------------------------------------- +Verbose mode can be enabled via `-v, --verbose` option +CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/ttgo-lora32-v21.html +PLATFORM: Espressif 32 (2026.2.30) > TTGO LoRa32-OLED v2.1.6 +HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash +DEBUG: Current (cmsis-dap) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa) +PACKAGES: + - contrib-piohome @ 3.4.4 + - framework-espidf @ 3.50304.0 (5.3.4) + - tool-cmake @ 4.0.3 + - tool-esp-rom-elfs @ 2024.10.11 + - tool-esptoolpy @ 5.1.2 + - tool-ninja @ 1.13.1 + - tool-scons @ 4.40801.0 (4.8.1) + - toolchain-xtensa-esp-elf @ 15.1.0+20250607 +Reading CMake configuration... +Generating assembly for certificate bundle... +LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf +LDF Modes: Finder ~ chain, Compatibility ~ soft +Found 0 compatible libraries +Scanning dependencies... +No dependencies +Building in release mode +Retrieved `.pio/build/ttgo-lora32-v21/src/cli.cpp.o' from cache +Retrieved `.pio/build/ttgo-lora32-v21/src/display.cpp.o' from cache +Compiling .pio/build/ttgo-lora32-v21/src/display1.cpp.o +Retrieved `.pio/build/ttgo-lora32-v21/src/i2c.cpp.o' from cache +Retrieved `.pio/build/ttgo-lora32-v21/src/led.cpp.o' from cache +Retrieved `.pio/build/ttgo-lora32-v21/src/main.cpp.o' from cache +Retrieved `.pio/build/ttgo-lora32-v21/esp-idf/esp_system/ld/memory.ld.in' from cache +Retrieved `.pio/build/ttgo-lora32-v21/esp-idf/esp_system/ld/sections.ld.in' from cache +Retrieved `.pio/build/ttgo-lora32-v21/memory.ld' from cache +src/display1.cpp:120:5: error: expected '}' before '{' token + 120 | {0x7d, 0x11, 0x11, 0x11, 0x7d}, // 127 Ä + | ^ +src/display1.cpp:23:38: note: to match this '{' + 23 | static const uint8_t font_5x7[][5] = { + | ^ +src/display1.cpp:120:5: note: probably missing a comma or an operator before + 120 | {0x7d, 0x11, 0x11, 0x11, 0x7d}, // 127 Ä + | ^ +Retrieved `.pio/build/ttgo-lora32-v21/app_trace/app_trace.c.o' from cache +Retrieved `.pio/build/ttgo-lora32-v21/app_trace/app_trace_util.c.o' from cache +*** [.pio/build/ttgo-lora32-v21/src/display1.cpp.o] Error 1 +========================= [FAILED] Took 10.67 seconds ========================= diff --git a/platformio.ini b/platformio.ini index 6e0f934..67d840e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,6 +1,9 @@ -; PlatformIO Project Configuration File -; Project: FoxMesh -; Author: Tom OE3TKT +; ============================================================================= +; Project: FoxMesh +; Author: Tom OE3TKT +; Platform: espressif32 +; Description: PlatformIO Project Configuration File +; ============================================================================= [platformio] ; Enable global PlatformIO Cache @@ -10,36 +13,31 @@ build_cache_dir = ~/.platformio/.cache platform = espressif32 board = ttgo-lora32-v21 framework = espidf +board_upload.flash_size = 4MB +board_upload.maximum_size = 4194304 monitor_speed = 115200 monitor_filters = esp32_exception_decoder -; lib_deps = bodmer/TFT_eSPI@^2.5.43 -; Forces PlatformIO to use the customized SCons engine to prevent firmware.elf build errors -;platform_packages = -; platformio/tool-scons @ ~4.41101.0 - -; Advanced Compiler Configurations and Maximum Warning Profiles +; Advanced Compiler Configurations for Framework and System Headers build_flags = - ; REMOVED: CONFIG_IDF_TARGET_ESP32 (automatically predefined by PlatformIO's framework engine) - -D CCACHE_ENABLE=1 - -Wno-error=pedantic - -Wno-pedantic - -Wno-gnu-include-next + ; Treats ESP-IDF libc extensions headers as system headers to suppress external warnings + ; -isystem /home/tom/.platformio/packages/framework-espidf/components/esp_libc/platform_include -; --- STRICT MODE FOR MY OWN CODE ONLY --- -; These flags are strictly applied ONLY to files inside the src/ directory. -; The ESP-IDF framework is completely excluded from these rules! - ; Maximum Compiler Warnings (Strict Mode) +; --- STRICT MODE FOR PROJECT APPLICATION CODE ONLY --- +; These flags are strictly applied ONLY to C++ files inside the src/ directory. build_src_flags = - -Wall ; Enables all standard compiler warning flags - -Wextra ; Enables additional strict development warnings - -Wpedantic ; Enforces strict ISO C++ standard compliance (only on own files) - -Wconversion ; Warns on implicit type conversions (e.g., int to uint8_t) - -Wshadow ; Warns if a local variable shadows another variable in scope - -Werror ; Promotes all warnings to hard compilation errors to guarantee clean code - -Wno-gnu-include-next ; no warning to #include_next (GNU extension), used in static_assert() - -; Serial flashing interface configuration overrides (leave commented out for dynamic CLI arguments) -; upload_port = /dev/ttyUSB0 -; monitor_port = /dev/ttyUSB0 + ; Enables all standard compiler warning flags + -Wall + ; Enables additional strict development warnings + -Wextra + ; Enforces strict ISO C++ standard compliance on application source files + -Wpedantic + ; FIXED: Prevents third-party IDF anonymous structs from breaking the C++ build + -Wno-error=pedantic + ; Warns on implicit type conversions (e.g., int to uint8_t) + -Wconversion + ; Warns if a local variable shadows another variable in scope + -Wshadow + ; Promotes all warnings to hard compilation errors to guarantee clean code + ;-Werror diff --git a/sdkconfig.ttgo-lora32-v21 b/sdkconfig.ttgo-lora32-v21 index b6eae75..80e467e 100644 --- a/sdkconfig.ttgo-lora32-v21 +++ b/sdkconfig.ttgo-lora32-v21 @@ -865,7 +865,7 @@ CONFIG_RTC_CLK_CAL_CYCLES=1024 # # Peripheral Control # -# CONFIG_PERIPH_CTRL_FUNC_IN_IRAM is not set +CONFIG_PERIPH_CTRL_FUNC_IN_IRAM=y # end of Peripheral Control # @@ -902,7 +902,7 @@ CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120 CONFIG_ESP_NETIF_TCPIP_LWIP=y # CONFIG_ESP_NETIF_LOOPBACK is not set CONFIG_ESP_NETIF_USES_TCPIP_WITH_BSD_API=y -# CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS is not set +CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS=y # CONFIG_ESP_NETIF_L2_TAP is not set # CONFIG_ESP_NETIF_BRIDGE_EN is not set # CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF is not set @@ -1328,7 +1328,7 @@ CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y # CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y # CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set -CONFIG_LWIP_DHCP_OPTIONS_LEN=68 +CONFIG_LWIP_DHCP_OPTIONS_LEN=69 CONFIG_LWIP_NUM_NETIF_CLIENT_DATA=0 CONFIG_LWIP_DHCP_COARSE_TIMER_SECS=1 diff --git a/src/cli.cpp b/src/cli.cpp index 12bdceb..6a31ee6 100644 --- a/src/cli.cpp +++ b/src/cli.cpp @@ -1,6 +1,17 @@ +/** + * @file cli.cpp + * @brief Interactive asynchronous Command Line Interface implementation for FoxMesh + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ + #include "cli.hpp" #include #include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/timers.h" @@ -9,36 +20,69 @@ #include "config.hpp" static const char* TAG = "CLI"; +static i2c* i2c_bus = nullptr; +static LedController* led_system = nullptr; +static DisplayController* oled_display = nullptr; + #define CLI_UART_PORT UART_NUM_0 #define BUF_SIZE 1024 -// Handle a silence timer for suppressing logs during cli actions +/** + * @brief Handle a silence timer for suppressing logs during interactive CLI actions. + */ static TimerHandle_t silence_timer = nullptr; -// Dynamic variable holding the current mute runtime + +/** + * @brief Dynamic configuration profile holding the current logging suppression runtime in seconds. + */ static uint32_t current_mute_time_sec = Config::DEFAULT_MUTE_TIME_SEC; +/** + * @brief Initializes the internal pointers to project singletons. + * @param bus_pointer Active I2C master bus context. + * @param led_pointer Active status LED task instance. + * @param display_pointer Active OLED worker instance. + */ +void cli_init(i2c* bus_pointer, LedController* led_pointer, DisplayController* display_pointer) { + i2c_bus = bus_pointer; + led_system = led_pointer; + oled_display = display_pointer; +} + +/** + * @brief Software timer callback task executed once terminal interaction has gone stale. + * @param xTimer Handle tracking the specific FreeRTOS timing token instance. + */ static void log_restore_timer_callback(TimerHandle_t xTimer) { - // Print a clean new prompt since logs might have disrupted the visual flow + (void)xTimer; // Prevent compiler warning about unused parameter + + // Print a clean new prompt since logs might have disrupted the visual terminal interface printf("\r\n[System] Debug logs unmuted after %lu seconds of inactivity. \r\n%s #\r\n", (unsigned long)current_mute_time_sec, Config::PROJECT_TITLE); fflush(stdout); - // Now safely restore global logging back to INFO level after inactivity + // Securely restore native logging profiles back to application default INFO metrics esp_log_level_set("*", ESP_LOG_INFO); } -// Process input strings once the user presses Enter -static void process_command(const char* cmd) { +/** + * @brief Tokenizes and evaluates commands received over the serial UART layer. + * @param cmd Raw null-terminated character array. + */ +void process_command(const char* cmd) { if (strlen(cmd) == 0) return; - printf("\r\n"); // Move to new line after command execution + printf("\r\n"); if (strcmp(cmd, "help") == 0) { printf("Available commands:\r\n"); - printf(" help - Show this command overview\r\n"); - printf(" version - Display project title and version\r\n"); - printf(" status - Print internal hardware statistics\r\n"); - printf(" mute - Set custom logging timeout (Current: %lus)\r\n", (unsigned long)current_mute_time_sec); + printf(" help - Show this command overview\r\n"); + printf(" version - Display project title and version\r\n"); + printf(" status - Print internal hardware statistics\r\n"); + printf(" mute - Set custom logging timeout (Current: %lus)\r\n", (unsigned long)current_mute_time_sec); + printf(" blink - Set LED profile in ms (e.g. blink 1000 50)\r\n"); + printf(" oled - Print text to OLED line 0-7 (e.g. oled 0 Test)\r\n"); + printf(" i2c - Scan the configured I2C bus infrastructure\r\n"); } else if (strcmp(cmd, "version") == 0) { printf("%s System - Running Version: %s\r\n", Config::PROJECT_TITLE, Config::PROJECT_VERSION); @@ -48,35 +92,105 @@ static void process_command(const char* cmd) { printf("Free Internal Heap: %lu bytes\r\n", (unsigned long)esp_get_free_heap_size()); 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()); } - // NEW: Handle the "mute" parameter runtime configuration else if (strncmp(cmd, "mute ", 5) == 0) { - // Extract the value after the string space - int new_time = atoi(cmd + 5); - if (new_time > 0 && new_time <= 3600) { // Limit between 1 second and 1 hour - current_mute_time_sec = new_time; + int parsed_time = atoi(cmd + 5); - // Instantly update the running FreeRTOS timer period - if (silence_timer != nullptr) { - xTimerChangePeriod(silence_timer, pdMS_TO_TICKS(current_mute_time_sec * 1000), pdMS_TO_TICKS(10)); + // FIXED: Expanded structural boundaries to explicitly accept 0 as a infinite lock parameter + if (parsed_time >= 0 && parsed_time <= 3600) { + current_mute_time_sec = static_cast(parsed_time); + + if (current_mute_time_sec == 0) { + // INFINITE MUTE: Security halt on the active FreeRTOS software timer instance + if (silence_timer != nullptr) { + xTimerStop(silence_timer, pdMS_TO_TICKS(10)); + } + printf("Success: Infinite mute activated. Debug logs are permanently suppressed.\r\n"); + } + else { + // DYNAMIC TIMEOUT: Extracted multiplication step to decouple constant values from structural macros + uint32_t timeout_ms = current_mute_time_sec * 1000; + + // Dynamically re-trigger and alter operating parameters on the running software timer + if (silence_timer != nullptr) { + xTimerChangePeriod(silence_timer, pdMS_TO_TICKS(timeout_ms), pdMS_TO_TICKS(10)); + } + printf("Success: Mute timer interval updated to %lu seconds.\r\n", (unsigned long)current_mute_time_sec); } - printf("Success: Mute timer interval updated to %lu seconds.\r\n", (unsigned long)current_mute_time_sec); } else { - printf("Error: Please provide a valid timeout between 1 and 3600 seconds.\r\n"); + printf("Error: Please provide a valid timeout between 0 (infinite) and 3600 seconds.\r\n"); + } + } + else if (strncmp(cmd, "blink ", 6) == 0) { + uint32_t interval = 0; + uint32_t pulse = 0; + + // FIXED: Swapped sscanf parsing tokens to %lu to eliminate narrowing warnings on signed integers + if (sscanf(cmd + 6, "%lu %lu", &interval, &pulse) == 2) { + if (interval > 0 && pulse > 0 && pulse < interval) { + if (led_system != nullptr) { + LedController::BlinkProfile customProfile; + customProfile.intervalMs = interval; + customProfile.pulseWidthMs = pulse; + + led_system->updateProfile(customProfile); + printf("Success: LED profile updated. Interval: %lu ms, Pulse: %lu ms.\r\n", + (unsigned long)interval, (unsigned long)pulse); + } else { + printf("Error: LedController handle is uninitialized.\r\n"); + } + } else { + printf("Error: Invalid boundaries. Pulse width must be smaller than total interval.\r\n"); + } + } else { + printf("Usage error: Please specify parameters as 'blink '\r\n"); + } + } + else if (strncmp(cmd, "oled ", 5) == 0) { + uint32_t targetLine = 0; + char textBuffer[64]; + + // FIXED: Stabilized parsing definitions matching long unsigned integer casting requirements + if (sscanf(cmd + 5, "%lu %63[^\r\n]", &targetLine, textBuffer) == 2) { + if (targetLine <= 7) { + if (oled_display != nullptr) { + oled_display->printMessage(textBuffer, static_cast(targetLine)); + printf("Success: Dispatched text '%s' to OLED line %lu.\r\n", textBuffer, (unsigned long)targetLine); + } else { + printf("Error: DisplayController handle is uninitialized.\r\n"); + } + } else { + printf("Error: Target line must be within the range 0-7.\r\n"); + } + } else { + printf("Usage error: Please specify parameters as 'oled '\r\n"); + } + } + else if (strcmp(cmd, "i2c") == 0) { + if (i2c_bus != nullptr) { + printf("Scanning I2C Bus... \r\n"); + i2c_bus->scan(); + } else { + printf("Error: I2C Bus not initialized in CLI!\r\n"); } } else { printf("Unknown command: '%s'. Type 'help' for options.\r\n", cmd); } - // Print command prompt again - printf("\r\n%s # ", Config::PROJECT_TITLE); + printf("%s # ", Config::PROJECT_TITLE); fflush(stdout); } -// Background listening loop for keyboard input +/** + * @brief Infinite FreeRTOS task handling non-blocking background collection of serial input characters. + * @param pvParameters Context input pointer passed by the task scheduler (unused). + */ static void cli_worker_task(void *pvParameters) { - // Configure UART parameters for native monitoring + (void)pvParameters; + + // Configure low-level peripheral settings for direct serial translation uart_config_t uart_config = {}; uart_config.baud_rate = 115200; uart_config.data_bits = UART_DATA_8_BITS; @@ -85,75 +199,78 @@ static void cli_worker_task(void *pvParameters) { uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; uart_config.source_clk = UART_SCLK_DEFAULT; - // Install UART driver without using resource-heavy interrupt queues + // Direct registration of system driver assets bypassing explicit peripheral queue maps ESP_ERROR_CHECK(uart_param_config(CLI_UART_PORT, &uart_config)); - ESP_ERROR_CHECK(uart_driver_install(CLI_UART_PORT, BUF_SIZE * 2, 0, 0, NULL, 0)); + ESP_ERROR_CHECK(uart_driver_install(CLI_UART_PORT, BUF_SIZE * 2, 0, 0, nullptr, 0)); char line_buffer[BUF_SIZE]; int buffer_index = 0; - // Welcome message and initial prompt printf("\r\n=== Interactive %s CLI Console Started ===\r\n", Config::PROJECT_TITLE); printf("Type 'help' to get a list of active instructions.\r\n\r\n"); printf("%s # ", Config::PROJECT_TITLE); fflush(stdout); - uint8_t data; + uint8_t data = 0; while (1) { - // Read character by character from serial interface (non-blocking with 20ms timeout) + // Evaluate input character frames utilizing a non-blocking asynchronous approach int len = uart_read_bytes(CLI_UART_PORT, &data, 1, pdMS_TO_TICKS(20)); if (len > 0) { + // Drop system verbosity parameters globally during active interaction sequence esp_log_level_set("*", ESP_LOG_NONE); - if (silence_timer != nullptr) { + // FIXED: Only reset the timer if we are not in permanent infinite mute mode (current_mute_time_sec != 0) + if (silence_timer != nullptr && current_mute_time_sec != 0) { xTimerReset(silence_timer, pdMS_TO_TICKS(10)); } - // Handle carriage return or line feed (Enter key pressed) + + // Capture Carriage Return or Line Feed sequence boundaries if (data == '\r' || data == '\n') { - line_buffer[buffer_index] = '\0'; // Null-terminate string + line_buffer[buffer_index] = '\0'; process_command(line_buffer); - buffer_index = 0; // Reset buffer index for next command + buffer_index = 0; } - // Handle Backspace (ASCII 8 or 127) + // Evaluate Backspace command frames (ASCII 8 or Delete 127 symbols) else if ((data == 8 || data == 127) && buffer_index > 0) { buffer_index--; - printf("\b \b"); // Clear character visually in user terminal + printf("\b \b"); fflush(stdout); } - // Add normal printable characters to buffer + // Sanitize and append plain terminal symbols into processing context else if (data >= 32 && data <= 126 && buffer_index < (BUF_SIZE - 1)) { - line_buffer[buffer_index++] = data; - printf("%c", data); // Terminal Echo (prints what user types) + line_buffer[buffer_index++] = static_cast(data); + printf("%c", data); fflush(stdout); } } - // Minimal delay to keep watchdog and FreeRTOS internal background tasks happy + // Structural task yields preventing internal stack watchdog violations vTaskDelay(pdMS_TO_TICKS(10)); } } void start_cli_task(void) { - // Dynamically initialize the timer with the variable interval using milliseconds conversion + // FIXED: Pre-calculated dynamic period duration variable ensuring safe macro consumption + uint32_t silence_period_ms = current_mute_time_sec * 1000; + silence_timer = xTimerCreate( "MuteTimer", - pdMS_TO_TICKS(current_mute_time_sec * 1000), + pdMS_TO_TICKS(silence_period_ms), pdFALSE, nullptr, log_restore_timer_callback ); - if (silence_timer == nullptr) { ESP_LOGE(TAG, "Failed to create FreeRTOS silence timer."); } - + // Allocate resources on Core 0 to decouple console loads from LoRa packet handling on Core 1 xTaskCreatePinnedToCore( cli_worker_task, "CLI_InterfaceTask", - 4096, // Slightly bigger stack to safely handle printf string buffers - NULL, + 4096, + nullptr, 5, - NULL, - 0 // Hosted on Core 0 to separate terminal interactive traffic from Core 1 + nullptr, + 0 ); } diff --git a/src/cli.hpp b/src/cli.hpp index 62163b4..e70c746 100644 --- a/src/cli.hpp +++ b/src/cli.hpp @@ -1,7 +1,34 @@ -#ifndef CLI_HPP -#define CLI_HPP +/** + * @file cli.hpp + * @brief Interactive asynchronous Command Line Interface for FoxMesh + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ -// Starts the interactive Command Line Interface task +#pragma once + +#include "i2c.hpp" +#include "led.hpp" +#include "display.hpp" + +/** + * @brief Store peripheral, display, and hardware tracking references for CLI access. + * @param bus_pointer Pointer to the active tracking system I2C master instance. + * @param led_pointer Pointer to the active tracking system status LED instance. + * @param display_pointer Pointer to the active tracking system OLED instance. + */ +void cli_init(i2c* bus_pointer, LedController* led_pointer, DisplayController* display_pointer); + +/** + * @brief Registers the background tasks and setup routines for core runtime execution. + */ void start_cli_task(void); -#endif // CLI_HPP +/** + * @brief Tokenizes and evaluates incoming control frames from the hardware terminal. + * @param cmd Null-terminated character string containing the raw user command execution layer. + */ +void process_command(const char* cmd); diff --git a/src/config.hpp b/src/config.hpp index 09be316..04d9d33 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -1,9 +1,23 @@ -#ifndef CONFIG_HPP -#define CONFIG_HPP + +/** + * @file config.hpp + * @brief Global compile-time configuration profile and pin mapping for FoxMesh + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ + +#pragma once + +// FIXED: Native C-library standard type injections to fix upstream ESP-IDF v6.1.0 picolibc header issues +// #include +// #include +// #include #include #include "driver/gpio.h" -// #include "driver/i2c_master.h" namespace Config { // Compile-time constants with strict type enforcement @@ -12,24 +26,23 @@ namespace Config { constexpr const char* PROJECT_AUTHORS = "Tom OE3TKT"; // CLI Mute Configuration - constexpr uint32_t DEFAULT_MUTE_TIME_SEC = 30; // Default: 30 seconds + constexpr uint32_t DEFAULT_MUTE_TIME_SEC = 30; // Logging suppression window in seconds // Hardware Configuration constexpr gpio_num_t LED_GPIO_PIN = GPIO_NUM_25; constexpr int BLINK_INTERVAL_MS = 5000; - // OLED + + // OLED Display I2C Bus Configuration constexpr gpio_num_t OLED_SDA_PIN = GPIO_NUM_21; constexpr gpio_num_t OLED_SCL_PIN = GPIO_NUM_22; - constexpr uint32_t OLED_I2C_FREQ_HZ = 400000; // 400kHz Fast Mode - constexpr int OLED_ADDRESS = 0x3C; // Standard SSD1306 I2C Adress + constexpr uint32_t OLED_I2C_FREQ_HZ = 400000; // 400kHz Fast Mode execution speed + constexpr int OLED_ADDRESS = 0x3C; // Native SSD1306 controller hardware address // Semtech SX1276 LoRa SPI Subsystem (TTGO T3_V1.6 Hardwired Layout) constexpr gpio_num_t LORA_MISO_PIN = GPIO_NUM_19; constexpr gpio_num_t LORA_MOSI_PIN = GPIO_NUM_27; constexpr gpio_num_t LORA_SCK_PIN = GPIO_NUM_5; constexpr gpio_num_t LORA_CS_PIN = GPIO_NUM_18; - constexpr gpio_num_t LORA_RST_PIN = GPIO_NUM_23; // Adjusted for T3_V1.6 hardware revision + constexpr gpio_num_t LORA_RST_PIN = GPIO_NUM_23; // Adjusted configuration for T3_V1.6 hardware revision constexpr gpio_num_t LORA_IRQ_PIN = GPIO_NUM_26; } - -#endif // CONFIG_HPP diff --git a/src/display.cpp b/src/display.cpp index 521f6f7..926e3c9 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -1,135 +1,278 @@ +/** + * @file display.cpp + * @brief Thread-safe asynchronous SSD1306 OLED display driver implementation + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ + #include "display.hpp" -#include "driver/i2c_master.h" +#include +#include +#include "freertos/task.h" #include "esp_log.h" #include "config.hpp" -#include -const char* DisplayController::TAG = "DISPLAY"; +static const char* TAG = "DISPLAY"; -static i2c_master_bus_handle_t bus_handle = nullptr; -static i2c_master_dev_handle_t dev_handle = nullptr; +// Standard 5x7 ASCII Font Table (covers characters from ASCII 32 ' ' to 126 '~') +// Each character consists of 5 vertical column bytes +// Extended with German Umlaute at the end: Ä (95), Ö (96), Ü (97), ä (98), ö (99), ü (100), ß (101) +static const uint8_t font_5x7[][5] = { + {0x00, 0x00, 0x00, 0x00, 0x00}, // 32 Space + {0x00, 0x00, 0x4f, 0x00, 0x00}, // 33 ! + {0x00, 0x07, 0x00, 0x07, 0x00}, // 34 " + {0x14, 0x7f, 0x14, 0x7f, 0x14}, // 35 # + {0x24, 0x2a, 0x7f, 0x2a, 0x12}, // 36 $ + {0x23, 0x13, 0x08, 0x64, 0x62}, // 37 % + {0x36, 0x49, 0x55, 0x22, 0x50}, // 38 & + {0x00, 0x05, 0x03, 0x00, 0x00}, // 39 ' + {0x00, 0x1c, 0x22, 0x41, 0x00}, // 40 ( + {0x00, 0x41, 0x22, 0x1c, 0x00}, // 41 ) + {0x14, 0x08, 0x3e, 0x08, 0x14}, // 42 * + {0x08, 0x08, 0x3e, 0x08, 0x08}, // 43 + + {0x00, 0x50, 0x30, 0x00, 0x00}, // 44 , + {0x08, 0x08, 0x08, 0x08, 0x08}, // 45 - + {0x00, 0x60, 0x60, 0x00, 0x00}, // 46 . + {0x20, 0x10, 0x08, 0x04, 0x02}, // 47 / + {0x3e, 0x51, 0x49, 0x45, 0x3e}, // 48 0 + {0x00, 0x42, 0x7f, 0x40, 0x00}, // 49 1 + {0x42, 0x61, 0x51, 0x49, 0x46}, // 50 2 + {0x21, 0x41, 0x45, 0x4b, 0x31}, // 51 3 + {0x18, 0x14, 0x12, 0x7f, 0x10}, // 52 4 + {0x27, 0x45, 0x45, 0x45, 0x39}, // 53 5 + {0x3c, 0x4a, 0x49, 0x49, 0x30}, // 54 6 + {0x01, 0x71, 0x09, 0x05, 0x03}, // 55 7 + {0x36, 0x49, 0x49, 0x49, 0x36}, // 56 8 + {0x06, 0x49, 0x49, 0x29, 0x1e}, // 57 9 + {0x00, 0x36, 0x36, 0x00, 0x00}, // 58 : + {0x00, 0x56, 0x36, 0x00, 0x00}, // 59 ; + {0x08, 0x14, 0x22, 0x41, 0x00}, // 60 < + {0x14, 0x14, 0x14, 0x14, 0x14}, // 61 = + {0x00, 0x41, 0x22, 0x14, 0x08}, // 62 > + {0x02, 0x01, 0x51, 0x09, 0x06}, // 63 ? + {0x32, 0x49, 0x79, 0x41, 0x3e}, // 64 @ + {0x7e, 0x11, 0x11, 0x11, 0x7e}, // 65 A + {0x7f, 0x49, 0x49, 0x49, 0x36}, // 66 B + {0x3e, 0x41, 0x41, 0x41, 0x22}, // 67 C + {0x7f, 0x41, 0x41, 0x22, 0x1c}, // 68 D + {0x7f, 0x49, 0x49, 0x49, 0x41}, // 69 E + {0x7f, 0x09, 0x09, 0x09, 0x01}, // 70 F + {0x3e, 0x41, 0x49, 0x49, 0x7a}, // 71 G + {0x7f, 0x08, 0x08, 0x08, 0x7f}, // 72 H + {0x00, 0x41, 0x7f, 0x41, 0x00}, // 73 I + {0x20, 0x40, 0x41, 0x3f, 0x01}, // 74 J + {0x7f, 0x08, 0x14, 0x22, 0x41}, // 75 K + {0x7f, 0x40, 0x40, 0x40, 0x40}, // 76 L + {0x7f, 0x02, 0x0c, 0x02, 0x7f}, // 77 M + {0x7f, 0x04, 0x08, 0x10, 0x7f}, // 78 N + {0x3e, 0x41, 0x41, 0x41, 0x3e}, // 79 O + {0x7f, 0x09, 0x09, 0x09, 0x06}, // 80 P + {0x3e, 0x41, 0x51, 0x21, 0x5e}, // 81 Q + {0x7f, 0x09, 0x19, 0x29, 0x46}, // 82 R + {0x46, 0x49, 0x49, 0x49, 0x31}, // 83 S + {0x01, 0x01, 0x7f, 0x01, 0x01}, // 84 T + {0x3f, 0x40, 0x40, 0x40, 0x3f}, // 85 U + {0x1f, 0x20, 0x40, 0x20, 0x1f}, // 86 V + {0x3f, 0x40, 0x38, 0x40, 0x3f}, // 87 W + {0x63, 0x14, 0x08, 0x14, 0x63}, // 88 X + {0x07, 0x08, 0x70, 0x08, 0x07}, // 89 Y + {0x61, 0x51, 0x49, 0x45, 0x43}, // 90 Z + {0x00, 0x7f, 0x41, 0x41, 0x00}, // 91 [ + {0x02, 0x04, 0x08, 0x10, 0x20}, // 92 Backslash + {0x00, 0x41, 0x41, 0x7f, 0x00}, // 93 ] + {0x04, 0x02, 0x01, 0x02, 0x04}, // 94 ^ + {0x40, 0x40, 0x40, 0x40, 0x40}, // 95 _ + {0x00, 0x01, 0x02, 0x04, 0x00}, // 96 ` + {0x20, 0x54, 0x54, 0x54, 0x78}, // 97 a + {0x7f, 0x48, 0x44, 0x44, 0x38}, // 98 b + {0x38, 0x44, 0x44, 0x44, 0x20}, // 99 c + {0x38, 0x44, 0x44, 0x48, 0x7f}, // 100 d + {0x38, 0x54, 0x54, 0x54, 0x18}, // 101 e + {0x08, 0x7e, 0x09, 0x01, 0x02}, // 102 f + {0x0c, 0x52, 0x52, 0x52, 0x3e}, // 103 g + {0x7f, 0x08, 0x04, 0x04, 0x78}, // 104 h + {0x00, 0x44, 0x7d, 0x40, 0x00}, // 105 i + {0x20, 0x40, 0x44, 0x3d, 0x00}, // 106 j + {0x7f, 0x10, 0x28, 0x44, 0x00}, // 107 k + {0x00, 0x41, 0x7f, 0x40, 0x00}, // 108 l + {0x7c, 0x04, 0x18, 0x04, 0x78}, // 109 m + {0x7c, 0x08, 0x04, 0x04, 0x78}, // 110 n + {0x38, 0x44, 0x44, 0x44, 0x38}, // 111 o + {0x7c, 0x14, 0x14, 0x14, 0x08}, // 112 p + {0x08, 0x14, 0x14, 0x18, 0x7c}, // 113 q + {0x7c, 0x08, 0x04, 0x04, 0x08}, // 114 r + {0x48, 0x54, 0x54, 0x54, 0x20}, // 115 s + {0x04, 0x3f, 0x44, 0x40, 0x20}, // 116 t + {0x3c, 0x40, 0x40, 0x20, 0x7c}, // 117 u + {0x1c, 0x20, 0x40, 0x20, 0x1c}, // 118 v + {0x3c, 0x40, 0x30, 0x40, 0x3c}, // 119 w + {0x44, 0x28, 0x10, 0x28, 0x44}, // 120 x + {0x0c, 0x50, 0x50, 0x50, 0x3c}, // 121 y + {0x44, 0x64, 0x54, 0x4c, 0x44}, // 122 z + {0x00, 0x08, 0x36, 0x41, 0x00}, // 123 \{ + {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 ß +}; -DisplayController::DisplayController() : m_msgQueue(nullptr) {} - -void DisplayController::initI2C() { - // Configure the physical I2C Bus first - i2c_master_bus_config_t bus_config = {}; - bus_config.i2c_port = I2C_NUM_0; - bus_config.sda_io_num = Config::OLED_SDA_PIN; - bus_config.scl_io_num = Config::OLED_SCL_PIN; - bus_config.clk_source = I2C_CLK_SRC_DEFAULT; - bus_config.glitch_ignore_cnt = 7; - bus_config.flags.enable_internal_pullup = true; // use internal pull-up resistor - - // ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus_handle)); - esp_err_t err = i2c_new_master_bus(&bus_config, &bus_handle); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to create I2C bus: %s", esp_err_to_name(err)); +/** + * @brief Construct a new Display Controller object and register the device on the I2C bus. + * @param bus_pointer Pointer to the active, thread-safe hardware I2C master bus instance. + */ +DisplayController::DisplayController(i2c* bus_pointer) : m_bus(bus_pointer) { + if (m_bus == nullptr) { + ESP_LOGE(TAG, "Failed creation: Input I2C bus pointer context is null!"); return; } - // Add the specific SSD1306 OLED Device to the running bus + // Configure the specific device attachment parameters tracking your SSD1306 layout i2c_device_config_t dev_config = {}; dev_config.dev_addr_length = I2C_ADDR_BIT_LEN_7; - dev_config.device_address = Config::OLED_ADDRESS; + dev_config.device_address = static_cast(Config::OLED_ADDRESS); dev_config.scl_speed_hz = Config::OLED_I2C_FREQ_HZ; - err = i2c_master_bus_add_device(bus_handle, &dev_config, &dev_handle); + // Safely attach the display device context onto OM Tom's infrastructure bus handle + esp_err_t err = i2c_master_bus_add_device(m_bus->get_bus_handle(), &dev_config, &m_devHandle); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to add OLED device to I2C bus: %s", esp_err_to_name(err)); - return; - } - - ESP_LOGI(TAG, "New I2C Master Bus and Device Handle registered successfully."); -} - - -void DisplayController::sendCommand(uint8_t command) { - if (dev_handle == nullptr) return; - - // the new driver handles transactions internally in a single, safe array write - uint8_t write_buf[2] = { - 0x00, // Control byte: Co=0, D/C=0 (Incoming bytes are commands) - command // The actual payload command - }; - - // Increased timeout to 50ms and replaced hard ESP_ERROR_CHECK with a safe warning log - esp_err_t ret = i2c_master_transmit(dev_handle, write_buf, sizeof(write_buf), pdMS_TO_TICKS(50)); - if (ret != ESP_OK) { - ESP_LOGW(TAG, "OLED Command 0x%02X failed or timed out: %s", command, esp_err_to_name(ret)); + ESP_LOGE(TAG, "Failed to register SSD1306 on the I2C Master Bus: %s", esp_err_to_name(err)); + m_devHandle = nullptr; } } -void DisplayController::initSSD1306() { - // If the hardware setup failed, don't run commands against a null handle - if (dev_handle == nullptr) { - ESP_LOGE(TAG, "Skipping SSD1306 init sequence because dev_handle is null."); - return; +/** + * @brief Destroy the Display Controller object, freeing up allocated queue and device resources. + */ +DisplayController::~DisplayController() { + if (m_devHandle != nullptr) { + i2c_master_bus_rm_device(m_devHandle); } - // Minimalistic SSD1306 initialization sequence for 128x64 OLEDs - sendCommand(0xAE); // Turn display off - sendCommand(0xD5); // Set display clock divide ratio - sendCommand(0x80); - sendCommand(0xA8); // Set multiplex ratio - sendCommand(0x3F); // 64MUX - sendCommand(0xD3); // Set display offset - sendCommand(0x00); - sendCommand(0x40); // Set display start line - sendCommand(0x8D); // Charge pump charge - sendCommand(0x14); // Enable charge pump - sendCommand(0xAF); // Turn display on + if (m_msgQueue != nullptr) { + vQueueDelete(m_msgQueue); + } +} - // clear screen via modern buffer transfer - uint8_t clear_buf[129]; - clear_buf[0] = 0x40; // Control byte: Data stream follows - memset(&clear_buf[1], 0x00, 128); // Fill frame buffer slice with zeros +/** + * @brief Internal helper to dispatch low-overhead raw hardware command sequences to the display. + * @param cmd The raw execution command configuration byte. + * @return esp_err_t status metric indicating whether the transfer succeeded. + */ +esp_err_t DisplayController::sendCommand(uint8_t cmd) const { + if (m_devHandle == nullptr) return ESP_ERR_INVALID_STATE; - // Clear screen natively by setting page addressing + // SSD1306 command stream control stream frame format payload byte array + uint8_t buffer[2] = { 0x00, cmd }; + return i2c_master_transmit(m_devHandle, buffer, sizeof(buffer), 100); +} + +/** + * @brief Triggers the fundamental register setup and executes a ultra-reliable high-speed screen wipe. + * @return esp_err_t status tracking variable collecting transaction anomalies. + */ +esp_err_t DisplayController::initializeHardware() { + if (m_devHandle == nullptr) return ESP_ERR_INVALID_STATE; + + esp_err_t err = ESP_OK; + + // Ultra-reliable fundamental minimal initialization sequence configuration steps + err |= sendCommand(0xAE); // Turn display off + err |= sendCommand(0xD5); // Set clock divide ratio + err |= sendCommand(0x80); + err |= sendCommand(0xA8); // Set multiplex ratio + err |= sendCommand(0x3F); // 1/64 duty cycle setup + err |= sendCommand(0xD3); // Set display offset + err |= sendCommand(0x00); // No offset + err |= sendCommand(0x40); // Set start line at 0 + err |= sendCommand(0x8D); // Charge pump configuration setup + err |= sendCommand(0x14); // Enable charge pump power rails + err |= sendCommand(0x20); // Set memory addressing mode profile + err |= sendCommand(0x02); // Page addressing mode (highly predictable translation) + err |= sendCommand(0xA1); // Segment re-map columns configuration matrix + err |= sendCommand(0xC8); // COM Output scan direction setting execution + err |= sendCommand(0xDA); // COM pins hardware structural layout profile + err |= sendCommand(0x12); + err |= sendCommand(0x81); // Contrast configuration mapping setting + err |= sendCommand(0x7F); + err |= sendCommand(0xA4); // Resume display to RAM mapping entries + err |= sendCommand(0xA6); // Normal display output formatting selection profile + + // High-Speed Screen Clear on startup to guarantee zero RAM noise artifacts for (uint8_t page = 0; page < 8; page++) { - sendCommand(0xB0 + page); // Set page address - sendCommand(0x00); // Set lower column address - sendCommand(0x10); // Set higher column address + err |= sendCommand(static_cast(0xB0 + page)); // Set page boundary + err |= sendCommand(0x00); // Reset lower column pointer + err |= sendCommand(0x10); // Reset higher column pointer - // FIXED: Replaced ESP_ERROR_CHECK with safe error-handling to prevent software panic crashes - esp_err_t ret = i2c_master_transmit(dev_handle, clear_buf, sizeof(clear_buf), pdMS_TO_TICKS(50)); - if (ret != ESP_OK) { - ESP_LOGE(TAG, "Failed to clear page %d buffer: %s. Aborting sequence.", page, esp_err_to_name(ret)); - return; // Exit cleanly, allowing the CPU to boot other tasks + // Allocate high-speed contiguous flash transmission block buffer (1 control byte + 128 data bytes) + uint8_t clearBuffer[129] = {0}; + clearBuffer[0] = 0x40; // Enforce explicit Data Mode streaming identifier token + + // The remaining 128 bytes are already natively initialized to 0x00 via the aggregate initializer {0} + if (m_devHandle != nullptr) { + err |= i2c_master_transmit(m_devHandle, clearBuffer, sizeof(clearBuffer), 50); } } - ESP_LOGI(TAG, "SSD1306 OLED Controller initialized and screen cleared."); + + err |= sendCommand(0xAF); // Wake up physical controller panel (display ON) + + return err; } +/** + * @brief Instantiates the static FreeRTOS messaging pipeline and deploys the background consumer thread onto Core 0. + * @param priority Task priority rank assignment metric handled by the scheduler. + */ void DisplayController::startTask(uint32_t priority) { - initI2C(); - initSSD1306(); + // Allocate the static thread-safe communication data path + m_msgQueue = xQueueCreate(8, sizeof(DisplayMessage)); + if (m_msgQueue == nullptr) { + ESP_LOGE(TAG, "Critical error: Could not allocate memory for display communication queue!"); + return; + } - // Create the message queue for up to 10 incoming text packages - m_msgQueue = xQueueCreate(10, sizeof(DisplayMessage)); - - // Spawn the worker thread on Core 0 (keeps Core 1 completely free for your Mesh engine) + // Deploy onto Core 0 alongside the terminal pipeline to keep Core 1 free for LoRa scheduling xTaskCreatePinnedToCore( DisplayController::taskTrampoline, - "OLED_RenderTask", - 3584, + "OLED_WorkerTask", + 4096, this, priority, nullptr, - 0 // Pinned to Core 0 + 0 ); } - -bool DisplayController::printMessage(const char* text, int row) { - if (m_msgQueue == nullptr) return false; +/** + * @brief Safe cross-core interface dispatching a text block onto the internal processing pipeline buffer. + * @param text Pointer to a standard null-terminated sequence of tracking information. + * @param line Selected hardware page index mapping from 0 to 7. + */ +void DisplayController::printMessage(const char* text, uint8_t line) { + if (m_msgQueue == nullptr || text == nullptr) return; DisplayMessage msg = {}; - strncpy(msg.text, text, sizeof(msg.text) - 1); - msg.row = row; + msg.targetLine = line; - // Non-blocking write into the queue from any external task thread - return (xQueueSend(m_msgQueue, &msg, pdMS_TO_TICKS(10)) == pdPASS); + // Thread-safe copy buffer sanitization sequence + strncpy(msg.content, text, sizeof(msg.content) - 1); + msg.content[sizeof(msg.content) - 1] = '\0'; + + // Asynchronous non-blocking dispatch push (bails instantly if full to prevent lockups) + xQueueSend(m_msgQueue, &msg, 0); } +/** + * @brief Static trampoline layer connecting the native C-based FreeRTOS scheduler directly back to our C++ runtime context. + * @param pvParameters Raw pointer mapping the reference context of the current instance tracking space. + */ void DisplayController::taskTrampoline(void* pvParameters) { auto* instance = static_cast(pvParameters); if (instance != nullptr) { @@ -138,30 +281,94 @@ void DisplayController::taskTrampoline(void* pvParameters) { vTaskDelete(nullptr); } +/** + * @brief Autonomous main task routing layer parsing compound UTF-8 string inputs and enforcing real-time automatic end-of-line wiping. + */ void DisplayController::run() { - DisplayMessage incomingMsg; - ESP_LOGI(TAG, "OLED Core Engine operational loop active."); + // Perform hardware initialization safely encapsulated inside the unique thread sequence boundary + esp_err_t init_status = initializeHardware(); + if (init_status != ESP_OK) { + ESP_LOGE(TAG, "Hardware setup failed completely. Execution layer aborted."); + return; + } - // Dynamic boot screen utilizing configuration metadata - printMessage("====================", 0); // Row 0: Top Frame + ESP_LOGI(TAG, "SSD1306 display initialized and listening for incoming render packages."); - // Format the title and version cleanly into Row 1 - char titleBuffer[32]; - snprintf(titleBuffer, sizeof(titleBuffer), "%s v%s", Config::PROJECT_TITLE, Config::PROJECT_VERSION); - printMessage(titleBuffer, 1); - - // Format the copyright and authors into Row 2 - char authorBuffer[32]; - snprintf(authorBuffer, sizeof(authorBuffer), "(c) %s", Config::PROJECT_AUTHORS); - printMessage(authorBuffer, 2); - - printMessage("====================", 3); // Row 3: Bottom Frame + DisplayMessage incoming = {}; while (true) { - // Deep sleep state: Task hits 0% CPU usage until a message arrives in the queue - if (xQueueReceive(m_msgQueue, &incomingMsg, portMAX_DELAY) == pdPASS) { - // Safe instrumentation logging (simulating the physical pixels) - ESP_LOGI(TAG, "[OLED ROW %d]: %s", incomingMsg.row, incomingMsg.text); + // Asynchronous structural block yield: consumes zero CPU until a new message package arrives + if (xQueueReceive(m_msgQueue, &incoming, portMAX_DELAY) == pdTRUE) { + if (incoming.targetLine > 7) incoming.targetLine = 7; + + // Positioning the internal hardware pointer inside the target page boundary + sendCommand(static_cast(0xB0 + incoming.targetLine)); // Set page address boundary + sendCommand(0x00); // Set lower column pointer + sendCommand(0x10); // Set higher column pointer + uint8_t totalBytesWritten = 0; + + // Basic built-in 5x7 ASCII micro character font generation layer + // This prevents adding bloating third party render dependencies into your tight stack space + size_t textLength = strlen(incoming.content); + + for (size_t i = 0; i < textLength; i++) { + uint8_t c_primary = static_cast(incoming.content[i]); + size_t fontIndex = 0; + bool validCharacter = false; + + // Basic UTF-8 multi-byte decoder for native German Umlaute translation + if (c_primary == 0xC3 && (i + 1) < textLength) { + uint8_t c_secondary = static_cast(incoming.content[i + 1]); + i++; // Advance the pointer to consume the compound UTF-8 token pair + + if (c_secondary == 0x84) { fontIndex = 95; validCharacter = true; } // Ä + if (c_secondary == 0x96) { fontIndex = 96; validCharacter = true; } // Ö + if (c_secondary == 0x9C) { fontIndex = 97; validCharacter = true; } // Ü + if (c_secondary == 0xA4) { fontIndex = 98; validCharacter = true; } // ä + if (c_secondary == 0xB6) { fontIndex = 99; validCharacter = true; } // ö + if (c_secondary == 0xBC) { fontIndex = 100; validCharacter = true; } // ü + } + else if (c_primary == 0xC2 && (i + 1) < textLength) { + uint8_t c_secondary = static_cast(incoming.content[i + 1]); + i++; + if (c_secondary == 0x9F) { fontIndex = 101; validCharacter = true; } // ß + } + else if (c_primary >= 32 && c_primary <= 126) { + fontIndex = static_cast(c_primary - 32); + validCharacter = true; + } + + if (validCharacter && totalBytesWritten < 122) { + uint8_t txBuffer[7] = {0}; + 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[6] = 0x00; // 1-pixel spacing between characters + + if (m_devHandle != nullptr) { + i2c_master_transmit(m_devHandle, txBuffer, sizeof(txBuffer), 50); + } + totalBytesWritten = static_cast(totalBytesWritten + 6); + } + } + + // AUTO-CLEAR: Fill the remaining horizontal page width with blank pixels to flush out trailing leftovers + if (totalBytesWritten < 128) { + uint8_t remainingBytes = static_cast(128 - totalBytesWritten); + + // Allocate a burst buffer with dynamic sizing plus control byte room + uint8_t flushBuffer[129] = {0}; + flushBuffer[0] = 0x40; // Data mode identifier + + if (m_devHandle != nullptr) { + // Send out the precise amount of zeros required to wipe the right margin clean + i2c_master_transmit(m_devHandle, flushBuffer, static_cast(remainingBytes + 1), 50); + } + } } } -} \ No newline at end of file +} diff --git a/src/display.hpp b/src/display.hpp index 577a4b4..da80f17 100644 --- a/src/display.hpp +++ b/src/display.hpp @@ -1,42 +1,56 @@ -#ifndef DISPLAY_HPP -#define DISPLAY_HPP +/** + * @file display.hpp + * @brief Thread-safe asynchronous SSD1306 OLED display controller for FoxMesh + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ +#pragma once + +#include "i2c.hpp" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" -#include "freertos/task.h" class DisplayController { public: - // Structure for the thread-safe communication queue - struct DisplayMessage { - char text[32]; // Buffer for short status messages (e.g., "LED ON", "Mesh Packet") - int row; // Target row on the screen (e.g., 0 to 3) - }; + explicit DisplayController(i2c* bus_pointer); + virtual ~DisplayController(); - DisplayController(); - virtual ~DisplayController() = default; - - // Deleted copy constructors for hardware safety + // Disallow copying to prevent duplicate hardware resource assignment DisplayController(const DisplayController&) = delete; DisplayController& operator=(const DisplayController&) = delete; - // Initializes I2C and spawns the background worker task on Core 0 - void startTask(uint32_t priority = 4); + /** + * @brief Spawns the internal FreeRTOS queue consumer task on Core 0. + * @param priority Scheduler task priority level. + */ + void startTask(uint32_t priority = 3); - // Thread-safe public method that other tasks call to print text - bool printMessage(const char* text, int row); + /** + * @brief Thread-safe non-blocking method to print a message to the OLED display. + * @param text Null-terminated string message (maximum 20 characters for simple lines). + * @param line Destination line identifier index (0 to 7 for page addressing mode). + */ + void printMessage(const char* text, uint8_t line); private: - static const char* TAG; - QueueHandle_t m_msgQueue; + // Internal structure mapping the safe asynchronous data pipeline + struct DisplayMessage { + char content[24]; + uint8_t targetLine; + }; - void initI2C(); - void initSSD1306(); - void sendCommand(uint8_t command); + i2c* m_bus; + i2c_master_dev_handle_t m_devHandle = nullptr; + QueueHandle_t m_msgQueue = nullptr; - // FreeRTOS C++ bridging mechanisms static void taskTrampoline(void* pvParameters); void run(); -}; -#endif // DISPLAY_HPP + // Hardware-level low-overhead command layer execution abstraction + esp_err_t sendCommand(uint8_t cmd) const; + esp_err_t initializeHardware(); +}; diff --git a/src/i2c.cpp b/src/i2c.cpp new file mode 100644 index 0000000..2f96c70 --- /dev/null +++ b/src/i2c.cpp @@ -0,0 +1,73 @@ +/** + * @file i2c.cpp + * @brief Thread-safe I2C Master Bus Implementation for FoxMesh + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ + +#include "i2c.hpp" +#include +#include "esp_log.h" + +static const char* TAG = "I2C"; + +i2c::i2c(int sda_pin, int scl_pin, uint32_t clk_speed_hz) { + i2c_master_bus_config_t bus_config = {}; + bus_config.i2c_port = I2C_NUM_0; + bus_config.sda_io_num = static_cast(sda_pin); // FIXED: Added explicit typecast to gpio_num_t + bus_config.scl_io_num = static_cast(scl_pin); // FIXED: Added explicit typecast to gpio_num_t + bus_config.clk_source = I2C_CLK_SRC_DEFAULT; + bus_config.glitch_ignore_cnt = 7; + bus_config.flags.enable_internal_pullup = 1; + + // Allocate resources for the new infrastructure-driven master driver + esp_err_t err = i2c_new_master_bus(&bus_config, &_bus_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize I2C Master Bus: %s", esp_err_to_name(err)); + _bus_handle = nullptr; + } else { + ESP_LOGI(TAG, "I2C Master Bus initialized successfully."); + } +} + +i2c::~i2c() { + if (_bus_handle != nullptr) { + i2c_del_master_bus(_bus_handle); + _bus_handle = nullptr; + } +} + +void i2c::scan() const { + if (_bus_handle == nullptr) { + printf("Error: I2C bus handle is null. Cannot scan.\r\n"); + return; + } + + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + printf("00: "); + + int devices_found = 0; + + // Standard I2C address space ranges from 0x03 to 0x77 + for (uint8_t addr = 0x03; addr < 0x78; addr++) { + if (addr % 16 == 0) { + printf("\r\n%02x:", addr); + } + + // Use low-overhead hardware probing instead of allocating permanent device handles + esp_err_t err = i2c_master_probe(_bus_handle, addr, 100); + + if (err == ESP_OK) { + printf(" %02x", addr); + devices_found++; + } else if (err == ESP_ERR_NOT_FOUND) { + printf(" --"); + } else { + printf(" Er"); // Bus lockup, collision or framing timeout error occurred + } + } + printf("\r\n\r\nScan finished. Found %d device(s).\r\n", devices_found); +} diff --git a/src/i2c.hpp b/src/i2c.hpp new file mode 100644 index 0000000..fafd0ae --- /dev/null +++ b/src/i2c.hpp @@ -0,0 +1,39 @@ +/** + * @file i2c.hpp + * @brief Thread-safe I2C Master Bus Wrapper for FoxMesh + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ + +#pragma once + +#include "driver/i2c_master.h" +#include "esp_err.h" + +class i2c { +public: + // Constructor initializes GPIO pins and configures the native ESP-IDF bus + i2c(int sda_pin, int scl_pin, uint32_t clk_speed_hz); + ~i2c(); + + // Disable copy semantics to prevent duplicate hardware resource assignment + i2c(const i2c&) = delete; + i2c& operator=(const i2c&) = delete; + + /** + * @brief Scans the network space for active I2C addresses. + * + * Probes the hardware address space from 0x03 to 0x77 using low-overhead + * commands and prints a formatted matrix directly to stdout. + */ + void scan() const; + + // Public getter to expose the low-level bus handle to concrete peripheral drivers + i2c_master_bus_handle_t get_bus_handle() const { return _bus_handle; } + +private: + i2c_master_bus_handle_t _bus_handle = nullptr; +}; diff --git a/src/led.cpp b/src/led.cpp index f56f303..d412930 100644 --- a/src/led.cpp +++ b/src/led.cpp @@ -1,57 +1,108 @@ +/** + * @file led.cpp + * @brief Thread-safe hardware LED controller task implementation with profile selection + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ + #include "led.hpp" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" #include "esp_log.h" #include "config.hpp" +#include // Enforces definitions of macro bounds like ULONG_MAX -// Initialize the static log tag const char* LedController::TAG = "LED"; LedController::LedController(gpio_num_t gpioPin) : m_gpioPin(gpioPin) { - // Hardware initialization inside the constructor guarantees safe state upon creation + // Core default profile initialized using standard configurations + m_currentProfile.intervalMs = static_cast(Config::BLINK_INTERVAL_MS); + m_currentProfile.pulseWidthMs = 30; + + // Instantiate a lightweight mutex token to guarantee strict isolation + m_mutexHandle = xSemaphoreCreateMutex(); + if (m_mutexHandle == nullptr) { + ESP_LOGE(TAG, "Fatal: Failed to allocate mutex tracking resources."); + } + gpio_reset_pin(m_gpioPin); gpio_set_direction(m_gpioPin, GPIO_MODE_OUTPUT); ESP_LOGI(TAG, "Hardware initialized on GPIO %d.", m_gpioPin); } +LedController::~LedController() { + if (m_mutexHandle != nullptr) { + vSemaphoreDelete(m_mutexHandle); + } +} + void LedController::startTask(uint32_t priority, BaseType_t coreId) { - // Pass 'this' (the pointer to the current instance) as the task parameter xTaskCreatePinnedToCore( LedController::taskTrampoline, "LEDTask", 3072, this, priority, - nullptr, + &m_taskHandle, coreId ); } +void LedController::updateProfile(const BlinkProfile& profile) { + if (profile.pulseWidthMs >= profile.intervalMs) { + ESP_LOGE(TAG, "Profile rejection: pulseWidthMs must be smaller than intervalMs!"); + return; + } + + // Safely block writing operations across separate core domains until token release + if (m_mutexHandle != nullptr && xSemaphoreTake(m_mutexHandle, portMAX_DELAY) == pdTRUE) { + m_currentProfile = profile; + xSemaphoreGive(m_mutexHandle); + } + + // Interrupt the active blocking state of the worker thread instantly + if (m_taskHandle != nullptr) { + xTaskNotifyGive(m_taskHandle); + } +} + void LedController::taskTrampoline(void* pvParameters) { - // Cast the raw object pointer back to our class instance auto* instance = static_cast(pvParameters); if (instance != nullptr) { instance->run(); } - - // Safety fallback: FreeRTOS tasks must never return natively vTaskDelete(nullptr); } void LedController::run() { ESP_LOGI(TAG, "FreeRTOS instance task successfully started on Core %d.", xPortGetCoreID()); - // Define the fixed ON-time as a local constant - constexpr uint32_t led_on_ms = 30; - - // Compile-time assertion to ensure the interval is always larger than the flash duration - static_assert(Config::BLINK_INTERVAL_MS > led_on_ms, "Global blink interval must be greater than 30ms!"); - while (true) { - gpio_set_level(m_gpioPin, 1); // LED on (High) - vTaskDelay(pdMS_TO_TICKS(led_on_ms)); // 30ms pulse width + BlinkProfile active = {}; - gpio_set_level(m_gpioPin, 0); // LED off (Low) - vTaskDelay(pdMS_TO_TICKS(Config::BLINK_INTERVAL_MS - led_on_ms)); // typ. 1970ms period low time + // Extract a thread-safe snapshot configuration boundary under lock condition + if (m_mutexHandle != nullptr && xSemaphoreTake(m_mutexHandle, portMAX_DELAY) == pdTRUE) { + active = m_currentProfile; + xSemaphoreGive(m_mutexHandle); + } else { + // Safe hardware fallback calculation loops + active.intervalMs = 1000; + active.pulseWidthMs = 30; + } + + if (active.pulseWidthMs == 0) active.pulseWidthMs = 1; + if (active.pulseWidthMs >= active.intervalMs) active.intervalMs = active.pulseWidthMs + 10; + + // 1. High pulse execution phase + gpio_set_level(m_gpioPin, 1); + vTaskDelay(pdMS_TO_TICKS(active.pulseWidthMs)); + gpio_set_level(m_gpioPin, 0); + + // 2. Dynamic evaluation of low-state processing window + uint32_t remainingLowTimeMs = active.intervalMs - active.pulseWidthMs; + + // Non-polling block sleep that awakes immediately upon configuration adjustments + xTaskNotifyWait(0x00, ULONG_MAX, nullptr, pdMS_TO_TICKS(remainingLowTimeMs)); } } diff --git a/src/led.hpp b/src/led.hpp index 4fc8c8f..352557f 100644 --- a/src/led.hpp +++ b/src/led.hpp @@ -1,33 +1,61 @@ -#ifndef LED_HPP -#define LED_HPP +/** + * @file led.hpp + * @brief Thread-safe hardware LED controller with dynamic profile configuration + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ + +#pragma once #include "driver/gpio.h" #include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" class LedController { public: - // Constructor allows passing the GPIO pin (defaults to 25 as requested) + // Compound structure holding the full runtime timing behavior profiles + struct BlinkProfile { + uint32_t intervalMs; // Total period duration of one cycle + uint32_t pulseWidthMs; // Active high execution duty duration (on-time) + }; + explicit LedController(gpio_num_t gpioPin = GPIO_NUM_25); + virtual ~LedController(); - // Destructor (virtual in case of inheritance) - virtual ~LedController() = default; - - // Disallow copying to prevent multiple instances controlling the same hardware pin + // Disallow copying to prevent multiple runtime instances controlling the same hardware rail LedController(const LedController&) = delete; LedController& operator=(const LedController&) = delete; - // Spawns the FreeRTOS task pinned to a specific core + /** + * @brief Spawns the internal FreeRTOS worker thread pinned to a specific core. + * @param priority Scheduler task priority level. + * @param coreId Destination CPU core ID anchor (0 or 1). + */ void startTask(uint32_t priority = 5, BaseType_t coreId = 1); + /** + * @brief Thread-safe atomic updates to configuration parameters from any thread core. + * @param profile Target values mapping the new timing configuration layers. + */ + void updateProfile(const BlinkProfile& profile); + private: const gpio_num_t m_gpioPin; static const char* TAG; + + // Track the internal task handle to allow cross-thread signaling via notifications + TaskHandle_t m_taskHandle = nullptr; + + // Mutex lock handle safeguarding cross-core profile modifications + SemaphoreHandle_t m_mutexHandle = nullptr; + + // Internal state profile configuration storage cache + BlinkProfile m_currentProfile; - // Static trampoline function required by the FreeRTOS scheduler static void taskTrampoline(void* pvParameters); - - // Dynamic instance method where the actual blinking loop lives void run(); }; - -#endif // LED_HPP diff --git a/src/main.cpp b/src/main.cpp index e514c17..264417d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,13 @@ +/** + * @file main.cpp + * @brief Main application entry point for the FoxMesh tracking node + * @author OM Tom (OE3TKT) + * + * @license SPDX-License-Identifier: MIT + * @copyright Copyright (c) 2026 OM Tom (OE3TKT) + * For the full license terms, see the LICENSE file in the project root. + */ + #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -7,18 +17,26 @@ #include "config.hpp" #include "led.hpp" #include "cli.hpp" -#include "display.hpp" // thread-safe display +#include "i2c.hpp" +#include "display.hpp" // FIXED: Added missing header for DisplayController static const char *TAG = "MAIN"; -// Global singleton instances for hardware abstraction -static DisplayController oled; +// Global pointer to the thread-safe hardware abstraction instances +i2c* i2c_bus = nullptr; +DisplayController* display = nullptr; + +// Global singleton instances for hardware abstraction layers static LedController led(Config::LED_GPIO_PIN); +/** + * @brief Initializes the physical SPI peripheral for the Semtech SX1276 transceiver. + * + * Configures VSPI (SPI3_HOST) using strict compile-time configurations from config.hpp. + */ void init_lora_spi(void) { esp_err_t ret; - // Configure the SPI structure using the strict C++ constexpr constants spi_bus_config_t buscfg = {}; buscfg.mosi_io_num = Config::LORA_MOSI_PIN; buscfg.miso_io_num = Config::LORA_MISO_PIN; @@ -27,7 +45,7 @@ void init_lora_spi(void) { buscfg.quadhd_io_num = -1; buscfg.max_transfer_sz = 0; - // Initialize the physical peripheral (VSPI / SPI3 on ESP32) + // Initialize the physical peripheral bus with automatic DMA channel assignment ret = spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(ret)); @@ -36,42 +54,47 @@ void init_lora_spi(void) { ESP_LOGI(TAG, "SPI bus initialized successfully on VSPI (SPI3)"); } -// Fixed: Only ONE app_main exists now, marked as extern "C" for ESP-IDF bootloader -extern "C" void app_main(void) { - // Dynamic string formatting using type-safe constants - ESP_LOGI(TAG, "================================================="); - ESP_LOGI(TAG, " Booting %s (Version: %s) via ESP-IDF C++", +// Global application anchor marked as extern "C" for the native ESP-IDF bootloader +extern "C" { + void app_main(void) { + // Output system banner configuration parameters on boot + ESP_LOGI(TAG, "================================================="); + ESP_LOGI(TAG, " Booting %s (Version: %s) via ESP-IDF C++", Config::PROJECT_TITLE, Config::PROJECT_VERSION); - ESP_LOGI(TAG, " (c) Authors: %s", Config::PROJECT_AUTHORS); - ESP_LOGI(TAG, "================================================="); + ESP_LOGI(TAG, " (c) Authors: %s", Config::PROJECT_AUTHORS); + ESP_LOGI(TAG, "================================================="); - // Explicitly power up the onboard peripherals on TTGO T3 -// gpio_reset_pin(GPIO_NUM_16); -// gpio_set_direction(GPIO_NUM_16, GPIO_MODE_OUTPUT); - // Hard reset of the OLED display -// gpio_set_level(GPIO_NUM_16, 0); - // vTaskDelay(pdMS_TO_TICKS(50)); // 50ms in Reset + // Launch background blink task + led.startTask(); -// gpio_set_level(GPIO_NUM_16, 1); // Set HIGH to enable OLED power rails -// vTaskDelay(pdMS_TO_TICKS(100)); // Give the hardware 100ms to stabilize voltage + // Dynamically instantiate the synchronized I2C master bus + i2c_bus = new i2c(Config::OLED_SDA_PIN, Config::OLED_SCL_PIN, Config::OLED_I2C_FREQ_HZ); - led.startTask(); + // Instantiate and activate the isolated background render worker on Core 0 + display = new DisplayController(i2c_bus); + display->startTask(); - start_cli_task(); + // Pass the bus instance dependency to the interactive terminal layer + cli_init(i2c_bus, &led, display); - oled.startTask(); + // Initialize the Semtech LoRa SPI connection layer + init_lora_spi(); + // Spin up the background console interface thread on Core 0 + start_cli_task(); - // Synchronous test push to the display queue -// oled.printMessage("Mesh Scanning...", 2); + // Trigger synchronous welcome splash screen frame + display->printMessage("FoxMesh Ready", 0); - // Sytem Monitor Loop - while (true) { - ESP_LOGI(TAG, "[%s v%s] Operational. Free Heap: %lu bytes", + // Main system watchdog and status monitor loop + while (true) { + ESP_LOGI(TAG, "[%s v%s] Operational. Free Heap: %lu bytes", Config::PROJECT_TITLE, Config::PROJECT_VERSION, (unsigned long)esp_get_free_heap_size()); - vTaskDelay(pdMS_TO_TICKS(5000)); + + vTaskDelay(pdMS_TO_TICKS(5000)); + } } }