From 982f3e11c79178bfd9a7e172b502b062d42811e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20H=C3=B6glinger?= Date: Sat, 20 Sep 2025 19:14:21 +0200 Subject: [PATCH] Radio: Refactor parameters and ParameterSet --- .../Source/RadiolibThreadedDevice.cpp | 4 +- Drivers/SX126x/Source/Sx1262.cpp | 107 ++++++++++++++---- Drivers/SX126x/Source/Sx1262.h | 17 ++- .../Tactility/hal/radio/ParameterSet.h | 22 +++- .../Include/Tactility/hal/radio/RadioDevice.h | 10 +- .../app/chirpchatter/ChirpChatterApp.cpp | 66 ++++++++--- Tactility/Source/hal/radio/RadioDevice.cpp | 4 + 7 files changed, 181 insertions(+), 49 deletions(-) diff --git a/Drivers/RadioLibCompat/Source/RadiolibThreadedDevice.cpp b/Drivers/RadioLibCompat/Source/RadiolibThreadedDevice.cpp index a86dbebe..ac0e00e6 100644 --- a/Drivers/RadioLibCompat/Source/RadiolibThreadedDevice.cpp +++ b/Drivers/RadioLibCompat/Source/RadiolibThreadedDevice.cpp @@ -6,14 +6,14 @@ constexpr const char* TAG = "RadiolibThreadedDevice"; bool RadiolibThreadedDevice::start(const Modulation modulation) { auto lock = getMutex().asScopedLock(); - if (!isCapableOf(modulation)) { + if (!canTransmit(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() != tt::Thread::State::Stopped) { + if ((thread != nullptr) && (thread->getState() != tt::Thread::State::Stopped)) { TT_LOG_W(TAG, "Already started"); return true; } diff --git a/Drivers/SX126x/Source/Sx1262.cpp b/Drivers/SX126x/Source/Sx1262.cpp index a7033a84..3720f272 100644 --- a/Drivers/SX126x/Source/Sx1262.cpp +++ b/Drivers/SX126x/Source/Sx1262.cpp @@ -9,37 +9,30 @@ void IRAM_ATTR dio1handler(void* context) { ((Sx1262*)context)->dio1Event(); } -bool Sx1262::configure(const Parameter parameter, const float value) { +bool Sx1262::setParameter(const Parameter parameter, const float value) { using enum Parameter; switch (parameter) { case Power: power = value; - TT_LOG_I(TAG, "Configure %s=%d", toString(parameter), power); return true; case Frequency: frequency = value; - TT_LOG_I(TAG, "Configure %s=%f", toString(parameter), frequency); return true; case Bandwidth: bandwidth = value; - TT_LOG_I(TAG, "Configure %s=%f", toString(parameter), bandwidth); return true; case SpreadFactor: spreadFactor = value; - TT_LOG_I(TAG, "Configure %s=%d", toString(parameter), spreadFactor); return true; case CodingRate: codingRate = value; - TT_LOG_I(TAG, "Configure %s=%d", toString(parameter), codingRate); return true; case SyncWord: syncWord = value; - TT_LOG_I(TAG, "Configure %s=%X", toString(parameter), syncWord); return true; case PreambleLength: preambleLength = value; - TT_LOG_I(TAG, "Configure %s=%d", toString(parameter), preambleLength); return true; case DataRate: bitRate = value; @@ -47,17 +40,62 @@ bool Sx1262::configure(const Parameter parameter, const float value) { case FrequencyDeviation: frequencyDeviation = value; return true; + case NarrowGrid: + narrowGrid = value; + return true; default: break; } + TT_LOG_W(TAG, "Tried to set unsupported parameter \"%s\" to %f", toString(parameter), value); + + return false; +} + +bool Sx1262::getParameter(const Parameter parameter, float &value) const { + using enum Parameter; + + switch (parameter) { + case Power: + value = power; + return true; + case Frequency: + value = frequency; + return true; + case Bandwidth: + value = bandwidth; + return true; + case SpreadFactor: + value = spreadFactor; + return true; + case CodingRate: + value = codingRate; + return true; + case SyncWord: + value = syncWord; + return true; + case PreambleLength: + value = preambleLength; + return true; + case DataRate: + value = bitRate; + return true; + case FrequencyDeviation: + value = frequencyDeviation; + return true; + case NarrowGrid: + value = narrowGrid; + return true; + default: + break; + } + + TT_LOG_W(TAG, "Tried to get unsupported parameter \"%s\"", toString(parameter)); + return false; } void Sx1262::registerDio1Isr() { - hal.pinMode(GPIO_NUM_9, GPIO_MODE_OUTPUT); - gpio_set_level(GPIO_NUM_9, 0); - gpio_hal_context_t gpiohal; gpiohal.dev = GPIO_LL_GET_HW(GPIO_PORT_0); @@ -123,6 +161,14 @@ int Sx1262::doBegin(const Modulation modulation) { configuration.tcxoVoltage, configuration.useRegulatorLdo ); + } else if (modulation == Modulation::LrFhss) { + rc = radio.beginLRFHSS( + bandwidth, + codingRate, + narrowGrid, + configuration.tcxoVoltage, + configuration.useRegulatorLdo + ); } else { TT_LOG_E(TAG, "SX1262 not capable of modulation \"%s\"", toString(modulation)); setState(State::Error); @@ -130,11 +176,12 @@ int Sx1262::doBegin(const Modulation modulation) { } if (rc != RADIOLIB_ERR_NONE) { - TT_LOG_E(TAG, "Radiolib init failed with code %hi", rc); + TT_LOG_E(TAG, "Radiolib initialization failed with code %hi", rc); setState(State::Error); return -1; } + currentModem = modulation; registerDio1Isr(); return 0; } @@ -145,19 +192,24 @@ void Sx1262::doEnd() { void Sx1262::doTransmit() { currentTx = popNextQueuedTx(); - radio.standby(); - uint16_t rc = radio.startTransmit(currentTx.packet.data.data(), currentTx.packet.data.size()); + uint16_t rc = RADIOLIB_ERR_NONE; + rc = radio.standby(); + if (rc != RADIOLIB_ERR_NONE) { + TT_LOG_W(TAG, "RadioLib returned %hi on standby", rc); + } + + rc = radio.startTransmit(currentTx.packet.data.data(), currentTx.packet.data.size()); if (rc == RADIOLIB_ERR_NONE) { currentTx.callback(currentTx.id, TransmissionState::PendingTransmit); - TT_LOG_I(TAG, "WAIT TX FLAG"); auto txEventFlags = events.wait(SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT, tt::EventFlag::WaitAny, pdMS_TO_TICKS(2000)); - TT_LOG_W(TAG, "Event, flag=%X", txEventFlags); // Thread might've been interrupted in the meanwhile if (isThreadInterrupted()) { return; } + + // If the DIO1 bit is unset, this means the wait timed out if (txEventFlags & SX1262_DIO1_EVENT_BIT) { currentTx.callback(currentTx.id, TransmissionState::Transmitted); } else { @@ -171,24 +223,31 @@ void Sx1262::doTransmit() { } void Sx1262::doListen() { - radio.startReceive(); - TT_LOG_I(TAG, "WAIT FLAG"); - auto eventFlags = events.wait(SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT | SX1262_QUEUED_TX_BIT); - TT_LOG_W(TAG, "Event, flag=%X", eventFlags); + if (currentModem != Modulation::LrFhss) { + radio.startReceive(); + events.wait(SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT | SX1262_QUEUED_TX_BIT); + } else { + // LR-FHSS modem only supports TX + events.wait(SX1262_INTERRUPT_BIT | SX1262_QUEUED_TX_BIT); + } } void Sx1262::doReceive() { + // LR-FHSS modem only supports TX + if (currentModem == Modulation::LrFhss) return; + uint16_t rxSize = radio.getPacketLength(true); std::vector data(rxSize); uint16_t rc = radio.readData(data.data(), rxSize); if (rc != RADIOLIB_ERR_NONE) { TT_LOG_E(TAG, "Error receiving data, RadioLib returned %hi", rc); } else if(rxSize == 0) { - //TT_LOG_W(TAG, "Received data length 0"); + // This can cause a flood of messages if there are ones emitted here, + // as a warning here doesn't bring that much to the table it is skipped. + // The body is kept empty intentionally.' } else { float rssi = radio.getRSSI(); float snr = radio.getSNR(); - TT_LOG_I(TAG, "LoRa RX size=%d RSSI=%f SNR=%f", rxSize, rssi, snr); auto rxPacket = tt::hal::radio::RxPacket { .data = data, .rssi = rssi, @@ -198,7 +257,7 @@ void Sx1262::doReceive() { publishRx(rxPacket); } - // A delay is needed before a new command - vTaskDelay(pdMS_TO_TICKS(100)); + // A delay before a new command improves reliability + vTaskDelay(pdMS_TO_TICKS(SX1262_COOLDOWN_MILLIS)); } diff --git a/Drivers/SX126x/Source/Sx1262.h b/Drivers/SX126x/Source/Sx1262.h index 78460aa8..7ea3b7ff 100644 --- a/Drivers/SX126x/Source/Sx1262.h +++ b/Drivers/SX126x/Source/Sx1262.h @@ -25,6 +25,7 @@ public: }; private: + static constexpr auto SX1262_COOLDOWN_MILLIS = 100; static constexpr auto SX1262_INTERRUPT_BIT = BIT0; static constexpr auto SX1262_DIO1_EVENT_BIT = BIT1; static constexpr auto SX1262_QUEUED_TX_BIT = BIT2; @@ -37,6 +38,7 @@ private: Module radioModule; SX1262 radio; TxItem currentTx; + Modulation currentModem; int8_t power = 0; float frequency = 0.0; @@ -47,6 +49,7 @@ private: uint16_t preambleLength = 0; float bitRate = 0.0; float frequencyDeviation = 0.0; + bool narrowGrid = false; void registerDio1Isr(); void unregisterDio1Isr(); @@ -70,17 +73,25 @@ public: , hal(configuration.spiHostDevice, configuration.spiFrequency, configuration.csPin, lock) , radioModule(&hal, configuration.csPin, configuration.irqPin, configuration.resetPin, configuration.busyPin) , radio(&radioModule) + , currentModem(Modulation::None) {} ~Sx1262() override = default; std::string getName() const override { return name; } - std::string getDescription() const override { return "SX1262 LoRa and FSK capable radio"; } + std::string getDescription() const override { return "Semtech SX1262 LoRa, FSK and LR-FHSS capable radio"; } - bool configure(const Parameter parameter, const float value) override; + bool setParameter(const Parameter parameter, const float value) override; + bool getParameter(const Parameter parameter, float &value) const override; - bool isCapableOf(const Modulation modulation) { + bool canTransmit(const Modulation modulation) override { + return (modulation == Modulation::Fsk) || + (modulation == Modulation::LoRa) || + (modulation == Modulation::LrFhss); + } + + bool canReceive(const Modulation modulation) override { return (modulation == Modulation::Fsk) || (modulation == Modulation::LoRa); } diff --git a/Tactility/Include/Tactility/hal/radio/ParameterSet.h b/Tactility/Include/Tactility/hal/radio/ParameterSet.h index fa28e34d..4016a595 100644 --- a/Tactility/Include/Tactility/hal/radio/ParameterSet.h +++ b/Tactility/Include/Tactility/hal/radio/ParameterSet.h @@ -8,13 +8,21 @@ namespace tt::hal::radio { class ParameterSet { private: - using Map = std::unordered_map >; + struct ParameterHash + { + std::size_t operator()(RadioDevice::Parameter t) const + { + return static_cast(t); + } + }; + + using Map = std::unordered_map; Map parameters; public: explicit ParameterSet() {} explicit ParameterSet(const ParameterSet& other) { parameters = other.parameters; } - ~ParameterSet() override = default; + ~ParameterSet() = default; float get(const RadioDevice::Parameter parameter) { return parameters[parameter]; } void set(const RadioDevice::Parameter parameter, const float value) { parameters[parameter] = value; } @@ -32,7 +40,15 @@ public: for (const auto& [parameter, value] : parameters) { // No break on error chosen to apply all parameters, // a bad one doesn't make the successive tries any more invalid - successful &= radio.configure(parameter, value); + successful &= radio.setParameter(parameter, value); + } + return successful; + } + + bool load(const RadioDevice &radio) { + bool successful = true; + for (const auto& [parameter, value] : parameters) { + successful &= radio.getParameter(parameter, parameters[parameter]); } return successful; } diff --git a/Tactility/Include/Tactility/hal/radio/RadioDevice.h b/Tactility/Include/Tactility/hal/radio/RadioDevice.h index b6531686..460b5393 100644 --- a/Tactility/Include/Tactility/hal/radio/RadioDevice.h +++ b/Tactility/Include/Tactility/hal/radio/RadioDevice.h @@ -25,6 +25,7 @@ class RadioDevice : public Device { public: enum class Modulation { + None, Fsk, LoRa, LrFhss @@ -40,7 +41,8 @@ public: PreambleLength, FrequencyDeviation, DataRate, - AddressWidth + AddressWidth, + NarrowGrid }; typedef int RxSubscriptionId; @@ -118,8 +120,10 @@ public: Type getType() const override { return Type::Radio; } - virtual bool configure(const Parameter parameter, const float value) = 0; - virtual bool isCapableOf(const Modulation modulation) = 0; + virtual bool setParameter(const Parameter parameter, const float value) = 0; + virtual bool getParameter(const Parameter parameter, float &value) const = 0; + virtual bool canTransmit(const Modulation modulation) = 0; + virtual bool canReceive(const Modulation modulation) = 0; virtual bool start(const Modulation modulation) = 0; virtual bool stop() = 0; diff --git a/Tactility/Source/app/chirpchatter/ChirpChatterApp.cpp b/Tactility/Source/app/chirpchatter/ChirpChatterApp.cpp index 5a064a66..f6a2c217 100644 --- a/Tactility/Source/app/chirpchatter/ChirpChatterApp.cpp +++ b/Tactility/Source/app/chirpchatter/ChirpChatterApp.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "Tactility/lvgl/LvglSync.h" @@ -90,6 +91,29 @@ std::string hexdump(const std::vector& data) { return oss.str(); } +bool isValidHexString(const std::string& hex) { + if (hex.empty() || (hex.size() % 2) != 0) return false; + for (char c : hex) + if (!std::isxdigit(static_cast(c))) + return false; + return true; +} + +bool parseHexString(const std::string& hex, std::vector& out) { + if (!isValidHexString(hex)) return false; + out.clear(); + out.reserve(hex.size() / 2); + for (size_t i = 0; i < hex.size(); i += 2) { + uint8_t high = std::isdigit(hex[i]) ? (hex[i] - '0') + : (std::tolower(hex[i]) - 'a' + 10); + uint8_t low = std::isdigit(hex[i+1]) ? (hex[i+1] - '0') + : (std::tolower(hex[i+1]) - 'a' + 10); + out.push_back(static_cast((high << 4) | low)); + } + return true; +} + + class LoraView { public: @@ -110,7 +134,8 @@ private: loraDevs.clear(); for (const auto& radio: radios) { - if (radio->isCapableOf(tt::hal::radio::RadioDevice::Modulation::LoRa)) { + if (radio->canTransmit(tt::hal::radio::RadioDevice::Modulation::LoRa) && + radio->canReceive(tt::hal::radio::RadioDevice::Modulation::LoRa)) { loraDevNames.push_back(radio->getName()); loraDevs.push_back(radio); } @@ -409,11 +434,11 @@ public: bool configured = true; buffer = lv_textarea_get_text(frequencyInput); if (!buffer.empty()) { - configured &= loraDevice->configure(Frequency, std::stof(buffer)); + configured &= loraDevice->setParameter(Frequency, std::stof(buffer)); } buffer = lv_textarea_get_text(bandwidthInput); if (!buffer.empty()) { - configured &= loraDevice->configure(Bandwidth, std::stof(buffer)); + configured &= loraDevice->setParameter(Bandwidth, std::stof(buffer)); } buffer = lv_textarea_get_text(syncwordInput); if (!buffer.empty()) { @@ -421,20 +446,20 @@ public: std::stringstream ss(buffer); ss >> std::hex >> syncWord; - configured &= loraDevice->configure(SyncWord, std::stoi(buffer, nullptr, 16)); + configured &= loraDevice->setParameter(SyncWord, std::stoi(buffer, nullptr, 16)); } value = lv_slider_get_value(deBitsInput); - configured &= loraDevice->configure(CodingRate, value); + configured &= loraDevice->setParameter(CodingRate, value); value = lv_slider_get_value(sfInput); - configured &= loraDevice->configure(SpreadFactor, value); + configured &= loraDevice->setParameter(SpreadFactor, value); value = lv_slider_get_value(preambleChirpsInput); - configured &= loraDevice->configure(PreambleLength, value); + configured &= loraDevice->setParameter(PreambleLength, value); buffer = lv_textarea_get_text(txPowInput); if (!buffer.empty()) { - configured &= loraDevice->configure(Power, std::stof(buffer)); + configured &= loraDevice->setParameter(Power, std::stof(buffer)); } } @@ -558,12 +583,11 @@ class ChirpChatterApp : public App { lv_obj_t* msg_label = lv_label_create(msg_container); - std::vector data(packet.data, packet.data + packet.size); std::string messageBuf = ""; - if (isPrintableData(data)) { - messageBuf = std::string((char*)packet.data, packet.size); + if (isPrintableData(packet.data)) { + messageBuf = std::string(packet.data.begin(), packet.data.end()); } else { - messageBuf = hexdump(data); + messageBuf = hexdump(packet.data); } lv_label_set_text(msg_label, messageBuf.c_str()); lv_obj_set_width(msg_label, lv_pct(100)); @@ -793,8 +817,22 @@ public: void sendMessage() { std::string message = lv_textarea_get_text(inputField); - std::vector data(message.begin(), message.end()); - loraDevice->transmit(data, [this](hal::radio::RadioDevice::TxId id, hal::radio::RadioDevice::TransmissionState state) { + std::vector data; + if (message == "!test1") { + parseHexString("ffffffff1147fec0c60940e5810800009671ad09dd2a0e7841ce266a3d759e967dc32a16bf4d5eecafde28d82b690f22eccf968a", data); + } else if (message == "!ack") { + parseHexString("ffffffff1147fec0f1dee9ab81080000922bf53364151a15", data); + } else if (message == "!gdn8") { + parseHexString("ffffffff1147fec025ffdd7a81080000768023f848619a3782ddadb5f686dc", data); + } else if (message == "!gm") { + parseHexString("ffffffff1147fec08a27ff4b810800003185053566e837fb0b88ade5fc84d9a13e", data); + } else if (message == "!ptest1") { + parseHexString("ffffffff1147fec0ef0e5bd48108000035f4be1f4bf703b3cce235423dd218a0c9ec745032a1f04be19c", data); + } else { + data = std::vector(message.begin(), message.end()); + } + + loraDevice->transmit(tt::hal::radio::TxPacket{.data = data}, [this](hal::radio::RadioDevice::TxId id, hal::radio::RadioDevice::TransmissionState state) { this->onTxStatus(id, state); }); } diff --git a/Tactility/Source/hal/radio/RadioDevice.cpp b/Tactility/Source/hal/radio/RadioDevice.cpp index cb8a30c7..19c43b72 100644 --- a/Tactility/Source/hal/radio/RadioDevice.cpp +++ b/Tactility/Source/hal/radio/RadioDevice.cpp @@ -28,6 +28,8 @@ void RadioDevice::publishRx(const RxPacket& packet) { const char* toString(RadioDevice::Modulation modulation) { using enum RadioDevice::Modulation; switch (modulation) { + case None: + return "none"; case Fsk: return "FSK"; case LoRa: @@ -62,6 +64,8 @@ const char* toString(RadioDevice::Parameter parameter) { return TT_STRINGIFY(DataRate); case AddressWidth: return TT_STRINGIFY(AddressWidth); + case NarrowGrid: + return TT_STRINGIFY(NarrowGrid); default: return "Unknown"; }