diff --git a/ExternalApps/RadioSet/main/Source/TermView.cpp b/ExternalApps/RadioSet/main/Source/TermView.cpp new file mode 100644 index 00000000..7d2f090c --- /dev/null +++ b/ExternalApps/RadioSet/main/Source/TermView.cpp @@ -0,0 +1,485 @@ +#include "TermView.h" + +#include +#include + +#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(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(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(); + } +} + diff --git a/ExternalApps/RadioSet/main/Source/TermView.h b/ExternalApps/RadioSet/main/Source/TermView.h new file mode 100644 index 00000000..0acb8b04 --- /dev/null +++ b/ExternalApps/RadioSet/main/Source/TermView.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include +#include + +#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 radios; + RadioHandle selectedRadio = nullptr; + Dequeue inMsgQueue; + LinkedList 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(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); +};