Radio: Refactor parameters and ParameterSet

This commit is contained in:
Dominic Höglinger 2025-09-20 19:14:21 +02:00
parent 6fabd3354c
commit 982f3e11c7
7 changed files with 181 additions and 49 deletions

View File

@ -6,14 +6,14 @@ constexpr const char* TAG = "RadiolibThreadedDevice";
bool RadiolibThreadedDevice::start(const Modulation modulation) { bool RadiolibThreadedDevice::start(const Modulation modulation) {
auto lock = getMutex().asScopedLock(); 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)); TT_LOG_E(TAG, "Can't start device \"%s\", not capable of modulation \"%s\"", getName().c_str(), toString(modulation));
return false; return false;
} }
lock.lock(); 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"); TT_LOG_W(TAG, "Already started");
return true; return true;
} }

View File

@ -9,37 +9,30 @@ void IRAM_ATTR dio1handler(void* context) {
((Sx1262*)context)->dio1Event(); ((Sx1262*)context)->dio1Event();
} }
bool Sx1262::configure(const Parameter parameter, const float value) { bool Sx1262::setParameter(const Parameter parameter, const float value) {
using enum Parameter; using enum Parameter;
switch (parameter) { switch (parameter) {
case Power: case Power:
power = value; power = value;
TT_LOG_I(TAG, "Configure %s=%d", toString(parameter), power);
return true; return true;
case Frequency: case Frequency:
frequency = value; frequency = value;
TT_LOG_I(TAG, "Configure %s=%f", toString(parameter), frequency);
return true; return true;
case Bandwidth: case Bandwidth:
bandwidth = value; bandwidth = value;
TT_LOG_I(TAG, "Configure %s=%f", toString(parameter), bandwidth);
return true; return true;
case SpreadFactor: case SpreadFactor:
spreadFactor = value; spreadFactor = value;
TT_LOG_I(TAG, "Configure %s=%d", toString(parameter), spreadFactor);
return true; return true;
case CodingRate: case CodingRate:
codingRate = value; codingRate = value;
TT_LOG_I(TAG, "Configure %s=%d", toString(parameter), codingRate);
return true; return true;
case SyncWord: case SyncWord:
syncWord = value; syncWord = value;
TT_LOG_I(TAG, "Configure %s=%X", toString(parameter), syncWord);
return true; return true;
case PreambleLength: case PreambleLength:
preambleLength = value; preambleLength = value;
TT_LOG_I(TAG, "Configure %s=%d", toString(parameter), preambleLength);
return true; return true;
case DataRate: case DataRate:
bitRate = value; bitRate = value;
@ -47,17 +40,62 @@ bool Sx1262::configure(const Parameter parameter, const float value) {
case FrequencyDeviation: case FrequencyDeviation:
frequencyDeviation = value; frequencyDeviation = value;
return true; return true;
case NarrowGrid:
narrowGrid = value;
return true;
default: default:
break; 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; return false;
} }
void Sx1262::registerDio1Isr() { void Sx1262::registerDio1Isr() {
hal.pinMode(GPIO_NUM_9, GPIO_MODE_OUTPUT);
gpio_set_level(GPIO_NUM_9, 0);
gpio_hal_context_t gpiohal; gpio_hal_context_t gpiohal;
gpiohal.dev = GPIO_LL_GET_HW(GPIO_PORT_0); gpiohal.dev = GPIO_LL_GET_HW(GPIO_PORT_0);
@ -123,6 +161,14 @@ int Sx1262::doBegin(const Modulation modulation) {
configuration.tcxoVoltage, configuration.tcxoVoltage,
configuration.useRegulatorLdo configuration.useRegulatorLdo
); );
} else if (modulation == Modulation::LrFhss) {
rc = radio.beginLRFHSS(
bandwidth,
codingRate,
narrowGrid,
configuration.tcxoVoltage,
configuration.useRegulatorLdo
);
} else { } else {
TT_LOG_E(TAG, "SX1262 not capable of modulation \"%s\"", toString(modulation)); TT_LOG_E(TAG, "SX1262 not capable of modulation \"%s\"", toString(modulation));
setState(State::Error); setState(State::Error);
@ -130,11 +176,12 @@ int Sx1262::doBegin(const Modulation modulation) {
} }
if (rc != RADIOLIB_ERR_NONE) { 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); setState(State::Error);
return -1; return -1;
} }
currentModem = modulation;
registerDio1Isr(); registerDio1Isr();
return 0; return 0;
} }
@ -145,19 +192,24 @@ void Sx1262::doEnd() {
void Sx1262::doTransmit() { void Sx1262::doTransmit() {
currentTx = popNextQueuedTx(); currentTx = popNextQueuedTx();
radio.standby(); uint16_t rc = RADIOLIB_ERR_NONE;
uint16_t rc = radio.startTransmit(currentTx.packet.data.data(), currentTx.packet.data.size()); 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) { if (rc == RADIOLIB_ERR_NONE) {
currentTx.callback(currentTx.id, TransmissionState::PendingTransmit); 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)); 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 // Thread might've been interrupted in the meanwhile
if (isThreadInterrupted()) { if (isThreadInterrupted()) {
return; return;
} }
// If the DIO1 bit is unset, this means the wait timed out
if (txEventFlags & SX1262_DIO1_EVENT_BIT) { if (txEventFlags & SX1262_DIO1_EVENT_BIT) {
currentTx.callback(currentTx.id, TransmissionState::Transmitted); currentTx.callback(currentTx.id, TransmissionState::Transmitted);
} else { } else {
@ -171,24 +223,31 @@ void Sx1262::doTransmit() {
} }
void Sx1262::doListen() { void Sx1262::doListen() {
radio.startReceive(); if (currentModem != Modulation::LrFhss) {
TT_LOG_I(TAG, "WAIT FLAG"); radio.startReceive();
auto eventFlags = events.wait(SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT | SX1262_QUEUED_TX_BIT); events.wait(SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT | SX1262_QUEUED_TX_BIT);
TT_LOG_W(TAG, "Event, flag=%X", eventFlags); } else {
// LR-FHSS modem only supports TX
events.wait(SX1262_INTERRUPT_BIT | SX1262_QUEUED_TX_BIT);
}
} }
void Sx1262::doReceive() { void Sx1262::doReceive() {
// LR-FHSS modem only supports TX
if (currentModem == Modulation::LrFhss) return;
uint16_t rxSize = radio.getPacketLength(true); uint16_t rxSize = radio.getPacketLength(true);
std::vector<uint8_t> data(rxSize); std::vector<uint8_t> data(rxSize);
uint16_t rc = radio.readData(data.data(), rxSize); uint16_t rc = radio.readData(data.data(), rxSize);
if (rc != RADIOLIB_ERR_NONE) { if (rc != RADIOLIB_ERR_NONE) {
TT_LOG_E(TAG, "Error receiving data, RadioLib returned %hi", rc); TT_LOG_E(TAG, "Error receiving data, RadioLib returned %hi", rc);
} else if(rxSize == 0) { } 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 { } else {
float rssi = radio.getRSSI(); float rssi = radio.getRSSI();
float snr = radio.getSNR(); 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 { auto rxPacket = tt::hal::radio::RxPacket {
.data = data, .data = data,
.rssi = rssi, .rssi = rssi,
@ -198,7 +257,7 @@ void Sx1262::doReceive() {
publishRx(rxPacket); publishRx(rxPacket);
} }
// A delay is needed before a new command // A delay before a new command improves reliability
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(SX1262_COOLDOWN_MILLIS));
} }

View File

@ -25,6 +25,7 @@ public:
}; };
private: private:
static constexpr auto SX1262_COOLDOWN_MILLIS = 100;
static constexpr auto SX1262_INTERRUPT_BIT = BIT0; static constexpr auto SX1262_INTERRUPT_BIT = BIT0;
static constexpr auto SX1262_DIO1_EVENT_BIT = BIT1; static constexpr auto SX1262_DIO1_EVENT_BIT = BIT1;
static constexpr auto SX1262_QUEUED_TX_BIT = BIT2; static constexpr auto SX1262_QUEUED_TX_BIT = BIT2;
@ -37,6 +38,7 @@ private:
Module radioModule; Module radioModule;
SX1262 radio; SX1262 radio;
TxItem currentTx; TxItem currentTx;
Modulation currentModem;
int8_t power = 0; int8_t power = 0;
float frequency = 0.0; float frequency = 0.0;
@ -47,6 +49,7 @@ private:
uint16_t preambleLength = 0; uint16_t preambleLength = 0;
float bitRate = 0.0; float bitRate = 0.0;
float frequencyDeviation = 0.0; float frequencyDeviation = 0.0;
bool narrowGrid = false;
void registerDio1Isr(); void registerDio1Isr();
void unregisterDio1Isr(); void unregisterDio1Isr();
@ -70,17 +73,25 @@ public:
, hal(configuration.spiHostDevice, configuration.spiFrequency, configuration.csPin, lock) , hal(configuration.spiHostDevice, configuration.spiFrequency, configuration.csPin, lock)
, radioModule(&hal, configuration.csPin, configuration.irqPin, configuration.resetPin, configuration.busyPin) , radioModule(&hal, configuration.csPin, configuration.irqPin, configuration.resetPin, configuration.busyPin)
, radio(&radioModule) , radio(&radioModule)
, currentModem(Modulation::None)
{} {}
~Sx1262() override = default; ~Sx1262() override = default;
std::string getName() const override { return name; } 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); return (modulation == Modulation::Fsk) || (modulation == Modulation::LoRa);
} }

View File

@ -8,13 +8,21 @@ namespace tt::hal::radio {
class ParameterSet { class ParameterSet {
private: private:
using Map = std::unordered_map<RadioDevice::Parameter, float, std::hash<int> >; struct ParameterHash
{
std::size_t operator()(RadioDevice::Parameter t) const
{
return static_cast<std::size_t>(t);
}
};
using Map = std::unordered_map<RadioDevice::Parameter, float, ParameterHash>;
Map parameters; Map parameters;
public: public:
explicit ParameterSet() {} explicit ParameterSet() {}
explicit ParameterSet(const ParameterSet& other) { parameters = other.parameters; } explicit ParameterSet(const ParameterSet& other) { parameters = other.parameters; }
~ParameterSet() override = default; ~ParameterSet() = default;
float get(const RadioDevice::Parameter parameter) { return parameters[parameter]; } float get(const RadioDevice::Parameter parameter) { return parameters[parameter]; }
void set(const RadioDevice::Parameter parameter, const float value) { parameters[parameter] = value; } void set(const RadioDevice::Parameter parameter, const float value) { parameters[parameter] = value; }
@ -32,7 +40,15 @@ public:
for (const auto& [parameter, value] : parameters) { for (const auto& [parameter, value] : parameters) {
// No break on error chosen to apply all parameters, // No break on error chosen to apply all parameters,
// a bad one doesn't make the successive tries any more invalid // 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; return successful;
} }

View File

@ -25,6 +25,7 @@ class RadioDevice : public Device {
public: public:
enum class Modulation { enum class Modulation {
None,
Fsk, Fsk,
LoRa, LoRa,
LrFhss LrFhss
@ -40,7 +41,8 @@ public:
PreambleLength, PreambleLength,
FrequencyDeviation, FrequencyDeviation,
DataRate, DataRate,
AddressWidth AddressWidth,
NarrowGrid
}; };
typedef int RxSubscriptionId; typedef int RxSubscriptionId;
@ -118,8 +120,10 @@ public:
Type getType() const override { return Type::Radio; } Type getType() const override { return Type::Radio; }
virtual bool configure(const Parameter parameter, const float value) = 0; virtual bool setParameter(const Parameter parameter, const float value) = 0;
virtual bool isCapableOf(const Modulation modulation) = 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 start(const Modulation modulation) = 0;
virtual bool stop() = 0; virtual bool stop() = 0;

View File

@ -5,6 +5,7 @@
#include <Tactility/Assets.h> #include <Tactility/Assets.h>
#include <Tactility/StringUtils.h> #include <Tactility/StringUtils.h>
#include <Tactility/hal/radio/RadioDevice.h> #include <Tactility/hal/radio/RadioDevice.h>
#include <Tactility/hal/radio/ParameterSet.h>
#include "Tactility/lvgl/LvglSync.h" #include "Tactility/lvgl/LvglSync.h"
@ -90,6 +91,29 @@ std::string hexdump(const std::vector<uint8_t>& data) {
return oss.str(); 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<unsigned char>(c)))
return false;
return true;
}
bool parseHexString(const std::string& hex, std::vector<uint8_t>& 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<uint8_t>((high << 4) | low));
}
return true;
}
class LoraView { class LoraView {
public: public:
@ -110,7 +134,8 @@ private:
loraDevs.clear(); loraDevs.clear();
for (const auto& radio: radios) { 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()); loraDevNames.push_back(radio->getName());
loraDevs.push_back(radio); loraDevs.push_back(radio);
} }
@ -409,11 +434,11 @@ public:
bool configured = true; bool configured = true;
buffer = lv_textarea_get_text(frequencyInput); buffer = lv_textarea_get_text(frequencyInput);
if (!buffer.empty()) { if (!buffer.empty()) {
configured &= loraDevice->configure(Frequency, std::stof(buffer)); configured &= loraDevice->setParameter(Frequency, std::stof(buffer));
} }
buffer = lv_textarea_get_text(bandwidthInput); buffer = lv_textarea_get_text(bandwidthInput);
if (!buffer.empty()) { if (!buffer.empty()) {
configured &= loraDevice->configure(Bandwidth, std::stof(buffer)); configured &= loraDevice->setParameter(Bandwidth, std::stof(buffer));
} }
buffer = lv_textarea_get_text(syncwordInput); buffer = lv_textarea_get_text(syncwordInput);
if (!buffer.empty()) { if (!buffer.empty()) {
@ -421,20 +446,20 @@ public:
std::stringstream ss(buffer); std::stringstream ss(buffer);
ss >> std::hex >> syncWord; 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); value = lv_slider_get_value(deBitsInput);
configured &= loraDevice->configure(CodingRate, value); configured &= loraDevice->setParameter(CodingRate, value);
value = lv_slider_get_value(sfInput); value = lv_slider_get_value(sfInput);
configured &= loraDevice->configure(SpreadFactor, value); configured &= loraDevice->setParameter(SpreadFactor, value);
value = lv_slider_get_value(preambleChirpsInput); value = lv_slider_get_value(preambleChirpsInput);
configured &= loraDevice->configure(PreambleLength, value); configured &= loraDevice->setParameter(PreambleLength, value);
buffer = lv_textarea_get_text(txPowInput); buffer = lv_textarea_get_text(txPowInput);
if (!buffer.empty()) { 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); lv_obj_t* msg_label = lv_label_create(msg_container);
std::vector<uint8_t> data(packet.data, packet.data + packet.size);
std::string messageBuf = ""; std::string messageBuf = "";
if (isPrintableData(data)) { if (isPrintableData(packet.data)) {
messageBuf = std::string((char*)packet.data, packet.size); messageBuf = std::string(packet.data.begin(), packet.data.end());
} else { } else {
messageBuf = hexdump(data); messageBuf = hexdump(packet.data);
} }
lv_label_set_text(msg_label, messageBuf.c_str()); lv_label_set_text(msg_label, messageBuf.c_str());
lv_obj_set_width(msg_label, lv_pct(100)); lv_obj_set_width(msg_label, lv_pct(100));
@ -793,8 +817,22 @@ public:
void sendMessage() { void sendMessage() {
std::string message = lv_textarea_get_text(inputField); std::string message = lv_textarea_get_text(inputField);
std::vector<uint8_t> data(message.begin(), message.end()); std::vector<uint8_t> data;
loraDevice->transmit(data, [this](hal::radio::RadioDevice::TxId id, hal::radio::RadioDevice::TransmissionState state) { 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<uint8_t>(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); this->onTxStatus(id, state);
}); });
} }

View File

@ -28,6 +28,8 @@ void RadioDevice::publishRx(const RxPacket& packet) {
const char* toString(RadioDevice::Modulation modulation) { const char* toString(RadioDevice::Modulation modulation) {
using enum RadioDevice::Modulation; using enum RadioDevice::Modulation;
switch (modulation) { switch (modulation) {
case None:
return "none";
case Fsk: case Fsk:
return "FSK"; return "FSK";
case LoRa: case LoRa:
@ -62,6 +64,8 @@ const char* toString(RadioDevice::Parameter parameter) {
return TT_STRINGIFY(DataRate); return TT_STRINGIFY(DataRate);
case AddressWidth: case AddressWidth:
return TT_STRINGIFY(AddressWidth); return TT_STRINGIFY(AddressWidth);
case NarrowGrid:
return TT_STRINGIFY(NarrowGrid);
default: default:
return "Unknown"; return "Unknown";
} }