From e254b7ad27604fcfd34bd9f20ef47d10d5d779e2 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Fri, 27 Feb 2026 17:50:50 +0100 Subject: [PATCH 01/20] added narrowband radiolib interface --- main/CMakeLists.txt | 2 +- main/EspHal.h | 322 +++++++++++++++++++++++++++++++++++++++++ main/idf_component.yml | 17 +++ main/narrowband.cpp | 48 ++++++ main/narrowband.h | 12 ++ 5 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 main/EspHal.h create mode 100644 main/idf_component.yml create mode 100644 main/narrowband.cpp create mode 100644 main/narrowband.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b732e5b..45bded6 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( - SRCS "main.c" + SRCS "main.c" "narrowband.cpp" INCLUDE_DIRS "." REQUIRES vigilant_engine ) diff --git a/main/EspHal.h b/main/EspHal.h new file mode 100644 index 0000000..3f7cd12 --- /dev/null +++ b/main/EspHal.h @@ -0,0 +1,322 @@ +#ifndef ESP_HAL_H +#define ESP_HAL_H + +// include RadioLib +#include + +// this example only works on ESP32 and is unlikely to work on ESP32S2/S3 etc. +// if you need high portability, you should probably use Arduino anyway ... +#if CONFIG_IDF_TARGET_ESP32 == 0 + #error This example HAL only supports ESP32 targets. Support for ESP32S2/S3 etc. can be added by adjusting this file to user needs. +#endif + +// include all the dependencies +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp32/rom/gpio.h" +#include "soc/rtc.h" +#include "soc/dport_reg.h" +#include "soc/spi_reg.h" +#include "soc/spi_struct.h" +#include "driver/gpio.h" +#include "hal/gpio_hal.h" +#include "esp_timer.h" +#include "esp_log.h" + +// define Arduino-style macros +#define LOW (0x0) +#define HIGH (0x1) +#define INPUT (0x01) +#define OUTPUT (0x03) +#define RISING (0x01) +#define FALLING (0x02) +#define NOP() asm volatile ("nop") + +#define MATRIX_DETACH_OUT_SIG (0x100) +#define MATRIX_DETACH_IN_LOW_PIN (0x30) + +// all of the following is needed to calculate SPI clock divider +#define ClkRegToFreq(reg) (apb_freq / (((reg)->clkdiv_pre + 1) * ((reg)->clkcnt_n + 1))) + +typedef union { + uint32_t value; + struct { + uint32_t clkcnt_l: 6; + uint32_t clkcnt_h: 6; + uint32_t clkcnt_n: 6; + uint32_t clkdiv_pre: 13; + uint32_t clk_equ_sysclk: 1; + }; +} spiClk_t; + +uint32_t getApbFrequency() { + rtc_cpu_freq_config_t conf; + rtc_clk_cpu_freq_get_config(&conf); + + if(conf.freq_mhz >= 80) { + return(80 * MHZ); + } + + return((conf.source_freq_mhz * MHZ) / conf.div); +} + +uint32_t spiFrequencyToClockDiv(uint32_t freq) { + uint32_t apb_freq = getApbFrequency(); + if(freq >= apb_freq) { + return SPI_CLK_EQU_SYSCLK; + } + + const spiClk_t minFreqReg = { 0x7FFFF000 }; + uint32_t minFreq = ClkRegToFreq((spiClk_t*) &minFreqReg); + if(freq < minFreq) { + return minFreqReg.value; + } + + uint8_t calN = 1; + spiClk_t bestReg = { 0 }; + int32_t bestFreq = 0; + while(calN <= 0x3F) { + spiClk_t reg = { 0 }; + int32_t calFreq; + int32_t calPre; + int8_t calPreVari = -2; + + reg.clkcnt_n = calN; + + while(calPreVari++ <= 1) { + calPre = (((apb_freq / (reg.clkcnt_n + 1)) / freq) - 1) + calPreVari; + if(calPre > 0x1FFF) { + reg.clkdiv_pre = 0x1FFF; + } else if(calPre <= 0) { + reg.clkdiv_pre = 0; + } else { + reg.clkdiv_pre = calPre; + } + reg.clkcnt_l = ((reg.clkcnt_n + 1) / 2); + calFreq = ClkRegToFreq(®); + if(calFreq == (int32_t) freq) { + memcpy(&bestReg, ®, sizeof(bestReg)); + break; + } else if(calFreq < (int32_t) freq) { + if(RADIOLIB_ABS(freq - calFreq) < RADIOLIB_ABS(freq - bestFreq)) { + bestFreq = calFreq; + memcpy(&bestReg, ®, sizeof(bestReg)); + } + } + } + if(calFreq == (int32_t) freq) { + break; + } + calN++; + } + return(bestReg.value); +} + +// create a new ESP-IDF hardware abstraction layer +// the HAL must inherit from the base RadioLibHal class +// and implement all of its virtual methods +// this is pretty much just copied from Arduino ESP32 core +class EspHal : public RadioLibHal { + public: + // default constructor - initializes the base HAL and any needed private members + EspHal(int8_t sck, int8_t miso, int8_t mosi) + : RadioLibHal(INPUT, OUTPUT, LOW, HIGH, RISING, FALLING), + spiSCK(sck), spiMISO(miso), spiMOSI(mosi) { + } + + void init() override { + // we only need to init the SPI here + spiBegin(); + } + + void term() override { + // we only need to stop the SPI here + spiEnd(); + } + + // GPIO-related methods (pinMode, digitalWrite etc.) should check + // RADIOLIB_NC as an alias for non-connected pins + void pinMode(uint32_t pin, uint32_t mode) override { + if(pin == RADIOLIB_NC) { + return; + } + + gpio_hal_context_t gpiohal; + gpiohal.dev = GPIO_LL_GET_HW(GPIO_PORT_0); + + gpio_config_t conf = { + .pin_bit_mask = (1ULL<pin[pin].int_type, + }; + gpio_config(&conf); + } + + void digitalWrite(uint32_t pin, uint32_t value) override { + if(pin == RADIOLIB_NC) { + return; + } + + gpio_set_level((gpio_num_t)pin, value); + } + + uint32_t digitalRead(uint32_t pin) override { + if(pin == RADIOLIB_NC) { + return(0); + } + + return(gpio_get_level((gpio_num_t)pin)); + } + + void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override { + if(interruptNum == RADIOLIB_NC) { + return; + } + + gpio_install_isr_service((int)ESP_INTR_FLAG_IRAM); + gpio_set_intr_type((gpio_num_t)interruptNum, (gpio_int_type_t)(mode & 0x7)); + + // this uses function typecasting, which is not defined when the functions have different signatures + // untested and might not work + gpio_isr_handler_add((gpio_num_t)interruptNum, (void (*)(void*))interruptCb, NULL); + } + + void detachInterrupt(uint32_t interruptNum) override { + if(interruptNum == RADIOLIB_NC) { + return; + } + + gpio_isr_handler_remove((gpio_num_t)interruptNum); + gpio_wakeup_disable((gpio_num_t)interruptNum); + gpio_set_intr_type((gpio_num_t)interruptNum, GPIO_INTR_DISABLE); + } + + void delay(unsigned long ms) override { + vTaskDelay(ms / portTICK_PERIOD_MS); + } + + void delayMicroseconds(unsigned long us) override { + uint64_t m = (uint64_t)esp_timer_get_time(); + if(us) { + uint64_t e = (m + us); + if(m > e) { // overflow + while((uint64_t)esp_timer_get_time() > e) { + NOP(); + } + } + while((uint64_t)esp_timer_get_time() < e) { + NOP(); + } + } + } + + unsigned long millis() override { + return((unsigned long)(esp_timer_get_time() / 1000ULL)); + } + + unsigned long micros() override { + return((unsigned long)(esp_timer_get_time())); + } + + long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override { + if(pin == RADIOLIB_NC) { + return(0); + } + + this->pinMode(pin, INPUT); + uint32_t start = this->micros(); + uint32_t curtick = this->micros(); + + while(this->digitalRead(pin) == state) { + if((this->micros() - curtick) > timeout) { + return(0); + } + } + + return(this->micros() - start); + } + + void spiBegin() { + // enable peripheral + DPORT_SET_PERI_REG_MASK(DPORT_PERIP_CLK_EN_REG, DPORT_SPI2_CLK_EN); + DPORT_CLEAR_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_SPI2_RST); + + // reset the control struct + this->spi->slave.trans_done = 0; + this->spi->slave.val = 0; + this->spi->pin.val = 0; + this->spi->user.val = 0; + this->spi->user1.val = 0; + this->spi->ctrl.val = 0; + this->spi->ctrl1.val = 0; + this->spi->ctrl2.val = 0; + this->spi->clock.val = 0; + this->spi->user.usr_mosi = 1; + this->spi->user.usr_miso = 1; + this->spi->user.doutdin = 1; + for(uint8_t i = 0; i < 16; i++) { + this->spi->data_buf[i] = 0x00000000; + } + + // set SPI mode 0 + this->spi->pin.ck_idle_edge = 0; + this->spi->user.ck_out_edge = 0; + + // set bit order to MSB first + this->spi->ctrl.wr_bit_order = 0; + this->spi->ctrl.rd_bit_order = 0; + + // set the clock + this->spi->clock.val = spiFrequencyToClockDiv(2000000); + + // initialize pins + this->pinMode(this->spiSCK, OUTPUT); + this->pinMode(this->spiMISO, INPUT); + this->pinMode(this->spiMOSI, OUTPUT); + gpio_matrix_out(this->spiSCK, HSPICLK_OUT_IDX, false, false); + gpio_matrix_in(this->spiMISO, HSPIQ_OUT_IDX, false); + gpio_matrix_out(this->spiMOSI, HSPID_IN_IDX, false, false); + } + + void spiBeginTransaction() { + // not needed - in ESP32 Arduino core, this function + // repeats clock div, mode and bit order configuration + } + + uint8_t spiTransferByte(uint8_t b) { + this->spi->mosi_dlen.usr_mosi_dbitlen = 7; + this->spi->miso_dlen.usr_miso_dbitlen = 7; + this->spi->data_buf[0] = b; + this->spi->cmd.usr = 1; + while(this->spi->cmd.usr); + return(this->spi->data_buf[0] & 0xFF); + } + + void spiTransfer(uint8_t* out, size_t len, uint8_t* in) { + for(size_t i = 0; i < len; i++) { + in[i] = this->spiTransferByte(out[i]); + } + } + + void spiEndTransaction() { + // nothing needs to be done here + } + + void spiEnd() { + // detach pins + gpio_matrix_out(this->spiSCK, MATRIX_DETACH_OUT_SIG, false, false); + gpio_matrix_in(this->spiMISO, MATRIX_DETACH_IN_LOW_PIN, false); + gpio_matrix_out(this->spiMOSI, MATRIX_DETACH_OUT_SIG, false, false); + } + + private: + // the HAL can contain any additional private members + int8_t spiSCK; + int8_t spiMISO; + int8_t spiMOSI; + spi_dev_t * spi = (volatile spi_dev_t *)(DR_REG_SPI2_BASE); +}; + +#endif diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..2405ef1 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,17 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + idf: + version: '>=4.1.0' + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true + jgromes/radiolib: ^7.6.0 diff --git a/main/narrowband.cpp b/main/narrowband.cpp new file mode 100644 index 0000000..75ae51f --- /dev/null +++ b/main/narrowband.cpp @@ -0,0 +1,48 @@ +#include "narrowband.h" +#include +#include "EspHal.h" + +// HAL and radio module pin definitions +constexpr int SCLK_PIN = 14; +constexpr int MISO_PIN = 12; +constexpr int MOSI_PIN = 13; + +constexpr int NSS_PIN = 19; +constexpr int DIO1_PIN = 26; +constexpr int NRST_PIN = 18; +constexpr int BUSY_PIN = 21; + + +static EspHal* s_hal = nullptr; +static LLCC68* s_radio = nullptr; + +void init_narrowband() { + + // create a new instance of the HAL class + s_hal = new EspHal(SCLK_PIN, MISO_PIN, MOSI_PIN); + + // now we can create the radio module + s_radio = new LLCC68(new Module(s_hal, NSS_PIN, DIO1_PIN, NRST_PIN, BUSY_PIN)); +} + +void send_narrowband(const char* data, int len) { + + // TODO: take last element from sensor data queue and send it + //s_radio->send(data, len); +} + +int recv_narrowband(char* buf, int len) { + + // TODO: put received cmd into cmd queue + //return s_radio->recv(buf, len); +} + +void destroy_narrowband() { + // free hal + delete s_hal; + s_hal = nullptr; + + // free radio + delete s_radio; + s_radio = nullptr; +} diff --git a/main/narrowband.h b/main/narrowband.h new file mode 100644 index 0000000..d5c2e29 --- /dev/null +++ b/main/narrowband.h @@ -0,0 +1,12 @@ +#ifdef __cplusplus +extern "C" { +#endif + +void init_radio(void); +void send_radio(const char* data, int len); +int recv_radio(char* buf, int len); +void destroy_radio(void); + +#ifdef __cplusplus +} +#endif \ No newline at end of file From 215438ae86efc4092135253071a3acd45169f637 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Fri, 27 Feb 2026 18:13:55 +0100 Subject: [PATCH 02/20] fixing cmake needing requirement --- main/CMakeLists.txt | 2 +- main/narrowband.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 45bded6..4767180 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRCS "main.c" "narrowband.cpp" INCLUDE_DIRS "." - REQUIRES vigilant_engine + REQUIRES vigilant_engine esp_timer driver ) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 75ae51f..c76d656 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -35,6 +35,7 @@ int recv_narrowband(char* buf, int len) { // TODO: put received cmd into cmd queue //return s_radio->recv(buf, len); + return 0; } void destroy_narrowband() { From c646b4d71c120cf1772a0c41b8709545fdb767a4 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Fri, 27 Feb 2026 20:11:09 +0100 Subject: [PATCH 03/20] added some reasonable init parameters, implemented narrowband_thread function, which controls the narrowband comms main loop, started working on handle_receive interrupt service routine (isr) --- main/narrowband.cpp | 102 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index c76d656..4884982 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -1,41 +1,127 @@ #include "narrowband.h" #include +#include +#include +#include #include "EspHal.h" +#include "esp_log.h" + +// radio comms interval +constexpr int RADIO_INTERVAL_MS = 500; // HAL and radio module pin definitions constexpr int SCLK_PIN = 14; constexpr int MISO_PIN = 12; constexpr int MOSI_PIN = 13; - constexpr int NSS_PIN = 19; constexpr int DIO1_PIN = 26; constexpr int NRST_PIN = 18; constexpr int BUSY_PIN = 21; +constexpr int RXEN_PIN = 16; static EspHal* s_hal = nullptr; static LLCC68* s_radio = nullptr; +static const char* TAG = "Narrowband"; + void init_narrowband() { + + ESP_LOGI(TAG, "[LLCC68] Initializing narrowband radio..."); // create a new instance of the HAL class s_hal = new EspHal(SCLK_PIN, MISO_PIN, MOSI_PIN); // now we can create the radio module s_radio = new LLCC68(new Module(s_hal, NSS_PIN, DIO1_PIN, NRST_PIN, BUSY_PIN)); + + // freq 434 Mhz, bitrate 2.4 kHz, frequency deviation 2.4 kHz, receiver bandwidth DSB 11.7 kHz, power 22 dBm, preamble length 32 bit, TCXO voltage 0 V, useRegulatorLDO false + int state = s_radio->beginFSK(434, 2.4, 2.4, 11.7, 22, 32, 0, false); + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "failed, code %d\n", state); + return; + } + + ESP_LOGI(TAG, "success!\n"); + + // RXEN pin: 16 + // TXEN pin controlled via dio2 + s_radio->setRfSwitchPins(RXEN_PIN, RADIOLIB_NC); + s_radio->setDio2AsRfSwitch(true); + + + state = s_radio->setPaConfig(0x04, 0x00, 0x07, 0x01); + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "failed pa config, code %d\n", state); + return; + } + ESP_LOGI(TAG, "[LLCC68] PA config configured!\n"); } -void send_narrowband(const char* data, int len) { - // TODO: take last element from sensor data queue and send it - //s_radio->send(data, len); +void parse_command() { + } -int recv_narrowband(char* buf, int len) { +// interrupt function +// needs IRAM_ATTR to be used as interrupt func, which makes the func be stored in IRAM instead of flash, to ensure it can be executed when flash is not accessible (e.g. during sleep) +void IRAM_ATTR handle_receive(void) { + if (s_hal == nullptr || s_radio == nullptr) { + return; + } + + size_t len = s_radio->getPacketLength(); + std::vector buf(len); + int state = s_radio->readData(buf.data(), buf.size()); + + if (state == RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "Received packet: %s\n", buf.data()); + // TODO: parse_command(), put received packet into cmd queue + + } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { + ESP_LOGI(TAG, "Received packet with CRC mismatch!\n"); + } else { + ESP_LOGI(TAG, "Failed to read received packet, code %d\n", state); + } +} + +void narrowband_thread() { + + if (s_hal == nullptr || s_radio == nullptr) { + ESP_LOGI(TAG, "HAL or radio not initialized!\n"); + return; + } + + ESP_LOGI(TAG, "[LLCC68] Started narrowband thread!\n"); + + int state = RADIOLIB_ERR_NONE; + while (true) { + + ESP_LOGI(TAG, "[LLCC68] Sending packet..."); + + state = s_radio->transmit("Hello world!"); + if(state == RADIOLIB_ERR_NONE) { + // the packet was successfully transmitted + ESP_LOGI(TAG, "success!\n"); + } else { + ESP_LOGI(TAG, "failed, code %d\n", state); + } + ESP_LOGI(TAG, "Datarate measured: %f\n", s_radio->getDataRate()); + + + state = s_radio->startReceive(); + if(state == RADIOLIB_ERR_NONE) { + // the module is now in receive mode, waiting for a packet + ESP_LOGI(TAG, "Waiting for a packet...\n"); + } else { + ESP_LOGI(TAG, "failed to start receiver, code %d\n", state); + } - // TODO: put received cmd into cmd queue - //return s_radio->recv(buf, len); - return 0; + // wait 0.5 s + s_hal->delay(RADIO_INTERVAL_MS); + + // if no packet was received, we just start sending again + } } void destroy_narrowband() { From 6dba47350c6759e21c3458cc06dcfd70224ed5ca Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Fri, 27 Feb 2026 20:15:50 +0100 Subject: [PATCH 04/20] upadte header file --- main/narrowband.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main/narrowband.h b/main/narrowband.h index d5c2e29..b5f485d 100644 --- a/main/narrowband.h +++ b/main/narrowband.h @@ -3,8 +3,7 @@ extern "C" { #endif void init_radio(void); -void send_radio(const char* data, int len); -int recv_radio(char* buf, int len); +void narrowband_thread(void); void destroy_radio(void); #ifdef __cplusplus From 44af46dcc7c24fa80774cb78380fb52c253bf10a Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Tue, 3 Mar 2026 19:38:29 +0100 Subject: [PATCH 05/20] implemented basic thread safe command queue, refractored narrowband module to use a static class instead of static variables --- main/narrowband.cpp | 180 +++++++++++++++++++++++++++++++++----------- main/narrowband.h | 12 ++- 2 files changed, 144 insertions(+), 48 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 4884982..157c263 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -5,6 +5,8 @@ #include #include "EspHal.h" #include "esp_log.h" +#include +#include // radio comms interval constexpr int RADIO_INTERVAL_MS = 500; @@ -19,24 +21,46 @@ constexpr int NRST_PIN = 18; constexpr int BUSY_PIN = 21; constexpr int RXEN_PIN = 16; - -static EspHal* s_hal = nullptr; -static LLCC68* s_radio = nullptr; +class NarrowbandRadio { +private: + EspHal* hal; + LLCC68* radio; + std::queue> cmd_queue; + std::mutex cmd_queue_mutex; + + static NarrowbandRadio* instance; + + NarrowbandRadio(); + +public: + ~NarrowbandRadio(); + static NarrowbandRadio* getInstance(); + void enqueueCommand(const std::vector& cmd); + std::vector dequeueCommand(); + void parseCommand(); + void handleReceive(); + void runThread(); +}; + +NarrowbandRadio* NarrowbandRadio::instance = nullptr; static const char* TAG = "Narrowband"; -void init_narrowband() { +// CLASS IMPLEMENTATION + +NarrowbandRadio::NarrowbandRadio() + : hal(nullptr), radio(nullptr) { ESP_LOGI(TAG, "[LLCC68] Initializing narrowband radio..."); // create a new instance of the HAL class - s_hal = new EspHal(SCLK_PIN, MISO_PIN, MOSI_PIN); + hal = new EspHal(SCLK_PIN, MISO_PIN, MOSI_PIN); // now we can create the radio module - s_radio = new LLCC68(new Module(s_hal, NSS_PIN, DIO1_PIN, NRST_PIN, BUSY_PIN)); + radio = new LLCC68(new Module(hal, NSS_PIN, DIO1_PIN, NRST_PIN, BUSY_PIN)); // freq 434 Mhz, bitrate 2.4 kHz, frequency deviation 2.4 kHz, receiver bandwidth DSB 11.7 kHz, power 22 dBm, preamble length 32 bit, TCXO voltage 0 V, useRegulatorLDO false - int state = s_radio->beginFSK(434, 2.4, 2.4, 11.7, 22, 32, 0, false); + int state = radio->beginFSK(434, 2.4, 2.4, 11.7, 22, 32, 0, false); if (state != RADIOLIB_ERR_NONE) { ESP_LOGI(TAG, "failed, code %d\n", state); return; @@ -44,40 +68,70 @@ void init_narrowband() { ESP_LOGI(TAG, "success!\n"); - // RXEN pin: 16 - // TXEN pin controlled via dio2 - s_radio->setRfSwitchPins(RXEN_PIN, RADIOLIB_NC); - s_radio->setDio2AsRfSwitch(true); - - - state = s_radio->setPaConfig(0x04, 0x00, 0x07, 0x01); - if (state != RADIOLIB_ERR_NONE) { - ESP_LOGI(TAG, "failed pa config, code %d\n", state); - return; - } - ESP_LOGI(TAG, "[LLCC68] PA config configured!\n"); + // RXEN pin: 16 + // TXEN pin controlled via dio2 + radio->setRfSwitchPins(RXEN_PIN, RADIOLIB_NC); + radio->setDio2AsRfSwitch(true); + + // for more details, see LLCC68 datasheet, this is the highest power setting, with 22 dBm set in beginFSK + state = radio->setPaConfig(0x04, 0x00, 0x07, 0x01); + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "failed pa config, code %d\n", state); + return; + } + ESP_LOGI(TAG, "[LLCC68] PA config configured!\n"); } +NarrowbandRadio::~NarrowbandRadio() { + if (hal != nullptr) { + delete hal; + hal = nullptr; + } + if (radio != nullptr) { + delete radio; + radio = nullptr; + } +} + +NarrowbandRadio* NarrowbandRadio::getInstance() { + if (instance == nullptr) { + instance = new NarrowbandRadio(); + } + return instance; +} -void parse_command() { +void NarrowbandRadio::enqueueCommand(const std::vector& cmd) { + std::lock_guard lock(cmd_queue_mutex); + cmd_queue.push(cmd); +} + +std::vector NarrowbandRadio::dequeueCommand() { + std::lock_guard lock(cmd_queue_mutex); + if (!cmd_queue.empty()) { + std::vector cmd = std::move(cmd_queue.front()); + cmd_queue.pop(); + return cmd; + } + return {}; +} +void NarrowbandRadio::parseCommand() { + // TODO: implement command parsing } -// interrupt function -// needs IRAM_ATTR to be used as interrupt func, which makes the func be stored in IRAM instead of flash, to ensure it can be executed when flash is not accessible (e.g. during sleep) -void IRAM_ATTR handle_receive(void) { - if (s_hal == nullptr || s_radio == nullptr) { +void NarrowbandRadio::handleReceive() { + if (hal == nullptr || radio == nullptr) { return; } - size_t len = s_radio->getPacketLength(); + size_t len = radio->getPacketLength(); std::vector buf(len); - int state = s_radio->readData(buf.data(), buf.size()); + int state = radio->readData(buf.data(), buf.size()); if (state == RADIOLIB_ERR_NONE) { ESP_LOGI(TAG, "Received packet: %s\n", buf.data()); - // TODO: parse_command(), put received packet into cmd queue - + // TODO: parseCommand(), put received packet into cmd queue + enqueueCommand(buf); } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { ESP_LOGI(TAG, "Received packet with CRC mismatch!\n"); } else { @@ -85,9 +139,8 @@ void IRAM_ATTR handle_receive(void) { } } -void narrowband_thread() { - - if (s_hal == nullptr || s_radio == nullptr) { +void NarrowbandRadio::runThread() { + if (hal == nullptr || radio == nullptr) { ESP_LOGI(TAG, "HAL or radio not initialized!\n"); return; } @@ -96,21 +149,19 @@ void narrowband_thread() { int state = RADIOLIB_ERR_NONE; while (true) { - ESP_LOGI(TAG, "[LLCC68] Sending packet..."); - state = s_radio->transmit("Hello world!"); - if(state == RADIOLIB_ERR_NONE) { + state = radio->transmit("Hello world!"); + if (state == RADIOLIB_ERR_NONE) { // the packet was successfully transmitted ESP_LOGI(TAG, "success!\n"); } else { ESP_LOGI(TAG, "failed, code %d\n", state); } - ESP_LOGI(TAG, "Datarate measured: %f\n", s_radio->getDataRate()); + ESP_LOGI(TAG, "Datarate measured: %f\n", radio->getDataRate()); - - state = s_radio->startReceive(); - if(state == RADIOLIB_ERR_NONE) { + state = radio->startReceive(); + if (state == RADIOLIB_ERR_NONE) { // the module is now in receive mode, waiting for a packet ESP_LOGI(TAG, "Waiting for a packet...\n"); } else { @@ -118,18 +169,55 @@ void narrowband_thread() { } // wait 0.5 s - s_hal->delay(RADIO_INTERVAL_MS); + hal->delay(RADIO_INTERVAL_MS); // if no packet was received, we just start sending again } } -void destroy_narrowband() { - // free hal - delete s_hal; - s_hal = nullptr; +// C COMPATIBILITY WRAPPERS + +extern "C" { + void init_narrowband() { + NarrowbandRadio::getInstance(); + } + + void enqueue_command(const uint8_t* data, size_t len) { + std::vector cmd(data, data + len); + NarrowbandRadio::getInstance()->enqueueCommand(cmd); + } - // free radio - delete s_radio; - s_radio = nullptr; + // Dequeue into caller-provided buffer; returns number of bytes written, + // 0 if no data, -1 on error or if buffer too small + ssize_t dequeue_command(uint8_t* buf, size_t max_len) { + std::vector v = NarrowbandRadio::getInstance()->dequeueCommand(); + if (v.empty()) { + return 0; + } + if (buf == nullptr || max_len == 0) { + return -1; + } + size_t needed = v.size(); + if (needed > max_len) { + // Do not truncate silently; signal error + return -1; + } + std::memcpy(buf, v.data(), needed); + return static_cast(needed); + } + + void IRAM_ATTR handle_receive(void) { + NarrowbandRadio::getInstance()->handleReceive(); + } + + void narrowband_thread() { + NarrowbandRadio::getInstance()->runThread(); + } + + void destroy_narrowband() { + if (NarrowbandRadio::instance != nullptr) { + delete NarrowbandRadio::instance; + NarrowbandRadio::instance = nullptr; + } + } } diff --git a/main/narrowband.h b/main/narrowband.h index b5f485d..fd7023d 100644 --- a/main/narrowband.h +++ b/main/narrowband.h @@ -1,10 +1,18 @@ #ifdef __cplusplus extern "C" { #endif + +#include +#include -void init_radio(void); +void init_narrowband(void); +void enqueue_command(const uint8_t* data, size_t len); +// Dequeue into caller-provided buffer; returns number of bytes written, +// 0 if no data, -1 on error or if buffer is too small +ssize_t dequeue_command(uint8_t* buf, size_t max_len); +void handle_receive(void); void narrowband_thread(void); -void destroy_radio(void); +void destroy_narrowband(void); #ifdef __cplusplus } From d7bea1f3c4a1d958fc35e28ac497223974bf30c1 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Tue, 3 Mar 2026 20:15:13 +0100 Subject: [PATCH 06/20] fix: interrupt handler for receiving narrowband packets is now ISR and IRAM safe --- main/narrowband.cpp | 50 +++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 157c263..7483ebc 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -7,6 +7,11 @@ #include "esp_log.h" #include #include +#include // for ISR flag + +// flag set in ISR when packet arrives +// atomic operation to avoid data race between interrupt and main thread +static std::atomic s_received_packet{false}; // radio comms interval constexpr int RADIO_INTERVAL_MS = 500; @@ -31,14 +36,13 @@ class NarrowbandRadio { static NarrowbandRadio* instance; NarrowbandRadio(); + void handleReceive(); + void enqueueCommand(const std::vector& cmd); public: ~NarrowbandRadio(); static NarrowbandRadio* getInstance(); - void enqueueCommand(const std::vector& cmd); std::vector dequeueCommand(); - void parseCommand(); - void handleReceive(); void runThread(); }; @@ -62,24 +66,27 @@ NarrowbandRadio::NarrowbandRadio() // freq 434 Mhz, bitrate 2.4 kHz, frequency deviation 2.4 kHz, receiver bandwidth DSB 11.7 kHz, power 22 dBm, preamble length 32 bit, TCXO voltage 0 V, useRegulatorLDO false int state = radio->beginFSK(434, 2.4, 2.4, 11.7, 22, 32, 0, false); if (state != RADIOLIB_ERR_NONE) { - ESP_LOGI(TAG, "failed, code %d\n", state); - return; + ESP_LOGE(TAG, "beginFSK failed, code %d (fatal)\n", state); + abort(); // fatal error, cannot continue without radio } - ESP_LOGI(TAG, "success!\n"); - // RXEN pin: 16 // TXEN pin controlled via dio2 radio->setRfSwitchPins(RXEN_PIN, RADIOLIB_NC); radio->setDio2AsRfSwitch(true); + ESP_LOGI(TAG, "success!\n"); + // for more details, see LLCC68 datasheet, this is the highest power setting, with 22 dBm set in beginFSK state = radio->setPaConfig(0x04, 0x00, 0x07, 0x01); if (state != RADIOLIB_ERR_NONE) { - ESP_LOGI(TAG, "failed pa config, code %d\n", state); - return; + ESP_LOGE(TAG, "PA config failed, code %d (fatal)\n", state); + abort(); } ESP_LOGI(TAG, "[LLCC68] PA config configured!\n"); + + // configure callback for received packet; must be a free/IRAM-safe function + radio->setPacketReceivedAction(narrowband_receive_isr); } NarrowbandRadio::~NarrowbandRadio() { @@ -115,14 +122,18 @@ std::vector NarrowbandRadio::dequeueCommand() { return {}; } -void NarrowbandRadio::parseCommand() { - // TODO: implement command parsing +// ISR callback stored in IRAM; just sets the atomic flag +extern "C" void IRAM_ATTR narrowband_receive_isr(void) { + s_received_packet.store(true, std::memory_order_relaxed); } void NarrowbandRadio::handleReceive() { - if (hal == nullptr || radio == nullptr) { + + // check and clear ISR flag + if (!s_received_packet.load(std::memory_order_relaxed)) { return; } + s_received_packet.store(false, std::memory_order_relaxed); size_t len = radio->getPacketLength(); std::vector buf(len); @@ -130,7 +141,7 @@ void NarrowbandRadio::handleReceive() { if (state == RADIOLIB_ERR_NONE) { ESP_LOGI(TAG, "Received packet: %s\n", buf.data()); - // TODO: parseCommand(), put received packet into cmd queue + // maybe parse command here, currently we just enqueue the raw packet data for processing in the main thread enqueueCommand(buf); } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { ESP_LOGI(TAG, "Received packet with CRC mismatch!\n"); @@ -139,6 +150,8 @@ void NarrowbandRadio::handleReceive() { } } +// TODO: add a way to break out of this loop and clean up resources, currently it will run indefinitely +// maybe consider a yield? void NarrowbandRadio::runThread() { if (hal == nullptr || radio == nullptr) { ESP_LOGI(TAG, "HAL or radio not initialized!\n"); @@ -151,6 +164,7 @@ void NarrowbandRadio::runThread() { while (true) { ESP_LOGI(TAG, "[LLCC68] Sending packet..."); + // TODO: replace with sensor data from sensor queue state = radio->transmit("Hello world!"); if (state == RADIOLIB_ERR_NONE) { // the packet was successfully transmitted @@ -172,6 +186,7 @@ void NarrowbandRadio::runThread() { hal->delay(RADIO_INTERVAL_MS); // if no packet was received, we just start sending again + handleReceive(); } } @@ -182,11 +197,6 @@ extern "C" { NarrowbandRadio::getInstance(); } - void enqueue_command(const uint8_t* data, size_t len) { - std::vector cmd(data, data + len); - NarrowbandRadio::getInstance()->enqueueCommand(cmd); - } - // Dequeue into caller-provided buffer; returns number of bytes written, // 0 if no data, -1 on error or if buffer too small ssize_t dequeue_command(uint8_t* buf, size_t max_len) { @@ -206,10 +216,6 @@ extern "C" { return static_cast(needed); } - void IRAM_ATTR handle_receive(void) { - NarrowbandRadio::getInstance()->handleReceive(); - } - void narrowband_thread() { NarrowbandRadio::getInstance()->runThread(); } From 10c0e3f052ea491145f63e429a847b19652c30a1 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Tue, 3 Mar 2026 20:17:45 +0100 Subject: [PATCH 07/20] fixed narroband header file --- main/narrowband.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/main/narrowband.h b/main/narrowband.h index fd7023d..d91d478 100644 --- a/main/narrowband.h +++ b/main/narrowband.h @@ -6,11 +6,9 @@ extern "C" { #include void init_narrowband(void); -void enqueue_command(const uint8_t* data, size_t len); // Dequeue into caller-provided buffer; returns number of bytes written, // 0 if no data, -1 on error or if buffer is too small ssize_t dequeue_command(uint8_t* buf, size_t max_len); -void handle_receive(void); void narrowband_thread(void); void destroy_narrowband(void); From 8302c01cc48197969dca216b808c9fd5fd2ff81c Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Wed, 11 Mar 2026 02:15:41 +0100 Subject: [PATCH 08/20] refractored narrowband class, better interface using freertos queues (todo), isr handler and rxtx task as static member function, removed singleton pattern in facor of anonymus namespace with single class instance --- main/narrowband.cpp | 320 ++++++++++++++++++-------------------------- main/narrowband.h | 11 +- 2 files changed, 137 insertions(+), 194 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 7483ebc..4d16018 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -1,6 +1,5 @@ #include "narrowband.h" #include -#include #include #include #include "EspHal.h" @@ -8,222 +7,169 @@ #include #include #include // for ISR flag +#include +#include -// flag set in ISR when packet arrives -// atomic operation to avoid data race between interrupt and main thread -static std::atomic s_received_packet{false}; - -// radio comms interval -constexpr int RADIO_INTERVAL_MS = 500; - -// HAL and radio module pin definitions -constexpr int SCLK_PIN = 14; -constexpr int MISO_PIN = 12; -constexpr int MOSI_PIN = 13; -constexpr int NSS_PIN = 19; -constexpr int DIO1_PIN = 26; -constexpr int NRST_PIN = 18; -constexpr int BUSY_PIN = 21; -constexpr int RXEN_PIN = 16; - -class NarrowbandRadio { -private: - EspHal* hal; - LLCC68* radio; - std::queue> cmd_queue; - std::mutex cmd_queue_mutex; - - static NarrowbandRadio* instance; - - NarrowbandRadio(); - void handleReceive(); - void enqueueCommand(const std::vector& cmd); - -public: - ~NarrowbandRadio(); - static NarrowbandRadio* getInstance(); - std::vector dequeueCommand(); - void runThread(); -}; - -NarrowbandRadio* NarrowbandRadio::instance = nullptr; - -static const char* TAG = "Narrowband"; - - -// CLASS IMPLEMENTATION - -NarrowbandRadio::NarrowbandRadio() - : hal(nullptr), radio(nullptr) { - ESP_LOGI(TAG, "[LLCC68] Initializing narrowband radio..."); - - // create a new instance of the HAL class - hal = new EspHal(SCLK_PIN, MISO_PIN, MOSI_PIN); - - // now we can create the radio module - radio = new LLCC68(new Module(hal, NSS_PIN, DIO1_PIN, NRST_PIN, BUSY_PIN)); - - // freq 434 Mhz, bitrate 2.4 kHz, frequency deviation 2.4 kHz, receiver bandwidth DSB 11.7 kHz, power 22 dBm, preamble length 32 bit, TCXO voltage 0 V, useRegulatorLDO false - int state = radio->beginFSK(434, 2.4, 2.4, 11.7, 22, 32, 0, false); - if (state != RADIOLIB_ERR_NONE) { - ESP_LOGE(TAG, "beginFSK failed, code %d (fatal)\n", state); - abort(); // fatal error, cannot continue without radio - } +namespace { - // RXEN pin: 16 - // TXEN pin controlled via dio2 - radio->setRfSwitchPins(RXEN_PIN, RADIOLIB_NC); - radio->setDio2AsRfSwitch(true); + // flag set in ISR when packet arrives + // atomic operation to avoid data race between interrupt and main thread + static std::atomic s_received_packet{false}; - ESP_LOGI(TAG, "success!\n"); + static const char* TAG = "Narrowband"; - // for more details, see LLCC68 datasheet, this is the highest power setting, with 22 dBm set in beginFSK - state = radio->setPaConfig(0x04, 0x00, 0x07, 0x01); - if (state != RADIOLIB_ERR_NONE) { - ESP_LOGE(TAG, "PA config failed, code %d (fatal)\n", state); - abort(); - } - ESP_LOGI(TAG, "[LLCC68] PA config configured!\n"); + // CLASS DEFINITION + class NarrowbandRadio { + private: - // configure callback for received packet; must be a free/IRAM-safe function - radio->setPacketReceivedAction(narrowband_receive_isr); -} + // pin definitions + static constexpr int SCLK_PIN = 14; + static constexpr int MISO_PIN = 12; + static constexpr int MOSI_PIN = 13; + static constexpr int NSS_PIN = 19; + static constexpr int DIO1_PIN = 26; + static constexpr int NRST_PIN = 18; + static constexpr int BUSY_PIN = 21; + static constexpr int RXEN_PIN = 16; -NarrowbandRadio::~NarrowbandRadio() { - if (hal != nullptr) { - delete hal; - hal = nullptr; - } - if (radio != nullptr) { - delete radio; - radio = nullptr; - } -} + int rxtx_interval_ms = 500; -NarrowbandRadio* NarrowbandRadio::getInstance() { - if (instance == nullptr) { - instance = new NarrowbandRadio(); - } - return instance; -} + EspHal hal; + Module module; + LLCC68 radio; -void NarrowbandRadio::enqueueCommand(const std::vector& cmd) { - std::lock_guard lock(cmd_queue_mutex); - cmd_queue.push(cmd); -} + QueueHandle_t* commandQueue; + QueueHandle_t* sensorDataQueue; + + void handleReceive(); + static void IRAM_ATTR receive_isr(void); + static void rxtx_task_trampoline(void* param); + void rxtx_task(); + + public: + NarrowbandRadio(); + void init(QueueHandle_t* commandQueue, QueueHandle_t* sensorDataQueue); + }; -std::vector NarrowbandRadio::dequeueCommand() { - std::lock_guard lock(cmd_queue_mutex); - if (!cmd_queue.empty()) { - std::vector cmd = std::move(cmd_queue.front()); - cmd_queue.pop(); - return cmd; - } - return {}; -} -// ISR callback stored in IRAM; just sets the atomic flag -extern "C" void IRAM_ATTR narrowband_receive_isr(void) { - s_received_packet.store(true, std::memory_order_relaxed); -} + // CLASS IMPLEMENTATION -void NarrowbandRadio::handleReceive() { + NarrowbandRadio::NarrowbandRadio() + : hal(SCLK_PIN, MISO_PIN, MOSI_PIN), + module(&hal, NSS_PIN, DIO1_PIN, NRST_PIN, BUSY_PIN), + radio(&module), + commandQueue(nullptr), + sensorDataQueue(nullptr) {} - // check and clear ISR flag - if (!s_received_packet.load(std::memory_order_relaxed)) { - return; - } - s_received_packet.store(false, std::memory_order_relaxed); - - size_t len = radio->getPacketLength(); - std::vector buf(len); - int state = radio->readData(buf.data(), buf.size()); - - if (state == RADIOLIB_ERR_NONE) { - ESP_LOGI(TAG, "Received packet: %s\n", buf.data()); - // maybe parse command here, currently we just enqueue the raw packet data for processing in the main thread - enqueueCommand(buf); - } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { - ESP_LOGI(TAG, "Received packet with CRC mismatch!\n"); - } else { - ESP_LOGI(TAG, "Failed to read received packet, code %d\n", state); - } -} + void NarrowbandRadio::init(QueueHandle_t* commandQueue, QueueHandle_t* sensorDataQueue) { + ESP_LOGI(TAG, "[LLCC68] Initializing narrowband radio..."); + + // TODO: remove magic numbers, use config values instead + // freq 434 Mhz, bitrate 2.4 kHz, frequency deviation 2.4 kHz, receiver bandwidth DSB 11.7 kHz, power 22 dBm, preamble length 32 bit, TCXO voltage 0 V, useRegulatorLDO false + int state = radio.beginFSK(434, 2.4, 2.4, 11.7, 22, 32, 0, false); + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGE(TAG, "beginFSK failed, code %d (fatal)\n", state); + abort(); // fatal error, cannot continue without radio + } + + // RXEN pin: 16 + // TXEN pin controlled via dio2 + radio.setRfSwitchPins(RXEN_PIN, RADIOLIB_NC); + radio.setDio2AsRfSwitch(true); + + ESP_LOGI(TAG, "success!\n"); + + // for more details, see LLCC68 datasheet, this is the highest power setting, with 22 dBm set in beginFSK + state = radio.setPaConfig(0x04, 0x00, 0x07, 0x01); + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGE(TAG, "PA config failed, code %d (fatal)\n", state); + abort(); + } + ESP_LOGI(TAG, "[LLCC68] PA config configured!\n"); + + // configure callback for received packet; must be a free/IRAM-safe function + radio.setPacketReceivedAction(receive_isr); + + // create the rx/tx task + // xTaskCreate(rxtx_task_trampoline, "narrowband_rxtx", 4096, this, 1, NULL); -// TODO: add a way to break out of this loop and clean up resources, currently it will run indefinitely -// maybe consider a yield? -void NarrowbandRadio::runThread() { - if (hal == nullptr || radio == nullptr) { - ESP_LOGI(TAG, "HAL or radio not initialized!\n"); - return; } - ESP_LOGI(TAG, "[LLCC68] Started narrowband thread!\n"); + // ISR callback stored in IRAM; just sets the atomic flag + void IRAM_ATTR NarrowbandRadio::receive_isr(void) { + s_received_packet.store(true, std::memory_order_relaxed); + } - int state = RADIOLIB_ERR_NONE; - while (true) { - ESP_LOGI(TAG, "[LLCC68] Sending packet..."); + void NarrowbandRadio::handleReceive() { - // TODO: replace with sensor data from sensor queue - state = radio->transmit("Hello world!"); - if (state == RADIOLIB_ERR_NONE) { - // the packet was successfully transmitted - ESP_LOGI(TAG, "success!\n"); - } else { - ESP_LOGI(TAG, "failed, code %d\n", state); + // check and clear ISR flag + if (!s_received_packet.load(std::memory_order_relaxed)) { + return; } - ESP_LOGI(TAG, "Datarate measured: %f\n", radio->getDataRate()); + s_received_packet.store(false, std::memory_order_relaxed); + + size_t len = radio.getPacketLength(); + uint8_t* buf = (uint8_t*)malloc(len); + + int state = radio.readData(buf, len); - state = radio->startReceive(); if (state == RADIOLIB_ERR_NONE) { - // the module is now in receive mode, waiting for a packet - ESP_LOGI(TAG, "Waiting for a packet...\n"); + ESP_LOGI(TAG, "Received packet: %s\n", buf); + // maybe parse command here, currently we just enqueue the raw packet data for processing in the main thread + // enqueueCommand(buf); + } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { + ESP_LOGI(TAG, "Received packet with CRC mismatch!\n"); } else { - ESP_LOGI(TAG, "failed to start receiver, code %d\n", state); + ESP_LOGI(TAG, "Failed to read received packet, code %d\n", state); } - // wait 0.5 s - hal->delay(RADIO_INTERVAL_MS); - - // if no packet was received, we just start sending again - handleReceive(); + free(buf); } -} -// C COMPATIBILITY WRAPPERS - -extern "C" { - void init_narrowband() { - NarrowbandRadio::getInstance(); + void NarrowbandRadio::rxtx_task_trampoline(void* param) { + static_cast(param)->rxtx_task(); } - // Dequeue into caller-provided buffer; returns number of bytes written, - // 0 if no data, -1 on error or if buffer too small - ssize_t dequeue_command(uint8_t* buf, size_t max_len) { - std::vector v = NarrowbandRadio::getInstance()->dequeueCommand(); - if (v.empty()) { - return 0; - } - if (buf == nullptr || max_len == 0) { - return -1; + void NarrowbandRadio::rxtx_task() { + ESP_LOGI(TAG, "[LLCC68] Started rxtx task!\n"); + + int state = RADIOLIB_ERR_NONE; + while (true) { + ESP_LOGI(TAG, "[LLCC68] Sending packet..."); + + // TODO: replace with sensor data from sensor queue + state = radio.transmit("Hello world!"); + if (state == RADIOLIB_ERR_NONE) { + // the packet was successfully transmitted + ESP_LOGI(TAG, "success!\n"); + } else { + ESP_LOGI(TAG, "failed, code %d\n", state); + } + + state = radio.startReceive(); + if (state == RADIOLIB_ERR_NONE) { + // the module is now in receive mode, waiting for a packet + ESP_LOGI(TAG, "Waiting for a packet...\n"); + } else { + ESP_LOGI(TAG, "failed to start receiver, code %d\n", state); + } + + // wait 0.5 s + hal.delay(rxtx_interval_ms); + + // if no packet was received, we just start sending again + handleReceive(); } - size_t needed = v.size(); - if (needed > max_len) { - // Do not truncate silently; signal error - return -1; - } - std::memcpy(buf, v.data(), needed); - return static_cast(needed); } - void narrowband_thread() { - NarrowbandRadio::getInstance()->runThread(); - } + // static instance of the narrowband class + NarrowbandRadio nb_radio; +} - void destroy_narrowband() { - if (NarrowbandRadio::instance != nullptr) { - delete NarrowbandRadio::instance; - NarrowbandRadio::instance = nullptr; - } +// C COMPATIBILITY WRAPPERS + +extern "C" { + void init_narrowband(QueueHandle_t* commandQueue, QueueHandle_t* sensorDataQueue) { + nb_radio.init(commandQueue, sensorDataQueue); } } diff --git a/main/narrowband.h b/main/narrowband.h index d91d478..924b3a6 100644 --- a/main/narrowband.h +++ b/main/narrowband.h @@ -1,16 +1,13 @@ +#pragma once #ifdef __cplusplus extern "C" { #endif #include -#include +#include +#include -void init_narrowband(void); -// Dequeue into caller-provided buffer; returns number of bytes written, -// 0 if no data, -1 on error or if buffer is too small -ssize_t dequeue_command(uint8_t* buf, size_t max_len); -void narrowband_thread(void); -void destroy_narrowband(void); +void init_narrowband(QueueHandle_t* commandQueue, QueueHandle_t* sensorDataQueue); #ifdef __cplusplus } From dc5f49efed6de9f063c4a92b80c7c29fa2130247 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Thu, 12 Mar 2026 02:23:44 +0100 Subject: [PATCH 09/20] implemented interrupt based receive via freertos task notifications (faster than semaphores) --- main/narrowband.cpp | 58 ++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 4d16018..63a6272 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -1,21 +1,16 @@ #include "narrowband.h" #include -#include -#include #include "EspHal.h" #include "esp_log.h" #include +#include #include -#include // for ISR flag #include #include +#include namespace { - // flag set in ISR when packet arrives - // atomic operation to avoid data race between interrupt and main thread - static std::atomic s_received_packet{false}; - static const char* TAG = "Narrowband"; // CLASS DEFINITION @@ -32,7 +27,7 @@ namespace { static constexpr int BUSY_PIN = 21; static constexpr int RXEN_PIN = 16; - int rxtx_interval_ms = 500; + static constexpr uint16_t rxtx_interval_ms = 500; EspHal hal; Module module; @@ -41,6 +36,9 @@ namespace { QueueHandle_t* commandQueue; QueueHandle_t* sensorDataQueue; + TaskHandle_t rxtxTaskHandle; + const UBaseType_t rxtxTaskNotifyIndex = 1; // index of the notification value used for receive ISR flag + void handleReceive(); static void IRAM_ATTR receive_isr(void); static void rxtx_task_trampoline(void* param); @@ -51,6 +49,8 @@ namespace { void init(QueueHandle_t* commandQueue, QueueHandle_t* sensorDataQueue); }; + // static instance of the narrowband class + NarrowbandRadio nb_radio; // CLASS IMPLEMENTATION @@ -59,7 +59,8 @@ namespace { module(&hal, NSS_PIN, DIO1_PIN, NRST_PIN, BUSY_PIN), radio(&module), commandQueue(nullptr), - sensorDataQueue(nullptr) {} + sensorDataQueue(nullptr), + rxtxTaskHandle(nullptr) {} void NarrowbandRadio::init(QueueHandle_t* commandQueue, QueueHandle_t* sensorDataQueue) { ESP_LOGI(TAG, "[LLCC68] Initializing narrowband radio..."); @@ -97,23 +98,24 @@ namespace { // ISR callback stored in IRAM; just sets the atomic flag void IRAM_ATTR NarrowbandRadio::receive_isr(void) { - s_received_packet.store(true, std::memory_order_relaxed); + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + configASSERT( nb_radio.rxtxTaskHandle != NULL ); + + vTaskNotifyGiveIndexedFromISR( nb_radio.rxtxTaskHandle, nb_radio.rxtxTaskNotifyIndex, &xHigherPriorityTaskWoken ); + portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); } void NarrowbandRadio::handleReceive() { - // check and clear ISR flag - if (!s_received_packet.load(std::memory_order_relaxed)) { - return; - } - s_received_packet.store(false, std::memory_order_relaxed); - size_t len = radio.getPacketLength(); - uint8_t* buf = (uint8_t*)malloc(len); + uint8_t* buf = (uint8_t*)malloc(len + 1); // +1 for null terminator int state = radio.readData(buf, len); if (state == RADIOLIB_ERR_NONE) { + // segfault if data is not null-terminated + buf[len] = '\0'; ESP_LOGI(TAG, "Received packet: %s\n", buf); // maybe parse command here, currently we just enqueue the raw packet data for processing in the main thread // enqueueCommand(buf); @@ -132,6 +134,7 @@ namespace { void NarrowbandRadio::rxtx_task() { ESP_LOGI(TAG, "[LLCC68] Started rxtx task!\n"); + rxtxTaskHandle = xTaskGetCurrentTaskHandle(); int state = RADIOLIB_ERR_NONE; while (true) { @@ -154,16 +157,23 @@ namespace { ESP_LOGI(TAG, "failed to start receiver, code %d\n", state); } - // wait 0.5 s - hal.delay(rxtx_interval_ms); - - // if no packet was received, we just start sending again - handleReceive(); + // receive for 0.5 s (the amount specified by rxtx_interval_ms) + uint32_t ulNotificationValue; + TickType_t start = xTaskGetTickCount(); + uint16_t elapsed_time_ms = 0; + while (elapsed_time_ms < rxtx_interval_ms) { + ulNotificationValue = ulTaskNotifyTakeIndexed(rxtxTaskNotifyIndex, pdTRUE, pdMS_TO_TICKS(rxtx_interval_ms - elapsed_time_ms)); + if (ulNotificationValue == 1) { + handleReceive(); + elapsed_time_ms = pdTICKS_TO_MS(xTaskGetTickCount() - start); + } else { + // timeout, no packet received within the interval + break; + } + } } } - // static instance of the narrowband class - NarrowbandRadio nb_radio; } // C COMPATIBILITY WRAPPERS From 8a0ee6c9cf741783f253cf72cf9783c58dd908b1 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Sun, 15 Mar 2026 01:21:52 +0100 Subject: [PATCH 10/20] finished sketch for interrupt based send and receive with freeRTOS notifications --- main/narrowband.cpp | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 63a6272..ed6fafb 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -28,6 +28,7 @@ namespace { static constexpr int RXEN_PIN = 16; static constexpr uint16_t rxtx_interval_ms = 500; + static constexpr uint32_t tx_timeout_ms = 500; EspHal hal; Module module; @@ -40,6 +41,7 @@ namespace { const UBaseType_t rxtxTaskNotifyIndex = 1; // index of the notification value used for receive ISR flag void handleReceive(); + static void IRAM_ATTR transmit_isr(void); static void IRAM_ATTR receive_isr(void); static void rxtx_task_trampoline(void* param); void rxtx_task(); @@ -88,14 +90,24 @@ namespace { } ESP_LOGI(TAG, "[LLCC68] PA config configured!\n"); - // configure callback for received packet; must be a free/IRAM-safe function + // configure callback for received/transmitted packet; must be a free/IRAM-safe function radio.setPacketReceivedAction(receive_isr); + radio.setPacketTransmittedAction(transmit_isr); // create the rx/tx task // xTaskCreate(rxtx_task_trampoline, "narrowband_rxtx", 4096, this, 1, NULL); } + void IRAM_ATTR NarrowbandRadio::transmit_isr(void) { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + configASSERT( nb_radio.rxtxTaskHandle != NULL ); + + vTaskNotifyGiveIndexedFromISR( nb_radio.rxtxTaskHandle, nb_radio.rxtxTaskNotifyIndex, &xHigherPriorityTaskWoken ); + portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); + } + // ISR callback stored in IRAM; just sets the atomic flag void IRAM_ATTR NarrowbandRadio::receive_isr(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; @@ -135,30 +147,40 @@ namespace { void NarrowbandRadio::rxtx_task() { ESP_LOGI(TAG, "[LLCC68] Started rxtx task!\n"); rxtxTaskHandle = xTaskGetCurrentTaskHandle(); + uint32_t ulNotificationValue; int state = RADIOLIB_ERR_NONE; while (true) { ESP_LOGI(TAG, "[LLCC68] Sending packet..."); // TODO: replace with sensor data from sensor queue - state = radio.transmit("Hello world!"); - if (state == RADIOLIB_ERR_NONE) { - // the packet was successfully transmitted - ESP_LOGI(TAG, "success!\n"); + state = radio.startTransmit("Hello world!"); + + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "\nstartTransmit failed, code %d\n", state); + } + + ulNotificationValue = ulTaskNotifyTakeIndexed(rxtxTaskNotifyIndex, pdTRUE, pdMS_TO_TICKS(tx_timeout_ms)); + if (ulNotificationValue == 1) { + state = radio.finishTransmit(); + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "\nTransmission failed during finishTransmit, code %d\n", state); + } else { + ESP_LOGI(TAG, " done!\n"); + } } else { - ESP_LOGI(TAG, "failed, code %d\n", state); + ESP_LOGI(TAG, "\nTransmission timeout, no callback received within %d ms\n", tx_timeout_ms); } + state = radio.startReceive(); if (state == RADIOLIB_ERR_NONE) { - // the module is now in receive mode, waiting for a packet ESP_LOGI(TAG, "Waiting for a packet...\n"); } else { ESP_LOGI(TAG, "failed to start receiver, code %d\n", state); } // receive for 0.5 s (the amount specified by rxtx_interval_ms) - uint32_t ulNotificationValue; TickType_t start = xTaskGetTickCount(); uint16_t elapsed_time_ms = 0; while (elapsed_time_ms < rxtx_interval_ms) { @@ -171,6 +193,12 @@ namespace { break; } } + + // TODO: check if this can be left out + state = radio.finishReceive(); + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "failed to finish receive, code %d\n", state); + } } } From b00b9c436548f75d715af8280fc9224415f13504 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Tue, 17 Mar 2026 20:55:39 +0100 Subject: [PATCH 11/20] implemented freertos queues for sensor data and received commands --- main/message.h | 15 +++++ main/narrowband.cpp | 140 ++++++++++++++++++++++++++------------------ 2 files changed, 97 insertions(+), 58 deletions(-) create mode 100644 main/message.h diff --git a/main/message.h b/main/message.h new file mode 100644 index 0000000..50e7648 --- /dev/null +++ b/main/message.h @@ -0,0 +1,15 @@ +#pragma once +#ifdef __cplusplus +extern "C" { +#endif + +#include + +struct message_t { + uint8_t *data; // max packet size for LLCC68 is 255 bytes, and 254 with address filtering, but we don't use address filtering + size_t length; +}; + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/main/narrowband.cpp b/main/narrowband.cpp index ed6fafb..b58bc1d 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -1,4 +1,3 @@ -#include "narrowband.h" #include #include "EspHal.h" #include "esp_log.h" @@ -8,6 +7,8 @@ #include #include #include +#include "narrowband.h" +#include "message.h" namespace { @@ -38,9 +39,11 @@ namespace { QueueHandle_t* sensorDataQueue; TaskHandle_t rxtxTaskHandle; - const UBaseType_t rxtxTaskNotifyIndex = 1; // index of the notification value used for receive ISR flag + static constexpr UBaseType_t rxtxTaskNotifyIndex = 1; // index of the notification value used for receive ISR flag void handleReceive(); + void transmit_sensor_data(); + void listen_for_command(); static void IRAM_ATTR transmit_isr(void); static void IRAM_ATTR receive_isr(void); static void rxtx_task_trampoline(void* param); @@ -92,7 +95,7 @@ namespace { // configure callback for received/transmitted packet; must be a free/IRAM-safe function radio.setPacketReceivedAction(receive_isr); - radio.setPacketTransmittedAction(transmit_isr); + radio.setPacketSentAction(transmit_isr); // create the rx/tx task // xTaskCreate(rxtx_task_trampoline, "narrowband_rxtx", 4096, this, 1, NULL); @@ -120,85 +123,106 @@ namespace { void NarrowbandRadio::handleReceive() { + // TODO: maybe implement a static memory pool to avoid memory fragmentation in the long run size_t len = radio.getPacketLength(); - uint8_t* buf = (uint8_t*)malloc(len + 1); // +1 for null terminator + uint8_t* buf = (uint8_t*)malloc(len); int state = radio.readData(buf, len); if (state == RADIOLIB_ERR_NONE) { - // segfault if data is not null-terminated - buf[len] = '\0'; - ESP_LOGI(TAG, "Received packet: %s\n", buf); - // maybe parse command here, currently we just enqueue the raw packet data for processing in the main thread - // enqueueCommand(buf); + + message_t msg = { + .data = buf, + .length = len + }; + + ESP_LOGI(TAG, "Received packet of length %d bytes\n", len); + + // msg struct is queued by copy, buffer is not, so we need to free it after dequeueing + if (xQueueSend( *commandQueue, (void *) &msg, ( TickType_t ) 0 ) != pdTRUE) { + ESP_LOGE(TAG, "Failed to enqueue received command, command queue is full!\n"); + } } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { ESP_LOGI(TAG, "Received packet with CRC mismatch!\n"); } else { ESP_LOGI(TAG, "Failed to read received packet, code %d\n", state); } - - free(buf); } - void NarrowbandRadio::rxtx_task_trampoline(void* param) { - static_cast(param)->rxtx_task(); - } + void NarrowbandRadio::transmit_sensor_data() { - void NarrowbandRadio::rxtx_task() { - ESP_LOGI(TAG, "[LLCC68] Started rxtx task!\n"); - rxtxTaskHandle = xTaskGetCurrentTaskHandle(); - uint32_t ulNotificationValue; - - int state = RADIOLIB_ERR_NONE; - while (true) { - ESP_LOGI(TAG, "[LLCC68] Sending packet..."); + message_t msg; + if (xQueueReceive( *sensorDataQueue, &msg, (TickType_t) 0 ) != pdTRUE) { + ESP_LOGE(TAG, "Failed to receive sensor data from queue\n"); + free(msg.data); + return; + } - // TODO: replace with sensor data from sensor queue - state = radio.startTransmit("Hello world!"); + int state = radio.startTransmit(msg.data, msg.length); + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "startTransmit failed, code %d\n", state); + free(msg.data); + return; + } + // wait for transmission to complete or timeout + uint32_t ulNotificationValue = ulTaskNotifyTakeIndexed(rxtxTaskNotifyIndex, pdTRUE, pdMS_TO_TICKS(tx_timeout_ms)); + if (ulNotificationValue == 1) { + state = radio.finishTransmit(); if (state != RADIOLIB_ERR_NONE) { - ESP_LOGI(TAG, "\nstartTransmit failed, code %d\n", state); + ESP_LOGI(TAG, "Transmission failed during finishTransmit, code %d\n", state); + } else { + ESP_LOGI(TAG, "Transmission successful!\n"); } + } else { + ESP_LOGI(TAG, "Transmission timeout, no callback received within %d ms\n", tx_timeout_ms); + } + + free(msg.data); + } + + // TODO: send back ACKknowledgement for received commands, to give sender the option to retry if ACK is not received within a certain time frame + void NarrowbandRadio::listen_for_command() { + int state = radio.startReceive(); + if (state == RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "Waiting for a packet...\n"); + } else { + ESP_LOGI(TAG, "failed to start receiver, code %d\n", state); + } - ulNotificationValue = ulTaskNotifyTakeIndexed(rxtxTaskNotifyIndex, pdTRUE, pdMS_TO_TICKS(tx_timeout_ms)); + // receive for 0.5 s (the amount specified by rxtx_interval_ms) + TickType_t start = xTaskGetTickCount(); + uint16_t elapsed_time_ms = 0; + uint32_t ulNotificationValue; + while (elapsed_time_ms < rxtx_interval_ms) { + ulNotificationValue = ulTaskNotifyTakeIndexed(rxtxTaskNotifyIndex, pdTRUE, pdMS_TO_TICKS(rxtx_interval_ms - elapsed_time_ms)); if (ulNotificationValue == 1) { - state = radio.finishTransmit(); - if (state != RADIOLIB_ERR_NONE) { - ESP_LOGI(TAG, "\nTransmission failed during finishTransmit, code %d\n", state); - } else { - ESP_LOGI(TAG, " done!\n"); - } + handleReceive(); + elapsed_time_ms = pdTICKS_TO_MS(xTaskGetTickCount() - start); } else { - ESP_LOGI(TAG, "\nTransmission timeout, no callback received within %d ms\n", tx_timeout_ms); + // timeout, no packet received within the interval + break; } + } + + // TODO: check if this can be left out + state = radio.finishReceive(); + if (state != RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "failed to finish receive, code %d\n", state); + } + } + void NarrowbandRadio::rxtx_task_trampoline(void* param) { + static_cast(param)->rxtx_task(); + } - state = radio.startReceive(); - if (state == RADIOLIB_ERR_NONE) { - ESP_LOGI(TAG, "Waiting for a packet...\n"); - } else { - ESP_LOGI(TAG, "failed to start receiver, code %d\n", state); - } + void NarrowbandRadio::rxtx_task() { + ESP_LOGI(TAG, "[LLCC68] Started rxtx task!\n"); + rxtxTaskHandle = xTaskGetCurrentTaskHandle(); - // receive for 0.5 s (the amount specified by rxtx_interval_ms) - TickType_t start = xTaskGetTickCount(); - uint16_t elapsed_time_ms = 0; - while (elapsed_time_ms < rxtx_interval_ms) { - ulNotificationValue = ulTaskNotifyTakeIndexed(rxtxTaskNotifyIndex, pdTRUE, pdMS_TO_TICKS(rxtx_interval_ms - elapsed_time_ms)); - if (ulNotificationValue == 1) { - handleReceive(); - elapsed_time_ms = pdTICKS_TO_MS(xTaskGetTickCount() - start); - } else { - // timeout, no packet received within the interval - break; - } - } - - // TODO: check if this can be left out - state = radio.finishReceive(); - if (state != RADIOLIB_ERR_NONE) { - ESP_LOGI(TAG, "failed to finish receive, code %d\n", state); - } + while (true) { + transmit_sensor_data(); + listen_for_command(); } } From 0cc125932c5225f6efe94c0b8053ffff8153391d Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Tue, 24 Mar 2026 19:33:58 +0100 Subject: [PATCH 12/20] start rxtx task in nb_radio.init() --- main/narrowband.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index b58bc1d..8123c13 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -98,7 +98,8 @@ namespace { radio.setPacketSentAction(transmit_isr); // create the rx/tx task - // xTaskCreate(rxtx_task_trampoline, "narrowband_rxtx", 4096, this, 1, NULL); + xTaskCreate(rxtx_task_trampoline, "rxtx", 4096, this, 1, &rxtxTaskHandle); + configASSERT( rxtxTaskHandle != NULL ); } @@ -218,7 +219,6 @@ namespace { void NarrowbandRadio::rxtx_task() { ESP_LOGI(TAG, "[LLCC68] Started rxtx task!\n"); - rxtxTaskHandle = xTaskGetCurrentTaskHandle(); while (true) { transmit_sensor_data(); From 5286e2f13651633713500a466fae8737ac46318b Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Tue, 24 Mar 2026 19:56:03 +0100 Subject: [PATCH 13/20] added init code to main to start nb module --- main/main.c | 17 +++++++++++++++++ main/narrowband.h | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/main/main.c b/main/main.c index 081067c..c4da9e5 100644 --- a/main/main.c +++ b/main/main.c @@ -5,6 +5,9 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "message.h" +#include "narrowband.h" + // static const char *TAG = "app_main"; void app_main(void) @@ -14,4 +17,18 @@ void app_main(void) .network_mode = NW_MODE_APSTA }; ESP_ERROR_CHECK(vigilant_init(VgConfig)); + + // init narrowband communication + QueueHandle_t commandQueue = xQueueCreate(10, sizeof(message_t)); // for now we initialize the queues to 10 elements, perhaps subject to change + QueueHandle_t sensorDataQueue = xQueueCreate(10, sizeof(message_t)); + init_narrowband(&commandQueue, &sensorDataQueue); + + while (1) { + ESP_ERROR_CHECK(status_led_set_rgb(100, 100, 100)); + vTaskDelay(pdMS_TO_TICKS(300)); + + ESP_ERROR_CHECK(status_led_off()); + vTaskDelay(pdMS_TO_TICKS(300)); + + } } diff --git a/main/narrowband.h b/main/narrowband.h index 924b3a6..bae9501 100644 --- a/main/narrowband.h +++ b/main/narrowband.h @@ -7,6 +7,11 @@ extern "C" { #include #include +/* +* Initializes the narrowband communication module, and starts the rxtx task which continuously transmits sensor data and listens for commands. +* @param commandQueue pointer to FreeRTOS queue for data received by narrowband module +* @param sensorDataQueue pointer to FreeRTOS queue for data to be transmitted by narrowband module +*/ void init_narrowband(QueueHandle_t* commandQueue, QueueHandle_t* sensorDataQueue); #ifdef __cplusplus From 5fee59ca0d44d744a051d1df6648dac1aa589973 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Tue, 7 Apr 2026 18:43:33 +0200 Subject: [PATCH 14/20] started working on fragmentation, UNFINSIHED --- main/narrowband.cpp | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 8123c13..ecf1983 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -40,8 +40,9 @@ namespace { TaskHandle_t rxtxTaskHandle; static constexpr UBaseType_t rxtxTaskNotifyIndex = 1; // index of the notification value used for receive ISR flag - + void handleReceive(); + std::array pack_messages(); void transmit_sensor_data(); void listen_for_command(); static void IRAM_ATTR transmit_isr(void); @@ -150,6 +151,41 @@ namespace { } } + // return value optimization makes sure no copying takes place even when returning by value, so this is efficient + std::array NarrowbandRadio::pack_messages(message_t& fragment, bool last_packet) { + std::array buffer; // max payload size of LLCC68 is 256 bytes + size_t offset = 0; + + if (fragment.length > 0 && fragment.length <= buffer.size()) { + memcpy(buffer.data(), fragment.data, fragment.length); + offset += fragment.length; + free(fragment.data); // free the message data after packing + } else if (fragment.length > buffer.size()) { + ESP_LOGE(TAG, "Fragment length %d exceeds buffer size, discarding fragment\n", fragment.length); + free(fragment.data); + } + + + while (offset < buffer.size()) { + message_t msg; + if (xQueueReceive( *sensorDataQueue, &msg, (TickType_t) 0 ) == pdTRUE) { + if (offset + msg.length <= buffer.size()) { + memcpy(buffer.data() + offset, msg.data, msg.length); + offset += msg.length; + } else { + // we can choose to either discard the message or stop packing further messages; for now we just stop packing + break; + } + free(msg.data); // free the message data after packing + } else { + // no more messages in the queue + break; + } + } + + return buffer; + } + void NarrowbandRadio::transmit_sensor_data() { message_t msg; From 8c0e22572156042d32fd7aea971bea7b8dbb7b05 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Tue, 14 Apr 2026 15:04:26 +0200 Subject: [PATCH 15/20] comments --- main/narrowband.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index ecf1983..927be82 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -123,6 +123,8 @@ namespace { portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); } + + // UNFINISHED, TODO: handle defragmentation void NarrowbandRadio::handleReceive() { // TODO: maybe implement a static memory pool to avoid memory fragmentation in the long run @@ -151,6 +153,7 @@ namespace { } } + // UNFINISHED // return value optimization makes sure no copying takes place even when returning by value, so this is efficient std::array NarrowbandRadio::pack_messages(message_t& fragment, bool last_packet) { std::array buffer; // max payload size of LLCC68 is 256 bytes From 2ea2262b8b647b08c0ee2979844bbbec6cf7e543 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Tue, 14 Apr 2026 20:11:25 +0200 Subject: [PATCH 16/20] finished pack_message for narrowband tp --- main/narrowband.cpp | 56 ++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 927be82..687f479 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -37,6 +37,11 @@ namespace { QueueHandle_t* commandQueue; QueueHandle_t* sensorDataQueue; + + message_t currentTxMessage; + message_t currentRxMessage; + size_t currentTxMessageOffset; + size_t currentRxMessageOffset; TaskHandle_t rxtxTaskHandle; static constexpr UBaseType_t rxtxTaskNotifyIndex = 1; // index of the notification value used for receive ISR flag @@ -66,6 +71,10 @@ namespace { radio(&module), commandQueue(nullptr), sensorDataQueue(nullptr), + currentTxMessage{nullptr, 0}, + currentRxMessage{nullptr, 0}, + currentTxMessageOffset(0), + currentRxMessageOffset(0), rxtxTaskHandle(nullptr) {} void NarrowbandRadio::init(QueueHandle_t* commandQueue, QueueHandle_t* sensorDataQueue) { @@ -153,35 +162,44 @@ namespace { } } - // UNFINISHED // return value optimization makes sure no copying takes place even when returning by value, so this is efficient - std::array NarrowbandRadio::pack_messages(message_t& fragment, bool last_packet) { + std::array NarrowbandRadio::pack_messages() { std::array buffer; // max payload size of LLCC68 is 256 bytes size_t offset = 0; - if (fragment.length > 0 && fragment.length <= buffer.size()) { - memcpy(buffer.data(), fragment.data, fragment.length); - offset += fragment.length; - free(fragment.data); // free the message data after packing - } else if (fragment.length > buffer.size()) { - ESP_LOGE(TAG, "Fragment length %d exceeds buffer size, discarding fragment\n", fragment.length); - free(fragment.data); + if (currentTxMessage.length > 0 && currentTxMessage.length + sizeof(uint8_t) <= buffer.size()) { + + buffer[offset++] = (uint8_t)(currentTxMessage.length & 0xFF); // store length as 1 byte, assuming max message length is 255 + memcpy(buffer.data() + offset, currentTxMessage.data, currentTxMessage.length); + offset += currentTxMessage.length; + free(currentTxMessage.data); // free the message data after packing + currentTxMessage.data = nullptr; + currentTxMessage.length = 0; + + } else if (currentTxMessage.length + sizeof(uint8_t) > buffer.size()) { + ESP_LOGE(TAG, "Fragment length %d exceeds buffer size, discarding fragment\n", currentTxMessage.length); + free(currentTxMessage.data); + currentTxMessage.data = nullptr; + currentTxMessage.length = 0; } while (offset < buffer.size()) { - message_t msg; - if (xQueueReceive( *sensorDataQueue, &msg, (TickType_t) 0 ) == pdTRUE) { - if (offset + msg.length <= buffer.size()) { - memcpy(buffer.data() + offset, msg.data, msg.length); - offset += msg.length; - } else { - // we can choose to either discard the message or stop packing further messages; for now we just stop packing - break; - } - free(msg.data); // free the message data after packing + free(currentTxMessage.data); // free the previous message data before receiving the next message from the queue + if (xQueueReceive( *sensorDataQueue, ¤tTxMessage, (TickType_t) 0 ) == pdTRUE) { + currentTxMessageOffset = 0; // reset offset for new message + + buffer[offset++] = (uint8_t)(currentTxMessage.length & 0xFF); // store length as 1 byte, assuming max message length is 255 + + size_t bytes_to_copy = std::min(currentTxMessage.length, buffer.size() - offset); + memcpy(buffer.data() + offset, currentTxMessage.data, bytes_to_copy); + offset += bytes_to_copy; + currentTxMessageOffset += bytes_to_copy; + } else { // no more messages in the queue + currentTxMessage.data = nullptr; + currentTxMessage.length = 0; break; } } From b92391214ae3ee2cdf6c2a00085f4458f2cd04da Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Wed, 15 Apr 2026 00:21:36 +0200 Subject: [PATCH 17/20] implemented pack and unpack message functions for transport layer --- main/narrowband.cpp | 100 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 687f479..dd366c7 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -163,32 +163,41 @@ namespace { } // return value optimization makes sure no copying takes place even when returning by value, so this is efficient - std::array NarrowbandRadio::pack_messages() { + std::array NarrowbandRadio::pack_messages(QueueHandle_t* queue) { std::array buffer; // max payload size of LLCC68 is 256 bytes size_t offset = 0; - if (currentTxMessage.length > 0 && currentTxMessage.length + sizeof(uint8_t) <= buffer.size()) { + if (currentTxMessage.length > currentTxMessageOffset) { - buffer[offset++] = (uint8_t)(currentTxMessage.length & 0xFF); // store length as 1 byte, assuming max message length is 255 - memcpy(buffer.data() + offset, currentTxMessage.data, currentTxMessage.length); - offset += currentTxMessage.length; - free(currentTxMessage.data); // free the message data after packing - currentTxMessage.data = nullptr; - currentTxMessage.length = 0; - - } else if (currentTxMessage.length + sizeof(uint8_t) > buffer.size()) { - ESP_LOGE(TAG, "Fragment length %d exceeds buffer size, discarding fragment\n", currentTxMessage.length); - free(currentTxMessage.data); - currentTxMessage.data = nullptr; - currentTxMessage.length = 0; + size_t bytes_to_copy = std::min(currentTxMessage.length - currentTxMessageOffset, buffer.size()); + memcpy(buffer.data(), currentTxMessage.data + currentTxMessageOffset, bytes_to_copy); + offset += bytes_to_copy; + if (currentTxMessageOffset + bytes_to_copy >= currentTxMessage.length) { + // message fully packed, free the message data and reset the current message + free(currentTxMessage.data); // free the message data after packing + currentTxMessage.data = nullptr; + currentTxMessage.length = 0; + currentTxMessageOffset = 0; + } else { + // message not fully packed, update the offset for the next pack + currentTxMessageOffset += bytes_to_copy; + offset += bytes_to_copy; // this makes sure to skip the while loop + } } - while (offset < buffer.size()) { free(currentTxMessage.data); // free the previous message data before receiving the next message from the queue - if (xQueueReceive( *sensorDataQueue, ¤tTxMessage, (TickType_t) 0 ) == pdTRUE) { + if (xQueueReceive( *queue, ¤tTxMessage, (TickType_t) 0 ) == pdTRUE) { currentTxMessageOffset = 0; // reset offset for new message + if (currentTxMessage.length == 0) { + // skip empty messages + continue; + } else if (currentTxMessage.length > 255) { + ESP_LOGE(TAG, "Message length exceeds maximum of 255 bytes, truncating message\n"); + currentTxMessage.length = 255; // truncate message to max length + } + buffer[offset++] = (uint8_t)(currentTxMessage.length & 0xFF); // store length as 1 byte, assuming max message length is 255 size_t bytes_to_copy = std::min(currentTxMessage.length, buffer.size() - offset); @@ -198,8 +207,10 @@ namespace { } else { // no more messages in the queue + buffer[offset] = 0; // indicate end of messages with a length of 0 currentTxMessage.data = nullptr; currentTxMessage.length = 0; + currentTxMessageOffset = 0; break; } } @@ -207,6 +218,63 @@ namespace { return buffer; } + void NarrowbandRadio::unpack_messages(const std::array& buffer, size_t length, QueueHandle_t* queue) { + size_t offset = 0; + + if (currentRxMessage.length > currentRxMessageOffset) { + size_t bytes_to_copy = std::min(currentRxMessage.length - currentRxMessageOffset, length); + memcpy(currentRxMessage.data + currentRxMessageOffset, buffer.data(), bytes_to_copy); + if (currentRxMessageOffset + bytes_to_copy >= currentRxMessage.length) { + // message fully unpacked, enqueue the message and reset the current message + message_t msg = { + .data = currentRxMessage.data, + .length = currentRxMessage.length + }; + + if (xQueueSend( *queue, (void *) &msg, ( TickType_t ) 0 ) != pdTRUE) { + ESP_LOGE(TAG, "Failed to enqueue received command, command queue is full!\n"); + free(currentRxMessage.data); // free the message data if it cannot be enqueued + } + + currentRxMessage.data = nullptr; + currentRxMessage.length = 0; + currentRxMessageOffset = 0; + } else { + // message not fully unpacked, update the offset for the next unpack + currentRxMessageOffset += bytes_to_copy; + offset += bytes_to_copy; // this makes sure to skip the while loop + } + } + + while (offset < length) { + uint8_t msg_length = buffer[offset++]; + + uint8_t* msg_data = (uint8_t*)malloc(msg_length); + + size_t bytes_to_copy = std::min((size_t)msg_length, length - offset); + memcpy(msg_data, buffer.data() + offset, bytes_to_copy); + offset += bytes_to_copy; + + if (bytes_to_copy < msg_length) { + // message not fully unpacked, store the partial message and wait for the next buffer to unpack the rest + currentRxMessage.data = msg_data; + currentRxMessage.length = msg_length; + currentRxMessageOffset = bytes_to_copy; + break; + } + + message_t msg = { + .data = msg_data, + .length = msg_length + }; + + if (xQueueSend( *queue, (void *) &msg, ( TickType_t ) 0 ) != pdTRUE) { + ESP_LOGE(TAG, "Failed to enqueue received command, command queue is full!\n"); + free(msg_data); // free the message data if it cannot be enqueued + } + } + } + void NarrowbandRadio::transmit_sensor_data() { message_t msg; From aba95f05f0898b6ca0f915f206d8c25e98d659da Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Wed, 15 Apr 2026 00:30:17 +0200 Subject: [PATCH 18/20] added some todo comments --- main/narrowband.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index dd366c7..70d8529 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -162,6 +162,8 @@ namespace { } } + // TODO: consider if dropping messages when queue is full is acceptable + // TODO: ackknowledgement mechanism? -> msg_len 0 could indicate an ack msg // return value optimization makes sure no copying takes place even when returning by value, so this is efficient std::array NarrowbandRadio::pack_messages(QueueHandle_t* queue) { std::array buffer; // max payload size of LLCC68 is 256 bytes @@ -207,7 +209,7 @@ namespace { } else { // no more messages in the queue - buffer[offset] = 0; // indicate end of messages with a length of 0 + // TODO: return length, so packet can be shorter than 256 bytes if there are no more messages to pack currentTxMessage.data = nullptr; currentTxMessage.length = 0; currentTxMessageOffset = 0; @@ -218,6 +220,8 @@ namespace { return buffer; } + // TODO: consider if dropping messages when queue is full is acceptable + // TODO: ackknowledgement mechanism? -> msg_len 0 could indicate an ack msg void NarrowbandRadio::unpack_messages(const std::array& buffer, size_t length, QueueHandle_t* queue) { size_t offset = 0; From 7758fa4a88903d42e2a703bbc34136c9b1d6af97 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Thu, 16 Apr 2026 19:38:13 +0200 Subject: [PATCH 19/20] refractored unpack and pack to use span instead of std::array, pack is returning size of packed buffer, added check to see if malloc fails --- main/narrowband.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 70d8529..35cb6c5 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -34,6 +34,7 @@ namespace { EspHal hal; Module module; LLCC68 radio; + static constexpr uint8_t max_payload_size = 256; // max payload size of LLCC68 is 256 bytes QueueHandle_t* commandQueue; QueueHandle_t* sensorDataQueue; @@ -164,9 +165,8 @@ namespace { // TODO: consider if dropping messages when queue is full is acceptable // TODO: ackknowledgement mechanism? -> msg_len 0 could indicate an ack msg - // return value optimization makes sure no copying takes place even when returning by value, so this is efficient - std::array NarrowbandRadio::pack_messages(QueueHandle_t* queue) { - std::array buffer; // max payload size of LLCC68 is 256 bytes + // returns number of bytes packed into buffer + size_t NarrowbandRadio::pack_messages(std::span buffer, QueueHandle_t* queue) { size_t offset = 0; if (currentTxMessage.length > currentTxMessageOffset) { @@ -196,6 +196,8 @@ namespace { // skip empty messages continue; } else if (currentTxMessage.length > 255) { + // 255 is the limit currently, because our lenght indicator is only 1 byte. + // if longer messages are needed, consider using 2 bytes for length indicator ESP_LOGE(TAG, "Message length exceeds maximum of 255 bytes, truncating message\n"); currentTxMessage.length = 255; // truncate message to max length } @@ -209,7 +211,6 @@ namespace { } else { // no more messages in the queue - // TODO: return length, so packet can be shorter than 256 bytes if there are no more messages to pack currentTxMessage.data = nullptr; currentTxMessage.length = 0; currentTxMessageOffset = 0; @@ -217,12 +218,13 @@ namespace { } } - return buffer; + return offset; } // TODO: consider if dropping messages when queue is full is acceptable // TODO: ackknowledgement mechanism? -> msg_len 0 could indicate an ack msg - void NarrowbandRadio::unpack_messages(const std::array& buffer, size_t length, QueueHandle_t* queue) { + void NarrowbandRadio::unpack_messages(const std::span buffer, QueueHandle_t* queue) { + size_t length = buffer.size(); size_t offset = 0; if (currentRxMessage.length > currentRxMessageOffset) { @@ -254,6 +256,10 @@ namespace { uint8_t msg_length = buffer[offset++]; uint8_t* msg_data = (uint8_t*)malloc(msg_length); + if (msg_data == nullptr) { + ESP_LOGE(TAG, "Failed to allocate memory for received message of length %d bytes\n", msg_length); + break; // stop unpacking + } size_t bytes_to_copy = std::min((size_t)msg_length, length - offset); memcpy(msg_data, buffer.data() + offset, bytes_to_copy); @@ -267,6 +273,7 @@ namespace { break; } + // message fully unpacked, enqueue the message message_t msg = { .data = msg_data, .length = msg_length From 5faa49bb32d27c47cd802973b4077dad3ea7af32 Mon Sep 17 00:00:00 2001 From: Till Krahmer Date: Thu, 16 Apr 2026 20:02:22 +0200 Subject: [PATCH 20/20] implemented message fragmentation --- main/narrowband.cpp | 84 +++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 45 deletions(-) diff --git a/main/narrowband.cpp b/main/narrowband.cpp index 35cb6c5..7f9ba3e 100644 --- a/main/narrowband.cpp +++ b/main/narrowband.cpp @@ -47,10 +47,11 @@ namespace { TaskHandle_t rxtxTaskHandle; static constexpr UBaseType_t rxtxTaskNotifyIndex = 1; // index of the notification value used for receive ISR flag - void handleReceive(); - std::array pack_messages(); - void transmit_sensor_data(); + size_t pack_messages(std::span buffer, QueueHandle_t* queue); + void unpack_messages(const std::span buffer, QueueHandle_t* queue); + void transmit_data(std::span buffer); void listen_for_command(); + void handle_receive(); static void IRAM_ATTR transmit_isr(void); static void IRAM_ATTR receive_isr(void); static void rxtx_task_trampoline(void* param); @@ -134,35 +135,6 @@ namespace { } - // UNFINISHED, TODO: handle defragmentation - void NarrowbandRadio::handleReceive() { - - // TODO: maybe implement a static memory pool to avoid memory fragmentation in the long run - size_t len = radio.getPacketLength(); - uint8_t* buf = (uint8_t*)malloc(len); - - int state = radio.readData(buf, len); - - if (state == RADIOLIB_ERR_NONE) { - - message_t msg = { - .data = buf, - .length = len - }; - - ESP_LOGI(TAG, "Received packet of length %d bytes\n", len); - - // msg struct is queued by copy, buffer is not, so we need to free it after dequeueing - if (xQueueSend( *commandQueue, (void *) &msg, ( TickType_t ) 0 ) != pdTRUE) { - ESP_LOGE(TAG, "Failed to enqueue received command, command queue is full!\n"); - } - } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { - ESP_LOGI(TAG, "Received packet with CRC mismatch!\n"); - } else { - ESP_LOGI(TAG, "Failed to read received packet, code %d\n", state); - } - } - // TODO: consider if dropping messages when queue is full is acceptable // TODO: ackknowledgement mechanism? -> msg_len 0 could indicate an ack msg // returns number of bytes packed into buffer @@ -211,6 +183,7 @@ namespace { } else { // no more messages in the queue + ESP_LOGI(TAG, "No more messages to pack in the queue!\n"); currentTxMessage.data = nullptr; currentTxMessage.length = 0; currentTxMessageOffset = 0; @@ -255,6 +228,7 @@ namespace { while (offset < length) { uint8_t msg_length = buffer[offset++]; + // TODO: maybe implement a static memory pool to avoid memory fragmentation in the long run uint8_t* msg_data = (uint8_t*)malloc(msg_length); if (msg_data == nullptr) { ESP_LOGE(TAG, "Failed to allocate memory for received message of length %d bytes\n", msg_length); @@ -286,19 +260,11 @@ namespace { } } - void NarrowbandRadio::transmit_sensor_data() { + void NarrowbandRadio::transmit_data(std::span buffer) { - message_t msg; - if (xQueueReceive( *sensorDataQueue, &msg, (TickType_t) 0 ) != pdTRUE) { - ESP_LOGE(TAG, "Failed to receive sensor data from queue\n"); - free(msg.data); - return; - } - - int state = radio.startTransmit(msg.data, msg.length); + int state = radio.startTransmit(buffer.data(), buffer.size()); if (state != RADIOLIB_ERR_NONE) { ESP_LOGI(TAG, "startTransmit failed, code %d\n", state); - free(msg.data); return; } @@ -315,7 +281,29 @@ namespace { ESP_LOGI(TAG, "Transmission timeout, no callback received within %d ms\n", tx_timeout_ms); } - free(msg.data); + } + + void NarrowbandRadio::handle_receive() { + + // TODO: maybe implement a static memory pool to avoid memory fragmentation in the long run + size_t len = radio.getPacketLength(); + uint8_t* buf = (uint8_t*)malloc(len); + + int state = radio.readData(buf, len); + + if (state == RADIOLIB_ERR_CRC_MISMATCH) { + ESP_LOGI(TAG, "Received packet with CRC mismatch!\n"); + free(buf); + return; + } else if (state != RADIOLIB_ERR_NONE) { + ESP_LOGI(TAG, "Failed to read received packet, code %d\n", state); + free(buf); + return; + } + + ESP_LOGI(TAG, "Received packet with length %d bytes\n", len); + unpack_messages(std::span(buf, len), commandQueue); + free(buf); } // TODO: send back ACKknowledgement for received commands, to give sender the option to retry if ACK is not received within a certain time frame @@ -334,7 +322,7 @@ namespace { while (elapsed_time_ms < rxtx_interval_ms) { ulNotificationValue = ulTaskNotifyTakeIndexed(rxtxTaskNotifyIndex, pdTRUE, pdMS_TO_TICKS(rxtx_interval_ms - elapsed_time_ms)); if (ulNotificationValue == 1) { - handleReceive(); + handle_receive(); elapsed_time_ms = pdTICKS_TO_MS(xTaskGetTickCount() - start); } else { // timeout, no packet received within the interval @@ -356,8 +344,14 @@ namespace { void NarrowbandRadio::rxtx_task() { ESP_LOGI(TAG, "[LLCC68] Started rxtx task!\n"); + std::array tx_buffer; while (true) { - transmit_sensor_data(); + + // TX + size_t bytes_copied = pack_messages(tx_buffer, sensorDataQueue); + transmit_data(std::span(tx_buffer.data(), bytes_copied)); + + // RX listen_for_command(); } }