Compare commits

...

15 Commits

Author SHA1 Message Date
8473c178e6 Radio: Some minor corrections and tweaks 2025-09-21 21:44:58 +02:00
09ef4c39d4 SX126x: Per-modem validation of parameters 2025-09-21 21:29:36 +02:00
b437c66fa3 Radio: Make modulation property of RadioDevice 2025-09-21 21:01:20 +02:00
6a250edcdb Radio: Add parameter validation, add units 2025-09-21 20:35:16 +02:00
cbfc7305f3 Radio: Refactor parameters and ParameterSet 2025-09-20 19:14:21 +02:00
26b122889c Radio: Refactor RadioDevice thread into compat class 2025-09-20 05:13:34 +02:00
8ae18b208f Radio: Add Parameter Set 2025-09-17 19:12:11 +02:00
e90c3e9170 Radio: Refactor RX/TX packages 2025-09-17 18:50:11 +02:00
6ddeef85b2 ChirpChatter: Update to improved Radio API
+ Add hexdump decode
 + Make progress/status functional
 + Transmit supported
2025-09-17 18:28:33 +02:00
ec7b738f33 Radio: Iteration 2 with Sx1262 - TX Update
Not quite as reliable still, but sending works.
2025-09-17 18:26:57 +02:00
be6266def6 Sx1262: Fixed DIO1 ISR registration by bypassing RadioLib 2025-09-14 11:31:02 +02:00
4d9617bf09 Add RadioDevice and support for SX1262 2025-09-14 09:53:22 +02:00
a0090bd48e ChripChatter: Uncomment all old LoRa API 2025-09-14 09:45:41 +02:00
1b04a18c02 Fixed layout, somewhat.. on the device it shifts in half. 2025-09-05 18:27:40 +02:00
38662dd6a0 Prototype ChripChatter GUI 2025-09-04 17:00:35 +02:00
21 changed files with 2292 additions and 3 deletions

View File

@ -19,4 +19,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'

View File

@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility esp_lcd ST7796 BQ25896 BQ27220 TCA8418 DRV2605 PwmBacklight driver esp_adc
REQUIRES Tactility esp_lcd ST7796 BQ25896 BQ27220 TCA8418 DRV2605 SX126x PwmBacklight driver esp_adc
)

View File

@ -8,6 +8,7 @@
#include <Bq25896.h>
#include <Drv2605.h>
#include <Sx1262.h>
#include <Tactility/hal/Configuration.h>
#define TPAGER_SPI_TRANSFER_SIZE_LIMIT (TPAGER_LCD_HORIZONTAL_RESOLUTION * TPAGER_LCD_SPI_TRANSFER_HEIGHT * (LV_COLOR_DEPTH / 8))
@ -23,6 +24,17 @@ DeviceVector createDevices() {
auto tca8418 = std::make_shared<Tca8418>(I2C_NUM_0);
auto keyboard = std::make_shared<TpagerKeyboard>(tca8418);
auto sx1262 = std::make_shared<Sx1262>(Sx1262::Configuration{
.spiHostDevice = SPI2_HOST,
.spiFrequency = 10'000'000,
.csPin = GPIO_NUM_36,
.resetPin = GPIO_NUM_47,
.busyPin = GPIO_NUM_48,
.irqPin = GPIO_NUM_14,
.tcxoVoltage = 3.0,
.useRegulatorLdo = false
});
return std::vector<std::shared_ptr<Device>> {
tca8418,
std::make_shared<Bq25896>(I2C_NUM_0),
@ -32,7 +44,8 @@ DeviceVector createDevices() {
createTpagerSdCard(),
createDisplay(),
keyboard,
std::make_shared<TpagerEncoder>()
std::make_shared<TpagerEncoder>(),
sx1262
};
}

View File

@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility radiolib
)

View File

@ -0,0 +1,3 @@
# RadioLibCompat
A set of helper classes to implement `RadioLib` drivers in Tactility.

View File

@ -0,0 +1,167 @@
#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),
.mode = (gpio_mode_t)mode,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = (gpio_int_type_t)gpiohal.dev->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;
}
if (!isrServiceInitialized) {
gpio_install_isr_service((int)ESP_INTR_FLAG_IRAM);
isrServiceInitialized = true;
}
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
// TODO: I think the wisest course of action is forbidding registration via RadioLib entirely,
// as it doesn't suit Tactility with its lack of context passing
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() {
if (!spiInitialized) {
TT_LOG_I(TAG, "SPI Begin!");
spi_device_interface_config_t devcfg = {};
devcfg.clock_speed_hz = spiFrequency;
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));
}
spiInitialized = true;
}
}
void RadiolibTactilityHal::spiBeginTransaction() {
// This function is used to set up the transaction (speed, bit order, mode, ...).
// With the ESP-IDF HAL this is automatically done, so no code needed.
}
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() {
if (spiInitialized) {
spi_bus_remove_device(spiDeviceHandle);
spiInitialized = false;
}
}

View File

@ -0,0 +1,64 @@
#pragma once
#include <Tactility/Lock.h>
#include <Tactility/hal/spi/Spi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <RadioLib.h>
#include <driver/gpio.h>
#include <driver/spi_master.h>
#include <memory>
class RadiolibTactilityHal : public RadioLibHal {
private:
spi_host_device_t spiHostDevice;
int spiFrequency;
gpio_num_t csPin;
spi_device_handle_t spiDeviceHandle;
std::shared_ptr<tt::Lock> lock;
bool spiInitialized;
bool isrServiceInitialized;
public:
explicit RadiolibTactilityHal(spi_host_device_t spiHostDevice, int spiFrequency, gpio_num_t csPin)
: RadioLibHal(
GPIO_MODE_INPUT,
GPIO_MODE_OUTPUT,
0, // LOW
1, // HIGH
GPIO_INTR_POSEDGE,
GPIO_INTR_NEGEDGE)
, spiHostDevice(spiHostDevice)
, spiFrequency(spiFrequency)
, csPin(csPin)
, lock(tt::hal::spi::getLock(spiHostDevice))
, spiInitialized(false)
, isrServiceInitialized(false) {}
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<tt::Lock> getLock() const { return lock; }
};

View File

@ -0,0 +1,93 @@
#include "RadiolibThreadedDevice.h"
#include <cstring>
constexpr const char* TAG = "RadiolibThreadedDevice";
bool RadiolibThreadedDevice::start() {
auto lock = getMutex().asScopedLock();
lock.lock();
if ((thread != nullptr) && (thread->getState() != tt::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<tt::Thread>(
threadName,
threadSize,
[this]() {
return this->threadMain();
}
);
thread->setPriority(tt::Thread::Priority::High);
thread->start();
TT_LOG_I(TAG, "Starting finished");
return true;
}
bool RadiolibThreadedDevice::stop() {
auto lock = getMutex().asScopedLock();
lock.lock();
setState(State::PendingOff);
if (thread != nullptr) {
threadInterrupted = true;
interruptSignal();
// Detach thread, it will auto-delete when leaving the current scope
auto old_thread = std::move(thread);
if (old_thread->getState() != tt::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 RadiolibThreadedDevice::isThreadInterrupted() const {
auto lock = getMutex().asScopedLock();
lock.lock();
return threadInterrupted;
}
int32_t RadiolibThreadedDevice::threadMain() {
int rc = doBegin(getModulation());
if (rc != 0) {
return rc;
}
setState(State::On);
while (!isThreadInterrupted()) {
doListen();
// Thread might've been interrupted in the meanwhile
if (isThreadInterrupted()) {
break;
}
if (getTxQueueSize() > 0) {
doTransmit();
} else {
doReceive();
}
}
doEnd();
return 0;
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <Tactility/hal/radio/RadioDevice.h>
#include <Tactility/Thread.h>
class RadiolibThreadedDevice : public tt::hal::radio::RadioDevice {
private:
std::string threadName;
size_t threadSize;
std::unique_ptr<tt::Thread> _Nullable thread;
bool threadInterrupted = false;
protected:
virtual int32_t threadMain();
bool isThreadInterrupted() const;
virtual void interruptSignal() = 0;
virtual int doBegin(const Modulation modulation) = 0;
virtual void doEnd() = 0;
virtual void doTransmit() = 0;
virtual void doListen() = 0;
virtual void doReceive() = 0;
public:
explicit RadiolibThreadedDevice(const std::string& threadName, const size_t threadSize)
: threadName(threadName)
, threadSize(threadSize)
{}
~RadiolibThreadedDevice() override = default;
virtual bool start() override;
virtual bool stop() override;
};

View File

@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility driver RadioLibCompat radiolib
)

7
Drivers/SX126x/README.md Normal file
View File

@ -0,0 +1,7 @@
# SX126x
Radio with LoRa/(G)FSK capabilities.
## SX1262
- [Product Information](https://www.semtech.com/products/wireless-rf/lora-connect/sx1262)

View File

@ -0,0 +1,441 @@
#include "Sx1262.h"
#include <cstring>
#include "hal/gpio_hal.h"
constexpr const char* TAG = "Sx1262";
template<typename T>
static constexpr Sx1262::ParameterStatus checkLimitsAndApply(T &target, const float value, const float lower, const float upper, const unsigned step = 0) {
if ((value >= lower) && (value <= upper)) {
if (step != 0) {
int ivalue = static_cast<int>(value);
if ((ivalue % step) != 0) {
return Sx1262::ParameterStatus::ValueError;
}
}
target = static_cast<T>(value);
return Sx1262::ParameterStatus::Success;
}
return Sx1262::ParameterStatus::ValueError;
}
template<typename T>
static constexpr Sx1262::ParameterStatus checkValuesAndApply(T &target, const float value, std::initializer_list<float> valids) {
for (float valid : valids) {
if (value == valid) {
target = static_cast<T>(value);
return Sx1262::ParameterStatus::Success;
}
}
return Sx1262::ParameterStatus::ValueError;
}
void IRAM_ATTR dio1handler(void* context) {
((Sx1262*)context)->dio1Event();
}
Sx1262::ParameterStatus Sx1262::setLoraParameter(const Parameter parameter, const float value) {
using enum Parameter;
switch (parameter) {
case Power:
return checkLimitsAndApply(power, value, -9.0, 22.0);
case Frequency:
return checkLimitsAndApply(frequency, value, 150.0, 960.0);
case Bandwidth:
return checkValuesAndApply(bandwidth, value, {
7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125.0, 250.0, 500.0
});
case SpreadFactor:
return checkLimitsAndApply(spreadFactor, value, 7.0, 12.0, 1);
case CodingRate:
return checkLimitsAndApply(codingRate, value, 5.0, 8.0, 1);
case SyncWord:
return checkLimitsAndApply(syncWord, value, 0.0, 255.0, 1);
case PreambleLength:
return checkLimitsAndApply(preambleLength, value, 0.0, 65535.0, 1);
default:
break;
}
TT_LOG_W(TAG, "Tried to set unsupported LoRa parameter \"%s\" to %f", toString(parameter), value);
return Sx1262::ParameterStatus::Unavailable;
}
Sx1262::ParameterStatus Sx1262::setFskParameter(const Parameter parameter, const float value) {
using enum Parameter;
switch (parameter) {
case Power:
return checkLimitsAndApply(power, value, -9.0, 22.0);
case Frequency:
return checkLimitsAndApply(frequency, value, 150.0, 960.0);
case Bandwidth:
return checkValuesAndApply(bandwidth, value, {
4.8, 5.8, 7.3, 9.7, 11.7, 14.6, 19.5, 23.4, 29.3, 39.0, 46.9, 58.6, 78.2
});
case PreambleLength:
return checkLimitsAndApply(preambleLength, value, 0.0, 65535.0, 1);
case DataRate:
return checkLimitsAndApply(bitRate, value, 0.6, 300.0);
case FrequencyDeviation:
return checkLimitsAndApply(frequencyDeviation, value, 0.0, 200.0);
default:
break;
}
TT_LOG_W(TAG, "Tried to set unsupported FSK parameter \"%s\" to %f", toString(parameter), value);
return Sx1262::ParameterStatus::Unavailable;
}
Sx1262::ParameterStatus Sx1262::setLrFhssParameter(const Parameter parameter, const float value) {
using enum Parameter;
switch (parameter) {
case Bandwidth:
return checkValuesAndApply(bandwidth, value, {
39.06, 85.94, 136.72, 183.59, 335.94, 386.72, 722.66, 773.44, 1523.4, 1574.2
});
case CodingRate:
return checkValuesAndApply(codingRate, value, {
RADIOLIB_SX126X_LR_FHSS_CR_5_6,
RADIOLIB_SX126X_LR_FHSS_CR_2_3,
RADIOLIB_SX126X_LR_FHSS_CR_1_2,
RADIOLIB_SX126X_LR_FHSS_CR_1_3
});
case NarrowGrid:
return checkLimitsAndApply(narrowGrid, value, 0.0, 1.0, 1);
default:
break;
}
TT_LOG_W(TAG, "Tried to set unsupported LR-FHSS parameter \"%s\" to %f", toString(parameter), value);
return Sx1262::ParameterStatus::Unavailable;
}
Sx1262::ParameterStatus Sx1262::setParameter(const Parameter parameter, const float value) {
const auto currentModulation = getModulation();
switch (currentModulation) {
case Modulation::LoRa:
return setLoraParameter(parameter, value);
case Modulation::Fsk:
return setFskParameter(parameter, value);
case Modulation::LrFhss:
return setLrFhssParameter(parameter, value);
default:
break;
}
// Shouldn't be reachable, return failsafe value
return Sx1262::ParameterStatus::Unavailable;
}
Sx1262::ParameterStatus Sx1262::getLoraParameter(const Parameter parameter, float &value) const {
using enum Parameter;
switch (parameter) {
case Power:
value = power;
return Sx1262::ParameterStatus::Success;
case Frequency:
value = frequency;
return Sx1262::ParameterStatus::Success;
case Bandwidth:
value = bandwidth;
return Sx1262::ParameterStatus::Success;
case SpreadFactor:
value = spreadFactor;
return Sx1262::ParameterStatus::Success;
case CodingRate:
value = codingRate;
return Sx1262::ParameterStatus::Success;
case SyncWord:
value = syncWord;
return Sx1262::ParameterStatus::Success;
case PreambleLength:
value = preambleLength;
return Sx1262::ParameterStatus::Success;
default:
break;
}
TT_LOG_W(TAG, "Tried to get unsupported LoRa parameter \"%s\"", toString(parameter));
return Sx1262::ParameterStatus::Unavailable;
}
Sx1262::ParameterStatus Sx1262::getFskParameter(const Parameter parameter, float &value) const {
using enum Parameter;
switch (parameter) {
case Power:
value = power;
return Sx1262::ParameterStatus::Success;
case Frequency:
value = frequency;
return Sx1262::ParameterStatus::Success;
case Bandwidth:
value = bandwidth;
return Sx1262::ParameterStatus::Success;
case DataRate:
value = bitRate;
return Sx1262::ParameterStatus::Success;
case FrequencyDeviation:
value = frequencyDeviation;
return Sx1262::ParameterStatus::Success;
default:
break;
}
TT_LOG_W(TAG, "Tried to get unsupported FSK parameter \"%s\"", toString(parameter));
return Sx1262::ParameterStatus::Unavailable;
}
Sx1262::ParameterStatus Sx1262::getLrFhssParameter(const Parameter parameter, float &value) const {
using enum Parameter;
switch (parameter) {
case Power:
value = power;
return Sx1262::ParameterStatus::Success;
case Bandwidth:
value = bandwidth;
return Sx1262::ParameterStatus::Success;
case CodingRate:
value = codingRate;
return Sx1262::ParameterStatus::Success;
case NarrowGrid:
value = narrowGrid;
return Sx1262::ParameterStatus::Success;
default:
break;
}
TT_LOG_W(TAG, "Tried to get unsupported LR-FHSS parameter \"%s\"", toString(parameter));
return Sx1262::ParameterStatus::Unavailable;
}
Sx1262::ParameterStatus Sx1262::getParameter(const Parameter parameter, float &value) const {
const auto currentModulation = getModulation();
switch (currentModulation) {
case Modulation::LoRa:
return getLoraParameter(parameter, value);
case Modulation::Fsk:
return getFskParameter(parameter, value);
case Modulation::LrFhss:
return getLrFhssParameter(parameter, value);
default:
break;
}
// Shouldn't be reachable, return failsafe value
return Sx1262::ParameterStatus::Unavailable;
}
tt::hal::radio::Unit Sx1262::getParameterUnit(const Parameter parameter) const {
using enum Parameter;
using Unit = tt::hal::radio::Unit;
switch (parameter) {
case Power:
return Unit(Unit::Name::DecibelMilliwatts);
case Frequency:
return Unit(Unit::Prefix::Mega, Unit::Name::Herz);
case Bandwidth:
return Unit(Unit::Prefix::Kilo, Unit::Name::Herz);
case SpreadFactor:
case CodingRate: // no break
case SyncWord: // no break
case PreambleLength: // no break
return Unit(Unit::Name::None);
case DataRate:
return Unit(Unit::Prefix::Kilo, Unit::Name::BitsPerSecond);
case FrequencyDeviation:
return Unit(Unit::Prefix::Kilo, Unit::Name::Herz);
case NarrowGrid:
return Unit(Unit::Name::None);
default:
break;
}
TT_LOG_W(TAG, "Tried to get unit for unsupported parameter \"%s\"", toString(parameter));
return Unit(Unit::Name::None);
}
void Sx1262::registerDio1Isr() {
gpio_hal_context_t gpiohal;
gpiohal.dev = GPIO_LL_GET_HW(GPIO_PORT_0);
gpio_config_t conf = {
.pin_bit_mask = (1ULL<<configuration.irqPin),
.mode = (gpio_mode_t)GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_ENABLE,
.intr_type = (gpio_int_type_t)gpiohal.dev->pin[configuration.irqPin].int_type,
};
gpio_config(&conf);
// We cannot use the RadioLib API to register this action,
// as it does not have the capability to pass an instance pointer via context.
// A trampoline has been tried, but is not linkable to be in IRAM_ATTR (dangerous relocation).
gpio_install_isr_service((int)ESP_INTR_FLAG_IRAM);
gpio_set_intr_type(configuration.irqPin, GPIO_INTR_POSEDGE);
gpio_isr_handler_add(configuration.irqPin, dio1handler, this);
}
void Sx1262::unregisterDio1Isr() {
gpio_isr_handler_remove(configuration.irqPin);
gpio_wakeup_disable(configuration.irqPin);
gpio_set_intr_type(configuration.irqPin, GPIO_INTR_DISABLE);
}
void IRAM_ATTR Sx1262::dio1Event() {
static const auto DRAM_ATTR bit = SX1262_DIO1_EVENT_BIT;
events.set(bit);
}
void Sx1262::txQueuedSignal() {
events.set(SX1262_QUEUED_TX_BIT);
}
void Sx1262::interruptSignal() {
events.set(SX1262_INTERRUPT_BIT);
}
int Sx1262::doBegin(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 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);
return -1;
}
if (rc != RADIOLIB_ERR_NONE) {
TT_LOG_E(TAG, "Radiolib initialization failed with code %hi", rc);
setState(State::Error);
return -1;
}
registerDio1Isr();
return 0;
}
void Sx1262::doEnd() {
unregisterDio1Isr();
}
void Sx1262::doTransmit() {
currentTx = popNextQueuedTx();
uint16_t rc = RADIOLIB_ERR_NONE;
rc = radio.standby();
if (rc != RADIOLIB_ERR_NONE) {
TT_LOG_W(TAG, "RadioLib returned %hi on standby", rc);
}
if (getModulation() == Modulation::Fsk) {
rc = radio.startTransmit(currentTx.packet.data.data(), currentTx.packet.data.size(),
currentTx.packet.address);
} else {
rc = radio.startTransmit(currentTx.packet.data.data(), currentTx.packet.data.size());
}
if (rc == RADIOLIB_ERR_NONE) {
currentTx.callback(currentTx.id, TransmissionState::PendingTransmit);
auto txEventFlags = events.wait(SX1262_INTERRUPT_BIT | SX1262_DIO1_EVENT_BIT, tt::EventFlag::WaitAny,
pdMS_TO_TICKS(SX1262_TX_TIMEOUT_MILLIS));
// 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 {
currentTx.callback(currentTx.id, TransmissionState::Timeout);
}
} else {
TT_LOG_E(TAG, "Error transmitting id=%d, rc=%hi", currentTx.id, rc);
currentTx.callback(currentTx.id, TransmissionState::Error);
}
}
void Sx1262::doListen() {
if (getModulation() != 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 (getModulation() == Modulation::LrFhss) return;
uint16_t rxSize = radio.getPacketLength(true);
std::vector<uint8_t> 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) {
// 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();
auto rxPacket = tt::hal::radio::RxPacket {
.data = data,
.rssi = rssi,
.snr = snr
};
publishRx(rxPacket);
}
// A delay before a new command improves reliability
vTaskDelay(pdMS_TO_TICKS(SX1262_COOLDOWN_MILLIS));
}

View File

@ -0,0 +1,107 @@
#pragma once
#include <Tactility/hal/spi/Spi.h>
#include <Tactility/EventFlag.h>
#include <Tactility/Lock.h>
#include <RadioLib.h>
#include "RadiolibTactilityHal.h"
#include "RadiolibThreadedDevice.h"
#include <utility>
class Sx1262 final : public RadiolibThreadedDevice {
public:
struct Configuration {
spi_host_device_t spiHostDevice;
int spiFrequency;
gpio_num_t csPin;
gpio_num_t resetPin;
gpio_num_t busyPin;
gpio_num_t irqPin;
float tcxoVoltage;
bool useRegulatorLdo;
};
private:
static constexpr auto SX1262_DEFAULT_NAME = "SX1262";
static constexpr auto SX1262_COOLDOWN_MILLIS = 100;
static constexpr auto SX1262_TX_TIMEOUT_MILLIS = 2000;
static constexpr auto SX1262_INTERRUPT_BIT = BIT0;
static constexpr auto SX1262_DIO1_EVENT_BIT = BIT1;
static constexpr auto SX1262_QUEUED_TX_BIT = BIT2;
std::string name;
const Configuration configuration;
std::shared_ptr<tt::Lock> lock;
tt::EventFlag events;
RadiolibTactilityHal hal;
Module radioModule;
SX1262 radio;
TxItem currentTx;
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;
bool narrowGrid = false;
void registerDio1Isr();
void unregisterDio1Isr();
ParameterStatus setLoraParameter(const Parameter parameter, const float value);
ParameterStatus setFskParameter(const Parameter parameter, const float value);
ParameterStatus setLrFhssParameter(const Parameter parameter, const float value);
ParameterStatus getLoraParameter(const Parameter parameter, float &value) const;
ParameterStatus getFskParameter(const Parameter parameter, float &value) const;
ParameterStatus getLrFhssParameter(const Parameter parameter, float &value) const;
protected:
virtual void txQueuedSignal() override;
virtual void interruptSignal() override;
virtual int doBegin(const Modulation modulation) override;
virtual void doEnd() override;
virtual void doTransmit() override;
virtual void doListen() override;
virtual void doReceive() override;
public:
explicit Sx1262(const Configuration& configuration, const std::string& name = SX1262_DEFAULT_NAME)
: RadiolibThreadedDevice(name, 4096)
, name(name)
, configuration(configuration)
, hal(configuration.spiHostDevice, configuration.spiFrequency, configuration.csPin)
, radioModule(&hal, configuration.csPin, configuration.irqPin, configuration.resetPin, configuration.busyPin)
, radio(&radioModule)
{}
~Sx1262() override = default;
std::string getName() const override { return name; }
std::string getDescription() const override { return "Semtech SX1262 LoRa, FSK and LR-FHSS capable radio"; }
ParameterStatus setParameter(const Parameter parameter, const float value) override;
ParameterStatus getParameter(const Parameter parameter, float &value) const override;
tt::hal::radio::Unit getParameterUnit(const Parameter parameter) const override;
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);
}
void dio1Event();
};

View File

@ -22,7 +22,8 @@ public:
Keyboard,
Encoder,
Power,
Gps
Gps,
Radio
};
typedef uint32_t Id;

View File

@ -0,0 +1,63 @@
#pragma once
#include "RadioDevice.h"
#include <map>
namespace tt::hal::radio {
class ParameterSet {
private:
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;
public:
explicit ParameterSet() {}
explicit ParameterSet(const ParameterSet& other) { parameters = other.parameters; }
~ParameterSet() = default;
float get(const RadioDevice::Parameter parameter) { return parameters[parameter]; }
void set(const RadioDevice::Parameter parameter, const float value) { parameters[parameter] = value; }
bool has(const RadioDevice::Parameter parameter) { return parameters.contains(parameter); }
bool erase(const RadioDevice::Parameter parameter) {
if (has(parameter)) {
parameters.erase(parameter);
return true;
}
return false;
}
void clear() { parameters.clear(); }
bool apply(RadioDevice &radio) {
bool successful = true;
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.setParameter(parameter, value) == RadioDevice::ParameterStatus::Success);
}
return successful;
}
void load(const RadioDevice &radio) {
// This loop has to be ajusted for each new parameter.
// Could be made more maintainable with a template enum iterator in an utility header.
for (RadioDevice::Parameter p = RadioDevice::Parameter::Power;
p < RadioDevice::Parameter::NarrowGrid;
p = static_cast<RadioDevice::Parameter>((size_t)p + 1)) {
float value = 0.0;
if (radio.getParameter(p, value) == RadioDevice::ParameterStatus::Success) {
set(p, value);
}
}
}
};
}

View File

@ -0,0 +1,174 @@
#pragma once
#include "../Device.h"
#include "Unit.h"
#include <Tactility/Mutex.h>
#include <Tactility/Thread.h>
#include <deque>
#include <utility>
namespace tt::hal::radio {
struct RxPacket {
std::vector<uint8_t> data;
float rssi;
float snr;
};
struct TxPacket {
std::vector<uint8_t> data;
uint32_t address; // FSK only
};
class RadioDevice : public Device {
public:
enum class Modulation {
None,
Fsk,
LoRa,
LrFhss
};
enum class Parameter {
Power,
Frequency,
Bandwidth,
SpreadFactor,
CodingRate,
SyncWord,
PreambleLength,
FrequencyDeviation,
DataRate,
AddressWidth,
NarrowGrid
};
enum class ParameterStatus {
Unavailable,
ValueError,
Success
};
typedef int RxSubscriptionId;
typedef int TxId;
enum class State {
PendingOn,
On,
Error,
PendingOff,
Off
};
enum class TransmissionState {
Queued,
PendingTransmit,
Transmitted,
Timeout,
Error
};
using TxStateCallback = std::function<void(TxId id, TransmissionState state)>;
protected:
struct TxItem {
TxId id;
TxPacket packet;
TxStateCallback callback;
};
private:
struct RxSubscription {
RxSubscriptionId id;
std::shared_ptr<std::function<void(Device::Id id, const RxPacket&)>> onData;
};
State state;
Modulation modulation;
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::vector<RxSubscription> rxSubscriptions;
std::deque<TxItem> txQueue;
TxId lastTxId = 0;
RxSubscriptionId lastRxSubscriptionId = 0;
protected:
const Mutex &getMutex() const { return mutex; }
void setState(State newState);
virtual void txQueuedSignal() = 0;
size_t getTxQueueSize() const {
auto lock = mutex.asScopedLock();
lock.lock();
const auto size = txQueue.size();
return size;
}
TxItem popNextQueuedTx() {
auto lock = mutex.asScopedLock();
lock.lock();
auto tx = std::move(txQueue.front());
txQueue.pop_front();
return tx;
}
void publishRx(const RxPacket& packet);
public:
explicit RadioDevice()
: state(State::Off), modulation(Modulation::None) {}
~RadioDevice() override = default;
Type getType() const override { return Type::Radio; }
bool setModulation(const Modulation newModulation);
Modulation getModulation() const;
virtual ParameterStatus setParameter(const Parameter parameter, const float value) = 0;
virtual ParameterStatus getParameter(const Parameter parameter, float &value) const = 0;
virtual Unit getParameterUnit(const Parameter parameter) const = 0;
virtual bool canTransmit(const Modulation modulation) = 0;
virtual bool canReceive(const Modulation modulation) = 0;
virtual bool start() = 0;
virtual bool stop() = 0;
TxId transmit(const TxPacket& packet, TxStateCallback callback) {
auto lock = mutex.asScopedLock();
lock.lock();
const auto txId = lastTxId;
txQueue.push_back(TxItem{.id = txId, .packet = packet, .callback = callback});
callback(txId, TransmissionState::Queued);
lastTxId++;
txQueuedSignal();
return txId;
}
RxSubscriptionId subscribeRx(const std::function<void(Device::Id id, const RxPacket&)>& onData) {
auto lock = mutex.asScopedLock();
lock.lock();
rxSubscriptions.push_back({
.id = ++lastRxSubscriptionId,
.onData = std::make_shared<std::function<void(Device::Id, const RxPacket&)>>(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);
}

View File

@ -0,0 +1,46 @@
#pragma once
#include <string>
namespace tt::hal::radio {
class Unit {
public:
enum class Prefix {
Femto,
Pico,
Nano,
Milli,
None,
Kilo,
Mega,
Giga,
Terra,
Peta
};
enum class Name
{
None,
BitsPerSecond,
BytesPerSecond,
Herz,
Decibel,
DecibelMilliwatts
};
const Prefix prefix;
const Name unit;
explicit Unit(const Prefix si, const Name unit)
: prefix(si), unit(unit) {}
explicit Unit(const Name unit)
: prefix(Prefix::None), unit(unit) {}
std::string toString() const;
};
const char* toString(Unit::Prefix prefix);
const char* toString(Unit::Name unit);
}

View File

@ -54,6 +54,7 @@ namespace app {
namespace boot { extern const AppManifest manifest; }
namespace calculator { extern const AppManifest manifest; }
namespace chat { extern const AppManifest manifest; }
namespace chirp { extern const AppManifest manifest; }
namespace development { extern const AppManifest manifest; }
namespace display { extern const AppManifest manifest; }
namespace filebrowser { extern const AppManifest manifest; }
@ -121,6 +122,7 @@ static void registerSystemApps() {
addApp(app::wifiapsettings::manifest);
addApp(app::wificonnect::manifest);
addApp(app::wifimanage::manifest);
addApp(app::chirp::manifest);
#if TT_FEATURE_SCREENSHOT_ENABLED
addApp(app::screenshot::manifest);

View File

@ -0,0 +1,898 @@
//#ifdef ESP_PLATFORM
#include <Tactility/app/AppManifest.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/Assets.h>
#include <Tactility/StringUtils.h>
#include <Tactility/hal/radio/RadioDevice.h>
#include <Tactility/hal/radio/ParameterSet.h>
#include "Tactility/lvgl/LvglSync.h"
#include <cstdio>
#include <cstring>
#include <vector>
#include <iomanip>
#include <ctime>
#include <sstream>
#include <algorithm>
#include <cctype>
#include <iomanip>
#include <sstream>
#include <vector>
#include <cstdint>
#include <lvgl.h>
extern const lv_obj_class_t lv_label_class;
namespace tt::app::chirp {
constexpr const char* TAG = "ChirpChatterApp";
enum CCViews
{
CCView_Msgs,
CCView_LoraSettings,
CCView_ProtoSettings
};
class ChirpChatterApp;
template<CCViews view>
static void changeViewHandler(lv_event_t* e) {
auto* self = static_cast<ChirpChatterApp*>(lv_event_get_user_data(e));
TT_LOG_I(TAG, "Clicked %d", view);
self->changeView(view);
}
static void buttonRecolorFocus(lv_event_t *event) {
lv_obj_t *image = (lv_obj_t *)lv_event_get_user_data(event);
if (image != NULL) {
lv_obj_set_style_image_recolor(image, lv_palette_main(LV_PALETTE_YELLOW), LV_STATE_DEFAULT);
}
}
static void buttonRecolorDefocus(lv_event_t *event) {
lv_obj_t *image = (lv_obj_t *)lv_event_get_user_data(event);
if (image != NULL) {
lv_obj_set_style_image_recolor(image, lv_theme_get_color_primary(image), LV_STATE_DEFAULT);
}
}
static void debugFocus(lv_event_t *event) {
lv_obj_t *target = (lv_obj_t *)lv_event_get_current_target(event);
if (target != NULL) {
lv_obj_set_style_bg_color(target, lv_color_hex(0xFF0000), 0);
}
}
static void debugDefocus(lv_event_t *event) {
lv_obj_t *target = (lv_obj_t *)lv_event_get_current_target(event);
if (target != NULL) {
lv_obj_set_style_bg_color(target, lv_color_hex(0x00FF00), 0);
}
}
bool isPrintableData(const std::vector<uint8_t>& data) {
return std::all_of(data.begin(), data.end(),
[](uint8_t byte) {
char c = static_cast<char>(byte);
return std::isprint(static_cast<unsigned char>(c));
});
}
std::string hexdump(const std::vector<uint8_t>& data) {
std::ostringstream oss;
for (size_t i = 0; i < data.size(); ++i) {
oss << std::hex << std::uppercase << std::setw(2) << std::setfill('0')
<< static_cast<unsigned>(data[i]);
if (i + 1 != data.size()) oss << ' ';
}
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 {
public:
using DeviceActivationCallback = std::function<void(std::shared_ptr<tt::hal::radio::RadioDevice>)>;
private:
//using LoraParameters = tt::hal::lora::LoraParameters;
std::vector<std::string> loraDevNames;
std::vector<std::shared_ptr<tt::hal::radio::RadioDevice>> loraDevs;
std::shared_ptr<tt::hal::radio::RadioDevice> loraDevice;
DeviceActivationCallback cbDevActive;
DeviceActivationCallback cbDevInactive;
void queryLoraDevs() {
auto radios = tt::hal::findDevices<tt::hal::radio::RadioDevice>(tt::hal::Device::Type::Radio);
loraDevNames.clear();
loraDevs.clear();
for (const auto& radio: radios) {
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);
}
}
}
lv_obj_t* initDropdownInput(int row, const char* const label, const char* const items) {
lv_obj_t* label_obj = lv_label_create(container);
lv_label_set_text(label_obj, label);
lv_obj_set_grid_cell(label_obj,
LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(label_obj, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_t* input = lv_dropdown_create(container);
lv_obj_set_grid_cell(input,
LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(input, lv_pct(100), LV_SIZE_CONTENT);
lv_dropdown_set_options(input, items);
return input;
}
lv_obj_t* initFormInput(int row, const char* const label, const char* const defval, const char* const unit) {
lv_obj_t* label_obj = lv_label_create(container);
lv_label_set_text(label_obj, label);
lv_obj_set_grid_cell(label_obj,
LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(label_obj, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_t* input = lv_textarea_create(container);
lv_obj_set_grid_cell(input,
LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(input, lv_pct(100), LV_SIZE_CONTENT);
lv_textarea_set_text(input, defval);
lv_textarea_set_one_line(input, true);
lv_obj_t* unit_obj = lv_label_create(container);
lv_label_set_text(unit_obj, unit);
lv_obj_set_grid_cell(unit_obj,
LV_GRID_ALIGN_STRETCH, 2, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(unit_obj, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_set_style_text_align(unit_obj , LV_TEXT_ALIGN_CENTER, 0);
return input;
}
lv_obj_t* initSliderInput(int row, const char* const label, int min, int max, int defval) {
lv_obj_t* label_obj = lv_label_create(container);
lv_label_set_text(label_obj, label);
lv_obj_set_grid_cell(label_obj,
LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(label_obj, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_t* input = lv_slider_create(container);
lv_obj_set_grid_cell(input,
LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(input, lv_pct(100), 10);
lv_slider_set_range(input, min, max);
lv_obj_t* number_obj = lv_label_create(container);
//lv_label_set_text(number_obj, unit);
lv_obj_set_grid_cell(number_obj,
LV_GRID_ALIGN_STRETCH, 2, 1,
LV_GRID_ALIGN_CENTER, row, 1);
lv_obj_set_size(number_obj, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_set_style_text_align(number_obj , LV_TEXT_ALIGN_CENTER, 0);
char buf[8] = {0};
lv_snprintf(buf, sizeof(buf), "%d", defval);
lv_label_set_text(number_obj, buf);
lv_obj_add_event_cb(input, [](lv_event_t * e) {
lv_obj_t* slider = lv_event_get_target_obj(e);
lv_obj_t* label = (lv_obj_t*)lv_event_get_user_data(e);
char buf[8] = {0};
lv_snprintf(buf, sizeof(buf), "%d", (int)lv_slider_get_value(slider));
lv_label_set_text(label, buf);
}, LV_EVENT_VALUE_CHANGED, number_obj);
lv_slider_set_value(input, defval, LV_ANIM_OFF);
return input;
}
void initUi(lv_obj_t *parent) {
container = lv_obj_create(parent);
lv_obj_set_size(container, lv_pct(100), lv_pct(100));
lv_obj_set_style_pad_all(container, 0, 0);
int grid_row_size = 40;
static lv_coord_t lora_col_dsc[] = {LV_GRID_FR(3), LV_GRID_FR(2), 45, LV_GRID_TEMPLATE_LAST};
static lv_coord_t lora_row_dsc[] = {
grid_row_size,
grid_row_size,
grid_row_size,
grid_row_size,
grid_row_size,
grid_row_size,
grid_row_size,
grid_row_size,
LV_GRID_TEMPLATE_LAST};
lv_obj_set_grid_dsc_array(container, lora_col_dsc, lora_row_dsc);
std::string dropdown_items = string::join(loraDevNames, "\n");
loraDeviceInput = initDropdownInput(0, "LoRa Device", dropdown_items.c_str());
loraDeviceOn = lv_switch_create(container);
lv_obj_set_grid_cell(loraDeviceOn,
LV_GRID_ALIGN_STRETCH, 2, 1,
LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_set_size(loraDeviceOn, lv_pct(100), 20);
frequencyInput = initFormInput(1, "Frequency", "869.525", "MHz");
bandwidthInput = initFormInput(2, "Bandwidth", "250", "kHz");
syncwordInput = initFormInput(3, "Sync Word", "2B", "hex");
deBitsInput = initSliderInput(4, "Coding Rate", 4, 8, 5);
sfInput = initSliderInput(5, "Spread Factor", 7, 12, 11);
preambleChirpsInput = initSliderInput(6, "Preamble Chirps", 4, 32, 16);
txPowInput = initFormInput(7, "TX Power", "27", "dBm");
/*
lv_obj_add_event_cb(frequencyInput, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
LoraParameters* params = (LoraParameters*)lv_event_get_user_data(e);
std::string buf(lv_textarea_get_text(input));
if (!buf.empty()) {
params->frequency = std::stof(buf);
}
}, LV_EVENT_VALUE_CHANGED, &loraParams);
lv_obj_add_event_cb(bandwidthInput, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
LoraParameters* params = (LoraParameters*)lv_event_get_user_data(e);
std::string buf(lv_textarea_get_text(input));
if (!buf.empty()) {
params->bandwidth = std::stof(buf);
}
}, LV_EVENT_VALUE_CHANGED, &loraParams);
lv_obj_add_event_cb(syncwordInput, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
LoraParameters* params = (LoraParameters*)lv_event_get_user_data(e);
std::string buf(lv_textarea_get_text(input));
if (!buf.empty()) {
std::stringstream ss;
ss << std::hex << buf;
ss >> params->syncWord;
}
}, LV_EVENT_VALUE_CHANGED, &loraParams);
lv_obj_add_event_cb(preambleChirpsInput, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
LoraParameters* params = (LoraParameters*)lv_event_get_user_data(e);
params->preambleLength = lv_slider_get_value(input);
}, LV_EVENT_VALUE_CHANGED, &loraParams);
lv_obj_add_event_cb(deBitsInput, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
LoraParameters* params = (LoraParameters*)lv_event_get_user_data(e);
params->deBits = lv_slider_get_value(input);
}, LV_EVENT_VALUE_CHANGED, &loraParams);
lv_obj_add_event_cb(sfInput, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
LoraParameters* params = (LoraParameters*)lv_event_get_user_data(e);
params->spreadFactor = lv_slider_get_value(input);
}, LV_EVENT_VALUE_CHANGED, &loraParams);
lv_obj_add_event_cb(txPowInput, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
LoraParameters* params = (LoraParameters*)lv_event_get_user_data(e);
std::string buf(lv_textarea_get_text(input));
if (!buf.empty()) {
params->power = std::stoi(buf);
}
}, LV_EVENT_VALUE_CHANGED, &loraParams);
*/
/*
if (loraDevNames.size() > 0) {
loraDevice = loraService->getDevice(loraDevNames[0]);
if (loraDevice)
{
using State = hal::lora::LoraDevice::State;
switch (loraDevice->getState()) {
case State::PendingOn:
case State::On:
setParameters(loraDevice->getParameters());
disableForm();
lv_obj_add_state(loraDeviceOn, LV_STATE_CHECKED);
break;
case State::Error:
case State::PendingOff:
case State::Off:
default:
break;
}
} else {
TT_LOG_E(TAG, "Attempted to load device \"%s\", what is happening!?", loraDevNames[0].c_str());
}
}*/
lv_obj_add_event_cb(loraDeviceOn, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
LoraView* self = (LoraView*)lv_event_get_user_data(e);
if (lv_obj_has_state(input, LV_STATE_CHECKED)) {
self->enableDevice();
} else {
self->disableDevice();
}
}, LV_EVENT_VALUE_CHANGED, this);
}
void disableForm()
{
lv_obj_add_state(loraDeviceInput, LV_STATE_DISABLED);
lv_obj_add_state(frequencyInput, LV_STATE_DISABLED);
lv_obj_add_state(bandwidthInput, LV_STATE_DISABLED);
lv_obj_add_state(syncwordInput, LV_STATE_DISABLED);
lv_obj_add_state(deBitsInput, LV_STATE_DISABLED);
lv_obj_add_state(preambleChirpsInput, LV_STATE_DISABLED);
lv_obj_add_state(txPowInput, LV_STATE_DISABLED);
}
void enableForm()
{
lv_obj_clear_state(loraDeviceInput, LV_STATE_DISABLED);
lv_obj_clear_state(frequencyInput, LV_STATE_DISABLED);
lv_obj_clear_state(bandwidthInput, LV_STATE_DISABLED);
lv_obj_clear_state(syncwordInput, LV_STATE_DISABLED);
lv_obj_clear_state(deBitsInput, LV_STATE_DISABLED);
lv_obj_clear_state(preambleChirpsInput, LV_STATE_DISABLED);
lv_obj_clear_state(txPowInput, LV_STATE_DISABLED);
}
public:
//std::shared_ptr<hal::lora::LoraDevice> loraDevice;
lv_obj_t* container;
lv_obj_t* loraDeviceOn;
lv_obj_t* loraDeviceInput;
lv_obj_t* frequencyInput;
lv_obj_t* bandwidthInput;
lv_obj_t* syncwordInput;
lv_obj_t* deBitsInput;
lv_obj_t* sfInput;
lv_obj_t* preambleChirpsInput;
lv_obj_t* txPowInput;
LoraView(lv_obj_t *parent) {
queryLoraDevs();
initUi(parent);
setParameters(
869.525,
250.0,
0x2B,
16,
5,
11,
22
);
}
void onDeviceActivation(DeviceActivationCallback cb) {
cbDevActive = cb;
}
void onDeviceDeactivation(DeviceActivationCallback cb) {
cbDevInactive = cb;
}
void setParameters(float frequency, float bandwidth, uint8_t syncWord, uint16_t preambleLength, uint8_t codingRate, uint8_t spreadFactor, int8_t power) {
std::string buf;
buf = std::format("{:.6f}", frequency);
lv_textarea_set_text(frequencyInput, buf.c_str());
buf = std::format("{:.2f}", bandwidth);
lv_textarea_set_text(bandwidthInput, buf.c_str());
buf = std::format("{:X}", syncWord);
lv_textarea_set_text(syncwordInput, buf.c_str());
lv_slider_set_value(preambleChirpsInput, preambleLength, LV_ANIM_OFF);
lv_slider_set_value(deBitsInput, codingRate, LV_ANIM_OFF);
lv_slider_set_value(sfInput, spreadFactor, LV_ANIM_OFF);
buf = std::format("{:d}", power);
lv_textarea_set_text(txPowInput, buf.c_str());
}
void configureFromForm() {
using enum tt::hal::radio::RadioDevice::Parameter;
using enum tt::hal::radio::RadioDevice::ParameterStatus;
std::string buffer;
int value = 0;
bool configured = true;
buffer = lv_textarea_get_text(frequencyInput);
if (!buffer.empty()) {
configured &= (loraDevice->setParameter(Frequency, std::stof(buffer)) == Success);
}
buffer = lv_textarea_get_text(bandwidthInput);
if (!buffer.empty()) {
configured &= (loraDevice->setParameter(Bandwidth, std::stof(buffer)) == Success);
}
buffer = lv_textarea_get_text(syncwordInput);
if (!buffer.empty()) {
uint8_t syncWord = 0;
std::stringstream ss(buffer);
ss >> std::hex >> syncWord;
configured &= (loraDevice->setParameter(SyncWord, std::stoi(buffer, nullptr, 16)) == Success);
}
value = lv_slider_get_value(deBitsInput);
configured &= (loraDevice->setParameter(CodingRate, value) == Success);
value = lv_slider_get_value(sfInput);
configured &= (loraDevice->setParameter(SpreadFactor, value) == Success);
value = lv_slider_get_value(preambleChirpsInput);
configured &= (loraDevice->setParameter(PreambleLength, value) == Success);
buffer = lv_textarea_get_text(txPowInput);
if (!buffer.empty()) {
configured &= (loraDevice->setParameter(Power, std::stof(buffer)) == Success);
}
}
void enableDevice()
{
loraDevice = loraDevs[lv_dropdown_get_selected(loraDeviceInput)];
if (loraDevice) {
disableForm();
loraDevice->setModulation(tt::hal::radio::RadioDevice::Modulation::LoRa);
configureFromForm();
loraDevice->start();
vTaskDelay(pdMS_TO_TICKS(500));
if (loraDevice->getState() != tt::hal::radio::RadioDevice::State::On) {
lv_obj_clear_state(loraDeviceOn, LV_STATE_CHECKED);
enableForm();
} else {
cbDevActive(loraDevice);
}
} else {
lv_obj_clear_state(loraDeviceOn, LV_STATE_CHECKED);
}
}
void disableDevice()
{
if (loraDevice) {
loraDevice->stop();
cbDevInactive(loraDevice);
}
enableForm();
}
};
class ChirpChatterApp : public App {
lv_obj_t* sidebar = nullptr;
lv_obj_t* mainView = nullptr;
lv_obj_t* progressBar = nullptr;
lv_obj_t* progressText = nullptr;
lv_obj_t* messageList = nullptr;
lv_obj_t* inputField = nullptr;
lv_obj_t* messageView = nullptr;
LoraView* loraView = nullptr;
hal::radio::RadioDevice::RxSubscriptionId rxSubId;
std::shared_ptr<tt::hal::radio::RadioDevice> loraDevice;
template<CCViews T>
lv_obj_t* createSidebarButton(lv_obj_t* parent, const char* image_file) {
auto* sidebar_button = lv_button_create(parent);
lv_obj_set_size(sidebar_button, 32, 32);
lv_obj_align(sidebar_button, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_style_pad_all(sidebar_button, 0, 0);
//lv_obj_set_style_pad_top(sidebar_button, 36, 0);
lv_obj_set_style_shadow_width(sidebar_button, 0, 0);
lv_obj_set_style_border_width(sidebar_button, 0, 0);
lv_obj_set_style_bg_opa(sidebar_button, 0, LV_PART_MAIN);
auto* button_image = lv_image_create(sidebar_button);
lv_image_set_src(button_image, image_file);
lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, LV_STATE_DEFAULT);
// Ensure buttons are still tappable when asset fails to load
lv_obj_set_size(button_image, 32, 32);
static lv_style_t style_focus;
lv_style_init(&style_focus);
lv_style_set_outline_width(&style_focus, 0);
lv_obj_add_style(sidebar_button, &style_focus, LV_PART_MAIN | LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
lv_obj_add_style(button_image, &style_focus, LV_PART_MAIN | LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
lv_obj_add_event_cb(button_image, changeViewHandler<T>, LV_EVENT_SHORT_CLICKED, (void*)this);
lv_obj_add_event_cb(sidebar_button, changeViewHandler<T>, LV_EVENT_SHORT_CLICKED, (void*)this);
lv_obj_add_event_cb(sidebar_button, buttonRecolorFocus, LV_EVENT_FOCUSED, button_image);
lv_obj_add_event_cb(sidebar_button, buttonRecolorDefocus, LV_EVENT_DEFOCUSED, button_image);
return sidebar_button;
}
void addDummyMessage(const char* const message) {
auto* msg_container = lv_obj_create(messageList);
lv_obj_set_flex_flow(msg_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_grow(msg_container, 0);
lv_obj_set_style_pad_all(msg_container, 1, 0);
lv_obj_add_flag(msg_container, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t* msg_label = lv_label_create(msg_container);
lv_label_set_text(msg_label, message);
lv_obj_set_width(msg_label, lv_pct(100));
lv_label_set_long_mode(msg_label, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_align(msg_label, LV_TEXT_ALIGN_LEFT, 0);
lv_obj_set_style_pad_all(msg_label, 0, 0);
lv_obj_t* msg_info = lv_label_create(msg_container);
lv_label_set_text(msg_info, "RX/2024-07-06+15:04");
lv_obj_set_width(msg_info, lv_pct(100));
lv_label_set_long_mode(msg_info, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_align(msg_info, LV_TEXT_ALIGN_LEFT, 0);
lv_obj_set_style_pad_all(msg_info, 0, 0);
lv_obj_set_style_text_font(msg_info, &lv_font_montserrat_10, 0);
lv_obj_set_width(msg_container, lv_pct(100));
lv_obj_set_height(msg_container, LV_SIZE_CONTENT);
lv_obj_scroll_to_y(messageList, lv_obj_get_scroll_y(messageList) + 1000, LV_ANIM_ON);
/*auto* group = lv_group_get_default();
lv_group_add_obj(group, msg_container);*/
}
void addPacketMessage(const hal::radio::RxPacket& packet) {
auto* msg_container = lv_obj_create(messageList);
lv_obj_set_flex_flow(msg_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_grow(msg_container, 0);
lv_obj_set_style_pad_all(msg_container, 1, 0);
lv_obj_add_flag(msg_container, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t* msg_label = lv_label_create(msg_container);
std::string messageBuf = "";
if (isPrintableData(packet.data)) {
messageBuf = std::string(packet.data.begin(), packet.data.end());
} else {
messageBuf = hexdump(packet.data);
}
lv_label_set_text(msg_label, messageBuf.c_str());
lv_obj_set_width(msg_label, lv_pct(100));
lv_label_set_long_mode(msg_label, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_align(msg_label, LV_TEXT_ALIGN_LEFT, 0);
lv_obj_set_style_pad_all(msg_label, 0, 0);
lv_obj_t* msg_info = lv_label_create(msg_container);
auto t = std::time(nullptr);
auto tm = *std::localtime(&t);
std::stringstream ss;
ss << "RX/RAW ";
ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
ss << " ";
ss << "RSSI:" << packet.rssi;
ss << " ";
ss << "SNR:" << packet.snr;
lv_label_set_text(msg_info, ss.str().c_str());
lv_obj_set_width(msg_info, lv_pct(100));
lv_label_set_long_mode(msg_info, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_align(msg_info, LV_TEXT_ALIGN_LEFT, 0);
lv_obj_set_style_pad_all(msg_info, 0, 0);
lv_obj_set_style_text_font(msg_info, &lv_font_montserrat_10, 0);
lv_obj_set_width(msg_container, lv_pct(100));
lv_obj_set_height(msg_container, LV_SIZE_CONTENT);
lv_obj_scroll_to_y(messageList, lv_obj_get_scroll_y(messageList) + 1000, LV_ANIM_ON);
/*auto* group = lv_group_get_default();
* lv_group_add_obj(group, msg_container);*/
}
public:
void onCreate(AppContext& appContext) override {
#ifdef ESP_PLATFORM
esp_log_level_set("*", ESP_LOG_DEBUG);
#endif
}
void onDestroy(AppContext& appContext) override {
}
void onShow(AppContext& context, lv_obj_t* parent) override {
static lv_coord_t grid_col_dsc[] = {36, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static lv_coord_t grid_row_dsc[] = {40, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
lv_obj_t * grid = lv_obj_create(parent);
lv_obj_set_grid_dsc_array(grid, grid_col_dsc, grid_row_dsc);
lv_obj_set_size(grid, lv_pct(100), lv_pct(100));
static lv_style_t style_grid;
lv_style_init(&style_grid);
lv_style_set_pad_row(&style_grid, 0);
lv_style_set_pad_column(&style_grid, 0);
lv_style_set_pad_all(&style_grid, 0);
lv_obj_add_style(grid, &style_grid, LV_PART_MAIN);
// Create toolbar
auto* toolbar = tt::lvgl::toolbar_create(grid, "Welcome to ChirpChatter!");
lv_obj_set_size(toolbar, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_grid_cell(toolbar, LV_GRID_ALIGN_STRETCH, 0, 2,
LV_GRID_ALIGN_STRETCH, 0, 1);
progressText = lv_obj_get_child_by_type(toolbar, 0, &lv_label_class);
lv_obj_align(progressText, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_set_style_text_font(progressText, &lv_font_montserrat_12, 0);
lv_obj_set_size(progressText, lv_pct(75), LV_SIZE_CONTENT);
// Create sidebar
sidebar = lv_obj_create(grid);
lv_obj_set_size(sidebar, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_grid_cell(sidebar, LV_GRID_ALIGN_START, 0, 1,
LV_GRID_ALIGN_START, 1, 1);
lv_obj_set_scrollbar_mode(sidebar, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_pad_all(sidebar, 2, 0);
lv_obj_set_style_border_width(sidebar, 0, 0);
lv_obj_set_flex_flow(sidebar, LV_FLEX_FLOW_COLUMN);
// Create progress bar
progressBar = lv_bar_create(toolbar);
//lv_obj_set_flex_flow(toolbar, LV_FLEX_FLOW_COLUMN);
lv_obj_align(progressBar, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_size(progressBar, lv_pct(25), 36);
lv_obj_set_style_radius(progressBar, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_radius(progressBar, 0, LV_PART_INDICATOR | LV_STATE_DEFAULT);
lv_bar_set_range(progressBar, 0, 100);
lv_bar_set_value(progressBar, 100, LV_ANIM_OFF);
auto paths = context.getPaths();
auto icon_msgs_path = paths->getSystemPathLvgl("icon_msgs.png");
createSidebarButton<CCView_Msgs>(sidebar, icon_msgs_path.c_str());
auto icon_lora_path = paths->getSystemPathLvgl("icon_lora.png");
createSidebarButton<CCView_LoraSettings>(sidebar, icon_lora_path.c_str());
auto icon_proto_path = paths->getSystemPathLvgl("icon_proto.png");
createSidebarButton<CCView_ProtoSettings>(sidebar, icon_proto_path.c_str());
// Main view
/*mainView = lv_obj_create(grid);
lv_obj_set_size(mainView, lv_pct(100), lv_pct(100));
lv_obj_set_grid_cell(mainView, LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_STRETCH, 1, 1);
//lv_obj_set_flex_flow(mainView, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_bg_color(mainView, lv_color_hex(0x00FF00), 0);
lv_obj_set_style_border_width(mainView, 0, 0);
lv_obj_set_style_pad_all(mainView, 0, 0);
*/
// Message view
messageView = lv_obj_create(grid);
//lv_obj_set_size(messageView, lv_disp_get_hor_res(NULL) - 40, lv_disp_get_ver_res(NULL) - toolbar_height*2);
lv_obj_set_size(messageView, lv_pct(100), lv_pct(100));
//lv_obj_set_flex_flow(messageView, LV_FLEX_FLOW_COLUMN);
//lv_obj_set_flex_grow(messageView, 1);
lv_obj_set_size(messageView, lv_pct(100), lv_pct(100));
lv_obj_set_grid_cell(messageView, LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_STRETCH, 1, 1);
lv_obj_set_style_pad_all(messageView, 0, 0);
messageList = lv_obj_create(messageView);
lv_obj_set_size(messageList, lv_pct(100), lv_pct(80));
lv_obj_set_flex_flow(messageList, LV_FLEX_FLOW_COLUMN);
lv_obj_align(messageList, LV_ALIGN_TOP_MID, 0, 0);
///lv_obj_set_style_bg_color(mainView, lv_color_hex(0xFF0000), 0);
lv_obj_set_style_border_width(messageList, 0, 0);
lv_obj_set_style_pad_all(messageList, 0, 0);
lv_obj_add_flag(messageList, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLL_ON_FOCUS));
auto* group = lv_group_get_default();
lv_group_add_obj(group, messageList);
lv_obj_add_event_cb(messageList, buttonRecolorFocus, LV_EVENT_FOCUSED, nullptr);
lv_obj_add_event_cb(messageList, buttonRecolorDefocus, LV_EVENT_DEFOCUSED, nullptr);
/*
messageList = lv_page_create(messageView, nullptr);
lv_obj_set_size(messageList, lv_pct(100), lv_pct(80));
lv_obj_set_style_border_width(messageList, 0, 0);
lv_obj_set_style_pad_all(messageList, 0, 0);
*/
// Input panel
auto* input_panel = lv_obj_create(messageView);
lv_obj_set_flex_flow(input_panel, LV_FLEX_FLOW_ROW);
lv_obj_set_size(input_panel, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_align(input_panel, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_flex_align(input_panel, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(input_panel, 5, 0);
// Input field
inputField = lv_textarea_create(input_panel);
lv_obj_set_flex_grow(inputField, 1);
lv_obj_set_height(inputField, LV_PCT(100));
lv_textarea_set_placeholder_text(inputField, "Type a message...");
lv_textarea_set_one_line(inputField, true);
// Send button
auto* send_btn = lv_btn_create(input_panel);
lv_obj_set_size(send_btn, 50, LV_SIZE_CONTENT);
//lv_obj_add_event_cb(send_btn, onSendClicked, LV_EVENT_CLICKED, this);
auto* btn_label = lv_label_create(send_btn);
lv_label_set_text(btn_label, "SEND");
lv_obj_center(btn_label);
lv_obj_set_flex_grow(messageList, 1);
lv_obj_set_flex_grow(input_panel, 0);
//lv_obj_set_style_bg_color(messageList, lv_color_hex(0xFF0000), 0);
//lv_obj_set_style_bg_color(input_panel, lv_color_hex(0x00FF00), 0);
//addDummyMessage("HELLO CHIRPCHAT!");
//addDummyMessage("How's biz?");
//addDummyMessage("Test");
//addDummyMessage("Test empfangen in Linz");
// LoRa settings view
loraView = new LoraView(grid);
lv_obj_set_grid_cell(loraView->container, LV_GRID_ALIGN_STRETCH, 1, 1,
LV_GRID_ALIGN_STRETCH, 1, 1);
//loraView->onDeviceActivation(std::bind(&ChirpChatterApp::onDeviceActivation, this));
//loraView->onDeviceDeactivation(std::bind(&ChirpChatterApp::onDeviceDeactivation, this));
loraView->onDeviceActivation([this](std::shared_ptr<tt::hal::radio::RadioDevice> dev) { this->onDeviceActivation(dev); });
loraView->onDeviceDeactivation([this](std::shared_ptr<tt::hal::radio::RadioDevice> dev) { this->onDeviceDeactivation(dev); });
lv_obj_add_event_cb(send_btn, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
ChirpChatterApp* self = (ChirpChatterApp*)lv_event_get_user_data(e);
self->sendMessage();
}, LV_EVENT_SHORT_CLICKED, (void*)this);
changeView(CCView_Msgs);
}
void onRxPacket(hal::Device::Id id, const hal::radio::RxPacket& packet) {
addPacketMessage(packet);
}
void onDeviceActivation(std::shared_ptr<tt::hal::radio::RadioDevice> dev) {
rxSubId = dev->subscribeRx([this](hal::Device::Id id, const hal::radio::RxPacket& packet) {
this->onRxPacket(id, packet);
});
loraDevice = dev;
std::ostringstream oss;
oss << "Device \"" << dev->getName() << "\" online";
lv_label_set_text(progressText, oss.str().c_str());
}
void onDeviceDeactivation(std::shared_ptr<tt::hal::radio::RadioDevice> dev) {
dev->unsubscribeRx(rxSubId);
loraDevice = nullptr;
lv_label_set_text(progressText, "Offline");
}
void sendMessage() {
std::string message = lv_textarea_get_text(inputField);
std::vector<uint8_t> 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<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);
});
}
void onTxStatus(hal::radio::RadioDevice::TxId id, hal::radio::RadioDevice::TransmissionState state) {
using enum hal::radio::RadioDevice::TransmissionState;
switch (state) {
case Queued:
lv_label_set_text(progressText, "Message queued...");
lv_bar_set_value(progressBar, 25, LV_ANIM_ON);
break;
case PendingTransmit:
lv_label_set_text(progressText, "Message transmitting...");
lv_bar_set_value(progressBar, 50, LV_ANIM_ON);
break;
case Transmitted:
lv_label_set_text(progressText, "Message transmitted!\nReturn to receive.");
lv_bar_set_value(progressBar, 100, LV_ANIM_ON);
break;
case Timeout:
lv_label_set_text(progressText, "Message transmit timed out!");
lv_bar_set_value(progressBar, 100, LV_ANIM_ON);
break;
case Error:
lv_label_set_text(progressText, "Error transmitting message!");
lv_bar_set_value(progressBar, 100, LV_ANIM_ON);
break;
}
}
void changeView(const CCViews view) {
lv_obj_add_flag(messageView, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(loraView->container, LV_OBJ_FLAG_HIDDEN);
switch (view) {
case CCView_Msgs:
lv_obj_clear_flag(messageView, LV_OBJ_FLAG_HIDDEN);
break;
case CCView_LoraSettings:
lv_obj_clear_flag(loraView->container, LV_OBJ_FLAG_HIDDEN);
break;
default:
break;
}
}
~ChirpChatterApp() override = default;
};
extern const AppManifest manifest = {
.id = "ChirpChatter",
.name = "ChirpChatter",
.icon = TT_ASSETS_APP_ICON_CHAT,
.createApp = create<ChirpChatterApp>
};
}
//#endif

View File

@ -0,0 +1,99 @@
#include "Tactility/hal/radio/RadioDevice.h"
#include <cstring>
namespace tt::hal::radio {
constexpr const char* TAG = "RadioDevice";
bool RadioDevice::setModulation(const RadioDevice::Modulation newModulation) {
// A bool is chosen over an enum class because:
// - this is not tied to user input and
// - the programmer can infer why it didn't work using
// other methods such as getState() and canTransmit/Receive()
const auto state = getState();
if ((state == State::PendingOn) || (state == State::On)) {
return false;
} else if (!(canTransmit(newModulation) || canReceive(newModulation))) {
return false;
} else {
auto lock = mutex.asScopedLock();
lock.lock();
modulation = newModulation;
}
return true;
}
RadioDevice::Modulation RadioDevice::getModulation() const {
auto lock = mutex.asScopedLock();
lock.lock();
return modulation;
}
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 None:
return "none";
case Fsk:
return "FSK";
case LoRa:
return "LoRa";
case LrFhss:
return "LR-FHSS";
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);
case NarrowGrid:
return TT_STRINGIFY(NarrowGrid);
default:
return "Unknown";
}
}
} // namespace tt::hal::radio

View File

@ -0,0 +1,60 @@
#include "Tactility/hal/radio/Unit.h"
#include <cstring>
namespace tt::hal::radio {
std::string Unit::toString() const {
return std::string(toString(prefix))+std::string(toString(unit));
}
const char* toString(Unit::Prefix prefix) {
using enum Unit::Prefix;
switch (prefix) {
case Femto:
return "f";
case Pico:
return "p";
case Nano:
return "n";
case Milli:
return "m";
case None:
return "";
case Kilo:
return "k";
case Mega:
return "M";
case Giga:
return "G";
case Terra:
return "T";
case Peta:
return "P";
}
return "?";
}
const char* toString(Unit::Name unit) {
using enum Unit::Name;
switch (unit) {
case None:
return "";
case BitsPerSecond:
return "bps";
case BytesPerSecond:
return "Bps";
case Herz:
return "Hz";
case Decibel:
return "dB";
case DecibelMilliwatts:
return "dBm";
}
return "?";
}
}