#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(); } }