//#ifdef ESP_PLATFORM #include #include #include #include #include #include #include "Tactility/lvgl/LvglSync.h" #include #include #include #include #include #include #include #include #include #include #include #include #include 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 static void changeViewHandler(lv_event_t* e) { auto* self = static_cast(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& data) { return std::all_of(data.begin(), data.end(), [](uint8_t byte) { char c = static_cast(byte); return std::isprint(static_cast(c)); }); } std::string hexdump(const std::vector& 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(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(c))) return false; return true; } bool parseHexString(const std::string& hex, std::vector& 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((high << 4) | low)); } return true; } class LoraView { public: using DeviceActivationCallback = std::function)>; private: //using LoraParameters = tt::hal::lora::LoraParameters; std::vector loraDevNames; std::vector> loraDevs; std::shared_ptr loraDevice; DeviceActivationCallback cbDevActive; DeviceActivationCallback cbDevInactive; void queryLoraDevs() { auto radios = tt::hal::findDevices(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 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 loraDevice; template 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, LV_EVENT_SHORT_CLICKED, (void*)this); lv_obj_add_event_cb(sidebar_button, changeViewHandler, 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(sidebar, icon_msgs_path.c_str()); auto icon_lora_path = paths->getSystemPathLvgl("icon_lora.png"); createSidebarButton(sidebar, icon_lora_path.c_str()); auto icon_proto_path = paths->getSystemPathLvgl("icon_proto.png"); createSidebarButton(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 dev) { this->onDeviceActivation(dev); }); loraView->onDeviceDeactivation([this](std::shared_ptr 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 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 dev) { dev->unsubscribeRx(rxSubId); loraDevice = nullptr; lv_label_set_text(progressText, "Offline"); } void sendMessage() { std::string message = lv_textarea_get_text(inputField); std::vector 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(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 }; } //#endif