diff --git a/modules/ui_shared/tests/test_boot_network_independence_contract.cpp b/modules/ui_shared/tests/test_boot_network_independence_contract.cpp index 86e357b6..90ebc440 100644 --- a/modules/ui_shared/tests/test_boot_network_independence_contract.cpp +++ b/modules/ui_shared/tests/test_boot_network_independence_contract.cpp @@ -31,8 +31,12 @@ int main(int argc, char** argv) assert(boot.find("constexpr uint32_t kMinShowMs = 3000;") == std::string::npos); // The menu is constructed and finalized without a synchronous Wi-Fi call. - const std::size_t menu_pos = startup.find("initializeShell();"); - const std::size_t finish_pos = startup.find("finishStartup(waking_from_sleep);"); + // Search the startup entry point, not the helper definition. The shell + // call may be guarded so failed initialization cannot reach finalization. + const std::size_t run_pos = startup.find("void run()"); + assert(run_pos != std::string::npos); + const std::size_t menu_pos = startup.find("initializeShell()", run_pos); + const std::size_t finish_pos = startup.find("finishStartup(waking_from_sleep);", run_pos); assert(menu_pos != std::string::npos); assert(finish_pos != std::string::npos); assert(menu_pos < finish_pos); diff --git a/tools/web_simulator/native_chat.cpp b/tools/web_simulator/native_chat.cpp index 19f25666..481e7ebc 100644 --- a/tools/web_simulator/native_chat.cpp +++ b/tools/web_simulator/native_chat.cpp @@ -1,7 +1,7 @@ // Preview data and application navigation around the unchanged firmware chat views. -#include "ui/screens/chat/chat_message_list_components.h" -#include "ui/screens/chat/chat_conversation_components.h" #include "ui/screens/chat/chat_compose_components.h" +#include "ui/screens/chat/chat_conversation_components.h" +#include "ui/screens/chat/chat_message_list_components.h" #include "ui_presentation/chat/chat_workspace_snapshot.h" #include #include @@ -10,52 +10,97 @@ extern "C" void native_open_page(int page); static std::unique_ptr message_list; static std::unique_ptr conversation; static std::unique_ptr compose; -static chat::ConversationId selected(chat::ChannelId::PRIMARY,0x12345678); +static chat::ConversationId selected(chat::ChannelId::PRIMARY, 0x12345678); static std::vector sent_messages; -void preview_chat_exit() {compose.reset();conversation.reset();message_list.reset();} -namespace chat::ui::runtime { -bool requestCompose(const chat::ConversationId& id){selected=id;return true;} -void clearRequestedCompose(){} +void preview_chat_exit() +{ + compose.reset(); + conversation.reset(); + message_list.reset(); } -namespace chat::ui::shell { -void enter(void*,lv_obj_t*){native_open_page(5);} -void exit(void*,lv_obj_t*){preview_chat_exit();} -lv_obj_t* get_container(){if(::compose)return ::compose->getObj();if(::conversation)return ::conversation->getObj();if(::message_list)return ::message_list->getObj();return nullptr;} +namespace chat::ui::runtime +{ +bool requestCompose(const chat::ConversationId& id) +{ + selected = id; + return true; } +void clearRequestedCompose() {} +} // namespace chat::ui::runtime +namespace chat::ui::shell +{ +void enter(void*, lv_obj_t*) { native_open_page(5); } +void exit(void*, lv_obj_t*) { preview_chat_exit(); } +lv_obj_t* get_container() +{ + if (::compose) return ::compose->getObj(); + if (::conversation) return ::conversation->getObj(); + if (::message_list) return ::message_list->getObj(); + return nullptr; +} +} // namespace chat::ui::shell void preview_chat_enter(int page) { - if(page==5) { - message_list=std::make_unique(lv_screen_active()); + if (page == 5) + { + message_list = std::make_unique(lv_screen_active()); std::vector rows(2); - rows[0].id=selected;rows[0].name="Alex";rows[0].preview="Meet at the ridge shelter?";rows[0].unread=2;rows[0].last_timestamp=1789110000; - rows[1].id=chat::ConversationId(chat::ChannelId::PRIMARY);rows[1].name="Primary";rows[1].preview="Trail conditions are clear.";rows[1].last_timestamp=1789109000; + rows[0].id = selected; + rows[0].name = "Alex"; + rows[0].preview = "Meet at the ridge shelter?"; + rows[0].unread = 2; + rows[0].last_timestamp = 1789110000; + rows[1].id = chat::ConversationId(chat::ChannelId::PRIMARY); + rows[1].name = "Primary"; + rows[1].preview = "Trail conditions are clear."; + rows[1].last_timestamp = 1789109000; message_list->setConversations(rows); - message_list->setActionCallback([](auto intent,const chat::ConversationId& id,void*) { + message_list->setActionCallback([](auto intent, const chat::ConversationId& id, void*) + { if(intent==chat::ui::ChatMessageListScreen::ActionIntent::SelectConversation){selected=id;native_open_page(6);} - if(intent==chat::ui::ChatMessageListScreen::ActionIntent::Back)native_open_page(10); - },nullptr); - } else if(page==6) { - conversation=std::make_unique(lv_screen_active(),selected); - conversation->setHeaderText(selected.peer?"Alex":"Primary"); + if(intent==chat::ui::ChatMessageListScreen::ActionIntent::Back)native_open_page(10); }, + nullptr); + } + else if (page == 6) + { + conversation = std::make_unique(lv_screen_active(), selected); + conversation->setHeaderText(selected.peer ? "Alex" : "Primary"); ::ui::chat::MessageRow row; - row.ref.protocol_id=1;row.sender_node_id=0x12345678; - ::ui::copyText(row.text,"Meet at the ridge shelter?");::ui::copyText(row.time_label,"09:41");::ui::copyText(row.sender_label,"Alex"); - row.delivery=::ui::chat::MessageDeliveryState::Received; + row.ref.protocol_id = 1; + row.sender_node_id = 0x12345678; + ::ui::copyText(row.text, "Meet at the ridge shelter?"); + ::ui::copyText(row.time_label, "09:41"); + ::ui::copyText(row.sender_label, "Alex"); + row.delivery = ::ui::chat::MessageDeliveryState::Received; conversation->addMessage(row); - for(const auto& text:sent_messages) { - row.outgoing=true;row.ref.protocol_id++;row.delivery=::ui::chat::MessageDeliveryState::Sent; - ::ui::copyText(row.text,text.c_str());::ui::copyText(row.sender_label,"You");conversation->addMessage(row); + for (const auto& text : sent_messages) + { + row.outgoing = true; + row.ref.protocol_id++; + row.delivery = ::ui::chat::MessageDeliveryState::Sent; + ::ui::copyText(row.text, text.c_str()); + ::ui::copyText(row.sender_label, "You"); + conversation->addMessage(row); } - conversation->setBackCallback([](void*){native_open_page(5);},nullptr); - conversation->setActionCallback([](auto intent,void*){if(intent==chat::ui::ChatConversationScreen::ActionIntent::Reply)native_open_page(7);},nullptr); - } else if(page==7) { - compose=std::make_unique(lv_screen_active(),selected); - compose->setHeaderText(selected.peer?"Alex":"Primary"); - compose->setBackCallback([](void*){native_open_page(6);},nullptr); - compose->setActionCallback([](auto intent,void*) { + conversation->setBackCallback([](void*) + { native_open_page(5); }, + nullptr); + conversation->setActionCallback([](auto intent, void*) + {if(intent==chat::ui::ChatConversationScreen::ActionIntent::Reply)native_open_page(7); }, + nullptr); + } + else if (page == 7) + { + compose = std::make_unique(lv_screen_active(), selected); + compose->setHeaderText(selected.peer ? "Alex" : "Primary"); + compose->setBackCallback([](void*) + { native_open_page(6); }, + nullptr); + compose->setActionCallback([](auto intent, void*) + { if(intent==chat::ui::ChatComposeScreen::ActionIntent::Send){sent_messages.push_back(compose->getText());native_open_page(6);} - else if(intent==chat::ui::ChatComposeScreen::ActionIntent::Cancel)native_open_page(6); - },nullptr); + else if(intent==chat::ui::ChatComposeScreen::ActionIntent::Cancel)native_open_page(6); }, + nullptr); } } diff --git a/tools/web_simulator/native_menu.cpp b/tools/web_simulator/native_menu.cpp index 05892388..4e3e23ce 100644 --- a/tools/web_simulator/native_menu.cpp +++ b/tools/web_simulator/native_menu.cpp @@ -1,70 +1,107 @@ // Native app-shell bindings. Visual creation stays in menu_layout/menu_runtime. // Names, icons and PIO order follow ui/app_catalog_builder.cpp. -#include "ui/app_runtime.h" #include "ui/app_catalog.h" #include "ui/app_catalog_builder.h" -#include +#include "ui/app_runtime.h" #include "ui/menu/menu_layout.h" #include "ui/menu/menu_runtime.h" #include "ui/runtime/ui_feedback.h" #include +#include extern "C" void native_open_page(int page); -static bool menu_created=false; -static AppScreen* active_app=nullptr; +static bool menu_created = false; +static AppScreen* active_app = nullptr; static ui::AppCatalog catalog; -static int page_for(const char* id) { - const char* ids[]={"calculator","sky_plot","usb_mass_storage","walkie_talkie","sstv","chat","","","extensions","network","","settings","contacts","team","tracker","map","energy_sweep"}; - for(int i=0;i<17;++i)if(std::strcmp(ids[i],id)==0)return i; +static int page_for(const char* id) +{ + const char* ids[] = {"calculator", "sky_plot", "usb_mass_storage", "walkie_talkie", "sstv", "chat", "", "", "extensions", "network", "", "settings", "contacts", "team", "tracker", "map", "energy_sweep"}; + for (int i = 0; i < 17; ++i) + if (std::strcmp(ids[i], id) == 0) return i; return -1; } void preview_menu_enter() { - if(!menu_created) { - auto* previous_group=app_g; + if (!menu_created) + { + auto* previous_group = app_g; ui::app_catalog_builder::FeatureFlags flags; - flags.include_usb=true;flags.include_network=true;flags.include_walkie_talkie=true;flags.include_power_off=true; + flags.include_usb = true; + flags.include_network = true; + flags.include_walkie_talkie = true; + flags.include_power_off = true; #if defined(ARDUINO_T_DECK) - flags.include_sstv=false; - flags.include_walkie_talkie=false; + flags.include_sstv = false; + flags.include_walkie_talkie = false; #endif - catalog=ui::app_catalog_builder::build(flags); + catalog = ui::app_catalog_builder::build(flags); ui::menu_layout::InitOptions options; - options.apps=catalog; + options.apps = catalog; ui::menu_layout::init(options); - if(previous_group) lv_group_delete(previous_group); + if (previous_group) lv_group_delete(previous_group); ui::menu_runtime::Hooks hooks; - hooks.format_time=[](char* out,size_t n){std::snprintf(out,n,"09:41");return true;}; - hooks.show_main_menu=[](){native_open_page(10);}; - ui::menu_runtime::init(lv_screen_active(),main_screen,ui::menu_layout::menuPanel(),hooks); - menu_created=true; + hooks.format_time = [](char* out, size_t n) + {std::snprintf(out,n,"09:41");return true; }; + hooks.show_main_menu = []() + { native_open_page(10); }; + ui::menu_runtime::init(lv_screen_active(), main_screen, ui::menu_layout::menuPanel(), hooks); + menu_created = true; } - lv_obj_clear_flag(main_screen,LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(main_screen, LV_OBJ_FLAG_HIDDEN); lv_tileview_set_tile_by_index(main_screen, 0, 0, LV_ANIM_OFF); ui::menu_layout::setMenuVisible(true); ui::menu_runtime::setScene(ui::menu_runtime::Scene::Menu); set_default_group(menu_g); } -void preview_menu_exit() { - if(menu_created){ui::menu_runtime::setScene(ui::menu_runtime::Scene::App);lv_obj_add_flag(main_screen,LV_OBJ_FLAG_HIDDEN);} +void preview_menu_exit() +{ + if (menu_created) + { + ui::menu_runtime::setScene(ui::menu_runtime::Scene::App); + lv_obj_add_flag(main_screen, LV_OBJ_FLAG_HIDDEN); + } } -bool ui_is_interruption_app_active(){return false;} -bool ui_is_overlay_active(){return false;} -AppScreen* ui_get_active_app(){return active_app;} -void ui_switch_to_app(AppScreen* app,lv_obj_t* parent){active_app=app;const int page=page_for(app->stable_id());if(page>=0)native_open_page(page);else app->enter(parent);} -void ui_clear_active_app(){active_app=nullptr;} -void ui_exit_active_app(lv_obj_t* parent){if(active_app)active_app->exit(parent);active_app=nullptr;} -void menu_show(){native_open_page(10);} +bool ui_is_interruption_app_active() { return false; } +bool ui_is_overlay_active() { return false; } +AppScreen* ui_get_active_app() { return active_app; } +void ui_switch_to_app(AppScreen* app, lv_obj_t* parent) +{ + active_app = app; + const int page = page_for(app->stable_id()); + if (page >= 0) native_open_page(page); + else app->enter(parent); +} +void ui_clear_active_app() { active_app = nullptr; } +void ui_exit_active_app(lv_obj_t* parent) +{ + if (active_app) active_app->exit(parent); + active_app = nullptr; +} +void menu_show() { native_open_page(10); } - -void preview_power_enter() { +void preview_power_enter() +{ preview_menu_enter(); - for(size_t i=0;istable_id(),"shutdown")==0){app->enter(nullptr==main_screen?lv_screen_active():main_screen);return;}} + for (size_t i = 0; i < ui::catalogCount(catalog); ++i) + { + auto* app = ui::catalogAt(catalog, i); + if (std::strcmp(app->stable_id(), "shutdown") == 0) + { + app->enter(nullptr == main_screen ? lv_screen_active() : main_screen); + return; + } + } } -static lv_obj_t* cancel_button(lv_obj_t* obj) { - if(lv_obj_check_type(obj,&lv_label_class)&&std::strcmp(lv_label_get_text(obj),"Cancel")==0&&lv_obj_check_type(lv_obj_get_parent(obj),&lv_button_class))return lv_obj_get_parent(obj); - for(uint32_t i=0;i static int current_page = -1; -int preview_current_page(){return current_page;} +int preview_current_page() { return current_page; } void preview_menu_enter(); void preview_menu_exit(); void preview_power_enter(); @@ -29,23 +29,59 @@ void preview_exit_page() { switch (current_page) { - case 0: calculator::ui::runtime::exit(lv_screen_active()); break; - case 1: gnss::ui::runtime::exit(lv_screen_active()); break; - case 2: usb_storage::ui::runtime::exit(lv_screen_active()); break; - case 3: walkie_page::ui::runtime::exit(lv_screen_active()); break; - case 4: sstv_page::ui::runtime::exit(lv_screen_active()); break; - case 8: extensions::ui::runtime::exit(lv_screen_active()); break; - case 9: network::ui::shell::exit(nullptr,lv_screen_active()); break; - case 11: settings::ui::runtime::exit(lv_screen_active()); break; - case 12: contacts::ui::runtime::exit(lv_screen_active()); break; - case 13: team::ui::runtime::exit(lv_screen_active()); break; - case 14: tracker::ui::runtime::exit(lv_screen_active()); break; - case 15: gps::ui::runtime::exit(lv_screen_active()); break; - case 16: energy_sweep::ui::runtime::exit(lv_screen_active()); break; - case 17: preview_menu_exit(); break; - case 18: preview_power_exit(); break; - case 10: preview_menu_exit(); break; - case 5: case 6: case 7: preview_chat_exit(); break; + case 0: + calculator::ui::runtime::exit(lv_screen_active()); + break; + case 1: + gnss::ui::runtime::exit(lv_screen_active()); + break; + case 2: + usb_storage::ui::runtime::exit(lv_screen_active()); + break; + case 3: + walkie_page::ui::runtime::exit(lv_screen_active()); + break; + case 4: + sstv_page::ui::runtime::exit(lv_screen_active()); + break; + case 8: + extensions::ui::runtime::exit(lv_screen_active()); + break; + case 9: + network::ui::shell::exit(nullptr, lv_screen_active()); + break; + case 11: + settings::ui::runtime::exit(lv_screen_active()); + break; + case 12: + contacts::ui::runtime::exit(lv_screen_active()); + break; + case 13: + team::ui::runtime::exit(lv_screen_active()); + break; + case 14: + tracker::ui::runtime::exit(lv_screen_active()); + break; + case 15: + gps::ui::runtime::exit(lv_screen_active()); + break; + case 16: + energy_sweep::ui::runtime::exit(lv_screen_active()); + break; + case 17: + preview_menu_exit(); + break; + case 18: + preview_power_exit(); + break; + case 10: + preview_menu_exit(); + break; + case 5: + case 6: + case 7: + preview_chat_exit(); + break; } current_page = -1; } @@ -54,27 +90,65 @@ extern "C" EMSCRIPTEN_KEEPALIVE void native_open_page(int page) { preview_exit_page(); lv_group_remove_all_objs(app_g); - if(page!=10&&page!=17&&page!=18)ui::menu_runtime::setScene(ui::menu_runtime::Scene::App); + if (page != 10 && page != 17 && page != 18) ui::menu_runtime::setScene(ui::menu_runtime::Scene::App); switch (page) { - case 0: calculator::ui::runtime::enter(nullptr, lv_screen_active()); break; - case 1: gnss::ui::runtime::enter(nullptr, lv_screen_active()); break; - case 2: usb_storage::ui::runtime::enter(nullptr, lv_screen_active()); break; - case 3: walkie_page::ui::runtime::enter(nullptr, lv_screen_active()); break; - case 4: sstv_page::ui::runtime::enter(nullptr, lv_screen_active()); break; - case 8: extensions::ui::runtime::enter(nullptr, lv_screen_active()); break; - case 9: network::ui::shell::enter(nullptr,lv_screen_active()); break; - case 11: settings::ui::runtime::enter(nullptr,lv_screen_active()); break; - case 12: contacts::ui::runtime::enter(nullptr,lv_screen_active()); break; - case 13: team::ui::runtime::enter(nullptr,lv_screen_active()); break; - case 14: tracker::ui::runtime::enter(nullptr,lv_screen_active()); break; - case 15: gps::ui::runtime::enter(nullptr,lv_screen_active()); break; - case 16: energy_sweep::ui::runtime::enter(nullptr,lv_screen_active()); break; - case 17: preview_menu_enter();ui::menu_runtime::handleShortcutKey('h',1);break; - case 18: preview_power_enter();break; - case 10: preview_menu_enter(); break; - case 5: case 6: case 7: preview_chat_enter(page); break; - default: return; + case 0: + calculator::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 1: + gnss::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 2: + usb_storage::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 3: + walkie_page::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 4: + sstv_page::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 8: + extensions::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 9: + network::ui::shell::enter(nullptr, lv_screen_active()); + break; + case 11: + settings::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 12: + contacts::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 13: + team::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 14: + tracker::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 15: + gps::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 16: + energy_sweep::ui::runtime::enter(nullptr, lv_screen_active()); + break; + case 17: + preview_menu_enter(); + ui::menu_runtime::handleShortcutKey('h', 1); + break; + case 18: + preview_power_enter(); + break; + case 10: + preview_menu_enter(); + break; + case 5: + case 6: + case 7: + preview_chat_enter(page); + break; + default: + return; } current_page = page; lv_obj_update_layout(lv_screen_active()); @@ -83,5 +157,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE void native_open_page(int page) void ui_request_rebuild_active_app() { - lv_async_call([](void*) {const int page=current_page;native_open_page(page);},nullptr); + lv_async_call([](void*) + {const int page=current_page;native_open_page(page); }, + nullptr); } diff --git a/tools/web_simulator/native_runtime.cpp b/tools/web_simulator/native_runtime.cpp index e1875cbc..aecc9f03 100644 --- a/tools/web_simulator/native_runtime.cpp +++ b/tools/web_simulator/native_runtime.cpp @@ -1,16 +1,16 @@ // Browser display/input adapter. Page objects are built by unchanged firmware sources. +#include "app/app_facade_access.h" #include "lvgl.h" +#include "platform/ui/walkie_runtime.h" #include "ui/app_runtime.h" #include "ui/localization.h" +#include "ui/menu/menu_runtime.h" #include "ui/page/page_profile.h" #include "ui/screens/calculator/calculator_page_runtime.h" -#include "ui/menu/menu_runtime.h" -#include "app/app_facade_access.h" -#include "platform/ui/walkie_runtime.h" -#include #include #include #include +#include #include lv_group_t* app_g = nullptr; @@ -30,10 +30,10 @@ int preview_current_page(); extern "C" void native_open_page(int page); EM_JS(void, send_frame, (uint32_t ptr, int width, int height), { - if (Module.onFrame) Module.onFrame(ptr, width, height); + if (Module.onFrame) Module.onFrame(ptr, width, height); }); EM_JS(void, request_menu, (), { - if (Module.onExit) Module.onExit(); + if (Module.onExit) Module.onExit(); }); void set_default_group(lv_group_t* group) { lv_group_set_default(group); } @@ -43,10 +43,11 @@ void ui_request_exit_to_menu() static bool pending = false; if (pending) return; pending = true; - lv_async_call([](void*) { + lv_async_call([](void*) + { pending = false; - native_open_page(10); - }, nullptr); + native_open_page(10); }, + nullptr); } static void append_json_string(const char* value) @@ -54,7 +55,11 @@ static void append_json_string(const char* value) snapshot += '"'; for (const unsigned char* p = reinterpret_cast(value); *p; ++p) { - if (*p == '"' || *p == '\\') { snapshot += '\\'; snapshot += *p; } + if (*p == '"' || *p == '\\') + { + snapshot += '\\'; + snapshot += *p; + } else if (*p == '\n') snapshot += "\\n"; else if (*p >= 32) snapshot += *p; } @@ -66,10 +71,13 @@ static void collect_labels(lv_obj_t* obj) if (!lv_obj_is_visible(obj)) return; if (lv_obj_check_type(obj, &lv_textarea_class)) { - lv_area_t area;lv_obj_get_coords(obj,&area); + lv_area_t area; + lv_obj_get_coords(obj, &area); char data[200]; - std::snprintf(data,sizeof(data),"%s{\"id\":%u,\"input\":true,\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,\"text\":",snapshot.size()>1?",":"",static_cast(reinterpret_cast(obj)),static_cast(area.x1),static_cast(area.y1),static_cast(lv_area_get_width(&area)),static_cast(lv_area_get_height(&area))); - snapshot+=data;append_json_string(lv_textarea_get_text(obj));snapshot+='}'; + std::snprintf(data, sizeof(data), "%s{\"id\":%u,\"input\":true,\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,\"text\":", snapshot.size() > 1 ? "," : "", static_cast(reinterpret_cast(obj)), static_cast(area.x1), static_cast(area.y1), static_cast(lv_area_get_width(&area)), static_cast(lv_area_get_height(&area))); + snapshot += data; + append_json_string(lv_textarea_get_text(obj)); + snapshot += '}'; } if (lv_obj_check_type(obj, &lv_label_class)) { @@ -90,91 +98,116 @@ static void collect_labels(lv_obj_t* obj) for (uint32_t i = 0; i < lv_obj_get_child_count(obj); ++i) collect_labels(lv_obj_get_child(obj, i)); } -extern "C" { -EMSCRIPTEN_KEEPALIVE void native_init(int deck) +extern "C" { - if (!initialized) + EMSCRIPTEN_KEEPALIVE void native_init(int deck) { - lv_init(); - setenv("TRAIL_MATE_SD_ROOT","/sd",1); - setenv("TRAIL_MATE_SETTINGS_ROOT","/settings",1); - preview_bind_app(); - display = lv_display_create(480, 222); - lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565); - lv_display_set_buffers(display, pixels, nullptr, sizeof(pixels), LV_DISPLAY_RENDER_MODE_FULL); - lv_display_set_flush_cb(display, [](lv_display_t* d, const lv_area_t*, uint8_t* buffer) { + if (!initialized) + { + lv_init(); + setenv("TRAIL_MATE_SD_ROOT", "/sd", 1); + setenv("TRAIL_MATE_SETTINGS_ROOT", "/settings", 1); + preview_bind_app(); + display = lv_display_create(480, 222); + lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565); + lv_display_set_buffers(display, pixels, nullptr, sizeof(pixels), LV_DISPLAY_RENDER_MODE_FULL); + lv_display_set_flush_cb(display, [](lv_display_t* d, const lv_area_t*, uint8_t* buffer) + { send_frame(static_cast(reinterpret_cast(buffer)), lv_display_get_horizontal_resolution(d), lv_display_get_vertical_resolution(d)); - lv_display_flush_ready(d); - }); - pointer_device = lv_indev_create(); - lv_indev_set_type(pointer_device, LV_INDEV_TYPE_POINTER); - lv_indev_set_read_cb(pointer_device, [](lv_indev_t*, lv_indev_data_t* data) { + lv_display_flush_ready(d); }); + pointer_device = lv_indev_create(); + lv_indev_set_type(pointer_device, LV_INDEV_TYPE_POINTER); + lv_indev_set_read_cb(pointer_device, [](lv_indev_t*, lv_indev_data_t* data) + { data->point.x = pointer_x; data->point.y = pointer_y; - data->state = pointer_down ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; - }); - app_g = lv_group_create(); - set_default_group(app_g); - ui::i18n::set_locale("en", false); - initialized = true; - } - preview_exit_page(); - lv_display_set_resolution(display, deck ? 320 : 480, deck ? 240 : 222); - page_profile = deck ? ui::page_profile::make_tdeck_profile() : ui::page_profile::make_pager_profile(); - ui::page_profile::set_active_profile(&page_profile); - native_open_page(0); - lv_obj_update_layout(lv_screen_active()); - lv_refr_now(display); -} -EMSCRIPTEN_KEEPALIVE void native_tick(int elapsed) -{ - lv_tick_inc(elapsed); - lv_timer_handler(); - ui::i18n::on_lvgl_frame_completed(); -} -EMSCRIPTEN_KEEPALIVE void native_pointer(int x, int y, int down) -{ - pointer_x = x; pointer_y = y; pointer_down = down != 0; - lv_indev_read(pointer_device); -} -EMSCRIPTEN_KEEPALIVE void native_activate(uint32_t id) -{ - auto* button = reinterpret_cast(static_cast(id)); - if (lv_obj_is_valid(button)&&!lv_obj_has_state(button,LV_STATE_DISABLED)) lv_obj_send_event(button, LV_EVENT_CLICKED, nullptr); - lv_refr_now(display); -} -EMSCRIPTEN_KEEPALIVE void native_key(int key) { - if (key == LV_KEY_ENTER && preview_current_page() == 10) { - auto* group = lv_group_get_default(); - auto* focused = group ? lv_group_get_focused(group) : nullptr; - if (focused && !lv_obj_has_state(focused, LV_STATE_DISABLED)) { - lv_obj_send_event(focused, LV_EVENT_CLICKED, nullptr); + data->state = pointer_down ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; }); + app_g = lv_group_create(); + set_default_group(app_g); + ui::i18n::set_locale("en", false); + initialized = true; } - return; + preview_exit_page(); + lv_display_set_resolution(display, deck ? 320 : 480, deck ? 240 : 222); + page_profile = deck ? ui::page_profile::make_tdeck_profile() : ui::page_profile::make_pager_profile(); + ui::page_profile::set_active_profile(&page_profile); + native_open_page(0); + lv_obj_update_layout(lv_screen_active()); + lv_refr_now(display); } - if(key<128&&ui::menu_runtime::handleShortcutKey(static_cast(key),1))return; - if(auto* group=lv_group_get_default())lv_group_send_data(group,key); -} -EMSCRIPTEN_KEEPALIVE void native_focus(uint32_t id){auto* obj=reinterpret_cast(static_cast(id));if(lv_obj_is_valid(obj))lv_group_focus_obj(obj);} -EMSCRIPTEN_KEEPALIVE void native_set_text(uint32_t id,const char* value){auto* obj=reinterpret_cast(static_cast(id));if(lv_obj_is_valid(obj)&&lv_obj_check_type(obj,&lv_textarea_class))lv_textarea_set_text(obj,value);} -EMSCRIPTEN_KEEPALIVE void native_protocol(int protocol){app::configFacade().switchMeshProtocol(static_cast(protocol),false);} -EMSCRIPTEN_KEEPALIVE int native_protocol_value(){return static_cast(app::configFacade().getMeshProtocol());} -EMSCRIPTEN_KEEPALIVE int native_page_value(){return preview_current_page();} -EMSCRIPTEN_KEEPALIVE void native_key_state(int key,int pressed){ - if(key==' ') { - if(ui::menu_runtime::handleWalkieKey(' ',pressed))return; - if(preview_current_page()==3){platform::ui::walkie::set_ptt(pressed!=0);return;} + EMSCRIPTEN_KEEPALIVE void native_tick(int elapsed) + { + lv_tick_inc(elapsed); + lv_timer_handler(); + ui::i18n::on_lvgl_frame_completed(); + } + EMSCRIPTEN_KEEPALIVE void native_pointer(int x, int y, int down) + { + pointer_x = x; + pointer_y = y; + pointer_down = down != 0; + lv_indev_read(pointer_device); + } + EMSCRIPTEN_KEEPALIVE void native_activate(uint32_t id) + { + auto* button = reinterpret_cast(static_cast(id)); + if (lv_obj_is_valid(button) && !lv_obj_has_state(button, LV_STATE_DISABLED)) lv_obj_send_event(button, LV_EVENT_CLICKED, nullptr); + lv_refr_now(display); + } + EMSCRIPTEN_KEEPALIVE void native_key(int key) + { + if (key == LV_KEY_ENTER && preview_current_page() == 10) + { + auto* group = lv_group_get_default(); + auto* focused = group ? lv_group_get_focused(group) : nullptr; + if (focused && !lv_obj_has_state(focused, LV_STATE_DISABLED)) + { + lv_obj_send_event(focused, LV_EVENT_CLICKED, nullptr); + } + return; + } + if (key < 128 && ui::menu_runtime::handleShortcutKey(static_cast(key), 1)) return; + if (auto* group = lv_group_get_default()) lv_group_send_data(group, key); + } + EMSCRIPTEN_KEEPALIVE void native_focus(uint32_t id) + { + auto* obj = reinterpret_cast(static_cast(id)); + if (lv_obj_is_valid(obj)) lv_group_focus_obj(obj); + } + EMSCRIPTEN_KEEPALIVE void native_set_text(uint32_t id, const char* value) + { + auto* obj = reinterpret_cast(static_cast(id)); + if (lv_obj_is_valid(obj) && lv_obj_check_type(obj, &lv_textarea_class)) lv_textarea_set_text(obj, value); + } + EMSCRIPTEN_KEEPALIVE void native_protocol(int protocol) { app::configFacade().switchMeshProtocol(static_cast(protocol), false); } + EMSCRIPTEN_KEEPALIVE int native_protocol_value() { return static_cast(app::configFacade().getMeshProtocol()); } + EMSCRIPTEN_KEEPALIVE int native_page_value() { return preview_current_page(); } + EMSCRIPTEN_KEEPALIVE void native_key_state(int key, int pressed) + { + if (key == ' ') + { + if (ui::menu_runtime::handleWalkieKey(' ', pressed)) return; + if (preview_current_page() == 3) + { + platform::ui::walkie::set_ptt(pressed != 0); + return; + } + } + if (pressed) native_key(key); + } + EMSCRIPTEN_KEEPALIVE void native_encoder(int delta) + { + if (auto* group = lv_group_get_default()) + { + if (delta < 0) lv_group_focus_prev(group); + else lv_group_focus_next(group); + } + } + EMSCRIPTEN_KEEPALIVE const char* native_snapshot() + { + snapshot = "["; + collect_labels(lv_screen_active()); + collect_labels(lv_layer_top()); + snapshot += ']'; + return snapshot.c_str(); } - if(pressed)native_key(key); -} -EMSCRIPTEN_KEEPALIVE void native_encoder(int delta) { - if(auto* group=lv_group_get_default()){if(delta<0)lv_group_focus_prev(group);else lv_group_focus_next(group);} -} -EMSCRIPTEN_KEEPALIVE const char* native_snapshot() -{ - snapshot = "["; - collect_labels(lv_screen_active()); - collect_labels(lv_layer_top()); - snapshot += ']'; - return snapshot.c_str(); -} } diff --git a/tools/web_simulator/preview_board.cpp b/tools/web_simulator/preview_board.cpp index 956abef7..650579ab 100644 --- a/tools/web_simulator/preview_board.cpp +++ b/tools/web_simulator/preview_board.cpp @@ -1,28 +1,29 @@ #include "board/BoardBase.h" #include "platform/ui/device_runtime.h" #include -EM_JS(void, preview_power_off, (), {if(Module.onPowerOff)Module.onPowerOff();}); -class BrowserBoard final : public BoardBase { -public: - uint32_t begin(uint32_t) override{return 0;} - void wakeUp() override{} - void handlePowerButton() override{} - void softwareShutdown() override{preview_power_off();} - void setBrightness(uint8_t value) override{platform::ui::device::set_screen_brightness(value);} - uint8_t getBrightness() override{return platform::ui::device::screen_brightness();} - bool hasKeyboard() override{return true;} - void keyboardSetBrightness(uint8_t value) override{platform::ui::device::set_keyboard_backlight(value);} - uint8_t keyboardGetBrightness() override{return platform::ui::device::keyboard_backlight();} - bool isRTCReady() const override{return true;} - bool isCharging() override{return false;} - int getBatteryLevel() override{return 85;} - bool isSDReady() const override{return true;} - bool isCardReady() override{return true;} - bool isGPSReady() const override{return true;} - bool hasGPSHardware() const override{return true;} - bool hasSstvAudioInput() const override{return true;} - void vibrator() override{} - void stopVibrator() override{} +EM_JS(void, preview_power_off, (), {if(Module.onPowerOff)Module.onPowerOff(); }); +class BrowserBoard final : public BoardBase +{ + public: + uint32_t begin(uint32_t) override { return 0; } + void wakeUp() override {} + void handlePowerButton() override {} + void softwareShutdown() override { preview_power_off(); } + void setBrightness(uint8_t value) override { platform::ui::device::set_screen_brightness(value); } + uint8_t getBrightness() override { return platform::ui::device::screen_brightness(); } + bool hasKeyboard() override { return true; } + void keyboardSetBrightness(uint8_t value) override { platform::ui::device::set_keyboard_backlight(value); } + uint8_t keyboardGetBrightness() override { return platform::ui::device::keyboard_backlight(); } + bool isRTCReady() const override { return true; } + bool isCharging() override { return false; } + int getBatteryLevel() override { return 85; } + bool isSDReady() const override { return true; } + bool isCardReady() override { return true; } + bool isGPSReady() const override { return true; } + bool hasGPSHardware() const override { return true; } + bool hasSstvAudioInput() const override { return true; } + void vibrator() override {} + void stopVibrator() override {} }; static BrowserBoard browser_board; -BoardBase& board=browser_board; +BoardBase& board = browser_board; diff --git a/tools/web_simulator/preview_extensions.cpp b/tools/web_simulator/preview_extensions.cpp index 0da5883a..5f9df363 100644 --- a/tools/web_simulator/preview_extensions.cpp +++ b/tools/web_simulator/preview_extensions.cpp @@ -2,27 +2,91 @@ #include "platform/ui/wifi_runtime.h" #include #include -namespace platform::ui::wifi { +namespace platform::ui::wifi +{ static Config config{}; -static bool connected=false; -bool is_supported(){return true;} -bool load_config(Config& out){out=config;return true;} -bool save_config(const Config& value){config=value;return true;} -bool apply_enabled(bool enabled){config.enabled=enabled;if(!enabled)connected=false;return true;} -bool connect(const Config* value){if(value)config=*value;config.enabled=true;connected=true;return true;} -void disconnect(){connected=false;} -Status status(){Status out;out.supported=true;out.enabled=config.enabled;out.connected=connected;out.state=connected?ConnectionState::Connected:ConnectionState::Disabled;out.has_credentials=true;std::snprintf(out.ssid,sizeof(out.ssid),"Preview network");return out;} +static bool connected = false; +bool is_supported() { return true; } +bool load_config(Config& out) +{ + out = config; + return true; } -namespace ui::runtime::packs { -bool is_supported(){return true;} -bool load_installed_packages(std::vector& out,std::string& error){out.clear();error.clear();return true;} -bool fetch_catalog(std::vector& out,std::string& error){ - out.clear();error.clear(); - PackageRecord p;p.id="zh-Hans";p.package_type="locale-bundle";p.version="1.3.0";p.display_name="Simplified Chinese";p.summary="Chinese fonts, translations and Pinyin IME";p.provided_locale_ids={"zh-Hans"};p.provided_ime_ids={"pinyin"};p.archive_size_bytes=1200000;p.compatible_firmware=true;p.compatible_memory_profile=true;out.push_back(p); - p.id="ja";p.version="1.2.0";p.display_name="Japanese";p.summary="Japanese fonts and translations (review)";p.provided_locale_ids={"ja"};p.provided_ime_ids.clear();out.push_back(p);return true; +bool save_config(const Config& value) +{ + config = value; + return true; } -bool start_install_package(const PackageRecord&,std::string& error){error="Browser preview does not install device packages";return false;} -bool install_package(const PackageRecord& p,std::string& error){return start_install_package(p,error);} -bool uninstall_package(const PackageRecord&,std::string& error){error="No device package is installed in this preview";return false;} -PackageInstallStatus install_status(){return {};} +bool apply_enabled(bool enabled) +{ + config.enabled = enabled; + if (!enabled) connected = false; + return true; } +bool connect(const Config* value) +{ + if (value) config = *value; + config.enabled = true; + connected = true; + return true; +} +void disconnect() { connected = false; } +Status status() +{ + Status out; + out.supported = true; + out.enabled = config.enabled; + out.connected = connected; + out.state = connected ? ConnectionState::Connected : ConnectionState::Disabled; + out.has_credentials = true; + std::snprintf(out.ssid, sizeof(out.ssid), "Preview network"); + return out; +} +} // namespace platform::ui::wifi +namespace ui::runtime::packs +{ +bool is_supported() { return true; } +bool load_installed_packages(std::vector& out, std::string& error) +{ + out.clear(); + error.clear(); + return true; +} +bool fetch_catalog(std::vector& out, std::string& error) +{ + out.clear(); + error.clear(); + PackageRecord p; + p.id = "zh-Hans"; + p.package_type = "locale-bundle"; + p.version = "1.3.0"; + p.display_name = "Simplified Chinese"; + p.summary = "Chinese fonts, translations and Pinyin IME"; + p.provided_locale_ids = {"zh-Hans"}; + p.provided_ime_ids = {"pinyin"}; + p.archive_size_bytes = 1200000; + p.compatible_firmware = true; + p.compatible_memory_profile = true; + out.push_back(p); + p.id = "ja"; + p.version = "1.2.0"; + p.display_name = "Japanese"; + p.summary = "Japanese fonts and translations (review)"; + p.provided_locale_ids = {"ja"}; + p.provided_ime_ids.clear(); + out.push_back(p); + return true; +} +bool start_install_package(const PackageRecord&, std::string& error) +{ + error = "Browser preview does not install device packages"; + return false; +} +bool install_package(const PackageRecord& p, std::string& error) { return start_install_package(p, error); } +bool uninstall_package(const PackageRecord&, std::string& error) +{ + error = "No device package is installed in this preview"; + return false; +} +PackageInstallStatus install_status() { return {}; } +} // namespace ui::runtime::packs diff --git a/tools/web_simulator/preview_facade.cpp b/tools/web_simulator/preview_facade.cpp index cec0f5a7..94afbaf8 100644 --- a/tools/web_simulator/preview_facade.cpp +++ b/tools/web_simulator/preview_facade.cpp @@ -1,7 +1,7 @@ // In-memory platform composition for the native website preview. // Chat/contact models and services remain the production implementations. -#include "app/app_facade_access.h" #include "app/app_config.h" +#include "app/app_facade_access.h" #include "chat/domain/chat_model.h" #include "chat/infra/mesh_peer_directory_core.h" #include "chat/infra/store/ram_store.h" @@ -11,39 +11,67 @@ #include #include -namespace { -class PreviewRadio final : public chat::IMeshAdapter { -public: - bool sendText(chat::ChannelId, const std::string&, chat::MessageId* id, chat::NodeId) override { if(id)*id=++next_id; return true; } - bool pollIncomingText(chat::MeshIncomingText*) override {return false;} - bool sendAppData(chat::ChannelId,uint32_t,const uint8_t*,size_t,chat::NodeId,bool,chat::MessageId,bool) override {return true;} - bool pollIncomingData(chat::MeshIncomingData*) override {return false;} - void applyConfig(const chat::MeshConfig&) override {} - bool isReady() const override {return true;} - bool pollIncomingRawPacket(uint8_t*,size_t&,size_t) override {return false;} - chat::NodeId getNodeId() const override {return 0xa1b35b7c;} - chat::MeshCapabilities getCapabilities() const override {chat::MeshCapabilities c;c.supports_unicast_text=true;c.supports_node_info=true;return c;} -private: uint32_t next_id=100; -}; -class MemoryBlob final : public chat::IMeshPeerDirectoryBlobStore { -public: - chat::MeshPeerDirectoryBlobLoadResult loadBlob(std::vector& out) override {out=data;return data.empty()?chat::MeshPeerDirectoryBlobLoadResult::Missing:chat::MeshPeerDirectoryBlobLoadResult::Loaded;} - bool saveBlob(const uint8_t* bytes,std::size_t size) override {data.assign(bytes,bytes+size);return true;} - void clearBlob() override {data.clear();} -private: std::vector data; -}; -class PreviewApp final : public app::IAppFacade { -public: - PreviewApp(): directory(blob),contacts(directory),chat(model,radio,store) { - directory.beginEmpty();contacts.begin(); - contacts.updateNodeInfo(0x12345678,"ALEX","Alex",8,-94,1789100000); - contacts.addContact(0x12345678,"Alex"); - contacts.updateNodeInfo(0x23456789,"MORG","Morgan",6,-102,1789100000); - contacts.addContact(0x23456789,"Morgan"); - std::snprintf(config.node_name,sizeof(config.node_name),"Trail Mate"); - std::snprintf(config.short_name,sizeof(config.short_name),"TM"); +namespace +{ +class PreviewRadio final : public chat::IMeshAdapter +{ + public: + bool sendText(chat::ChannelId, const std::string&, chat::MessageId* id, chat::NodeId) override + { + if (id) *id = ++next_id; + return true; } - app::AppConfigEdit beginConfigEdit() override {return {&config,nullptr,nullptr,nullptr};} + bool pollIncomingText(chat::MeshIncomingText*) override { return false; } + bool sendAppData(chat::ChannelId, uint32_t, const uint8_t*, size_t, chat::NodeId, bool, chat::MessageId, bool) override { return true; } + bool pollIncomingData(chat::MeshIncomingData*) override { return false; } + void applyConfig(const chat::MeshConfig&) override {} + bool isReady() const override { return true; } + bool pollIncomingRawPacket(uint8_t*, size_t&, size_t) override { return false; } + chat::NodeId getNodeId() const override { return 0xa1b35b7c; } + chat::MeshCapabilities getCapabilities() const override + { + chat::MeshCapabilities c; + c.supports_unicast_text = true; + c.supports_node_info = true; + return c; + } + + private: + uint32_t next_id = 100; +}; +class MemoryBlob final : public chat::IMeshPeerDirectoryBlobStore +{ + public: + chat::MeshPeerDirectoryBlobLoadResult loadBlob(std::vector& out) override + { + out = data; + return data.empty() ? chat::MeshPeerDirectoryBlobLoadResult::Missing : chat::MeshPeerDirectoryBlobLoadResult::Loaded; + } + bool saveBlob(const uint8_t* bytes, std::size_t size) override + { + data.assign(bytes, bytes + size); + return true; + } + void clearBlob() override { data.clear(); } + + private: + std::vector data; +}; +class PreviewApp final : public app::IAppFacade +{ + public: + PreviewApp() : directory(blob), contacts(directory), chat(model, radio, store) + { + directory.beginEmpty(); + contacts.begin(); + contacts.updateNodeInfo(0x12345678, "ALEX", "Alex", 8, -94, 1789100000); + contacts.addContact(0x12345678, "Alex"); + contacts.updateNodeInfo(0x23456789, "MORG", "Morgan", 6, -102, 1789100000); + contacts.addContact(0x23456789, "Morgan"); + std::snprintf(config.node_name, sizeof(config.node_name), "Trail Mate"); + std::snprintf(config.short_name, sizeof(config.short_name), "TM"); + } + app::AppConfigEdit beginConfigEdit() override { return {&config, nullptr, nullptr, nullptr}; } void saveConfig() override {} void saveConfig(app::AppConfigChangeSet) override {} void applyMeshConfig() override {} @@ -52,41 +80,56 @@ public: void applyNetworkLimits() override {} void applyPrivacyConfig() override {} void applyChatDefaults() override {} - chat::MeshProtocol getMeshProtocol() const override {return static_cast(config.mesh_protocol);} - void getEffectiveUserInfo(char* a,std::size_t n,char* b,std::size_t m) const override {std::snprintf(a,n,"%s",config.node_name);std::snprintf(b,m,"%s",config.short_name);} - bool switchMeshProtocol(chat::MeshProtocol p,bool) override {config.mesh_protocol=p;contacts.setActiveProtocol(p);return true;} - chat::ChatService& getChatService() override {return chat;} - chat::contacts::ContactService& getContactService() override {return contacts;} - chat::IMeshAdapter* getMeshAdapter() override {return &radio;} - const chat::IMeshAdapter* getMeshAdapter() const override {return &radio;} - chat::NodeId getSelfNodeId() const override {return radio.getNodeId();} - team::TeamController* getTeamController() override {return nullptr;} - team::TeamPairingService* getTeamPairing() override {return nullptr;} - team::TeamService* getTeamService() override {return nullptr;} - const team::TeamService* getTeamService() const override {return nullptr;} - team::TeamTrackSampler* getTeamTrackSampler() override {return nullptr;} + chat::MeshProtocol getMeshProtocol() const override { return static_cast(config.mesh_protocol); } + void getEffectiveUserInfo(char* a, std::size_t n, char* b, std::size_t m) const override + { + std::snprintf(a, n, "%s", config.node_name); + std::snprintf(b, m, "%s", config.short_name); + } + bool switchMeshProtocol(chat::MeshProtocol p, bool) override + { + config.mesh_protocol = p; + contacts.setActiveProtocol(p); + return true; + } + chat::ChatService& getChatService() override { return chat; } + chat::contacts::ContactService& getContactService() override { return contacts; } + chat::IMeshAdapter* getMeshAdapter() override { return &radio; } + const chat::IMeshAdapter* getMeshAdapter() const override { return &radio; } + chat::NodeId getSelfNodeId() const override { return radio.getNodeId(); } + team::TeamController* getTeamController() override { return nullptr; } + team::TeamPairingService* getTeamPairing() override { return nullptr; } + team::TeamService* getTeamService() override { return nullptr; } + const team::TeamService* getTeamService() const override { return nullptr; } + team::TeamTrackSampler* getTeamTrackSampler() override { return nullptr; } void setTeamModeActive(bool) override {} void broadcastNodeInfo() override {} - void clearNodeDb() override {directory.clear();} - void clearMessageDb() override {store.clearAll();model.clearAll();} - ble::BleManager* getBleManager() override {return nullptr;} - const ble::BleManager* getBleManager() const override {return nullptr;} - bool isBleEnabled() const override {return ble_enabled;} - void setBleEnabled(bool enabled) override {ble_enabled=enabled;} + void clearNodeDb() override { directory.clear(); } + void clearMessageDb() override + { + store.clearAll(); + model.clearAll(); + } + ble::BleManager* getBleManager() override { return nullptr; } + const ble::BleManager* getBleManager() const override { return nullptr; } + bool isBleEnabled() const override { return ble_enabled; } + void setBleEnabled(bool enabled) override { ble_enabled = enabled; } void restartDevice() override {} - chat::ui::IChatUiRuntime* getChatUiRuntime() override {return ui_runtime;} - void setChatUiRuntime(chat::ui::IChatUiRuntime* value) override {ui_runtime=value;} - BoardBase* getBoard() override {return nullptr;} - const BoardBase* getBoard() const override {return nullptr;} + chat::ui::IChatUiRuntime* getChatUiRuntime() override { return ui_runtime; } + void setChatUiRuntime(chat::ui::IChatUiRuntime* value) override { ui_runtime = value; } + BoardBase* getBoard() override { return nullptr; } + const BoardBase* getBoard() const override { return nullptr; } void updateCoreServices() override {} void tickEventRuntime() override {} void dispatchPendingEvents(std::size_t) override {} -protected: - const app::AppConfig& getConfig() const override {return config;} -private: + + protected: + const app::AppConfig& getConfig() const override { return config; } + + private: app::AppConfig config{}; - bool ble_enabled=false; - chat::ui::IChatUiRuntime* ui_runtime=nullptr; + bool ble_enabled = false; + chat::ui::IChatUiRuntime* ui_runtime = nullptr; MemoryBlob blob; chat::MeshPeerDirectoryCore directory; chat::contacts::ContactService contacts; @@ -95,5 +138,9 @@ private: chat::RamStore store; chat::ChatService chat; }; +} // namespace +void preview_bind_app() +{ + static PreviewApp app; + app::bindAppFacade(app); } -void preview_bind_app() {static PreviewApp app; app::bindAppFacade(app);} diff --git a/tools/web_simulator/preview_map_tiles.cpp b/tools/web_simulator/preview_map_tiles.cpp index b6d258b7..c0f2c289 100644 --- a/tools/web_simulator/preview_map_tiles.cpp +++ b/tools/web_simulator/preview_map_tiles.cpp @@ -8,75 +8,192 @@ #include #include -static uint8_t source_id=0; -static bool contours=false; -static bool missing=false; -static double world_size(int z){return std::ldexp(256.0,z);} -static double world_x(double lon,int z){return (lon+180.0)/360.0*world_size(z);} -static double world_y(double lat,int z){lat=std::clamp(lat,-85.05112878,85.05112878);const double s=std::sin(lat*M_PI/180.0);return (.5-std::log((1+s)/(1-s))/(4*M_PI))*world_size(z);} -static void world_geo(double x,double y,int z,double& lat,double& lon){const double size=world_size(z);lon=x/size*360-180;lat=std::atan(std::sinh(M_PI*(1-2*y/size)))*180/M_PI;} -void normalize_tile(int z,int& x,int& y){ui::map_tiles::normalizeTile(z,x,y);} -void latLngToTile(double lat,double lon,int zoom,int& x,int& y){ui::map_tiles::latLngToTile(lat,lon,zoom,x,y);} -void tileToLatLng(int x,int y,int zoom,double& lat,double& lon){world_geo(x*256.0,y*256.0,zoom,lat,lon);} -void tileToPixel(int x,int y,int,int& px,int& py){px=x*256;py=y*256;} -uint8_t sanitize_map_source(uint8_t source){return source<3?source:0;} -const char* map_source_label(uint8_t source){return source==1?"Terrain":source==2?"Satellite":"OSM";} -bool build_base_tile_path(int z,int x,int y,uint8_t source,char* out,size_t n){const char* key=source==1?"terrain":source==2?"satellite":"osm";return std::snprintf(out,n,"A:/maps/base/%s/%d/%d/%d.png",key,z,x,y)>0;} -bool build_contour_tile_path(int z,int x,int y,char* out,size_t n){return std::snprintf(out,n,"A:/maps/contour/major-50/%d/%d/%d.png",z,x,y)>0;} -static bool exists(const char* path){FILE* f=std::fopen(path+2,"rb");if(!f)return false;std::fclose(f);return true;} -bool base_tile_available(int z,int x,int y,uint8_t source){char path[180];build_base_tile_path(z,x,y,source,path,sizeof(path));return exists(path);} -bool map_source_directory_available(uint8_t source){return source<3;} -bool contour_directory_available(){return false;} -bool take_missing_tile_notice(uint8_t* out){if(out)*out=source_id;const bool result=missing;missing=false;return result;} -void set_map_render_options(uint8_t source,bool contour){source_id=sanitize_map_source(source);contours=contour;} -void update_map_anchor(TileContext& c,double lat,double lon,int zoom,int pan_x,int pan_y,bool fix) +static uint8_t source_id = 0; +static bool contours = false; +static bool missing = false; +static double world_size(int z) { return std::ldexp(256.0, z); } +static double world_x(double lon, int z) { return (lon + 180.0) / 360.0 * world_size(z); } +static double world_y(double lat, int z) { - if(!c.anchor||!c.map_container)return; - auto& a=*c.anchor;a.valid=fix;if(!fix)return; - a.z=zoom;a.n=std::ldexp(1.0,zoom); - int x,y;latLngToTile(lat,lon,zoom,x,y);a.gps_tile_x=x;a.gps_tile_y=y; - a.gps_global_pixel_x=static_cast(std::lround(world_x(lon,zoom)));a.gps_global_pixel_y=static_cast(std::lround(world_y(lat,zoom))); - a.gps_tile_pixel_x=x*256;a.gps_tile_pixel_y=y*256; - a.gps_offset_x=a.gps_global_pixel_x-a.gps_tile_pixel_x;a.gps_offset_y=a.gps_global_pixel_y-a.gps_tile_pixel_y; - a.gps_tile_screen_x=lv_obj_get_width(c.map_container)/2-a.gps_offset_x+pan_x; - a.gps_tile_screen_y=lv_obj_get_height(c.map_container)/2-a.gps_offset_y+pan_y; + lat = std::clamp(lat, -85.05112878, 85.05112878); + const double s = std::sin(lat * M_PI / 180.0); + return (.5 - std::log((1 + s) / (1 - s)) / (4 * M_PI)) * world_size(z); } -bool gps_screen_pos(const TileContext& c,double lat,double lon,int& x,int& y) +static void world_geo(double x, double y, int z, double& lat, double& lon) { - if(!c.anchor||!c.anchor->valid)return false;const auto& a=*c.anchor; - double dx=world_x(lon,a.z)-a.gps_global_pixel_x;const double size=world_size(a.z);if(dx>size/2)dx-=size;if(dx<-size/2)dx+=size; - x=std::lround(a.gps_tile_screen_x+a.gps_offset_x+dx);y=std::lround(a.gps_tile_screen_y+a.gps_offset_y+world_y(lat,a.z)-a.gps_global_pixel_y);return true; + const double size = world_size(z); + lon = x / size * 360 - 180; + lat = std::atan(std::sinh(M_PI * (1 - 2 * y / size))) * 180 / M_PI; } -bool tile_screen_pos_xyz(const TileContext& c,int x,int y,int z,int& sx,int& sy) +void normalize_tile(int z, int& x, int& y) { ui::map_tiles::normalizeTile(z, x, y); } +void latLngToTile(double lat, double lon, int zoom, int& x, int& y) { ui::map_tiles::latLngToTile(lat, lon, zoom, x, y); } +void tileToLatLng(int x, int y, int zoom, double& lat, double& lon) { world_geo(x * 256.0, y * 256.0, zoom, lat, lon); } +void tileToPixel(int x, int y, int, int& px, int& py) { - if(!c.anchor||!c.anchor->valid||c.anchor->z!=z)return false;const auto& a=*c.anchor; - int dx=x-a.gps_tile_x,n=1<n/2)dx-=n;if(dx<-n/2)dx+=n; - sx=a.gps_tile_screen_x+dx*256;sy=a.gps_tile_screen_y+(y-a.gps_tile_y)*256;return true; + px = x * 256; + py = y * 256; } -void get_screen_center_lat_lng(const TileContext& c,double& lat,double& lon) +uint8_t sanitize_map_source(uint8_t source) { return source < 3 ? source : 0; } +const char* map_source_label(uint8_t source) { return source == 1 ? "Terrain" : source == 2 ? "Satellite" + : "OSM"; } +bool build_base_tile_path(int z, int x, int y, uint8_t source, char* out, size_t n) { - if(!c.anchor||!c.anchor->valid){lat=lon=0;return;}const auto& a=*c.anchor; - world_geo(a.gps_global_pixel_x+lv_obj_get_width(c.map_container)/2-a.gps_tile_screen_x-a.gps_offset_x,a.gps_global_pixel_y+lv_obj_get_height(c.map_container)/2-a.gps_tile_screen_y-a.gps_offset_y,a.z,lat,lon); + const char* key = source == 1 ? "terrain" : source == 2 ? "satellite" + : "osm"; + return std::snprintf(out, n, "A:/maps/base/%s/%d/%d/%d.png", key, z, x, y) > 0; } -bool tile_in_rect(int x,int y,int w,int h,int margin){return x-margin&&y+256>-margin;} -void cleanup_tiles(TileContext& c){if(c.tiles){for(auto& t:*c.tiles){if(t.img_obj)lv_obj_delete(t.img_obj);if(t.contour_obj)lv_obj_delete(t.contour_obj);}c.tiles->clear();}if(c.has_map_data)*c.has_map_data=false;if(c.has_visible_map_data)*c.has_visible_map_data=false;} -void calculate_required_tiles(TileContext& c,double lat,double lon,int z,int px,int py,bool fix) +bool build_contour_tile_path(int z, int x, int y, char* out, size_t n) { return std::snprintf(out, n, "A:/maps/contour/major-50/%d/%d/%d.png", z, x, y) > 0; } +static bool exists(const char* path) { - update_map_anchor(c,lat,lon,z,px,py,fix);if(!c.tiles||!c.anchor||!c.anchor->valid)return; - bool any=false; - for(auto& t:*c.tiles) {t.visible=false;if(t.img_obj)lv_obj_add_flag(t.img_obj,LV_OBJ_FLAG_HIDDEN);} - const int w=lv_obj_get_width(c.map_container),h=lv_obj_get_height(c.map_container); - double center_lat,center_lon;get_screen_center_lat_lng(c,center_lat,center_lon); - for(auto xy:ui::map_tiles::tilesAround(center_lat,center_lon,z,w/256+2,h/256+2)) { - int sx,sy;if(!tile_screen_pos_xyz(c,xy.x,xy.y,z,sx,sy)||!tile_in_rect(sx,sy,w,h,0))continue; - auto found=std::find_if(c.tiles->begin(),c.tiles->end(),[&](const auto& t){return t.x==xy.x&&t.y==xy.y&&t.z==z&&t.map_source==source_id;}); - if(found==c.tiles->end()) {MapTile t;t.x=xy.x;t.y=xy.y;t.z=z;t.map_source=source_id;t.has_png_file=base_tile_available(z,t.x,t.y,source_id);c.tiles->push_back(t);found=c.tiles->end()-1;} - auto& tile=*found;if(!tile.has_png_file){missing=true;continue;} - if(!tile.img_obj){tile.img_obj=lv_image_create(c.map_container);char path[180];build_base_tile_path(z,tile.x,tile.y,source_id,path,sizeof(path));lv_image_set_src(tile.img_obj,path);} - lv_obj_set_pos(tile.img_obj,sx,sy);lv_obj_clear_flag(tile.img_obj,LV_OBJ_FLAG_HIDDEN);tile.visible=true;any=true; + FILE* f = std::fopen(path + 2, "rb"); + if (!f) return false; + std::fclose(f); + return true; +} +bool base_tile_available(int z, int x, int y, uint8_t source) +{ + char path[180]; + build_base_tile_path(z, x, y, source, path, sizeof(path)); + return exists(path); +} +bool map_source_directory_available(uint8_t source) { return source < 3; } +bool contour_directory_available() { return false; } +bool take_missing_tile_notice(uint8_t* out) +{ + if (out) *out = source_id; + const bool result = missing; + missing = false; + return result; +} +void set_map_render_options(uint8_t source, bool contour) +{ + source_id = sanitize_map_source(source); + contours = contour; +} +void update_map_anchor(TileContext& c, double lat, double lon, int zoom, int pan_x, int pan_y, bool fix) +{ + if (!c.anchor || !c.map_container) return; + auto& a = *c.anchor; + a.valid = fix; + if (!fix) return; + a.z = zoom; + a.n = std::ldexp(1.0, zoom); + int x, y; + latLngToTile(lat, lon, zoom, x, y); + a.gps_tile_x = x; + a.gps_tile_y = y; + a.gps_global_pixel_x = static_cast(std::lround(world_x(lon, zoom))); + a.gps_global_pixel_y = static_cast(std::lround(world_y(lat, zoom))); + a.gps_tile_pixel_x = x * 256; + a.gps_tile_pixel_y = y * 256; + a.gps_offset_x = a.gps_global_pixel_x - a.gps_tile_pixel_x; + a.gps_offset_y = a.gps_global_pixel_y - a.gps_tile_pixel_y; + a.gps_tile_screen_x = lv_obj_get_width(c.map_container) / 2 - a.gps_offset_x + pan_x; + a.gps_tile_screen_y = lv_obj_get_height(c.map_container) / 2 - a.gps_offset_y + pan_y; +} +bool gps_screen_pos(const TileContext& c, double lat, double lon, int& x, int& y) +{ + if (!c.anchor || !c.anchor->valid) return false; + const auto& a = *c.anchor; + double dx = world_x(lon, a.z) - a.gps_global_pixel_x; + const double size = world_size(a.z); + if (dx > size / 2) dx -= size; + if (dx < -size / 2) dx += size; + x = std::lround(a.gps_tile_screen_x + a.gps_offset_x + dx); + y = std::lround(a.gps_tile_screen_y + a.gps_offset_y + world_y(lat, a.z) - a.gps_global_pixel_y); + return true; +} +bool tile_screen_pos_xyz(const TileContext& c, int x, int y, int z, int& sx, int& sy) +{ + if (!c.anchor || !c.anchor->valid || c.anchor->z != z) return false; + const auto& a = *c.anchor; + int dx = x - a.gps_tile_x, n = 1 << z; + if (dx > n / 2) dx -= n; + if (dx < -n / 2) dx += n; + sx = a.gps_tile_screen_x + dx * 256; + sy = a.gps_tile_screen_y + (y - a.gps_tile_y) * 256; + return true; +} +void get_screen_center_lat_lng(const TileContext& c, double& lat, double& lon) +{ + if (!c.anchor || !c.anchor->valid) + { + lat = lon = 0; + return; } - if(c.has_map_data)*c.has_map_data=any;if(c.has_visible_map_data)*c.has_visible_map_data=any; + const auto& a = *c.anchor; + world_geo(a.gps_global_pixel_x + lv_obj_get_width(c.map_container) / 2 - a.gps_tile_screen_x - a.gps_offset_x, a.gps_global_pixel_y + lv_obj_get_height(c.map_container) / 2 - a.gps_tile_screen_y - a.gps_offset_y, a.z, lat, lon); +} +bool tile_in_rect(int x, int y, int w, int h, int margin) { return x < w + margin && y < h + margin && x + 256 > -margin && y + 256 > -margin; } +void cleanup_tiles(TileContext& c) +{ + if (c.tiles) + { + for (auto& t : *c.tiles) + { + if (t.img_obj) lv_obj_delete(t.img_obj); + if (t.contour_obj) lv_obj_delete(t.contour_obj); + } + c.tiles->clear(); + } + if (c.has_map_data) *c.has_map_data = false; + if (c.has_visible_map_data) *c.has_visible_map_data = false; +} +void calculate_required_tiles(TileContext& c, double lat, double lon, int z, int px, int py, bool fix) +{ + update_map_anchor(c, lat, lon, z, px, py, fix); + if (!c.tiles || !c.anchor || !c.anchor->valid) return; + bool any = false; + for (auto& t : *c.tiles) + { + t.visible = false; + if (t.img_obj) lv_obj_add_flag(t.img_obj, LV_OBJ_FLAG_HIDDEN); + } + const int w = lv_obj_get_width(c.map_container), h = lv_obj_get_height(c.map_container); + double center_lat, center_lon; + get_screen_center_lat_lng(c, center_lat, center_lon); + for (auto xy : ui::map_tiles::tilesAround(center_lat, center_lon, z, w / 256 + 2, h / 256 + 2)) + { + int sx, sy; + if (!tile_screen_pos_xyz(c, xy.x, xy.y, z, sx, sy) || !tile_in_rect(sx, sy, w, h, 0)) continue; + auto found = std::find_if(c.tiles->begin(), c.tiles->end(), [&](const auto& t) + { return t.x == xy.x && t.y == xy.y && t.z == z && t.map_source == source_id; }); + if (found == c.tiles->end()) + { + MapTile t; + t.x = xy.x; + t.y = xy.y; + t.z = z; + t.map_source = source_id; + t.has_png_file = base_tile_available(z, t.x, t.y, source_id); + c.tiles->push_back(t); + found = c.tiles->end() - 1; + } + auto& tile = *found; + if (!tile.has_png_file) + { + missing = true; + continue; + } + if (!tile.img_obj) + { + tile.img_obj = lv_image_create(c.map_container); + char path[180]; + build_base_tile_path(z, tile.x, tile.y, source_id, path, sizeof(path)); + lv_image_set_src(tile.img_obj, path); + } + lv_obj_set_pos(tile.img_obj, sx, sy); + lv_obj_clear_flag(tile.img_obj, LV_OBJ_FLAG_HIDDEN); + tile.visible = true; + any = true; + } + if (c.has_map_data) *c.has_map_data = any; + if (c.has_visible_map_data) *c.has_visible_map_data = any; +} +void tile_loader_step(TileContext&) {} +void init_tile_context(TileContext& c, lv_obj_t* obj, MapAnchor* anchor, std::vector* tiles, ui::map_tiles::MapTileRenderQueue* queue, bool* has, bool* visible) { c = {obj, anchor, tiles, queue, has, visible}; } +void release_tile_context(TileContext& c) +{ + cleanup_tiles(c); + c = {}; } -void tile_loader_step(TileContext&){} -void init_tile_context(TileContext& c,lv_obj_t* obj,MapAnchor* anchor,std::vector* tiles,ui::map_tiles::MapTileRenderQueue* queue,bool* has,bool* visible){c={obj,anchor,tiles,queue,has,visible};} -void release_tile_context(TileContext& c){cleanup_tiles(c);c={};} diff --git a/tools/web_simulator/preview_platform.cpp b/tools/web_simulator/preview_platform.cpp index d5ebb68a..035e6160 100644 --- a/tools/web_simulator/preview_platform.cpp +++ b/tools/web_simulator/preview_platform.cpp @@ -4,13 +4,21 @@ #include "ui/widgets/foreground_operation_overlay.h" #include -namespace platform::ui::device { +namespace platform::ui::device +{ BatteryInfo battery_info() { return {true, false, 85}; } void handle_low_battery(const BatteryInfo&) {} -} -namespace ui::widgets::foreground_operation { +} // namespace platform::ui::device +namespace ui::widgets::foreground_operation +{ Snapshot make_snapshot(Slot slot, Policy policy, Priority priority, const char*, const char*, int, const char*, uint32_t) -{ Snapshot s; s.slot=slot; s.policy=policy; s.priority=priority; return s; } -void publish(const Snapshot&) {} -void clear(Slot,uint32_t) {} +{ + Snapshot s; + s.slot = slot; + s.policy = policy; + s.priority = priority; + return s; } +void publish(const Snapshot&) {} +void clear(Slot, uint32_t) {} +} // namespace ui::widgets::foreground_operation diff --git a/tools/web_simulator/preview_radio.cpp b/tools/web_simulator/preview_radio.cpp index e62c3b9a..e36bc661 100644 --- a/tools/web_simulator/preview_radio.cpp +++ b/tools/web_simulator/preview_radio.cpp @@ -1,12 +1,17 @@ #include "platform/ui/lora_runtime.h" -namespace platform::ui::lora { -static bool acquired=false; -bool acquire(){acquired=true;return true;} -bool is_online(){return acquired;} -bool configure_receive(float,const ReceiveConfig&){return acquired;} -bool transmit_packet(const uint8_t*,std::size_t){return false;} -// A quiet simulated receiver must never invent OBSERVED/CONFIRMED evidence. -bool poll_received_packet(uint8_t*,std::size_t,ReceivedPacket*){return false;} -float read_instant_rssi(){return -120.f;} -void release(){acquired=false;} +namespace platform::ui::lora +{ +static bool acquired = false; +bool acquire() +{ + acquired = true; + return true; } +bool is_online() { return acquired; } +bool configure_receive(float, const ReceiveConfig&) { return acquired; } +bool transmit_packet(const uint8_t*, std::size_t) { return false; } +// A quiet simulated receiver must never invent OBSERVED/CONFIRMED evidence. +bool poll_received_packet(uint8_t*, std::size_t, ReceivedPacket*) { return false; } +float read_instant_rssi() { return -120.f; } +void release() { acquired = false; } +} // namespace platform::ui::lora diff --git a/tools/web_simulator/preview_reticulum.cpp b/tools/web_simulator/preview_reticulum.cpp index eeda6902..a5681393 100644 --- a/tools/web_simulator/preview_reticulum.cpp +++ b/tools/web_simulator/preview_reticulum.cpp @@ -5,29 +5,112 @@ #include #include -namespace platform::ui::reticulum_directory { -static Status ready(){Status s;s.supported=true;s.sd_present=true;s.file_present=true;s.loaded=true;return s;} -Status load_announces(AnnounceRecord* out,std::size_t max,std::size_t* count){*count=max?1:0;if(*count){out[0]={};out[0].valid=true;std::memset(out[0].destination_hash,0x11,16);std::memset(out[0].identity_hash,0x22,16);out[0].aspect=AnnounceAspect::NomadNetworkNode;out[0].source=EntrySource::RuntimeRx;out[0].hops=1;std::snprintf(out[0].display_name,sizeof(out[0].display_name),"Ridge shelter");}return ready();} -Status load_lxmf_addresses(LxmfAddressRecord*,std::size_t,std::size_t* count){*count=0;return ready();} -Status load_lxmf_addresses_matching(const char*,LxmfAddressRecord* out,std::size_t n,std::size_t* count){return load_lxmf_addresses(out,n,count);} -Status find_lxmf_address_by_destination(const uint8_t*,LxmfAddressRecord* out){if(out)*out={};return ready();} -Status set_lxmf_address_favorite_now(const uint8_t*,bool){return ready();} +namespace platform::ui::reticulum_directory +{ +static Status ready() +{ + Status s; + s.supported = true; + s.sd_present = true; + s.file_present = true; + s.loaded = true; + return s; } -namespace platform::ui::reticulum_page { -static bool cached=true; -static bool completed=false; -static Status ready(){Status s;s.supported=true;s.sd_present=true;return s;} -bool normalize_path(const char* path,char* out,std::size_t n){if(!out||!n)return false;const char* value=path&&*path?path:"/page/index.mu";return std::snprintf(out,n,"%s",value)(n);} -Status request_cached_page_load(const char*,const char*,bool){auto s=ready();s.cache_checked=true;s.file_present=cached;return s;} -Status poll_cached_page_load(const char*,const char*,char* body,std::size_t cap,std::size_t* count){ - auto s=ready();s.cache_checked=true;s.file_present=cached;s.loaded=cached; - const char* text=">Ridge shelter\nWelcome to the field bulletin.\n\nWater point: east side.\nTrail: open to Pine Ridge.\n\nThis bulletin is sample data supplied to the firmware Micron renderer.\n"; - *count=0;if(cached&&body&&cap){*count=std::min(cap-1,std::strlen(text));std::memcpy(body,text,*count);body[*count]=0;}return s; +Status load_announces(AnnounceRecord* out, std::size_t max, std::size_t* count) +{ + *count = max ? 1 : 0; + if (*count) + { + out[0] = {}; + out[0].valid = true; + std::memset(out[0].destination_hash, 0x11, 16); + std::memset(out[0].identity_hash, 0x22, 16); + out[0].aspect = AnnounceAspect::NomadNetworkNode; + out[0].source = EntrySource::RuntimeRx; + out[0].hops = 1; + std::snprintf(out[0].display_name, sizeof(out[0].display_name), "Ridge shelter"); + } + return ready(); } -Status clear_cached_page(const char*,const char*){cached=false;return ready();} -Status request_page(const char*,const char*){cached=true;completed=true;auto s=ready();s.request_started=true;return s;} -Status request_page_with_data(const char* dest,const char* path,const uint8_t*,std::size_t){return request_page(dest,path);} -Status cancel_request(const char*,const char*){auto s=ready();s.cancelled=true;return s;} -void clear_request_progress(const char*,const char*){completed=false;} -RequestProgress get_request_progress(const char*,const char*){RequestProgress p;p.complete=completed;p.progress_percent=completed?100:-1;return p;} +Status load_lxmf_addresses(LxmfAddressRecord*, std::size_t, std::size_t* count) +{ + *count = 0; + return ready(); } +Status load_lxmf_addresses_matching(const char*, LxmfAddressRecord* out, std::size_t n, std::size_t* count) { return load_lxmf_addresses(out, n, count); } +Status find_lxmf_address_by_destination(const uint8_t*, LxmfAddressRecord* out) +{ + if (out) *out = {}; + return ready(); +} +Status set_lxmf_address_favorite_now(const uint8_t*, bool) { return ready(); } +} // namespace platform::ui::reticulum_directory +namespace platform::ui::reticulum_page +{ +static bool cached = true; +static bool completed = false; +static Status ready() +{ + Status s; + s.supported = true; + s.sd_present = true; + return s; +} +bool normalize_path(const char* path, char* out, std::size_t n) +{ + if (!out || !n) return false; + const char* value = path && *path ? path : "/page/index.mu"; + return std::snprintf(out, n, "%s", value) < static_cast(n); +} +Status request_cached_page_load(const char*, const char*, bool) +{ + auto s = ready(); + s.cache_checked = true; + s.file_present = cached; + return s; +} +Status poll_cached_page_load(const char*, const char*, char* body, std::size_t cap, std::size_t* count) +{ + auto s = ready(); + s.cache_checked = true; + s.file_present = cached; + s.loaded = cached; + const char* text = ">Ridge shelter\nWelcome to the field bulletin.\n\nWater point: east side.\nTrail: open to Pine Ridge.\n\nThis bulletin is sample data supplied to the firmware Micron renderer.\n"; + *count = 0; + if (cached && body && cap) + { + *count = std::min(cap - 1, std::strlen(text)); + std::memcpy(body, text, *count); + body[*count] = 0; + } + return s; +} +Status clear_cached_page(const char*, const char*) +{ + cached = false; + return ready(); +} +Status request_page(const char*, const char*) +{ + cached = true; + completed = true; + auto s = ready(); + s.request_started = true; + return s; +} +Status request_page_with_data(const char* dest, const char* path, const uint8_t*, std::size_t) { return request_page(dest, path); } +Status cancel_request(const char*, const char*) +{ + auto s = ready(); + s.cancelled = true; + return s; +} +void clear_request_progress(const char*, const char*) { completed = false; } +RequestProgress get_request_progress(const char*, const char*) +{ + RequestProgress p; + p.complete = completed; + p.progress_percent = completed ? 100 : -1; + return p; +} +} // namespace platform::ui::reticulum_page diff --git a/tools/web_simulator/preview_sensors.cpp b/tools/web_simulator/preview_sensors.cpp index 6bd5a256..4f1a43d9 100644 --- a/tools/web_simulator/preview_sensors.cpp +++ b/tools/web_simulator/preview_sensors.cpp @@ -1,57 +1,108 @@ #include "platform/ui/device_runtime.h" #include "platform/ui/gps_runtime.h" -#include "platform/ui/walkie_runtime.h" -#include "platform/ui/usb_support_runtime.h" #include "platform/ui/screen_runtime.h" +#include "platform/ui/usb_support_runtime.h" +#include "platform/ui/walkie_runtime.h" +#include "ui/runtime/ui_feedback.h" #include "ui/ui_common.h" #include "ui/widgets/top_bar_power_presenter.h" -#include "ui/runtime/ui_feedback.h" #include #include -namespace platform::ui::device { +namespace platform::ui::device +{ bool gps_supported() { return true; } bool card_ready() { return true; } bool sd_ready() { return true; } int power_tier() { return 0; } bool gps_ready() { return true; } void delay_ms(uint32_t) {} -} -namespace platform::ui::gps { +} // namespace platform::ui::device +namespace platform::ui::gps +{ static bool sample_fix = true; -GpsState get_data() { GpsState s; s.lat=31.2304; s.lng=121.4737; s.alt_m=842; s.satellites=12; s.valid=sample_fix; s.has_alt=sample_fix; return s; } +GpsState get_data() +{ + GpsState s; + s.lat = 31.2304; + s.lng = 121.4737; + s.alt_m = 842; + s.satellites = 12; + s.valid = sample_fix; + s.has_alt = sample_fix; + return s; +} bool get_gnss_snapshot(GnssSatInfo* out, std::size_t max, std::size_t* count, GnssStatus* status) { - *count=std::min(12,max); - for(std::size_t i=0;i<*count;++i) out[i]={static_cast(i+1),static_cast(i%4),static_cast(i*29),static_cast(15+(i*13)%70),static_cast(25+i),sample_fix}; - status->sats_in_use=sample_fix?12:0; status->sats_in_view=12; status->hdop=sample_fix?.9f:0; status->fix=sample_fix?GnssFix::FIX3D:GnssFix::NOFIX; + *count = std::min(12, max); + for (std::size_t i = 0; i < *count; ++i) out[i] = {static_cast(i + 1), static_cast(i % 4), static_cast(i * 29), static_cast(15 + (i * 13) % 70), static_cast(25 + i), sample_fix}; + status->sats_in_use = sample_fix ? 12 : 0; + status->sats_in_view = 12; + status->hdop = sample_fix ? .9f : 0; + status->fix = sample_fix ? GnssFix::FIX3D : GnssFix::NOFIX; return true; } -GpsDiagnosticsSnapshot diagnostics() { GpsDiagnosticsSnapshot s; s.enabled=true; s.powered=true; s.ready=true; s.has_fix=sample_fix; s.sats_in_view=12; s.sats_in_use=sample_fix?12:0; s.satellites=s.sats_in_use; s.last_rx_age_ms=100; s.chars_recent=512; return s; } +GpsDiagnosticsSnapshot diagnostics() +{ + GpsDiagnosticsSnapshot s; + s.enabled = true; + s.powered = true; + s.ready = true; + s.has_fix = sample_fix; + s.sats_in_view = 12; + s.sats_in_use = sample_fix ? 12 : 0; + s.satellites = s.sats_in_use; + s.last_rx_age_ms = 100; + s.chars_recent = 512; + return s; +} void acquire_power_lease(const char*) {} void release_power_lease(const char*) {} +} // namespace platform::ui::gps +namespace platform::ui::walkie +{ +static Status status{false, false, false, 0, 0, 433.175f}; +bool is_supported() { return true; } +bool start() +{ + status.active = true; + return true; } -namespace platform::ui::walkie { -static Status status{false,false,false,0,0,433.175f}; -bool is_supported(){return true;} -bool start(){status.active=true;return true;} -void stop(){status.active=false;status.tx=false;} -bool is_active(){return status.active;} -void set_ptt(bool pressed){status.tx=pressed&&status.active;status.tx_level=status.tx?65:0;} -bool set_monitor_enabled(bool enabled){status.monitor_enabled=enabled;status.active=enabled;return true;} -bool monitor_enabled(){return status.monitor_enabled;} -int volume(){return 60;} -Status get_status(){return status;} -const char* last_error(){return "";} +void stop() +{ + status.active = false; + status.tx = false; } -namespace platform::ui::usb_support { +bool is_active() { return status.active; } +void set_ptt(bool pressed) +{ + status.tx = pressed && status.active; + status.tx_level = status.tx ? 65 : 0; +} +bool set_monitor_enabled(bool enabled) +{ + status.monitor_enabled = enabled; + status.active = enabled; + return true; +} +bool monitor_enabled() { return status.monitor_enabled; } +int volume() { return 60; } +Status get_status() { return status; } +const char* last_error() { return ""; } +} // namespace platform::ui::walkie +namespace platform::ui::usb_support +{ static Status status{}; -bool is_supported(){return true;} -bool start(){status={true,false,"USB Active"};return true;} -void stop(){status={false,false,"USB Idle"};} -Status get_status(){return status;} -bool prepare_mass_storage_mode(){return true;} -void restore_mass_storage_mode(){} +bool is_supported() { return true; } +bool start() +{ + status = {true, false, "USB Active"}; + return true; } -void ui_update_top_bar_battery(ui::widgets::TopBar&){ui::widgets::top_bar_power::refresh_now();} -void ui_set_overlay_active(bool){} +void stop() { status = {false, false, "USB Idle"}; } +Status get_status() { return status; } +bool prepare_mass_storage_mode() { return true; } +void restore_mass_storage_mode() {} +} // namespace platform::ui::usb_support +void ui_update_top_bar_battery(ui::widgets::TopBar&) { ui::widgets::top_bar_power::refresh_now(); } +void ui_set_overlay_active(bool) {} diff --git a/tools/web_simulator/preview_settings.cpp b/tools/web_simulator/preview_settings.cpp index 6b88cd8a..6fec8b40 100644 --- a/tools/web_simulator/preview_settings.cpp +++ b/tools/web_simulator/preview_settings.cpp @@ -1,54 +1,95 @@ -#include "platform/ui/settings_store.h" #include "platform/ui/device_runtime.h" #include "platform/ui/gps_runtime.h" +#include "platform/ui/settings_store.h" #include "platform/ui/wifi_runtime.h" -#include -#include +#include #include #include -#include -namespace platform::ui::settings_store { -static std::map values; -static std::string key_for(const char* ns,const char* key){return std::string(ns)+"/"+key;} -int get_int(const char* ns,const char* key,int fallback){auto i=values.find(key_for(ns,key));return i==values.end()?fallback:std::strtol(i->second.c_str(),nullptr,10);} -uint32_t get_uint(const char* ns,const char* key,uint32_t fallback){auto i=values.find(key_for(ns,key));return i==values.end()?fallback:std::strtoul(i->second.c_str(),nullptr,10);} -bool get_bool(const char* ns,const char* key,bool fallback){return get_int(ns,key,fallback?1:0)!=0;} -void put_int(const char* ns,const char* key,int value){values[key_for(ns,key)]=std::to_string(value);} -void put_uint(const char* ns,const char* key,uint32_t value){values[key_for(ns,key)]=std::to_string(value);} -void put_bool(const char* ns,const char* key,bool value){put_int(ns,key,value?1:0);} -bool put_string(const char* ns,const char* key,const char* value){values[key_for(ns,key)]=value?value:"";return true;} -bool get_string(const char* ns,const char* key,std::string& out){auto i=values.find(key_for(ns,key));if(i==values.end())return false;out=i->second;return true;} -void remove_keys(const char* ns,const char* const* keys,std::size_t count){for(size_t i=0;ifirst.compare(0,prefix.size(),prefix)==0)i=values.erase(i);else++i;} -void begin_change_batch(){} -void end_change_batch(){} +#include +#include +namespace platform::ui::settings_store +{ +static std::map values; +static std::string key_for(const char* ns, const char* key) { return std::string(ns) + "/" + key; } +int get_int(const char* ns, const char* key, int fallback) +{ + auto i = values.find(key_for(ns, key)); + return i == values.end() ? fallback : std::strtol(i->second.c_str(), nullptr, 10); } -namespace platform::ui::device { -static uint8_t tone_volume=60; -uint8_t default_message_tone_volume(){return tone_volume;} -void set_message_tone_volume(uint8_t value){tone_volume=value;} -void play_message_tone(){} -void trigger_haptic(){} -bool supports_configurable_battery_gauge(){return true;} -void reload_configurable_battery_gauge(){} -void restart(){} +uint32_t get_uint(const char* ns, const char* key, uint32_t fallback) +{ + auto i = values.find(key_for(ns, key)); + return i == values.end() ? fallback : std::strtoul(i->second.c_str(), nullptr, 10); } -namespace platform::ui::gps { -bool supports_receiver_baud_setting(){return true;} -bool supports_receiver_init_policy_settings(){return true;} -bool supports_gnss_runtime_settings(){return true;} -bool supports_collection_interval_setting(){return true;} -bool supports_altitude_reference_setting(){return true;} -bool supports_coordinate_format_setting(){return true;} -bool supports_external_nmea_output_setting(){return true;} -void set_enabled(bool value){platform::ui::settings_store::put_bool("preview","gps",value);} -void set_collection_interval(uint32_t value){platform::ui::settings_store::put_uint("preview","gps_interval",value);} -void set_receiver_init_config(const GpsReceiverInitConfig&){} -void set_gnss_config(uint8_t,uint8_t){} -void set_power_strategy(uint8_t){} -void set_external_nmea_config(uint8_t,uint8_t){} +bool get_bool(const char* ns, const char* key, bool fallback) { return get_int(ns, key, fallback ? 1 : 0) != 0; } +void put_int(const char* ns, const char* key, int value) { values[key_for(ns, key)] = std::to_string(value); } +void put_uint(const char* ns, const char* key, uint32_t value) { values[key_for(ns, key)] = std::to_string(value); } +void put_bool(const char* ns, const char* key, bool value) { put_int(ns, key, value ? 1 : 0); } +bool put_string(const char* ns, const char* key, const char* value) +{ + values[key_for(ns, key)] = value ? value : ""; + return true; } -namespace platform::ui::wifi { -bool find_saved_config(const char*,Config& out){return load_config(out);} -bool scan(ScanResult* out,std::size_t capacity,std::size_t& count){count=capacity?1:0;if(count){std::snprintf(out[0].ssid,sizeof(out[0].ssid),"Preview network");out[0].rssi=-55;out[0].requires_password=false;}return true;} +bool get_string(const char* ns, const char* key, std::string& out) +{ + auto i = values.find(key_for(ns, key)); + if (i == values.end()) return false; + out = i->second; + return true; } +void remove_keys(const char* ns, const char* const* keys, std::size_t count) +{ + for (size_t i = 0; i < count; ++i) values.erase(key_for(ns, keys[i])); +} +void clear_namespace(const char* ns) +{ + const auto prefix = std::string(ns) + "/"; + for (auto i = values.begin(); i != values.end();) + if (i->first.compare(0, prefix.size(), prefix) == 0) i = values.erase(i); + else ++i; +} +void begin_change_batch() {} +void end_change_batch() {} +} // namespace platform::ui::settings_store +namespace platform::ui::device +{ +static uint8_t tone_volume = 60; +uint8_t default_message_tone_volume() { return tone_volume; } +void set_message_tone_volume(uint8_t value) { tone_volume = value; } +void play_message_tone() {} +void trigger_haptic() {} +bool supports_configurable_battery_gauge() { return true; } +void reload_configurable_battery_gauge() {} +void restart() {} +} // namespace platform::ui::device +namespace platform::ui::gps +{ +bool supports_receiver_baud_setting() { return true; } +bool supports_receiver_init_policy_settings() { return true; } +bool supports_gnss_runtime_settings() { return true; } +bool supports_collection_interval_setting() { return true; } +bool supports_altitude_reference_setting() { return true; } +bool supports_coordinate_format_setting() { return true; } +bool supports_external_nmea_output_setting() { return true; } +void set_enabled(bool value) { platform::ui::settings_store::put_bool("preview", "gps", value); } +void set_collection_interval(uint32_t value) { platform::ui::settings_store::put_uint("preview", "gps_interval", value); } +void set_receiver_init_config(const GpsReceiverInitConfig&) {} +void set_gnss_config(uint8_t, uint8_t) {} +void set_power_strategy(uint8_t) {} +void set_external_nmea_config(uint8_t, uint8_t) {} +} // namespace platform::ui::gps +namespace platform::ui::wifi +{ +bool find_saved_config(const char*, Config& out) { return load_config(out); } +bool scan(ScanResult* out, std::size_t capacity, std::size_t& count) +{ + count = capacity ? 1 : 0; + if (count) + { + std::snprintf(out[0].ssid, sizeof(out[0].ssid), "Preview network"); + out[0].rssi = -55; + out[0].requires_password = false; + } + return true; +} +} // namespace platform::ui::wifi diff --git a/tools/web_simulator/preview_sstv.cpp b/tools/web_simulator/preview_sstv.cpp index 83de385d..f76e2abb 100644 --- a/tools/web_simulator/preview_sstv.cpp +++ b/tools/web_simulator/preview_sstv.cpp @@ -1,19 +1,37 @@ #include "platform/ui/sstv_runtime.h" #include "sys/clock.h" #include -namespace platform::ui::sstv { -static bool active=false; -static uint32_t began=0; -static uint16_t image[320*256]; -bool is_supported(){return true;} -bool start(){active=true;began=sys::millis_now();for(int y=0;y<256;++y)for(int x=0;x<320;++x)image[y*320+x]=static_cast(((x/11)&31)<<11|((y/4)&63)<<5|((x+y)/19&31));return true;} -void stop(){active=false;} -bool is_active(){return active;} -Status get_status(){Status s;if(!active)return s;s.progress=std::min(1.0f,(sys::millis_now()-began)/12000.f);s.line=static_cast(s.progress*256);s.audio_level=.45f;s.has_image=true;s.state=s.progress>=1?State::Complete:State::Receiving;return s;} -const char* last_error(){return "";} -const char* last_saved_path(){return "";} -const char* mode_name(){return "Sample RX";} -const uint16_t* framebuffer(){return image;} -uint16_t frame_width(){return 320;} -uint16_t frame_height(){return 256;} +namespace platform::ui::sstv +{ +static bool active = false; +static uint32_t began = 0; +static uint16_t image[320 * 256]; +bool is_supported() { return true; } +bool start() +{ + active = true; + began = sys::millis_now(); + for (int y = 0; y < 256; ++y) + for (int x = 0; x < 320; ++x) image[y * 320 + x] = static_cast(((x / 11) & 31) << 11 | ((y / 4) & 63) << 5 | ((x + y) / 19 & 31)); + return true; } +void stop() { active = false; } +bool is_active() { return active; } +Status get_status() +{ + Status s; + if (!active) return s; + s.progress = std::min(1.0f, (sys::millis_now() - began) / 12000.f); + s.line = static_cast(s.progress * 256); + s.audio_level = .45f; + s.has_image = true; + s.state = s.progress >= 1 ? State::Complete : State::Receiving; + return s; +} +const char* last_error() { return ""; } +const char* last_saved_path() { return ""; } +const char* mode_name() { return "Sample RX"; } +const uint16_t* framebuffer() { return image; } +uint16_t frame_width() { return 320; } +uint16_t frame_height() { return 256; } +} // namespace platform::ui::sstv diff --git a/tools/web_simulator/preview_status.cpp b/tools/web_simulator/preview_status.cpp index 317293ed..7b4b1168 100644 --- a/tools/web_simulator/preview_status.cpp +++ b/tools/web_simulator/preview_status.cpp @@ -1,20 +1,28 @@ #include "platform/ui/device_runtime.h" +#include "platform/ui/lora_runtime.h" #include "platform/ui/time_runtime.h" #include "platform/ui/tracker_runtime.h" #include "platform/ui/wifi_runtime.h" -#include "platform/ui/lora_runtime.h" #include -namespace platform::ui::device { -MemoryStats memory_stats(){return {512*1024,260*1024,8*1024*1024,6*1024*1024,true};} -bool supports_screen_brightness(){return true;} -bool supports_keyboard_backlight(){return true;} -static uint8_t brightness=200,keys=0; -uint8_t screen_brightness(){return brightness;} -uint8_t screen_brightness_max(){return 255;} -void set_screen_brightness(uint8_t value){brightness=value;} -uint8_t keyboard_backlight(){return keys;} -uint8_t keyboard_backlight_max(){return 255;} -void set_keyboard_backlight(uint8_t value){keys=value;} -} -namespace platform::ui::lora {bool is_supported(){return true;}} -namespace platform::ui::gps {bool is_enabled(){return true;} bool is_powered(){return true;}} +namespace platform::ui::device +{ +MemoryStats memory_stats() { return {512 * 1024, 260 * 1024, 8 * 1024 * 1024, 6 * 1024 * 1024, true}; } +bool supports_screen_brightness() { return true; } +bool supports_keyboard_backlight() { return true; } +static uint8_t brightness = 200, keys = 0; +uint8_t screen_brightness() { return brightness; } +uint8_t screen_brightness_max() { return 255; } +void set_screen_brightness(uint8_t value) { brightness = value; } +uint8_t keyboard_backlight() { return keys; } +uint8_t keyboard_backlight_max() { return 255; } +void set_keyboard_backlight(uint8_t value) { keys = value; } +} // namespace platform::ui::device +namespace platform::ui::lora +{ +bool is_supported() { return true; } +} // namespace platform::ui::lora +namespace platform::ui::gps +{ +bool is_enabled() { return true; } +bool is_powered() { return true; } +} // namespace platform::ui::gps