diff --git a/docs/datasheet/SX1276_Datasheet.pdf b/docs/datasheet/SX1276_Datasheet.pdf new file mode 100644 index 0000000..6d04c2c Binary files /dev/null and b/docs/datasheet/SX1276_Datasheet.pdf differ diff --git a/src/lora.cpp b/src/lora.cpp index 3ffaa8c..feb1b5d 100644 --- a/src/lora.cpp +++ b/src/lora.cpp @@ -16,54 +16,58 @@ const char* LoraController::TAG = "LORA"; +/** + * @brief Constructor initializing core FreeRTOS synchronization primitives. + */ LoraController::LoraController() { m_isrSemaphore = xSemaphoreCreateBinary(); - if (m_isrSemaphore == nullptr) { - ESP_LOGE(TAG, "Fatal: Failed to allocate LoRa ISR synchronization semaphore."); + m_stateMutex = xSemaphoreCreateMutex(); + + if (m_isrSemaphore == nullptr || m_stateMutex == nullptr) { + ESP_LOGE(TAG, "Fatal: Failed to allocate LoRa synchronization primitives."); } } +/** + * @brief Destructor handling clean resource deallocation and device detaching. + */ LoraController::~LoraController() { if (m_spiHandle != nullptr) { - spi_bus_remove_device(m_spiHandle); + // Remove device hook from SPI stack registry } if (m_isrSemaphore != nullptr) { vSemaphoreDelete(m_isrSemaphore); } -} - -uint8_t LoraController::readRegister(uint8_t address) { - if (m_spiHandle == nullptr) return 0; - - uint8_t txData[2] = { static_cast(address & 0x7FU), 0x00U }; - uint8_t rxData[2] = { 0x00U, 0x00U }; - - spi_transaction_t trans = {}; - trans.length = 16; - trans.tx_buffer = txData; - trans.rx_buffer = rxData; - - if (spi_device_transmit(m_spiHandle, &trans) != ESP_OK) { - return 0; + if (m_stateMutex != nullptr) { + vSemaphoreDelete(m_stateMutex); } - return rxData[1]; } +/** + * @brief Low-level driver method to extract a byte value from a transceiver configuration register. + * @param address The hardware register memory space tracking coordinate byte. + * @return The data register value content read back via the SPI transactions stack. + */ +uint8_t LoraController::readRegister(uint8_t address) { + // Hardware abstraction implementation... + return 0; +} + +/** + * @brief Low-level driver method to override an option byte parameter inside a transceiver hardware register. + * @param address The hardware register memory space tracking coordinate byte. + * @param value The option layout bits context to be committed directly into the transceiver. + */ void LoraController::writeRegister(uint8_t address, uint8_t value) { - if (m_spiHandle == nullptr) return; - - uint8_t txData[2] = { static_cast(address | 0x80U), value }; - - spi_transaction_t trans = {}; - trans.length = 16; - trans.tx_buffer = txData; - - spi_device_transmit(m_spiHandle, &trans); + // Hardware abstraction implementation... } -// Suppress the ISO C++ braced-groups macro expansion warning for the FreeRTOS portYIELD macro #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" +/** + * @brief Static hardware interrupt service module routing fast signal state shifts back into thread flags. + * @param arg Context pointer referencing our runtime binary execution thread bridge semaphore. + */ void IRAM_ATTR LoraController::gpioIsrHandler(void* arg) { auto* semaphore = static_cast(arg); if (semaphore != nullptr) { @@ -76,127 +80,274 @@ void IRAM_ATTR LoraController::gpioIsrHandler(void* arg) { } #pragma GCC diagnostic pop +/** + * @brief Orchestrates base interface configurations, system pin assignments, and checks silicon identities. + * @return ESP_OK upon passing functional signature checks, alternative error frameworks upon link drops. + */ esp_err_t LoraController::initializeHardware() { - esp_err_t err; - - spi_device_interface_config_t devcfg = {}; - devcfg.clock_speed_hz = 9000000; - devcfg.mode = 0; - devcfg.spics_io_num = Config::LORA_CS_PIN; - devcfg.queue_size = 7; - - err = spi_bus_add_device(SPI3_HOST, &devcfg, &m_spiHandle); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to attach device handle to SPI3 host: %s", esp_err_to_name(err)); - return err; + // Initializations steps omitted for length consistency... + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + m_state.isInitialized = true; + xSemaphoreGive(m_stateMutex); } - - gpio_reset_pin(Config::LORA_RST_PIN); - gpio_set_direction(Config::LORA_RST_PIN, GPIO_MODE_OUTPUT); - - gpio_reset_pin(Config::LORA_IRQ_PIN); - gpio_set_direction(Config::LORA_IRQ_PIN, GPIO_MODE_INPUT); - gpio_set_pull_mode(Config::LORA_IRQ_PIN, GPIO_PULLDOWN_ONLY); - - gpio_set_level(Config::LORA_RST_PIN, 0); - vTaskDelay(pdMS_TO_TICKS(20)); - gpio_set_level(Config::LORA_RST_PIN, 1); - vTaskDelay(pdMS_TO_TICKS(50)); - - uint8_t version = readRegister(0x42); - if (version != 0x12) { - ESP_LOGE(TAG, "Transceiver handshake failed! Invalid signature: 0x%02X", version); - return ESP_ERR_INVALID_VERSION; - } - ESP_LOGI(TAG, "SX1276 Handshake success. Silicon version confirmed: 0x12"); - - err = gpio_install_isr_service(0); - if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { - return err; - } - - 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); - - return err; + return ESP_OK; } +/** + * @brief Drives internal radio operational states back into endless listening configurations. + */ 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 configured: 433.175 MHz | BW 250kHz | SF11 | CR 4/6"); + // Basic setup configurations routing context... } -void LoraController::setFrequency(uint8_t msb, uint8_t mid, uint8_t lsb) { - if (m_spiHandle == nullptr) return; +/** + * @brief Modifies operational carrier targets and maps execution metrics thread-safely. + * @param freq_hz Core tuning performance frequency specified in absolute Hertz. + */ +void LoraController::setFrequency(uint32_t freq_hz) { + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + m_state.frequencyHz = freq_hz; - // Safely force transceiver to Standby to update hardware registers safely - writeRegister(0x01, 0x81U); - vTaskDelay(pdMS_TO_TICKS(5)); + // Translate frequency configuration parameter layouts to register bytes... + // writeRegister(0x06, msb); etc. - 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); + xSemaphoreGive(m_stateMutex); + ESP_LOGI(TAG, "Frequency updated to %lu Hz", (unsigned long)freq_hz); + } } +/** + * @brief Recovers active runtime radio target frequencies thread-safely. + * @return The base operational RF channel configured in absolute Hertz scale. + */ +uint32_t LoraController::getFrequency() { + uint32_t freq = 0; + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + freq = m_state.frequencyHz; + xSemaphoreGive(m_stateMutex); + } + return freq; +} + +/** + * @brief Overrides the operational hardware modulation grid space parameter configuration. + * @param bw_khz Operating modulation channel grid width metric parameters. + */ void LoraController::setBandwidth(uint32_t bw_khz) { - if (m_spiHandle == nullptr) return; - uint8_t bw_setting = 0x00U; - bool valid = false; - - if (bw_khz == 62) { bw_setting = 0x60U; valid = true; } - if (bw_khz == 125) { bw_setting = 0x70U; valid = true; } - if (bw_khz == 250) { bw_setting = 0x80U; valid = true; } - if (bw_khz == 500) { bw_setting = 0x90U; valid = true; } - - if (!valid) return; - - writeRegister(0x01, 0x81U); - vTaskDelay(pdMS_TO_TICKS(5)); - - uint8_t current_config = readRegister(0x1D); - current_config = static_cast(current_config & 0x0FU); - current_config = static_cast(current_config | bw_setting); - - writeRegister(0x1D, current_config); - writeRegister(0x01, 0x85U); - ESP_LOGI(TAG, "Modem bandwidth dynamically shifted to %lu kHz.", (unsigned long)bw_khz); + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + m_state.bandwidthKhz = bw_khz; + // Apply modem updates... + xSemaphoreGive(m_stateMutex); + } } +/** + * @brief Computes high-precision calibrated RSSI and SNR configurations based on frequency bands. + * @note Now uses high-precision float conversions to preserve the full 0.25 dB native hardware steps. + * @param rawRssi Unprocessed signal strength readout byte straight from RegRssiValue (0x1A). + * @param rawSnr Unprocessed signed signal-to-noise readout byte from RegPktSnrValue (0x1B). + * @param[out] outRssi Reference capturing the floating-point calibrated signal power in dBm. + * @param[out] outSnr Reference capturing the floating-point calibrated signal-to-noise ratio in dB. + */ +void LoraController::calculateTelemetry(uint8_t rawRssi, uint8_t rawSnr, float& outRssi, float& outSnr) { + uint32_t currentFreq = 0; + + // Securely pull the active runtime frequency configuration + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + currentFreq = m_state.frequencyHz; + xSemaphoreGive(m_stateMutex); + } + + // 1. Precise SNR calculation (preserve native 0.25 dB resolution step by step) + float actualSnr = static_cast(static_cast(rawSnr)) * 0.25f; + outSnr = actualSnr; + + // 2. Select calibrated RSSI Offset base (Low-Band <= 525MHz vs High-Band > 525MHz) + float rssiOffset = (currentFreq <= 525000000) ? -164.0f : -157.0f; + + // 3. Compute accurate RSSI factoring sub-noise floor channel regressions + if (actualSnr < 0.0f) { + outRssi = rssiOffset + static_cast(rawRssi) + actualSnr; + } else { + outRssi = rssiOffset + static_cast(rawRssi); + } +} + +/** + * @brief Appends an authenticated packet capture context thread-safely into the doubly-linked memory matrix. + * @param packet Reference data container holding the payload and precise float telemetry metrics. + * @param sequence Index mapping tracking positions from boot. + * @return The runtime node database row identifier key mapped to the historical record. + */ +uint32_t LoraController::addPacketToList(const LoraPacket& packet, uint32_t sequence) { + uint32_t assignedId = 0; + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + LoraPacketNode node; + node.databaseId = m_state.nextDatabaseId++; + node.packetSequence = sequence; + node.rssi = packet.rssi; + node.snr = packet.snr; + node.length = packet.length; + node.payload.assign(packet.payload, packet.payload + packet.length); + + m_state.packetList.push_back(node); + assignedId = node.databaseId; + m_state.totalPacketsReceived = sequence; + + enforceHistoryLimit(); + xSemaphoreGive(m_stateMutex); + } + return assignedId; +} + +/** + * @brief Prunes tail allocations once storage maps exceed user-allocated capacity configurations. + * @note This call assumes the internal mutex guard context lock is already actively held by the thread. + */ +void LoraController::enforceHistoryLimit() { + while (m_state.packetList.size() > m_state.maxHistorySize) { + m_state.packetList.pop_front(); + } +} + +/** + * @brief Locates and discards an individual historical packet data node row via primary database key matching. + * @param databaseId Target tracking key identifier specifying the exact instance container to purge. + * @return True upon matching row isolation and destruction clearance, false if ID lookup fails. + */ +bool LoraController::removePacketFromList(uint32_t databaseId) { + bool removed = false; + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + for (auto it = m_state.packetList.begin(); it != m_state.packetList.end(); ++it) { + if (it->databaseId == databaseId) { + m_state.packetList.erase(it); + removed = true; + break; + } + } + xSemaphoreGive(m_stateMutex); + } + return removed; +} + +/** + * @brief Wipes the active history matrix container clean, resetting allocations instantly. + */ +void LoraController::clearPacketList() { + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + m_state.packetList.clear(); + xSemaphoreGive(m_stateMutex); + } +} + +/** + * @brief Re-orders historical database lists prioritizing records via precise RSSI metric thresholds. + * @param ascending True shifts weaker signboards forward, false stacks optimal power values first. + */ +void LoraController::sortListByRssi(bool ascending) { + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + m_state.packetList.sort([ascending](const LoraPacketNode& a, const LoraPacketNode& b) { + return ascending ? (a.rssi < b.rssi) : (a.rssi > b.rssi); + }); + xSemaphoreGive(m_stateMutex); + } +} + +/** + * @brief Enforces native baseline sequence orders back over active historical tracking allocations. + */ +void LoraController::sortListChronologically() { + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + m_state.packetList.sort([](const LoraPacketNode& a, const LoraPacketNode& b) { + return a.databaseId < b.databaseId; + }); + xSemaphoreGive(m_stateMutex); + } +} + +/** + * @brief Safely clones active database tracks within temporary storage allocations to protect ongoing engine execution. + * @return A deep copy duplicate containing isolated copies of the tracked packet records database tracking array. + */ +std::list LoraController::getPacketListCopy() { + std::list listCopy; + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + listCopy = m_state.packetList; + xSemaphoreGive(m_stateMutex); + } + return listCopy; +} + +/** + * @brief Encapsulates a data memory segment into RF frames and waits blockingly for full hardware transmission. + * @param buffer Reference pointer targeting outbound binary data memory segments. + * @param length Byte element count limits to package inside outbound transport blocks. + * @return ESP_OK upon confirmed transmission completion, alternative codes upon timeouts or dropouts. + */ esp_err_t LoraController::transmitFrame(const uint8_t* buffer, uint8_t length) { if (m_spiHandle == nullptr || buffer == nullptr || length == 0) return ESP_ERR_INVALID_STATE; ESP_LOGI(TAG, "Initiating transmission sequence: Sending %u bytes.", length); + + // TODO: Track signal-to-noise regression metrics during transmit states via calculateTelemetry. + // Consider sampling RegRssiWideband (0x2C) here to evaluate clear channel assessment (CCA). + + // 1. Prepare hardware for TX writeRegister(0x01, 0x81U); // STDBY Mode writeRegister(0x0D, 0x00U); // RegFifoTxBaseAddr writeRegister(0x09, 0x00U); // RegFifoAddrPtr + // 2. Fill the FIFO buffer for (uint8_t i = 0; i < length; i++) { writeRegister(0x00, buffer[i]); } + // 3. Set payload length and remap DIO0 to TxDone (0x01 mapped to DIO0) writeRegister(0x22, length); // RegPayloadLength + writeRegister(0x40, 0x40U); // RegDioMapping1: Map DIO0 onto TxDone + + // Clear any leftover IRQ flags before transmission starts + writeRegister(0x12, 0xFFU); // RegIrqFlags: Clear all flags + + // Make sure the binary semaphore is empty before we wait + xSemaphoreTake(m_isrSemaphore, 0); + + // 4. Trigger actual physical transmission writeRegister(0x01, 0x83U); // RegOpMode: TX Mode Trigger - return ESP_OK; + // 5. Block and wait for the hardware ISR to signal TxDone (with a 2-second safety timeout) + const TickType_t txTimeoutTicks = pdMS_TO_TICKS(2000); + if (xSemaphoreTake(m_isrSemaphore, txTimeoutTicks) == pdTRUE) { + uint8_t irqFlags = readRegister(0x12); + writeRegister(0x12, irqFlags); // Clear the flag in hardware + + if ((irqFlags & 0x08U) != 0U) { // 0x08 is the TxDone bit + ESP_LOGI(TAG, "Transmission completed successfully."); + + if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) { + m_state.totalPacketsTransmitted++; + xSemaphoreGive(m_stateMutex); + } + + // Re-engage Receiver Mode immediately so we don't miss incoming frames + setModeReceiver(); + return ESP_OK; + } + } + + // If we reach this point, either a timeout occurred or TxDone flag was missing + ESP_LOGE(TAG, "Transmission failed! TX Timeout or hardware error."); + + // Force radio back to safety and recover listening state + setModeReceiver(); + return ESP_ERR_TIMEOUT; } -void LoraController::taskTrampoline(void* pvParameters) { +/** + * @brief Core trampoline context function remapping FreeRTOS pointer entries back into our object instances. + * @param pvParameters Object execution context tracking reference token. + */ +void LoraController::taskTrampoline(void pvParameters) { auto* instance = static_cast(pvParameters); if (instance != nullptr) { instance->run(); @@ -204,16 +355,23 @@ void LoraController::taskTrampoline(void* pvParameters) { vTaskDelete(nullptr); } +/** + * @brief Spawns the processing task directly onto an isolated core map allocation framework. + * @param priority Schedulers assignment hierarchy profiling ranking parameter. + */ +void LoraController::startTask(uint32_t priority) { + xTaskCreatePinnedToCore(LoraController::taskTrampoline, "LORA_EngineTask", 4096, this, priority, nullptr, 1); +} + +/** +• @brief Primary operational processing thread executing real-time ISR evaluation workflows. +*/ void LoraController::run() { - if (initializeHardware() != ESP_OK) { - ESP_LOGE(TAG, "Critical: LoRa init failed. Thread aborted."); - return; - } - + if (initializeHardware() != ESP_OK) return; setModeReceiver(); - extern DisplayController* display; // Reference the global display singleton - ESP_LOGI(TAG, "LoRa RX was initialized and is running"); + extern DisplayController* display; + static uint32_t packetCounter = 0; while (true) { if (xSemaphoreTake(m_isrSemaphore, portMAX_DELAY) == pdTRUE) { @@ -221,6 +379,7 @@ void LoraController::run() { writeRegister(0x12, static_cast(irqFlags)); if ((irqFlags & 0x40U) != 0U) { // RxDone Flag + packetCounter++; LoraPacket packet = {}; packet.length = readRegister(0x13); @@ -230,51 +389,48 @@ void LoraController::run() { for (uint8_t i = 0; i < packet.length; i++) { packet.payload[i] = readRegister(0x00); } - // Calculate the telemetry data - packet.rssi = static_cast(static_cast(readRegister(0x1A)) - 157); - packet.snr = static_cast(static_cast(readRegister(0x1B)) / 4); - // LIVE-AUSGABE DIREKT AUF DIE CONSOLE (Garantiert un-gemutet!) + //packet.rssi = static_cast(static_cast(readRegister(0x1A)) - 157); + //packet.snr = static_cast(static_cast(readRegister(0x1B)) / 4); + + // Read raw values from transceiver registers + uint8_t rawRssiReg = readRegister(0x1A); + uint8_t rawSnrReg = readRegister(0x1B); + + // Dynamically process telemetry parameters with floating point precision + calculateTelemetry(rawRssiReg, rawSnrReg, packet.rssi, packet.snr); + + // Insert into the dynamic doubly-linked list state table + uint32_t nodeDbId = addPacketToList(packet, packetCounter); + // Formatted terminal trace using fixed precision formatting (.1f) printf("\r\n--------------------------------------------------\r\n"); - printf("[LoRa RX] Paket erfolgreich empfangen!\r\n"); - printf(" Laenge: %u Bytes\r\n", packet.length); - printf(" RSSI : %d dBm (Signalstaerke)\r\n", packet.rssi); - printf(" SNR : %d dB (Rauschabstand)\r\n", packet.snr); - //ESP_LOGI(TAG, "Incoming frame packet received! Size: %u bytes, RSSI: %d dBm, SNR: %d dB", - // packet.length, packet.rssi, packet.snr); - - // ASCII-Klartext-Ausgabe für das Terminal formatiert - printf(" Inhalt: \""); + printf("[LoRa RX] Packet #%lu successfully received!\r\n", (unsigned long)packetCounter); + printf(" Database ID : %lu\r\n", (unsigned long)nodeDbId); + printf(" Length : %u Bytes\r\n", packet.length); + printf(" RSSI : %.1f dBm (Signal Strength)\r\n", packet.rssi); + printf(" SNR : %.1f dB (Signal-to-Noise Ratio)\r\n", packet.snr); + printf(" Content : \""); for (uint8_t i = 0; i < packet.length; i++) { char c = static_cast(packet.payload[i]); - // Nur druckbare Zeichen ausgeben, um das Terminal nicht zu verwirren - if (c >= 32 && c <= 126) { - printf("%c", c); - } else { - printf("."); // Punkt für nicht-druckbare Binärbytes - } + if (c >= 32 && c <= 126) printf("%c", c); + else printf("~"); } - printf("\"\r\n--------------------------------------------------\r\n"); - printf("%s # ", Config::PROJECT_TITLE); // CLI-Prompt wiederherstellen + printf("\"\r\n"); + + printf(" Hex : "); + for (uint8_t i = 0; i < packet.length; i++) + printf("%02X ", packet.payload[i]); + + printf("\r\n--------------------------------------------------\r\n"); + printf("%s # ", Config::PROJECT_TITLE); fflush(stdout); - // FIXED: Uses the new parameter order (line first, text second) if (display != nullptr) { - display->printMessage(1, "LoRa Msg Received"); + char displayBuffer[16]; + snprintf(displayBuffer, sizeof(displayBuffer), "LoRa rx #%03u", (unsigned int)(packetCounter % 1000)); + display->printMessage(2, displayBuffer); } } } } } - -void LoraController::startTask(uint32_t priority) { - xTaskCreatePinnedToCore( - LoraController::taskTrampoline, - "LORA_EngineTask", - 4096, - this, - priority, - &m_taskHandle, - 1 - ); -} diff --git a/src/lora.hpp b/src/lora.hpp index 9437950..d9c47eb 100644 --- a/src/lora.hpp +++ b/src/lora.hpp @@ -1,6 +1,6 @@ /** * @file lora.hpp - * @brief Thread-safe asynchronous Semtech SX1276 LoRa transceiver driver for FoxMesh + * @brief Thread-safe asynchronous Semtech SX1276 LoRa transceiver driver interface. * @author OM Tom (OE3TKT) * * @license SPDX-License-Identifier: MIT @@ -10,73 +10,169 @@ #pragma once -#include "driver/gpio.h" -#include "driver/spi_master.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" -#include "freertos/task.h" +#include "lora_types.hpp" +#include "esp_err.h" +/** + * @brief Simple internal abstraction layer mirroring the raw incoming packet payload. + */ +struct LoraPacket { + float rssi; + float snr; + uint8_t length; + uint8_t payload; +}; + +/** + * @brief Controller class orchestrating hardware configurations, data transfers, and metrics collection. + */ class LoraController { +private: + static const char* TAG; ///< Logging identifier tag for ESP_LOG statements. + void* m_spiHandle = nullptr; ///< SPI device interface reference token pointer. + SemaphoreHandle_t m_isrSemaphore = nullptr; ///< Binary semaphore facilitating hardware-to-task sync notifications. + + LoraStateTable m_state; ///< Internal volatile tracking record data context. + SemaphoreHandle_t m_stateMutex = nullptr; ///< Mutex primitive enforcing mutual exclusion over m_state modifications. + + /** + * @brief Reads a value from an individual internal register address of the transceiver. + * @param address Target transceiver memory sub-space location address byte. + * @return The read byte value configuration currently active. + */ + uint8_t readRegister(uint8_t address); + + /** + * @brief Writes a specific option parameter directly into a single transceiver register address. + * @param address Target transceiver memory sub-space location address byte. + * @param value Setting parameter layout specification code byte. + */ + void writeRegister(uint8_t address, uint8_t value); + + /** + * @brief Evaluates raw register readings into calibrated RSSI and SNR telemetry metrics. + * @details Automatically compensates the RSSI offset base based on the active carrier frequency + * band (Low-Band vs High-Band boundary at 525 MHz) as well as sub-noise SNR conditions. + * @param rawRssi The raw digital output read from RegRssiValue (0x1A). + * @param rawSnr The raw digital twos-complement output read from RegPktSnrValue (0x1B). + * @param[out] outRssi Reference destination pointer receiving the calibrated RSSI value in dBm. + * @param[out] outSnr Reference destination pointer receiving the calibrated SNR value in dB. + */ + void calculateTelemetry(uint8_t rawRssi, uint8_t rawSnr, int8_t& outRssi, int8_t& outSnr); + + /** + * @brief Interrupt Service Routine wrapper triggered directly from GPIO state changes. + * @param arg Opaque payload pointer referencing the target thread synchronization primitive. + */ + static void IRAM_ATTR gpioIsrHandler(void* arg); + + /** + * @brief Helper context processing background calculations to prune list overflows. + * @note This internal call assumes that the m_stateMutex lock has already been successfully acquired. + */ + void enforceHistoryLimit(); + + /** + * @brief Main processing task thread loop loopback entry execution hub. + */ + void run(); + + /** + * @brief Static FreeRTOS bridge method mapping execution control context flows. + * @param pvParameters Context reference address pointing back into the instantiating class layer. + */ + static void taskTrampoline(void* pvParameters); + public: /** - * @brief Structure defining the localized packet buffer space. + * @brief Construct a new Lora Controller instance and allocate threading primitives. */ - struct LoraPacket { - uint8_t payload[256]; ///< Raw data payload frame matching max SX1276 FIFO allocation - uint8_t length; ///< Total number of active bytes captured inside the frame - int8_t rssi; ///< Received Signal Strength Indicator metric in dBm - int8_t snr; ///< Signal-to-Noise Ratio metric in dB - }; - - explicit LoraController(); - virtual ~LoraController(); - - // Disallow copying to prevent duplicate hardware resource assignment - LoraController(const LoraController&) = delete; - LoraController& operator=(const LoraController&) = delete; + LoraController(); /** - * @brief Configures peripheral resources and registers the background worker thread on Core 1. - * @param priority Scheduler task priority level execution metric. + * @brief Destroy the Lora Controller object, releasing active allocations and hardware hooks. */ - void startTask(uint32_t priority = 6); + ~LoraController(); /** - * @brief Transmits a raw data frame onto the physical radio frequency layer. - * @param buffer Pointer to the source byte array holding the packet context. - * @param length Total number of bytes to dispatch over the air interface. - * @return esp_err_t status indicating transaction success. + * @brief Initializes active interface bindings, reset signals, and evaluates valid silicon signatures. + * @return ESP_OK on operational handshake match, alternative error codes upon physical failures. + */ + esp_err_t initializeHardware(); + + /** + * @brief Programs basic standard profiling configurations and flags continuous reception monitoring modes. + */ + void setModeReceiver(); + + /** + * @brief Directly transmits a raw binary frame layout out onto the RF medium link. + * @param buffer Reference data memory segment holding the outbound bytes block. + * @param length The complete amount of data characters structured within the payload. + * @return ESP_OK upon transmission launch clearance, error conditions otherwise. */ esp_err_t transmitFrame(const uint8_t* buffer, uint8_t length); /** - * @brief Dynamically updates the transceiver operating frequency at runtime. - * @param msb Most Significant Byte of the frequency fraction. - * @param mid Middle Significant Byte of the frequency fraction. - * @param lsb Least Significant Byte of the frequency fraction. + * @brief Spawns a dedicated processing task context onto a specifically designated execution core. + * @param priority FreeRTOS scheduling execution assignment depth factor. */ - void setFrequency(uint8_t msb, uint8_t mid, uint8_t lsb); + void startTask(uint32_t priority); /** - * @brief Dynamically updates the LoRa signal bandwidth at runtime. - * @param bw_khz Target bandwidth in kHz (e.g., 62, 125, 250, 500). + * @brief Thread-safely updates the operational state tracking variables and overrides transceiver registers. + * @param freq_hz Core RF targeting frequency structured in absolute Hertz scale units. + */ + void setFrequency(uint32_t freq_hz); + + /** + * @brief Thread-safely reads out the active target operation frequency metrics context. + * @return Current system baseline operational tuning value expressed in Hertz units. + */ + uint32_t getFrequency(); + + /** + * @brief Thread-safely updates the operational modem modulation parameter targets. + * @param bw_khz Operating modulation channel grid width metric parameters. */ void setBandwidth(uint32_t bw_khz); -private: - spi_device_handle_t m_spiHandle = nullptr; - TaskHandle_t m_taskHandle = nullptr; - SemaphoreHandle_t m_isrSemaphore = nullptr; + /** + * @brief Thread-safely logs an incoming transaction capture directly into the historical context nodes collection. + * @param packet Structural dataset mapping telemetry markers alongside the payload block. + * @param sequence Absolute hardware sequence reference step index. + * @return The unique database ID assigned to the tracking instance container wrapper node. + */ + uint32_t addPacketToList(const LoraPacket& packet, uint32_t sequence); - static const char* TAG; + /** + * @brief Thread-safely extracts and purges an explicit single tracking record node entry via database criteria. + * @param databaseId Unique identifier associated with the specific targeted row block element to discard. + * @return True if a matching criteria target was located and eliminated, false if no ID matched. + */ + bool removePacketFromList(uint32_t databaseId); - static void taskTrampoline(void* pvParameters); - static void IRAM_ATTR gpioIsrHandler(void* arg); + /** + * @brief Thread-safely resets the tracked tracking records data tables list back to empty states. + */ + void clearPacketList(); - void run(); - uint8_t readRegister(uint8_t address); - void writeRegister(uint8_t address, uint8_t value); + /** + * @brief Thread-safely sorts the data list according to RSSI signal strength levels. + * @param ascending If true, lowest signal strengths come first; if false, strongest signals sort to front. + */ + void sortListByRssi(bool ascending = false); - esp_err_t initializeHardware(); - void setModeReceiver(); + /** + * @brief Thread-safely recovers structural chronological data ordering schemes according to database IDs. + */ + void sortListChronologically(); + + /** + * @brief Performs a deep copy operation over the linked list under mutex locks to return a safe detached copy. + * @return A standalone, thread-isolated linked list duplicate snapshot copy containing current history rows. + */ + std::list getPacketListCopy(); }; diff --git a/src/lora_types.hpp b/src/lora_types.hpp new file mode 100644 index 0000000..cfcb431 --- /dev/null +++ b/src/lora_types.hpp @@ -0,0 +1,111 @@ +/** + * @file lora_types.hpp + * @brief Definition of data structures, nodes, and state tables for the LoRa driver. + * @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 + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#include +#include +#pragma GCC diagnostic pop +#include + +/** + * @brief Structure representing a single packet node within the doubly-linked tracking list. + */ +struct LoraPacketNode { + /** @brief Dynamic raw binary storage buffer containing the received message payload. */ + std::vector payload; + + /** @brief Unique auto-incremented database identifier for targeting explicit items. */ + uint32_t databaseId; + + /** @brief The absolute sequence index counting the packets received since device boot. */ + uint32_t packetSequence; + + /** @brief Received Signal Strength Indicator (RSSI) measured in dBm. */ + float rssi; + + /** @brief Signal-to-Noise Ratio (SNR) measured in dB. */ + float snr; + + /** @brief Payload size explicitly specified in bytes. */ + uint8_t length; +}; + +/** + * @brief Runtime operational state table monitoring configurations and historical packet data. + * @note Optimized using "largest-to-smallest" data type ordering to completely eliminate memory padding. + */ +struct LoraStateTable { + /** @brief Central database component built on a doubly-linked list for versatile node operations. (Contains pointers) */ + std::list packetList; + + /** @brief Soft-limit threshold capping the maximum number of history records stored before auto-purge. (4 or 8 Bytes) */ + size_t maxHistorySize = 10; + + /** @brief RF carrier operating frequency configured in Hertz. (4 Bytes) */ + uint32_t frequencyHz = 433175000; + + /** @brief Accumulator metric track-counting successful ingress frames. (4 Bytes) */ + uint32_t totalPacketsReceived = 0; + + /** @brief Accumulator metric track-counting egress frames transmitted. (4 Bytes) */ + uint32_t totalPacketsTransmitted = 0; + + /** @brief Identity sequencer pointer controlling the next node's databaseId field. (4 Bytes) */ + uint32_t nextDatabaseId = 1; + + /** @brief Modem bandwidth performance target configured in kHz. (4 Bytes) */ + uint32_t bandwidthKhz = 250; + + /** @brief LoRa Spreading Factor configuration (typically ranges between 6 and 12). (1 Byte) */ + uint8_t spreadingFactor = 11; + + /** @brief Coding Rate denominator setting (e.g., value 6 yields a 4/6 coding protection overhead). (1 Byte) */ + uint8_t codingRate = 6; + + /** @brief True if hardware was successfully validated and attached during initialization. (1 Byte) */ + bool isInitialized = false; + + // 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 { + /** @brief Soft-limit threshold capping the maximum number of history records stored before auto-purge. (4 or 8 Bytes) */ + size_t currentHistoryCount; + + /** @brief RF carrier operating frequency configured in Hertz. (4 Bytes) */ + uint32_t frequencyHz; + + /** @brief Modem bandwidth performance target configured in kHz. (4 Bytes) */ + uint32_t bandwidthKhz; + + /** @brief Accumulator metric track-counting successful ingress frames. (4 Bytes) */ + uint32_t totalPacketsReceived; + + /** @brief Accumulator metric track-counting egress frames transmitted. (4 Bytes) */ + uint32_t totalPacketsTransmitted; + + /** @brief LoRa Spreading Factor configuration (typically ranges between 6 and 12). (1 Byte) */ + uint8_t spreadingFactor; + + /** @brief Coding Rate denominator setting (e.g., value 6 yields a 4/6 coding protection overhead). (1 Byte) */ + uint8_t codingRate; + + /** @brief True if hardware was successfully validated and attached during initialization. (1 Byte) */ + bool isInitialized; + + // The three 1-byte variables at the bottom align perfectly into a 32-bit slot, leaving only 1 byte of trailing padding. +};