RadioSet: Forgot yet another set of source files, it's TermView

This commit is contained in:
Dominic Höglinger 2025-10-08 17:54:40 +02:00
parent 34c09e6b1d
commit 38e5f51511
2 changed files with 595 additions and 0 deletions

View File

@ -0,0 +1,485 @@
#include "TermView.h"
#include <tt_kernel.h>
#include <time.h>
#include "Str.h"
#include "Utils.h"
extern lv_font_t lv_font_unscii_8;
extern lv_font_t lv_font_ptmono_12;
class Message {
public:
uint8_t* data;
size_t size;
Message(const uint8_t* packetData, const size_t packetSize) {
size = packetSize;
data = new uint8_t[size];
memcpy(data, packetData, size);
}
virtual lv_obj_t* makeMessageBox(lv_obj_t* parent) = 0;
virtual void refresh() = 0;
virtual ~Message() {
delete[] data;
}
};
class RxMessage : public Message {
public:
RadioHandle radio;
float rssi;
float snr;
lv_obj_t* msgBox = nullptr;
RxMessage(RadioHandle radio, const RadioRxPacket* packet)
: Message(packet->data, packet->size)
, radio(radio) {
rssi = packet->rssi;
snr = packet->snr;
}
static void applyStyle(lv_obj_t* obj) {
static lv_style_t style;
static bool init = false;
if (!init) {
lv_style_init(&style);
lv_style_set_border_color(&style, lv_color_make(0x0A, 0x5C, 0x36));
lv_style_set_border_side(&style, LV_BORDER_SIDE_LEFT);
lv_style_set_border_width(&style, 5);
lv_style_set_pad_all(&style, 1);
lv_style_set_margin_bottom(&style, 2);
lv_style_set_radius(&style, 0);
}
lv_obj_add_style(obj, &style, LV_PART_MAIN);
}
virtual lv_obj_t* makeMessageBox(lv_obj_t* parent) {
char timebuffer[32] = {0};
Str info;
Str message;
bool is_txt = false;
auto* msg_container = lv_obj_create(parent);
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);
is_txt = isPrintable(data, size);
if (is_txt) {
message.set((const char*)data, ((const char*)data + size));
} else {
hexdump(message, data, size);
}
lv_label_set_text(msg_label, message.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_set_style_text_font(msg_label, &lv_font_ptmono_12, 0);
lv_obj_t* msg_info = lv_label_create(msg_container);
// Get time
time_t now = time(nullptr);
struct tm* tm_info = localtime(&now);
strftime(timebuffer, sizeof(timebuffer), "%H:%M:%S", tm_info);
info.append("RX/");
// Get device name and modulation
auto modulation = MODULATION_NONE;
if (radio) {
info.append(tt_hal_radio_get_name(radio));
modulation = tt_hal_radio_get_modulation(radio);
info.append(" ");
} else {
info.append("?");
}
info.append(toString(modulation));
info.append(is_txt ? "/TXT " : "/BIN ");
info.append(timebuffer);
info.appendf(" SI:%.2f SN:%.2f PS:%d", rssi, snr, size);
lv_label_set_text(msg_info, info.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_unscii_8, 0);
lv_obj_set_width(msg_container, lv_pct(100));
lv_obj_update_layout(msg_label);
lv_obj_update_layout(msg_info);
lv_obj_set_height(msg_container, LV_SIZE_CONTENT);
applyStyle(msg_container);
msgBox = msg_container;
//clownvomit(msg_container);
return msg_container;
}
virtual void refresh() {}
virtual ~RxMessage() {
if (msgBox) {
lv_obj_del(msgBox);
}
}
};
class TxMessage : public Message {
public:
RadioHandle radio;
RadioTxState lastState;
uint32_t address;
lv_obj_t* msgBox = nullptr;
lv_obj_t* statusLabel = nullptr;
TxMessage(RadioHandle radio, const RadioTxPacket* packet)
: Message(packet->data, packet->size)
, radio(radio) {
address = packet->address;
}
static void updateStatusFor(RadioTxId id, RadioTxState state, void* ctx) {
static_cast<TxMessage*>(ctx)->updateStatus(id, state);
}
static void applyStyle(lv_obj_t* obj) {
static lv_style_t style;
static bool init = false;
if (!init) {
lv_style_init(&style);
lv_style_set_border_color(&style, lv_color_make(0xFF, 0xBF, 0x00));
lv_style_set_border_side(&style, LV_BORDER_SIDE_LEFT);
lv_style_set_border_width(&style, 5);
lv_style_set_pad_all(&style, 1);
lv_style_set_margin_bottom(&style, 2);
lv_style_set_radius(&style, 0);
}
lv_obj_add_style(obj, &style, LV_PART_MAIN);
}
virtual lv_obj_t* makeMessageBox(lv_obj_t* parent) {
char timebuffer[32] = {0};
Str info;
Str message;
auto* msg_container = lv_obj_create(parent);
static lv_coord_t col_dsc[] = {LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static lv_coord_t row_dsc[] = {LV_GRID_CONTENT, /* row 0 label 0 */
LV_GRID_CONTENT, /* row 1 labels 1+2 */
LV_GRID_TEMPLATE_LAST};
lv_obj_set_grid_dsc_array(msg_container, col_dsc, row_dsc);
lv_obj_set_style_pad_all(msg_container, 1, 0);
lv_obj_t* msg_label = lv_label_create(msg_container);
if (isPrintable(data, size)) {
message.set((const char*)data, ((const char*)data + size));
} else {
hexdump(message, data, size);
}
lv_label_set_text(msg_label, message.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_set_style_text_font(msg_label, &lv_font_ptmono_12, 0);
lv_obj_t* msg_info = lv_label_create(msg_container);
// Get time
time_t now = time(nullptr);
struct tm* tm_info = localtime(&now);
strftime(timebuffer, sizeof(timebuffer), "%H:%M:%S", tm_info);
info.append("TX/");
// Get device name and modulation
auto modulation = MODULATION_NONE;
if (radio) {
info.append(tt_hal_radio_get_name(radio));
modulation = tt_hal_radio_get_modulation(radio);
info.append(" ");
} else {
info.append("?");
}
bool is_txt = isPrintable(data, size);
info.append(toString(modulation));
info.append(is_txt ? "/TXT " : "/BIN ");
info.append(timebuffer);
info.appendf(" PS:%d", size);
lv_label_set_text(msg_info, info.c_str());
lv_obj_set_width(msg_info, LV_SIZE_CONTENT);
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_unscii_8, 0);
statusLabel = lv_label_create(msg_container);
lv_label_set_text(statusLabel, "NEW");
lv_obj_set_width(statusLabel, LV_SIZE_CONTENT);
lv_label_set_long_mode(statusLabel, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_align(statusLabel, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_set_style_pad_all(statusLabel, 0, 0);
lv_obj_set_style_text_font(statusLabel, &lv_font_unscii_8, 0);
lv_obj_update_layout(msg_label);
lv_obj_update_layout(msg_info);
lv_obj_update_layout(statusLabel);
/* attach items (child index, column, row, column-span, row-span) */
lv_obj_set_grid_cell(msg_label, LV_GRID_ALIGN_STRETCH, 0, 1,
LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_set_grid_cell(msg_info, LV_GRID_ALIGN_START, 0, 1,
LV_GRID_ALIGN_CENTER, 1, 1);
lv_obj_set_grid_cell(statusLabel, LV_GRID_ALIGN_END, 0, 1,
LV_GRID_ALIGN_CENTER, 1, 1);
lv_obj_set_width(msg_container, lv_pct(100));
lv_obj_set_height(msg_container, LV_SIZE_CONTENT);
applyStyle(msg_container);
msgBox = msg_container;
return msg_container;
}
void updateStatus(RadioTxId id, RadioTxState state) {
lastState = state;
}
virtual void refresh() {
const auto* status_txt = toString(lastState);
if (statusLabel) {
auto indicatorColor = lv_color_make(0xFF, 0x00, 0x00);
switch (lastState) {
case RADIO_TX_QUEUED:
// Dark Amber
indicatorColor = lv_color_make(0xD5, 0x36, 0x00);
break;
case RADIO_TX_PENDING_TRANSMIT:
// Light Amber
indicatorColor = lv_color_make(0xFF, 0xD2, 0x2B);
break;
case RADIO_TX_TRANSMITTED:
// Green
indicatorColor = lv_color_make(0x8F, 0xCE, 0x00);
break;
case RADIO_TX_TIMEOUT:
// Purple
indicatorColor = lv_color_make(0xC9, 0x00, 0x76);
break;
case RADIO_TX_ERROR:
// Full Red
default:
break;
}
lv_obj_set_style_bg_opa(statusLabel, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(statusLabel, indicatorColor, 0);
lv_obj_set_style_text_color(statusLabel, lv_color_make(0x00, 0x00, 0x00), 0);
lv_obj_set_style_pad_all(statusLabel, 1, 0);
lv_label_set_text(statusLabel, status_txt);
}
}
virtual ~TxMessage() {
if (msgBox) {
lv_obj_del(msgBox);
}
}
};
void TermView::initUi(lv_obj_t* parent) {
mainPanel = lv_obj_create(parent);
lv_obj_set_size(mainPanel, lv_pct(100), lv_pct(100));
lv_obj_set_flex_flow(mainPanel, LV_FLEX_FLOW_COLUMN);
lv_obj_align(mainPanel, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_style_border_width(mainPanel, 0, 0);
lv_obj_set_style_pad_all(mainPanel, 0, 0);
messageList = lv_obj_create(mainPanel);
lv_obj_set_size(messageList, lv_pct(100), lv_pct(100));
lv_obj_set_flex_flow(messageList, LV_FLEX_FLOW_COLUMN);
lv_obj_align(messageList, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_style_border_width(messageList, 0, 0);
lv_obj_set_style_pad_all(messageList, 0, 0);
make_scrollable(messageList);
// Input panel
auto* input_panel = lv_obj_create(mainPanel);
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
sendButton = lv_btn_create(input_panel);
lv_obj_set_size(sendButton, 50, LV_SIZE_CONTENT);
//lv_obj_add_event_cb(send_btn, onSendClicked, LV_EVENT_CLICKED, this);
auto* btn_label = lv_label_create(sendButton);
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_add_event_cb(sendButton, [](lv_event_t * e) {
lv_obj_t* input = lv_event_get_target_obj(e);
auto* self = (TermView*)lv_event_get_user_data(e);
self->sendMessage();
}, LV_EVENT_SHORT_CLICKED, (void*)this);
renderMessagesTimer = lv_timer_create([](lv_timer_t* timer) {
auto self = reinterpret_cast<TermView*>(lv_timer_get_user_data(timer));
self->updateMessages();
}, 500, this);
}
void TermView::setVisible(bool visible) {
if (visible) {
lv_obj_clear_flag(mainPanel, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(mainPanel, LV_OBJ_FLAG_HIDDEN);
}
}
void TermView::addRadio(RadioHandle radio) {
auto sub_id = tt_hal_radio_subscribe_receive(radio, [](DeviceId id, const RadioRxPacket* packet, void* ctx) {
auto* self = (TermView*)ctx;
self->pushRxMessage(id, packet);
}, this);
radios.pushBack(RadioItem{
.handle = radio,
.rxSubId = sub_id
});
// TODO: Remove once radio select dropdown is there
selectedRadio = radio;
}
void TermView::cleanupRadios() {
for (auto iter = radios.begin(); iter != radios.end(); iter++) {
tt_hal_radio_unsubscribe_receive(iter->handle, iter->rxSubId);
}
}
void TermView::sendMessage() {
auto* message = lv_textarea_get_text(inputField);
auto txPacket = RadioTxPacket {
.data = (uint8_t*)message, // I am NOT allocating for this
.size = strlen(message),
.address = 0
};
auto* txBox = new TxMessage(selectedRadio, &txPacket);
tt_mutex_lock(mutex, portMAX_DELAY);
inMsgQueue.pushBack(txBox);
signalHasMsgs = true;
tt_mutex_unlock(mutex);
tt_hal_radio_transmit(selectedRadio, txPacket, TxMessage::updateStatusFor, txBox);
}
void TermView::pushRxMessage(DeviceId device, const RadioRxPacket* packet) {
auto radio = getRadioForDeviceId(device);
tt_mutex_lock(mutex, portMAX_DELAY);
auto* rxBox = new RxMessage(radio, packet);
inMsgQueue.pushBack(rxBox);
signalHasMsgs = true;
tt_mutex_unlock(mutex);
}
void TermView::run() {
while(!getSignalStop()) {
while(!getSignalHasMsg() && !getSignalStop()) {
tt_kernel_delay_millis(100);
}
if (getSignalStop()) {
break;
}
tt_mutex_lock(lvglMutex, portMAX_DELAY);
while (!inMsgQueue.empty()) {
auto* message = inMsgQueue.front();
inMsgQueue.popFront();
if (messages.size() >= MESSAGE_LIMIT) {
delete messages.back();
messages.popBack();
}
message->makeMessageBox(messageList);
messages.pushFront(message);
}
lv_obj_update_layout(messageList);
tt_mutex_unlock(lvglMutex);
clearSignalHasMsg();
}
}
void TermView::updateMessages() {
bool at_bottom = (lv_obj_get_scroll_bottom(messageList) <= 0);
bool focused = lv_obj_has_state(messageList, LV_STATE_FOCUSED);
tt_mutex_lock(lvglMutex, portMAX_DELAY);
while (!inMsgQueue.empty()) {
auto* message = inMsgQueue.front();
inMsgQueue.popFront();
if (messages.size() >= MESSAGE_LIMIT) {
delete messages.back();
messages.popBack();
}
message->makeMessageBox(messageList);
messages.pushFront(message);
}
lv_obj_update_layout(messageList);
tt_mutex_unlock(lvglMutex);
// demorgan spinning in his grave for his teachings are for naught
//if (!(!at_bottom || focused)) {
if (at_bottom) {
lv_obj_scroll_to_y(messageList, LV_COORD_MAX, LV_ANIM_ON);
}
for (auto iter = messages.begin(); iter != messages.end(); iter++) {
(*iter)->refresh();
}
}

View File

@ -0,0 +1,110 @@
#pragma once
#include <lvgl.h>
#include <tt_hal_radio.h>
#include <tt_mutex.h>
#include <tt_thread.h>
#include "LinkedList.h"
#include "Dequeue.h"
class Message;
class TermView {
static size_t constexpr MESSAGE_LIMIT = 64;
struct RadioItem {
RadioHandle handle;
RadioRxSubscriptionId rxSubId;
};
ThreadHandle thread;
MutexHandle lvglMutex;
MutexHandle mutex;
bool signalStop = false;
bool signalHasMsgs = false;
LinkedList<RadioItem> radios;
RadioHandle selectedRadio = nullptr;
Dequeue<Message*> inMsgQueue;
LinkedList<Message*> messages;
lv_obj_t* mainPanel = nullptr;
lv_obj_t* messageList = nullptr;
lv_obj_t* inputField = nullptr;
lv_obj_t* sendButton = nullptr;
lv_timer_t* renderMessagesTimer = nullptr;
static int32_t threadMain(void* context) {
static_cast<TermView*>(context)->run();
return 0;
}
void initUi(lv_obj_t* parent);
void cleanupRadios();
void setSignalStop() {
tt_mutex_lock(mutex, portMAX_DELAY);
signalStop = true;
tt_mutex_unlock(mutex);
}
bool getSignalStop() {
//tt_mutex_lock(mutex, portMAX_DELAY);
auto s = signalStop;
//tt_mutex_unlock(mutex);
return s;
}
void setSignalHasMsg() {
tt_mutex_lock(mutex, portMAX_DELAY);
signalHasMsgs = true;
tt_mutex_unlock(mutex);
}
void clearSignalHasMsg() {
tt_mutex_lock(mutex, portMAX_DELAY);
signalHasMsgs = false;
tt_mutex_unlock(mutex);
}
bool getSignalHasMsg() {
tt_mutex_lock(mutex, portMAX_DELAY);
auto s = signalHasMsgs;
tt_mutex_unlock(mutex);
return s;
}
void run();
RadioHandle getRadioForDeviceId(DeviceId devid) {
for (auto iter = radios.begin(); iter != radios.end(); iter++) {
if (tt_hal_radio_get_device_id(iter->handle) == devid) {
return iter->handle;
}
}
return nullptr;
}
public:
TermView(MutexHandle lvglMutex, lv_obj_t* parent)
: lvglMutex(lvglMutex) {
initUi(parent);
thread = tt_thread_alloc_ext("RadioTerm.TermView", 2*4096, threadMain, this);
mutex = tt_mutex_alloc(MUTEX_TYPE_RECURSIVE);
//tt_thread_start(thread);
}
virtual ~TermView() {
setSignalStop();
tt_thread_join(thread, portMAX_DELAY);
tt_thread_free(thread);
cleanupRadios();
}
void setVisible(bool visible);
void addRadio(RadioHandle radio);
void sendMessage();
void updateMessages();
void pushRxMessage(DeviceId device, const RadioRxPacket* packet);
};