From 1ab7c4ce9abc7a73c1ecc08ef523d7322644defe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20H=C3=B6glinger?= Date: Sun, 14 Sep 2025 09:53:22 +0200 Subject: [PATCH] Add RadioDevice and support for SX1262 --- App/idf_component.yml | 5 + Drivers/RadioLibCompat/CMakeLists.txt | 5 + Drivers/RadioLibCompat/README.md | 3 + .../RadioLibCompat/Source/IsrTrampoline.cpp | 38 +++++ Drivers/RadioLibCompat/Source/IsrTrampoline.h | 118 +++++++++++++ .../Source/RadiolibTactilityHal.cpp | 155 ++++++++++++++++++ .../Source/RadiolibTactilityHal.h | 58 +++++++ Drivers/SX126x/CMakeLists.txt | 5 + Drivers/SX126x/README.md | 7 + Drivers/SX126x/Source/Sx1262.cpp | 125 ++++++++++++++ Drivers/SX126x/Source/Sx1262.h | 80 +++++++++ Tactility/Include/Tactility/hal/Device.h | 3 +- .../Include/Tactility/hal/radio/RadioDevice.h | 118 +++++++++++++ Tactility/Source/hal/radio/RadioDevice.cpp | 136 +++++++++++++++ 14 files changed, 855 insertions(+), 1 deletion(-) create mode 100644 Drivers/RadioLibCompat/CMakeLists.txt create mode 100644 Drivers/RadioLibCompat/README.md create mode 100644 Drivers/RadioLibCompat/Source/IsrTrampoline.cpp create mode 100644 Drivers/RadioLibCompat/Source/IsrTrampoline.h create mode 100644 Drivers/RadioLibCompat/Source/RadiolibTactilityHal.cpp create mode 100644 Drivers/RadioLibCompat/Source/RadiolibTactilityHal.h create mode 100644 Drivers/SX126x/CMakeLists.txt create mode 100644 Drivers/SX126x/README.md create mode 100644 Drivers/SX126x/Source/Sx1262.cpp create mode 100644 Drivers/SX126x/Source/Sx1262.h create mode 100644 Tactility/Include/Tactility/hal/radio/RadioDevice.h create mode 100644 Tactility/Source/hal/radio/RadioDevice.cpp diff --git a/App/idf_component.yml b/App/idf_component.yml index 1d33b876..87afb64e 100644 --- a/App/idf_component.yml +++ b/App/idf_component.yml @@ -20,4 +20,9 @@ dependencies: version: "1.7.6~1" rules: - if: "target == esp32s3" + jgromes/radiolib: + version: "7.2.1" + rules: + - if: "target in [esp32s3, esp32p4]" + idf: '5.5' diff --git a/Drivers/RadioLibCompat/CMakeLists.txt b/Drivers/RadioLibCompat/CMakeLists.txt new file mode 100644 index 00000000..05a87a9d --- /dev/null +++ b/Drivers/RadioLibCompat/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility radiolib +) diff --git a/Drivers/RadioLibCompat/README.md b/Drivers/RadioLibCompat/README.md new file mode 100644 index 00000000..0266d062 --- /dev/null +++ b/Drivers/RadioLibCompat/README.md @@ -0,0 +1,3 @@ +# RadioLibCompat + +A set of helper classes to implement `RadioLib` drivers in Tactility. diff --git a/Drivers/RadioLibCompat/Source/IsrTrampoline.cpp b/Drivers/RadioLibCompat/Source/IsrTrampoline.cpp new file mode 100644 index 00000000..2a885890 --- /dev/null +++ b/Drivers/RadioLibCompat/Source/IsrTrampoline.cpp @@ -0,0 +1,38 @@ +#include "IsrTrampoline.h" + +std::array IsrTrampolines::functions{}; +void (*const IsrTrampolines::trampolines[IsrTrampolines::MAX_SLOTS])() = { + &IsrTrampolines::trampoline0, + &IsrTrampolines::trampoline1, + &IsrTrampolines::trampoline2, + &IsrTrampolines::trampoline3, + &IsrTrampolines::trampoline4, + &IsrTrampolines::trampoline5, + &IsrTrampolines::trampoline6, + &IsrTrampolines::trampoline7, + &IsrTrampolines::trampoline8, + &IsrTrampolines::trampoline9, + &IsrTrampolines::trampoline10, + &IsrTrampolines::trampoline11, + &IsrTrampolines::trampoline12, + &IsrTrampolines::trampoline13, + &IsrTrampolines::trampoline14, + &IsrTrampolines::trampoline15, + &IsrTrampolines::trampoline16, + &IsrTrampolines::trampoline17, + &IsrTrampolines::trampoline18, + &IsrTrampolines::trampoline19, + &IsrTrampolines::trampoline20, + &IsrTrampolines::trampoline21, + &IsrTrampolines::trampoline22, + &IsrTrampolines::trampoline23, + &IsrTrampolines::trampoline24, + &IsrTrampolines::trampoline25, + &IsrTrampolines::trampoline26, + &IsrTrampolines::trampoline27, + &IsrTrampolines::trampoline28, + &IsrTrampolines::trampoline29, + &IsrTrampolines::trampoline30, + &IsrTrampolines::trampoline31 + +}; diff --git a/Drivers/RadioLibCompat/Source/IsrTrampoline.h b/Drivers/RadioLibCompat/Source/IsrTrampoline.h new file mode 100644 index 00000000..9c766bc1 --- /dev/null +++ b/Drivers/RadioLibCompat/Source/IsrTrampoline.h @@ -0,0 +1,118 @@ +#pragma once + +#include +#include + +#include "esp_attr.h" + +#define ISR_TRAMPOLINE_FUNC(slot) \ +static void IRAM_ATTR trampoline##slot() { static const int DRAM_ATTR i = slot; auto &f = IsrTrampolines::functions[i]; if (f) f(); } + +class IsrTrampolines { + +public: + + typedef std::size_t SlotNumber; + static constexpr SlotNumber MAX_SLOTS = 32; + using TrampolinePointer = void (*)(); + using VoidFunction = std::function; + static std::array functions; + static void (*const trampolines[MAX_SLOTS])(); + + static bool registerSlot(VoidFunction f, SlotNumber& slotRegistered) { + for (SlotNumber slot = 0; slot < MAX_SLOTS; ++slot) { + if (!functions[slot]) { + slotRegistered = slot; + functions[slot] = f; + return true; + } + } + + return false; + } + + static bool unregisterSlot(const SlotNumber slotRegistered) { + if (!functions[slotRegistered]) { + return false; + } + + functions[slotRegistered] = nullptr; + return true; + } + + static TrampolinePointer getForSlot(const SlotNumber slotRegistered) { + if (slotRegistered < MAX_SLOTS) { + return trampolines[slotRegistered]; + } + return nullptr; + } + + // Unfortunately, these cannot be templated because the linker cannot correctly handle + // its integer constant indexing the function table (dangerous relocation) + ISR_TRAMPOLINE_FUNC(0); + ISR_TRAMPOLINE_FUNC(1); + ISR_TRAMPOLINE_FUNC(2); + ISR_TRAMPOLINE_FUNC(3); + ISR_TRAMPOLINE_FUNC(4); + ISR_TRAMPOLINE_FUNC(5); + ISR_TRAMPOLINE_FUNC(6); + ISR_TRAMPOLINE_FUNC(7); + ISR_TRAMPOLINE_FUNC(8); + ISR_TRAMPOLINE_FUNC(9); + ISR_TRAMPOLINE_FUNC(10); + ISR_TRAMPOLINE_FUNC(11); + ISR_TRAMPOLINE_FUNC(12); + ISR_TRAMPOLINE_FUNC(13); + ISR_TRAMPOLINE_FUNC(14); + ISR_TRAMPOLINE_FUNC(15); + ISR_TRAMPOLINE_FUNC(16); + ISR_TRAMPOLINE_FUNC(17); + ISR_TRAMPOLINE_FUNC(18); + ISR_TRAMPOLINE_FUNC(19); + ISR_TRAMPOLINE_FUNC(20); + ISR_TRAMPOLINE_FUNC(21); + ISR_TRAMPOLINE_FUNC(22); + ISR_TRAMPOLINE_FUNC(23); + ISR_TRAMPOLINE_FUNC(24); + ISR_TRAMPOLINE_FUNC(25); + ISR_TRAMPOLINE_FUNC(26); + ISR_TRAMPOLINE_FUNC(27); + ISR_TRAMPOLINE_FUNC(28); + ISR_TRAMPOLINE_FUNC(29); + ISR_TRAMPOLINE_FUNC(30); + ISR_TRAMPOLINE_FUNC(31); + + /*static void IRAM_ATTR trampoline0() ISR_TRAMPOLINE_FUNC(0) + static void IRAM_ATTR trampoline1() { auto &f = IsrTrampolines::functions[1]; if (f) f(); } + static void IRAM_ATTR trampoline2() { auto &f = IsrTrampolines::functions[2]; if (f) f(); } + static void IRAM_ATTR trampoline3() { auto &f = IsrTrampolines::functions[3]; if (f) f(); } + static void IRAM_ATTR trampoline4() { auto &f = IsrTrampolines::functions[4]; if (f) f(); } + static void IRAM_ATTR trampoline5() { auto &f = IsrTrampolines::functions[5]; if (f) f(); } + static void IRAM_ATTR trampoline6() { auto &f = IsrTrampolines::functions[6]; if (f) f(); } + static void IRAM_ATTR trampoline7() { auto &f = IsrTrampolines::functions[7]; if (f) f(); } + static void IRAM_ATTR trampoline8() { auto &f = IsrTrampolines::functions[8]; if (f) f(); } + static void IRAM_ATTR trampoline9() { auto &f = IsrTrampolines::functions[9]; if (f) f(); } + static void IRAM_ATTR trampoline10() { auto &f = IsrTrampolines::functions[10]; if (f) f(); } + static void IRAM_ATTR trampoline11() { auto &f = IsrTrampolines::functions[11]; if (f) f(); } + static void IRAM_ATTR trampoline12() { auto &f = IsrTrampolines::functions[12]; if (f) f(); } + static void IRAM_ATTR trampoline13() { auto &f = IsrTrampolines::functions[13]; if (f) f(); } + static void IRAM_ATTR trampoline14() { auto &f = IsrTrampolines::functions[14]; if (f) f(); } + static void IRAM_ATTR trampoline15() { auto &f = IsrTrampolines::functions[15]; if (f) f(); } + static void IRAM_ATTR trampoline16() { auto &f = IsrTrampolines::functions[16]; if (f) f(); } + static void IRAM_ATTR trampoline17() { auto &f = IsrTrampolines::functions[17]; if (f) f(); } + static void IRAM_ATTR trampoline18() { auto &f = IsrTrampolines::functions[18]; if (f) f(); } + static void IRAM_ATTR trampoline19() { auto &f = IsrTrampolines::functions[19]; if (f) f(); } + static void IRAM_ATTR trampoline20() { auto &f = IsrTrampolines::functions[20]; if (f) f(); } + static void IRAM_ATTR trampoline21() { auto &f = IsrTrampolines::functions[21]; if (f) f(); } + static void IRAM_ATTR trampoline22() { auto &f = IsrTrampolines::functions[22]; if (f) f(); } + static void IRAM_ATTR trampoline23() { auto &f = IsrTrampolines::functions[23]; if (f) f(); } + static void IRAM_ATTR trampoline24() { auto &f = IsrTrampolines::functions[24]; if (f) f(); } + static void IRAM_ATTR trampoline25() { auto &f = IsrTrampolines::functions[25]; if (f) f(); } + static void IRAM_ATTR trampoline26() { auto &f = IsrTrampolines::functions[26]; if (f) f(); } + static void IRAM_ATTR trampoline27() { auto &f = IsrTrampolines::functions[27]; if (f) f(); } + static void IRAM_ATTR trampoline28() { auto &f = IsrTrampolines::functions[28]; if (f) f(); } + static void IRAM_ATTR trampoline29() { auto &f = IsrTrampolines::functions[29]; if (f) f(); } + static void IRAM_ATTR trampoline30() { auto &f = IsrTrampolines::functions[30]; if (f) f(); } + static void IRAM_ATTR trampoline31() { auto &f = IsrTrampolines::functions[31]; if (f) f(); } + */ +}; diff --git a/Drivers/RadioLibCompat/Source/RadiolibTactilityHal.cpp b/Drivers/RadioLibCompat/Source/RadiolibTactilityHal.cpp new file mode 100644 index 00000000..58996b15 --- /dev/null +++ b/Drivers/RadioLibCompat/Source/RadiolibTactilityHal.cpp @@ -0,0 +1,155 @@ +#include "RadiolibTactilityHal.h" + +#include "hal/gpio_hal.h" +#include "esp_timer.h" + +constexpr const char* TAG = "RadiolibTactilityHal"; + +void RadiolibTactilityHal::init() { + // we only need to init the SPI here + spiBegin(); +} + +void RadiolibTactilityHal::term() { + // we only need to stop the SPI here + spiEnd(); +} + +void RadiolibTactilityHal::pinMode(uint32_t pin, uint32_t mode) { + 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 RadiolibTactilityHal::digitalWrite(uint32_t pin, uint32_t value) { + if(pin == RADIOLIB_NC) { + return; + } + + gpio_set_level((gpio_num_t)pin, value); +} + +uint32_t RadiolibTactilityHal::digitalRead(uint32_t pin) { + if(pin == RADIOLIB_NC) { + return(0); + } + + return(gpio_get_level((gpio_num_t)pin)); +} + +void RadiolibTactilityHal::attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) { + 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 RadiolibTactilityHal::detachInterrupt(uint32_t interruptNum) { + 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 RadiolibTactilityHal::delay(unsigned long ms) { + vTaskDelay(ms / portTICK_PERIOD_MS); +} + +void RadiolibTactilityHal::delayMicroseconds(unsigned long us) { + 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); + } + while((uint64_t)esp_timer_get_time() < e); + } +} + +unsigned long RadiolibTactilityHal::millis() { + return((unsigned long)(esp_timer_get_time() / 1000ULL)); +} + +unsigned long RadiolibTactilityHal::micros() { + return((unsigned long)(esp_timer_get_time())); +} + +long RadiolibTactilityHal::pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) { + if(pin == RADIOLIB_NC) { + return(0); + } + + this->pinMode(pin, GPIO_MODE_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 RadiolibTactilityHal::spiBegin() { + spi_device_interface_config_t devcfg = {}; + devcfg.clock_speed_hz = 1 * 1000 * 1000; // 1MHz + devcfg.mode = 0; + devcfg.spics_io_num = csPin; + devcfg.queue_size = 1; + esp_err_t ret = spi_bus_add_device(spiHostDevice, &devcfg, &spiDeviceHandle); + if (ret != ESP_OK) { + TT_LOG_E(TAG, "Failed to add SPI device: %s", esp_err_to_name(ret)); + } +} + +void RadiolibTactilityHal::spiBeginTransaction() { + // not needed - in ESP32 Arduino core, this function + // repeats clock div, mode and bit order configuration +} + +void RadiolibTactilityHal::spiTransfer(uint8_t* out, size_t len, uint8_t* in) { + spi_transaction_t t; + + auto lock = getLock()->asScopedLock(); + bool locked = lock.lock(portMAX_DELAY); + if (!locked) { + TT_LOG_E(TAG, "Failed to aquire SPI lock"); + } + + memset(&t, 0, sizeof(t)); // Zero out the transaction + t.length = len * 8; // Length is in bits + t.tx_buffer = out; // The data to send + t.rx_buffer = in; // The data to receive + spi_device_polling_transmit(spiDeviceHandle, &t); +} + +void RadiolibTactilityHal::spiEndTransaction() { + // nothing needs to be done here +} + +void RadiolibTactilityHal::spiEnd() { + +} diff --git a/Drivers/RadioLibCompat/Source/RadiolibTactilityHal.h b/Drivers/RadioLibCompat/Source/RadiolibTactilityHal.h new file mode 100644 index 00000000..e26ee1bb --- /dev/null +++ b/Drivers/RadioLibCompat/Source/RadiolibTactilityHal.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +#include +#include + +#include + +#include +#include + +#include + +class RadiolibTactilityHal : public RadioLibHal { +private: + spi_host_device_t spiHostDevice; + gpio_num_t csPin; + spi_device_handle_t spiDeviceHandle; + std::shared_ptr lock; + +public: + explicit RadiolibTactilityHal(spi_host_device_t spiHostDevice, gpio_num_t csPin) + : RadioLibHal( + GPIO_MODE_INPUT, + GPIO_MODE_OUTPUT, + 0, // LOW + 1, // HIGH + GPIO_INTR_POSEDGE, + GPIO_INTR_NEGEDGE) + , spiHostDevice(spiHostDevice) + , csPin(csPin) + , lock(tt::hal::spi::getLock(spiHostDevice)) {} + + void init() override; + void term() override; + + void pinMode(uint32_t pin, uint32_t mode) override; + void digitalWrite(uint32_t pin, uint32_t value) override; + uint32_t digitalRead(uint32_t pin) override; + void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override; + void detachInterrupt(uint32_t interruptNum) override; + + void delay(unsigned long ms) override; + void delayMicroseconds(unsigned long us) override; + unsigned long millis() override; + unsigned long micros() override; + long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override; + + void spiBegin() override; + void spiBeginTransaction() override; + void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override; + void spiEndTransaction() override; + void spiEnd(); + + std::shared_ptr getLock() const { return lock; } +}; diff --git a/Drivers/SX126x/CMakeLists.txt b/Drivers/SX126x/CMakeLists.txt new file mode 100644 index 00000000..4b3bff0d --- /dev/null +++ b/Drivers/SX126x/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility driver RadioLibCompat radiolib +) diff --git a/Drivers/SX126x/README.md b/Drivers/SX126x/README.md new file mode 100644 index 00000000..7bdf06bf --- /dev/null +++ b/Drivers/SX126x/README.md @@ -0,0 +1,7 @@ +# SX126x + +Radio with LoRa/(G)FSK capabilities. + +## SX1262 + +- [Product Information](https://www.semtech.com/products/wireless-rf/lora-connect/sx1262) diff --git a/Drivers/SX126x/Source/Sx1262.cpp b/Drivers/SX126x/Source/Sx1262.cpp new file mode 100644 index 00000000..470d9f9c --- /dev/null +++ b/Drivers/SX126x/Source/Sx1262.cpp @@ -0,0 +1,125 @@ +#include "Sx1262.h" + +#include + +constexpr const char* TAG = "LoraDevice"; + +bool Sx1262::configure(const Parameter parameter, const float value) { + using enum Parameter; + + switch (parameter) { + case Power: + power = value; + return true; + case Frequency: + frequency = value; + return true; + case Bandwidth: + bandwidth = value; + return true; + case SpreadFactor: + spreadFactor = value; + return true; + case CodingRate: + codingRate = value; + return true; + case SyncWord: + syncWord = value; + return true; + case PreambleLength: + preambleLength = value; + return true; + case DataRate: + bitRate = value; + return true; + case FrequencyDeviation: + frequencyDeviation = value; + return true; + default: + break; + } + + return false; +} + +void Sx1262::onReceive() { + getEventFlag().set(RADIO_RECEIVED_BIT); +} + +int32_t Sx1262::threadMain(const Modulation modulation) { + + uint16_t rc = RADIOLIB_ERR_NONE; + + if (modulation == Modulation::LoRa) { + rc = radio.begin( + frequency, + bandwidth, + spreadFactor, + codingRate, + syncWord, + power, + preambleLength, + configuration.tcxoVoltage, + configuration.useRegulatorLdo + ); + } else if (modulation == Modulation::Fsk) { + rc = radio.beginFSK( + frequency, + bitRate, + frequencyDeviation, + bandwidth, + power, + preambleLength, + configuration.tcxoVoltage, + configuration.useRegulatorLdo + ); + } else { + TT_LOG_E(TAG, "SX1262 not capable of modulation \"%s\"", toString(modulation)); + setState(State::Error); + return -1; + } + + if (rc != RADIOLIB_ERR_NONE) { + TT_LOG_E(TAG, "Radiolib init failed with code %hi", rc); + setState(State::Error); + return -1; + } + + setState(State::On); + + TT_LOG_I(TAG, "SX1262 device ready to receive!"); + + while (!isThreadInterrupted()) { + radio.startReceive(); + getEventFlag().wait(RADIO_TERMINATE_BIT, RADIO_RECEIVED_BIT); + + // Thread might've been interrupted in the meanwhile + if (isThreadInterrupted()) { + break; + } + + uint16_t rxSize = radio.getPacketLength(true); + uint8_t *dataBuffer = new uint8_t[rxSize]; + uint16_t rc = radio.readData(dataBuffer, rxSize); + if (rc != RADIOLIB_ERR_NONE) { + TT_LOG_E(TAG, "Error receiving data, RadioLib returned %hi", rc); + } else { + float rssi = radio.getRSSI(); + float snr = radio.getSNR(); + TT_LOG_I(TAG, "LoRa RX size=%d RSSI=%f SNR=%f", rxSize, rssi, snr); + std::string message((char*)dataBuffer, rxSize); + TT_LOG_I(TAG, "msg=%s", message.c_str()); + auto rxPacket = tt::hal::radio::RxPacket { + .data = dataBuffer, + .size = rxSize, + .rssi = rssi, + .snr = snr + }; + + publishRx(rxPacket); + } + delete[] dataBuffer; + } + + return 0; +} diff --git a/Drivers/SX126x/Source/Sx1262.h b/Drivers/SX126x/Source/Sx1262.h new file mode 100644 index 00000000..fc2e30d1 --- /dev/null +++ b/Drivers/SX126x/Source/Sx1262.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include + +#include +#include "RadiolibTactilityHal.h" +#include "IsrTrampoline.h" + +#include + +class Sx1262 final : public tt::hal::radio::RadioDevice { + +public: + struct Configuration { + spi_host_device_t spiHostDevice; + gpio_num_t csPin; + gpio_num_t resetPin; + gpio_num_t busyPin; + gpio_num_t irqPin; + float tcxoVoltage; + bool useRegulatorLdo; + }; + +private: + + std::string name; + const Configuration configuration; + std::shared_ptr lock; + RadiolibTactilityHal hal; + Module radioModule; + SX1262 radio; + IsrTrampolines::SlotNumber isrTrampolineSlot; + int8_t power = 0; + float frequency = 0.0; + float bandwidth = 0.0; + uint8_t spreadFactor = 0.0; + uint8_t codingRate = 0; + uint8_t syncWord = 0; + uint16_t preambleLength = 0; + float bitRate = 0.0; + float frequencyDeviation = 0.0; + + int32_t threadMain(); + + void onReceive(); + +public: + + explicit Sx1262(const std::string& name, const Configuration& configuration) + : RadioDevice(name, 1024) + , name(name) + , configuration(configuration) + , hal(configuration.spiHostDevice, configuration.csPin) + , radioModule(&hal, configuration.csPin, configuration.irqPin, configuration.resetPin, configuration.busyPin) + , radio(&radioModule) { + IsrTrampolines::registerSlot([this]() { + this->onReceive(); + }, isrTrampolineSlot); + radio.setDio1Action(IsrTrampolines::getForSlot(isrTrampolineSlot)); + } + + ~Sx1262() override { + IsrTrampolines::unregisterSlot(isrTrampolineSlot); + } + + std::string getName() const override { return name; } + + std::string getDescription() const override { return "SX1262 LoRa and FSK capable radio"; } + + bool configure(const Parameter parameter, const float value) override; + + bool isCapableOf(const Modulation modulation) { + return (modulation == Modulation::Fsk) || (modulation == Modulation::LoRa); + } + +protected: + + int32_t threadMain(const Modulation modulation) override; +}; diff --git a/Tactility/Include/Tactility/hal/Device.h b/Tactility/Include/Tactility/hal/Device.h index e323c965..bd7f72ff 100644 --- a/Tactility/Include/Tactility/hal/Device.h +++ b/Tactility/Include/Tactility/hal/Device.h @@ -21,7 +21,8 @@ public: Keyboard, Encoder, Power, - Gps + Gps, + Radio }; typedef uint32_t Id; diff --git a/Tactility/Include/Tactility/hal/radio/RadioDevice.h b/Tactility/Include/Tactility/hal/radio/RadioDevice.h new file mode 100644 index 00000000..96f8ce0e --- /dev/null +++ b/Tactility/Include/Tactility/hal/radio/RadioDevice.h @@ -0,0 +1,118 @@ +#pragma once + +#include "../Device.h" +#include +#include +#include +#include +#include + +namespace tt::hal::radio { + +struct RxPacket { + uint8_t const *data; + size_t const size; + float rssi; + float snr; +}; + +class RadioDevice : public Device { + +public: + + enum class Modulation { + Fsk, + LoRa + }; + + enum class Parameter { + Power, + Frequency, + Bandwidth, + SpreadFactor, + CodingRate, + SyncWord, + PreambleLength, + FrequencyDeviation, + DataRate, + AddressWidth + }; + + typedef int RxSubscriptionId; + + enum class State { + PendingOn, + On, + Error, + PendingOff, + Off + }; + +private: + + struct RxSubscription { + RxSubscriptionId id; + std::shared_ptr> onData; + }; + + std::string threadName; + size_t threadSize; + State state; + EventFlag events; + Mutex mutex = Mutex(Mutex::Type::Recursive); + std::unique_ptr _Nullable thread; + bool threadInterrupted = false; + std::vector rxSubscriptions; + RxSubscriptionId lastRxSubscriptionId = 0; + +protected: + + static constexpr auto RADIO_TERMINATE_BIT = BIT0; + static constexpr auto RADIO_RECEIVED_BIT = BIT1; + + virtual int32_t threadMain(const Modulation modulation) = 0; + Mutex &getMutex() { return mutex; } + EventFlag &getEventFlag() { return events; } + bool isThreadInterrupted() const; + void setState(State newState); + + void publishRx(const RxPacket& packet); + +public: + explicit RadioDevice(const std::string& threadName, const size_t threadSize) + : threadName(threadName) + , threadSize(threadSize) + , state(State::Off) {} + + ~RadioDevice() override = default; + + Type getType() const override { return Type::Radio; } + + virtual bool configure(const Parameter parameter, const float value) = 0; + virtual bool isCapableOf(const Modulation modulation) = 0; + + bool start(const Modulation modulation); + bool stop(); + + RxSubscriptionId subscribeRx(const std::function& onData) { + auto lock = mutex.asScopedLock(); + lock.lock(); + rxSubscriptions.push_back({ + .id = ++lastRxSubscriptionId, + .onData = std::make_shared>(onData) + }); + return lastRxSubscriptionId; + } + + void unsubscribeRx(RxSubscriptionId subscriptionId) { + auto lock = mutex.asScopedLock(); + lock.lock(); + std::erase_if(rxSubscriptions, [subscriptionId](auto& subscription) { return subscription.id == subscriptionId; }); + } + + State getState() const; +}; + +const char* toString(RadioDevice::Modulation modulation); +const char* toString(RadioDevice::Parameter parameter); +} diff --git a/Tactility/Source/hal/radio/RadioDevice.cpp b/Tactility/Source/hal/radio/RadioDevice.cpp new file mode 100644 index 00000000..62f06245 --- /dev/null +++ b/Tactility/Source/hal/radio/RadioDevice.cpp @@ -0,0 +1,136 @@ +#include "Tactility/hal/radio/RadioDevice.h" +#include + +namespace tt::hal::radio { + +constexpr const char* TAG = "RadioDevice"; + +bool RadioDevice::start(const Modulation modulation) { + auto lock = mutex.asScopedLock(); + + if (!isCapableOf(modulation)) { + TT_LOG_E(TAG, "Can't start device \"%s\", not capable of modulation \"%s\"", getName().c_str(), toString(modulation)); + return false; + } + + lock.lock(); + + if (thread != nullptr && thread->getState() != Thread::State::Stopped) { + TT_LOG_W(TAG, "Already started"); + return true; + } + + threadInterrupted = false; + + TT_LOG_I(TAG, "Starting thread"); + setState(State::PendingOn); + + thread = std::make_unique( + threadName, + threadSize, + [this, modulation]() { + return this->threadMain(modulation); + } + ); + thread->setPriority(tt::Thread::Priority::High); + thread->start(); + + TT_LOG_I(TAG, "Starting finished"); + return true; +} + +bool RadioDevice::stop() { + auto lock = mutex.asScopedLock(); + lock.lock(); + + setState(State::PendingOff); + + if (thread != nullptr) { + threadInterrupted = true; + getEventFlag().set(RADIO_TERMINATE_BIT); + + // Detach thread, it will auto-delete when leaving the current scope + auto old_thread = std::move(thread); + + if (old_thread->getState() != Thread::State::Stopped) { + // Unlock so thread can lock + lock.unlock(); + // Wait for thread to finish + old_thread->join(); + // Re-lock to continue logic below + lock.lock(); + } + } + + setState(State::Off); + + return true; +} + +bool RadioDevice::isThreadInterrupted() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return threadInterrupted; +} + +RadioDevice::State RadioDevice::getState() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return state; // Make copy because of thread safety +} + +void RadioDevice::setState(State newState) { + auto lock = mutex.asScopedLock(); + lock.lock(); + state = newState; +} + +void RadioDevice::publishRx(const RxPacket& packet) { + mutex.lock(); + for (auto& subscription : rxSubscriptions) { + (*subscription.onData)(getId(), packet); + } + mutex.unlock(); +} + +const char* toString(RadioDevice::Modulation modulation) { + using enum RadioDevice::Modulation; + switch (modulation) { + case Fsk: + return "FSK"; + case LoRa: + return "LoRa"; + default: + return "Unkown"; + } +} + +const char* toString(RadioDevice::Parameter parameter) { + using enum RadioDevice::Parameter; + switch (parameter) { + case Power: + return TT_STRINGIFY(Power); + case Frequency: + return TT_STRINGIFY(Frequency); + case Bandwidth: + return TT_STRINGIFY(Bandwidth); + case SpreadFactor: + return TT_STRINGIFY(SpreadFactor); + case CodingRate: + return TT_STRINGIFY(CodingRate); + case SyncWord: + return TT_STRINGIFY(SyncWord); + case PreambleLength: + return TT_STRINGIFY(PreambleLength); + case FrequencyDeviation: + return TT_STRINGIFY(FrequencyDeviation); + case DataRate: + return TT_STRINGIFY(DataRate); + case AddressWidth: + return TT_STRINGIFY(AddressWidth); + default: + return "Unknown"; + } +} + +} // namespace tt::hal::radio