diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 5a2fd7df..7ea9c266 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -6,9 +6,13 @@ on: - main paths: - site/** + - packs/** - docs/images/** + - scripts/build_pack_repository.py - scripts/prepare_pages_site.py - scripts/webflash_targets.py + - tools/generate_binfont_with_lv_font_conv.py + - tools/fonts/** - .github/workflows/ci.yml - .github/workflows/pages.yml release: @@ -39,6 +43,11 @@ jobs: with: python-version: "3.11" + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + - name: Configure GitHub Pages uses: actions/configure-pages@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f056bf6..1fb6261b 100644 Binary files a/CHANGELOG.md and b/CHANGELOG.md differ diff --git a/apps/esp_idf/CMakeLists.txt b/apps/esp_idf/CMakeLists.txt index c97a0ec9..cb21a52c 100644 --- a/apps/esp_idf/CMakeLists.txt +++ b/apps/esp_idf/CMakeLists.txt @@ -1,5 +1,5 @@ -# Shared ESP-IDF application component. -# This component builds the migrated shared modules plus the shared IDF app shell. +#Shared ESP - IDF application component. +#This component builds the migrated shared modules plus the shared IDF app shell. file(GLOB_RECURSE trail_mate_idf_local_sources ${CMAKE_CURRENT_LIST_DIR}/src/*.c @@ -53,8 +53,12 @@ list(APPEND trail_mate_idf_external_sources set(trail_mate_idf_requires esp_app_format esp_common + esp_event + esp_http_client esp_lcd + esp_netif esp_timer + json log lvgl__lvgl nvs_flash @@ -77,6 +81,7 @@ set(trail_mate_idf_ui_shared_sources ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/app_runtime.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/app_catalog_builder.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/formatters.cpp + ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/loop_shell.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/startup_shell.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/menu/menu_runtime.cpp @@ -89,10 +94,13 @@ set(trail_mate_idf_ui_shared_sources ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/widgets/toast/toast_widget.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/widgets/ime/pinyin_ime.cpp + ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/fonts/font_utils.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/components/two_pane_layout.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/components/two_pane_styles.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/components/two_pane_nav.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/page/page_profile.cpp + ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/runtime/memory_profile.cpp + ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/runtime/pack_repository.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/common/placeholder_page.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/chat/chat_compose_components.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/chat/chat_compose_input.cpp @@ -125,6 +133,8 @@ set(trail_mate_idf_ui_shared_sources ${CMAKE_SOURCE_DIR}/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/energy_sweep/energy_sweep_page_shell.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/energy_sweep/energy_sweep_page_runtime.cpp + ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/extensions/extensions_page_shell.cpp + ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/sstv/sstv_page_shell.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/sstv/sstv_page_runtime.cpp ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/screens/pc_link/pc_link_page_shell.cpp @@ -176,10 +186,11 @@ set(trail_mate_idf_ui_shared_sources ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/route_topbar.c ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/team_topbar.c ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/tracker_topbar.c + ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/wifi_topbar.c ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/ble_topbar.c ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/Satellite.c ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/contact.c - ${CMAKE_SOURCE_DIR}/platform/esp/arduino_common/src/ui/assets/fonts/lv_font_noto_cjk_16_2bpp.c + ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/ext.c ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/rally.c ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/Spectrum.c ${CMAKE_SOURCE_DIR}/modules/ui_shared/src/ui/assets/sos.c diff --git a/apps/esp_idf/src/app_registry.cpp b/apps/esp_idf/src/app_registry.cpp index eb406d1c..20282d6c 100644 --- a/apps/esp_idf/src/app_registry.cpp +++ b/apps/esp_idf/src/app_registry.cpp @@ -24,6 +24,7 @@ ui::app_catalog_builder::FeatureFlags buildFeatureFlags() flags.include_pc_link = platform::ui::hostlink::is_supported(); flags.include_sstv = platform::ui::sstv::is_supported(); flags.include_usb = platform::ui::usb_support::is_supported(); + flags.include_extensions = true; flags.include_walkie_talkie = platform::ui::walkie::is_supported(); return flags; } diff --git a/apps/esp_pio/src/app_context.cpp b/apps/esp_pio/src/app_context.cpp index 54f568ce..25a9f19d 100644 --- a/apps/esp_pio/src/app_context.cpp +++ b/apps/esp_pio/src/app_context.cpp @@ -240,7 +240,7 @@ void AppContext::applyPrivacyConfig() { if (mesh_router_) { - mesh_router_->setPrivacyConfig(config_.privacy_encrypt_mode, config_.privacy_pki); + mesh_router_->setPrivacyConfig(config_.privacy_encrypt_mode); } } @@ -311,7 +311,7 @@ bool AppContext::switchMeshProtocol(chat::MeshProtocol protocol, bool persist) short_name, sizeof(short_name)); backend->setUserInfo(long_name, short_name); backend->setNetworkLimits(config_.net_duty_cycle, config_.net_channel_util); - backend->setPrivacyConfig(config_.privacy_encrypt_mode, config_.privacy_pki); + backend->setPrivacyConfig(config_.privacy_encrypt_mode); if (!mesh_router_->installBackend(protocol, std::move(backend))) { diff --git a/apps/esp_pio/src/app_registry.cpp b/apps/esp_pio/src/app_registry.cpp index a841ce53..4b9931f4 100644 --- a/apps/esp_pio/src/app_registry.cpp +++ b/apps/esp_pio/src/app_registry.cpp @@ -27,6 +27,7 @@ ui::app_catalog_builder::FeatureFlags buildFeatureFlags() flags.include_pc_link = platform::ui::hostlink::is_supported(); flags.include_sstv = platform::ui::sstv::is_supported(); flags.include_usb = platform::ui::usb_support::is_supported() && platform::ui::device::sd_ready(); + flags.include_extensions = true; flags.include_walkie_talkie = platform::ui::walkie::is_supported(); APP_REG_LOG( "flags gps_map=%d skyplot=%d tracker=%d chat=%d sweep=%d pc_link=%d sstv=%d usb=%d walkie=%d gps_supported=%d gps_ready=%d sd_ready=%d\n", diff --git a/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp b/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp index d9153f02..2e11b0b1 100644 --- a/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp +++ b/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp @@ -406,7 +406,7 @@ void AppFacadeRuntime::applyPrivacyConfig() } if (mesh_router_) { - mesh_router_->setPrivacyConfig(config_.privacy_encrypt_mode, config_.privacy_pki); + mesh_router_->setPrivacyConfig(config_.privacy_encrypt_mode); } } diff --git a/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp b/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp index 4ce06f31..d55aac98 100644 --- a/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp +++ b/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp @@ -115,6 +115,17 @@ BatteryInfo battery_info() return info; } +MemoryStats memory_stats() +{ + MemoryStats stats{}; + return stats; +} + +const char* firmware_version() +{ + return "unknown"; +} + void handle_low_battery(const BatteryInfo& info) { (void)info; diff --git a/docs/LOCALE_PACKS.md b/docs/LOCALE_PACKS.md new file mode 100644 index 00000000..af41b770 --- /dev/null +++ b/docs/LOCALE_PACKS.md @@ -0,0 +1,509 @@ +# Locale, Font, and IME Packs + +## Goals + +The pack system exists to solve four problems at the same time: + +1. Keep the firmware image minimal. English stays built in, large script assets move to SD. +2. Keep `installed` separate from `loaded`. A pack being present on SD must not mean its font is already in RAM. +3. Let mixed-language content render correctly. A Spanish UI still needs to show a Chinese contact name if that font pack is installed. +4. Bound RAM usage across very different devices, including no-PSRAM targets. + +This is now a first-class runtime architecture, not a collection of text-based bypasses. + +## Core Model + +Trail Mate treats localization as three explicit pack types: + +- `Locale Pack` + Owns translated UI strings and declares which UI font pack, content font pack, and optional IME pack it needs. +- `Font Pack` + Owns glyph coverage metadata plus the external `font.bin` asset. +- `IME Pack` + Declares script-specific input behavior such as Pinyin. + +The Settings page selects a `Locale Pack`. It does not directly choose a font or IME. + +## Built-In vs External + +The firmware intentionally ships with the smallest built-in baseline: + +- built-in locale packs: `en` +- built-in font packs: `builtin-latin-ui` +- built-in IME packs: none + +English is therefore always available, even when the SD card has no language packs at all. + +External packs are discovered from the SD card under: + +```text +/trailmate/packs/fonts//manifest.ini +/trailmate/packs/locales//manifest.ini +/trailmate/packs/ime//manifest.ini +``` + +At boot the registry is built in two phases: + +1. Catalog every built-in pack and every external pack manifest. +2. Resolve dependencies and decide which locales are allowed on the current memory profile. + +Important: manifest discovery is cheap. External fonts are not loaded into RAM during cataloging. + +## Three Layouts + +The same localization asset now exists in three different representations, and keeping them +separate is intentional: + +### Repository Source Bundle + +What lives under `packs//` in Git. + +- contains runtime manifests +- contains human-facing package metadata +- contains build-only files such as `charset.txt` and `build.ini` +- may omit tracked `font.bin`, because the Pages build can regenerate it + +### Installed Runtime Layout + +What the firmware actually scans on SD: + +```text +/trailmate/packs/fonts//... +/trailmate/packs/locales//... +/trailmate/packs/ime//... +``` + +This is the only layout the runtime registry understands. + +### Distribution Package + +What the website and future Extensions page should traffic in: + +- one zip per installable bundle +- one package manifest with version and compatibility metadata +- one description file for UI presentation +- one remote catalog entry for discovery and update checks + +The runtime does not scan zip files directly. The package manager layer downloads a zip, +unpacks its payload into the installed runtime layout, then asks the runtime registry to +refresh. + +## Installed Is Not Loaded + +This distinction is the center of the design. + +- `Installed` + The manifest exists on SD and the registry knows the pack exists. +- `Loaded` + The external `font.bin` has actually been passed to `lv_binfont_create()` and now consumes runtime RAM. + +The runtime behaves like this: + +1. The active locale is resolved from `settings/display_locale`. +2. The active UI font pack is loaded immediately when that locale is activated. +3. The active content font pack is loaded lazily, only when content-scope text needs it. +4. Additional content supplement packs are loaded lazily if the current text contains codepoints not covered by the active content chain. +5. Changing locale unloads all runtime-loaded external fonts and rebuilds the chains from scratch. + +That means a device can have many packs installed while only one or two are resident in RAM. + +## UI Scope vs Content Scope + +The runtime keeps two different fallback chains on purpose. + +### UI Chain + +Used for static application chrome: + +- menu labels +- settings labels +- headings +- buttons +- other translated interface text + +The chain is: + +```text +screen-selected Latin base font -> active UI font pack +``` + +### Content Chain + +Used for user-generated or externally received text: + +- chat sender lines +- chat message previews and bodies +- contact names +- team member names +- node names and descriptions +- locale display names shown in the selector + +The chain is: + +```text +screen-selected Latin base font +-> active content font pack +-> active UI font pack +-> lazily loaded content supplement packs +``` + +This is what lets a non-Chinese UI still render Chinese content when the corresponding pack is installed. + +## Memory Profiles + +Pack availability is constrained by a board-specific memory profile in shared runtime code. + +Current profiles: + +- `constrained` + Locale font budget `128 KiB`, content supplements disabled, decoded map cache `2` tiles, cache not retained on page exit. +- `standard` + Locale font budget `768 KiB`, content supplement budget `640 KiB`, at most `1` supplement pack, decoded map cache `4` tiles, cache not retained. +- `extended` + Locale font budget `2 MiB`, content supplement budget `2 MiB`, at most `3` supplement packs, decoded map cache `12` tiles, cache retained on page exit. + +Current board mapping: + +- `extended`: `Tab5`, `T-Display P4` +- `standard`: `T-Deck`, `T-Deck Pro` +- `constrained`: everything else, including no-PSRAM and pager-class targets + +The locale budget is checked against the actual active locale cost: + +```text +unique(UI font pack, content font pack) +``` + +The supplement budget is separate and only applies to extra content packs that are pulled in later for mixed-script content. + +## Persistence + +The active locale is stored as: + +- namespace: `settings` +- key: `display_locale` + +Legacy ESP installs using the old integer `display_language` key are migrated once to the new string key. After migration, the legacy key is removed. + +## Manifest Schema + +Manifests are plain `key=value` files. + +### Font Pack Manifest + +```ini +kind=font +id=zh-hans-cjk +display_name=Simplified Chinese CJK +usage=both +estimated_ram_bytes=476075 +source=binfont +file=font.bin +ranges=ranges.txt +``` + +Fields: + +- `id` + Stable pack identifier. +- `display_name` + Human-readable name used for diagnostics. +- `usage` + One of `ui`, `content`, or `both`. +- `estimated_ram_bytes` + Expected runtime RAM cost after loading with LVGL binfont loader. This is used for profile decisions and supplement planning. +- `source` + Currently `binfont` for external files, or `builtin` for an alias to a compiled font pack. +- `file` + Path to `font.bin`, relative to the pack directory. +- `ranges` + Coverage metadata file, relative to the pack directory. This is used for codepoint planning, not for rendering. + +If `estimated_ram_bytes` is missing or `0`, the runtime treats the pack as having unknown cost and cannot budget it accurately. Repository packs should always provide it. + +### IME Pack Manifest + +```ini +kind=ime +id=zh-hans-pinyin +display_name=Pinyin +backend=builtin-pinyin +``` + +Today the shipped backend implementation is: + +- `builtin-pinyin` + +The backend lives in firmware code. The IME pack manifest is the runtime registration layer that exposes it. + +### Locale Pack Manifest + +```ini +kind=locale +id=zh-Hans +display_name=Simplified Chinese +native_name=简体中文 +ui_font_pack=zh-hans-cjk +content_font_pack=zh-hans-cjk +ime_pack=zh-hans-pinyin +strings=strings.tsv +``` + +Fields: + +- `id` + Locale identifier shown in persistence and logs. +- `display_name` + English-facing name. +- `native_name` + Native self-name shown in the selector. +- `ui_font_pack` + Font pack used for interface chrome in this locale. +- `content_font_pack` + Font pack preferred for content surfaces in this locale. +- `ime_pack` + Optional IME dependency. +- `strings` + Path to the locale TSV file, relative to the pack directory. + +If `content_font_pack` is omitted, it defaults to `ui_font_pack`. +If `ui_font_pack` is omitted, it defaults to `builtin-latin-ui`. + +## String Table Format + +Locale strings are stored as TSV: + +```text +English source stringLocalized string +``` + +Supported escapes: + +- `\\n` +- `\\t` +- `\\r` +- `\\\\` + +Example: + +```text +Settings Paramètres +Send this code and compare:\\n Envoyez ce code et comparez:\\n +``` + +The English source string remains the stable lookup key in code. + +## Failure Behavior + +If a locale pack cannot be used: + +- missing font pack dependency: locale is skipped +- missing IME dependency: locale is skipped +- active locale font cost exceeds the current memory profile: locale is skipped +- persisted locale id no longer resolves: runtime falls back to `en` + +The persisted `display_locale` value is not erased just because a removable pack is absent. If the pack returns later, the locale becomes selectable again. + +If a content supplement cannot be loaded, the UI still stays alive. That specific text may render as missing glyphs, but the active locale remains unchanged. + +## Repository Bundles + +The repository ships source bundles under: + +- `packs/europe-latin-ext` +- `packs/zh-Hans` +- `packs/zh-Hant` +- `packs/ja` +- `packs/ko` + +Each source bundle contains: + +- `package.ini` +- `DESCRIPTION.txt` +- generation notes in `README.md` +- runtime payload trees under `fonts/`, `locales/`, and optional `ime/` +- `build.ini` plus `charset.txt` in each external font-pack directory +- `ranges.txt` for runtime coverage planning + +`font.bin` is intentionally not part of the source-of-truth layout. If it already exists +locally, it will be used. If it is absent, the Pages pack build regenerates it from the +bundled `charset.txt` and `build.ini` metadata before producing the zip archive. + +## Package Manifest + +Every source bundle now carries a bundle-level `package.ini`. + +Example: + +```ini +kind=package +id=zh-Hans +package_type=locale-bundle +version=1.0.0 +display_name=Simplified Chinese +summary=Full Simplified Chinese locale bundle with external CJK font coverage and the built-in Pinyin IME backend. +description=DESCRIPTION.txt +readme=README.md +author=Trail Mate +homepage=https://vicliu624.github.io/trail-mate/ +min_firmware_version=0.1.18-alpha +supported_memory_profiles=standard,extended +tags=language,cjk,chinese,ime +``` + +Fields: + +- `id` + Stable package identifier used by the remote catalog and future installed-index file. +- `package_type` + High-level package role. Today repository bundles use `locale-bundle`. +- `version` + Package version, independent from the firmware tag. +- `display_name` + Human-facing package name for the Extensions UI. +- `summary` + Short one-line description for list views. +- `description` + Plain-text description file relative to the bundle root. +- `readme` + Developer-facing documentation file relative to the bundle root. +- `author` + Package publisher string. +- `homepage` + Project or package home page. +- `min_firmware_version` + Minimum firmware version expected to understand the bundle contract correctly. +- `supported_memory_profiles` + Declared compatibility hint for `constrained`, `standard`, and/or `extended` devices. +- `tags` + Search and grouping metadata for a future Extensions browser. + +## Font Build Metadata + +Each external font-pack directory may also declare a `build.ini` used only by tooling. + +Example: + +```ini +font=tools/fonts/NotoSansCJKsc-Regular.otf +size=16 +bpp=2 +no_compress=true +``` + +Fields: + +- `font` + Source font file in the repository. +- `size` + Pixel size passed to the binfont generator. +- `bpp` + Bits-per-pixel for the generated font. +- `no_compress` + Whether the generated `font.bin` should disable lv_font_conv RLE compression. + +This file is build metadata only. The runtime never reads it. + +## Distribution Archive Layout + +The GitHub Pages build now produces one zip per bundle under: + +```text +site/assets/packs/-.zip +``` + +Each archive contains: + +```text +package.ini +DESCRIPTION.txt +README.md +payload/fonts//... +payload/locales//... +payload/ime//... +``` + +Inside `payload/`, only installable runtime files are included. Build-only files such as +`charset.txt`, `build.ini`, and `.gitignore` are excluded from the archive. + +## Remote Catalog + +The Pages build also produces: + +```text +site/data/packs.json +``` + +This catalog is the discovery surface for a future Extensions UI. Each entry contains: + +- package id, version, display name, summary, and long description +- compatibility hints such as `min_firmware_version` and supported memory profiles +- the list of locale/font/IME records provided by the bundle +- archive path, size, and SHA-256 for download and update checks +- estimated runtime font RAM totals for planning before install + +The catalog intentionally sits above the runtime registry. It describes downloadable bundles, +not currently loaded fonts. + +## Installed Package Index + +The runtime registry still catalogs installed unpacked resources directly from `/trailmate/packs`. +For package management, the corresponding installer layer should additionally maintain: + +```text +/trailmate/packs/.index/installed.json +``` + +That file is expected to record: + +- installed package id +- installed package version +- install time +- source archive SHA-256 + +Future update prompts should compare this installed index against `site/data/packs.json` +instead of guessing based on directory names. + +## Current IME Coverage + +Today only Simplified Chinese declares an IME pack in the repository bundle: + +- `zh-Hans` -> `zh-hans-pinyin` + +Traditional Chinese, Japanese, and Korean bundles are display-only for now. They intentionally keep input on the existing `EN` / `123` path until dedicated IME packs exist. + +## Adding A New Language + +1. Decide whether the locale can reuse an existing font pack or needs a new one. +2. Add a source bundle `packs//` with `package.ini`, `DESCRIPTION.txt`, and `README.md`. +3. Generate `charset.txt` and `ranges.txt` for each external font pack in that bundle. +4. Write `build.ini` for each generated font pack. +5. Write the runtime font manifest with `usage`, `estimated_ram_bytes`, and `ranges`. +6. Write the locale pack manifest with `ui_font_pack` and `content_font_pack`. +7. Add `ime_pack` only if the script truly needs extra input behavior. +8. Run `python scripts/build_pack_repository.py --pack-root packs --site-root site`. +9. Either install manually by copying the runtime payload to SD, or ship the generated zip through Pages for the future Extensions installer. +10. Reboot and select the locale from Settings. + +## Design Rules + +This architecture intentionally avoids: + +- integer language enums +- eager loading of every installed font at boot +- a single global "non-ASCII => swap font" shortcut +- conflating UI chrome text with user content text +- per-platform localization implementations drifting apart +- mixing runtime manifests with package-distribution metadata +- forcing the runtime registry to understand remote catalogs or zip archives + +The dependency graph stays explicit: + +```text +Locale Pack -> UI Font Pack +Locale Pack -> Content Font Pack +Locale Pack -> IME Pack +Content text -> Optional supplement font packs +``` + +That makes the code easier to reason about and gives Trail Mate a path to broader language support without forcing every device to pay the same firmware and RAM cost. diff --git a/images/ext.png b/images/ext.png new file mode 100644 index 00000000..804f17dd Binary files /dev/null and b/images/ext.png differ diff --git a/images/wifi_topbar.png b/images/wifi_topbar.png new file mode 100644 index 00000000..dcf1febc Binary files /dev/null and b/images/wifi_topbar.png differ diff --git a/modules/core_chat/include/chat/infra/mesh_adapter_router_core.h b/modules/core_chat/include/chat/infra/mesh_adapter_router_core.h index f37720a6..36189510 100644 --- a/modules/core_chat/include/chat/infra/mesh_adapter_router_core.h +++ b/modules/core_chat/include/chat/infra/mesh_adapter_router_core.h @@ -40,7 +40,7 @@ class MeshAdapterRouterCore : public IMeshAdapter void applyConfig(const MeshConfig& config) override; void setUserInfo(const char* long_name, const char* short_name) override; void setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) override; - void setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) override; + void setPrivacyConfig(uint8_t encrypt_mode) override; bool isReady() const override; bool pollIncomingRawPacket(uint8_t* out_data, size_t& out_len, size_t max_len) override; void handleRawPacket(const uint8_t* data, size_t size) override; diff --git a/modules/core_chat/include/chat/infra/store/ram_store.h b/modules/core_chat/include/chat/infra/store/ram_store.h index 8abbcd31..8fa48d92 100644 --- a/modules/core_chat/include/chat/infra/store/ram_store.h +++ b/modules/core_chat/include/chat/infra/store/ram_store.h @@ -35,6 +35,7 @@ class RamStore : public IChatStore void clearConversation(const ConversationId& conv) override; void clearAll() override; bool updateMessageStatus(MessageId msg_id, MessageStatus status) override; + bool getMessage(MessageId msg_id, ChatMessage* out) const override; private: struct StoredMessageEntry diff --git a/modules/core_chat/include/chat/ports/i_chat_store.h b/modules/core_chat/include/chat/ports/i_chat_store.h index 5d803f79..c9ded377 100644 --- a/modules/core_chat/include/chat/ports/i_chat_store.h +++ b/modules/core_chat/include/chat/ports/i_chat_store.h @@ -78,6 +78,14 @@ class IChatStore */ virtual bool updateMessageStatus(MessageId msg_id, MessageStatus status) = 0; + /** + * @brief Look up a stored message by message ID + * @param msg_id Message ID + * @param out Optional out-parameter populated on success + * @return true if found + */ + virtual bool getMessage(MessageId msg_id, ChatMessage* out) const = 0; + /** * @brief Flush pending buffered writes to persistent storage * diff --git a/modules/core_chat/include/chat/ports/i_mesh_adapter.h b/modules/core_chat/include/chat/ports/i_mesh_adapter.h index 30209449..8719718b 100644 --- a/modules/core_chat/include/chat/ports/i_mesh_adapter.h +++ b/modules/core_chat/include/chat/ports/i_mesh_adapter.h @@ -203,10 +203,9 @@ class IMeshAdapter /** * @brief Apply privacy configuration */ - virtual void setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) + virtual void setPrivacyConfig(uint8_t encrypt_mode) { (void)encrypt_mode; - (void)pki_enabled; } /** diff --git a/modules/core_chat/include/chat/usecase/chat_service.h b/modules/core_chat/include/chat/usecase/chat_service.h index 9af1c03d..e8b16e21 100644 --- a/modules/core_chat/include/chat/usecase/chat_service.h +++ b/modules/core_chat/include/chat/usecase/chat_service.h @@ -161,6 +161,7 @@ class ChatService ChannelId current_channel_; bool model_enabled_ = true; MeshProtocol active_protocol_ = MeshProtocol::Meshtastic; + mutable ChatMessage store_lookup_cache_{}; std::vector incoming_text_observers_; std::vector incoming_message_observers_; diff --git a/modules/core_chat/src/infra/mesh_adapter_router_core.cpp b/modules/core_chat/src/infra/mesh_adapter_router_core.cpp index 0f9eeaa6..f96507b9 100644 --- a/modules/core_chat/src/infra/mesh_adapter_router_core.cpp +++ b/modules/core_chat/src/infra/mesh_adapter_router_core.cpp @@ -170,12 +170,12 @@ void MeshAdapterRouterCore::setNetworkLimits(bool duty_cycle_enabled, uint8_t ut } } -void MeshAdapterRouterCore::setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) +void MeshAdapterRouterCore::setPrivacyConfig(uint8_t encrypt_mode) { IMeshAdapter* backend = activeBackend(); if (backend) { - backend->setPrivacyConfig(encrypt_mode, pki_enabled); + backend->setPrivacyConfig(encrypt_mode); } } diff --git a/modules/core_chat/src/infra/store/ram_store.cpp b/modules/core_chat/src/infra/store/ram_store.cpp index 174298dd..5cd8ff46 100644 --- a/modules/core_chat/src/infra/store/ram_store.cpp +++ b/modules/core_chat/src/infra/store/ram_store.cpp @@ -168,6 +168,32 @@ bool RamStore::updateMessageStatus(MessageId msg_id, MessageStatus status) return false; } +bool RamStore::getMessage(MessageId msg_id, ChatMessage* out) const +{ + if (msg_id == 0) + { + return false; + } + + for (const auto& pair : conversations_) + { + const ConversationStorage& storage = pair.second; + for (const auto& entry : storage.messages) + { + if (entry.message.msg_id != msg_id) + { + continue; + } + if (out) + { + *out = entry.message; + } + return true; + } + } + return false; +} + void RamStore::evictOldestMessage() { auto oldest_it = conversations_.end(); diff --git a/modules/core_chat/src/usecase/chat_service.cpp b/modules/core_chat/src/usecase/chat_service.cpp index 3819af73..966e3de2 100644 --- a/modules/core_chat/src/usecase/chat_service.cpp +++ b/modules/core_chat/src/usecase/chat_service.cpp @@ -110,23 +110,22 @@ void ChatService::switchChannel(ChannelId channel) bool ChatService::resendFailed(MessageId msg_id) { - const ChatMessage* msg = model_.getMessage(msg_id); - if (!msg || msg->status != MessageStatus::Failed) + ChatMessage msg; + if (const ChatMessage* model_msg = model_.getMessage(msg_id)) + { + msg = *model_msg; + } + else if (!store_.getMessage(msg_id, &msg)) { return false; } - MessageId new_msg_id = 0; - if (adapter_.sendText(msg->channel, msg->text, &new_msg_id, msg->peer)) + if (msg.status != MessageStatus::Failed) { - ChatMessage resend_msg = *msg; - resend_msg.msg_id = (new_msg_id != 0) ? new_msg_id : msg_id; - resend_msg.status = MessageStatus::Queued; - model_.onSendQueued(resend_msg); - return true; + return false; } - return false; + return sendText(msg.channel, msg.text, msg.peer) != 0; } std::vector ChatService::getRecentMessages(const ConversationId& conv, size_t limit) const @@ -349,7 +348,15 @@ void ChatService::handleSendResult(MessageId msg_id, bool ok) const ChatMessage* ChatService::getMessage(MessageId msg_id) const { - return model_.getMessage(msg_id); + if (const ChatMessage* msg = model_.getMessage(msg_id)) + { + return msg; + } + if (store_.getMessage(msg_id, &store_lookup_cache_)) + { + return &store_lookup_cache_; + } + return nullptr; } void ChatService::setModelEnabled(bool enabled) diff --git a/modules/core_sys/include/app/app_config.h b/modules/core_sys/include/app/app_config.h index 8efdeac8..b01a01ea 100644 --- a/modules/core_sys/include/app/app_config.h +++ b/modules/core_sys/include/app/app_config.h @@ -125,7 +125,6 @@ struct AppConfig // Privacy settings uint8_t privacy_encrypt_mode; - bool privacy_pki; uint8_t privacy_nmea_output; uint8_t privacy_nmea_sentence; @@ -190,7 +189,6 @@ struct AppConfig net_channel_util = 0; privacy_encrypt_mode = 1; - privacy_pki = false; privacy_nmea_output = 0; privacy_nmea_sentence = 0; route_enabled = false; diff --git a/modules/core_sys/include/platform/ui/device_runtime.h b/modules/core_sys/include/platform/ui/device_runtime.h index 111c9d6a..a72d4181 100644 --- a/modules/core_sys/include/platform/ui/device_runtime.h +++ b/modules/core_sys/include/platform/ui/device_runtime.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace platform::ui::device @@ -12,10 +13,21 @@ struct BatteryInfo int level = -1; }; +struct MemoryStats +{ + std::size_t ram_total_bytes = 0; + std::size_t ram_free_bytes = 0; + std::size_t psram_total_bytes = 0; + std::size_t psram_free_bytes = 0; + bool psram_available = false; +}; + void delay_ms(uint32_t ms); void restart(); bool rtc_ready(); BatteryInfo battery_info(); +MemoryStats memory_stats(); +const char* firmware_version(); void handle_low_battery(const BatteryInfo& info); bool supports_screen_brightness(); uint8_t screen_brightness(); diff --git a/modules/core_sys/include/platform/ui/settings_store.h b/modules/core_sys/include/platform/ui/settings_store.h index bae561bf..1844d89c 100644 --- a/modules/core_sys/include/platform/ui/settings_store.h +++ b/modules/core_sys/include/platform/ui/settings_store.h @@ -2,6 +2,7 @@ #include #include +#include #include namespace platform::ui::settings_store @@ -10,10 +11,12 @@ namespace platform::ui::settings_store void put_int(const char* ns, const char* key, int value); void put_bool(const char* ns, const char* key, bool value); void put_uint(const char* ns, const char* key, uint32_t value); +bool put_string(const char* ns, const char* key, const char* value); bool put_blob(const char* ns, const char* key, const void* data, std::size_t len); int get_int(const char* ns, const char* key, int default_value); bool get_bool(const char* ns, const char* key, bool default_value); uint32_t get_uint(const char* ns, const char* key, uint32_t default_value); +bool get_string(const char* ns, const char* key, std::string& out); bool get_blob(const char* ns, const char* key, std::vector& out); void remove_keys(const char* ns, const char* const* keys, std::size_t key_count); void clear_namespace(const char* ns); diff --git a/modules/core_sys/include/platform/ui/wifi_runtime.h b/modules/core_sys/include/platform/ui/wifi_runtime.h new file mode 100644 index 00000000..d06f219d --- /dev/null +++ b/modules/core_sys/include/platform/ui/wifi_runtime.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +namespace platform::ui::wifi +{ + +constexpr std::size_t kMaxSsidLength = 32; +constexpr std::size_t kMaxPasswordLength = 64; +constexpr std::size_t kMaxIpLength = 47; +constexpr std::size_t kMaxStatusMessageLength = 95; + +enum class ConnectionState : uint8_t +{ + Unsupported = 0, + Disabled, + Idle, + Scanning, + Connecting, + Connected, + Error, +}; + +struct Config +{ + bool enabled = false; + char ssid[kMaxSsidLength + 1] = {}; + char password[kMaxPasswordLength + 1] = {}; +}; + +struct Status +{ + bool supported = false; + bool enabled = false; + bool connected = false; + bool scanning = false; + bool has_credentials = false; + int rssi = -127; + char ssid[kMaxSsidLength + 1] = {}; + char ip[kMaxIpLength + 1] = {}; + char message[kMaxStatusMessageLength + 1] = {}; + ConnectionState state = ConnectionState::Unsupported; +}; + +struct ScanResult +{ + char ssid[kMaxSsidLength + 1] = {}; + int rssi = -127; + bool requires_password = true; +}; + +bool is_supported(); +bool load_config(Config& out); +bool save_config(const Config& config); +bool apply_enabled(bool enabled); +bool connect(const Config* override_config = nullptr); +void disconnect(); +bool scan(std::vector& out_results); +Status status(); + +} // namespace platform::ui::wifi diff --git a/modules/ui_shared/include/ui/app_catalog_builder.h b/modules/ui_shared/include/ui/app_catalog_builder.h index 43300f1b..953142ce 100644 --- a/modules/ui_shared/include/ui/app_catalog_builder.h +++ b/modules/ui_shared/include/ui/app_catalog_builder.h @@ -25,6 +25,7 @@ struct FeatureFlags bool include_sstv = true; bool include_usb = false; bool include_settings = true; + bool include_extensions = true; bool include_walkie_talkie = false; }; diff --git a/modules/ui_shared/include/ui/assets/fonts/font_utils.h b/modules/ui_shared/include/ui/assets/fonts/font_utils.h index 96849733..bade8bdf 100644 --- a/modules/ui_shared/include/ui/assets/fonts/font_utils.h +++ b/modules/ui_shared/include/ui/assets/fonts/font_utils.h @@ -1,9 +1,11 @@ #pragma once #include +#include #include "lvgl.h" #include "ui/assets/fonts/fonts.h" +#include "ui/localization.h" #include "ui/page/page_profile.h" #if !defined(LV_FONT_MONTSERRAT_16) || !LV_FONT_MONTSERRAT_16 @@ -16,6 +18,23 @@ namespace ui::fonts { +enum class FontScope : uint8_t +{ + Ui = 0, + Content, +}; + +struct LocalizedFontBinding +{ + const lv_font_t* base = nullptr; + FontScope scope = FontScope::Ui; + lv_font_t composed{}; + bool used = false; +}; + +LocalizedFontBinding* localized_font_binding_storage(); +std::size_t localized_font_binding_storage_size(); + inline bool utf8_has_non_ascii(const char* text) { if (!text) @@ -40,25 +59,153 @@ inline const lv_font_t* ui_chrome_font() : &lv_font_montserrat_16; } -inline const lv_font_t* chat_content_font(const char* text) +inline const lv_font_t* unwrap_localized_font(const lv_font_t* font) { -#if defined(GAT562_NO_CJK) && GAT562_NO_CJK - (void)text; - return ui_chrome_font(); -#else - return utf8_has_non_ascii(text) ? &lv_font_noto_cjk_16_2bpp : ui_chrome_font(); -#endif + if (!font) + { + return nullptr; + } + + LocalizedFontBinding* bindings = localized_font_binding_storage(); + const std::size_t binding_count = localized_font_binding_storage_size(); + for (std::size_t index = 0; index < binding_count; ++index) + { + const LocalizedFontBinding& binding = bindings[index]; + if (binding.used && font == &binding.composed) + { + return binding.base; + } + } + + return font; +} + +inline void sync_localized_font_binding(LocalizedFontBinding& binding) +{ + if (!binding.base) + { + return; + } + + binding.composed = *binding.base; + const lv_font_t* fallback = binding.scope == FontScope::Content + ? ::ui::i18n::active_content_font_fallback() + : ::ui::i18n::active_ui_font_fallback(); + binding.composed.fallback = (fallback && fallback != binding.base) ? fallback : nullptr; +} + +inline LocalizedFontBinding* find_localized_font_binding(const lv_font_t* base_font, FontScope scope) +{ + LocalizedFontBinding* bindings = localized_font_binding_storage(); + const std::size_t binding_count = localized_font_binding_storage_size(); + for (std::size_t index = 0; index < binding_count; ++index) + { + LocalizedFontBinding& binding = bindings[index]; + if (binding.used && binding.base == base_font && binding.scope == scope) + { + return &binding; + } + } + return nullptr; +} + +inline LocalizedFontBinding* acquire_localized_font_binding(const lv_font_t* base_font, FontScope scope) +{ + LocalizedFontBinding* bindings = localized_font_binding_storage(); + const std::size_t binding_count = localized_font_binding_storage_size(); + for (std::size_t index = 0; index < binding_count; ++index) + { + LocalizedFontBinding& binding = bindings[index]; + if (!binding.used) + { + binding.used = true; + binding.base = base_font; + binding.scope = scope; + sync_localized_font_binding(binding); + return &binding; + } + } + return nullptr; +} + +inline void clear_locale_font_bindings() +{ + LocalizedFontBinding* bindings = localized_font_binding_storage(); + const std::size_t binding_count = localized_font_binding_storage_size(); + for (std::size_t index = 0; index < binding_count; ++index) + { + LocalizedFontBinding& binding = bindings[index]; + if (!binding.used) + { + continue; + } + binding.composed = *binding.base; + binding.composed.fallback = nullptr; + } +} + +inline void refresh_locale_font_bindings() +{ + LocalizedFontBinding* bindings = localized_font_binding_storage(); + const std::size_t binding_count = localized_font_binding_storage_size(); + for (std::size_t index = 0; index < binding_count; ++index) + { + LocalizedFontBinding& binding = bindings[index]; + if (!binding.used) + { + continue; + } + sync_localized_font_binding(binding); + } +} + +inline const lv_font_t* localized_font(FontScope scope, + const char* text, + const lv_font_t* ascii_font = nullptr) +{ + if (scope == FontScope::Content) + { + (void)::ui::i18n::ensure_content_font_for_text(text); + } + + const lv_font_t* base_font = unwrap_localized_font(ascii_font ? ascii_font : ui_chrome_font()); + if (!base_font) + { + base_font = ui_chrome_font(); + } + + if (LocalizedFontBinding* existing = find_localized_font_binding(base_font, scope)) + { + sync_localized_font_binding(*existing); + return &existing->composed; + } + + if (LocalizedFontBinding* created = acquire_localized_font_binding(base_font, scope)) + { + return &created->composed; + } + + return base_font; +} + +inline const lv_font_t* localized_font(const lv_font_t* ascii_font = nullptr) +{ + return localized_font(FontScope::Ui, nullptr, ascii_font); } inline const lv_font_t* localized_font(const char* text, const lv_font_t* ascii_font) { - const lv_font_t* fallback = ascii_font ? ascii_font : ui_chrome_font(); -#if defined(GAT562_NO_CJK) && GAT562_NO_CJK - (void)text; - return fallback; -#else - return utf8_has_non_ascii(text) ? &lv_font_noto_cjk_16_2bpp : fallback; -#endif + return localized_font(FontScope::Ui, text, ascii_font); +} + +inline const lv_font_t* content_font(const char* text, const lv_font_t* ascii_font = nullptr) +{ + return localized_font(FontScope::Content, text, ascii_font ? ascii_font : ui_chrome_font()); +} + +inline const lv_font_t* chat_content_font(const char* text) +{ + return content_font(text, ui_chrome_font()); } inline void apply_font(lv_obj_t* label, const lv_font_t* font) @@ -71,17 +218,22 @@ inline void apply_font(lv_obj_t* label, const lv_font_t* font) inline void apply_ui_chrome_font(lv_obj_t* label) { - apply_font(label, ui_chrome_font()); + apply_font(label, localized_font(ui_chrome_font())); +} + +inline void apply_content_font(lv_obj_t* label, const char* text, const lv_font_t* ascii_font = nullptr) +{ + apply_font(label, content_font(text, ascii_font)); } inline void apply_chat_content_font(lv_obj_t* label, const char* text) { - apply_font(label, chat_content_font(text)); + apply_content_font(label, text, ui_chrome_font()); } inline void apply_localized_font(lv_obj_t* label, const char* text, const lv_font_t* ascii_font) { - apply_font(label, localized_font(text, ascii_font)); + apply_font(label, localized_font(FontScope::Ui, text, ascii_font)); } } // namespace ui::fonts diff --git a/modules/ui_shared/include/ui/assets/fonts/fonts.h b/modules/ui_shared/include/ui/assets/fonts/fonts.h index ddc2cdf7..07758f5d 100644 --- a/modules/ui_shared/include/ui/assets/fonts/fonts.h +++ b/modules/ui_shared/include/ui/assets/fonts/fonts.h @@ -7,8 +7,6 @@ #include "lvgl.h" -#if defined(GAT562_NO_CJK) && GAT562_NO_CJK +// Legacy compatibility alias only. Simplified Chinese glyph coverage now +// comes from external font packs instead of a compiled-in LVGL font. #define lv_font_noto_cjk_16_2bpp lv_font_montserrat_14 -#else -LV_FONT_DECLARE(lv_font_noto_cjk_16_2bpp); -#endif diff --git a/modules/ui_shared/include/ui/localization.h b/modules/ui_shared/include/ui/localization.h index 3596147c..0d1c9a72 100644 --- a/modules/ui_shared/include/ui/localization.h +++ b/modules/ui_shared/include/ui/localization.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -9,23 +10,38 @@ namespace ui::i18n { -enum class Language : uint8_t +struct LocaleInfo { - English = 0, - Chinese = 1, + const char* id = nullptr; + const char* display_name = nullptr; + const char* native_name = nullptr; + const char* ui_font_pack_id = nullptr; + const char* content_font_pack_id = nullptr; + const char* ime_pack_id = nullptr; + bool builtin = true; }; -Language language_from_raw(int raw_value); void reload_language(); -Language current_language(); -bool set_language(Language language, bool persist = true); -bool supports_chinese(); +std::size_t locale_count(); +const LocaleInfo* locale_at(std::size_t index); +int current_locale_index(); +const char* current_locale_id(); +const char* current_locale_display_name(); +bool set_locale(const char* locale_id, bool persist = true); +bool set_locale_by_index(std::size_t index, bool persist = true); +const lv_font_t* active_ui_font_fallback(); +const lv_font_t* active_content_font_fallback(); +bool ensure_content_font_for_text(const char* text); +const char* active_ime_pack_id(); +bool active_locale_supports_script_input(); const char* tr(const char* english); std::string vformat(const char* english_fmt, va_list args); std::string format(const char* english_fmt, ...); void set_label_text(lv_obj_t* label, const char* english); void set_label_text_raw(lv_obj_t* label, const char* text); +void set_content_label_text(lv_obj_t* label, const char* english); +void set_content_label_text_raw(lv_obj_t* label, const char* text); void set_label_text_fmt(lv_obj_t* label, const char* english_fmt, ...); } // namespace ui::i18n diff --git a/modules/ui_shared/include/ui/menu/menu_layout.h b/modules/ui_shared/include/ui/menu/menu_layout.h index 529eb9d6..46bb3b8e 100644 --- a/modules/ui_shared/include/ui/menu/menu_layout.h +++ b/modules/ui_shared/include/ui/menu/menu_layout.h @@ -20,6 +20,10 @@ void init(const InitOptions& options); lv_obj_t* menuPanel(); void bringContentToFront(); void refresh_localized_text(); +void set_bottom_bar_node_text(const char* text); +void set_bottom_bar_ram_text(const char* text); +void set_bottom_bar_psram_text(const char* text); +void set_bottom_bar_psram_visible(bool visible); void setMenuVisible(bool visible); } // namespace menu_layout diff --git a/modules/ui_shared/include/ui/runtime/memory_profile.h b/modules/ui_shared/include/ui/runtime/memory_profile.h new file mode 100644 index 00000000..2f3fab37 --- /dev/null +++ b/modules/ui_shared/include/ui/runtime/memory_profile.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace ui::runtime +{ + +enum class MemoryProfileKind : uint8_t +{ + Constrained = 0, + Standard, + Extended, +}; + +struct MemoryProfile +{ + const char* name = "constrained"; + MemoryProfileKind kind = MemoryProfileKind::Constrained; + + std::size_t max_locale_font_ram_bytes = 0; + std::size_t max_content_supplement_ram_bytes = 0; + std::size_t max_content_supplement_packs = 0; + + std::size_t max_map_decode_tiles = 0; + bool retain_map_decode_cache_on_page_exit = false; +}; + +const MemoryProfile& current_memory_profile(); + +inline bool allows_content_supplements() +{ + const MemoryProfile& profile = current_memory_profile(); + return profile.max_content_supplement_packs > 0 && profile.max_content_supplement_ram_bytes > 0; +} + +} // namespace ui::runtime diff --git a/modules/ui_shared/include/ui/runtime/pack_repository.h b/modules/ui_shared/include/ui/runtime/pack_repository.h new file mode 100644 index 00000000..55d56297 --- /dev/null +++ b/modules/ui_shared/include/ui/runtime/pack_repository.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +namespace ui::runtime::packs +{ + +struct InstalledPackageRecord +{ + std::string id; + std::string version; + std::string archive_sha256; + std::uint64_t installed_at_epoch = 0; +}; + +struct PackageRecord +{ + std::string id; + std::string package_type; + std::string version; + std::string display_name; + std::string summary; + std::string description; + std::string author; + std::string homepage; + std::string min_firmware_version; + std::vector supported_memory_profiles; + std::vector tags; + std::vector provided_locale_ids; + std::vector provided_font_ids; + std::vector provided_ime_ids; + std::size_t estimated_unique_font_ram_bytes = 0; + std::size_t archive_size_bytes = 0; + std::string archive_path; + std::string archive_sha256; + std::string download_url; + bool compatible_memory_profile = true; + bool compatible_firmware = true; + bool installed = false; + bool update_available = false; + InstalledPackageRecord installed_record{}; +}; + +bool is_supported(); +bool load_installed_packages(std::vector& out_installed, std::string& out_error); +bool fetch_catalog(std::vector& out_packages, std::string& out_error); +bool install_package(const PackageRecord& package, std::string& out_error); + +} // namespace ui::runtime::packs diff --git a/modules/ui_shared/include/ui/screens/chat/chat_protocol_support.h b/modules/ui_shared/include/ui/screens/chat/chat_protocol_support.h new file mode 100644 index 00000000..a7653816 --- /dev/null +++ b/modules/ui_shared/include/ui/screens/chat/chat_protocol_support.h @@ -0,0 +1,16 @@ +#pragma once + +#include "chat/domain/chat_types.h" +#include "chat/ports/i_mesh_adapter.h" + +namespace chat::ui::support +{ + +chat::MeshProtocol active_mesh_protocol(); +chat::MeshCapabilities active_mesh_capabilities(); +bool supports_local_text_chat(); +bool supports_team_chat(); +const char* local_text_chat_unavailable_message(); +const char* team_chat_unavailable_message(); + +} // namespace chat::ui::support diff --git a/modules/ui_shared/include/ui/screens/chat/chat_send_flow.h b/modules/ui_shared/include/ui/screens/chat/chat_send_flow.h new file mode 100644 index 00000000..016ceff8 --- /dev/null +++ b/modules/ui_shared/include/ui/screens/chat/chat_send_flow.h @@ -0,0 +1,26 @@ +#pragma once + +#include "chat/domain/chat_types.h" +#include + +namespace chat +{ +class ChatService; +} + +namespace chat::ui +{ +class ChatComposeScreen; +} + +namespace chat::ui::send_flow +{ + +bool begin_local_text_send(ChatComposeScreen* compose, + chat::ChatService* service, + const chat::ConversationId& conv, + const std::string& text, + void (*done_cb)(bool ok, bool timeout, void*), + void* user_data); + +} // namespace chat::ui::send_flow diff --git a/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h b/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h index e5552fa3..00217f14 100644 --- a/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h +++ b/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h @@ -74,6 +74,8 @@ class UiController : public IChatUiRuntime void switchToCompose(chat::ConversationId conv); void handleChannelSelected(const chat::ConversationId& conv); void handleSendMessage(const std::string& text); + void handleComposeSendDone(bool ok, bool timeout); + static void handleComposeSendDoneCallback(bool ok, bool timeout, void* user_data); void refreshUnreadCounts(); void refreshUnreadCounts(bool force_reload); void cleanupComposeIme(); diff --git a/modules/ui_shared/include/ui/screens/extensions/extensions_page_runtime.h b/modules/ui_shared/include/ui/screens/extensions/extensions_page_runtime.h new file mode 100644 index 00000000..cfc56a19 --- /dev/null +++ b/modules/ui_shared/include/ui/screens/extensions/extensions_page_runtime.h @@ -0,0 +1,18 @@ +/** + * @file extensions_page_runtime.h + * @brief Shared Extensions page runtime + */ + +#pragma once + +#include "lvgl.h" +#include "ui/screens/extensions/extensions_page_shell.h" + +namespace extensions::ui::runtime +{ + +bool is_available(); +void enter(const shell::Host* host, lv_obj_t* parent); +void exit(lv_obj_t* parent); + +} // namespace extensions::ui::runtime diff --git a/modules/ui_shared/include/ui/screens/extensions/extensions_page_shell.h b/modules/ui_shared/include/ui/screens/extensions/extensions_page_shell.h new file mode 100644 index 00000000..da296044 --- /dev/null +++ b/modules/ui_shared/include/ui/screens/extensions/extensions_page_shell.h @@ -0,0 +1,19 @@ +/** + * @file extensions_page_shell.h + * @brief Shared Extensions page shell entrypoints + */ + +#pragma once + +#include "lvgl.h" +#include "ui/page/page_host.h" + +namespace extensions::ui::shell +{ + +using Host = ::ui::page::Host; + +void enter(void* user_data, lv_obj_t* parent); +void exit(void* user_data, lv_obj_t* parent); + +} // namespace extensions::ui::shell diff --git a/modules/ui_shared/include/ui/screens/settings/settings_state.h b/modules/ui_shared/include/ui/screens/settings/settings_state.h index 4f3b0331..55899cc3 100644 --- a/modules/ui_shared/include/ui/screens/settings/settings_state.h +++ b/modules/ui_shared/include/ui/screens/settings/settings_state.h @@ -14,6 +14,7 @@ namespace settings::ui enum class SettingType { + Info, Toggle, Enum, Text, @@ -101,7 +102,6 @@ struct SettingsData // Chat/GPS (privacy-related controls) int privacy_encrypt_mode = 1; - bool privacy_pki = false; int privacy_nmea_output = 0; int privacy_nmea_sentence = 0; @@ -110,10 +110,17 @@ struct SettingsData int screen_brightness = 16; int timezone_offset_min = 0; int speaker_volume = 45; - int display_language = 0; + int display_locale_index = 0; bool ble_enabled = true; bool vibration_enabled = true; + // Wi-Fi + bool wifi_enabled = false; + int wifi_network_index = -1; + char wifi_status[96] = ""; + char wifi_ssid[33] = ""; + char wifi_password[65] = ""; + // Power / Gauge (System) char gauge_design_mah[8] = ""; char gauge_full_mah[8] = ""; diff --git a/modules/ui_shared/include/ui/ui_status.h b/modules/ui_shared/include/ui/ui_status.h index 56d5f7dc..cc69644a 100644 --- a/modules/ui_shared/include/ui/ui_status.h +++ b/modules/ui_shared/include/ui/ui_status.h @@ -17,6 +17,7 @@ void register_menu_status_row(lv_obj_t* row, lv_obj_t* route_icon, lv_obj_t* tracker_icon, lv_obj_t* gps_icon, + lv_obj_t* wifi_icon, lv_obj_t* team_icon, lv_obj_t* msg_icon, lv_obj_t* ble_icon); diff --git a/modules/ui_shared/src/ui/app_catalog_builder.cpp b/modules/ui_shared/src/ui/app_catalog_builder.cpp index 8aecab5a..829e74e8 100644 --- a/modules/ui_shared/src/ui/app_catalog_builder.cpp +++ b/modules/ui_shared/src/ui/app_catalog_builder.cpp @@ -14,6 +14,7 @@ #include "ui/screens/chat/chat_page_shell.h" #include "ui/screens/contacts/contacts_page_shell.h" #include "ui/screens/energy_sweep/energy_sweep_page_shell.h" +#include "ui/screens/extensions/extensions_page_shell.h" #include "ui/screens/gnss/gnss_skyplot_page_shell.h" #include "ui/screens/gps/gps_page_shell.h" #if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK @@ -63,6 +64,7 @@ extern "C" extern const lv_image_dsc_t sstv; #endif extern const lv_image_dsc_t Setting; + extern const lv_image_dsc_t ext; #if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK extern const lv_image_dsc_t img_usb; #endif @@ -135,6 +137,10 @@ ui::CallbackAppScreen s_setting_app("settings", "Setting", &Setting, settings::ui::shell::enter, settings::ui::shell::exit, &s_menu_host); +ui::CallbackAppScreen s_extensions_app("extensions", "Extensions", &ext, + extensions::ui::shell::enter, + extensions::ui::shell::exit, + &s_menu_host); ui::CallbackAppScreen s_walkie_app("walkie_talkie", "Walkie Talkie", &walkie_talkie, walkie_page::ui::shell::enter, walkie_page::ui::shell::exit, @@ -215,6 +221,10 @@ AppCatalog build(const FeatureFlags& flags) { add(&s_setting_app); } + if (flags.include_extensions) + { + add(&s_extensions_app); + } }; if (flags.profile == CatalogProfile::IdfDefault) diff --git a/modules/ui_shared/src/ui/assets/ext.c b/modules/ui_shared/src/ui/assets/ext.c new file mode 100644 index 00000000..81966de6 --- /dev/null +++ b/modules/ui_shared/src/ui/assets/ext.c @@ -0,0 +1,165 @@ +#ifdef __has_include + #if __has_include("lvgl.h") + #ifndef LV_LVGL_H_INCLUDE_SIMPLE + #define LV_LVGL_H_INCLUDE_SIMPLE + #endif + #endif +#endif + +#if defined(LV_LVGL_H_INCLUDE_SIMPLE) + #include "lvgl.h" +#else + #include "lvgl/lvgl.h" +#endif + + +#ifndef LV_ATTRIBUTE_MEM_ALIGN +#define LV_ATTRIBUTE_MEM_ALIGN +#endif + +#ifndef LV_ATTRIBUTE_IMAGE_EXT +#define LV_ATTRIBUTE_IMAGE_EXT +#endif + +const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMAGE_EXT uint8_t ext_map[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0xfd, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd, 0xe0, 0xfc, 0x61, 0xe4, 0xea, 0x20, 0x68, 0x39, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0x64, 0xab, 0xea, 0x20, 0xe7, 0x59, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0xfd, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd, 0x61, 0xe4, 0xea, 0x20, 0xea, 0x20, 0xe5, 0x92, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0xea, 0x20, 0x68, 0x39, 0xea, 0x20, 0xe7, 0x59, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xe7, 0x59, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0x27, 0x62, 0x64, 0xab, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0xe9, 0x20, 0x68, 0x39, 0xe7, 0x59, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0xfd, 0xe0, 0xfc, 0x00, 0xfd, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0xe2, 0xcb, 0xe5, 0x92, 0x68, 0x39, 0x66, 0x72, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x31, 0xea, 0x20, 0x64, 0xab, 0xe0, 0xfc, 0x00, 0xfd, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0x68, 0x39, 0xea, 0x20, 0x61, 0xe4, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0x66, 0x72, 0xea, 0x20, 0x64, 0xab, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xe5, 0x92, 0xea, 0x20, 0x68, 0x39, 0x61, 0xe4, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x68, 0x39, 0xea, 0x20, 0x68, 0x39, 0xe2, 0xcb, 0x61, 0xe4, 0xe2, 0xcb, 0x66, 0x72, 0x68, 0x39, 0xe5, 0x92, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd, 0xe0, 0xfc, 0xe0, 0xfc, 0x61, 0xe4, 0xe7, 0x59, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x64, 0xab, 0xe7, 0x59, 0x68, 0x39, 0xea, 0x20, 0xe7, 0x59, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x64, 0xab, 0x66, 0x72, 0x68, 0x39, 0x68, 0x39, 0xe7, 0x59, 0x64, 0xab, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd, 0x00, 0xfd, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0xfd, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x61, 0xe4, 0xe7, 0x59, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xe7, 0x59, 0x61, 0xe4, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe7, 0x59, 0xea, 0x20, 0x68, 0x39, 0x64, 0xab, 0x61, 0xe4, 0x61, 0xe4, 0xe2, 0xcb, 0x68, 0x39, 0xea, 0x20, 0xe7, 0x59, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe5, 0x92, 0xea, 0x20, 0x68, 0x39, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x68, 0x39, 0xea, 0x20, 0x64, 0xab, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xa8, 0x49, 0xea, 0x20, 0xea, 0x20, 0x00, 0xfd, 0xe0, 0xfc, 0x00, 0xfd, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe5, 0x92, 0xea, 0x20, 0x66, 0x72, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x61, 0xe4, 0xea, 0x20, 0x68, 0x39, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0xfd, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd, 0xe0, 0xfc, 0x61, 0xe4, 0xea, 0x20, 0x68, 0x39, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xe5, 0x92, 0xea, 0x20, 0x68, 0x39, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd, 0xe0, 0xfc, 0x29, 0x31, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0x29, 0x29, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xfc, 0xe0, 0xfc, 0xea, 0x20, 0xea, 0x20, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0xe0, 0xfc, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xe9, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0xe9, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xca, 0x20, 0xca, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xe9, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xe9, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xe9, 0x20, 0xe9, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0xca, 0x20, 0xea, 0x20, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xca, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xe9, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, + 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, + 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, 0xea, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xbf, 0x9f, 0x00, 0x00, 0xbf, 0x9f, 0x00, 0x00, 0xbf, 0x9f, 0x00, 0x00, 0xbf, 0x9f, 0x00, 0x00, 0xbf, 0x9f, 0x00, 0x00, 0xbf, 0x9f, 0x00, 0x00, 0xbf, 0x9f, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, + 0xbf, 0xbf, 0x00, 0x00, 0xbf, 0xbf, 0x00, 0x00, 0xbf, 0xbf, 0x00, 0x00, 0xbf, 0xbf, 0x00, 0x00, 0xbf, 0xbf, 0x00, 0x00, 0xbf, 0xbf, 0x00, 0x00, 0xbf, 0xbf, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0xdf, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x9f, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20, 0x00, 0x7f, 0xff, 0xdf, 0x20, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x20, 0x00, 0x00, 0xdf, 0xff, 0xdf, 0x40, 0x20, 0x00, 0x60, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x60, 0xbf, 0xdf, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x9f, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x40, 0x00, 0x20, 0x40, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xdf, 0x60, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0xff, 0xff, 0xdf, 0xbf, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xdf, 0xbf, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xff, 0x60, 0x20, 0x40, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0xdf, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0xbf, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xdf, 0x20, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0xdf, 0x40, 0x20, 0x00, 0x60, 0xdf, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xbf, 0xdf, 0xff, 0x9f, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x9f, 0xdf, 0xdf, 0xbf, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x9f, 0xdf, 0xdf, 0xbf, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xbf, 0x9f, 0x20, 0x20, 0x40, 0xdf, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xdf, 0x60, 0x20, 0x40, 0x7f, 0xdf, 0x9f, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xdf, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xbf, 0x7f, 0x20, 0x00, 0x60, 0xdf, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0xdf, 0x40, 0x20, 0x40, 0x9f, 0xdf, 0x7f, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbf, 0xdf, 0xff, 0x9f, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xbf, 0xdf, 0xff, 0x9f, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x9f, 0xdf, 0xdf, 0xbf, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xdf, 0x60, 0x20, 0x20, 0x40, 0xdf, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdf, 0xff, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xdf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const lv_image_dsc_t ext = { + .header.magic = LV_IMAGE_HEADER_MAGIC, + .header.cf = LV_COLOR_FORMAT_RGB565A8, + .header.flags = 0, + .header.w = 64, + .header.h = 64, + .header.stride = 128, + .data_size = sizeof(ext_map), + .data = ext_map, +}; diff --git a/modules/ui_shared/src/ui/assets/fonts/font_utils.cpp b/modules/ui_shared/src/ui/assets/fonts/font_utils.cpp new file mode 100644 index 00000000..bf782423 --- /dev/null +++ b/modules/ui_shared/src/ui/assets/fonts/font_utils.cpp @@ -0,0 +1,23 @@ +#include "ui/assets/fonts/font_utils.h" + +namespace ui::fonts +{ +namespace +{ + +constexpr std::size_t kMaxLocalizedFontBindings = 32; +LocalizedFontBinding s_localized_font_bindings[kMaxLocalizedFontBindings]{}; + +} // namespace + +LocalizedFontBinding* localized_font_binding_storage() +{ + return s_localized_font_bindings; +} + +std::size_t localized_font_binding_storage_size() +{ + return kMaxLocalizedFontBindings; +} + +} // namespace ui::fonts diff --git a/modules/ui_shared/src/ui/assets/wifi_topbar.c b/modules/ui_shared/src/ui/assets/wifi_topbar.c new file mode 100644 index 00000000..2ca9df77 --- /dev/null +++ b/modules/ui_shared/src/ui/assets/wifi_topbar.c @@ -0,0 +1,86 @@ +#ifdef __has_include + #if __has_include("lvgl.h") + #ifndef LV_LVGL_H_INCLUDE_SIMPLE + #define LV_LVGL_H_INCLUDE_SIMPLE + #endif + #endif +#endif + +#if defined(LV_LVGL_H_INCLUDE_SIMPLE) + #include "lvgl.h" +#else + #include "lvgl/lvgl.h" +#endif + + +#ifndef LV_ATTRIBUTE_MEM_ALIGN +#define LV_ATTRIBUTE_MEM_ALIGN +#endif + +#ifndef LV_ATTRIBUTE_IMAGE_WIFI_TOPBAR +#define LV_ATTRIBUTE_IMAGE_WIFI_TOPBAR +#endif + +const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMAGE_WIFI_TOPBAR uint8_t wifi_topbar_map[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b, 0x3a, 0x2b, 0x32, 0x4b, 0x32, 0x4b, 0x3a, 0x4b, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b, 0x32, 0x4b, 0x32, 0x4b, 0x32, 0x4b, 0x32, 0x2e, 0x53, 0x0e, 0x4b, 0x6b, 0x3a, 0x4b, 0x32, 0x6b, 0x3a, 0x4b, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b, 0x32, 0x4b, 0x32, 0x4f, 0x5b, 0x9b, 0xd6, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xf7, 0xff, 0xff, 0x9a, 0xce, 0xb0, 0x63, 0x4b, 0x32, 0x4b, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b, 0x32, 0x4b, 0x32, 0xb7, 0xad, 0xdf, 0xf7, 0x7e, 0xcf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x5e, 0xc7, 0x9f, 0xe7, 0x15, 0x95, 0x8b, 0x3a, 0x4b, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x32, 0x2b, 0x32, 0x6b, 0x3a, 0x35, 0x9d, 0x9f, 0xe7, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x9f, 0xe7, 0xf5, 0x94, 0x6b, 0x3a, 0x2b, 0x32, 0x4b, 0x3a, 0x4b, 0x32, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x2b, 0x32, 0x4b, 0x32, 0x8c, 0x3a, 0x6b, 0x3a, 0xac, 0x42, 0x90, 0x5b, 0x6f, 0x53, 0x6f, 0x53, 0x6f, 0x53, 0x6f, 0x53, 0x6f, 0x53, 0x2e, 0x4b, 0x8c, 0x3a, 0x8c, 0x42, 0x8c, 0x42, 0x6f, 0x5b, 0xb0, 0x63, 0xed, 0x4a, 0x8c, 0x3a, 0x6b, 0x3a, 0x4b, 0x3a, 0x4b, 0x32, 0x00, 0x00, + 0x00, 0x00, 0x4b, 0x32, 0xf9, 0x9d, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x5b, 0xa6, 0x3e, 0xbf, 0x3e, 0xbf, 0x90, 0x5b, 0x5d, 0xef, 0xff, 0xff, 0x5d, 0xef, 0x5d, 0xef, 0xff, 0xff, 0xff, 0xff, 0x9e, 0xf7, 0xff, 0xff, 0x19, 0xbe, 0x6b, 0x3a, 0x4b, 0x32, + 0x4b, 0x32, 0x6b, 0x3a, 0x3e, 0xbf, 0xb4, 0x7c, 0x3e, 0xbf, 0x12, 0x6c, 0xf9, 0x9d, 0xf9, 0x9d, 0xf9, 0x9d, 0xd1, 0x63, 0xbc, 0xb6, 0x9b, 0xae, 0x15, 0x9d, 0xff, 0xff, 0x5d, 0xef, 0xed, 0x52, 0x73, 0x84, 0xd8, 0xb5, 0x9a, 0xce, 0x56, 0xa5, 0xff, 0xff, 0xff, 0xff, 0xac, 0x42, 0x2b, 0x32, + 0x4b, 0x32, 0x6b, 0x3a, 0x3e, 0xbf, 0xb4, 0x7c, 0xf9, 0x9d, 0xcd, 0x42, 0x12, 0x6c, 0x12, 0x6c, 0x9b, 0xae, 0xb4, 0x7c, 0x3e, 0xbf, 0xf9, 0x9d, 0x15, 0x9d, 0xff, 0xff, 0x9b, 0xd6, 0x93, 0x8c, 0xfc, 0xde, 0xff, 0xff, 0x5d, 0xef, 0xd8, 0xb5, 0xff, 0xff, 0xff, 0xff, 0xed, 0x4a, 0x2b, 0x32, + 0x4b, 0x32, 0x6b, 0x3a, 0x3e, 0xbf, 0x9b, 0xae, 0xcd, 0x42, 0x12, 0x6c, 0x12, 0x6c, 0x12, 0x6c, 0x3e, 0xbf, 0x6f, 0x53, 0x3e, 0xbf, 0xf9, 0x9d, 0x15, 0x9d, 0xff, 0xff, 0x19, 0xbe, 0x8f, 0x63, 0xd4, 0x94, 0xfc, 0xde, 0x9b, 0xd6, 0x35, 0x9d, 0xff, 0xff, 0xff, 0xff, 0xed, 0x4a, 0x2b, 0x32, + 0x4b, 0x32, 0x6b, 0x3a, 0x3e, 0xbf, 0x3e, 0xbf, 0x6f, 0x53, 0xf9, 0x9d, 0x12, 0x6c, 0xb4, 0x7c, 0x3e, 0xbf, 0x6f, 0x53, 0x3e, 0xbf, 0xf9, 0x9d, 0x15, 0x9d, 0xff, 0xff, 0x9b, 0xd6, 0x35, 0x9d, 0xff, 0xff, 0xff, 0xff, 0x9b, 0xd6, 0x35, 0x9d, 0xff, 0xff, 0xff, 0xff, 0xed, 0x4a, 0x2b, 0x32, + 0x4b, 0x3a, 0x4b, 0x32, 0x9b, 0xae, 0x3e, 0xbf, 0xf9, 0x9d, 0x3e, 0xbf, 0x9b, 0xae, 0x3e, 0xbf, 0x3e, 0xbf, 0x9b, 0xae, 0x3e, 0xbf, 0xb4, 0x7c, 0x35, 0x9d, 0xff, 0xff, 0x5d, 0xef, 0x5d, 0xef, 0xff, 0xff, 0xff, 0xff, 0x5d, 0xef, 0x5d, 0xef, 0xff, 0xff, 0xff, 0xff, 0x6b, 0x3a, 0x2b, 0x32, + 0x00, 0x00, 0x4b, 0x32, 0xcd, 0x42, 0x12, 0x6c, 0x12, 0x6c, 0x6f, 0x53, 0xb4, 0x7c, 0xb4, 0x7c, 0xb4, 0x7c, 0xb4, 0x7c, 0xed, 0x42, 0x4b, 0x32, 0xb0, 0x63, 0xb0, 0x63, 0xd4, 0x8c, 0x15, 0x9d, 0x15, 0x9d, 0x15, 0x95, 0x52, 0x7c, 0x8f, 0x63, 0x8f, 0x63, 0xac, 0x42, 0x4b, 0x3a, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2b, 0x32, 0x6b, 0x3a, 0x6b, 0x3a, 0x4b, 0x32, 0x6b, 0x3a, 0x97, 0x8d, 0xf9, 0x9d, 0xf9, 0x9d, 0x15, 0x85, 0x15, 0x85, 0x15, 0x85, 0x15, 0x85, 0x77, 0x8d, 0xf9, 0x9d, 0x15, 0x85, 0x4b, 0x32, 0x4b, 0x32, 0x4b, 0x32, 0x4b, 0x32, 0x4b, 0x32, 0x4b, 0x32, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b, 0x32, 0x4b, 0x32, 0xac, 0x42, 0x57, 0x8d, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0x3e, 0xbf, 0xf9, 0x9d, 0x4b, 0x32, 0x4b, 0x32, 0x4b, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b, 0x32, 0x4b, 0x32, 0x4b, 0x32, 0xcd, 0x42, 0x12, 0x6c, 0x57, 0x8d, 0xb4, 0x7c, 0xb4, 0x7c, 0x4b, 0x32, 0x8b, 0x3a, 0x4b, 0x32, 0x2b, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x76, 0x9d, 0x8c, 0x42, 0x4b, 0x32, 0x6b, 0x3a, 0xac, 0x42, 0x8c, 0x3a, 0x8c, 0x3a, 0x4b, 0x32, 0xcd, 0x42, 0x76, 0x9d, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x5a, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x76, 0x9d, 0xd0, 0x6b, 0x8f, 0x63, 0x4e, 0x53, 0x93, 0x7c, 0xf8, 0xad, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x5a, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5a, 0xbe, 0x5a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x7a, 0xbe, 0x79, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x88, 0x74, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xc3, 0xff, 0xff, 0xfd, 0xfb, 0xfd, 0xff, 0xcb, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xf4, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x88, 0x88, 0xfa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf4, 0x60, 0x40, 0x20, 0x00, 0x00, + 0x00, 0x88, 0xff, 0xfc, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0xf9, 0xf7, 0x8f, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x20, + 0x58, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x88, + 0x58, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x88, + 0x58, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x88, + 0x58, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x88, + 0x40, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x60, + 0x00, 0xf4, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfb, 0xf7, 0xf6, 0xf0, 0x00, + 0x00, 0x00, 0x88, 0xd5, 0xda, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb7, 0xb7, 0x74, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x9d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0xbf, 0x7f, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x7f, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdf, 0x7f, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x20, 0x60, 0x60, 0x60, 0x7f, 0x7f, 0x7f, 0x60, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const lv_image_dsc_t wifi_topbar = { + .header.magic = LV_IMAGE_HEADER_MAGIC, + .header.cf = LV_COLOR_FORMAT_RGB565A8, + .header.flags = 0, + .header.w = 24, + .header.h = 24, + .header.stride = 48, + .data_size = sizeof(wifi_topbar_map), + .data = wifi_topbar_map, + }; + diff --git a/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp b/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp new file mode 100644 index 00000000..9f406607 --- /dev/null +++ b/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp @@ -0,0 +1,1931 @@ +#include "ui/localization.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform/ui/settings_store.h" +#include "ui/assets/fonts/font_utils.h" +#include "ui/runtime/memory_profile.h" +#include "ui/support/lvgl_fs_utils.h" + +#if __has_include("lv_binfont_loader.h") +#include "lv_binfont_loader.h" +#define UI_I18N_HAVE_BINFONT 1 +#elif __has_include("src/font/lv_binfont_loader.h") +#include "src/font/lv_binfont_loader.h" +#define UI_I18N_HAVE_BINFONT 1 +#else +#define UI_I18N_HAVE_BINFONT 0 +#endif + +namespace ui::i18n +{ +namespace +{ + +constexpr const char* kLogTag = "[I18N]"; +constexpr const char* kSettingsNamespace = "settings"; +constexpr const char* kDisplayLocaleKey = "display_locale"; +constexpr const char* kLegacyDisplayLanguageKey = "display_language"; +constexpr const char* kDefaultLocaleId = "en"; +constexpr const char* kBuiltinEnglishName = "English"; +constexpr const char* kBuiltinLatinFontPackId = "builtin-latin-ui"; +constexpr const char* kPackRoot = "/trailmate/packs"; +constexpr const char* kFontPackRoot = "/trailmate/packs/fonts"; +constexpr const char* kLocalePackRoot = "/trailmate/packs/locales"; +constexpr const char* kImePackRoot = "/trailmate/packs/ime"; + +enum class FontPackUsage : uint8_t +{ + UiOnly = 0, + ContentOnly, + Both, +}; + +struct CodepointRange +{ + uint32_t first = 0; + uint32_t last = 0; +}; + +struct FontPackRecord +{ + std::string id; + std::string display_name; + FontPackUsage usage = FontPackUsage::Both; + bool builtin = false; + const lv_font_t* builtin_font = nullptr; + lv_font_t* owned_font = nullptr; + std::string source_path; + std::size_t estimated_ram_bytes = 0; + std::vector coverage; +}; + +struct ImePackRecord +{ + std::string id; + std::string display_name; + std::string backend; + bool builtin = false; +}; + +struct LocalePackRecord +{ + std::string id; + std::string display_name; + std::string native_name; + std::string ui_font_pack_id; + std::string content_font_pack_id; + std::string ime_pack_id; + bool builtin = false; + std::vector> translations; +}; + +struct ManifestEntry +{ + std::string key; + std::string value; +}; + +using Manifest = std::vector; + +struct FontChainState +{ + std::vector packs; + std::vector composed_fonts; + const lv_font_t* head = nullptr; + std::string desc; +}; + +std::vector s_font_packs; +std::vector s_ime_packs; +std::vector s_locale_packs; +std::vector s_locale_views; + +LocalePackRecord* s_active_locale = nullptr; +FontPackRecord* s_active_ui_font_pack = nullptr; +FontPackRecord* s_active_content_font_pack = nullptr; +ImePackRecord* s_active_ime_pack = nullptr; + +FontChainState s_ui_font_chain; +FontChainState s_content_font_chain; +std::vector s_content_supplement_packs; + +bool s_registry_ready = false; + +const char* safe_text(const char* value) +{ + return value ? value : ""; +} + +const char* usage_name(FontPackUsage usage) +{ + switch (usage) + { + case FontPackUsage::UiOnly: + return "ui"; + case FontPackUsage::ContentOnly: + return "content"; + case FontPackUsage::Both: + default: + return "both"; + } +} + +bool external_pack_scan_enabled() +{ +#if defined(LV_USE_FS_POSIX) && LV_USE_FS_POSIX + return true; +#else + return false; +#endif +} + +void trim_in_place(std::string& value) +{ + std::size_t start = 0; + while (start < value.size() && std::isspace(static_cast(value[start])) != 0) + { + ++start; + } + + std::size_t end = value.size(); + while (end > start && std::isspace(static_cast(value[end - 1])) != 0) + { + --end; + } + + if (start == 0 && end == value.size()) + { + return; + } + + value = value.substr(start, end - start); +} + +std::string trim_copy(std::string value) +{ + trim_in_place(value); + return value; +} + +std::string lowercase_ascii(std::string value) +{ + for (char& ch : value) + { + ch = static_cast(std::tolower(static_cast(ch))); + } + return value; +} + +std::string join_path(const std::string& base, const std::string& child) +{ + if (base.empty()) + { + return child; + } + if (child.empty()) + { + return base; + } + if (child.size() >= 2 && child[1] == ':') + { + return child; + } + if (child.front() == '/') + { + return child; + } + if (base.back() == '/') + { + return base + child; + } + return base + "/" + child; +} + +std::string normalize_dir_entry(std::string name) +{ + while (!name.empty() && (name.front() == '/' || name.front() == '\\')) + { + name.erase(name.begin()); + } + while (!name.empty() && (name.back() == '/' || name.back() == '\\')) + { + name.pop_back(); + } + return name; +} + +std::string decode_escapes(const std::string& text) +{ + std::string out; + out.reserve(text.size()); + + for (std::size_t i = 0; i < text.size(); ++i) + { + const char ch = text[i]; + if (ch != '\\' || (i + 1U) >= text.size()) + { + out.push_back(ch); + continue; + } + + const char next = text[++i]; + switch (next) + { + case 'n': + out.push_back('\n'); + break; + case 't': + out.push_back('\t'); + break; + case 'r': + out.push_back('\r'); + break; + case '\\': + out.push_back('\\'); + break; + default: + out.push_back(next); + break; + } + } + + return out; +} + +const char* manifest_value(const Manifest& manifest, const char* key) +{ + if (!key) + { + return nullptr; + } + + const std::string wanted = lowercase_ascii(key); + for (const auto& entry : manifest) + { + if (entry.key == wanted) + { + return entry.value.c_str(); + } + } + + return nullptr; +} + +bool parse_manifest_file(const std::string& path, Manifest& out) +{ + out.clear(); + + std::string contents; + if (!::ui::fs::read_text_file(path.c_str(), contents)) + { + return false; + } + + std::size_t line_start = 0; + while (line_start <= contents.size()) + { + const std::size_t line_end = contents.find('\n', line_start); + std::string line = contents.substr( + line_start, + line_end == std::string::npos ? std::string::npos : (line_end - line_start)); + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + trim_in_place(line); + + if (!line.empty() && line[0] != '#' && line[0] != ';') + { + const std::size_t sep = line.find('='); + if (sep != std::string::npos && sep > 0) + { + std::string key = lowercase_ascii(trim_copy(line.substr(0, sep))); + std::string value = trim_copy(line.substr(sep + 1)); + if (!key.empty()) + { + out.push_back({std::move(key), std::move(value)}); + } + } + } + + if (line_end == std::string::npos) + { + break; + } + line_start = line_end + 1U; + } + + return !out.empty(); +} + +bool list_directory_entries(const char* path, std::vector& out) +{ + out.clear(); + if (!external_pack_scan_enabled()) + { + return false; + } + +#if defined(LV_USE_FS_POSIX) && LV_USE_FS_POSIX + const std::string normalized = ::ui::fs::normalize_path(path); + if (normalized.empty()) + { + return false; + } + + lv_fs_dir_t dir; + if (lv_fs_dir_open(&dir, normalized.c_str()) != LV_FS_RES_OK) + { + return false; + } + + char entry_name[256]; + while (true) + { + entry_name[0] = '\0'; + if (lv_fs_dir_read(&dir, entry_name, sizeof(entry_name)) != LV_FS_RES_OK) + { + break; + } + if (entry_name[0] == '\0') + { + break; + } + + std::string normalized_name = normalize_dir_entry(entry_name); + if (normalized_name.empty() || normalized_name == "." || normalized_name == "..") + { + continue; + } + + out.push_back(std::move(normalized_name)); + } + + lv_fs_dir_close(&dir); + std::sort(out.begin(), out.end()); + return true; +#else + (void)path; + return false; +#endif +} + +template +TPack* find_pack_by_id(std::vector& packs, const char* id) +{ + if (!id || id[0] == '\0') + { + return nullptr; + } + + for (auto& pack : packs) + { + if (pack.id == id) + { + return &pack; + } + } + + return nullptr; +} + +template +const TPack* find_pack_by_id(const std::vector& packs, const char* id) +{ + if (!id || id[0] == '\0') + { + return nullptr; + } + + for (const auto& pack : packs) + { + if (pack.id == id) + { + return &pack; + } + } + + return nullptr; +} + +std::size_t parse_size_value(const char* value) +{ + if (!value || value[0] == '\0') + { + return 0; + } + + char* end = nullptr; + const unsigned long parsed = std::strtoul(value, &end, 10); + if (end == value || (end && *end != '\0')) + { + return 0; + } + + return static_cast(parsed); +} + +bool parse_codepoint_token(const std::string& token, uint32_t& out_codepoint) +{ + if (token.empty()) + { + return false; + } + + char* end = nullptr; + const unsigned long value = std::strtoul(token.c_str(), &end, 0); + if (end == token.c_str() || (end && *end != '\0') || value > 0x10FFFFUL) + { + return false; + } + + out_codepoint = static_cast(value); + return true; +} + +void normalize_ranges(std::vector& ranges) +{ + if (ranges.empty()) + { + return; + } + + std::sort(ranges.begin(), ranges.end(), + [](const CodepointRange& lhs, const CodepointRange& rhs) + { + if (lhs.first != rhs.first) + { + return lhs.first < rhs.first; + } + return lhs.last < rhs.last; + }); + + std::vector merged; + merged.reserve(ranges.size()); + for (const CodepointRange& range : ranges) + { + if (merged.empty() || range.first > (merged.back().last + 1U)) + { + merged.push_back(range); + continue; + } + + if (range.last > merged.back().last) + { + merged.back().last = range.last; + } + } + + ranges.swap(merged); +} + +bool parse_ranges_file(const std::string& path, std::vector& out) +{ + out.clear(); + + std::string contents; + if (!::ui::fs::read_text_file(path.c_str(), contents)) + { + return false; + } + + std::size_t token_start = 0; + while (token_start <= contents.size()) + { + const std::size_t token_end = contents.find_first_of(",\r\n", token_start); + std::string token = contents.substr( + token_start, + token_end == std::string::npos ? std::string::npos : (token_end - token_start)); + trim_in_place(token); + if (!token.empty()) + { + const std::size_t dash = token.find('-'); + uint32_t first = 0; + uint32_t last = 0; + if (dash == std::string::npos) + { + if (parse_codepoint_token(token, first)) + { + last = first; + CodepointRange range; + range.first = first; + range.last = last; + out.push_back(range); + } + } + else + { + const std::string lhs = trim_copy(token.substr(0, dash)); + const std::string rhs = trim_copy(token.substr(dash + 1)); + if (parse_codepoint_token(lhs, first) && parse_codepoint_token(rhs, last) && + first <= last) + { + CodepointRange range; + range.first = first; + range.last = last; + out.push_back(range); + } + } + } + + if (token_end == std::string::npos) + { + break; + } + token_start = token_end + 1U; + } + + normalize_ranges(out); + return !out.empty(); +} + +FontPackUsage parse_font_usage(const char* value) +{ + const std::string normalized = lowercase_ascii(value ? value : ""); + if (normalized == "ui") + { + return FontPackUsage::UiOnly; + } + if (normalized == "content") + { + return FontPackUsage::ContentOnly; + } + return FontPackUsage::Both; +} + +bool font_pack_supports_ui(const FontPackRecord& pack) +{ + return pack.usage == FontPackUsage::UiOnly || pack.usage == FontPackUsage::Both; +} + +bool font_pack_supports_content(const FontPackRecord& pack) +{ + return pack.usage == FontPackUsage::ContentOnly || pack.usage == FontPackUsage::Both; +} + +std::size_t estimated_pack_ram_bytes(const FontPackRecord* pack) +{ + if (!pack || pack->builtin) + { + return 0; + } + + return pack->estimated_ram_bytes; +} + +std::size_t active_locale_font_ram_bytes(const FontPackRecord* ui_pack, const FontPackRecord* content_pack) +{ + std::size_t total = 0; + total += estimated_pack_ram_bytes(ui_pack); + if (content_pack != ui_pack) + { + total += estimated_pack_ram_bytes(content_pack); + } + return total; +} + +bool locale_font_budget_allows(const FontPackRecord* ui_pack, const FontPackRecord* content_pack) +{ + const std::size_t total = active_locale_font_ram_bytes(ui_pack, content_pack); + if (total == 0) + { + return true; + } + + return total <= ::ui::runtime::current_memory_profile().max_locale_font_ram_bytes; +} + +const lv_font_t* resolved_font(const FontPackRecord* pack) +{ + if (!pack) + { + return nullptr; + } + + return pack->builtin ? pack->builtin_font : pack->owned_font; +} + +bool is_font_runtime_loaded(const FontPackRecord& pack) +{ + return resolved_font(&pack) != nullptr; +} + +bool font_pack_covers_codepoint(const FontPackRecord& pack, uint32_t codepoint) +{ + if (pack.coverage.empty()) + { + return false; + } + + std::size_t left = 0; + std::size_t right = pack.coverage.size(); + while (left < right) + { + const std::size_t mid = left + ((right - left) / 2U); + const CodepointRange& range = pack.coverage[mid]; + if (codepoint < range.first) + { + right = mid; + } + else if (codepoint > range.last) + { + left = mid + 1U; + } + else + { + return true; + } + } + + return false; +} + +bool decode_next_utf8(const unsigned char*& ptr, uint32_t& out_codepoint) +{ + if (!ptr || *ptr == 0) + { + return false; + } + + const unsigned char lead = *ptr++; + if ((lead & 0x80U) == 0) + { + out_codepoint = lead; + return true; + } + + int extra_bytes = 0; + uint32_t codepoint = 0; + if ((lead & 0xE0U) == 0xC0U) + { + extra_bytes = 1; + codepoint = static_cast(lead & 0x1FU); + } + else if ((lead & 0xF0U) == 0xE0U) + { + extra_bytes = 2; + codepoint = static_cast(lead & 0x0FU); + } + else if ((lead & 0xF8U) == 0xF0U) + { + extra_bytes = 3; + codepoint = static_cast(lead & 0x07U); + } + else + { + out_codepoint = 0xFFFD; + return true; + } + + for (int i = 0; i < extra_bytes; ++i) + { + const unsigned char next = *ptr; + if ((next & 0xC0U) != 0x80U) + { + out_codepoint = 0xFFFD; + return true; + } + ++ptr; + codepoint = (codepoint << 6U) | static_cast(next & 0x3FU); + } + + out_codepoint = codepoint; + return true; +} + +void collect_non_ascii_codepoints(const char* text, std::vector& out) +{ + out.clear(); + if (!text || text[0] == '\0') + { + return; + } + + const unsigned char* ptr = reinterpret_cast(text); + while (*ptr != 0) + { + uint32_t codepoint = 0; + if (!decode_next_utf8(ptr, codepoint)) + { + break; + } + if (codepoint >= 0x80U) + { + out.push_back(codepoint); + } + } + + if (out.empty()) + { + return; + } + + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); +} + +std::size_t coverage_hit_count(const FontPackRecord& pack, const std::vector& missing) +{ + std::size_t hits = 0; + for (uint32_t codepoint : missing) + { + if (font_pack_covers_codepoint(pack, codepoint)) + { + ++hits; + } + } + return hits; +} + +void append_unique_pack(std::vector& packs, FontPackRecord* pack) +{ + if (!pack) + { + return; + } + + for (FontPackRecord* existing : packs) + { + if (existing == pack) + { + return; + } + } + + packs.push_back(pack); +} + +void reset_font_chain(FontChainState& chain) +{ + chain.packs.clear(); + chain.composed_fonts.clear(); + chain.head = nullptr; + chain.desc.clear(); +} + +void rebuild_font_chain(FontChainState& chain, const std::vector& requested_packs) +{ + reset_font_chain(chain); + + std::vector usable_packs; + usable_packs.reserve(requested_packs.size()); + for (FontPackRecord* pack : requested_packs) + { + if (!pack || resolved_font(pack) == nullptr) + { + continue; + } + + bool duplicate = false; + for (FontPackRecord* existing : usable_packs) + { + if (existing == pack) + { + duplicate = true; + break; + } + } + if (!duplicate) + { + usable_packs.push_back(pack); + } + } + + chain.packs = usable_packs; + chain.composed_fonts.resize(usable_packs.size()); + + const lv_font_t* next_fallback = nullptr; + for (std::size_t index = usable_packs.size(); index > 0; --index) + { + FontPackRecord* pack = usable_packs[index - 1U]; + chain.composed_fonts[index - 1U] = *resolved_font(pack); + chain.composed_fonts[index - 1U].fallback = next_fallback; + next_fallback = &chain.composed_fonts[index - 1U]; + } + + chain.head = next_fallback; + for (std::size_t index = 0; index < usable_packs.size(); ++index) + { + if (!chain.desc.empty()) + { + chain.desc += ","; + } + chain.desc += usable_packs[index]->id; + } +} + +void unload_external_font_pack(FontPackRecord& pack) +{ +#if UI_I18N_HAVE_BINFONT + if (pack.owned_font != nullptr) + { + std::printf("%s font unload id=%s\n", kLogTag, pack.id.c_str()); + lv_binfont_destroy(pack.owned_font); + pack.owned_font = nullptr; + } +#else + pack.owned_font = nullptr; +#endif +} + +void release_runtime_fonts() +{ + for (auto& pack : s_font_packs) + { + if (!pack.builtin) + { + unload_external_font_pack(pack); + } + } + + s_content_supplement_packs.clear(); + reset_font_chain(s_ui_font_chain); + reset_font_chain(s_content_font_chain); +} + +bool load_font_pack(FontPackRecord& pack) +{ + if (pack.builtin) + { + return true; + } + if (pack.owned_font != nullptr) + { + return true; + } + if (pack.source_path.empty()) + { + return false; + } + +#if UI_I18N_HAVE_BINFONT + pack.owned_font = lv_binfont_create(pack.source_path.c_str()); + if (pack.owned_font == nullptr) + { + std::printf("%s font load failed id=%s source=%s\n", + kLogTag, + pack.id.c_str(), + pack.source_path.c_str()); + return false; + } + + std::printf("%s font load id=%s source=%s ram=%lu\n", + kLogTag, + pack.id.c_str(), + pack.source_path.c_str(), + static_cast(pack.estimated_ram_bytes)); + return true; +#else + std::printf("%s font load skipped id=%s reason=binfont_unavailable\n", + kLogTag, + pack.id.c_str()); + return false; +#endif +} + +bool ensure_font_pack_loaded(FontPackRecord* pack) +{ + if (!pack) + { + return false; + } + if (pack->builtin) + { + return true; + } + return load_font_pack(*pack); +} + +std::vector current_content_pack_sequence() +{ + std::vector packs; + packs.reserve(2U + s_content_supplement_packs.size()); + append_unique_pack(packs, s_active_content_font_pack); + append_unique_pack(packs, s_active_ui_font_pack); + for (FontPackRecord* pack : s_content_supplement_packs) + { + append_unique_pack(packs, pack); + } + return packs; +} + +void rebuild_runtime_font_chains() +{ + std::vector ui_packs; + append_unique_pack(ui_packs, s_active_ui_font_pack); + rebuild_font_chain(s_ui_font_chain, ui_packs); + rebuild_font_chain(s_content_font_chain, current_content_pack_sequence()); + ::ui::fonts::refresh_locale_font_bindings(); +} + +bool ensure_active_content_pack_loaded() +{ + bool changed = false; + + if (s_active_content_font_pack && !is_font_runtime_loaded(*s_active_content_font_pack)) + { + if (ensure_font_pack_loaded(s_active_content_font_pack)) + { + changed = true; + } + } + + if (s_active_ui_font_pack && !is_font_runtime_loaded(*s_active_ui_font_pack)) + { + if (ensure_font_pack_loaded(s_active_ui_font_pack)) + { + changed = true; + } + } + + if (changed) + { + rebuild_runtime_font_chains(); + } + + return true; +} + +bool codepoint_covered_by_loaded_packs(const std::vector& packs, uint32_t codepoint) +{ + if (codepoint < 0x80U) + { + return true; + } + + for (FontPackRecord* pack : packs) + { + if (!pack || !is_font_runtime_loaded(*pack)) + { + continue; + } + if (font_pack_covers_codepoint(*pack, codepoint)) + { + return true; + } + } + + return false; +} + +std::vector missing_content_codepoints(const char* text) +{ + std::vector codepoints; + collect_non_ascii_codepoints(text, codepoints); + if (codepoints.empty()) + { + return {}; + } + + const std::vector packs = current_content_pack_sequence(); + std::vector missing; + missing.reserve(codepoints.size()); + for (uint32_t codepoint : codepoints) + { + if (!codepoint_covered_by_loaded_packs(packs, codepoint)) + { + missing.push_back(codepoint); + } + } + return missing; +} + +std::size_t content_supplement_ram_bytes() +{ + std::size_t total = 0; + for (const FontPackRecord* pack : s_content_supplement_packs) + { + if (pack) + { + total += pack->estimated_ram_bytes; + } + } + return total; +} + +bool can_add_content_supplement(const FontPackRecord& pack) +{ + const auto& profile = ::ui::runtime::current_memory_profile(); + if (profile.max_content_supplement_packs == 0 || profile.max_content_supplement_ram_bytes == 0) + { + return false; + } + + for (const FontPackRecord* existing : s_content_supplement_packs) + { + if (existing == &pack) + { + return true; + } + } + + if (s_content_supplement_packs.size() >= profile.max_content_supplement_packs) + { + return false; + } + + if (pack.estimated_ram_bytes == 0) + { + return true; + } + + return (content_supplement_ram_bytes() + pack.estimated_ram_bytes) <= + profile.max_content_supplement_ram_bytes; +} + +FontPackRecord* choose_content_supplement(const std::vector& missing) +{ + FontPackRecord* best = nullptr; + std::size_t best_hits = 0; + std::size_t best_bytes = 0; + + for (auto& pack : s_font_packs) + { + if (&pack == s_active_content_font_pack || &pack == s_active_ui_font_pack) + { + continue; + } + if (!font_pack_supports_content(pack)) + { + continue; + } + if (pack.coverage.empty()) + { + continue; + } + if (!can_add_content_supplement(pack)) + { + continue; + } + + const std::size_t hits = coverage_hit_count(pack, missing); + if (hits == 0) + { + continue; + } + + const std::size_t bytes = pack.estimated_ram_bytes; + if (best == nullptr || hits > best_hits || (hits == best_hits && bytes < best_bytes)) + { + best = &pack; + best_hits = hits; + best_bytes = bytes; + } + } + + return best; +} + +void rebuild_locale_views() +{ + s_locale_views.clear(); + s_locale_views.reserve(s_locale_packs.size()); + + for (const auto& pack : s_locale_packs) + { + LocaleInfo view{}; + view.id = pack.id.c_str(); + view.display_name = pack.display_name.c_str(); + view.native_name = pack.native_name.empty() ? pack.display_name.c_str() : pack.native_name.c_str(); + view.ui_font_pack_id = pack.ui_font_pack_id.c_str(); + view.content_font_pack_id = pack.content_font_pack_id.c_str(); + view.ime_pack_id = pack.ime_pack_id.empty() ? nullptr : pack.ime_pack_id.c_str(); + view.builtin = pack.builtin; + s_locale_views.push_back(view); + } +} + +void add_builtin_font_packs() +{ + FontPackRecord latin_pack{}; + latin_pack.id = kBuiltinLatinFontPackId; + latin_pack.display_name = "Latin UI"; + latin_pack.usage = FontPackUsage::Both; + latin_pack.builtin = true; + latin_pack.builtin_font = nullptr; + latin_pack.estimated_ram_bytes = 0; + s_font_packs.push_back(std::move(latin_pack)); +} + +void add_builtin_locale_packs() +{ + LocalePackRecord english_pack{}; + english_pack.id = kDefaultLocaleId; + english_pack.display_name = kBuiltinEnglishName; + english_pack.native_name = kBuiltinEnglishName; + english_pack.ui_font_pack_id = kBuiltinLatinFontPackId; + english_pack.content_font_pack_id = kBuiltinLatinFontPackId; + english_pack.builtin = true; + s_locale_packs.push_back(std::move(english_pack)); +} + +std::string resolve_pack_file_path(const std::string& pack_dir, const char* path_value) +{ + if (!path_value || path_value[0] == '\0') + { + return {}; + } + + return join_path(pack_dir, path_value); +} + +bool catalog_external_font_pack(const std::string& pack_dir) +{ + const std::string manifest_path = join_path(pack_dir, "manifest.ini"); + Manifest manifest; + if (!parse_manifest_file(manifest_path, manifest)) + { + return false; + } + + const char* kind = manifest_value(manifest, "kind"); + if (!kind || std::strcmp(kind, "font") != 0) + { + return false; + } + + const char* id = manifest_value(manifest, "id"); + if (!id || id[0] == '\0' || find_pack_by_id(s_font_packs, id) != nullptr) + { + return false; + } + + FontPackRecord pack{}; + pack.id = id; + if (const char* display_name = manifest_value(manifest, "display_name")) + { + pack.display_name = display_name; + } + if (pack.display_name.empty()) + { + pack.display_name = pack.id; + } + pack.usage = parse_font_usage(manifest_value(manifest, "usage")); + pack.estimated_ram_bytes = parse_size_value(manifest_value(manifest, "estimated_ram_bytes")); + + const char* source = manifest_value(manifest, "source"); + const std::string source_kind = lowercase_ascii(source ? source : "binfont"); + if (source_kind == "builtin") + { + const char* builtin_id = manifest_value(manifest, "builtin_id"); + const FontPackRecord* builtin_pack = find_pack_by_id(s_font_packs, builtin_id); + if (!builtin_pack) + { + std::printf("%s skip font pack id=%s reason=missing_builtin builtin_id=%s\n", + kLogTag, + pack.id.c_str(), + safe_text(builtin_id)); + return false; + } + + pack.builtin = true; + pack.builtin_font = builtin_pack->builtin_font; + pack.source_path = builtin_pack->id; + } + else + { + const char* file_value = manifest_value(manifest, "file"); + const std::string font_path = resolve_pack_file_path(pack_dir, file_value); + if (font_path.empty()) + { + std::printf("%s skip font pack id=%s reason=missing_file\n", kLogTag, pack.id.c_str()); + return false; + } + pack.source_path = ::ui::fs::normalize_path(font_path.c_str()); + } + + std::string ranges_path; + if (const char* ranges_value = manifest_value(manifest, "ranges")) + { + ranges_path = resolve_pack_file_path(pack_dir, ranges_value); + } + else + { + const std::string fallback_ranges_path = join_path(pack_dir, "ranges.txt"); + if (::ui::fs::file_exists(fallback_ranges_path.c_str())) + { + ranges_path = fallback_ranges_path; + } + } + + if (!ranges_path.empty()) + { + (void)parse_ranges_file(ranges_path, pack.coverage); + } + + std::printf("%s font pack catalog id=%s builtin=%d usage=%s ram=%lu source=%s\n", + kLogTag, + pack.id.c_str(), + pack.builtin ? 1 : 0, + usage_name(pack.usage), + static_cast(pack.estimated_ram_bytes), + pack.source_path.empty() ? "" : pack.source_path.c_str()); + s_font_packs.push_back(std::move(pack)); + return true; +} + +bool catalog_external_ime_pack(const std::string& pack_dir) +{ + const std::string manifest_path = join_path(pack_dir, "manifest.ini"); + Manifest manifest; + if (!parse_manifest_file(manifest_path, manifest)) + { + return false; + } + + const char* kind = manifest_value(manifest, "kind"); + if (!kind || std::strcmp(kind, "ime") != 0) + { + return false; + } + + const char* id = manifest_value(manifest, "id"); + if (!id || id[0] == '\0' || find_pack_by_id(s_ime_packs, id) != nullptr) + { + return false; + } + + ImePackRecord pack{}; + pack.id = id; + if (const char* display_name = manifest_value(manifest, "display_name")) + { + pack.display_name = display_name; + } + if (pack.display_name.empty()) + { + pack.display_name = pack.id; + } + if (const char* backend = manifest_value(manifest, "backend")) + { + pack.backend = backend; + } + if (pack.backend.empty()) + { + pack.backend = "none"; + } + + std::printf("%s ime pack catalog id=%s backend=%s\n", + kLogTag, + pack.id.c_str(), + pack.backend.c_str()); + s_ime_packs.push_back(std::move(pack)); + return true; +} + +bool parse_locale_strings(const std::string& path, + std::vector>& out) +{ + out.clear(); + + std::string contents; + if (!::ui::fs::read_text_file(path.c_str(), contents)) + { + return false; + } + + std::size_t line_start = 0; + while (line_start <= contents.size()) + { + const std::size_t line_end = contents.find('\n', line_start); + std::string line = contents.substr( + line_start, + line_end == std::string::npos ? std::string::npos : (line_end - line_start)); + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + + if (!line.empty() && line[0] != '#') + { + const std::size_t sep = line.find('\t'); + if (sep != std::string::npos && sep > 0 && sep + 1U < line.size()) + { + std::string english = decode_escapes(line.substr(0, sep)); + std::string localized = decode_escapes(line.substr(sep + 1U)); + if (!english.empty()) + { + out.emplace_back(std::move(english), std::move(localized)); + } + } + } + + if (line_end == std::string::npos) + { + break; + } + line_start = line_end + 1U; + } + + return !out.empty(); +} + +bool catalog_external_locale_pack(const std::string& pack_dir) +{ + const std::string manifest_path = join_path(pack_dir, "manifest.ini"); + Manifest manifest; + if (!parse_manifest_file(manifest_path, manifest)) + { + return false; + } + + const char* kind = manifest_value(manifest, "kind"); + if (!kind || std::strcmp(kind, "locale") != 0) + { + return false; + } + + const char* id = manifest_value(manifest, "id"); + if (!id || id[0] == '\0' || find_pack_by_id(s_locale_packs, id) != nullptr) + { + return false; + } + + LocalePackRecord pack{}; + pack.id = id; + if (const char* display_name = manifest_value(manifest, "display_name")) + { + pack.display_name = display_name; + } + if (pack.display_name.empty()) + { + pack.display_name = pack.id; + } + if (const char* native_name = manifest_value(manifest, "native_name")) + { + pack.native_name = native_name; + } + if (pack.native_name.empty()) + { + pack.native_name = pack.display_name; + } + + if (const char* ui_font_pack = manifest_value(manifest, "ui_font_pack")) + { + pack.ui_font_pack_id = ui_font_pack; + } + if (const char* content_font_pack = manifest_value(manifest, "content_font_pack")) + { + pack.content_font_pack_id = content_font_pack; + } + if (pack.content_font_pack_id.empty()) + { + pack.content_font_pack_id = pack.ui_font_pack_id; + } + if (pack.ui_font_pack_id.empty()) + { + pack.ui_font_pack_id = kBuiltinLatinFontPackId; + } + if (pack.content_font_pack_id.empty()) + { + pack.content_font_pack_id = pack.ui_font_pack_id; + } + + if (const char* ime_pack = manifest_value(manifest, "ime_pack")) + { + pack.ime_pack_id = ime_pack; + } + + const char* strings_value = manifest_value(manifest, "strings"); + const std::string strings_path = resolve_pack_file_path(pack_dir, strings_value); + if (strings_path.empty() || !parse_locale_strings(strings_path, pack.translations)) + { + std::printf("%s skip locale pack id=%s reason=missing_strings path=%s\n", + kLogTag, + pack.id.c_str(), + strings_path.empty() ? "" : strings_path.c_str()); + return false; + } + + std::printf("%s locale pack catalog id=%s ui_font=%s content_font=%s ime=%s strings=%lu\n", + kLogTag, + pack.id.c_str(), + pack.ui_font_pack_id.c_str(), + pack.content_font_pack_id.c_str(), + pack.ime_pack_id.empty() ? "" : pack.ime_pack_id.c_str(), + static_cast(pack.translations.size())); + s_locale_packs.push_back(std::move(pack)); + return true; +} + +void scan_pack_group(const char* root_path, bool (*loader)(const std::string&)) +{ + std::vector entries; + if (!list_directory_entries(root_path, entries)) + { + return; + } + + for (const auto& entry : entries) + { + const std::string pack_dir = join_path(root_path, entry); + if (!::ui::fs::dir_exists(pack_dir.c_str())) + { + continue; + } + (void)loader(pack_dir); + } +} + +void catalog_external_packs() +{ + if (!external_pack_scan_enabled() || !::ui::fs::dir_exists(kPackRoot)) + { + return; + } + + scan_pack_group(kFontPackRoot, catalog_external_font_pack); + scan_pack_group(kImePackRoot, catalog_external_ime_pack); + scan_pack_group(kLocalePackRoot, catalog_external_locale_pack); +} + +bool locale_dependencies_resolved(const LocalePackRecord& locale) +{ + const FontPackRecord* ui_pack = find_pack_by_id(s_font_packs, locale.ui_font_pack_id.c_str()); + const FontPackRecord* content_pack = + find_pack_by_id(s_font_packs, locale.content_font_pack_id.c_str()); + + if (ui_pack == nullptr) + { + std::printf("%s skip locale id=%s reason=missing_ui_font_pack font=%s\n", + kLogTag, + locale.id.c_str(), + locale.ui_font_pack_id.c_str()); + return false; + } + if (content_pack == nullptr) + { + std::printf("%s skip locale id=%s reason=missing_content_font_pack font=%s\n", + kLogTag, + locale.id.c_str(), + locale.content_font_pack_id.c_str()); + return false; + } + if (!font_pack_supports_ui(*ui_pack)) + { + std::printf("%s skip locale id=%s reason=ui_font_usage font=%s usage=%s\n", + kLogTag, + locale.id.c_str(), + ui_pack->id.c_str(), + usage_name(ui_pack->usage)); + return false; + } + if (!font_pack_supports_content(*content_pack)) + { + std::printf("%s skip locale id=%s reason=content_font_usage font=%s usage=%s\n", + kLogTag, + locale.id.c_str(), + content_pack->id.c_str(), + usage_name(content_pack->usage)); + return false; + } + const std::size_t locale_font_ram = active_locale_font_ram_bytes(ui_pack, content_pack); + if (!locale_font_budget_allows(ui_pack, content_pack)) + { + std::printf( + "%s skip locale id=%s reason=memory_profile profile=%s ui_font=%s content_font=%s locale_font_ram=%lu budget=%lu\n", + kLogTag, + locale.id.c_str(), + ::ui::runtime::current_memory_profile().name, + ui_pack->id.c_str(), + content_pack->id.c_str(), + static_cast(locale_font_ram), + static_cast( + ::ui::runtime::current_memory_profile().max_locale_font_ram_bytes)); + return false; + } + if (!locale.ime_pack_id.empty() && + find_pack_by_id(s_ime_packs, locale.ime_pack_id.c_str()) == nullptr) + { + std::printf("%s skip locale id=%s reason=missing_ime_pack ime=%s\n", + kLogTag, + locale.id.c_str(), + locale.ime_pack_id.c_str()); + return false; + } + + return true; +} + +void prune_unresolved_locales() +{ + s_locale_packs.erase( + std::remove_if( + s_locale_packs.begin(), + s_locale_packs.end(), + [](const LocalePackRecord& locale) + { + return !locale_dependencies_resolved(locale); + }), + s_locale_packs.end()); +} + +void remove_legacy_locale_key() +{ + const char* keys[] = {kLegacyDisplayLanguageKey}; + ::platform::ui::settings_store::remove_keys( + kSettingsNamespace, keys, sizeof(keys) / sizeof(keys[0])); +} + +std::string migrate_legacy_locale_if_needed() +{ + std::string locale_id; + if (::platform::ui::settings_store::get_string(kSettingsNamespace, kDisplayLocaleKey, locale_id) && + !locale_id.empty()) + { + return locale_id; + } + + const int legacy_value = + ::platform::ui::settings_store::get_int(kSettingsNamespace, kLegacyDisplayLanguageKey, -1); + if (legacy_value < 0) + { + return {}; + } + + locale_id = legacy_value == 1 ? "zh-Hans" : kDefaultLocaleId; + (void)::platform::ui::settings_store::put_string( + kSettingsNamespace, kDisplayLocaleKey, locale_id.c_str()); + remove_legacy_locale_key(); + + std::printf("%s migrate legacy display_language=%d -> display_locale=%s\n", + kLogTag, + legacy_value, + locale_id.c_str()); + return locale_id; +} + +LocalePackRecord* resolve_active_locale(const std::string& preferred_id) +{ + if (!preferred_id.empty()) + { + if (LocalePackRecord* preferred = find_pack_by_id(s_locale_packs, preferred_id.c_str())) + { + return preferred; + } + } + + if (LocalePackRecord* english = find_pack_by_id(s_locale_packs, kDefaultLocaleId)) + { + return english; + } + + return s_locale_packs.empty() ? nullptr : &s_locale_packs.front(); +} + +void clear_registry() +{ + ::ui::fonts::clear_locale_font_bindings(); + release_runtime_fonts(); + + s_font_packs.clear(); + s_ime_packs.clear(); + s_locale_packs.clear(); + s_locale_views.clear(); + + s_active_locale = nullptr; + s_active_ui_font_pack = nullptr; + s_active_content_font_pack = nullptr; + s_active_ime_pack = nullptr; + + s_registry_ready = false; +} + +bool activate_locale(LocalePackRecord* locale) +{ + release_runtime_fonts(); + + s_active_locale = locale; + s_active_ui_font_pack = locale ? find_pack_by_id(s_font_packs, locale->ui_font_pack_id.c_str()) : nullptr; + s_active_content_font_pack = + locale ? find_pack_by_id(s_font_packs, locale->content_font_pack_id.c_str()) : nullptr; + s_active_ime_pack = (locale && !locale->ime_pack_id.empty()) + ? find_pack_by_id(s_ime_packs, locale->ime_pack_id.c_str()) + : nullptr; + + if (s_active_ui_font_pack != nullptr && !ensure_font_pack_loaded(s_active_ui_font_pack)) + { + if (locale != nullptr && locale->id != kDefaultLocaleId) + { + LocalePackRecord* fallback = resolve_active_locale(kDefaultLocaleId); + if (fallback && fallback != locale) + { + return activate_locale(fallback); + } + } + } + + rebuild_runtime_font_chains(); + + std::printf("%s active locale=%s ui_font=%s content_font=%s ime=%s ui_chain=%s content_chain=%s\n", + kLogTag, + s_active_locale ? s_active_locale->id.c_str() : "", + s_active_ui_font_pack ? s_active_ui_font_pack->id.c_str() : "", + s_active_content_font_pack ? s_active_content_font_pack->id.c_str() : "", + s_active_ime_pack ? s_active_ime_pack->id.c_str() : "", + s_ui_font_chain.desc.empty() ? "" : s_ui_font_chain.desc.c_str(), + s_content_font_chain.desc.empty() ? "" : s_content_font_chain.desc.c_str()); + return true; +} + +void rebuild_registry() +{ + clear_registry(); + add_builtin_font_packs(); + add_builtin_locale_packs(); + catalog_external_packs(); + prune_unresolved_locales(); + rebuild_locale_views(); + s_registry_ready = true; + + const std::string preferred_locale = migrate_legacy_locale_if_needed(); + (void)activate_locale(resolve_active_locale(preferred_locale)); +} + +void ensure_registry() +{ + if (!s_registry_ready) + { + rebuild_registry(); + } +} + +const char* lookup_translation(const LocalePackRecord& locale, const char* english) +{ + if (!english || english[0] == '\0') + { + return ""; + } + + for (const auto& entry : locale.translations) + { + if (entry.first == english) + { + return entry.second.c_str(); + } + } + + return english; +} + +} // namespace + +void reload_language() +{ + rebuild_registry(); +} + +std::size_t locale_count() +{ + ensure_registry(); + return s_locale_views.size(); +} + +const LocaleInfo* locale_at(std::size_t index) +{ + ensure_registry(); + if (index >= s_locale_views.size()) + { + return nullptr; + } + return &s_locale_views[index]; +} + +int current_locale_index() +{ + ensure_registry(); + if (s_active_locale == nullptr) + { + return 0; + } + + for (std::size_t index = 0; index < s_locale_packs.size(); ++index) + { + if (&s_locale_packs[index] == s_active_locale) + { + return static_cast(index); + } + } + + return 0; +} + +const char* current_locale_id() +{ + ensure_registry(); + return s_active_locale ? s_active_locale->id.c_str() : kDefaultLocaleId; +} + +const char* current_locale_display_name() +{ + ensure_registry(); + if (s_active_locale == nullptr) + { + return kBuiltinEnglishName; + } + return s_active_locale->native_name.empty() ? s_active_locale->display_name.c_str() + : s_active_locale->native_name.c_str(); +} + +bool set_locale(const char* locale_id, bool persist) +{ + ensure_registry(); + LocalePackRecord* next_locale = find_pack_by_id(s_locale_packs, locale_id); + if (next_locale == nullptr) + { + return false; + } + + const LocalePackRecord* previous_locale = s_active_locale; + (void)activate_locale(next_locale); + + if (persist && s_active_locale != nullptr) + { + (void)::platform::ui::settings_store::put_string( + kSettingsNamespace, kDisplayLocaleKey, s_active_locale->id.c_str()); + remove_legacy_locale_key(); + } + + return previous_locale != s_active_locale; +} + +bool set_locale_by_index(std::size_t index, bool persist) +{ + const LocaleInfo* locale = locale_at(index); + return locale != nullptr && set_locale(locale->id, persist); +} + +const lv_font_t* active_ui_font_fallback() +{ + ensure_registry(); + return s_ui_font_chain.head; +} + +const lv_font_t* active_content_font_fallback() +{ + ensure_registry(); + return s_content_font_chain.head; +} + +bool ensure_content_font_for_text(const char* text) +{ + ensure_registry(); + if (!text || text[0] == '\0') + { + return true; + } + + bool changed = false; + changed |= ensure_active_content_pack_loaded(); + + std::vector missing = missing_content_codepoints(text); + while (!missing.empty()) + { + FontPackRecord* candidate = choose_content_supplement(missing); + if (candidate == nullptr) + { + break; + } + if (!ensure_font_pack_loaded(candidate)) + { + break; + } + + append_unique_pack(s_content_supplement_packs, candidate); + changed = true; + rebuild_runtime_font_chains(); + missing = missing_content_codepoints(text); + } + + if (changed) + { + rebuild_runtime_font_chains(); + } + + return missing.empty(); +} + +const char* active_ime_pack_id() +{ + ensure_registry(); + return s_active_ime_pack ? s_active_ime_pack->id.c_str() : nullptr; +} + +bool active_locale_supports_script_input() +{ + ensure_registry(); + return s_active_ime_pack != nullptr && !s_active_ime_pack->backend.empty() && + s_active_ime_pack->backend != "none"; +} + +const char* tr(const char* english) +{ + ensure_registry(); + if (!english) + { + return ""; + } + if (s_active_locale == nullptr || s_active_locale->id == kDefaultLocaleId) + { + return english; + } + return lookup_translation(*s_active_locale, english); +} + +std::string vformat(const char* english_fmt, va_list args) +{ + const char* localized_fmt = tr(english_fmt); + if (localized_fmt == nullptr) + { + return {}; + } + + va_list size_args; + va_copy(size_args, args); + const int size = std::vsnprintf(nullptr, 0, localized_fmt, size_args); + va_end(size_args); + if (size <= 0) + { + return localized_fmt; + } + + std::vector buffer(static_cast(size) + 1U, '\0'); + va_list write_args; + va_copy(write_args, args); + std::vsnprintf(buffer.data(), buffer.size(), localized_fmt, write_args); + va_end(write_args); + return std::string(buffer.data()); +} + +std::string format(const char* english_fmt, ...) +{ + va_list args; + va_start(args, english_fmt); + std::string out = vformat(english_fmt, args); + va_end(args); + return out; +} + +void set_label_text(lv_obj_t* label, const char* english) +{ + const char* localized = tr(english); + if (label) + { + lv_label_set_text(label, localized); + ::ui::fonts::apply_localized_font(label, localized, lv_obj_get_style_text_font(label, LV_PART_MAIN)); + } +} + +void set_label_text_raw(lv_obj_t* label, const char* text) +{ + if (label) + { + const char* value = text ? text : ""; + lv_label_set_text(label, value); + ::ui::fonts::apply_localized_font(label, value, lv_obj_get_style_text_font(label, LV_PART_MAIN)); + } +} + +void set_content_label_text(lv_obj_t* label, const char* english) +{ + const char* localized = tr(english); + if (label) + { + lv_label_set_text(label, localized); + ::ui::fonts::apply_content_font(label, localized, lv_obj_get_style_text_font(label, LV_PART_MAIN)); + } +} + +void set_content_label_text_raw(lv_obj_t* label, const char* text) +{ + if (label) + { + const char* value = text ? text : ""; + lv_label_set_text(label, value); + ::ui::fonts::apply_content_font(label, value, lv_obj_get_style_text_font(label, LV_PART_MAIN)); + } +} + +void set_label_text_fmt(lv_obj_t* label, const char* english_fmt, ...) +{ + if (!label) + { + return; + } + + va_list args; + va_start(args, english_fmt); + const std::string text = vformat(english_fmt, args); + va_end(args); + set_label_text_raw(label, text.c_str()); +} + +} // namespace ui::i18n diff --git a/modules/ui_shared/src/ui/localization.cpp b/modules/ui_shared/src/ui/localization.cpp deleted file mode 100644 index 2044b189..00000000 --- a/modules/ui_shared/src/ui/localization.cpp +++ /dev/null @@ -1,713 +0,0 @@ -#include "ui/localization.h" - -#include -#include -#include -#include -#include - -#include "platform/ui/settings_store.h" -#include "ui/assets/fonts/font_utils.h" - -namespace ui::i18n -{ -namespace -{ - -constexpr const char* kSettingsNamespace = "settings"; -constexpr const char* kDisplayLanguageKey = "display_language"; - -struct TranslationEntry -{ - const char* english; - const char* chinese; -}; - -constexpr TranslationEntry kTranslations[] = { - {"Action", "操作"}, - {"Add", "添加"}, - {"Advanced", "高级"}, - {"Always", "常亮"}, - {"Altitude", "高度"}, - {"Altitude Reference", "高度参考"}, - {"App", "应用"}, - {"Area Cleared", "区域清除"}, - {"Auto", "自动"}, - {"Awaiting teammate", "等待队友"}, - {"Back", "返回"}, - {"Base Camp", "营地"}, - {"Beijing (UTC+8)", "北京 (UTC+8)"}, - {"BLE bridge ready | %s", "BLE 桥接已就绪 | %s"}, - {"BLE linked | %s", "BLE 已连接 | %s"}, - {"Bluetooth", "蓝牙"}, - {"Bluetooth Pairing", "蓝牙配对"}, - {"Bcast", "广播"}, - {"Bridge error", "桥接错误"}, - {"Broadcast", "广播"}, - {"Cancel", "取消"}, - {"Channel", "信道"}, - {"Chat", "聊天"}, - {"Chat unavailable", "聊天不可用"}, - {"Chinese", "中文"}, - {"Closes automatically after pairing", "配对完成后将自动关闭"}, - {"Close", "关闭"}, - {"Command", "指令"}, - {"Command: %s", "指令:%s"}, - {"Command: %s %s", "指令:%s %s"}, - {"Command: %s %s %s", "指令:%s %s %s"}, - {"Compare Verification Code", "比对验证码"}, - {"Compass", "指南针"}, - {"Connected", "已连接"}, - {"Connected, handshaking...", "已连接,正在握手..."}, - {"Contacts", "联系人"}, - {"Contacts (%s)", "联系人(%s)"}, - {"Contacts (LXMF)", "联系人(LXMF)"}, - {"Contacts (LXMF - %.3fMHz)", "联系人(LXMF - %.3fMHz)"}, - {"Contacts (MeshCore)", "联系人(MeshCore)"}, - {"Contacts (Meshtastic - %.3fMHz)", "联系人(Meshtastic - %.3fMHz)"}, - {"Contacts (RNode)", "联系人(RNode)"}, - {"Contacts (RNode - %.3fMHz)", "联系人(RNode - %.3fMHz)"}, - {"Conversation protocol mismatch", "会话协议不匹配"}, - {"Data Exchange", "数据交换"}, - {"Debug Logs", "调试日志"}, - {"Decoding: %d%%", "解码中:%d%%"}, - {"Decoding: 0%", "解码中:0%"}, - {"Delete", "删除"}, - {"Delete %s?", "删除 %s?"}, - {"Delete contact %s?", "删除联系人 %s?"}, - {"Delete failed", "删除失败"}, - {"Device: %s", "设备:%s"}, - {"Direct", "私聊"}, - {"Disabled", "已禁用"}, - {"Discover", "发现"}, - {"Display Language", "显示语言"}, - {"Duplicate name not allowed", "名称不能重复"}, - {"Edit", "编辑"}, - {"Edit nickname", "编辑昵称"}, - {"Emergency SOS", "紧急求救"}, - {"Energy Sweep", "频谱扫描"}, - {"English", "English"}, - {"English keyboard", "英文键盘"}, - {"Enter nickname", "输入昵称"}, - {"Enter number for %s", "输入 %s 的校验数字"}, - {"Enter the 6-digit number", "请输入 6 位数字"}, - {"Enter this 6-digit PIN on your phone", "请在手机上输入这个 6 位 PIN 码"}, - {"Error", "错误"}, - {"Error: %lu", "错误:%lu"}, - {"FSK Voice", "FSK 语音"}, - {"Failed", "失败"}, - {"Fix", "定位"}, - {"Fixed PIN", "固定 PIN"}, - {"Gauge Design (mAh)", "电池设计容量 (mAh)"}, - {"Gauge Full (mAh)", "电池满充容量 (mAh)"}, - {"Good Find", "发现目标"}, - {"GPS", "GPS"}, - {"GPS Status", "GPS 状态"}, - {"HDOP", "HDOP"}, - {"Heading starting", "航向启动中"}, - {"Hide", "隐藏"}, - {"Host bridge", "主机桥接"}, - {"Host connected, probing modem...", "主机已连接,正在探测调制解调器..."}, - {"IDLE", "空闲"}, - {"Ignored", "已忽略"}, - {"Ignore", "忽略"}, - {"Ignore unavailable", "当前无法忽略"}, - {"Ignore update failed", "忽略状态更新失败"}, - {"Image received", "图像已接收"}, - {"Info", "信息"}, - {"Initializing...", "初始化中..."}, - {"Invalid airtime factor", "空口系数无效"}, - {"Invalid design capacity (mAh)", "设计容量 (mAh) 无效"}, - {"Invalid frequency offset", "频偏无效"}, - {"Invalid frequency value", "频率值无效"}, - {"Invalid full capacity (mAh)", "满充容量 (mAh) 无效"}, - {"Invalid marker", "无效标记"}, - {"Invalid MeshCore bandwidth", "MeshCore 带宽无效"}, - {"Invalid MeshCore frequency", "MeshCore 频率无效"}, - {"Invalid number", "数字无效"}, - {"Invalid RX delay", "接收延迟无效"}, - {"Key marked trusted", "密钥已标记为可信"}, - {"Key must be 32 hex or 16 chars", "密钥必须为 32 位十六进制或 16 个字符"}, - {"Key trust failed", "密钥信任失败"}, - {"Key Verification", "密钥校验"}, - {"LINKED", "已组网"}, - {"LISTEN", "监听"}, - {"LIVE", "实时"}, - {"Loading...", "加载中..."}, - {"Location", "位置"}, - {"Location: %s", "位置:%s"}, - {"Location: %s %s", "位置:%s %s"}, - {"LoRa direct path | %s", "LoRa 直连 | %s"}, - {"Low battery - audio disabled", "电量低,已禁用音频"}, - {"Map", "地图"}, - {"Map Layer", "地图图层"}, - {"MESH", "网状"}, - {"Mesh Status", "网状状态"}, - {"Mesh unavailable", "Mesh 不可用"}, - {"MeshCore only", "仅支持 MeshCore"}, - {"Message DB cleared", "消息数据库已清空"}, - {"Messages", "消息"}, - {"MESSAGES", "消息"}, - {"Messages and broadcasts show here", "消息和广播会显示在这里"}, - {"Mini Compass", "迷你指南针"}, - {"MT send uses slot 0/1 only", "Meshtastic 发送仅支持槽位 0/1"}, - {"N/A", "不适用"}, - {"Nearby", "附近"}, - {"Need local GPS", "需要本机 GPS"}, - {"Network", "网络"}, - {"New", "新建"}, - {"new activity", "有新动态"}, - {"Next", "下一页"}, - {"No GPS fix", "没有 GPS 定位"}, - {"No image", "没有图像"}, - {"No messages", "暂无消息"}, - {"No preview", "无预览"}, - {"No recent traffic", "暂无最近活动"}, - {"No response", "无响应"}, - {"No route selected", "未选择路线"}, - {"No target", "没有目标"}, - {"No track files", "没有轨迹文件"}, - {"Node DB reset", "节点数据库已重置"}, - {"Node ignored", "节点已忽略"}, - {"Node ignored and hidden", "节点已忽略并隐藏"}, - {"Node unignored", "已取消忽略节点"}, - {"North locked", "北向已锁定"}, - {"Not set", "未设置"}, - {"NO", "否"}, - {"NO FIX", "无定位"}, - {"Number keyboard", "数字键盘"}, - {"OFF", "关"}, - {"OK", "确定"}, - {"ON", "开"}, - {"online", "在线"}, - {"Pairing", "配对"}, - {"PC Link", "电脑连接"}, - {"PC Link is unavailable on this target.", "当前目标暂不支持电脑连接."}, - {"peer lock | turn %03.0f deg", "目标锁定 | 转向 %03.0f 度"}, - {"Pinyin keyboard", "拼音键盘"}, - {"Pinyin: %s", "拼音:%s"}, - {"Position", "位置"}, - {"position stream active | %u in view", "定位流活跃 | 可见 %u 颗"}, - {"Power", "电量"}, - {"Press Back", "按返回键"}, - {"Press RX to start", "按 RX 开始"}, - {"Primary", "主信道"}, - {"Protocol", "协议"}, - {"Protocol switch failed", "协议切换失败"}, - {"Protocol switched", "协议已切换"}, - {"quiet net", "网络安静"}, - {"Random PIN", "随机 PIN"}, - {"Ready", "就绪"}, - {"RECEIVING", "接收中"}, - {"Recent Activity", "最近活动"}, - {"Record", "记录"}, - {"Recording", "记录中"}, - {"Region", "区域"}, - {"Remain: %u", "剩余:%u"}, - {"Reply", "回复"}, - {"Reply disabled for this protocol", "此协议下不能回复"}, - {"Reset Mesh Profiles", "重置 Mesh 配置"}, - {"Reset Node DB", "重置节点数据库"}, - {"Resetting...", "重置中..."}, - {"Return", "返回"}, - {"Rally Point", "集结点"}, - {"RNode Bridge", "RNode Bridge"}, - {"RNode modem ready", "RNode 调制解调器已就绪"}, - {"RNode text chat runs on host", "RNode 文本聊天在主机侧运行"}, - {"Route", "路线"}, - {"Run", "执行"}, - {"RX", "RX"}, - {"RX: %lu TX: %lu", "接收:%lu 发送:%lu"}, - {"RX: 0 TX: 0", "接收:0 发送:0"}, - {"Save", "保存"}, - {"Saved: %s", "已保存:%s"}, - {"Scan failed", "扫描失败"}, - {"Scan Local", "本地扫描"}, - {"Scanning 5s...", "扫描 5 秒中..."}, - {"Screen Brightness", "屏幕亮度"}, - {"Screen Timeout", "熄屏超时"}, - {"SEARCH", "搜索中"}, - {"Secondary", "次信道"}, - {"Select a route", "请选择路线"}, - {"Select a track", "请选择轨迹"}, - {"Select marker", "选择标记"}, - {"Select Track", "选择轨迹"}, - {"Selected: %s", "已选:%s"}, - {"Send", "发送"}, - {"Send failed", "发送失败"}, - {"Send this code and compare:\n", "发送这个验证码并比对:\n"}, - {"Send ID Broadcast", "广播发送 ID"}, - {"Send ID Local", "本地发送 ID"}, - {"Sending...", "发送中..."}, - {"Sent", "已发送"}, - {"Settings", "设置"}, - {"Setting", "设置"}, - {"Share Position Marker", "分享位置标记"}, - {"Share this number:\n", "请分享这个数字:\n"}, - {"Short Name", "短名称"}, - {"SOLO", "单机"}, - {"Speaker Volume", "提示音音量"}, - {"Sky Plot", "星图"}, - {"SSTV RX READY", "SSTV 接收就绪"}, - {"SSTV start failed", "SSTV 启动失败"}, - {"standing by", "待命"}, - {"Status", "状态"}, - {"Stopped", "已停止"}, - {"Stop", "停止"}, - {"Stop recording first", "请先停止记录"}, - {"Submit", "提交"}, - {"Submit failed", "提交失败"}, - {"System", "系统"}, - {"SATELLITE STATUS", "卫星状态"}, - {"Talk", "说话"}, - {"TALK", "说话"}, - {"Team", "团队"}, - {"TEAM", "团队"}, - {"Team chat send failed", "团队消息发送失败"}, - {"Team chat unavailable", "团队聊天不可用"}, - {"Team chat unavailable in RNode mode", "RNode 模式下团队聊天不可用"}, - {"team member position will appear here", "队友位置会显示在这里"}, - {"Team keys not ready", "团队密钥未就绪"}, - {"Team location encode failed", "团队位置编码失败"}, - {"Text chat unavailable", "文本聊天不可用"}, - {"threads active", "活跃会话"}, - {"Time Zone", "时区"}, - {"Tracker", "轨迹"}, - {"traffic active", "流量活跃"}, - {"Trust Key", "信任密钥"}, - {"U", "用"}, - {"Unignore", "取消忽略"}, - {"Unread", "未读"}, - {"Unknown", "未知"}, - {"USB Mass Storage", "USB 大容量存储"}, - {"USB Disk", "USB存储"}, - {"USB Bridge", "USB 桥接"}, - {"USB host bridge", "USB 主机桥接"}, - {"USB Bridge is unavailable on this target.", "当前目标暂不支持 USB 桥接."}, - {"USB CDC KISS modem for Reticulum", "面向 Reticulum 的 USB CDC KISS 调制解调器"}, - {"USE", "用"}, - {"Used", "已用"}, - {"User Name", "用户名"}, - {"Verification Number", "校验数字"}, - {"Verification number sent", "校验数字已发送"}, - {"Vibration", "振动"}, - {"VOL %d", "音量 %d"}, - {"VOL 80", "音量 80"}, - {"waiting for better sky view | %u in view", "等待更好的天空视野 | 可见 %u 颗"}, - {"waiting for module GNSS compass", "等待 GNSS/指南针模块"}, - {"waiting for traffic", "等待流量"}, - {"Waiting for host...", "等待主机连接..."}, - {"Waiting for Reticulum host...", "等待 Reticulum 主机连接..."}, - {"WAIT IMU", "等待 IMU"}, - {"Walkie not available", "对讲功能不可用"}, - {"Walkie start failed", "对讲启动失败"}, - {"Walkie Talkie", "对讲机"}, - {"YES", "是"}, - {"6 digits", "6 位数字"}, - {"%u threads active", "%u 个会话活跃"}, - {"%ud", "%u天"}, - {"%uh", "%u小时"}, - {"%um", "%u分"}, - {"%umo", "%u月"}, - {"%uy", "%u年"}, - {"Confirm received code:\n", "确认收到的验证码:\n"}, - {"device heading from 9-axis module", "设备航向来自 9 轴模块"}, - {"heading unlocks after first fix", "首次定位后解锁航向"}, - {"If it matches, trust the key.", "如果一致,请信任此密钥。"}, - {"magnetometer heading active", "磁力计航向已启用"}, - {"magnetometer still warming up", "磁力计仍在预热"}, - {"needle shows north until target lock", "锁定目标前指针指向北方"}, - {"nearest peer | %.0f m away", "最近队友 | 距离 %.0f 米"}, - {"no recent traffic", "暂无最近活动"}, - {"now", "刚刚"}, - {"%s layer missing", "%s 图层缺失"}, - {"Active: %s", "已激活:%s"}, - {"Alt: %.0f m", "高度:%.0f 米"}, - {"Alt: -- m", "高度:-- 米"}, - {"Base: %s", "底图:%s"}, - {"Battery critical", "电池电量危险"}, - {"Broadcast (%d)", "广播(%d)"}, - {"COMPLETE", "已完成"}, - {"Calibrating...", "校准中..."}, - {"Confirm", "确认"}, - {"Contour: OFF", "等高线:关"}, - {"Decoder error", "解码错误"}, - {"Direct (%d)", "私聊(%d)"}, - {"E", "东"}, - {"ERROR", "错误"}, - {"Exiting USB...", "正在退出 USB..."}, - {"Hold", "原地待命"}, - {"ID: !%08lX", "ID: !%08lX"}, - {"ID: -", "ID: -"}, - {"Key refresh not implemented", "暂未实现密钥刷新"}, - {"Key verify: %s %s", "密钥校验:%s %s"}, - {"Key verify: confirm %s %s", "密钥校验:请确认 %s %s"}, - {"Key verify: enter number for %s", "密钥校验:请为 %s 输入数字"}, - {"Key verify: send %s %s", "密钥校验:请发送 %s %s"}, - {"Later", "稍后"}, - {"Level %d", "级别 %d"}, - {"Listening for SSTV signal...", "正在监听 SSTV 信号..."}, - {"Low battery: %d%%", "电量低:%d%%"}, - {"MODE: %s", "模式:%s"}, - {"MODE: Auto", "模式:自动"}, - {"Map - %.48s", "地图 - %.48s"}, - {"Map - %s Missing", "地图 - %s 缺失"}, - {"Map - No Map Data", "地图 - 没有地图数据"}, - {"Map - No SD Card", "地图 - 没有 SD 卡"}, - {"Map - no gps data", "地图 - 没有 GPS 数据"}, - {"Message", "消息"}, - {"Mic TBD", "麦克风功能待定义"}, - {"Mic init failed", "麦克风初始化失败"}, - {"Morse Input", "摩斯电码输入"}, - {"MoveTo", "移动到"}, - {"N", "北"}, - {"NODE INFO", "节点信息"}, - {"Name required", "必须输入名称"}, - {"Name too long", "名称过长"}, - {"No KML routes", "没有 KML 路线"}, - {"No SD Card", "没有 SD 卡"}, - {"No routes folder", "没有 routes 文件夹"}, - {"No tracks yet", "暂无轨迹"}, - {"Only leader can manage", "只有队长可以管理"}, - {"Only leader can pair", "只有队长可以配对"}, - {"Paired successfully", "配对成功"}, - {"Pairing failed", "配对失败"}, - {"Pairing init failed", "配对初始化失败"}, - {"Pairing not available", "当前无法配对"}, - {"Pairing not ready", "配对尚未就绪"}, - {"Press Back to exit USB mode", "按返回键退出 USB 模式"}, - {"Press SPACE to resume", "按 SPACE 唤醒屏幕"}, - {"RallyTo", "集结到"}, - {"Route configured", "已配置路线"}, - {"Route: %s", "路线:%s"}, - {"SD Card Not Found\nPlease insert SD card", "未找到 SD 卡\n请插入 SD 卡"}, - {"SSTV RECEIVER", "SSTV 接收器"}, - {"Save failed", "保存失败"}, - {"Scan +%u/%u", "扫描新增:+%u/%u"}, - {"Select level", "选择级别"}, - {"Selected level: %d", "已选级别:%d"}, - {"Shutting down to protect battery", "正在关机以保护电池"}, - {"Status: CALIB", "状态:校准"}, - {"Step 1/2: Tap short dots", "第 1/2 步:点击短点"}, - {"Step 2/2: Tap long dashes", "第 2/2 步:点击长线"}, - {"Switch to %s to chat", "切换到 %s 以聊天"}, - {"Symbol: ", "符号:"}, - {"Tap to input, idle 3s to send", "点击输入,空闲 3 秒后发送"}, - {"Team keys mismatch", "团队密钥不匹配"}, - {"Team: ", "团队:"}, - {"Text: ", "文本:"}, - {"Unread: %d", "未读:%d"}, - {"Unread: 0", "未读:0"}, - {"W", "西"}, - {"0° Horizon", "0° 地平线"}, - {"PSK must be 32 hex or 16 chars", "PSK 必须是 32 位十六进制或 16 个字符"}, - {"Restarting...", "正在重启..."}, - {"awaiting position", "等待位置"}, - {"compass wakes after GPS fix", "GPS 定位后指南针将启用"}, - {"searching satellites", "正在搜索卫星"}, - {"%s -", "%s -"}, - {"%s 0s", "%s 0秒"}, - {"%s %us", "%s %u秒"}, - {"%s %um", "%s %u分"}, - {"%s %uh", "%s %u小时"}, - {"%s %ud", "%s %u天"}, - {"AUTO", "自动"}, - {"Alt: %ld m", "高度:%ld 米"}, - {"Alt: -", "高度:-"}, - {"BEST", "最佳"}, - {"BW: %.0fk", "带宽:%.0fk"}, - {"BW: %.1fk", "带宽:%.1fk"}, - {"BW: -", "带宽:-"}, - {"CAD", "CAD"}, - {"CURSOR", "游标"}, - {"Ch: %.3f", "信道:%.3f"}, - {"Ch: -", "信道:-"}, - {"Hop: %u", "跳数:%u"}, - {"Hop: -", "跳数:-"}, - {"Hop: - / NH: %02X", "跳数:- / 下一跳:%02X"}, - {"ID: !%06lx", "ID: !%06lx"}, - {"ID: !%08lx", "ID: !%08lx"}, - {"Last heard:", "最后收到:"}, - {"Link", "链路"}, - {"Link / MeshCore", "链路 / MeshCore"}, - {"Link / Meshtastic", "链路 / Meshtastic"}, - {"MODE: RSSI", "模式:RSSI"}, - {"NOISE %.0f dBm", "噪声 %.0f dBm"}, - {"NOISE -104 dBm", "噪声 -104 dBm"}, - {"PKI: -", "PKI:-"}, - {"PKI: known", "PKI:已知"}, - {"PKI: verified", "PKI:已验证"}, - {"RSSI %.0f dBm", "RSSI %.0f dBm"}, - {"RSSI -92 dBm", "RSSI -92 dBm"}, - {"RSSI: %.0f dBm", "RSSI:%.0f dBm"}, - {"RSSI: -", "RSSI:-"}, - {"Role: %s", "角色:%s"}, - {"SCAN", "扫描"}, - {"SF: %u", "扩频因子:%u"}, - {"SF: -", "扩频因子:-"}, - {"SIM", "模拟"}, - {"SNR +%d", "SNR +%d"}, - {"SNR +12", "SNR +12"}, - {"SNR: %.1f dB", "SNR:%.1f dB"}, - {"SNR: -", "SNR:-"}, - {"SNR %.0f", "信噪比 %.0f"}, - {"SNR -", "信噪比 -"}, - {"SNR Good", "信噪比良好"}, - {"SNR Weak", "信噪比较弱"}, - {"STEP %.0fk | BW %.0fk", "步进 %.0fk | 带宽 %.0fk"}, - {"STEP %.1fk | BW %.1fk", "步进 %.1fk | 带宽 %.1fk"}, - {"STEP -- | BW --", "步进 -- | 带宽 --"}, - {"STOP", "停止"}, - {"Slot", "槽位"}, - {"Slot %u", "槽位 %u"}, - {"SUB-GHz SCAN", "Sub-GHz 扫描"}, - {"Actions", "操作"}, - {"Actions: %s", "操作:%s"}, - {"Access to team data revoked", "队伍数据访问已撤销"}, - {"Capability:", "能力:"}, - {"Channel Actions", "频道操作"}, - {"Confirm Kick", "确认移除"}, - {"Create Team", "创建队伍"}, - {"ELEV", "仰角"}, - {"Join Team", "加入队伍"}, - {"In View", "可见"}, - {"Keep devices within 5m", "请将设备保持在 5 米内"}, - {"KeyDist", "密钥分发"}, - {"KeyId: %u", "密钥 ID:%u"}, - {"KeyId: --", "密钥 ID:--"}, - {"Keys", "密钥"}, - {"Kick", "移除"}, - {"Kick Member", "移除成员"}, - {"Last paired: %s", "最近配对:%s"}, - {"Last seen %uh ago", "上次在线 %u 小时前"}, - {"Last seen %um ago", "上次在线 %u 分钟前"}, - {"Last seen --", "上次在线:--"}, - {"Last update %us ago", "上次更新 %u 秒前"}, - {"Last update --", "上次更新:--"}, - {"Last update 0s ago", "上次更新 0 秒前"}, - {"Leader", "队长"}, - {"Leader online", "队长在线"}, - {"Leave", "离开"}, - {"Leave team?", "离开队伍?"}, - {"Manage", "管理"}, - {"Member: %s", "成员:%s"}, - {"Members", "成员"}, - {"Members: %u", "成员数:%u"}, - {"Members: %u Online: %u", "成员:%u 在线:%u"}, - {"Modem Bridge", "调制解调器桥接"}, - {"No members yet", "暂无成员"}, - {"Not Used", "未使用"}, - {"Offline", "离线"}, - {"Online", "在线"}, - {"Online: %u", "在线:%u"}, - {"Pair Again", "再次配对"}, - {"Pair Member", "配对成员"}, - {"Pairing in progress", "正在配对"}, - {"Prev", "上一页"}, - {"Remove %s from team?", "将 %s 移出队伍?"}, - {"Request Keys", "请求密钥"}, - {"Retry", "重试"}, - {"Security Round: %u", "安全轮次:%u"}, - {"Security Round: --", "安全轮次:--"}, - {"Security: OK (Round %u)", "安全状态:正常(轮次 %u)"}, - {"Security: OK (Round --)", "安全状态:正常(轮次 --)"}, - {"Seen %ud", "%u 天前"}, - {"Seen %uh", "%u 小时前"}, - {"Seen %um", "%u 分钟前"}, - {"Send failed (keys not ready)", "发送失败(密钥未就绪)"}, - {"Status: %s", "状态:%s"}, - {"Target: %s", "目标:%s"}, - {"Team / %s", "队伍 / %s"}, - {"Team Actions", "队伍操作"}, - {"Team Health", "队伍状态"}, - {"Team Status", "队伍状态"}, - {"Team: %s", "队伍:%s"}, - {"This clears local keys.", "这将清除本地密钥。"}, - {"This will update the security round.\nThe member will no longer receive\nteam messages or waypoints.", "这将更新安全轮次。\n该成员将不再接收\n队伍消息或航点。"}, - {"Transfer", "移交"}, - {"Transfer Leader", "移交队长"}, - {"View Team", "查看队伍"}, - {"Waiting for new keys...", "正在等待新密钥..."}, - {"Waypoint", "航点"}, - {"You are no longer in this team", "你已不在此队伍中"}, - {"You are not in a team", "你当前未加入队伍"}, - {"- No shared map\n- No team awareness", "- 无共享地图\n- 无队伍态势"}, - {"1 member stale", "1 名成员状态过期"}, - {"encrypt failed", "加密失败"}, - {"member", "成员"}, - {"queue full", "队列已满"}, - {"unsupported protocol", "协议不支持"}, - {"Factory Reset", "恢复出厂设置"}, - {"Clear all settings and restart?", "清空全部设置并重启?"}, -}; - -bool s_loaded = false; -Language s_language = Language::English; - -Language load_language_from_store() -{ - return language_from_raw( - ::platform::ui::settings_store::get_int(kSettingsNamespace, kDisplayLanguageKey, 0)); -} - -void ensure_loaded() -{ - if (s_loaded) - { - return; - } - - s_language = load_language_from_store(); - s_loaded = true; -} - -const char* lookup_translation(const char* english) -{ - if (!english) - { - return ""; - } - - for (const auto& entry : kTranslations) - { - if (std::strcmp(entry.english, english) == 0) - { - return entry.chinese; - } - } - - return english; -} - -void apply_label_font(lv_obj_t* label, const char* text) -{ - if (!label) - { - return; - } - - const lv_font_t* current_font = lv_obj_get_style_text_font(label, LV_PART_MAIN); - ::ui::fonts::apply_localized_font(label, text, current_font); -} - -} // namespace - -Language language_from_raw(int raw_value) -{ - return raw_value == static_cast(Language::Chinese) ? Language::Chinese : Language::English; -} - -void reload_language() -{ - s_language = load_language_from_store(); - s_loaded = true; -} - -Language current_language() -{ - ensure_loaded(); - return s_language; -} - -bool set_language(Language language, bool persist) -{ - ensure_loaded(); - const bool changed = s_language != language; - s_language = language; - - if (persist) - { - ::platform::ui::settings_store::put_int( - kSettingsNamespace, kDisplayLanguageKey, static_cast(language)); - } - - return changed; -} - -bool supports_chinese() -{ -#if defined(GAT562_NO_CJK) && GAT562_NO_CJK - return false; -#else - return true; -#endif -} - -const char* tr(const char* english) -{ - ensure_loaded(); - if (s_language == Language::Chinese) - { - return lookup_translation(english); - } - return english ? english : ""; -} - -std::string vformat(const char* english_fmt, va_list args) -{ - const char* localized_fmt = tr(english_fmt); - if (localized_fmt == nullptr) - { - return {}; - } - - va_list size_args; - va_copy(size_args, args); - const int size = std::vsnprintf(nullptr, 0, localized_fmt, size_args); - va_end(size_args); - if (size <= 0) - { - return localized_fmt; - } - - std::vector buffer(static_cast(size) + 1U, '\0'); - va_list write_args; - va_copy(write_args, args); - std::vsnprintf(buffer.data(), buffer.size(), localized_fmt, write_args); - va_end(write_args); - return std::string(buffer.data()); -} - -std::string format(const char* english_fmt, ...) -{ - va_list args; - va_start(args, english_fmt); - std::string out = vformat(english_fmt, args); - va_end(args); - return out; -} - -void set_label_text(lv_obj_t* label, const char* english) -{ - const char* localized = tr(english); - if (label) - { - lv_label_set_text(label, localized); - apply_label_font(label, localized); - } -} - -void set_label_text_raw(lv_obj_t* label, const char* text) -{ - if (label) - { - lv_label_set_text(label, text ? text : ""); - apply_label_font(label, text ? text : ""); - } -} - -void set_label_text_fmt(lv_obj_t* label, const char* english_fmt, ...) -{ - if (!label) - { - return; - } - - va_list args; - va_start(args, english_fmt); - const std::string text = vformat(english_fmt, args); - va_end(args); - set_label_text_raw(label, text.c_str()); -} - -} // namespace ui::i18n diff --git a/modules/ui_shared/src/ui/menu/menu_layout.cpp b/modules/ui_shared/src/ui/menu/menu_layout.cpp index c423c7cb..d99ec116 100644 --- a/modules/ui_shared/src/ui/menu/menu_layout.cpp +++ b/modules/ui_shared/src/ui/menu/menu_layout.cpp @@ -43,11 +43,20 @@ struct MenuAppUi lv_obj_t* label = nullptr; }; +struct BottomBarChipUi +{ + lv_obj_t* container = nullptr; + lv_obj_t* label = nullptr; +}; + lv_obj_t* s_menu_panel = nullptr; lv_obj_t* s_app_panel = nullptr; lv_obj_t* s_grid_panel = nullptr; lv_obj_t* s_desc_label = nullptr; -lv_obj_t* s_node_id_label = nullptr; +lv_obj_t* s_bottom_bar = nullptr; +BottomBarChipUi s_bottom_node_chip{}; +BottomBarChipUi s_bottom_ram_chip{}; +BottomBarChipUi s_bottom_psram_chip{}; MenuAppUi s_menu_apps[kMaxMenuApps]; InitOptions s_init_options{}; @@ -458,6 +467,66 @@ void createAppButton(lv_obj_t* parent, AppScreen* app, size_t idx) lv_obj_add_event_cb(btn, menuButtonClickCallback, LV_EVENT_CLICKED, app); } +BottomBarChipUi createBottomBarChip(lv_obj_t* parent, + const ui::menu_profile::MenuLayoutProfile& profile, + lv_color_t bg_color, + const char* text) +{ + BottomBarChipUi chip{}; + chip.container = lv_obj_create(parent); + lv_obj_set_size(chip.container, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_bg_color(chip.container, bg_color, 0); + lv_obj_set_style_bg_opa(chip.container, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(chip.container, 0, 0); + lv_obj_set_style_radius(chip.container, 10, 0); + lv_obj_set_style_shadow_width(chip.container, 0, 0); + lv_obj_set_style_pad_left(chip.container, 8, 0); + lv_obj_set_style_pad_right(chip.container, 8, 0); + lv_obj_set_style_pad_top(chip.container, 3, 0); + lv_obj_set_style_pad_bottom(chip.container, 3, 0); + lv_obj_set_style_min_height(chip.container, profile.top_bar_height - 6, 0); + lv_obj_clear_flag(chip.container, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(chip.container, LV_OBJ_FLAG_CLICKABLE); + + chip.label = lv_label_create(chip.container); + lv_obj_set_width(chip.label, LV_SIZE_CONTENT); + lv_obj_set_style_text_align(chip.label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(chip.label, ui::theme::text(), 0); + lv_obj_set_style_bg_opa(chip.label, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_all(chip.label, 0, 0); + lv_obj_set_style_text_font(chip.label, profile.node_id_font, 0); + lv_label_set_long_mode(chip.label, LV_LABEL_LONG_CLIP); + lv_label_set_text(chip.label, text ? text : ""); + lv_obj_center(chip.label); + return chip; +} + +void setBottomBarChipText(const BottomBarChipUi& chip, const char* text) +{ + if (chip.label == nullptr) + { + return; + } + lv_label_set_text(chip.label, text ? text : ""); +} + +void setBottomBarChipVisible(const BottomBarChipUi& chip, bool visible) +{ + if (chip.container == nullptr) + { + return; + } + + if (visible) + { + lv_obj_clear_flag(chip.container, LV_OBJ_FLAG_HIDDEN); + } + else + { + lv_obj_add_flag(chip.container, LV_OBJ_FLAG_HIDDEN); + } +} + void createPanels() { main_screen = lv_tileview_create(lv_screen_active()); @@ -582,28 +651,43 @@ void createAppGrid() lv_obj_set_style_text_font(s_desc_label, profile.desc_font, 0); #if !defined(ARDUINO_T_WATCH_S3) - s_node_id_label = lv_label_create(s_menu_panel); - lv_obj_set_width(s_node_id_label, LV_SIZE_CONTENT); - lv_obj_set_style_text_align(s_node_id_label, LV_TEXT_ALIGN_LEFT, 0); - lv_obj_set_style_text_color(s_node_id_label, ui::theme::text_muted(), 0); - lv_obj_set_style_text_font(s_node_id_label, profile.node_id_font, 0); - lv_obj_align(s_node_id_label, LV_ALIGN_BOTTOM_LEFT, profile.node_id_offset_x, profile.node_id_offset_y); + s_bottom_bar = lv_obj_create(s_menu_panel); + lv_obj_set_size(s_bottom_bar, LV_PCT(100), profile.top_bar_height); + lv_obj_set_style_bg_color(s_bottom_bar, ui::theme::surface_alt(), 0); + lv_obj_set_style_bg_opa(s_bottom_bar, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(s_bottom_bar, 0, 0); + lv_obj_set_style_radius(s_bottom_bar, 0, 0); + lv_obj_set_style_shadow_width(s_bottom_bar, 0, 0); + lv_obj_set_style_pad_left(s_bottom_bar, profile.top_bar_side_inset, 0); + lv_obj_set_style_pad_right(s_bottom_bar, profile.top_bar_side_inset, 0); + lv_obj_set_style_pad_top(s_bottom_bar, 0, 0); + lv_obj_set_style_pad_bottom(s_bottom_bar, 0, 0); + lv_obj_set_style_pad_column(s_bottom_bar, 6, 0); + lv_obj_set_flex_flow(s_bottom_bar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(s_bottom_bar, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_align(s_bottom_bar, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_clear_flag(s_bottom_bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(s_bottom_bar, LV_OBJ_FLAG_CLICKABLE); if (!profile.show_node_id) { - lv_obj_add_flag(s_node_id_label, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(s_bottom_bar, LV_OBJ_FLAG_HIDDEN); } + s_bottom_node_chip = createBottomBarChip(s_bottom_bar, profile, lv_color_hex(0xF1B75A), "-"); + s_bottom_ram_chip = createBottomBarChip(s_bottom_bar, profile, lv_color_hex(0xCFE4FF), "--/--"); + s_bottom_psram_chip = createBottomBarChip(s_bottom_bar, profile, lv_color_hex(0xD4F0D2), "--/--"); + char node_id_buf[24]; const uint32_t self_id = s_init_options.messaging ? s_init_options.messaging->getSelfNodeId() : 0; if (self_id != 0) { - std::snprintf(node_id_buf, sizeof(node_id_buf), "ID: !%08lX", static_cast(self_id)); + std::snprintf(node_id_buf, sizeof(node_id_buf), "!%08lX", static_cast(self_id)); } else { - std::snprintf(node_id_buf, sizeof(node_id_buf), "ID: -"); + std::snprintf(node_id_buf, sizeof(node_id_buf), "-"); } - lv_label_set_text(s_node_id_label, node_id_buf); + setBottomBarChipText(s_bottom_node_chip, node_id_buf); #endif lv_label_set_long_mode(s_desc_label, LV_LABEL_LONG_SCROLL_CIRCULAR); @@ -647,9 +731,9 @@ void bringContentToFront() { lv_obj_move_foreground(s_desc_label); } - if (s_node_id_label != nullptr) + if (s_bottom_bar != nullptr) { - lv_obj_move_foreground(s_node_id_label); + lv_obj_move_foreground(s_bottom_bar); } ui::menu::dashboard::bringToFront(); } @@ -686,6 +770,26 @@ void refresh_localized_text() ui::menu::dashboard::refresh_localized_text(); } +void set_bottom_bar_node_text(const char* text) +{ + setBottomBarChipText(s_bottom_node_chip, text); +} + +void set_bottom_bar_ram_text(const char* text) +{ + setBottomBarChipText(s_bottom_ram_chip, text); +} + +void set_bottom_bar_psram_text(const char* text) +{ + setBottomBarChipText(s_bottom_psram_chip, text); +} + +void set_bottom_bar_psram_visible(bool visible) +{ + setBottomBarChipVisible(s_bottom_psram_chip, visible); +} + void setMenuVisible(bool visible) { if (s_menu_panel != nullptr) diff --git a/modules/ui_shared/src/ui/menu/menu_runtime.cpp b/modules/ui_shared/src/ui/menu/menu_runtime.cpp index e702928f..75ac3db1 100644 --- a/modules/ui_shared/src/ui/menu/menu_runtime.cpp +++ b/modules/ui_shared/src/ui/menu/menu_runtime.cpp @@ -59,6 +59,83 @@ bool formatMenuTime(char* out, size_t out_len) return s_runtime.hooks.format_time ? s_runtime.hooks.format_time(out, out_len) : false; } +std::size_t used_bytes(std::size_t total_bytes, std::size_t free_bytes) +{ + return total_bytes > free_bytes ? (total_bytes - free_bytes) : 0; +} + +void format_memory_value(std::size_t bytes, char* out, std::size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + if (bytes >= (1024U * 1024U)) + { + const unsigned whole = static_cast(bytes / (1024U * 1024U)); + const unsigned tenth = + static_cast((bytes % (1024U * 1024U)) * 10U / (1024U * 1024U)); + if (tenth == 0U) + { + std::snprintf(out, out_len, "%uM", whole); + } + else + { + std::snprintf(out, out_len, "%u.%uM", whole, tenth); + } + return; + } + + std::snprintf(out, + out_len, + "%luK", + static_cast((bytes + 1023U) / 1024U)); +} + +void refreshBottomBar() +{ + char node_text[24]; + const uint32_t self_id = app::messagingFacade().getSelfNodeId(); + if (self_id != 0) + { + std::snprintf(node_text, sizeof(node_text), "!%08lX", static_cast(self_id)); + } + else + { + std::snprintf(node_text, sizeof(node_text), "-"); + } + ui::menu_layout::set_bottom_bar_node_text(node_text); + + const platform::ui::device::MemoryStats stats = platform::ui::device::memory_stats(); + char ram_used[16]; + char ram_total[16]; + char psram_used[16]; + char psram_total[16]; + char ram_text[32]; + char psram_text[32]; + + format_memory_value(used_bytes(stats.ram_total_bytes, stats.ram_free_bytes), ram_used, sizeof(ram_used)); + format_memory_value(stats.ram_total_bytes, ram_total, sizeof(ram_total)); + std::snprintf(ram_text, sizeof(ram_text), "%s/%s", ram_used, ram_total); + ui::menu_layout::set_bottom_bar_ram_text(ram_text); + + if (stats.psram_available && stats.psram_total_bytes > 0) + { + format_memory_value(used_bytes(stats.psram_total_bytes, stats.psram_free_bytes), + psram_used, + sizeof(psram_used)); + format_memory_value(stats.psram_total_bytes, psram_total, sizeof(psram_total)); + std::snprintf(psram_text, sizeof(psram_text), "%s/%s", psram_used, psram_total); + ui::menu_layout::set_bottom_bar_psram_text(psram_text); + ui::menu_layout::set_bottom_bar_psram_visible(true); + } + else + { + ui::menu_layout::set_bottom_bar_psram_visible(false); + } +} + void showMainMenu() { #if defined(ESP_PLATFORM) @@ -219,6 +296,7 @@ void refreshBatteryLabel() } updateWatchFaceTime(); + refreshBottomBar(); } void createTopBar() @@ -277,6 +355,7 @@ void createTopBar() lv_obj_t* menu_route_icon = nullptr; lv_obj_t* menu_tracker_icon = nullptr; lv_obj_t* menu_gps_icon = nullptr; + lv_obj_t* menu_wifi_icon = nullptr; lv_obj_t* menu_team_icon = nullptr; lv_obj_t* menu_msg_icon = nullptr; lv_obj_t* menu_ble_icon = nullptr; @@ -285,12 +364,14 @@ void createTopBar() menu_route_icon = lv_image_create(menu_status_row); menu_tracker_icon = lv_image_create(menu_status_row); menu_gps_icon = lv_image_create(menu_status_row); + menu_wifi_icon = lv_image_create(menu_status_row); menu_team_icon = lv_image_create(menu_status_row); menu_msg_icon = lv_image_create(menu_status_row); menu_ble_icon = lv_image_create(menu_status_row); lv_obj_add_flag(menu_route_icon, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(menu_tracker_icon, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(menu_gps_icon, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(menu_wifi_icon, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(menu_team_icon, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(menu_msg_icon, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(menu_ble_icon, LV_OBJ_FLAG_HIDDEN); @@ -301,7 +382,14 @@ void createTopBar() } ui::status::register_menu_status_row( - menu_status_row, menu_route_icon, menu_tracker_icon, menu_gps_icon, menu_team_icon, menu_msg_icon, menu_ble_icon); + menu_status_row, + menu_route_icon, + menu_tracker_icon, + menu_gps_icon, + menu_wifi_icon, + menu_team_icon, + menu_msg_icon, + menu_ble_icon); } void initWatchFace() @@ -359,6 +447,7 @@ void init(lv_obj_t* screen_root, lv_obj_t* main_screen, lv_obj_t* menu_panel, co createTimers(); refreshTimeLabel(); refreshBatteryLabel(); + refreshBottomBar(); setScene(Scene::Menu); } @@ -418,6 +507,7 @@ void setMenuActive(bool active) { refreshTimeLabel(); refreshBatteryLabel(); + refreshBottomBar(); } } diff --git a/modules/ui_shared/src/ui/runtime/memory_profile.cpp b/modules/ui_shared/src/ui/runtime/memory_profile.cpp new file mode 100644 index 00000000..988d094e --- /dev/null +++ b/modules/ui_shared/src/ui/runtime/memory_profile.cpp @@ -0,0 +1,65 @@ +#include "ui/runtime/memory_profile.h" + +namespace ui::runtime +{ +namespace +{ + +MemoryProfile make_constrained_profile() +{ + MemoryProfile profile{}; + profile.name = "constrained"; + profile.kind = MemoryProfileKind::Constrained; + profile.max_locale_font_ram_bytes = 128U * 1024U; + profile.max_content_supplement_ram_bytes = 0; + profile.max_content_supplement_packs = 0; + profile.max_map_decode_tiles = 2; + profile.retain_map_decode_cache_on_page_exit = false; + return profile; +} + +MemoryProfile make_standard_profile() +{ + MemoryProfile profile{}; + profile.name = "standard"; + profile.kind = MemoryProfileKind::Standard; + profile.max_locale_font_ram_bytes = 768U * 1024U; + profile.max_content_supplement_ram_bytes = 640U * 1024U; + profile.max_content_supplement_packs = 1; + profile.max_map_decode_tiles = 4; + profile.retain_map_decode_cache_on_page_exit = false; + return profile; +} + +MemoryProfile make_extended_profile() +{ + MemoryProfile profile{}; + profile.name = "extended"; + profile.kind = MemoryProfileKind::Extended; + profile.max_locale_font_ram_bytes = 2U * 1024U * 1024U; + profile.max_content_supplement_ram_bytes = 2U * 1024U * 1024U; + profile.max_content_supplement_packs = 3; + profile.max_map_decode_tiles = 12; + profile.retain_map_decode_cache_on_page_exit = true; + return profile; +} + +} // namespace + +const MemoryProfile& current_memory_profile() +{ + static const MemoryProfile profile = []() + { +#if defined(TRAIL_MATE_ESP_BOARD_TAB5) || defined(TRAIL_MATE_ESP_BOARD_T_DISPLAY_P4) + return make_extended_profile(); +#elif defined(ARDUINO_T_DECK) || defined(ARDUINO_T_DECK_PRO) + return make_standard_profile(); +#else + return make_constrained_profile(); +#endif + }(); + + return profile; +} + +} // namespace ui::runtime diff --git a/modules/ui_shared/src/ui/runtime/pack_repository.cpp b/modules/ui_shared/src/ui/runtime/pack_repository.cpp new file mode 100644 index 00000000..594c1d73 --- /dev/null +++ b/modules/ui_shared/src/ui/runtime/pack_repository.cpp @@ -0,0 +1,1578 @@ +#include "ui/runtime/pack_repository.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform/ui/device_runtime.h" +#include "platform/ui/wifi_runtime.h" +#include "ui/localization.h" +#include "ui/runtime/memory_profile.h" + +#if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) + +#include "cJSON.h" +#include "esp_err.h" +#include "esp_heap_caps.h" +#include "esp_http_client.h" +#include "mbedtls/platform.h" +#include "mbedtls/sha256.h" +#ifdef INADDR_NONE +#undef INADDR_NONE +#endif +#include "rom/miniz.h" + +#define UI_PACKS_HAVE_CRT_BUNDLE 1 + +extern "C" esp_err_t esp_crt_bundle_attach(void* conf); + +#if defined(ARDUINO) +#include +#include +#else +#include "platform/esp/idf_common/bsp_runtime.h" +#include +#include +#include +#include +#endif + +namespace ui::runtime::packs +{ +namespace +{ + +constexpr const char* kCatalogUrl = "https://vicliu624.github.io/trail-mate/data/packs.json"; +constexpr const char* kCatalogBaseUrl = "https://vicliu624.github.io/trail-mate"; +constexpr const char* kPackRoot = "/trailmate/packs"; +constexpr const char* kIndexDir = "/trailmate/packs/.index"; +constexpr const char* kTempDir = "/trailmate/packs/.index/tmp"; +constexpr const char* kInstalledIndexPath = "/trailmate/packs/.index/installed.json"; +constexpr int kHttpBufferSize = 1024; +constexpr int kHttpTxBufferSize = 512; +constexpr std::size_t kTlsLargeAllocThresholdBytes = 4096; +constexpr std::size_t kZipEocdMinSize = 22; +constexpr std::size_t kZipTailSearchMax = 0x10000 + kZipEocdMinSize; +constexpr std::uint32_t kZipEocdSignature = 0x06054B50u; +constexpr std::uint32_t kZipCentralSignature = 0x02014B50u; +constexpr std::uint32_t kZipLocalSignature = 0x04034B50u; + +struct InstalledIndex +{ + std::vector packages; +}; + +void log_pack_memory_snapshot(const char* stage) +{ + std::printf("[Packs][TLS] %s ram_free=%u ram_largest=%u psram_free=%u psram_largest=%u\n", + stage ? stage : "state", + static_cast( + heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)), + static_cast( + heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)), + static_cast(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)), + static_cast(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM))); +} + +// Arduino's prebuilt mbedTLS keeps 16 KB TLS record buffers, which can exhaust +// internal RAM on ESP32 UI builds. Route large allocations to PSRAM first while +// leaving small control allocations in internal RAM for lower latency. +void* mbedtls_calloc_prefer_psram(std::size_t count, std::size_t size) +{ + if (count == 0 || size == 0) + { + return nullptr; + } + if (count > (static_cast(-1) / size)) + { + return nullptr; + } + + const std::size_t bytes = count * size; + const bool prefer_psram = + heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0 && + bytes >= kTlsLargeAllocThresholdBytes; + + const uint32_t primary_caps = prefer_psram ? (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) + : (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const uint32_t secondary_caps = prefer_psram ? (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) + : (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + + return heap_caps_calloc_prefer(count, size, 2, primary_caps, secondary_caps); +} + +void mbedtls_free_prefer_psram(void* ptr) +{ + heap_caps_free(ptr); +} + +bool ensure_tls_allocator_configured() +{ + static bool attempted = false; + static bool configured = false; + + if (attempted) + { + return configured; + } + + attempted = true; + log_pack_memory_snapshot("before allocator config"); + configured = mbedtls_platform_set_calloc_free(&mbedtls_calloc_prefer_psram, + &mbedtls_free_prefer_psram) == 0; + std::printf("[Packs][TLS] allocator configured=%d threshold=%lu\n", + configured ? 1 : 0, + static_cast(kTlsLargeAllocThresholdBytes)); + log_pack_memory_snapshot("after allocator config"); + return configured; +} + +std::string lowercase_ascii(std::string value) +{ + for (char& ch : value) + { + ch = static_cast(std::tolower(static_cast(ch))); + } + return value; +} + +bool starts_with(const std::string& value, const char* prefix) +{ + if (!prefix) + { + return false; + } + const std::size_t prefix_len = std::strlen(prefix); + return value.size() >= prefix_len && value.compare(0, prefix_len, prefix) == 0; +} + +std::string trim_trailing_slash(std::string value) +{ + while (!value.empty() && value.back() == '/') + { + value.pop_back(); + } + return value; +} + +std::string join_url(const std::string& base, const std::string& path) +{ + if (path.empty()) + { + return base; + } + if (starts_with(path, "http://") || starts_with(path, "https://")) + { + return path; + } + const std::string normalized_base = trim_trailing_slash(base); + if (path.front() == '/') + { + return normalized_base + path; + } + return normalized_base + "/" + path; +} + +std::string active_memory_profile_name() +{ + const ui::runtime::MemoryProfile& profile = ui::runtime::current_memory_profile(); + return profile.name ? profile.name : ""; +} + +struct ParsedVersion +{ + int major = 0; + int minor = 0; + int patch = 0; + std::string prerelease; +}; + +ParsedVersion parse_version(const std::string& text) +{ + ParsedVersion version{}; + if (text.empty()) + { + return version; + } + + std::string numeric = text; + const std::size_t dash = numeric.find('-'); + if (dash != std::string::npos) + { + version.prerelease = numeric.substr(dash + 1); + numeric = numeric.substr(0, dash); + } + + int parts[3] = {0, 0, 0}; + std::size_t part_index = 0; + std::size_t start = 0; + while (start <= numeric.size() && part_index < 3) + { + const std::size_t end = numeric.find('.', start); + const std::string token = numeric.substr( + start, + end == std::string::npos ? std::string::npos : (end - start)); + if (!token.empty()) + { + parts[part_index] = std::atoi(token.c_str()); + } + ++part_index; + if (end == std::string::npos) + { + break; + } + start = end + 1; + } + + version.major = parts[0]; + version.minor = parts[1]; + version.patch = parts[2]; + return version; +} + +int compare_versions(const std::string& lhs, const std::string& rhs) +{ + const ParsedVersion left = parse_version(lhs); + const ParsedVersion right = parse_version(rhs); + + if (left.major != right.major) + { + return left.major < right.major ? -1 : 1; + } + if (left.minor != right.minor) + { + return left.minor < right.minor ? -1 : 1; + } + if (left.patch != right.patch) + { + return left.patch < right.patch ? -1 : 1; + } + if (left.prerelease == right.prerelease) + { + return 0; + } + if (left.prerelease.empty()) + { + return 1; + } + if (right.prerelease.empty()) + { + return -1; + } + return left.prerelease < right.prerelease ? -1 : 1; +} + +#if defined(ARDUINO) + +bool ensure_dir_recursive(const std::string& logical_dir) +{ + if (logical_dir.empty()) + { + return false; + } + + std::string current; + for (char ch : logical_dir) + { + current.push_back(ch); + if (ch != '/') + { + continue; + } + if (!current.empty() && !SD.exists(current.c_str())) + { + if (!SD.mkdir(current.c_str())) + { + return false; + } + } + } + + if (!SD.exists(logical_dir.c_str())) + { + return SD.mkdir(logical_dir.c_str()); + } + return true; +} + +bool logical_file_exists(const std::string& logical_path) +{ + return SD.exists(logical_path.c_str()); +} + +bool write_binary_file(const std::string& logical_path, const void* data, std::size_t len) +{ + const std::size_t slash = logical_path.find_last_of('/'); + if (slash != std::string::npos) + { + if (!ensure_dir_recursive(logical_path.substr(0, slash))) + { + return false; + } + } + + if (SD.exists(logical_path.c_str())) + { + (void)SD.remove(logical_path.c_str()); + } + + File file = SD.open(logical_path.c_str(), FILE_WRITE); + if (!file) + { + return false; + } + + const std::size_t written = file.write(static_cast(data), len); + file.close(); + return written == len; +} + +bool write_text_file(const std::string& logical_path, const std::string& text) +{ + return write_binary_file(logical_path, text.data(), text.size()); +} + +bool read_binary_file(const std::string& logical_path, std::vector& out) +{ + out.clear(); + File file = SD.open(logical_path.c_str(), FILE_READ); + if (!file) + { + return false; + } + + const std::size_t size = static_cast(file.size()); + out.resize(size); + const std::size_t read = file.read(out.data(), size); + file.close(); + if (read != size) + { + out.clear(); + return false; + } + return true; +} + +bool read_text_file(const std::string& logical_path, std::string& out) +{ + std::vector bytes; + if (!read_binary_file(logical_path, bytes)) + { + out.clear(); + return false; + } + out.assign(reinterpret_cast(bytes.data()), bytes.size()); + return true; +} + +bool remove_file_if_exists(const std::string& logical_path) +{ + if (!SD.exists(logical_path.c_str())) + { + return true; + } + return SD.remove(logical_path.c_str()); +} + +class RandomAccessFile +{ + public: + bool open(const std::string& logical_path) + { + file_ = SD.open(logical_path.c_str(), FILE_READ); + return static_cast(file_); + } + + void close() + { + if (file_) + { + file_.close(); + } + } + + std::size_t size() const + { + return file_ ? static_cast(file_.size()) : 0; + } + + bool read_at(std::size_t offset, void* out, std::size_t len) + { + if (!file_ || !file_.seek(offset)) + { + return false; + } + return static_cast(file_.read(static_cast(out), len)) == len; + } + + private: + File file_{}; +}; + +class SequentialWriteFile +{ + public: + bool open(const std::string& logical_path) + { + const std::size_t slash = logical_path.find_last_of('/'); + if (slash != std::string::npos) + { + if (!ensure_dir_recursive(logical_path.substr(0, slash))) + { + return false; + } + } + if (SD.exists(logical_path.c_str())) + { + (void)SD.remove(logical_path.c_str()); + } + file_ = SD.open(logical_path.c_str(), FILE_WRITE); + return static_cast(file_); + } + + bool write(const void* data, std::size_t len) + { + return file_ && static_cast(file_.write(static_cast(data), len)) == len; + } + + void close() + { + if (file_) + { + file_.close(); + } + } + + private: + File file_{}; +}; + +#else + +std::string host_path(const std::string& logical_path) +{ + return std::string(platform::esp::idf_common::bsp_runtime::sdcard_mount_point()) + logical_path; +} + +bool ensure_dir_recursive(const std::string& logical_dir) +{ + if (logical_dir.empty()) + { + return false; + } + + std::string host_dir = host_path(logical_dir); + std::string current; + for (char ch : host_dir) + { + current.push_back(ch); + if (ch != '/') + { + continue; + } + if (current.size() <= 1) + { + continue; + } + if (::mkdir(current.c_str(), 0775) != 0 && errno != EEXIST) + { + return false; + } + } + + return ::mkdir(host_dir.c_str(), 0775) == 0 || errno == EEXIST; +} + +bool logical_file_exists(const std::string& logical_path) +{ + struct stat st + { + }; + return ::stat(host_path(logical_path).c_str(), &st) == 0 && S_ISREG(st.st_mode); +} + +bool write_binary_file(const std::string& logical_path, const void* data, std::size_t len) +{ + const std::size_t slash = logical_path.find_last_of('/'); + if (slash != std::string::npos) + { + if (!ensure_dir_recursive(logical_path.substr(0, slash))) + { + return false; + } + } + + FILE* file = std::fopen(host_path(logical_path).c_str(), "wb"); + if (!file) + { + return false; + } + const std::size_t written = std::fwrite(data, 1, len, file); + std::fclose(file); + return written == len; +} + +bool write_text_file(const std::string& logical_path, const std::string& text) +{ + return write_binary_file(logical_path, text.data(), text.size()); +} + +bool read_binary_file(const std::string& logical_path, std::vector& out) +{ + out.clear(); + FILE* file = std::fopen(host_path(logical_path).c_str(), "rb"); + if (!file) + { + return false; + } + + if (std::fseek(file, 0, SEEK_END) != 0) + { + std::fclose(file); + return false; + } + const long file_size = std::ftell(file); + if (file_size < 0) + { + std::fclose(file); + return false; + } + if (std::fseek(file, 0, SEEK_SET) != 0) + { + std::fclose(file); + return false; + } + + out.resize(static_cast(file_size)); + const std::size_t read = std::fread(out.data(), 1, out.size(), file); + std::fclose(file); + if (read != out.size()) + { + out.clear(); + return false; + } + return true; +} + +bool read_text_file(const std::string& logical_path, std::string& out) +{ + std::vector bytes; + if (!read_binary_file(logical_path, bytes)) + { + out.clear(); + return false; + } + out.assign(reinterpret_cast(bytes.data()), bytes.size()); + return true; +} + +bool remove_file_if_exists(const std::string& logical_path) +{ + if (!logical_file_exists(logical_path)) + { + return true; + } + return std::remove(host_path(logical_path).c_str()) == 0; +} + +class RandomAccessFile +{ + public: + bool open(const std::string& logical_path) + { + file_ = std::fopen(host_path(logical_path).c_str(), "rb"); + if (!file_) + { + return false; + } + if (std::fseek(file_, 0, SEEK_END) != 0) + { + std::fclose(file_); + file_ = nullptr; + return false; + } + const long len = std::ftell(file_); + if (len < 0) + { + std::fclose(file_); + file_ = nullptr; + return false; + } + size_ = static_cast(len); + return std::fseek(file_, 0, SEEK_SET) == 0; + } + + void close() + { + if (file_) + { + std::fclose(file_); + file_ = nullptr; + size_ = 0; + } + } + + std::size_t size() const + { + return size_; + } + + bool read_at(std::size_t offset, void* out, std::size_t len) + { + if (!file_ || std::fseek(file_, static_cast(offset), SEEK_SET) != 0) + { + return false; + } + return std::fread(out, 1, len, file_) == len; + } + + private: + FILE* file_ = nullptr; + std::size_t size_ = 0; +}; + +class SequentialWriteFile +{ + public: + bool open(const std::string& logical_path) + { + const std::size_t slash = logical_path.find_last_of('/'); + if (slash != std::string::npos) + { + if (!ensure_dir_recursive(logical_path.substr(0, slash))) + { + return false; + } + } + file_ = std::fopen(host_path(logical_path).c_str(), "wb"); + return file_ != nullptr; + } + + bool write(const void* data, std::size_t len) + { + return file_ && std::fwrite(data, 1, len, file_) == len; + } + + void close() + { + if (file_) + { + std::fclose(file_); + file_ = nullptr; + } + } + + private: + FILE* file_ = nullptr; +}; + +#endif + +std::uint16_t read_le16(const std::uint8_t* ptr) +{ + return static_cast(ptr[0]) | + (static_cast(ptr[1]) << 8); +} + +std::uint32_t read_le32(const std::uint8_t* ptr) +{ + return static_cast(ptr[0]) | + (static_cast(ptr[1]) << 8) | + (static_cast(ptr[2]) << 16) | + (static_cast(ptr[3]) << 24); +} + +void configure_http_client(esp_http_client_config_t& config, const std::string& url) +{ + (void)ensure_tls_allocator_configured(); + config = esp_http_client_config_t{}; + config.url = url.c_str(); + config.method = HTTP_METHOD_GET; + config.timeout_ms = 30000; + config.disable_auto_redirect = false; + config.buffer_size = kHttpBufferSize; + config.buffer_size_tx = kHttpTxBufferSize; +#if UI_PACKS_HAVE_CRT_BUNDLE + config.crt_bundle_attach = esp_crt_bundle_attach; +#endif +} + +bool http_get_text(const std::string& url, std::string& out, std::string& out_error) +{ + out.clear(); + out_error.clear(); + + esp_http_client_config_t config{}; + configure_http_client(config, url); + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (client == nullptr) + { + out_error = "Create HTTP client failed"; + return false; + } + + bool ok = false; + if (esp_http_client_open(client, 0) == ESP_OK) + { + if (esp_http_client_fetch_headers(client) >= 0) + { + const int status_code = esp_http_client_get_status_code(client); + if (status_code < 200 || status_code >= 300) + { + char buffer[64]; + std::snprintf(buffer, sizeof(buffer), "Catalog HTTP %d", status_code); + out_error = buffer; + } + else + { + char buffer[kHttpBufferSize]; + while (true) + { + const int read = esp_http_client_read(client, buffer, sizeof(buffer)); + if (read < 0) + { + out_error = "Read catalog failed"; + break; + } + if (read == 0) + { + ok = true; + break; + } + out.append(buffer, static_cast(read)); + } + } + } + else + { + out_error = "Fetch catalog headers failed"; + } + } + else + { + log_pack_memory_snapshot("catalog open failed"); + out_error = "Open catalog request failed"; + } + + esp_http_client_close(client); + esp_http_client_cleanup(client); + return ok; +} + +bool http_download_file(const std::string& url, + const std::string& logical_path, + std::string& out_error) +{ + out_error.clear(); + + const std::size_t slash = logical_path.find_last_of('/'); + if (slash != std::string::npos) + { + if (!ensure_dir_recursive(logical_path.substr(0, slash))) + { + out_error = "Create temp directory failed"; + return false; + } + } + + SequentialWriteFile file; + if (!file.open(logical_path)) + { + out_error = "Open destination file failed"; + return false; + } + + esp_http_client_config_t config{}; + configure_http_client(config, url); + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (client == nullptr) + { + file.close(); + out_error = "Create download client failed"; + return false; + } + + bool ok = false; + if (esp_http_client_open(client, 0) == ESP_OK) + { + if (esp_http_client_fetch_headers(client) >= 0) + { + const int status_code = esp_http_client_get_status_code(client); + if (status_code < 200 || status_code >= 300) + { + char buffer[64]; + std::snprintf(buffer, sizeof(buffer), "Download HTTP %d", status_code); + out_error = buffer; + } + else + { + std::uint8_t buffer[kHttpBufferSize]; + while (true) + { + const int read = esp_http_client_read(client, + reinterpret_cast(buffer), + sizeof(buffer)); + if (read < 0) + { + out_error = "Download read failed"; + break; + } + if (read == 0) + { + ok = true; + break; + } + if (!file.write(buffer, static_cast(read))) + { + out_error = "Write download file failed"; + break; + } + } + } + } + else + { + out_error = "Fetch download headers failed"; + } + } + else + { + log_pack_memory_snapshot("download open failed"); + out_error = "Open download request failed"; + } + + file.close(); + esp_http_client_close(client); + esp_http_client_cleanup(client); + + if (!ok) + { + (void)remove_file_if_exists(logical_path); + } + return ok; +} + +std::string sha256_hex_of_bytes(const std::uint8_t* data, std::size_t len) +{ + unsigned char hash[32]; + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts_ret(&ctx, 0); + mbedtls_sha256_update_ret(&ctx, data, len); + mbedtls_sha256_finish_ret(&ctx, hash); + mbedtls_sha256_free(&ctx); + + char hex[65]; + for (int i = 0; i < 32; ++i) + { + std::snprintf(hex + (i * 2), 3, "%02x", hash[i]); + } + hex[64] = '\0'; + return hex; +} + +bool sha256_file(const std::string& logical_path, std::string& out_hex) +{ + std::vector data; + if (!read_binary_file(logical_path, data)) + { + out_hex.clear(); + return false; + } + out_hex = sha256_hex_of_bytes(data.data(), data.size()); + return true; +} + +cJSON* parse_json_document(const std::string& text) +{ + return cJSON_ParseWithLength(text.c_str(), text.size()); +} + +std::string json_string(cJSON* object, const char* key) +{ + if (!object || !key) + { + return {}; + } + cJSON* item = cJSON_GetObjectItemCaseSensitive(object, key); + return cJSON_IsString(item) && item->valuestring ? item->valuestring : ""; +} + +std::size_t json_size_t(cJSON* object, const char* key) +{ + if (!object || !key) + { + return 0; + } + cJSON* item = cJSON_GetObjectItemCaseSensitive(object, key); + if (!cJSON_IsNumber(item)) + { + return 0; + } + return static_cast(item->valuedouble); +} + +std::uint64_t json_u64(cJSON* object, const char* key) +{ + if (!object || !key) + { + return 0; + } + cJSON* item = cJSON_GetObjectItemCaseSensitive(object, key); + if (!cJSON_IsNumber(item)) + { + return 0; + } + return static_cast(item->valuedouble); +} + +std::vector json_string_array(cJSON* object, const char* key) +{ + std::vector out; + if (!object || !key) + { + return out; + } + + cJSON* array = cJSON_GetObjectItemCaseSensitive(object, key); + if (!cJSON_IsArray(array)) + { + return out; + } + + cJSON* item = nullptr; + cJSON_ArrayForEach(item, array) + { + if (cJSON_IsString(item) && item->valuestring) + { + out.emplace_back(item->valuestring); + } + } + return out; +} + +void collect_provided_ids(cJSON* provides, + const char* group_key, + const char* id_key, + std::vector& out) +{ + out.clear(); + if (!provides || !group_key || !id_key) + { + return; + } + + cJSON* array = cJSON_GetObjectItemCaseSensitive(provides, group_key); + if (!cJSON_IsArray(array)) + { + return; + } + + cJSON* item = nullptr; + cJSON_ArrayForEach(item, array) + { + const std::string id = json_string(item, id_key); + if (!id.empty()) + { + out.push_back(id); + } + } +} + +bool compatible_with_profile(const PackageRecord& package) +{ + if (package.supported_memory_profiles.empty()) + { + return true; + } + const std::string current = lowercase_ascii(active_memory_profile_name()); + for (const std::string& profile : package.supported_memory_profiles) + { + if (lowercase_ascii(profile) == current) + { + return true; + } + } + return false; +} + +bool compatible_with_firmware(const PackageRecord& package) +{ + if (package.min_firmware_version.empty()) + { + return true; + } + return compare_versions(platform::ui::device::firmware_version(), + package.min_firmware_version) >= 0; +} + +bool load_installed_index(InstalledIndex& out_index, std::string& out_error) +{ + out_index = InstalledIndex{}; + out_error.clear(); + + if (!logical_file_exists(kInstalledIndexPath)) + { + return true; + } + + std::string text; + if (!read_text_file(kInstalledIndexPath, text)) + { + out_error = "Read installed index failed"; + return false; + } + + cJSON* root = parse_json_document(text); + if (!root) + { + out_error = "Parse installed index failed"; + return false; + } + + cJSON* packages = cJSON_GetObjectItemCaseSensitive(root, "packages"); + if (cJSON_IsArray(packages)) + { + cJSON* item = nullptr; + cJSON_ArrayForEach(item, packages) + { + InstalledPackageRecord record{}; + record.id = json_string(item, "id"); + record.version = json_string(item, "version"); + record.archive_sha256 = json_string(item, "archive_sha256"); + record.installed_at_epoch = json_u64(item, "installed_at_epoch"); + if (!record.id.empty()) + { + out_index.packages.push_back(std::move(record)); + } + } + } + + cJSON_Delete(root); + return true; +} + +bool save_installed_index(const InstalledIndex& index, std::string& out_error) +{ + out_error.clear(); + if (!ensure_dir_recursive(kIndexDir)) + { + out_error = "Create index directory failed"; + return false; + } + + cJSON* root = cJSON_CreateObject(); + cJSON_AddNumberToObject(root, "schema_version", 1); + cJSON* packages = cJSON_AddArrayToObject(root, "packages"); + for (const InstalledPackageRecord& record : index.packages) + { + cJSON* item = cJSON_CreateObject(); + cJSON_AddStringToObject(item, "id", record.id.c_str()); + cJSON_AddStringToObject(item, "version", record.version.c_str()); + cJSON_AddStringToObject(item, "archive_sha256", record.archive_sha256.c_str()); + cJSON_AddNumberToObject(item, + "installed_at_epoch", + static_cast(record.installed_at_epoch)); + cJSON_AddItemToArray(packages, item); + } + + char* text = cJSON_Print(root); + cJSON_Delete(root); + if (!text) + { + out_error = "Serialize installed index failed"; + return false; + } + + std::string output(text); + cJSON_free(text); + if (!write_text_file(kInstalledIndexPath, output)) + { + out_error = "Write installed index failed"; + return false; + } + return true; +} + +bool read_zip_tail(RandomAccessFile& file, + std::vector& out_tail) +{ + const std::size_t size = file.size(); + if (size < kZipEocdMinSize) + { + return false; + } + + const std::size_t tail_size = std::min(size, kZipTailSearchMax); + out_tail.resize(tail_size); + return file.read_at(size - tail_size, out_tail.data(), tail_size); +} + +struct ZipEntry +{ + std::string name; + std::uint16_t method = 0; + std::uint32_t compressed_size = 0; + std::uint32_t uncompressed_size = 0; + std::uint32_t local_header_offset = 0; +}; + +bool enumerate_zip_entries(RandomAccessFile& file, + std::vector& out_entries, + std::string& out_error) +{ + out_entries.clear(); + out_error.clear(); + + std::vector tail; + if (!read_zip_tail(file, tail)) + { + out_error = "Read zip tail failed"; + return false; + } + + std::size_t eocd_offset = std::string::npos; + for (std::size_t i = tail.size() - kZipEocdMinSize + 1; i > 0; --i) + { + const std::size_t pos = i - 1; + if (read_le32(&tail[pos]) == kZipEocdSignature) + { + eocd_offset = pos; + break; + } + } + if (eocd_offset == std::string::npos) + { + out_error = "Zip EOCD not found"; + return false; + } + + const std::uint16_t entry_count = read_le16(&tail[eocd_offset + 10]); + const std::uint32_t central_size = read_le32(&tail[eocd_offset + 12]); + const std::uint32_t central_offset = read_le32(&tail[eocd_offset + 16]); + + std::vector central(central_size); + if (!file.read_at(central_offset, central.data(), central.size())) + { + out_error = "Read zip central directory failed"; + return false; + } + + std::size_t offset = 0; + for (std::uint16_t entry_index = 0; entry_index < entry_count; ++entry_index) + { + if (offset + 46 > central.size() || read_le32(¢ral[offset]) != kZipCentralSignature) + { + out_error = "Zip central entry invalid"; + return false; + } + + const std::uint16_t method = read_le16(¢ral[offset + 10]); + const std::uint32_t compressed_size = read_le32(¢ral[offset + 20]); + const std::uint32_t uncompressed_size = read_le32(¢ral[offset + 24]); + const std::uint16_t name_len = read_le16(¢ral[offset + 28]); + const std::uint16_t extra_len = read_le16(¢ral[offset + 30]); + const std::uint16_t comment_len = read_le16(¢ral[offset + 32]); + const std::uint32_t local_offset = read_le32(¢ral[offset + 42]); + if (offset + 46 + name_len + extra_len + comment_len > central.size()) + { + out_error = "Zip central entry truncated"; + return false; + } + + ZipEntry entry{}; + entry.name.assign(reinterpret_cast(¢ral[offset + 46]), name_len); + entry.method = method; + entry.compressed_size = compressed_size; + entry.uncompressed_size = uncompressed_size; + entry.local_header_offset = local_offset; + out_entries.push_back(std::move(entry)); + + offset += 46 + name_len + extra_len + comment_len; + } + + return true; +} + +bool extract_zip_payload(const std::string& logical_zip_path, std::string& out_error) +{ + out_error.clear(); + + RandomAccessFile file; + if (!file.open(logical_zip_path)) + { + out_error = "Open zip file failed"; + return false; + } + + std::vector entries; + if (!enumerate_zip_entries(file, entries, out_error)) + { + file.close(); + return false; + } + + for (const ZipEntry& entry : entries) + { + if (!starts_with(entry.name, "payload/")) + { + continue; + } + + const std::string logical_target = std::string(kPackRoot) + "/" + entry.name.substr(8); + if (!entry.name.empty() && entry.name.back() == '/') + { + if (!ensure_dir_recursive(logical_target)) + { + out_error = "Create payload directory failed"; + file.close(); + return false; + } + continue; + } + + std::uint8_t local_header[30]; + if (!file.read_at(entry.local_header_offset, local_header, sizeof(local_header)) || + read_le32(local_header) != kZipLocalSignature) + { + out_error = "Read zip local header failed"; + file.close(); + return false; + } + + const std::uint16_t name_len = read_le16(&local_header[26]); + const std::uint16_t extra_len = read_le16(&local_header[28]); + const std::size_t data_offset = + static_cast(entry.local_header_offset) + 30U + name_len + extra_len; + + std::vector compressed(entry.compressed_size); + if (!compressed.empty() && + !file.read_at(data_offset, compressed.data(), compressed.size())) + { + out_error = "Read zip entry data failed"; + file.close(); + return false; + } + + std::vector output; + if (entry.method == 0) + { + output = std::move(compressed); + } + else if (entry.method == 8) + { + output.resize(entry.uncompressed_size); + const std::size_t actual = + tinfl_decompress_mem_to_mem(output.data(), + output.size(), + compressed.data(), + compressed.size(), + 0); + if (actual == TINFL_DECOMPRESS_MEM_TO_MEM_FAILED || actual != output.size()) + { + out_error = "Inflate zip entry failed"; + file.close(); + return false; + } + } + else + { + out_error = "Unsupported zip compression"; + file.close(); + return false; + } + + if (!write_binary_file(logical_target, output.data(), output.size())) + { + out_error = "Write extracted payload failed"; + file.close(); + return false; + } + } + + file.close(); + return true; +} + +void merge_installed_state(const std::vector& installed, + std::vector& packages) +{ + for (PackageRecord& package : packages) + { + for (const InstalledPackageRecord& record : installed) + { + if (record.id != package.id) + { + continue; + } + package.installed = true; + package.installed_record = record; + package.update_available = + record.version != package.version || + lowercase_ascii(record.archive_sha256) != lowercase_ascii(package.archive_sha256); + break; + } + } +} + +} // namespace + +bool is_supported() +{ + return platform::ui::device::card_ready(); +} + +bool load_installed_packages(std::vector& out_installed, std::string& out_error) +{ + InstalledIndex index; + if (!load_installed_index(index, out_error)) + { + out_installed.clear(); + return false; + } + out_installed = index.packages; + return true; +} + +bool fetch_catalog(std::vector& out_packages, std::string& out_error) +{ + out_packages.clear(); + out_error.clear(); + + const platform::ui::wifi::Status wifi_status = platform::ui::wifi::status(); + if (!wifi_status.supported) + { + out_error = "Wi-Fi unsupported"; + return false; + } + if (!wifi_status.connected) + { + out_error = "Connect Wi-Fi in Settings first"; + return false; + } + + std::string text; + if (!http_get_text(kCatalogUrl, text, out_error)) + { + return false; + } + + cJSON* root = parse_json_document(text); + if (!root) + { + out_error = "Parse remote catalog failed"; + return false; + } + + cJSON* packages = cJSON_GetObjectItemCaseSensitive(root, "packages"); + if (!cJSON_IsArray(packages)) + { + cJSON_Delete(root); + out_error = "Remote catalog packages missing"; + return false; + } + + cJSON* item = nullptr; + cJSON_ArrayForEach(item, packages) + { + PackageRecord package{}; + package.id = json_string(item, "id"); + package.package_type = json_string(item, "package_type"); + package.version = json_string(item, "version"); + package.display_name = json_string(item, "display_name"); + package.summary = json_string(item, "summary"); + package.description = json_string(item, "description"); + package.author = json_string(item, "author"); + package.homepage = json_string(item, "homepage"); + package.min_firmware_version = json_string(item, "min_firmware_version"); + package.supported_memory_profiles = json_string_array(item, "supported_memory_profiles"); + package.tags = json_string_array(item, "tags"); + + cJSON* runtime = cJSON_GetObjectItemCaseSensitive(item, "runtime"); + package.estimated_unique_font_ram_bytes = + json_size_t(runtime, "estimated_unique_font_ram_bytes"); + + cJSON* archive = cJSON_GetObjectItemCaseSensitive(item, "archive"); + package.archive_path = json_string(archive, "path"); + package.archive_size_bytes = json_size_t(archive, "size_bytes"); + package.archive_sha256 = json_string(archive, "sha256"); + package.download_url = join_url(kCatalogBaseUrl, package.archive_path); + + cJSON* provides = cJSON_GetObjectItemCaseSensitive(item, "provides"); + collect_provided_ids(provides, "locales", "id", package.provided_locale_ids); + collect_provided_ids(provides, "fonts", "id", package.provided_font_ids); + collect_provided_ids(provides, "ime", "id", package.provided_ime_ids); + + package.compatible_memory_profile = compatible_with_profile(package); + package.compatible_firmware = compatible_with_firmware(package); + + if (!package.id.empty()) + { + out_packages.push_back(std::move(package)); + } + } + + cJSON_Delete(root); + + std::vector installed; + std::string installed_error; + if (load_installed_packages(installed, installed_error)) + { + merge_installed_state(installed, out_packages); + } + + std::sort(out_packages.begin(), + out_packages.end(), + [](const PackageRecord& lhs, const PackageRecord& rhs) + { + return lowercase_ascii(lhs.display_name) < lowercase_ascii(rhs.display_name); + }); + return true; +} + +bool install_package(const PackageRecord& package, std::string& out_error) +{ + out_error.clear(); + + if (package.id.empty() || package.download_url.empty()) + { + out_error = "Package metadata is incomplete"; + return false; + } + + const platform::ui::wifi::Status wifi_status = platform::ui::wifi::status(); + if (!wifi_status.connected) + { + out_error = "Wi-Fi is not connected"; + return false; + } + if (!platform::ui::device::card_ready()) + { + out_error = "SD card is not ready"; + return false; + } + + if (!ensure_dir_recursive(kPackRoot) || + !ensure_dir_recursive(kIndexDir) || + !ensure_dir_recursive(kTempDir)) + { + out_error = "Create pack directories failed"; + return false; + } + + const std::string temp_zip_path = std::string(kTempDir) + "/" + package.id + "-" + package.version + ".zip"; + if (!http_download_file(package.download_url, temp_zip_path, out_error)) + { + return false; + } + + std::string archive_sha256; + if (!sha256_file(temp_zip_path, archive_sha256)) + { + (void)remove_file_if_exists(temp_zip_path); + out_error = "Hash downloaded archive failed"; + return false; + } + if (!package.archive_sha256.empty() && + lowercase_ascii(archive_sha256) != lowercase_ascii(package.archive_sha256)) + { + (void)remove_file_if_exists(temp_zip_path); + out_error = "Archive SHA256 mismatch"; + return false; + } + + if (!extract_zip_payload(temp_zip_path, out_error)) + { + (void)remove_file_if_exists(temp_zip_path); + return false; + } + + InstalledIndex index; + std::string index_error; + if (!load_installed_index(index, index_error)) + { + (void)remove_file_if_exists(temp_zip_path); + out_error = index_error; + return false; + } + + bool updated = false; + for (InstalledPackageRecord& record : index.packages) + { + if (record.id != package.id) + { + continue; + } + record.version = package.version; + record.archive_sha256 = archive_sha256; + record.installed_at_epoch = static_cast(std::time(nullptr)); + updated = true; + break; + } + + if (!updated) + { + InstalledPackageRecord record{}; + record.id = package.id; + record.version = package.version; + record.archive_sha256 = archive_sha256; + record.installed_at_epoch = static_cast(std::time(nullptr)); + index.packages.push_back(std::move(record)); + } + + if (!save_installed_index(index, out_error)) + { + (void)remove_file_if_exists(temp_zip_path); + return false; + } + + (void)remove_file_if_exists(temp_zip_path); + ::ui::i18n::reload_language(); + return true; +} + +} // namespace ui::runtime::packs + +#else + +namespace ui::runtime::packs +{ + +bool is_supported() +{ + return false; +} + +bool load_installed_packages(std::vector& out_installed, std::string& out_error) +{ + out_installed.clear(); + out_error = "Pack installation is unsupported on this platform"; + return false; +} + +bool fetch_catalog(std::vector& out_packages, std::string& out_error) +{ + out_packages.clear(); + out_error = "Pack installation is unsupported on this platform"; + return false; +} + +bool install_package(const PackageRecord& package, std::string& out_error) +{ + (void)package; + out_error = "Pack installation is unsupported on this platform"; + return false; +} + +} // namespace ui::runtime::packs + +#endif diff --git a/modules/ui_shared/src/ui/screens/chat/chat_compose_styles.cpp b/modules/ui_shared/src/ui/screens/chat/chat_compose_styles.cpp index 5efc2620..2a07388e 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_compose_styles.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_compose_styles.cpp @@ -1,6 +1,6 @@ #if !defined(ARDUINO_T_WATCH_S3) #include "ui/screens/chat/chat_compose_styles.h" -#include "ui/assets/fonts/fonts.h" +#include "ui/assets/fonts/font_utils.h" namespace chat::ui::compose::styles { @@ -36,7 +36,7 @@ void init_once() lv_style_set_bg_color(&s_textarea, lv_color_hex(0xFFF7E9)); lv_style_set_bg_opa(&s_textarea, LV_OPA_COVER); lv_style_set_text_color(&s_textarea, lv_color_hex(0x3A2A1A)); - lv_style_set_text_font(&s_textarea, &lv_font_noto_cjk_16_2bpp); + lv_style_set_text_font(&s_textarea, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font())); lv_style_set_border_width(&s_textarea, 1); lv_style_set_border_color(&s_textarea, lv_color_hex(0xD9B06A)); lv_style_set_radius(&s_textarea, 6); diff --git a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp index 663f95a5..cbcee536 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp @@ -520,9 +520,7 @@ void ChatConversationScreen::createMessageItem(const chat::ChatMessage& msg) } } std::string line = sender + " " + time_buf; - ::ui::i18n::set_label_text_raw(item.time_label, line.c_str()); - ::ui::fonts::apply_localized_font( - item.time_label, lv_label_get_text(item.time_label), ::ui::fonts::ui_chrome_font()); + ::ui::i18n::set_content_label_text_raw(item.time_label, line.c_str()); } else { diff --git a/modules/ui_shared/src/ui/screens/chat/chat_conversation_styles.cpp b/modules/ui_shared/src/ui/screens/chat/chat_conversation_styles.cpp index cf38045d..7b5a9378 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_conversation_styles.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_conversation_styles.cpp @@ -1,6 +1,6 @@ #if !defined(ARDUINO_T_WATCH_S3) #include "ui/screens/chat/chat_conversation_styles.h" -#include "ui/assets/fonts/fonts.h" +#include "ui/assets/fonts/font_utils.h" namespace chat::ui::conversation::styles { @@ -81,7 +81,7 @@ void init_once() lv_style_init(&s_reply_label); lv_style_set_text_color(&s_reply_label, lv_color_hex(0x3A2A1A)); - lv_style_set_text_font(&s_reply_label, &lv_font_noto_cjk_16_2bpp); + lv_style_set_text_font(&s_reply_label, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font())); lv_style_init(&s_row); lv_style_set_bg_opa(&s_row, LV_OPA_TRANSP); @@ -114,7 +114,7 @@ void init_once() lv_style_init(&s_bubble_text); lv_style_set_text_color(&s_bubble_text, kTextColor); lv_style_set_text_align(&s_bubble_text, LV_TEXT_ALIGN_LEFT); - lv_style_set_text_font(&s_bubble_text, &lv_font_noto_cjk_16_2bpp); + lv_style_set_text_font(&s_bubble_text, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font())); lv_style_init(&s_bubble_time); lv_style_set_text_color(&s_bubble_time, lv_color_hex(0x6A5646)); diff --git a/modules/ui_shared/src/ui/screens/chat/chat_message_list_styles.cpp b/modules/ui_shared/src/ui/screens/chat/chat_message_list_styles.cpp index 4464ea13..8740794c 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_message_list_styles.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_message_list_styles.cpp @@ -1,7 +1,6 @@ #if !defined(ARDUINO_T_WATCH_S3) #include "ui/screens/chat/chat_message_list_styles.h" #include "ui/assets/fonts/font_utils.h" -#include "ui/assets/fonts/fonts.h" #include "ui/components/info_card.h" #include "ui/components/two_pane_styles.h" @@ -31,7 +30,7 @@ void init_once() lv_style_set_radius(&s_root, 0); lv_style_init(&s_label_font); - lv_style_set_text_font(&s_label_font, &lv_font_noto_cjk_16_2bpp); + lv_style_set_text_font(&s_label_font, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font())); } void apply_root_container(lv_obj_t* obj) diff --git a/modules/ui_shared/src/ui/screens/chat/chat_protocol_support.cpp b/modules/ui_shared/src/ui/screens/chat/chat_protocol_support.cpp new file mode 100644 index 00000000..a53f738f --- /dev/null +++ b/modules/ui_shared/src/ui/screens/chat/chat_protocol_support.cpp @@ -0,0 +1,44 @@ +#include "ui/screens/chat/chat_protocol_support.h" + +#include "app/app_config.h" +#include "app/app_facade_access.h" + +namespace chat::ui::support +{ + +chat::MeshProtocol active_mesh_protocol() +{ + return app::configFacade().getConfig().mesh_protocol; +} + +chat::MeshCapabilities active_mesh_capabilities() +{ + chat::IMeshAdapter* adapter = app::messagingFacade().getMeshAdapter(); + return adapter ? adapter->getCapabilities() : chat::MeshCapabilities{}; +} + +bool supports_local_text_chat() +{ + return active_mesh_capabilities().supports_unicast_text; +} + +bool supports_team_chat() +{ + return active_mesh_capabilities().supports_unicast_appdata; +} + +const char* local_text_chat_unavailable_message() +{ + return (active_mesh_protocol() == chat::MeshProtocol::RNode) + ? "RNode text chat runs on host" + : "Text chat unavailable"; +} + +const char* team_chat_unavailable_message() +{ + return (active_mesh_protocol() == chat::MeshProtocol::RNode) + ? "Team chat unavailable in RNode mode" + : "Team chat unavailable"; +} + +} // namespace chat::ui::support diff --git a/modules/ui_shared/src/ui/screens/chat/chat_send_flow.cpp b/modules/ui_shared/src/ui/screens/chat/chat_send_flow.cpp new file mode 100644 index 00000000..e2970295 --- /dev/null +++ b/modules/ui_shared/src/ui/screens/chat/chat_send_flow.cpp @@ -0,0 +1,26 @@ +#include "ui/screens/chat/chat_send_flow.h" + +#include "chat/usecase/chat_service.h" +#include "ui/screens/chat/chat_compose_components.h" + +namespace chat::ui::send_flow +{ + +bool begin_local_text_send(ChatComposeScreen* compose, + chat::ChatService* service, + const chat::ConversationId& conv, + const std::string& text, + void (*done_cb)(bool ok, bool timeout, void*), + void* user_data) +{ + if (!compose || !service || text.empty()) + { + return false; + } + + const chat::MessageId msg_id = service->sendText(conv.channel, text, conv.peer); + compose->beginSend(service, msg_id, done_cb, user_data); + return true; +} + +} // namespace chat::ui::send_flow diff --git a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp index 2c648656..8e50814d 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp @@ -15,9 +15,11 @@ #include "sys/event_bus.h" #include "team/protocol/team_location_marker.h" #include "team/usecase/team_controller.h" -#include "ui/assets/fonts/fonts.h" +#include "ui/assets/fonts/font_utils.h" #include "ui/localization.h" #include "ui/page/page_profile.h" +#include "ui/screens/chat/chat_protocol_support.h" +#include "ui/screens/chat/chat_send_flow.h" #include "ui/screens/team/team_ui_store.h" #include "ui/ui_common.h" #include "ui/widgets/ime/ime_widget.h" @@ -54,6 +56,8 @@ namespace ui namespace { +namespace chat_support = chat::ui::support; + constexpr uint8_t kTeamChatChannelRaw = 2; constexpr chat::ChannelId kTeamChatChannel = static_cast(kTeamChatChannelRaw); @@ -92,11 +96,6 @@ const TeamPositionIconOption* find_team_position_icon_option(uint8_t icon_id) return nullptr; } -chat::MeshProtocol active_mesh_protocol() -{ - return app::configFacade().getConfig().mesh_protocol; -} - const char* protocol_short_label(chat::MeshProtocol protocol) { return chat::infra::meshProtocolShortName(protocol); @@ -132,39 +131,9 @@ std::string base_conversation_name(const chat::ConversationId& conv) return buf; } -chat::MeshCapabilities active_mesh_capabilities() -{ - chat::IMeshAdapter* adapter = app::messagingFacade().getMeshAdapter(); - return adapter ? adapter->getCapabilities() : chat::MeshCapabilities{}; -} - -bool supports_local_text_chat() -{ - return active_mesh_capabilities().supports_unicast_text; -} - -bool supports_team_chat() -{ - return active_mesh_capabilities().supports_unicast_appdata; -} - -const char* local_text_chat_unavailable_message() -{ - return (active_mesh_protocol() == chat::MeshProtocol::RNode) - ? "RNode text chat runs on host" - : "Text chat unavailable"; -} - -const char* team_chat_unavailable_message() -{ - return (active_mesh_protocol() == chat::MeshProtocol::RNode) - ? "Team chat unavailable in RNode mode" - : "Team chat unavailable"; -} - chat::ConversationId teamConversationId() { - return chat::ConversationId(kTeamChatChannel, 0, active_mesh_protocol()); + return chat::ConversationId(kTeamChatChannel, 0, chat_support::active_mesh_protocol()); } bool isTeamConversationId(const chat::ConversationId& conv) @@ -400,7 +369,7 @@ void handle_conversation_back(void* user_data) UiController::UiController(lv_obj_t* parent, chat::ChatService& service, chat::ChannelId initial_channel, ExitRequestCallback exit_request, void* exit_request_user_data) : parent_(parent), service_(service), state_(State::ChannelList), current_channel_(initial_channel), - current_conv_(chat::ConversationId(initial_channel, 0, active_mesh_protocol())), + current_conv_(chat::ConversationId(initial_channel, 0, chat_support::active_mesh_protocol())), exit_request_(exit_request), exit_request_user_data_(exit_request_user_data) { } @@ -668,8 +637,9 @@ void UiController::switchToConversation(chat::ConversationId conv) conversation_->setBackCallback(handle_conversation_back, this); } const bool can_reply = team_conv_active_ - ? supports_team_chat() - : (conv.protocol == active_mesh_protocol() && supports_local_text_chat()); + ? chat_support::supports_team_chat() + : (conv.protocol == chat_support::active_mesh_protocol() && + chat_support::supports_local_text_chat()); conversation_->setReplyEnabled(can_reply); if (team_conv_active_) @@ -735,19 +705,19 @@ void UiController::switchToCompose(chat::ConversationId conv) { closeTeamPositionPicker(true); const bool is_team_conv = isTeamConversation(conv); - if (!is_team_conv && conv.protocol != active_mesh_protocol()) + if (!is_team_conv && conv.protocol != chat_support::active_mesh_protocol()) { ::ui::SystemNotification::show("Conversation protocol mismatch", 2000); return; } - if (!is_team_conv && !supports_local_text_chat()) + if (!is_team_conv && !chat_support::supports_local_text_chat()) { - ::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200); + ::ui::SystemNotification::show(chat_support::local_text_chat_unavailable_message(), 2200); return; } - if (is_team_conv && !supports_team_chat()) + if (is_team_conv && !chat_support::supports_team_chat()) { - ::ui::SystemNotification::show(team_chat_unavailable_message(), 2200); + ::ui::SystemNotification::show(chat_support::team_chat_unavailable_message(), 2200); return; } @@ -870,12 +840,36 @@ void UiController::handleSendMessage(const std::string& text) { return; } - if (!supports_local_text_chat()) + if (!chat_support::supports_local_text_chat()) { - ::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200); + ::ui::SystemNotification::show(chat_support::local_text_chat_unavailable_message(), 2200); return; } - service_.sendText(current_channel_, text, current_conv_.peer); + chat::ui::send_flow::begin_local_text_send(compose_.get(), + &service_, + current_conv_, + text, + UiController::handleComposeSendDoneCallback, + this); +} + +void UiController::handleComposeSendDone(bool ok, bool timeout) +{ + (void)ok; + (void)timeout; + if (state_ == State::Compose) + { + switchToConversation(current_conv_); + } +} + +void UiController::handleComposeSendDoneCallback(bool ok, bool timeout, void* user_data) +{ + auto* controller = static_cast(user_data); + if (controller) + { + controller->handleComposeSendDone(ok, timeout); + } } void UiController::refreshUnreadCounts() @@ -1098,7 +1092,7 @@ void UiController::refreshTeamConversation() for (const auto& entry : entries) { chat::ChatMessage msg; - msg.protocol = active_mesh_protocol(); + msg.protocol = chat_support::active_mesh_protocol(); msg.channel = chat::ChannelId::PRIMARY; msg.peer = 0; msg.from = entry.incoming ? entry.peer_id : 0; @@ -1586,7 +1580,7 @@ void UiController::openKeyVerificationNumberModal(chat::NodeId node_id, uint64_t lv_obj_t* title = lv_label_create(key_verify_panel_); ::ui::i18n::set_label_text(title, "Key Verification"); lv_obj_set_style_text_color(title, lv_color_hex(0x6B4A1E), 0); - lv_obj_set_style_text_font(title, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font(title, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 0); std::string desc = ::ui::i18n::format("Enter number for %s", resolve_contact_name(node_id).c_str()); @@ -1955,19 +1949,19 @@ void UiController::handleConversationAction(ChatConversationScreen::ActionIntent { if (intent == ChatConversationScreen::ActionIntent::Reply) { - if (!team_conv_active_ && current_conv_.protocol != active_mesh_protocol()) + if (!team_conv_active_ && current_conv_.protocol != chat_support::active_mesh_protocol()) { ::ui::SystemNotification::show("Reply disabled for this protocol", 2000); return; } - if (!team_conv_active_ && !supports_local_text_chat()) + if (!team_conv_active_ && !chat_support::supports_local_text_chat()) { - ::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200); + ::ui::SystemNotification::show(chat_support::local_text_chat_unavailable_message(), 2200); return; } - if (team_conv_active_ && !supports_team_chat()) + if (team_conv_active_ && !chat_support::supports_team_chat()) { - ::ui::SystemNotification::show(team_chat_unavailable_message(), 2200); + ::ui::SystemNotification::show(chat_support::team_chat_unavailable_message(), 2200); return; } switchToCompose(current_conv_); @@ -2078,6 +2072,7 @@ void UiController::handleComposeAction(ChatComposeScreen::ActionIntent intent) if (!text.empty()) { handleSendMessage(text); + return; } } switchToConversation(current_conv_); diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp index 87e7cff7..73a9eb67 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp @@ -21,6 +21,8 @@ #include "ui/screens/chat/chat_compose_components.h" #include "ui/screens/chat/chat_conversation_components.h" #include "ui/screens/chat/chat_page_shell.h" +#include "ui/screens/chat/chat_protocol_support.h" +#include "ui/screens/chat/chat_send_flow.h" #include "ui/screens/contacts/contacts_page_input.h" #include "ui/screens/contacts/contacts_page_layout.h" #include "ui/screens/contacts/contacts_page_styles.h" @@ -47,6 +49,7 @@ #endif using namespace contacts::ui; +namespace chat_support = chat::ui::support; static constexpr int kItemsPerPage = 4; static constexpr int kButtonHeight = 28; @@ -295,11 +298,6 @@ static std::string format_time_status(uint32_t last_seen) return ::ui::i18n::format("SNR %.0f", snr); } -static chat::MeshProtocol active_mesh_protocol() -{ - return app::appFacade().getConfig().mesh_protocol; -} - static const char* mesh_protocol_short_label(chat::MeshProtocol protocol) { return chat::infra::meshProtocolShortName(protocol); @@ -378,36 +376,6 @@ static constexpr DiscoveryActionSpec kDiscoveryActionSpecs[] = { {"Cancel", "Back", DiscoveryActionCommand::Cancel}, }; -static chat::MeshCapabilities active_mesh_capabilities() -{ - chat::IMeshAdapter* adapter = app::messagingFacade().getMeshAdapter(); - return adapter ? adapter->getCapabilities() : chat::MeshCapabilities{}; -} - -static bool supports_local_text_chat() -{ - return active_mesh_capabilities().supports_unicast_text; -} - -static bool supports_team_chat() -{ - return active_mesh_capabilities().supports_unicast_appdata; -} - -static const char* local_text_chat_unavailable_message() -{ - return (active_mesh_protocol() == chat::MeshProtocol::RNode) - ? "RNode text chat runs on host" - : "Text chat unavailable"; -} - -static const char* team_chat_unavailable_message() -{ - return (active_mesh_protocol() == chat::MeshProtocol::RNode) - ? "Team chat unavailable in RNode mode" - : "Team chat unavailable"; -} - static const char* broadcast_chat_unavailable_message(const BroadcastTargetSpec& spec) { if (spec.protocol == chat::MeshProtocol::Meshtastic) @@ -423,7 +391,7 @@ static const char* broadcast_chat_unavailable_message(const BroadcastTargetSpec& static size_t get_broadcast_target_count() { - switch (active_mesh_protocol()) + switch (chat_support::active_mesh_protocol()) { case chat::MeshProtocol::Meshtastic: return 8U; @@ -443,7 +411,7 @@ static bool get_broadcast_target_spec(int index, BroadcastTargetSpec* out) return false; } - if (active_mesh_protocol() == chat::MeshProtocol::Meshtastic) + if (chat_support::active_mesh_protocol() == chat::MeshProtocol::Meshtastic) { if (index >= 8) { @@ -459,7 +427,7 @@ static bool get_broadcast_target_spec(int index, BroadcastTargetSpec* out) return true; } - if (active_mesh_protocol() == chat::MeshProtocol::RNode) + if (chat_support::active_mesh_protocol() == chat::MeshProtocol::RNode) { if (index != 0) { @@ -1300,7 +1268,7 @@ static void open_chat_compose() chat::ChannelId channel = chat::ChannelId::PRIMARY; uint32_t peer_id = 0; - chat::MeshProtocol protocol = active_mesh_protocol(); + chat::MeshProtocol protocol = chat_support::active_mesh_protocol(); std::string title; if (g_contacts_state.current_mode == ContactsMode::Broadcast) { @@ -1322,9 +1290,9 @@ static void open_chat_compose() } else if (g_contacts_state.current_mode == ContactsMode::Team) { - if (!supports_team_chat()) + if (!chat_support::supports_team_chat()) { - ::ui::SystemNotification::show(team_chat_unavailable_message(), 2200); + ::ui::SystemNotification::show(chat_support::team_chat_unavailable_message(), 2200); return; } channel = chat::ChannelId::PRIMARY; @@ -1335,9 +1303,9 @@ static void open_chat_compose() } else { - if (!supports_local_text_chat()) + if (!chat_support::supports_local_text_chat()) { - ::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200); + ::ui::SystemNotification::show(chat_support::local_text_chat_unavailable_message(), 2200); return; } channel = chat::ChannelId::PRIMARY; @@ -1621,16 +1589,16 @@ static void on_compose_action(chat::ui::ChatComposeScreen::ActionIntent intent, return; } - if (s_compose_protocol != active_mesh_protocol()) + if (s_compose_protocol != chat_support::active_mesh_protocol()) { ::ui::SystemNotification::show("Conversation protocol mismatch", 2000); close_chat_compose(); return; } - if (!supports_local_text_chat()) + if (!chat_support::supports_local_text_chat()) { - ::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200); + ::ui::SystemNotification::show(chat_support::local_text_chat_unavailable_message(), 2200); close_chat_compose(); return; } @@ -1642,13 +1610,13 @@ static void on_compose_action(chat::ui::ChatComposeScreen::ActionIntent intent, { s_last_sent_text = text; s_last_sent_ts = sys::epoch_seconds_now(); - chat::MessageId msg_id = g_contacts_state.chat_service->sendText( - s_compose_channel, text, s_compose_peer_id); - g_contacts_state.compose_screen->beginSend( - g_contacts_state.chat_service, - msg_id, - on_compose_send_done, - nullptr); + const chat::ConversationId conv(s_compose_channel, s_compose_peer_id, s_compose_protocol); + chat::ui::send_flow::begin_local_text_send(g_contacts_state.compose_screen, + g_contacts_state.chat_service, + conv, + text, + on_compose_send_done, + nullptr); return; } } @@ -2051,7 +2019,7 @@ static void execute_discovery_command(uint8_t command_index) return; } - if (active_mesh_protocol() != chat::MeshProtocol::MeshCore || !g_contacts_state.chat_service) + if (chat_support::active_mesh_protocol() != chat::MeshProtocol::MeshCore || !g_contacts_state.chat_service) { ::ui::SystemNotification::show("MeshCore only", 2000); return; @@ -2260,7 +2228,9 @@ static void open_action_menu_modal() g_contacts_state.current_mode == ContactsMode::Nearby); const bool allow_chat_action = - (g_contacts_state.current_mode == ContactsMode::Team) ? supports_team_chat() : supports_local_text_chat(); + (g_contacts_state.current_mode == ContactsMode::Team) + ? chat_support::supports_team_chat() + : chat_support::supports_local_text_chat(); int action_count = allow_chat_action ? 2 : 1; // Chat + Cancel if (g_contacts_state.current_mode == ContactsMode::Contacts) { @@ -2434,8 +2404,8 @@ void refresh_ui() } ::ui::components::air_status_footer::refresh(g_contacts_state.air_status_footer); - bool team_available = is_team_available() && supports_team_chat(); - const bool meshcore_mode = (active_mesh_protocol() == chat::MeshProtocol::MeshCore); + bool team_available = is_team_available() && chat_support::supports_team_chat(); + const bool meshcore_mode = (chat_support::active_mesh_protocol() == chat::MeshProtocol::MeshCore); if (g_contacts_state.team_btn) { if (team_available) diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp index 7639872a..8849fd15 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_layout.cpp @@ -233,7 +233,7 @@ lv_obj_t* create_list_item(lv_obj_t* parent, if (::ui::components::info_card::use_tdeck_layout()) { const auto slots = ::ui::components::info_card::create_content(item); - ::ui::i18n::set_label_text_raw(slots.header_main_label, display_name.c_str()); + ::ui::i18n::set_content_label_text_raw(slots.header_main_label, display_name.c_str()); style::apply_label_primary(slots.header_main_label); ::ui::i18n::set_label_text(slots.body_main_label, status_text); @@ -242,7 +242,7 @@ lv_obj_t* create_list_item(lv_obj_t* parent, else { lv_obj_t* name_label = lv_label_create(item); - ::ui::i18n::set_label_text_raw(name_label, display_name.c_str()); + ::ui::i18n::set_content_label_text_raw(name_label, display_name.c_str()); lv_obj_align(name_label, LV_ALIGN_LEFT_MID, 10, 0); style::apply_label_primary(name_label); diff --git a/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp b/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp new file mode 100644 index 00000000..8fd0e2c4 --- /dev/null +++ b/modules/ui_shared/src/ui/screens/extensions/extensions_page_runtime.cpp @@ -0,0 +1,494 @@ +#include "ui/screens/extensions/extensions_page_runtime.h" + +#include +#include +#include +#include + +#include "platform/ui/device_runtime.h" +#include "platform/ui/wifi_runtime.h" +#include "ui/app_runtime.h" +#include "ui/localization.h" +#include "ui/page/page_profile.h" +#include "ui/runtime/memory_profile.h" +#include "ui/runtime/pack_repository.h" +#include "ui/ui_theme.h" +#include "ui/widgets/system_notification.h" +#include "ui/widgets/top_bar.h" + +#if !defined(LV_FONT_MONTSERRAT_16) || !LV_FONT_MONTSERRAT_16 +#define lv_font_montserrat_16 lv_font_montserrat_14 +#endif + +namespace +{ + +using Host = extensions::ui::shell::Host; + +struct RuntimeState +{ + const Host* host = nullptr; + lv_obj_t* root = nullptr; + lv_obj_t* content = nullptr; + lv_obj_t* toolbar = nullptr; + lv_obj_t* status_label = nullptr; + lv_obj_t* list = nullptr; + lv_obj_t* refresh_btn = nullptr; + ::ui::widgets::TopBar top_bar; + std::vector packages; +}; + +RuntimeState s_runtime{}; + +void request_exit() +{ + if (s_runtime.host) + { + ::ui::page::request_exit(s_runtime.host); + return; + } + ui_request_exit_to_menu(); +} + +void on_back(void*) +{ + request_exit(); +} + +void on_focus_scroll(lv_event_t* event) +{ + if (lv_event_get_code(event) != LV_EVENT_FOCUSED) + { + return; + } + + lv_obj_t* target = lv_event_get_target_obj(event); + if (target && lv_obj_is_valid(target)) + { + lv_obj_scroll_to_view(target, LV_ANIM_ON); + } +} + +std::string format_size(std::size_t bytes) +{ + char buffer[32]; + if (bytes >= (1024U * 1024U)) + { + const unsigned whole = static_cast(bytes / (1024U * 1024U)); + const unsigned tenth = + static_cast((bytes % (1024U * 1024U)) * 10U / (1024U * 1024U)); + std::snprintf(buffer, sizeof(buffer), "%u.%u MB", whole, tenth); + } + else + { + std::snprintf(buffer, + sizeof(buffer), + "%lu KB", + static_cast((bytes + 1023U) / 1024U)); + } + return buffer; +} + +std::string compatibility_text(const ui::runtime::packs::PackageRecord& package) +{ + if (package.compatible_firmware && package.compatible_memory_profile) + { + return ::ui::i18n::tr("Compatibility: this device"); + } + + std::string text = ::ui::i18n::tr("Compatibility: "); + bool appended = false; + if (!package.compatible_memory_profile) + { + text += ::ui::i18n::tr("memory profile mismatch"); + appended = true; + } + if (!package.compatible_firmware) + { + if (appended) + { + text += ", "; + } + text += ::ui::i18n::format("needs %s", package.min_firmware_version.c_str()); + } + return text; +} + +std::string state_text(const ui::runtime::packs::PackageRecord& package) +{ + if (package.update_available) + { + return ::ui::i18n::format("Update available: %s", package.version.c_str()); + } + if (package.installed) + { + return ::ui::i18n::format("Installed: %s", package.installed_record.version.c_str()); + } + return ::ui::i18n::tr("Not installed"); +} + +const char* action_label(const ui::runtime::packs::PackageRecord& package) +{ + if (!package.compatible_firmware || !package.compatible_memory_profile) + { + return "Incompatible"; + } + if (package.update_available) + { + return "Update"; + } + if (package.installed) + { + return "Installed"; + } + return "Install"; +} + +void update_top_bar_status() +{ + const platform::ui::wifi::Status wifi = platform::ui::wifi::status(); + if (!wifi.supported) + { + ::ui::widgets::top_bar_set_right_text(s_runtime.top_bar, ::ui::i18n::tr("No Wi-Fi")); + return; + } + if (!wifi.connected) + { + ::ui::widgets::top_bar_set_right_text(s_runtime.top_bar, ::ui::i18n::tr("Offline")); + return; + } + ::ui::widgets::top_bar_set_right_text(s_runtime.top_bar, ::ui::i18n::tr("Online")); +} + +void set_status_text(const char* text) +{ + if (s_runtime.status_label == nullptr) + { + return; + } + ::ui::i18n::set_content_label_text_raw(s_runtime.status_label, text ? text : ""); +} + +void clear_list() +{ + if (s_runtime.list == nullptr) + { + return; + } + lv_obj_clean(s_runtime.list); + if (app_g != nullptr && s_runtime.top_bar.back_btn != nullptr) + { + lv_group_remove_all_objs(app_g); + lv_group_add_obj(app_g, s_runtime.top_bar.back_btn); + if (s_runtime.refresh_btn != nullptr) + { + lv_group_add_obj(app_g, s_runtime.refresh_btn); + } + } +} + +void show_empty_message(const char* text) +{ + clear_list(); + if (s_runtime.list == nullptr) + { + return; + } + + lv_obj_t* label = lv_label_create(s_runtime.list); + lv_obj_set_width(label, LV_PCT(100)); + lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(label, ui::theme::text_muted(), 0); + ::ui::i18n::set_content_label_text_raw(label, text ? text : ""); +} + +void rebuild_list(); + +void on_install_clicked(lv_event_t* event) +{ + const uintptr_t raw_index = reinterpret_cast(lv_event_get_user_data(event)); + if (raw_index == 0U) + { + return; + } + + const std::size_t index = static_cast(raw_index - 1U); + if (index >= s_runtime.packages.size()) + { + return; + } + + std::string error; + set_status_text(::ui::i18n::tr("Installing package...")); + if (!ui::runtime::packs::install_package(s_runtime.packages[index], error)) + { + set_status_text(error.c_str()); + ::ui::SystemNotification::show(error.c_str(), 3000); + return; + } + + ::ui::SystemNotification::show(::ui::i18n::tr("Package installed"), 2000); + rebuild_list(); +} + +void create_package_card(const ui::runtime::packs::PackageRecord& package, std::size_t index) +{ + lv_obj_t* card = lv_obj_create(s_runtime.list); + lv_obj_set_width(card, LV_PCT(100)); + lv_obj_set_style_bg_color(card, ui::theme::surface(), 0); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(card, 1, 0); + lv_obj_set_style_border_color(card, ui::theme::border(), 0); + lv_obj_set_style_radius(card, 14, 0); + lv_obj_set_style_pad_all(card, 10, 0); + lv_obj_set_style_pad_row(card, 6, 0); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* title_row = lv_obj_create(card); + lv_obj_set_width(title_row, LV_PCT(100)); + lv_obj_set_style_bg_opa(title_row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(title_row, 0, 0); + lv_obj_set_style_pad_all(title_row, 0, 0); + lv_obj_set_style_pad_column(title_row, 8, 0); + lv_obj_set_flex_flow(title_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(title_row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(title_row, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* title = lv_label_create(title_row); + lv_obj_set_flex_grow(title, 1); + lv_obj_set_width(title, LV_PCT(100)); + lv_label_set_long_mode(title, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(title, ui::theme::text(), 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0); + const std::string title_text = ::ui::i18n::format("%s %s", + package.display_name.c_str(), + package.version.c_str()); + ::ui::i18n::set_content_label_text_raw(title, title_text.c_str()); + + lv_obj_t* action_btn = lv_btn_create(title_row); + lv_obj_set_size(action_btn, 96, ::ui::page_profile::resolve_control_button_height()); + lv_obj_add_event_cb(action_btn, + on_install_clicked, + LV_EVENT_CLICKED, + reinterpret_cast(static_cast(index + 1U))); + lv_obj_add_event_cb(action_btn, on_focus_scroll, LV_EVENT_FOCUSED, nullptr); + if ((!package.compatible_firmware || !package.compatible_memory_profile) || + (package.installed && !package.update_available)) + { + lv_obj_add_state(action_btn, LV_STATE_DISABLED); + } + lv_obj_t* action_label_obj = lv_label_create(action_btn); + ::ui::i18n::set_label_text(action_label_obj, action_label(package)); + lv_obj_center(action_label_obj); + + lv_obj_t* summary = lv_label_create(card); + lv_obj_set_width(summary, LV_PCT(100)); + lv_label_set_long_mode(summary, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(summary, ui::theme::text_muted(), 0); + ::ui::i18n::set_content_label_text_raw(summary, package.summary.c_str()); + + lv_obj_t* meta = lv_label_create(card); + lv_obj_set_width(meta, LV_PCT(100)); + lv_label_set_long_mode(meta, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(meta, ui::theme::text_muted(), 0); + const std::string meta_text = + ::ui::i18n::format("Size: %s", format_size(package.archive_size_bytes).c_str()); + ::ui::i18n::set_content_label_text_raw(meta, meta_text.c_str()); + + lv_obj_t* compat = lv_label_create(card); + lv_obj_set_width(compat, LV_PCT(100)); + lv_label_set_long_mode(compat, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(compat, + (package.compatible_firmware && package.compatible_memory_profile) + ? ui::theme::text_muted() + : ui::theme::error(), + 0); + const std::string compat_text = compatibility_text(package); + ::ui::i18n::set_content_label_text_raw(compat, compat_text.c_str()); + + lv_obj_t* state = lv_label_create(card); + lv_obj_set_width(state, LV_PCT(100)); + lv_label_set_long_mode(state, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(state, + package.update_available ? ui::theme::status_blue() + : ui::theme::status_green(), + 0); + const std::string state_value = state_text(package); + ::ui::i18n::set_content_label_text_raw(state, state_value.c_str()); + + if (app_g != nullptr) + { + lv_group_add_obj(app_g, action_btn); + } +} + +bool load_catalog(std::string& out_error) +{ + out_error.clear(); + s_runtime.packages.clear(); + + if (!platform::ui::device::card_ready()) + { + out_error = "Insert an SD card to use Extensions"; + return false; + } + + if (!ui::runtime::packs::fetch_catalog(s_runtime.packages, out_error)) + { + return false; + } + return true; +} + +void rebuild_list() +{ + update_top_bar_status(); + + std::string error; + if (!load_catalog(error)) + { + set_status_text(error.c_str()); + show_empty_message(error.c_str()); + return; + } + + if (s_runtime.packages.empty()) + { + set_status_text(::ui::i18n::tr("No packages available")); + show_empty_message(::ui::i18n::tr("No packages available")); + return; + } + + set_status_text(::ui::i18n::format("Available packages: %lu", + static_cast(s_runtime.packages.size())) + .c_str()); + clear_list(); + for (std::size_t i = 0; i < s_runtime.packages.size(); ++i) + { + create_package_card(s_runtime.packages[i], i); + } + + if (app_g != nullptr && s_runtime.top_bar.back_btn != nullptr) + { + lv_group_focus_obj(s_runtime.top_bar.back_btn); + set_default_group(app_g); + lv_group_set_editing(app_g, false); + } +} + +void on_refresh_clicked(lv_event_t* event) +{ + (void)event; + rebuild_list(); +} + +} // namespace + +namespace extensions::ui::runtime +{ + +bool is_available() +{ + return true; +} + +void enter(const shell::Host* host, lv_obj_t* parent) +{ + s_runtime.host = host; + + lv_group_t* previous_group = lv_group_get_default(); + set_default_group(nullptr); + + s_runtime.root = lv_obj_create(parent); + lv_obj_set_size(s_runtime.root, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(s_runtime.root, ::ui::theme::page_bg(), 0); + lv_obj_set_style_bg_opa(s_runtime.root, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(s_runtime.root, 0, 0); + lv_obj_set_style_radius(s_runtime.root, 0, 0); + lv_obj_set_style_pad_all(s_runtime.root, 0, 0); + lv_obj_set_style_pad_row(s_runtime.root, 0, 0); + lv_obj_set_flex_flow(s_runtime.root, LV_FLEX_FLOW_COLUMN); + lv_obj_clear_flag(s_runtime.root, LV_OBJ_FLAG_SCROLLABLE); + + ::ui::widgets::top_bar_init(s_runtime.top_bar, s_runtime.root); + ::ui::widgets::top_bar_set_title(s_runtime.top_bar, ::ui::i18n::tr("Extensions")); + ::ui::widgets::top_bar_set_back_callback(s_runtime.top_bar, on_back, nullptr); + + s_runtime.content = lv_obj_create(s_runtime.root); + lv_obj_set_width(s_runtime.content, LV_PCT(100)); + lv_obj_set_flex_grow(s_runtime.content, 1); + lv_obj_set_style_bg_opa(s_runtime.content, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_runtime.content, 0, 0); + lv_obj_set_style_pad_all(s_runtime.content, 10, 0); + lv_obj_set_style_pad_row(s_runtime.content, 8, 0); + lv_obj_set_flex_flow(s_runtime.content, LV_FLEX_FLOW_COLUMN); + lv_obj_clear_flag(s_runtime.content, LV_OBJ_FLAG_SCROLLABLE); + + s_runtime.toolbar = lv_obj_create(s_runtime.content); + lv_obj_set_width(s_runtime.toolbar, LV_PCT(100)); + lv_obj_set_style_bg_color(s_runtime.toolbar, ::ui::theme::surface(), 0); + lv_obj_set_style_bg_opa(s_runtime.toolbar, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(s_runtime.toolbar, 1, 0); + lv_obj_set_style_border_color(s_runtime.toolbar, ::ui::theme::border(), 0); + lv_obj_set_style_radius(s_runtime.toolbar, 12, 0); + lv_obj_set_style_pad_all(s_runtime.toolbar, 8, 0); + lv_obj_set_style_pad_column(s_runtime.toolbar, 8, 0); + lv_obj_set_flex_flow(s_runtime.toolbar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(s_runtime.toolbar, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_clear_flag(s_runtime.toolbar, LV_OBJ_FLAG_SCROLLABLE); + + s_runtime.status_label = lv_label_create(s_runtime.toolbar); + lv_obj_set_flex_grow(s_runtime.status_label, 1); + lv_obj_set_width(s_runtime.status_label, LV_PCT(100)); + lv_label_set_long_mode(s_runtime.status_label, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(s_runtime.status_label, ::ui::theme::text_muted(), 0); + ::ui::i18n::set_label_text(s_runtime.status_label, "Loading..."); + + s_runtime.refresh_btn = lv_btn_create(s_runtime.toolbar); + lv_obj_set_size(s_runtime.refresh_btn, 90, ::ui::page_profile::resolve_control_button_height()); + lv_obj_add_event_cb(s_runtime.refresh_btn, on_refresh_clicked, LV_EVENT_CLICKED, nullptr); + lv_obj_add_event_cb(s_runtime.refresh_btn, on_focus_scroll, LV_EVENT_FOCUSED, nullptr); + lv_obj_t* refresh_label = lv_label_create(s_runtime.refresh_btn); + ::ui::i18n::set_label_text(refresh_label, "Refresh"); + lv_obj_center(refresh_label); + + s_runtime.list = lv_obj_create(s_runtime.content); + lv_obj_set_width(s_runtime.list, LV_PCT(100)); + lv_obj_set_flex_grow(s_runtime.list, 1); + lv_obj_set_style_bg_opa(s_runtime.list, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_runtime.list, 0, 0); + lv_obj_set_style_pad_all(s_runtime.list, 0, 0); + lv_obj_set_style_pad_row(s_runtime.list, 8, 0); + lv_obj_set_flex_flow(s_runtime.list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_scrollbar_mode(s_runtime.list, LV_SCROLLBAR_MODE_AUTO); + + if (app_g != nullptr && s_runtime.top_bar.back_btn != nullptr) + { + lv_group_remove_all_objs(app_g); + lv_group_add_obj(app_g, s_runtime.top_bar.back_btn); + lv_group_add_obj(app_g, s_runtime.refresh_btn); + lv_group_focus_obj(s_runtime.top_bar.back_btn); + set_default_group(app_g); + lv_group_set_editing(app_g, false); + } + else + { + set_default_group(previous_group); + } + + rebuild_list(); +} + +void exit(lv_obj_t* parent) +{ + (void)parent; + if (s_runtime.root != nullptr) + { + lv_obj_del(s_runtime.root); + } + s_runtime = RuntimeState{}; +} + +} // namespace extensions::ui::runtime diff --git a/modules/ui_shared/src/ui/screens/extensions/extensions_page_shell.cpp b/modules/ui_shared/src/ui/screens/extensions/extensions_page_shell.cpp new file mode 100644 index 00000000..436f2992 --- /dev/null +++ b/modules/ui_shared/src/ui/screens/extensions/extensions_page_shell.cpp @@ -0,0 +1,20 @@ +#include "ui/screens/extensions/extensions_page_shell.h" + +#include "ui/screens/extensions/extensions_page_runtime.h" + +namespace extensions::ui::shell +{ + +void enter(void* user_data, lv_obj_t* parent) +{ + const Host* host = static_cast(user_data); + runtime::enter(host, parent); +} + +void exit(void* user_data, lv_obj_t* parent) +{ + (void)user_data; + runtime::exit(parent); +} + +} // namespace extensions::ui::shell diff --git a/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp b/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp index 2dfb7ca3..0354da93 100644 --- a/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp @@ -8,6 +8,7 @@ #include "app/app_facade_access.h" #include "chat/infra/meshtastic/mt_region.h" #include "sys/clock.h" +#include "ui/assets/fonts/font_utils.h" #include "ui/localization.h" #include "ui/screens/node_info/node_info_page_layout.h" #include "ui/ui_common.h" @@ -175,6 +176,7 @@ lv_obj_t* create_label(lv_obj_t* parent, const char* text, const lv_font_t* font lv_label_set_text(label, text); lv_obj_set_style_text_font(label, font, 0); lv_obj_set_style_text_color(label, color, 0); + ::ui::fonts::apply_localized_font(label, text, font); return label; } @@ -183,6 +185,7 @@ void set_label_text(lv_obj_t* label, const char* text) if (label && text) { lv_label_set_text(label, text); + ::ui::fonts::apply_localized_font(label, text, lv_obj_get_style_text_font(label, LV_PART_MAIN)); } } @@ -772,8 +775,8 @@ void set_node_info(const chat::contacts::NodeInfo& node) snprintf(role_buf, sizeof(role_buf), "%s", role_text.c_str()); set_label_text(s_widgets.avatar_label, avatar_text); - set_label_text(s_widgets.name_label, name.c_str()); - set_label_text(s_widgets.desc_label, long_name); + ::ui::i18n::set_content_label_text_raw(s_widgets.name_label, name.c_str()); + ::ui::i18n::set_content_label_text_raw(s_widgets.desc_label, long_name); set_label_text(s_widgets.id_label, id_buf); set_label_text(s_widgets.role_label, role_buf); diff --git a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp index 6daf445c..219d8799 100644 --- a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "app/app_config.h" #include "app/app_facade_access.h" @@ -21,6 +22,7 @@ #include "platform/ui/settings_store.h" #include "platform/ui/time_runtime.h" #include "platform/ui/tracker_runtime.h" +#include "platform/ui/wifi_runtime.h" #include "ui/app_runtime.h" #include "ui/components/info_card.h" #include "ui/localization.h" @@ -47,9 +49,11 @@ namespace gps_runtime = ::platform::ui::gps; namespace screen_runtime = ::platform::ui::screen; namespace settings_store = ::platform::ui::settings_store; namespace tracker_runtime = ::platform::ui::tracker; +namespace wifi_runtime = ::platform::ui::wifi; constexpr size_t kMaxItems = 32; constexpr size_t kMaxOptions = 40; +constexpr size_t kMaxWifiNetworks = 24; constexpr const char* kPrefsNs = "settings"; constexpr int kNetTxPowerMin = app::AppConfig::kTxPowerMinDbm; constexpr int kNetTxPowerMax = app::AppConfig::kTxPowerMaxDbm; @@ -81,9 +85,78 @@ static size_t kMeshCoreRegionPresetOptionCount = 0; static settings::ui::SettingOption kTxPowerOptions[64] = {}; static size_t kTxPowerOptionCount = 0; static char kTxPowerLabels[64][12] = {}; +static settings::ui::SettingOption kLocaleOptions[16] = {}; +static size_t kLocaleOptionCount = 0; +static char kLocaleOptionLabels[16][48] = {}; +static settings::ui::SettingOption kWifiNetworkOptions[kMaxWifiNetworks] = {}; +static size_t kWifiNetworkOptionCount = 0; +static char kWifiNetworkOptionLabels[kMaxWifiNetworks][64] = {}; +static wifi_runtime::ScanResult kWifiScanResults[kMaxWifiNetworks] = {}; static void update_item_value(settings::ui::ItemWidget& widget); static void open_factory_reset_modal(); +static bool option_labels_are_translated(const settings::ui::SettingItem& item); +static bool option_labels_use_content_font(const settings::ui::SettingItem& item); + +static void copy_bounded(char* out, size_t out_len, const char* text) +{ + if (!out || out_len == 0) + { + return; + } + std::snprintf(out, out_len, "%s", text ? text : ""); +} + +static void clear_wifi_scan_options() +{ + kWifiNetworkOptionCount = 0; + for (size_t i = 0; i < kMaxWifiNetworks; ++i) + { + kWifiNetworkOptions[i] = settings::ui::SettingOption{}; + kWifiNetworkOptionLabels[i][0] = '\0'; + kWifiScanResults[i] = wifi_runtime::ScanResult{}; + } + g_settings.wifi_network_index = -1; +} + +static void rebuild_wifi_scan_options(const std::vector& results) +{ + clear_wifi_scan_options(); + + const size_t limit = results.size() < kMaxWifiNetworks ? results.size() : kMaxWifiNetworks; + for (size_t i = 0; i < limit; ++i) + { + kWifiScanResults[i] = results[i]; + std::snprintf(kWifiNetworkOptionLabels[i], + sizeof(kWifiNetworkOptionLabels[i]), + "%s (%d dBm%s)", + results[i].ssid, + results[i].rssi, + results[i].requires_password ? ", lock" : ""); + kWifiNetworkOptions[i].label = kWifiNetworkOptionLabels[i]; + kWifiNetworkOptions[i].value = static_cast(i); + } + kWifiNetworkOptionCount = limit; +} + +static void refresh_wifi_state_from_runtime() +{ + wifi_runtime::Config config{}; + (void)wifi_runtime::load_config(config); + + const wifi_runtime::Status status = wifi_runtime::status(); + g_settings.wifi_enabled = config.enabled; + copy_bounded(g_settings.wifi_ssid, sizeof(g_settings.wifi_ssid), config.ssid); + copy_bounded(g_settings.wifi_password, sizeof(g_settings.wifi_password), config.password); + if (!status.supported && status.message[0] == '\0') + { + copy_bounded(g_settings.wifi_status, sizeof(g_settings.wifi_status), "Wi-Fi unsupported"); + } + else + { + copy_bounded(g_settings.wifi_status, sizeof(g_settings.wifi_status), status.message); + } +} static bool use_tdeck_info_card_layout() { @@ -609,6 +682,27 @@ static void settings_load() } } + kLocaleOptionCount = 0; + const size_t locale_limit = sizeof(kLocaleOptions) / sizeof(kLocaleOptions[0]); + for (size_t index = 0; index < ::ui::i18n::locale_count() && kLocaleOptionCount < locale_limit; ++index) + { + const ::ui::i18n::LocaleInfo* locale = ::ui::i18n::locale_at(index); + if (!locale) + { + continue; + } + + const char* display_name = + (locale->native_name && locale->native_name[0] != '\0') ? locale->native_name : locale->display_name; + std::snprintf(kLocaleOptionLabels[kLocaleOptionCount], + sizeof(kLocaleOptionLabels[kLocaleOptionCount]), + "%s", + display_name ? display_name : ""); + kLocaleOptions[kLocaleOptionCount].label = kLocaleOptionLabels[kLocaleOptionCount]; + kLocaleOptions[kLocaleOptionCount].value = static_cast(index); + ++kLocaleOptionCount; + } + app_ctx.getEffectiveUserInfo(g_settings.user_name, sizeof(g_settings.user_name), g_settings.short_name, @@ -734,7 +828,6 @@ static void settings_load() } g_settings.privacy_encrypt_mode = cfg.privacy_encrypt_mode; - g_settings.privacy_pki = cfg.privacy_pki; g_settings.privacy_nmea_output = cfg.privacy_nmea_output; g_settings.privacy_nmea_sentence = cfg.privacy_nmea_sentence; @@ -753,10 +846,11 @@ static void settings_load() g_settings.speaker_volume = 100; } apply_message_tone_volume(static_cast(g_settings.speaker_volume)); - g_settings.display_language = static_cast(::ui::i18n::current_language()); + g_settings.display_locale_index = ::ui::i18n::current_locale_index(); g_settings.ble_enabled = cfg.ble_enabled; g_settings.vibration_enabled = prefs_get_bool("vibration_enabled", true); + refresh_wifi_state_from_runtime(); g_settings.advanced_debug_logs = prefs_get_bool("adv_debug", false); @@ -785,6 +879,16 @@ static void format_value(const settings::ui::SettingItem& item, char* out, size_ out[0] = '\0'; switch (item.type) { + case settings::ui::SettingType::Info: + if (item.text_value && item.text_value[0] != '\0') + { + std::snprintf(out, out_len, "%s", item.text_value); + } + else + { + std::snprintf(out, out_len, "%s", ::ui::i18n::tr("N/A")); + } + break; case settings::ui::SettingType::Toggle: snprintf(out, out_len, "%s", ::ui::i18n::tr((item.bool_value && *item.bool_value) ? "ON" : "OFF")); break; @@ -796,7 +900,9 @@ static void format_value(const settings::ui::SettingItem& item, char* out, size_ { if (item.options[i].value == value) { - label = ::ui::i18n::tr(item.options[i].label); + label = option_labels_are_translated(item) + ? ::ui::i18n::tr(item.options[i].label) + : (item.options[i].label ? item.options[i].label : ""); break; } } @@ -834,7 +940,14 @@ static void update_item_value(settings::ui::ItemWidget& widget) } char value[48]; format_value(*widget.def, value, sizeof(value)); - ::ui::i18n::set_label_text_raw(widget.value_label, value); + if (option_labels_use_content_font(*widget.def)) + { + ::ui::i18n::set_content_label_text_raw(widget.value_label, value); + } + else + { + ::ui::i18n::set_label_text_raw(widget.value_label, value); + } } static void modal_prepare_group() @@ -1108,6 +1221,18 @@ static void on_text_save_clicked(lv_event_t* e) } prefs_put_uint_ns("power", "gauge_full_mah", static_cast(value)); } + if (g_state.editing_item->pref_key && + (strcmp(g_state.editing_item->pref_key, "wifi_ssid") == 0 || + strcmp(g_state.editing_item->pref_key, "wifi_password") == 0)) + { + wifi_runtime::Config config{}; + config.enabled = g_settings.wifi_enabled; + copy_bounded(config.ssid, sizeof(config.ssid), g_settings.wifi_ssid); + copy_bounded(config.password, sizeof(config.password), g_settings.wifi_password); + (void)wifi_runtime::save_config(config); + refresh_wifi_state_from_runtime(); + update_item_value(*g_state.editing_widget); + } } modal_close(); } @@ -1199,14 +1324,36 @@ static void on_option_clicked(lv_event_t* e) prefs_put_int(payload->item->pref_key, payload->value); } update_item_value(*payload->widget); - if (payload->item->pref_key && strcmp(payload->item->pref_key, "display_language") == 0) + if (payload->item->pref_key && strcmp(payload->item->pref_key, "display_locale") == 0) { - const ::ui::i18n::Language target_language = ::ui::i18n::language_from_raw(payload->value); - if (::ui::i18n::set_language(target_language, true)) + if (::ui::i18n::set_locale_by_index(static_cast(payload->value), true)) { refresh_menu_labels = true; rebuild_active_app = true; } + else + { + *payload->item->enum_value = previous_value; + } + g_settings.display_locale_index = ::ui::i18n::current_locale_index(); + update_item_value(*payload->widget); + } + if (payload->item->pref_key && strcmp(payload->item->pref_key, "wifi_network") == 0) + { + if (payload->value >= 0 && + static_cast(payload->value) < kWifiNetworkOptionCount) + { + const wifi_runtime::ScanResult& result = + kWifiScanResults[static_cast(payload->value)]; + copy_bounded(g_settings.wifi_ssid, sizeof(g_settings.wifi_ssid), result.ssid); + wifi_runtime::Config config{}; + config.enabled = g_settings.wifi_enabled; + copy_bounded(config.ssid, sizeof(config.ssid), g_settings.wifi_ssid); + copy_bounded(config.password, sizeof(config.password), g_settings.wifi_password); + (void)wifi_runtime::save_config(config); + refresh_wifi_state_from_runtime(); + rebuild_list = true; + } } if (payload->item->pref_key && strcmp(payload->item->pref_key, "mesh_protocol") == 0) { @@ -1711,7 +1858,21 @@ static void open_option_modal(const settings::ui::SettingItem& item, settings::u lv_obj_set_size(btn, LV_PCT(100), ::ui::page_profile::resolve_control_button_height()); style::apply_btn_modal(btn); lv_obj_t* label = lv_label_create(btn); - ::ui::i18n::set_label_text(label, item.options[i].label); + if (option_labels_are_translated(item)) + { + ::ui::i18n::set_label_text(label, item.options[i].label); + } + else + { + if (option_labels_use_content_font(item)) + { + ::ui::i18n::set_content_label_text_raw(label, item.options[i].label); + } + else + { + ::ui::i18n::set_label_text_raw(label, item.options[i].label); + } + } style::apply_label_primary(label); lv_obj_center(label); @@ -1964,11 +2125,6 @@ static const settings::ui::SettingOption kSpeakerVolumeOptions[] = { {"100%", 100}, }; -static const settings::ui::SettingOption kLanguageOptions[] = { - {"English", static_cast(::ui::i18n::Language::English)}, - {"Chinese", static_cast(::ui::i18n::Language::Chinese)}, -}; - static const settings::ui::SettingOption kTimeZoneOptions[] = { {"UTC", 0}, {"Beijing (UTC+8)", 480}, @@ -2024,7 +2180,6 @@ static settings::ui::SettingItem kChatItems[] = { {"Channel", settings::ui::SettingType::Enum, kChatChannelOptions, 2, &g_settings.chat_channel, nullptr, nullptr, 0, false, "chat_channel"}, {"Channel Key / PSK", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.chat_psk, sizeof(g_settings.chat_psk), true, "chat_psk"}, {"Encryption Mode", settings::ui::SettingType::Enum, kPrivacyEncryptOptions, 3, &g_settings.privacy_encrypt_mode, nullptr, nullptr, 0, false, "privacy_encrypt"}, - {"PKI", settings::ui::SettingType::Toggle, nullptr, 0, nullptr, &g_settings.privacy_pki, nullptr, 0, false, "privacy_pki"}, {"Reset Mesh Profiles", settings::ui::SettingType::Action, nullptr, 0, nullptr, nullptr, nullptr, 0, false, "chat_reset_mesh"}, {"Reset Node DB", settings::ui::SettingType::Action, nullptr, 0, nullptr, nullptr, nullptr, 0, false, "chat_reset_nodes"}, {"Clear Message DB", settings::ui::SettingType::Action, nullptr, 0, nullptr, nullptr, nullptr, 0, false, "chat_clear_messages"}, @@ -2063,9 +2218,9 @@ static settings::ui::SettingItem kNetworkItems[] = { }; static settings::ui::SettingItem kScreenItems[] = { - {"Display Language", settings::ui::SettingType::Enum, kLanguageOptions, - sizeof(kLanguageOptions) / sizeof(kLanguageOptions[0]), &g_settings.display_language, nullptr, nullptr, 0, false, - "display_language"}, + {"Display Language", settings::ui::SettingType::Enum, kLocaleOptions, + 0, &g_settings.display_locale_index, nullptr, nullptr, 0, false, + "display_locale"}, {"Screen Timeout", settings::ui::SettingType::Enum, kScreenTimeoutOptions, 4, &g_settings.screen_timeout_ms, nullptr, nullptr, 0, false, "screen_timeout"}, {"Screen Brightness", settings::ui::SettingType::Enum, kScreenBrightnessOptions, sizeof(kScreenBrightnessOptions) / sizeof(kScreenBrightnessOptions[0]), &g_settings.screen_brightness, nullptr, nullptr, 0, false, "screen_brightness"}, @@ -2081,6 +2236,17 @@ static settings::ui::SettingItem kScreenItems[] = { {"Factory Reset", settings::ui::SettingType::Action, nullptr, 0, nullptr, nullptr, nullptr, 0, false, "system_factory_reset"}, }; +static settings::ui::SettingItem kWifiItems[] = { + {"Wi-Fi Enabled", settings::ui::SettingType::Toggle, nullptr, 0, nullptr, &g_settings.wifi_enabled, nullptr, 0, false, "wifi_enabled"}, + {"Status", settings::ui::SettingType::Info, nullptr, 0, nullptr, nullptr, g_settings.wifi_status, sizeof(g_settings.wifi_status), false, "wifi_status"}, + {"Scan Networks", settings::ui::SettingType::Action, nullptr, 0, nullptr, nullptr, nullptr, 0, false, "wifi_scan"}, + {"Detected Network", settings::ui::SettingType::Enum, kWifiNetworkOptions, 0, &g_settings.wifi_network_index, nullptr, nullptr, 0, false, "wifi_network"}, + {"SSID", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.wifi_ssid, sizeof(g_settings.wifi_ssid), false, "wifi_ssid"}, + {"Password", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.wifi_password, sizeof(g_settings.wifi_password), true, "wifi_password"}, + {"Connect", settings::ui::SettingType::Action, nullptr, 0, nullptr, nullptr, nullptr, 0, false, "wifi_connect"}, + {"Disconnect", settings::ui::SettingType::Action, nullptr, 0, nullptr, nullptr, nullptr, 0, false, "wifi_disconnect"}, +}; + static settings::ui::SettingItem kAdvancedItems[] = { {"Debug Logs", settings::ui::SettingType::Toggle, nullptr, 0, nullptr, &g_settings.advanced_debug_logs, nullptr, 0, false, "adv_debug"}, }; @@ -2091,6 +2257,7 @@ static const CategoryDef kCategories[] = { {"Chat", kChatItems, sizeof(kChatItems) / sizeof(kChatItems[0])}, {"Network", kNetworkItems, sizeof(kNetworkItems) / sizeof(kNetworkItems[0])}, {"System", kScreenItems, sizeof(kScreenItems) / sizeof(kScreenItems[0])}, + {"Wi-Fi", kWifiItems, sizeof(kWifiItems) / sizeof(kWifiItems[0])}, {"Advanced", kAdvancedItems, sizeof(kAdvancedItems) / sizeof(kAdvancedItems[0])}, }; @@ -2169,6 +2336,18 @@ static bool has_pref_key(const settings::ui::SettingItem& item, const char* key) return item.pref_key && key && strcmp(item.pref_key, key) == 0; } +static bool option_labels_are_translated(const settings::ui::SettingItem& item) +{ + return !has_pref_key(item, "display_locale") && + !has_pref_key(item, "wifi_network"); +} + +static bool option_labels_use_content_font(const settings::ui::SettingItem& item) +{ + return has_pref_key(item, "display_locale") || + has_pref_key(item, "wifi_network"); +} + static bool should_show_item(const settings::ui::SettingItem& item) { if (!item.pref_key) @@ -2176,6 +2355,18 @@ static bool should_show_item(const settings::ui::SettingItem& item) return true; } + if (has_pref_key(item, "wifi_network")) + { + return wifi_runtime::is_supported() && kWifiNetworkOptionCount > 0; + } + if ((has_pref_key(item, "wifi_scan") || has_pref_key(item, "wifi_connect") || + has_pref_key(item, "wifi_disconnect") || has_pref_key(item, "wifi_ssid") || + has_pref_key(item, "wifi_password") || has_pref_key(item, "wifi_enabled")) && + !wifi_runtime::is_supported()) + { + return false; + } + const bool meshcore = is_meshcore_protocol_selected(); const bool rnode = is_rnode_protocol_selected(); @@ -2189,17 +2380,11 @@ static bool should_show_item(const settings::ui::SettingItem& item) { return false; } - if (has_pref_key(item, "display_language") && !::ui::i18n::supports_chinese()) - { - return false; - } - if (meshcore) { if (has_pref_key(item, "chat_region")) return false; if (has_pref_key(item, "chat_channel")) return false; if (has_pref_key(item, "chat_psk")) return false; - if (has_pref_key(item, "privacy_pki")) return false; if (has_pref_key(item, "net_use_preset")) return false; if (has_pref_key(item, "net_preset")) return false; @@ -2219,7 +2404,6 @@ static bool should_show_item(const settings::ui::SettingItem& item) if (has_pref_key(item, "chat_channel")) return false; if (has_pref_key(item, "chat_psk")) return false; if (has_pref_key(item, "privacy_encrypt")) return false; - if (has_pref_key(item, "privacy_pki")) return false; if (has_pref_key(item, "net_use_preset")) return false; if (has_pref_key(item, "net_preset")) return false; @@ -2311,6 +2495,16 @@ static void build_item_list() { const_cast(widget.def)->option_count = kMeshCoreRegionPresetOptionCount; } + if (widget.def && widget.def->pref_key && + strcmp(widget.def->pref_key, "display_locale") == 0) + { + const_cast(widget.def)->option_count = kLocaleOptionCount; + } + if (widget.def && widget.def->pref_key && + strcmp(widget.def->pref_key, "wifi_network") == 0) + { + const_cast(widget.def)->option_count = kWifiNetworkOptionCount; + } if (widget.def && widget.def->pref_key && (strcmp(widget.def->pref_key, "net_tx_power") == 0 || strcmp(widget.def->pref_key, "mc_tx_power") == 0)) { @@ -2367,6 +2561,10 @@ static bool activate_item_widget(settings::ui::ItemWidget& widget) } const SettingItem& item = *widget.def; + if (item.type == settings::ui::SettingType::Info) + { + return false; + } if (item.type == settings::ui::SettingType::Toggle) { if (item.bool_value) @@ -2445,13 +2643,6 @@ static bool activate_item_widget(settings::ui::ItemWidget& widget) app_ctx.saveConfig(); app_ctx.applyMeshConfig(); } - if (item.pref_key && strcmp(item.pref_key, "privacy_pki") == 0) - { - app::IAppFacade& app_ctx = app::appFacade(); - app_ctx.getConfig().privacy_pki = *item.bool_value; - app_ctx.saveConfig(); - app_ctx.applyPrivacyConfig(); - } if (item.pref_key && strcmp(item.pref_key, "ble_enabled") == 0) { app::IAppFacade& app_ctx = app::appFacade(); @@ -2459,6 +2650,24 @@ static bool activate_item_widget(settings::ui::ItemWidget& widget) app_ctx.saveConfig(); app_ctx.setBleEnabled(*item.bool_value); } + if (item.pref_key && strcmp(item.pref_key, "wifi_enabled") == 0) + { + wifi_runtime::Config config{}; + config.enabled = *item.bool_value; + copy_bounded(config.ssid, sizeof(config.ssid), g_settings.wifi_ssid); + copy_bounded(config.password, sizeof(config.password), g_settings.wifi_password); + (void)wifi_runtime::save_config(config); + if (!wifi_runtime::apply_enabled(config.enabled) && config.enabled) + { + ::ui::SystemNotification::show(::ui::i18n::tr("Wi-Fi start failed"), 3000); + } + if (!config.enabled) + { + clear_wifi_scan_options(); + } + refresh_wifi_state_from_runtime(); + build_item_list(); + } if (item.pref_key && strcmp(item.pref_key, "vibration_enabled") == 0 && *item.bool_value) { device_runtime::trigger_haptic(); @@ -2494,6 +2703,52 @@ static bool activate_item_widget(settings::ui::ItemWidget& widget) { open_factory_reset_modal(); } + else if (item.pref_key && strcmp(item.pref_key, "wifi_scan") == 0) + { + std::vector results; + if (!wifi_runtime::scan(results)) + { + refresh_wifi_state_from_runtime(); + } + else + { + rebuild_wifi_scan_options(results); + for (size_t i = 0; i < kWifiNetworkOptionCount; ++i) + { + if (std::strcmp(kWifiScanResults[i].ssid, g_settings.wifi_ssid) == 0) + { + g_settings.wifi_network_index = static_cast(i); + break; + } + } + refresh_wifi_state_from_runtime(); + } + build_item_list(); + } + else if (item.pref_key && strcmp(item.pref_key, "wifi_connect") == 0) + { + wifi_runtime::Config config{}; + config.enabled = true; + copy_bounded(config.ssid, sizeof(config.ssid), g_settings.wifi_ssid); + copy_bounded(config.password, sizeof(config.password), g_settings.wifi_password); + g_settings.wifi_enabled = true; + (void)wifi_runtime::save_config(config); + if (!wifi_runtime::apply_enabled(true) || !wifi_runtime::connect(&config)) + { + refresh_wifi_state_from_runtime(); + } + else + { + refresh_wifi_state_from_runtime(); + } + build_item_list(); + } + else if (item.pref_key && strcmp(item.pref_key, "wifi_disconnect") == 0) + { + wifi_runtime::disconnect(); + refresh_wifi_state_from_runtime(); + build_item_list(); + } return true; } diff --git a/modules/ui_shared/src/ui/screens/team/team_page_components.cpp b/modules/ui_shared/src/ui/screens/team/team_page_components.cpp index 97b0737e..d69d350f 100644 --- a/modules/ui_shared/src/ui/screens/team/team_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/team/team_page_components.cpp @@ -573,7 +573,7 @@ lv_obj_t* create_list_item(const char* left, const char* right) style::apply_list_item(btn); lv_obj_t* left_label = lv_label_create(btn); - ::ui::i18n::set_label_text_raw(left_label, left); + ::ui::i18n::set_content_label_text_raw(left_label, left); lv_obj_set_width(left_label, LV_PCT(70)); lv_label_set_long_mode(left_label, LV_LABEL_LONG_CLIP); @@ -2110,7 +2110,7 @@ void render_team_home() for (const auto& m : g_team_state.members) { lv_obj_t* label = lv_label_create(row); - ::ui::i18n::set_label_text_raw(label, m.name.c_str()); + ::ui::i18n::set_content_label_text_raw(label, m.name.c_str()); lv_label_set_long_mode(label, LV_LABEL_LONG_CLIP); lv_obj_set_width(label, LV_PCT(24)); lv_obj_set_style_bg_opa(label, LV_OPA_COVER, 0); @@ -2182,7 +2182,7 @@ void render_join_pending() for (const auto& m : g_team_state.members) { lv_obj_t* label = lv_label_create(row); - ::ui::i18n::set_label_text_raw(label, m.name.c_str()); + ::ui::i18n::set_content_label_text_raw(label, m.name.c_str()); lv_label_set_long_mode(label, LV_LABEL_LONG_CLIP); lv_obj_set_width(label, LV_PCT(24)); lv_obj_set_style_bg_opa(label, LV_OPA_COVER, 0); diff --git a/modules/ui_shared/src/ui/screens/tracker/tracker_page_components.cpp b/modules/ui_shared/src/ui/screens/tracker/tracker_page_components.cpp index 9f6a0201..be5cd6f1 100644 --- a/modules/ui_shared/src/ui/screens/tracker/tracker_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/tracker/tracker_page_components.cpp @@ -4,7 +4,7 @@ #include "platform/ui/device_runtime.h" #include "platform/ui/route_storage.h" #include "platform/ui/tracker_runtime.h" -#include "ui/assets/fonts/fonts.h" +#include "ui/assets/fonts/font_utils.h" #include "ui/localization.h" #include "ui/page/page_profile.h" #include "ui/screens/tracker/tracker_page_input.h" @@ -251,7 +251,7 @@ void init_button_styles() lv_style_init(&s_btn_label); lv_style_set_text_color(&s_btn_label, lv_color_hex(kPanelBtnText)); - lv_style_set_text_font(&s_btn_label, &lv_font_noto_cjk_16_2bpp); + lv_style_set_text_font(&s_btn_label, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font())); s_btn_styles_inited = true; } @@ -414,7 +414,7 @@ lv_obj_t* create_list_item_button(const std::string& text, intptr_t user_data, b lv_obj_align(label, LV_ALIGN_LEFT_MID, 10, 0); lv_label_set_text(label, text.c_str()); lv_obj_add_style(label, &s_btn_label, LV_PART_MAIN); - lv_obj_set_style_text_font(label, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font(label, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_label_set_long_mode(label, LV_LABEL_LONG_DOT); lv_obj_set_width(label, LV_PCT(100)); @@ -575,8 +575,7 @@ void update_record_status() const bool recording = platform::ui::tracker::is_recording(); if (state.mode == TrackerPageState::Mode::Record) { - lv_label_set_text(state.status_label, - ::ui::i18n::tr(recording ? "Recording" : "Stopped")); + ::ui::i18n::set_label_text(state.status_label, recording ? "Recording" : "Stopped"); } } @@ -586,7 +585,7 @@ void update_start_stop_button() const bool recording = platform::ui::tracker::is_recording(); if (state.start_stop_label) { - lv_label_set_text(state.start_stop_label, ::ui::i18n::tr(recording ? "Stop" : "New")); + ::ui::i18n::set_label_text(state.start_stop_label, recording ? "Stop" : "New"); } } @@ -752,22 +751,23 @@ void update_route_status() { return; } - lv_obj_set_style_text_font(state.status_label, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font( + state.status_label, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); if (state.mode == TrackerPageState::Mode::Route) { if (!state.active_route.empty()) { const std::string text = ::ui::i18n::format("Active: %s", state.active_route.c_str()); - lv_label_set_text(state.status_label, text.c_str()); + ::ui::i18n::set_label_text_raw(state.status_label, text.c_str()); } else if (!state.selected_route.empty()) { const std::string text = ::ui::i18n::format("Selected: %s", state.selected_route.c_str()); - lv_label_set_text(state.status_label, text.c_str()); + ::ui::i18n::set_label_text_raw(state.status_label, text.c_str()); } else { - lv_label_set_text(state.status_label, ::ui::i18n::tr("No route selected")); + ::ui::i18n::set_label_text(state.status_label, "No route selected"); } } } @@ -976,7 +976,7 @@ lv_obj_t* create_action_menu_button(lv_obj_t* parent, const char* text) lv_obj_set_size(btn, LV_PCT(100), action_menu_button_height()); lv_obj_t* label = lv_label_create(btn); - lv_label_set_text(label, ::ui::i18n::tr(text)); + ::ui::i18n::set_label_text(label, text); lv_obj_center(label); apply_action_button(btn, label); return btn; @@ -1088,7 +1088,7 @@ void open_action_menu_modal() lv_obj_t* title_label = lv_label_create(win); const char* title = (state.mode == TrackerPageState::Mode::Record) ? "Track Actions" : "Route Actions"; - lv_label_set_text(title_label, ::ui::i18n::tr(title)); + ::ui::i18n::set_label_text(title_label, title); lv_obj_set_width(title_label, LV_PCT(100)); lv_label_set_long_mode(title_label, LV_LABEL_LONG_DOT); lv_obj_set_style_text_align(title_label, LV_TEXT_ALIGN_CENTER, 0); @@ -1162,7 +1162,7 @@ void on_list_item_focused(lv_event_t* e) { if (lv_obj_t* label = lv_obj_get_child(target, -1)) { - lv_label_set_text(label, ::ui::i18n::tr("Back")); + ::ui::i18n::set_label_text(label, "Back"); lv_label_set_long_mode(label, LV_LABEL_LONG_DOT); } return; @@ -1184,7 +1184,7 @@ void on_list_item_focused(lv_event_t* e) return; } lv_label_set_text(label, names[idx].c_str()); - lv_obj_set_style_text_font(label, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font(label, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL_CIRCULAR); } @@ -1229,7 +1229,7 @@ void on_list_item_defocused(lv_event_t* e) std::string display_name = format_list_name(names[idx]); lv_label_set_text(label, display_name.c_str()); lv_obj_add_style(label, &s_btn_label, LV_PART_MAIN); - lv_obj_set_style_text_font(label, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font(label, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_label_set_long_mode(label, LV_LABEL_LONG_DOT); } @@ -1278,7 +1278,7 @@ void on_route_load_clicked(lv_event_t*) { if (state.status_label) { - lv_label_set_text(state.status_label, ::ui::i18n::tr("Select a route")); + ::ui::i18n::set_label_text(state.status_label, "Select a route"); } return; } @@ -1336,7 +1336,7 @@ void on_del_confirm_clicked(lv_event_t*) { if (state.status_label) { - lv_label_set_text(state.status_label, ::ui::i18n::tr("Stop recording first")); + ::ui::i18n::set_label_text(state.status_label, "Stop recording first"); } modal_close(state.del_confirm_modal); return; @@ -1382,7 +1382,7 @@ void on_del_confirm_clicked(lv_event_t*) if (!ok && state.status_label) { - lv_label_set_text(state.status_label, ::ui::i18n::tr("Delete failed")); + ::ui::i18n::set_label_text(state.status_label, "Delete failed"); } modal_close(state.del_confirm_modal); } @@ -1407,7 +1407,7 @@ void open_delete_confirm_modal() { if (state.status_label) { - lv_label_set_text(state.status_label, ::ui::i18n::tr("Select a track")); + ::ui::i18n::set_label_text(state.status_label, "Select a track"); } return; } @@ -1424,7 +1424,7 @@ void open_delete_confirm_modal() { if (state.status_label) { - lv_label_set_text(state.status_label, ::ui::i18n::tr("Select a route")); + ::ui::i18n::set_label_text(state.status_label, "Select a route"); } return; } @@ -1440,7 +1440,7 @@ void open_delete_confirm_modal() const std::string msg = ::ui::i18n::format("Delete %s?", state.pending_delete_name.c_str()); lv_obj_t* label = lv_label_create(win); - lv_label_set_text(label, msg.c_str()); + ::ui::i18n::set_label_text_raw(label, msg.c_str()); lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 10); lv_obj_t* btn_row = lv_obj_create(win); @@ -1457,7 +1457,7 @@ void open_delete_confirm_modal() lv_obj_t* confirm_btn = lv_btn_create(btn_row); lv_obj_set_size(confirm_btn, modal_button_width(), action_menu_button_height()); lv_obj_t* confirm_label = lv_label_create(confirm_btn); - lv_label_set_text(confirm_label, ::ui::i18n::tr("Confirm")); + ::ui::i18n::set_label_text(confirm_label, "Confirm"); lv_obj_center(confirm_label); apply_action_button(confirm_btn, confirm_label); lv_obj_add_event_cb(confirm_btn, on_del_confirm_clicked, LV_EVENT_CLICKED, nullptr); @@ -1465,7 +1465,7 @@ void open_delete_confirm_modal() lv_obj_t* cancel_btn = lv_btn_create(btn_row); lv_obj_set_size(cancel_btn, modal_button_width(), action_menu_button_height()); lv_obj_t* cancel_label = lv_label_create(cancel_btn); - lv_label_set_text(cancel_label, ::ui::i18n::tr("Cancel")); + ::ui::i18n::set_label_text(cancel_label, "Cancel"); lv_obj_center(cancel_label); apply_action_button(cancel_btn, cancel_label); lv_obj_add_event_cb(cancel_btn, on_del_cancel_clicked, LV_EVENT_CLICKED, nullptr); @@ -1513,20 +1513,23 @@ void init_page(lv_obj_t* parent) lv_obj_set_width(state.mode_record_btn, LV_PCT(100)); lv_obj_set_height(state.mode_record_btn, filter_button_height()); state.mode_record_label = lv_label_create(state.mode_record_btn); - lv_label_set_text(state.mode_record_label, ::ui::i18n::tr("Record")); + lv_obj_add_style(state.mode_record_label, &s_btn_label, LV_PART_MAIN); + ::ui::i18n::set_label_text(state.mode_record_label, "Record"); lv_obj_center(state.mode_record_label); state.mode_route_btn = lv_btn_create(state.filter_panel); lv_obj_set_width(state.mode_route_btn, LV_PCT(100)); lv_obj_set_height(state.mode_route_btn, filter_button_height()); state.mode_route_label = lv_label_create(state.mode_route_btn); - lv_label_set_text(state.mode_route_label, ::ui::i18n::tr("Route")); + lv_obj_add_style(state.mode_route_label, &s_btn_label, LV_PART_MAIN); + ::ui::i18n::set_label_text(state.mode_route_label, "Route"); lv_obj_center(state.mode_route_label); state.status_label = lv_label_create(state.list_panel); - lv_label_set_text(state.status_label, ::ui::i18n::tr("Stopped")); + ::ui::i18n::set_label_text(state.status_label, "Stopped"); lv_obj_set_width(state.status_label, LV_PCT(100)); - lv_obj_set_style_text_font(state.status_label, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font( + state.status_label, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_obj_set_style_text_color(state.status_label, lv_color_hex(kPanelTextMuted), 0); state.list_container = layout::create_list_container(state.list_panel); diff --git a/modules/ui_shared/src/ui/startup_shell.cpp b/modules/ui_shared/src/ui/startup_shell.cpp index baf8088a..7292fe8f 100644 --- a/modules/ui_shared/src/ui/startup_shell.cpp +++ b/modules/ui_shared/src/ui/startup_shell.cpp @@ -104,7 +104,6 @@ void prepareBootUi(bool waking_from_sleep) void initializeShell(const Hooks& hooks) { - ::ui::i18n::reload_language(); lv_obj_set_style_bg_color(lv_screen_active(), lv_color_black(), LV_PART_MAIN); lv_obj_set_style_radius(lv_screen_active(), 0, 0); diff --git a/modules/ui_shared/src/ui/startup_ui_shell.cpp b/modules/ui_shared/src/ui/startup_ui_shell.cpp index 6955871d..03f0163c 100644 --- a/modules/ui_shared/src/ui/startup_ui_shell.cpp +++ b/modules/ui_shared/src/ui/startup_ui_shell.cpp @@ -55,7 +55,6 @@ bool initializeMenuSkeleton(const Hooks& hooks) } lv_obj_t* active_screen = lv_screen_active(); - ::ui::i18n::reload_language(); lv_obj_set_style_bg_color(active_screen, lv_color_black(), LV_PART_MAIN); lv_obj_set_style_radius(active_screen, 0, 0); diff --git a/modules/ui_shared/src/ui/ui_status.cpp b/modules/ui_shared/src/ui/ui_status.cpp index 76d68336..feb76a96 100644 --- a/modules/ui_shared/src/ui/ui_status.cpp +++ b/modules/ui_shared/src/ui/ui_status.cpp @@ -11,6 +11,7 @@ #include "chat/usecase/chat_service.h" #include "platform/ui/gps_runtime.h" #include "platform/ui/tracker_runtime.h" +#include "platform/ui/wifi_runtime.h" #include "sys/clock.h" #if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM #include "ui/screens/team/team_ui_store.h" @@ -23,6 +24,7 @@ extern "C" extern const lv_image_dsc_t gps_topbar; extern const lv_image_dsc_t message_topbar; extern const lv_image_dsc_t route_topbar; + extern const lv_image_dsc_t wifi_topbar; #if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM extern const lv_image_dsc_t team_topbar; #endif @@ -42,6 +44,7 @@ struct StatusSnapshot bool route_active = false; bool track_recording = false; bool gps_enabled = false; + bool wifi_enabled = false; bool team_active = false; bool ble_enabled = false; int unread = 0; @@ -60,6 +63,7 @@ lv_obj_t* s_menu_status_row = nullptr; lv_obj_t* s_menu_route_icon = nullptr; lv_obj_t* s_menu_tracker_icon = nullptr; lv_obj_t* s_menu_gps_icon = nullptr; +lv_obj_t* s_menu_wifi_icon = nullptr; lv_obj_t* s_menu_team_icon = nullptr; lv_obj_t* s_menu_msg_icon = nullptr; lv_obj_t* s_menu_ble_icon = nullptr; @@ -114,6 +118,7 @@ StatusSnapshot collect_status() snap.route_active = cfg.route_enabled && (cfg.route_path[0] != '\0'); snap.track_recording = platform::ui::tracker::is_recording(); snap.gps_enabled = platform::ui::gps::is_enabled(); + snap.wifi_enabled = platform::ui::wifi::status().enabled; if (auto* ble = app::runtimeFacade().getBleManager()) { snap.ble_enabled = ble->isEnabled(); @@ -156,6 +161,7 @@ void apply_menu_icons(const StatusSnapshot& snap) apply_icon(s_menu_route_icon, &route_topbar, snap.route_active); apply_icon(s_menu_tracker_icon, &tracker_topbar, snap.track_recording); apply_icon(s_menu_gps_icon, &gps_topbar, snap.gps_enabled); + apply_icon(s_menu_wifi_icon, &wifi_topbar, snap.wifi_enabled); #if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM apply_icon(s_menu_team_icon, &team_topbar, snap.team_active); #else @@ -164,9 +170,9 @@ void apply_menu_icons(const StatusSnapshot& snap) apply_icon(s_menu_msg_icon, &message_topbar, snap.unread > 0); apply_icon(s_menu_ble_icon, &ble_topbar, snap.ble_enabled); - const bool any = - snap.route_active || snap.track_recording || snap.gps_enabled || snap.team_active || - snap.ble_enabled || (snap.unread > 0); + const bool any = snap.route_active || snap.track_recording || snap.gps_enabled || + snap.wifi_enabled || snap.team_active || snap.ble_enabled || + (snap.unread > 0); if (any) { lv_obj_clear_flag(s_menu_status_row, LV_OBJ_FLAG_HIDDEN); @@ -227,6 +233,7 @@ void register_menu_status_row(lv_obj_t* row, lv_obj_t* route_icon, lv_obj_t* tracker_icon, lv_obj_t* gps_icon, + lv_obj_t* wifi_icon, lv_obj_t* team_icon, lv_obj_t* msg_icon, lv_obj_t* ble_icon) @@ -235,6 +242,7 @@ void register_menu_status_row(lv_obj_t* row, s_menu_route_icon = route_icon; s_menu_tracker_icon = tracker_icon; s_menu_gps_icon = gps_icon; + s_menu_wifi_icon = wifi_icon; s_menu_team_icon = team_icon; s_menu_msg_icon = msg_icon; s_menu_ble_icon = ble_icon; diff --git a/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp b/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp index 7b0f0d01..e08db072 100644 --- a/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp +++ b/modules/ui_shared/src/ui/widgets/ime/ime_widget.cpp @@ -6,7 +6,6 @@ #include "ui/widgets/ime/ime_widget.h" #include "ui/assets/fonts/font_utils.h" -#include "ui/assets/fonts/fonts.h" #include "ui/localization.h" #include "ui/page/page_profile.h" @@ -25,6 +24,11 @@ namespace ImeWidget* s_active_ime = nullptr; static constexpr int kCandidatesPerPage = 12; +bool script_input_available() +{ + return ::ui::i18n::active_locale_supports_script_input(); +} + #if UI_SHARED_TOUCH_IME_ENABLED static const char* kTouchEnMap[] = { "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "Bksp", "\n", @@ -103,7 +107,7 @@ void set_button_label(lv_obj_t* button, const char* text) { label = lv_label_create(button); } - lv_obj_set_style_text_font(label, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font(label, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_obj_set_style_text_color(label, lv_color_hex(0x3A2A1A), 0); lv_label_set_text(label, text ? text : ""); lv_obj_center(label); @@ -246,7 +250,7 @@ void ImeWidget::init_compact_ui(lv_obj_t* parent) toggle_label_ = lv_label_create(toggle_btn_); lv_label_set_text(toggle_label_, "EN"); - lv_obj_set_style_text_font(toggle_label_, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font(toggle_label_, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_obj_set_style_text_color(toggle_label_, lv_color_hex(0x3A2A1A), 0); lv_obj_center(toggle_label_); @@ -266,7 +270,7 @@ void ImeWidget::init_compact_ui(lv_obj_t* parent) candidates_label_ = lv_label_create(container_); lv_label_set_text(candidates_label_, ""); - lv_obj_set_style_text_font(candidates_label_, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font(candidates_label_, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_obj_set_style_text_color(candidates_label_, lv_color_hex(0x3A2A1A), 0); lv_obj_set_flex_grow(candidates_label_, 1); lv_obj_set_style_text_align(candidates_label_, LV_TEXT_ALIGN_RIGHT, 0); @@ -335,7 +339,7 @@ void ImeWidget::init_touch_ui(lv_obj_t* parent) ::ui::i18n::set_label_text(candidates_label_, "English keyboard"); lv_obj_set_flex_grow(candidates_label_, 1); lv_obj_set_style_text_align(candidates_label_, LV_TEXT_ALIGN_LEFT, 0); - lv_obj_set_style_text_font(candidates_label_, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font(candidates_label_, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_obj_set_style_text_color(candidates_label_, lv_color_hex(0x6B4A1E), 0); candidate_row_ = lv_obj_create(container_); @@ -362,7 +366,8 @@ void ImeWidget::init_touch_ui(lv_obj_t* parent) set_button_label(candidate_btns_[i], ""); apply_candidate_button_style(candidate_btns_[i], false); lv_obj_add_event_cb(candidate_btns_[i], on_candidate_clicked, LV_EVENT_CLICKED, this); - lv_obj_set_style_text_font(candidate_btns_[i], &lv_font_noto_cjk_16_2bpp, LV_PART_MAIN); + lv_obj_set_style_text_font( + candidate_btns_[i], ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), LV_PART_MAIN); } candidate_next_btn_ = lv_btn_create(candidate_row_); @@ -420,6 +425,11 @@ void ImeWidget::detach() void ImeWidget::setMode(Mode mode) { + if (mode == Mode::CN && !script_input_available()) + { + mode = Mode::EN; + } + mode_ = mode; ime_.setEnabled(mode_ == Mode::CN); if (textarea_) @@ -447,6 +457,12 @@ ImeWidget::Mode ImeWidget::mode() const void ImeWidget::cycleMode() { + if (!script_input_available()) + { + setMode(mode_ == Mode::EN ? Mode::NUM : Mode::EN); + return; + } + if (mode_ == Mode::EN) { setMode(Mode::CN); @@ -771,6 +787,12 @@ void ImeWidget::refresh_labels() return; } + if (mode_ == Mode::CN && !script_input_available()) + { + mode_ = Mode::EN; + ime_.setEnabled(false); + } + if (textarea_) { if (mode_ == Mode::CN && ime_.hasBuffer()) diff --git a/modules/ui_shared/src/ui/widgets/system_notification.cpp b/modules/ui_shared/src/ui/widgets/system_notification.cpp index fdb3cb04..ea4ee69a 100644 --- a/modules/ui_shared/src/ui/widgets/system_notification.cpp +++ b/modules/ui_shared/src/ui/widgets/system_notification.cpp @@ -6,7 +6,6 @@ #include "ui/widgets/system_notification.h" #include "ui/assets/fonts/font_utils.h" -#include "ui/assets/fonts/fonts.h" #include "ui/localization.h" #include "ui/page/page_profile.h" @@ -72,7 +71,7 @@ void SystemNotification::init() label_ = lv_label_create(container_); lv_label_set_text(label_, ""); lv_obj_set_style_text_color(label_, lv_color_hex(0x3A2A1A), 0); - lv_obj_set_style_text_font(label_, &lv_font_noto_cjk_16_2bpp, 0); + lv_obj_set_style_text_font(label_, ::ui::fonts::localized_font(::ui::fonts::ui_chrome_font()), 0); lv_obj_set_flex_grow(label_, 1); // Initially hidden @@ -106,7 +105,7 @@ void SystemNotification::show(const char* text, uint32_t duration_ms) } lv_label_set_text(label_, truncated); - ::ui::fonts::apply_localized_font(label_, truncated, &lv_font_noto_cjk_16_2bpp); + ::ui::fonts::apply_localized_font(label_, truncated, ::ui::fonts::ui_chrome_font()); // Cancel existing timer if any if (hide_timer_) diff --git a/packs/europe-latin-ext/DESCRIPTION.txt b/packs/europe-latin-ext/DESCRIPTION.txt new file mode 100644 index 00000000..11261f5a --- /dev/null +++ b/packs/europe-latin-ext/DESCRIPTION.txt @@ -0,0 +1,6 @@ +European Latin Extended adds a shared external font pack for common accented Latin text +and installs locale packs for German, Spanish, French, Italian, Dutch, Polish, and +Portuguese (Portugal). + +This bundle is designed to stay lightweight while still rendering mixed-language contact +names, chat content, and localized UI labels cleanly across European languages. diff --git a/packs/europe-latin-ext/README.md b/packs/europe-latin-ext/README.md new file mode 100644 index 00000000..37c49c86 --- /dev/null +++ b/packs/europe-latin-ext/README.md @@ -0,0 +1,102 @@ +# Europe Latin-Extended Pack Bundle + +This bundle provides a shared Latin Extended font pack plus locale packs for: + +- `de` +- `es` +- `fr` +- `it` +- `nl` +- `pl` +- `pt-PT` + +All of them point to the same external font pack: + +- `ui_font_pack=latin-ext-eu` +- `content_font_pack=latin-ext-eu` + +That keeps the install footprint small while still giving the runtime one reusable pack for accented interface text and mixed-language content. + +Bundle-level package metadata lives in: + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` + +## Bundle Layout + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` +- `fonts/latin-ext-eu/` + - `manifest.ini` + - `build.ini` + - `charset.txt` + - `ranges.txt` + - `font.bin` after generation +- `locales//` + - `manifest.ini` + - `strings.tsv` + +## Generate The Shared Font Pack + +1. Refresh the subset files: + +```bash +python tools/build_locale_pack_charset.py --pack-root packs/europe-latin-ext --font-pack-id latin-ext-eu +``` + +2. Generate `font.bin`: + +```powershell +python tools/generate_binfont_with_lv_font_conv.py ` + --font tools/fonts/NotoSansCJKsc-Regular.otf ` + --charset-file packs/europe-latin-ext/fonts/latin-ext-eu/charset.txt ` + --output packs/europe-latin-ext/fonts/latin-ext-eu/font.bin ` + --size 16 ` + --bpp 2 ` + --node-exe C:\Users\VicLi\AppData\Local\nodejs22\current\node.exe ` + --no-compress +``` + +Notes: + +- `font.bin` is ignored by Git. Keep it locally or regenerate it when the subset changes. +- `build.ini` is the source-of-truth for generating `font.bin` during Pages/package builds. +- `ranges.txt` and `estimated_ram_bytes` are used by the runtime to decide whether the pack fits the current memory profile. +- The locale string tables are intentionally partial. Missing keys fall back to built-in English. + +## Distribution Package + +`python scripts/build_pack_repository.py --pack-root packs --site-root site` produces: + +- `site/assets/packs/europe-latin-ext-1.0.0.zip` +- `site/data/packs.json` + +The zip is the bundle artifact for a future Extensions downloader. Its `payload/` directory unpacks into `/trailmate/packs/...`. + +## Copy To SD Card + +Copy the bundle contents so the SD card ends up with: + +```text +/trailmate/packs/fonts/latin-ext-eu/manifest.ini +/trailmate/packs/fonts/latin-ext-eu/ranges.txt +/trailmate/packs/fonts/latin-ext-eu/font.bin +/trailmate/packs/locales/de/manifest.ini +/trailmate/packs/locales/de/strings.tsv +/trailmate/packs/locales/es/manifest.ini +/trailmate/packs/locales/es/strings.tsv +/trailmate/packs/locales/fr/manifest.ini +/trailmate/packs/locales/fr/strings.tsv +/trailmate/packs/locales/it/manifest.ini +/trailmate/packs/locales/it/strings.tsv +/trailmate/packs/locales/nl/manifest.ini +/trailmate/packs/locales/nl/strings.tsv +/trailmate/packs/locales/pl/manifest.ini +/trailmate/packs/locales/pl/strings.tsv +/trailmate/packs/locales/pt-PT/manifest.ini +/trailmate/packs/locales/pt-PT/strings.tsv +``` + +After reboot, these locales are cataloged immediately, but the external font is only loaded when one of them becomes active or is needed as content coverage. diff --git a/packs/europe-latin-ext/fonts/latin-ext-eu/.gitignore b/packs/europe-latin-ext/fonts/latin-ext-eu/.gitignore new file mode 100644 index 00000000..2224454d --- /dev/null +++ b/packs/europe-latin-ext/fonts/latin-ext-eu/.gitignore @@ -0,0 +1 @@ +font.bin diff --git a/packs/europe-latin-ext/fonts/latin-ext-eu/build.ini b/packs/europe-latin-ext/fonts/latin-ext-eu/build.ini new file mode 100644 index 00000000..d0889975 --- /dev/null +++ b/packs/europe-latin-ext/fonts/latin-ext-eu/build.ini @@ -0,0 +1,4 @@ +font=tools/fonts/NotoSansCJKsc-Regular.otf +size=16 +bpp=2 +no_compress=true diff --git a/packs/europe-latin-ext/fonts/latin-ext-eu/charset.txt b/packs/europe-latin-ext/fonts/latin-ext-eu/charset.txt new file mode 100644 index 00000000..dfac4ca7 --- /dev/null +++ b/packs/europe-latin-ext/fonts/latin-ext-eu/charset.txt @@ -0,0 +1 @@ +¿ÀÉßàáãäçèéêñóõöúüąćꣳńŚśźż diff --git a/packs/europe-latin-ext/fonts/latin-ext-eu/manifest.ini b/packs/europe-latin-ext/fonts/latin-ext-eu/manifest.ini new file mode 100644 index 00000000..7b2ee99d --- /dev/null +++ b/packs/europe-latin-ext/fonts/latin-ext-eu/manifest.ini @@ -0,0 +1,8 @@ +kind=font +id=latin-ext-eu +display_name=Latin Extended EU +usage=both +estimated_ram_bytes=941 +source=binfont +file=font.bin +ranges=ranges.txt diff --git a/packs/europe-latin-ext/fonts/latin-ext-eu/ranges.txt b/packs/europe-latin-ext/fonts/latin-ext-eu/ranges.txt new file mode 100644 index 00000000..3c3597ea --- /dev/null +++ b/packs/europe-latin-ext/fonts/latin-ext-eu/ranges.txt @@ -0,0 +1 @@ +0xBF-0xC0,0xC9,0xDF-0xE1,0xE3-0xE4,0xE7-0xEA,0xF1,0xF3,0xF5-0xF6,0xFA,0xFC,0x105,0x107,0x119,0x141-0x142,0x144,0x15A-0x15B,0x17A,0x17C diff --git a/packs/europe-latin-ext/locales/de/manifest.ini b/packs/europe-latin-ext/locales/de/manifest.ini new file mode 100644 index 00000000..d89b0a54 --- /dev/null +++ b/packs/europe-latin-ext/locales/de/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=de +display_name=German +native_name=Deutsch +ui_font_pack=latin-ext-eu +content_font_pack=latin-ext-eu +strings=strings.tsv diff --git a/packs/europe-latin-ext/locales/de/strings.tsv b/packs/europe-latin-ext/locales/de/strings.tsv new file mode 100644 index 00000000..8a6f7e78 --- /dev/null +++ b/packs/europe-latin-ext/locales/de/strings.tsv @@ -0,0 +1,77 @@ +Action Aktion +Add Hinzufügen +Advanced Erweitert +Always Immer +App App +Auto Auto +Back Zurück +Bluetooth Bluetooth +Bluetooth Pairing Bluetooth-Kopplung +Cancel Abbrechen +Channel Kanal +Chat Chat +Chinese Chinesisch +Close Schließen +Contacts Kontakte +Data Exchange Datenaustausch +Delete Löschen +Disabled Deaktiviert +Discover Suchen +Display Language Anzeigesprache +Edit Bearbeiten +Energy Sweep Frequenzscan +English Englisch +English keyboard Englische Tastatur +Error Fehler +Factory Reset Werkreset +GPS GPS +GPS Status GPS-Status +Hide Ausblenden +Info Info +Loading... Lädt... +Map Karte +Map Layer Kartenebene +Nearby In der Nähe +Network Netzwerk +New Neu +No messages Keine Nachrichten +No recent traffic Kein neuer Funkverkehr +Not set Nicht gesetzt +Number keyboard Zifferntastatur +OFF AUS +OK OK +ON EIN +Pairing Koppeln +PC Link PC-Verbindung +Power Strom +Protocol Protokoll +Ready Bereit +Recent Activity Letzte Aktivität +Reply Antworten +Run Ausführen +Save Speichern +Scan failed Scan fehlgeschlagen +Scan Local Lokal scannen +Scanning 5s... Scanne 5 s... +SEARCH SUCHE +Screen Brightness Bildschirmhelligkeit +Screen Timeout Bildschirm-Timeout +Settings Einstellungen +Setting Einstellung +Sky Plot Sky Plot +Speaker Volume Lautstärke +Status Status +Stopped Gestoppt +Stop Stopp +Submit Bestätigen +System System +Team Team +Team Status Teamstatus +Time Zone Zeitzone +Tracker Tracker +Unknown Unbekannt +USB Disk USB-Speicher +User Name Benutzername +Verification Number Bestätigungsnummer +Vibration Vibration +Clear all settings and restart? Alle Einstellungen löschen und neu starten? diff --git a/packs/europe-latin-ext/locales/es/manifest.ini b/packs/europe-latin-ext/locales/es/manifest.ini new file mode 100644 index 00000000..c3c24d76 --- /dev/null +++ b/packs/europe-latin-ext/locales/es/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=es +display_name=Spanish +native_name=Español +ui_font_pack=latin-ext-eu +content_font_pack=latin-ext-eu +strings=strings.tsv diff --git a/packs/europe-latin-ext/locales/es/strings.tsv b/packs/europe-latin-ext/locales/es/strings.tsv new file mode 100644 index 00000000..8a8da738 --- /dev/null +++ b/packs/europe-latin-ext/locales/es/strings.tsv @@ -0,0 +1,77 @@ +Action Acción +Add Agregar +Advanced Avanzado +Always Siempre +App App +Auto Auto +Back Atrás +Bluetooth Bluetooth +Bluetooth Pairing Emparejamiento Bluetooth +Cancel Cancelar +Channel Canal +Chat Chat +Chinese Chino +Close Cerrar +Contacts Contactos +Data Exchange Interc. datos +Delete Eliminar +Disabled Desactivado +Discover Descubrir +Display Language Idioma de pantalla +Edit Editar +Energy Sweep Barrido RF +English Inglés +English keyboard Teclado inglés +Error Error +Factory Reset Rest. de fábrica +GPS GPS +GPS Status Estado GPS +Hide Ocultar +Info Información +Loading... Cargando... +Map Mapa +Map Layer Capa de mapa +Nearby Cercano +Network Red +New Nuevo +No messages No hay mensajes +No recent traffic Sin tráfico reciente +Not set Sin configurar +Number keyboard Teclado numérico +OFF APAG +OK OK +ON ENC +Pairing Emparejar +PC Link Enlace PC +Power Alimentación +Protocol Protocolo +Ready Listo +Recent Activity Actividad reciente +Reply Responder +Run Ejecutar +Save Guardar +Scan failed Escaneo fallido +Scan Local Escaneo local +Scanning 5s... Escaneando 5 s... +SEARCH BUSCAR +Screen Brightness Brillo pantalla +Screen Timeout Tiempo pantalla +Settings Ajustes +Setting Ajuste +Sky Plot Cielo GPS +Speaker Volume Volumen del altavoz +Status Estado +Stopped Detenido +Stop Detener +Submit Enviar +System Sistema +Team Equipo +Team Status Estado del equipo +Time Zone Zona horaria +Tracker Seguimiento +Unknown Desconocido +USB Disk Disco USB +User Name Nombre de usuario +Verification Number Número de verificación +Vibration Vibración +Clear all settings and restart? ¿Borrar todos los ajustes y reiniciar? diff --git a/packs/europe-latin-ext/locales/fr/manifest.ini b/packs/europe-latin-ext/locales/fr/manifest.ini new file mode 100644 index 00000000..4efe3f0d --- /dev/null +++ b/packs/europe-latin-ext/locales/fr/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=fr +display_name=French +native_name=Français +ui_font_pack=latin-ext-eu +content_font_pack=latin-ext-eu +strings=strings.tsv diff --git a/packs/europe-latin-ext/locales/fr/strings.tsv b/packs/europe-latin-ext/locales/fr/strings.tsv new file mode 100644 index 00000000..3e838f96 --- /dev/null +++ b/packs/europe-latin-ext/locales/fr/strings.tsv @@ -0,0 +1,77 @@ +Action Action +Add Ajouter +Advanced Avancé +Always Toujours +App Appli +Auto Auto +Back Retour +Bluetooth Bluetooth +Bluetooth Pairing Appairage Bluetooth +Cancel Annuler +Channel Canal +Chat Chat +Chinese Chinois +Close Fermer +Contacts Contacts +Data Exchange Échange données +Delete Supprimer +Disabled Désactivé +Discover Découvrir +Display Language Langue d'affichage +Edit Modifier +Energy Sweep Balayage RF +English Anglais +English keyboard Clavier anglais +Error Erreur +Factory Reset Réinit. usine +GPS GPS +GPS Status État GPS +Hide Masquer +Info Infos +Loading... Chargement... +Map Carte +Map Layer Couche de carte +Nearby À proximité +Network Réseau +New Nouveau +No messages Aucun message +No recent traffic Aucun trafic récent +Not set Non défini +Number keyboard Clavier numérique +OFF OFF +OK OK +ON ON +Pairing Appairage +PC Link Lien PC +Power Alimentation +Protocol Protocole +Ready Prêt +Recent Activity Activité récente +Reply Répondre +Run Exécuter +Save Enregistrer +Scan failed Échec du scan +Scan Local Scan local +Scanning 5s... Scan 5 s... +SEARCH RECHERCHE +Screen Brightness Luminosité écran +Screen Timeout Veille écran +Settings Paramètres +Setting Réglage +Sky Plot Ciel GPS +Speaker Volume Volume haut-parleur +Status Statut +Stopped Arrêté +Stop Arrêter +Submit Valider +System Système +Team Équipe +Team Status État équipe +Time Zone Fuseau horaire +Tracker Suivi +Unknown Inconnu +USB Disk Disque USB +User Name Nom d'utilisateur +Verification Number Numéro de vérification +Vibration Vibration +Clear all settings and restart? Effacer tous les réglages et redémarrer ? diff --git a/packs/europe-latin-ext/locales/it/manifest.ini b/packs/europe-latin-ext/locales/it/manifest.ini new file mode 100644 index 00000000..42101e44 --- /dev/null +++ b/packs/europe-latin-ext/locales/it/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=it +display_name=Italian +native_name=Italiano +ui_font_pack=latin-ext-eu +content_font_pack=latin-ext-eu +strings=strings.tsv diff --git a/packs/europe-latin-ext/locales/it/strings.tsv b/packs/europe-latin-ext/locales/it/strings.tsv new file mode 100644 index 00000000..400ed802 --- /dev/null +++ b/packs/europe-latin-ext/locales/it/strings.tsv @@ -0,0 +1,77 @@ +Action Azione +Add Aggiungi +Advanced Avanzato +Always Sempre +App App +Auto Auto +Back Indietro +Bluetooth Bluetooth +Bluetooth Pairing Associazione Bluetooth +Cancel Annulla +Channel Canale +Chat Chat +Chinese Cinese +Close Chiudi +Contacts Contatti +Data Exchange Scambio dati +Delete Elimina +Disabled Disattivato +Discover Scopri +Display Language Lingua display +Edit Modifica +Energy Sweep Scansione RF +English Inglese +English keyboard Tastiera inglese +Error Errore +Factory Reset Riprist. fabbrica +GPS GPS +GPS Status Stato GPS +Hide Nascondi +Info Info +Loading... Caricamento... +Map Mappa +Map Layer Livello mappa +Nearby Vicino +Network Rete +New Nuovo +No messages Nessun messaggio +No recent traffic Nessun traffico recente +Not set Non impostato +Number keyboard Tastiera numerica +OFF OFF +OK OK +ON ON +Pairing Associazione +PC Link Link PC +Power Alimentazione +Protocol Protocollo +Ready Pronto +Recent Activity Attività recenti +Reply Rispondi +Run Esegui +Save Salva +Scan failed Scansione fallita +Scan Local Scansione locale +Scanning 5s... Scansione 5 s... +SEARCH CERCA +Screen Brightness Luminosità schermo +Screen Timeout Timeout schermo +Settings Impostazioni +Setting Impostazione +Sky Plot Cielo GPS +Speaker Volume Volume altoparlante +Status Stato +Stopped Arrestato +Stop Ferma +Submit Invia +System Sistema +Team Squadra +Team Status Stato squadra +Time Zone Fuso orario +Tracker Tracciamento +Unknown Sconosciuto +USB Disk Disco USB +User Name Nome utente +Verification Number Numero di verifica +Vibration Vibrazione +Clear all settings and restart? Cancellare tutte le impostazioni e riavviare? diff --git a/packs/europe-latin-ext/locales/nl/manifest.ini b/packs/europe-latin-ext/locales/nl/manifest.ini new file mode 100644 index 00000000..e1faa9e7 --- /dev/null +++ b/packs/europe-latin-ext/locales/nl/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=nl +display_name=Dutch +native_name=Nederlands +ui_font_pack=latin-ext-eu +content_font_pack=latin-ext-eu +strings=strings.tsv diff --git a/packs/europe-latin-ext/locales/nl/strings.tsv b/packs/europe-latin-ext/locales/nl/strings.tsv new file mode 100644 index 00000000..dbfa8a39 --- /dev/null +++ b/packs/europe-latin-ext/locales/nl/strings.tsv @@ -0,0 +1,77 @@ +Action Actie +Add Toevoegen +Advanced Geavanceerd +Always Altijd +App App +Auto Auto +Back Terug +Bluetooth Bluetooth +Bluetooth Pairing Bluetooth-koppeling +Cancel Annuleren +Channel Kanaal +Chat Chat +Chinese Chinees +Close Sluiten +Contacts Contacten +Data Exchange Gegevenslink +Delete Verwijderen +Disabled Uitgeschakeld +Discover Ontdekken +Display Language Schermtaal +Edit Bewerken +Energy Sweep RF-scan +English Engels +English keyboard Engels toetsenbord +Error Fout +Factory Reset Fabrieksreset +GPS GPS +GPS Status GPS-status +Hide Verbergen +Info Info +Loading... Laden... +Map Kaart +Map Layer Kaartlaag +Nearby Dichtbij +Network Netwerk +New Nieuw +No messages Geen berichten +No recent traffic Geen recent verkeer +Not set Niet ingesteld +Number keyboard Cijfertoetsenbord +OFF UIT +OK OK +ON AAN +Pairing Koppelen +PC Link PC-koppeling +Power Voeding +Protocol Protocol +Ready Klaar +Recent Activity Recente activiteit +Reply Antwoorden +Run Uitvoeren +Save Opslaan +Scan failed Scannen mislukt +Scan Local Lokaal scannen +Scanning 5s... 5 s scannen... +SEARCH ZOEKEN +Screen Brightness Schermhelderheid +Screen Timeout Schermtimeout +Settings Instellingen +Setting Instelling +Sky Plot Sky Plot +Speaker Volume Luidsprekervolume +Status Status +Stopped Gestopt +Stop Stoppen +Submit Verzenden +System Systeem +Team Team +Team Status Teamstatus +Time Zone Tijdzone +Tracker Tracker +Unknown Onbekend +USB Disk USB-schijf +User Name Gebruikersnaam +Verification Number Verificatienummer +Vibration Trilling +Clear all settings and restart? Alle instellingen wissen en herstarten? diff --git a/packs/europe-latin-ext/locales/pl/manifest.ini b/packs/europe-latin-ext/locales/pl/manifest.ini new file mode 100644 index 00000000..189fe5b2 --- /dev/null +++ b/packs/europe-latin-ext/locales/pl/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=pl +display_name=Polish +native_name=Polski +ui_font_pack=latin-ext-eu +content_font_pack=latin-ext-eu +strings=strings.tsv diff --git a/packs/europe-latin-ext/locales/pl/strings.tsv b/packs/europe-latin-ext/locales/pl/strings.tsv new file mode 100644 index 00000000..739fbd90 --- /dev/null +++ b/packs/europe-latin-ext/locales/pl/strings.tsv @@ -0,0 +1,77 @@ +Action Akcja +Add Dodaj +Advanced Zaawansowane +Always Zawsze +App Aplikacja +Auto Auto +Back Wróć +Bluetooth Bluetooth +Bluetooth Pairing Parowanie Bluetooth +Cancel Anuluj +Channel Kanał +Chat Czat +Chinese Chiński +Close Zamknij +Contacts Kontakty +Data Exchange Wymiana danych +Delete Usuń +Disabled Wyłączone +Discover Wykryj +Display Language Język interfejsu +Edit Edytuj +Energy Sweep Skan RF +English Angielski +English keyboard Klawiatura angielska +Error Błąd +Factory Reset Reset fabryczny +GPS GPS +GPS Status Stan GPS +Hide Ukryj +Info Info +Loading... Ładowanie... +Map Mapa +Map Layer Warstwa mapy +Nearby W pobliżu +Network Sieć +New Nowy +No messages Brak wiadomości +No recent traffic Brak ostatniego ruchu +Not set Nie ustawiono +Number keyboard Klawiatura numeryczna +OFF WYŁ +OK OK +ON WŁ +Pairing Parowanie +PC Link Połączenie PC +Power Zasilanie +Protocol Protokół +Ready Gotowe +Recent Activity Ostatnia aktywność +Reply Odpowiedz +Run Uruchom +Save Zapisz +Scan failed Skanowanie nieudane +Scan Local Skan lokalny +Scanning 5s... Skanowanie 5 s... +SEARCH SZUKAJ +Screen Brightness Jasność ekranu +Screen Timeout Wygaszanie ekranu +Settings Ustawienia +Setting Ustawienie +Sky Plot Wykres nieba +Speaker Volume Głośność głośnika +Status Status +Stopped Zatrzymano +Stop Stop +Submit Zatwierdź +System System +Team Zespół +Team Status Stan zespołu +Time Zone Strefa czasowa +Tracker Śledzenie +Unknown Nieznane +USB Disk Dysk USB +User Name Nazwa użytkownika +Verification Number Numer weryfikacyjny +Vibration Wibracja +Clear all settings and restart? Wyczyścić wszystkie ustawienia i uruchomić ponownie? diff --git a/packs/europe-latin-ext/locales/pt-PT/manifest.ini b/packs/europe-latin-ext/locales/pt-PT/manifest.ini new file mode 100644 index 00000000..85158f98 --- /dev/null +++ b/packs/europe-latin-ext/locales/pt-PT/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=pt-PT +display_name=Portuguese (Portugal) +native_name=Português (Portugal) +ui_font_pack=latin-ext-eu +content_font_pack=latin-ext-eu +strings=strings.tsv diff --git a/packs/europe-latin-ext/locales/pt-PT/strings.tsv b/packs/europe-latin-ext/locales/pt-PT/strings.tsv new file mode 100644 index 00000000..a20100e5 --- /dev/null +++ b/packs/europe-latin-ext/locales/pt-PT/strings.tsv @@ -0,0 +1,77 @@ +Action Ação +Add Adicionar +Advanced Avançado +Always Sempre +App App +Auto Auto +Back Voltar +Bluetooth Bluetooth +Bluetooth Pairing Emparelhamento Bluetooth +Cancel Cancelar +Channel Canal +Chat Chat +Chinese Chinês +Close Fechar +Contacts Contactos +Data Exchange Troca dados +Delete Eliminar +Disabled Desativado +Discover Descobrir +Display Language Idioma do ecrã +Edit Editar +Energy Sweep Varrimento RF +English Inglês +English keyboard Teclado inglês +Error Erro +Factory Reset Repor de fábrica +GPS GPS +GPS Status Estado GPS +Hide Ocultar +Info Info +Loading... A carregar... +Map Mapa +Map Layer Camada do mapa +Nearby Próximo +Network Rede +New Novo +No messages Sem mensagens +No recent traffic Sem tráfego recente +Not set Não definido +Number keyboard Teclado numérico +OFF DESL +OK OK +ON LIG +Pairing Emparelhar +PC Link Ligação PC +Power Alimentação +Protocol Protocolo +Ready Pronto +Recent Activity Atividade recente +Reply Responder +Run Executar +Save Guardar +Scan failed Varrimento falhou +Scan Local Varrimento local +Scanning 5s... A varrer 5 s... +SEARCH PESQUISA +Screen Brightness Brilho do ecrã +Screen Timeout Tempo do ecrã +Settings Definições +Setting Definição +Sky Plot Céu GPS +Speaker Volume Volume do altifalante +Status Estado +Stopped Parado +Stop Parar +Submit Enviar +System Sistema +Team Equipa +Team Status Estado da equipa +Time Zone Fuso horário +Tracker Seguimento +Unknown Desconhecido +USB Disk Disco USB +User Name Nome de utilizador +Verification Number Número de verificação +Vibration Vibração +Clear all settings and restart? Limpar todas as definições e reiniciar? diff --git a/packs/europe-latin-ext/package.ini b/packs/europe-latin-ext/package.ini new file mode 100644 index 00000000..e954f501 --- /dev/null +++ b/packs/europe-latin-ext/package.ini @@ -0,0 +1,13 @@ +kind=package +id=europe-latin-ext +package_type=locale-bundle +version=1.0.0 +display_name=European Latin Extended +summary=Shared Latin-Extended font coverage plus German, Spanish, French, Italian, Dutch, Polish, and Portuguese locale packs. +description=DESCRIPTION.txt +readme=README.md +author=Trail Mate +homepage=https://vicliu624.github.io/trail-mate/ +min_firmware_version=0.1.18-alpha +supported_memory_profiles=constrained,standard,extended +tags=language,latin,europe diff --git a/packs/ja/DESCRIPTION.txt b/packs/ja/DESCRIPTION.txt new file mode 100644 index 00000000..00b7a134 --- /dev/null +++ b/packs/ja/DESCRIPTION.txt @@ -0,0 +1,5 @@ +Japanese installs the ja locale together with the ja-cjk external font pack so menu text, +labels, and received Japanese content can render without inflating the firmware image. + +This bundle is currently display-only. It does not add a Japanese IME yet, but it keeps +Japanese coverage available as an installable package on SD storage. diff --git a/packs/ja/README.md b/packs/ja/README.md new file mode 100644 index 00000000..2867adcd --- /dev/null +++ b/packs/ja/README.md @@ -0,0 +1,85 @@ +# ja Pack Bundle + +This bundle keeps Japanese external to the firmware image. + +It contains: + +- locale pack: `ja` +- font pack: `ja-cjk` + +The locale manifest points to: + +- `ui_font_pack=ja-cjk` +- `content_font_pack=ja-cjk` + +Bundle-level package metadata lives in: + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` + +## Bundle Layout + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` +- `fonts/ja-cjk/` + - `manifest.ini` + - `build.ini` + - `charset.txt` + - `ranges.txt` + - `font.bin` after generation +- `locales/ja/` + - `manifest.ini` + - `strings.tsv` + +## Generate The Font Pack + +1. Refresh the subset files: + +```bash +python tools/build_locale_pack_charset.py --pack-root packs/ja --font-pack-id ja-cjk +``` + +2. Generate `font.bin`: + +```powershell +python tools/generate_binfont_with_lv_font_conv.py ` + --font tools/fonts/NotoSansCJKsc-Regular.otf ` + --charset-file packs/ja/fonts/ja-cjk/charset.txt ` + --output packs/ja/fonts/ja-cjk/font.bin ` + --size 16 ` + --bpp 2 ` + --node-exe C:\Users\VicLi\AppData\Local\nodejs22\current\node.exe ` + --no-compress +``` + +Notes: + +- This bundle is display-only for now. It does not declare an IME pack yet. +- `build.ini` is the source-of-truth for generating `font.bin` during Pages/package builds. +- `font.bin` is ignored by Git. Regenerate it whenever the subset changes. +- The manifest carries `ranges.txt` and `estimated_ram_bytes` so the runtime can treat Japanese as an installable but lazily loaded coverage pack. + +## Distribution Package + +`python scripts/build_pack_repository.py --pack-root packs --site-root site` produces: + +- `site/assets/packs/ja-1.0.0.zip` +- `site/data/packs.json` + +The zip is the bundle artifact for a future Extensions downloader. Its `payload/` directory unpacks into `/trailmate/packs/...`. + +## Copy To SD Card + +Copy the bundle contents so the SD card ends up with: + +```text +/trailmate/packs/fonts/ja-cjk/manifest.ini +/trailmate/packs/fonts/ja-cjk/ranges.txt +/trailmate/packs/fonts/ja-cjk/font.bin +/trailmate/packs/locales/ja/manifest.ini +/trailmate/packs/locales/ja/strings.tsv +``` + +After reboot, `日本語` appears in Settings. The font is loaded only when Japanese becomes the active locale or when Japanese content requires it. diff --git a/packs/ja/fonts/ja-cjk/.gitignore b/packs/ja/fonts/ja-cjk/.gitignore new file mode 100644 index 00000000..2224454d --- /dev/null +++ b/packs/ja/fonts/ja-cjk/.gitignore @@ -0,0 +1 @@ +font.bin diff --git a/packs/ja/fonts/ja-cjk/build.ini b/packs/ja/fonts/ja-cjk/build.ini new file mode 100644 index 00000000..d0889975 --- /dev/null +++ b/packs/ja/fonts/ja-cjk/build.ini @@ -0,0 +1,4 @@ +font=tools/fonts/NotoSansCJKsc-Regular.otf +size=16 +bpp=2 +no_compress=true diff --git a/packs/ja/fonts/ja-cjk/charset.txt b/packs/ja/fonts/ja-cjk/charset.txt new file mode 100644 index 00000000..bd28ab2d --- /dev/null +++ b/packs/ja/fonts/ja-cjk/charset.txt @@ -0,0 +1,3 @@ +かくさしじすてなのべまみるをアィイウエカキクグコザシジスセゾタチッテデトドネバビピブプペボムメャヤユョラリルレロワンー不中了交体 +作保信停備先再出削加効動去号名周図国地報場失字存完定実工常情態戻接換操敗数新日明時最未本検止波消済源準無状画番確示秒索細絡続編繁 +自英荷行表規言設詳認語読起跡軌込近返追送通連量閉除隠集電面韓音? diff --git a/packs/ja/fonts/ja-cjk/manifest.ini b/packs/ja/fonts/ja-cjk/manifest.ini new file mode 100644 index 00000000..13bd1346 --- /dev/null +++ b/packs/ja/fonts/ja-cjk/manifest.ini @@ -0,0 +1,8 @@ +kind=font +id=ja-cjk +display_name=Japanese CJK +usage=both +estimated_ram_bytes=10707 +source=binfont +file=font.bin +ranges=ranges.txt diff --git a/packs/ja/fonts/ja-cjk/ranges.txt b/packs/ja/fonts/ja-cjk/ranges.txt new file mode 100644 index 00000000..4b4331c7 --- /dev/null +++ b/packs/ja/fonts/ja-cjk/ranges.txt @@ -0,0 +1 @@ +0x304B,0x304F,0x3055,0x3057-0x3059,0x3066,0x306A,0x306E,0x3079,0x307E-0x307F,0x308B,0x3092,0x30A2-0x30A4,0x30A6,0x30A8,0x30AB,0x30AD,0x30AF-0x30B0,0x30B3,0x30B6-0x30B9,0x30BB,0x30BE-0x30BF,0x30C1,0x30C3,0x30C6-0x30C9,0x30CD,0x30D0,0x30D3-0x30D4,0x30D6-0x30D7,0x30DA,0x30DC,0x30E0-0x30E1,0x30E3-0x30E4,0x30E6-0x30E7,0x30E9-0x30ED,0x30EF,0x30F3,0x30FC,0x4E0D,0x4E2D,0x4E86,0x4EA4,0x4F53,0x4F5C,0x4FDD,0x4FE1,0x505C,0x5099,0x5148,0x518D,0x51FA,0x524A,0x52A0,0x52B9,0x52D5,0x53BB,0x53F7,0x540D,0x5468,0x56F3,0x56FD,0x5730,0x5831,0x5834,0x5931,0x5B57-0x5B58,0x5B8C,0x5B9A,0x5B9F,0x5DE5,0x5E38,0x60C5,0x614B,0x623B,0x63A5,0x63DB,0x64CD,0x6557,0x6570,0x65B0,0x65E5,0x660E,0x6642,0x6700,0x672A,0x672C,0x691C,0x6B62,0x6CE2,0x6D88,0x6E08,0x6E90,0x6E96,0x7121,0x72B6,0x753B,0x756A,0x78BA,0x793A,0x79D2,0x7D22,0x7D30,0x7D61,0x7D9A,0x7DE8,0x7E41,0x81EA,0x82F1,0x8377,0x884C,0x8868,0x898F,0x8A00,0x8A2D,0x8A73,0x8A8D,0x8A9E,0x8AAD,0x8D77,0x8DE1,0x8ECC,0x8FBC,0x8FD1,0x8FD4,0x8FFD,0x9001,0x901A,0x9023,0x91CF,0x9589,0x9664,0x96A0,0x96C6,0x96FB,0x9762,0x97D3,0x97F3,0xFF1F diff --git a/packs/ja/locales/ja/manifest.ini b/packs/ja/locales/ja/manifest.ini new file mode 100644 index 00000000..d9251389 --- /dev/null +++ b/packs/ja/locales/ja/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=ja +display_name=Japanese +native_name=日本語 +ui_font_pack=ja-cjk +content_font_pack=ja-cjk +strings=strings.tsv diff --git a/packs/ja/locales/ja/strings.tsv b/packs/ja/locales/ja/strings.tsv new file mode 100644 index 00000000..a906bbb8 --- /dev/null +++ b/packs/ja/locales/ja/strings.tsv @@ -0,0 +1,80 @@ +Action 操作 +Add 追加 +Advanced 詳細 +Always 常時 +App アプリ +Auto 自動 +Back 戻る +Bluetooth Bluetooth +Bluetooth Pairing Bluetoothペアリング +Cancel キャンセル +Channel チャンネル +Chat チャット +Chinese 中国語 +Close 閉じる +Contacts 連絡先 +Data Exchange データ交換 +Delete 削除 +Disabled 無効 +Discover 検出 +Display Language 表示言語 +Edit 編集 +Energy Sweep 周波数スイープ +English 英語 +English keyboard 英語キーボード +Error エラー +Factory Reset 工場出荷時設定 +GPS GPS +GPS Status GPS状態 +Hide 隠す +Info 情報 +Japanese 日本語 +Korean 韓国語 +Loading... 読み込み中... +Map 地図 +Map Layer 地図レイヤー +Nearby 近く +Network ネットワーク +New 新規 +No messages メッセージなし +No recent traffic 最近の通信なし +Not set 未設定 +Number keyboard 数字キーボード +OFF OFF +OK OK +ON ON +Pairing ペアリング +PC Link PC接続 +Power 電源 +Protocol プロトコル +Ready 準備完了 +Recent Activity 最近のアクティビティ +Reply 返信 +Run 実行 +Save 保存 +Scan failed スキャン失敗 +Scan Local ローカルスキャン +Scanning 5s... 5秒スキャン中... +SEARCH 検索 +Screen Brightness 画面の明るさ +Screen Timeout 画面タイムアウト +Settings 設定 +Setting 設定 +Sky Plot スカイプロット +Speaker Volume スピーカー音量 +Status 状態 +Stopped 停止済み +Stop 停止 +Submit 送信 +System システム +Team チーム +Team Status チーム状態 +Time Zone タイムゾーン +Tracker 軌跡 +Traditional Chinese 繁体字中国語 +Unknown 不明 +USB Disk USBストレージ +User Name ユーザー名 +Verification Number 確認番号 +Vibration バイブレーション +Clear all settings and restart? すべての設定を消去して再起動しますか? diff --git a/packs/ja/package.ini b/packs/ja/package.ini new file mode 100644 index 00000000..9afcb97a --- /dev/null +++ b/packs/ja/package.ini @@ -0,0 +1,13 @@ +kind=package +id=ja +package_type=locale-bundle +version=1.0.0 +display_name=Japanese +summary=Japanese locale bundle with external CJK font coverage for UI text and mixed-language content. +description=DESCRIPTION.txt +readme=README.md +author=Trail Mate +homepage=https://vicliu624.github.io/trail-mate/ +min_firmware_version=0.1.18-alpha +supported_memory_profiles=constrained,standard,extended +tags=language,cjk,japanese diff --git a/packs/ko/DESCRIPTION.txt b/packs/ko/DESCRIPTION.txt new file mode 100644 index 00000000..84618bbb --- /dev/null +++ b/packs/ko/DESCRIPTION.txt @@ -0,0 +1,5 @@ +Korean installs the ko locale together with the ko-cjk external font pack so interface +labels and Korean content can render correctly without carrying the glyph set in firmware. + +This bundle is currently display-only. It does not add a Korean IME yet, but it keeps +Korean support available as an installable SD package. diff --git a/packs/ko/README.md b/packs/ko/README.md new file mode 100644 index 00000000..f188e575 --- /dev/null +++ b/packs/ko/README.md @@ -0,0 +1,85 @@ +# ko Pack Bundle + +This bundle keeps Korean external to the firmware image. + +It contains: + +- locale pack: `ko` +- font pack: `ko-cjk` + +The locale manifest points to: + +- `ui_font_pack=ko-cjk` +- `content_font_pack=ko-cjk` + +Bundle-level package metadata lives in: + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` + +## Bundle Layout + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` +- `fonts/ko-cjk/` + - `manifest.ini` + - `build.ini` + - `charset.txt` + - `ranges.txt` + - `font.bin` after generation +- `locales/ko/` + - `manifest.ini` + - `strings.tsv` + +## Generate The Font Pack + +1. Refresh the subset files: + +```bash +python tools/build_locale_pack_charset.py --pack-root packs/ko --font-pack-id ko-cjk +``` + +2. Generate `font.bin`: + +```powershell +python tools/generate_binfont_with_lv_font_conv.py ` + --font tools/fonts/NotoSansCJKsc-Regular.otf ` + --charset-file packs/ko/fonts/ko-cjk/charset.txt ` + --output packs/ko/fonts/ko-cjk/font.bin ` + --size 16 ` + --bpp 2 ` + --node-exe C:\Users\VicLi\AppData\Local\nodejs22\current\node.exe ` + --no-compress +``` + +Notes: + +- This bundle is display-only for now. It does not declare an IME pack yet. +- `build.ini` is the source-of-truth for generating `font.bin` during Pages/package builds. +- `font.bin` is ignored by Git. Regenerate it whenever the subset changes. +- The runtime uses `ranges.txt` and `estimated_ram_bytes` to decide when Korean can be activated or loaded as extra content coverage. + +## Distribution Package + +`python scripts/build_pack_repository.py --pack-root packs --site-root site` produces: + +- `site/assets/packs/ko-1.0.0.zip` +- `site/data/packs.json` + +The zip is the bundle artifact for a future Extensions downloader. Its `payload/` directory unpacks into `/trailmate/packs/...`. + +## Copy To SD Card + +Copy the bundle contents so the SD card ends up with: + +```text +/trailmate/packs/fonts/ko-cjk/manifest.ini +/trailmate/packs/fonts/ko-cjk/ranges.txt +/trailmate/packs/fonts/ko-cjk/font.bin +/trailmate/packs/locales/ko/manifest.ini +/trailmate/packs/locales/ko/strings.tsv +``` + +After reboot, `한국어` appears in Settings. The font is loaded only when Korean becomes active or when Korean content needs coverage. diff --git a/packs/ko/fonts/ko-cjk/.gitignore b/packs/ko/fonts/ko-cjk/.gitignore new file mode 100644 index 00000000..2224454d --- /dev/null +++ b/packs/ko/fonts/ko-cjk/.gitignore @@ -0,0 +1 @@ +font.bin diff --git a/packs/ko/fonts/ko-cjk/build.ini b/packs/ko/fonts/ko-cjk/build.ini new file mode 100644 index 00000000..d0889975 --- /dev/null +++ b/packs/ko/fonts/ko-cjk/build.ini @@ -0,0 +1,4 @@ +font=tools/fonts/NotoSansCJKsc-Regular.otf +size=16 +bpp=2 +no_compress=true diff --git a/packs/ko/fonts/ko-cjk/charset.txt b/packs/ko/fonts/ko-cjk/charset.txt new file mode 100644 index 00000000..fd5f14a1 --- /dev/null +++ b/packs/ko/fonts/ko-cjk/charset.txt @@ -0,0 +1,3 @@ +가간검결고공교국궤근급기까꺼끔널네다닫답대데도동됨뒤드든들딩락래량레로루류름링만메면모밝번보본블비사삭상새색설성소수숨숫스시실안 +알앱어언업없연영오요용우워원위을음이인일자작장저적전정제주준중지진짐집채처체초최추출취캔커컬켬콜크키태터템토투트팀팅파패페편표프 +피픽한할항행호화확환활 diff --git a/packs/ko/fonts/ko-cjk/manifest.ini b/packs/ko/fonts/ko-cjk/manifest.ini new file mode 100644 index 00000000..c8d38822 --- /dev/null +++ b/packs/ko/fonts/ko-cjk/manifest.ini @@ -0,0 +1,8 @@ +kind=font +id=ko-cjk +display_name=Korean CJK +usage=both +estimated_ram_bytes=8143 +source=binfont +file=font.bin +ranges=ranges.txt diff --git a/packs/ko/fonts/ko-cjk/ranges.txt b/packs/ko/fonts/ko-cjk/ranges.txt new file mode 100644 index 00000000..8141e3c7 --- /dev/null +++ b/packs/ko/fonts/ko-cjk/ranges.txt @@ -0,0 +1 @@ +0xAC00,0xAC04,0xAC80,0xACB0,0xACE0,0xACF5,0xAD50,0xAD6D,0xADA4,0xADFC,0xAE09,0xAE30,0xAE4C,0xAEBC,0xB054,0xB110,0xB124,0xB2E4,0xB2EB,0xB2F5,0xB300,0xB370,0xB3C4,0xB3D9,0xB428,0xB4A4,0xB4DC,0xB4E0,0xB4E4,0xB529,0xB77D,0xB798,0xB7C9,0xB808,0xB85C,0xB8E8,0xB958,0xB984,0xB9C1,0xB9CC,0xBA54,0xBA74,0xBAA8,0xBC1D,0xBC88,0xBCF4,0xBCF8,0xBE14,0xBE44,0xC0AC-0xC0AD,0xC0C1,0xC0C8-0xC0C9,0xC124,0xC131,0xC18C,0xC218,0xC228,0xC22B,0xC2A4,0xC2DC,0xC2E4,0xC548,0xC54C,0xC571,0xC5B4,0xC5B8,0xC5C5-0xC5C6,0xC5F0,0xC601,0xC624,0xC694,0xC6A9,0xC6B0,0xC6CC,0xC6D0,0xC704,0xC744,0xC74C,0xC774,0xC778,0xC77C,0xC790-0xC791,0xC7A5,0xC800-0xC801,0xC804,0xC815,0xC81C,0xC8FC,0xC900,0xC911,0xC9C0,0xC9C4,0xC9D0-0xC9D1,0xCC44,0xCC98,0xCCB4,0xCD08,0xCD5C,0xCD94,0xCD9C,0xCDE8,0xCE94,0xCEE4,0xCEEC,0xCF2C,0xCF5C,0xD06C,0xD0A4,0xD0DC,0xD130,0xD15C,0xD1A0,0xD22C,0xD2B8,0xD300,0xD305,0xD30C,0xD328,0xD398,0xD3B8,0xD45C,0xD504,0xD53C-0xD53D,0xD55C,0xD560,0xD56D,0xD589,0xD638,0xD654-0xD655,0xD658,0xD65C diff --git a/packs/ko/locales/ko/manifest.ini b/packs/ko/locales/ko/manifest.ini new file mode 100644 index 00000000..05dcd227 --- /dev/null +++ b/packs/ko/locales/ko/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=ko +display_name=Korean +native_name=한국어 +ui_font_pack=ko-cjk +content_font_pack=ko-cjk +strings=strings.tsv diff --git a/packs/ko/locales/ko/strings.tsv b/packs/ko/locales/ko/strings.tsv new file mode 100644 index 00000000..f906756f --- /dev/null +++ b/packs/ko/locales/ko/strings.tsv @@ -0,0 +1,80 @@ +Action 작업 +Add 추가 +Advanced 고급 +Always 항상 +App 앱 +Auto 자동 +Back 뒤로 +Bluetooth 블루투스 +Bluetooth Pairing 블루투스 페어링 +Cancel 취소 +Channel 채널 +Chat 채팅 +Chinese 중국어 +Close 닫기 +Contacts 연락처 +Data Exchange 데이터 교환 +Delete 삭제 +Disabled 비활성화 +Discover 검색 +Display Language 표시 언어 +Edit 편집 +Energy Sweep 주파수 스캔 +English 영어 +English keyboard 영어 키보드 +Error 오류 +Factory Reset 공장 초기화 +GPS GPS +GPS Status GPS 상태 +Hide 숨기기 +Info 정보 +Japanese 일본어 +Korean 한국어 +Loading... 로딩 중... +Map 지도 +Map Layer 지도 레이어 +Nearby 근처 +Network 네트워크 +New 새로 만들기 +No messages 메시지 없음 +No recent traffic 최근 트래픽 없음 +Not set 설정 안 됨 +Number keyboard 숫자 키보드 +OFF 끔 +OK 확인 +ON 켬 +Pairing 페어링 +PC Link PC 연결 +Power 전원 +Protocol 프로토콜 +Ready 준비됨 +Recent Activity 최근 활동 +Reply 답장 +Run 실행 +Save 저장 +Scan failed 스캔 실패 +Scan Local 로컬 스캔 +Scanning 5s... 5초 스캔 중... +SEARCH 검색 +Screen Brightness 화면 밝기 +Screen Timeout 화면 꺼짐 시간 +Settings 설정 +Setting 설정 +Sky Plot 위성도 +Speaker Volume 스피커 음량 +Status 상태 +Stopped 중지됨 +Stop 중지 +Submit 제출 +System 시스템 +Team 팀 +Team Status 팀 상태 +Time Zone 시간대 +Tracker 궤적 +Traditional Chinese 번체 중국어 +Unknown 알 수 없음 +USB Disk USB 저장소 +User Name 사용자 이름 +Verification Number 확인 번호 +Vibration 진동 +Clear all settings and restart? 모든 설정을 지우고 다시 시작할까요? diff --git a/packs/ko/package.ini b/packs/ko/package.ini new file mode 100644 index 00000000..011fb875 --- /dev/null +++ b/packs/ko/package.ini @@ -0,0 +1,13 @@ +kind=package +id=ko +package_type=locale-bundle +version=1.0.0 +display_name=Korean +summary=Korean locale bundle with external CJK font coverage for UI text and mixed-language content. +description=DESCRIPTION.txt +readme=README.md +author=Trail Mate +homepage=https://vicliu624.github.io/trail-mate/ +min_firmware_version=0.1.18-alpha +supported_memory_profiles=constrained,standard,extended +tags=language,cjk,korean diff --git a/packs/zh-Hans/DESCRIPTION.txt b/packs/zh-Hans/DESCRIPTION.txt new file mode 100644 index 00000000..e3df4eb7 --- /dev/null +++ b/packs/zh-Hans/DESCRIPTION.txt @@ -0,0 +1,5 @@ +Simplified Chinese installs the zh-Hans locale, the zh-hans-cjk external CJK font pack, +and the zh-hans-pinyin IME registration used by the built-in Pinyin input backend. + +Use this bundle when you want a Chinese interface, Chinese text entry, or reliable +rendering of Chinese names and chat content while another locale remains active. diff --git a/packs/zh-Hans/README.md b/packs/zh-Hans/README.md new file mode 100644 index 00000000..98b43037 --- /dev/null +++ b/packs/zh-Hans/README.md @@ -0,0 +1,90 @@ +# zh-Hans Pack Bundle + +This bundle keeps Simplified Chinese fully external to the firmware image. + +It contains: + +- locale pack: `zh-Hans` +- font pack: `zh-hans-cjk` +- IME pack: `zh-hans-pinyin` + +The locale manifest points to: + +- `ui_font_pack=zh-hans-cjk` +- `content_font_pack=zh-hans-cjk` +- `ime_pack=zh-hans-pinyin` + +Bundle-level package metadata lives in: + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` + +## Bundle Layout + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` +- `fonts/zh-hans-cjk/` + - `manifest.ini` + - `build.ini` + - `charset.txt` + - `ranges.txt` + - `font.bin` after generation +- `locales/zh-Hans/` + - `manifest.ini` + - `strings.tsv` +- `ime/zh-hans-pinyin/` + - `manifest.ini` + +## Generate The Font Pack + +1. Refresh the subset files: + +```bash +python tools/build_locale_pack_charset.py --pack-root packs/zh-Hans --extra-chars-file tools/pinyin_chars.txt +``` + +2. Generate `font.bin`: + +```powershell +python tools/generate_binfont_with_lv_font_conv.py ` + --font tools/fonts/NotoSansCJKsc-Regular.otf ` + --charset-file packs/zh-Hans/fonts/zh-hans-cjk/charset.txt ` + --output packs/zh-Hans/fonts/zh-hans-cjk/font.bin ` + --size 16 ` + --bpp 2 ` + --node-exe C:\Users\VicLi\AppData\Local\nodejs22\current\node.exe ` + --no-compress +``` + +Notes: + +- `charset.txt` and `ranges.txt` are derived from translated strings plus the built-in Pinyin candidate set. +- `estimated_ram_bytes` in the font manifest reflects the runtime LVGL load cost, not the raw file size. +- `build.ini` is the source-of-truth for generating `font.bin` during Pages/package builds. +- `font.bin` is ignored by Git. Regenerate it whenever the strings or IME glyph subset changes. + +## Distribution Package + +`python scripts/build_pack_repository.py --pack-root packs --site-root site` produces: + +- `site/assets/packs/zh-Hans-1.0.0.zip` +- `site/data/packs.json` + +The zip is the bundle artifact for a future Extensions downloader. Its `payload/` directory unpacks into `/trailmate/packs/...`. + +## Copy To SD Card + +Copy the bundle contents so the SD card ends up with: + +```text +/trailmate/packs/fonts/zh-hans-cjk/manifest.ini +/trailmate/packs/fonts/zh-hans-cjk/ranges.txt +/trailmate/packs/fonts/zh-hans-cjk/font.bin +/trailmate/packs/locales/zh-Hans/manifest.ini +/trailmate/packs/locales/zh-Hans/strings.tsv +/trailmate/packs/ime/zh-hans-pinyin/manifest.ini +``` + +After reboot, `简体中文` appears in Settings. The font is only loaded when the locale becomes active or when Chinese content needs to be rendered as a supplement on a different UI locale. diff --git a/packs/zh-Hans/fonts/zh-hans-cjk/.gitignore b/packs/zh-Hans/fonts/zh-hans-cjk/.gitignore new file mode 100644 index 00000000..2224454d --- /dev/null +++ b/packs/zh-Hans/fonts/zh-hans-cjk/.gitignore @@ -0,0 +1 @@ +font.bin diff --git a/packs/zh-Hans/fonts/zh-hans-cjk/build.ini b/packs/zh-Hans/fonts/zh-hans-cjk/build.ini new file mode 100644 index 00000000..d0889975 --- /dev/null +++ b/packs/zh-Hans/fonts/zh-hans-cjk/build.ini @@ -0,0 +1,4 @@ +font=tools/fonts/NotoSansCJKsc-Regular.otf +size=16 +bpp=2 +no_compress=true diff --git a/packs/zh-Hans/fonts/zh-hans-cjk/charset.txt b/packs/zh-Hans/fonts/zh-hans-cjk/charset.txt new file mode 100644 index 00000000..3500a168 --- /dev/null +++ b/packs/zh-Hans/fonts/zh-hans-cjk/charset.txt @@ -0,0 +1,106 @@ +°。一丁七万丈三上下丌不与丐丑专且丕世丘丙业丛东丝丞丢两严丧丨个丫丬中丰串临丶丸丹为主丽举丿乃久乇么义之乌乍乎乏乐乒乓乔乖乘乙乜 +九乞也习乡书乩买乱乳乾了予争事二亍于亏云互亓五井亘亚些亟亠亡亢交亥亦产亨亩享京亭亮亲亳亵人亻亿什仁仂仃仄仅仆仇仉今介仍从仑仓仔仕 +他仗付仙仝仞仟仡代令以仨仪仫们仰仲仳仵件价任份仿企伉伊伍伎伏伐休众优伙会伛伞伟传伢伤伥伦伧伪伫伯估伲伴伶伸伺似伽佃但位低住佐佑体 +何佗佘余佚佛作佝佞佟你佣佤佥佧佩佬佯佰佳佴佶佻佼佾使侃侄侈侉例侍侏侑侔侗供依侠侣侥侦侧侨侩侪侬侮侯侵便促俄俅俊俎俏俐俑俗俘俚俜保 +俞俟信俣俦俨俩俪俭修俯俱俳俸俺俾倌倍倏倒倔倘候倚倜借倡倥倦倨倩倪倬倭倮债值倾偃假偈偌偎偏偕做停健偬偶偷偻偾偿傀傅傈傍傣傥傧储傩催 +傲傺傻像僖僚僦僧僬僭僮僳僵僻儆儇儋儒儡儿兀允元兄充兆先光克免兑兔兕兖党兜兢入全八公六兮兰共关兴兵其具典兹养兼兽冀冁冂内冈冉册再冒 +冕冖冗写军农冠冢冤冥冫冬冯冰冱冲决况冶冷冻冼冽净凄准凇凉凋凌减凑凛凝几凡凤凫凭凯凰凳凵凶凸凹出击凼函凿刀刁刂刃分切刈刊刍刎刑划刖 +列刘则刚创初删判刨利别刭刮到刳制刷券刹刺刻刽刿剀剁剂剃削剌前剐剑剔剖剜剞剡剥剧剩剪副割剽剿劁劂劈劐劓力劝办功加务劢劣动助努劫劬劭 +励劲劳劾势勃勇勉勋勐勒勖勘募勤勰勹勺勾勿匀包匆匈匍匏匐匕化北匙匚匝匠匡匣匦匪匮匹区医匾匿十千卅升午卉半华协卑卒卓单卖南博卜卞卟占 +卡卢卣卤卦卧卩卫卮卯印危即却卵卷卸卺卿厂厄厅历厉压厌厍厕厘厚厝原厢厣厥厦厨厩厮厶去县叁参又叉及友双反发叔取受变叙叛叟叠口古句另叨 +叩只叫召叭叮可台叱史右叵叶号司叹叻叼叽吁吃各吆合吉吊同名后吏吐向吒吓吕吖吗君吝吞吟吠吡吣否吧吨吩含听吭吮启吱吲吴吵吸吹吻吼吾呀呃 +呆呈告呋呐呒呓呔呕呖呗员呙呛呜呢呤呦周呱呲味呵呶呷呸呻呼命咀咂咄咆咋和咎咏咐咒咔咕咖咙咚咛咝咣咤咦咧咨咩咪咫咬咭咯咱咳咴咸咻咽咿 +哀品哂哄哆哇哈哉哌响哎哏哐哑哒哓哔哕哗哙哚哜哝哞哟哥哦哧哨哩哪哭哮哲哳哺哼哽哿唁唆唇唉唏唐唑唔唛唠唢唣唤唧唪唬售唯唰唱唳唷唼唾唿 +啁啃啄商啉啊啐啕啖啜啡啤啥啦啧啪啬啭啮啵啶啷啸啻啼啾喀喁喂喃善喇喈喉喊喋喏喑喔喘喙喜喝喟喧喱喳喵喷喹喻喽喾嗄嗅嗉嗌嗍嗑嗒嗓嗔嗖嗜 +嗝嗟嗡嗣嗤嗥嗦嗨嗪嗫嗬嗯嗲嗳嗵嗷嗽嗾嘀嘁嘈嘉嘌嘎嘏嘘嘛嘞嘟嘣嘤嘧嘬嘭嘱嘲嘴嘶嘹嘻嘿噌噍噎噔噗噘噙噜噢噤器噩噪噫噬噱噶噻噼嚅嚆嚎嚏 +嚓嚣嚯嚷嚼囊囔囗囚四囝回囟因囡团囤囫园困囱围囵囹固国图囿圃圄圆圈圉圊圜土圣在圩圪圬圭圮圯地圳圹场圻圾址坂均坊坌坍坎坏坐坑块坚坛坜 +坝坞坟坠坡坤坦坨坩坪坫坭坯坳坶坷坻坼垂垃垄垅垆型垌垒垓垛垠垡垢垣垤垦垧垩垫垭垮垲垴垸埂埃埋城埏埒埔埕埘埙埚埝域埠埤埭埯埴埸培基埽 +堀堂堆堇堋堍堑堕堙堞堠堡堤堪堰堵塄塌塍塑塔塘塞塥填塬塾墀墁境墅墉墒墓墙墚增墟墨墩墼壁壅壑壕壤士壬壮声壳壶壹夂处备复夏夔夕外夙多夜 +够夤夥大天太夫夭央夯失头夷夸夹夺夼奁奂奄奇奈奉奋奎奏契奔奕奖套奘奚奠奢奥女奴奶奸她好妁如妃妄妆妇妈妊妍妒妓妖妗妙妞妣妤妥妨妩妪妫 +妮妯妲妹妻妾姆姊始姐姑姒姓委姗姘姚姜姝姣姥姨姬姹姻姿威娃娄娅娆娇娈娉娌娑娓娘娜娟娠娣娥娩娱娲娴娶娼婀婆婉婊婕婚婢婧婪婴婵婶婷婺婿 +媒媚媛媪媲媳媵媸媾嫁嫂嫉嫌嫒嫔嫖嫘嫜嫠嫡嫣嫦嫩嫫嫱嬉嬖嬗嬲嬴嬷孀子孑孓孔孕字存孙孚孛孜孝孟孢季孤孥学孩孪孬孰孱孳孵孺孽宀宁它宄宅 +宇守安宋完宏宓宕宗官宙定宛宜宝实宠审客宣室宥宦宪宫宰害宴宵家宸容宽宾宿寂寄寅密寇富寐寒寓寝寞察寡寤寥寨寮寰寸对寺寻导寿封射将尉尊 +小少尔尕尖尘尚尜尝尢尤尥尧尬就尴尸尹尺尻尼尽尾尿局屁层居屈屉届屋屎屏屐屑展屙属屠屡屣履屦屮屯山屹屺屿岁岂岈岌岍岐岑岔岖岗岘岙岚岛 +岜岢岣岩岫岬岭岱岳岵岷岸岽岿峁峄峋峒峙峡峤峥峦峨峪峭峰峻崂崃崆崇崎崔崖崛崞崤崦崧崩崭崮崴崽崾嵇嵊嵋嵌嵘嵛嵝嵩嵫嵬嵯嵴嶂嶙嶝嶷巅巍 +巛川州巡巢工左巧巨巩巫差巯己已巳巴巷巽巾币市布帅帆师希帏帐帑帔帕帖帘帙帚帛帜帝带帧席帮帱帷常帻帼帽幂幄幅幌幔幕幛幞幡幢干平年并幸 +幺幻幼幽广庀庄庆庇床庋序庐庑库应底庖店庙庚府庞废庠庥度座庭庳庵庶康庸庹庾廉廊廑廒廓廖廛廨廪廴延廷建廾廿开弁异弃弄弈弊弋式弑弓引弗 +弘弛弟张弥弦弧弩弪弭弯弱弹强弼彀彐归当录彖彗彘彝彡形彤彦彩彪彬彭彰影彳彷役彻彼往征徂径待徇很徉徊律後徐徒徕得徘徙徜御徨循徭微徵德 +徼徽心忄必忆忉忌忍忏忐忑忒忖志忘忙忝忠忡忤忧忪快忭忮忱念忸忻忽忾忿怀态怂怃怄怅怆怊怍怎怏怒怔怕怖怙怛怜思怠怡急怦性怨怩怪怫怯怵总 +怼怿恁恂恃恋恍恐恒恕恙恚恝恢恣恤恧恨恩恪恫恬恭息恰恳恶恸恹恺恻恼恽恿悃悄悉悌悍悒悔悖悚悛悝悟悠患悦您悫悬悭悯悱悲悴悸悻悼情惆惊惋 +惑惕惘惚惜惝惟惠惦惧惨惩惫惬惭惮惯惰想惴惶惹惺愀愁愆愈愉愍愎意愕愚感愠愣愤愦愧愫愿慈慊慌慎慑慕慝慢慧慨慰慵慷憋憎憔憝憧憨憩憬憷憾 +懂懈懊懋懑懒懔懦懵懿戆戈戊戋戌戍戎戏成我戒戕或戗战戚戛戟戡戢戤戥截戬戮戳戴户戽戾房所扁扃扇扈扉手扌才扎扑扒打扔托扛扣扦执扩扪扫扬 +扭扮扯扰扳扶批扼找承技抄抉把抑抒抓投抖抗折抚抛抟抠抡抢护报抨披抬抱抵抹抻押抽抿拂拄担拆拇拈拉拊拌拍拎拐拒拓拔拖拗拘拙拚招拜拟拢拣 +拥拦拧拨择括拭拮拯拱拳拴拶拷拼拽拾拿持挂指挈按挎挑挖挚挛挝挞挟挠挡挢挣挤挥挨挪挫振挲挹挺挽捂捃捅捆捉捋捌捍捎捏捐捕捞损捡换捣捧捩 +捭据捱捶捷捺捻掀掂掇授掉掊掌掎掏掐排掖掘掠探掣接控推掩措掬掭掮掰掳掴掷掸掺掼掾揄揆揉揍揎描提插揖揞揠握揣揩揪揭揲援揶揸揽揿搀搁搂 +搅搋搌搏搐搓搔搛搜搞搠搡搦搪搬搭搴携搽搿摁摄摅摆摇摈摊摒摔摘摞摧摩摭摸摹摺撂撄撅撇撑撒撕撖撙撞撤撩撬播撮撰撵撷撸撺撼擀擂擅操擎擐 +擒擗擘擞擢擤擦攀攉攒攘攥攫攮支攴攵收攸改攻放政故效敉敌敏救敕敖教敛敝敞敢散敦敫敬数敲整敷文斋斌斐斑斓斗料斛斜斟斡斤斥斧斩斫断斯新 +方於施旁旃旄旅旆旋旌旎族旒旖旗无既日旦旧旨早旬旭旮旯旰旱时旷旺昀昂昃昆昊昌明昏易昔昕昙昝星映春昧昨昭是昱昴昵昶昼显晁晃晋晌晏晒晓 +晔晕晖晗晚晟晡晤晦晨普景晰晴晶晷智晾暂暄暇暌暑暖暗暝暧暨暮暴暹暾曙曛曜曝曦曩曰曲曳更曷曹曼曾替最月有朊朋服朐朔朕朗望朝期朦木未末 +本札术朱朴朵机朽杀杂权杆杈杉杌李杏材村杓杖杜杞束杠条来杨杩杪杭杯杰杲杳杵杷杼松板极构枇枉枋析枕林枘枚果枝枞枢枣枥枧枨枪枫枭枯枰枳 +枵架枷枸柁柃柄柏某柑柒染柔柘柙柚柜柝柞柠柢查柩柬柯柰柱柳柴柽柿栀栅标栈栉栊栋栌栎栏树栓栖栗栝校栩株栲栳样核根格栽栾桀桁桂桃桄桅框 +案桉桊桌桎桐桑桓桔桕桠桡桢档桤桥桦桧桨桩桫桴桶桷梁梃梅梆梏梓梗梢梦梧梨梭梯械梳梵检棂棉棋棍棒棕棘棚棠棣森棰棱棵棹棺棼椁椅椋植椎椐 +椒椟椠椤椭椰椴椹椽椿楂楔楗楚楝楞楠楣楦楫楮楱楷楸楹楼榀概榄榆榇榈榉榍榔榕榘榛榜榧榨榫榭榱榴榷榻槁槊槌槎槐槔槛槟槠槭槲槽槿樊樗樘樟 +模樨横樯樱樵樽樾橄橇橐橘橙橛橡橥橱橹橼檀檄檎檐檑檗檠檩檫檬欠次欢欣欤欧欲欷欹欺款歃歆歇歉歌歙止正此步武歧歪歹死歼殁殂殃殄殆殇殉殊 +残殍殒殓殖殚殛殡殪殳殴段殷殿毁毂毅毋母每毒毓比毕毖毗毙毛毡毪毫毯毳毵毹毽氅氆氇氍氏氐民氓气氕氖氘氙氚氛氟氡氢氤氦氧氨氩氪氮氯氰氲 +水氵永氽汀汁求汆汇汉汊汐汔汕汗汛汜汝汞江池污汤汨汩汪汰汲汴汶汹汽汾沁沂沃沅沆沈沉沌沏沐沓沔沙沛沟没沣沤沥沦沧沩沪沫沭沮沱沲河沸油 +治沼沽沾沿泄泅泉泊泌泐泓泔法泖泗泛泞泠泡波泣泥注泪泫泮泯泰泱泳泵泶泷泸泺泻泼泽泾洁洄洇洋洌洎洒洗洙洚洛洞津洧洪洫洮洱洲洳洵洹活洼 +洽派流浃浅浆浇浈浊测浍济浏浑浒浓浔浙浚浜浞浠浣浦浩浪浮浯浴海浸浼涂涅消涉涌涎涑涓涔涕涛涝涞涟涠涡涣涤润涧涨涩涪涫涮涯液涵涸涿淀淄 +淅淆淇淋淌淑淖淘淙淝淞淠淡淤淦淫淬淮深淳混淹添淼清渊渌渍渎渐渑渔渖渗渚渝渠渡渣渤渥温渫渭港渲渴游渺湃湄湍湎湓湔湖湘湛湟湫湮湾湿溃 +溅溆溉溏源溘溜溟溢溥溧溪溯溱溲溴溶溷溺溻溽滁滂滇滋滏滑滓滔滕滗滚滞滟滠满滢滤滥滦滨滩滴滹漂漆漉漏漓演漕漠漤漩漪漫漭漯漱漳漶漾潆潇 +潋潍潘潜潞潢潦潭潮潲潴潸潺潼澄澈澉澌澍澎澜澡澧澳澶澹激濂濉濑濒濞濠濡濮濯瀑瀚瀛瀣瀵瀹灌灏灞火灬灭灯灰灵灶灸灼灾灿炀炅炉炊炎炒炔炕 +炖炙炜炝炫炬炭炮炯炱炳炷炸点炻炼炽烀烁烂烃烈烊烘烙烛烟烤烦烧烨烩烫烬热烯烷烹烽焉焊焐焓焕焖焘焙焚焦焯焰焱然煅煊煌煎煜煞煤煦照煨煮 +煲煳煸煺煽熄熊熏熔熘熙熟熠熨熬熳熵熹燃燎燔燕燠燥燧燮燹爆爝爨爪爬爰爱爵父爷爸爹爻爽爿片版牌牍牒牖牙牛牝牟牡牢牦牧物牮牯牲牵特牺牾 +牿犀犁犄犊犋犍犏犒犟犬犭犯犰犴状犷犸犹狁狂狃狄狈狍狎狐狒狗狙狞狠狡狨狩独狭狮狯狰狱狲狳狴狷狸狺狻狼猁猃猊猎猓猕猖猗猛猜猝猞猡猢猥 +猩猪猫猬献猱猴猷猸猹猾猿獍獐獒獗獠獬獭獯獾玄率玉王玎玑玖玛玟玢玩玫玮环现玲玳玷玺玻珀珂珈珉珊珍珏珐珑珙珞珠珥珧珩班珲球琅理琉琊琏 +琐琚琛琢琥琦琨琪琬琮琰琳琴琵琶琼瑁瑕瑗瑙瑚瑛瑜瑞瑟瑭瑰瑶瑷瑾璀璁璃璇璋璎璐璜璞璧璨璩璺瓒瓜瓞瓠瓢瓣瓤瓦瓮瓯瓴瓶瓷瓿甄甍甏甑甓甘甙 +甚甜生甥用甩甫甬甭甯田由甲申电男甸町画甾畀畅畈畋界畎畏畔留畚畛畜略畦番畲畴畸畹畿疃疆疋疏疑疒疔疖疗疙疚疝疟疠疡疣疤疥疫疬疮疯疰疱 +疲疳疴疵疸疹疼疽疾痂痃痄病症痈痉痊痍痒痔痕痖痘痛痞痢痣痤痦痧痨痪痫痰痱痴痹痼痿瘀瘁瘃瘅瘊瘌瘐瘕瘗瘘瘙瘛瘟瘠瘢瘤瘥瘦瘩瘪瘫瘭瘰瘳瘴 +瘵瘸瘼瘾瘿癀癃癌癍癔癖癜癞癣癫癯癸登白百皂的皆皇皈皋皎皑皓皖皙皤皮皱皲皴皿盂盅盆盈益盍盎盏盐监盒盔盖盗盘盛盟盥目盯盱盲直相盹盼盾 +省眄眇眈眉看眍眙眚真眠眢眦眨眩眭眯眵眶眷眸眺眼着睁睃睇睐睑睚睛睡睢督睥睦睨睫睬睹睽睾睿瞀瞄瞅瞌瞍瞎瞑瞒瞟瞠瞢瞥瞧瞩瞪瞬瞰瞳瞵瞻瞽 +瞿矍矗矛矜矢矣知矧矩矫矬短矮石矶矸矽矾矿砀码砂砉砌砍砑砒研砖砗砘砚砜砝砟砣砥砦砧砩砬砭砰破砷砸砹砺砻砼砾础硅硇硌硎硐硒硕硖硗硝硪 +硫硬硭确硷硼碇碉碌碍碎碑碓碗碘碚碛碜碟碡碣碥碧碰碱碲碳碴碹碾磁磅磉磊磋磐磔磕磙磨磬磲磴磷磺礁礅礓礞礤礴示礻礼社祀祁祆祈祉祓祖祗祚 +祛祜祝神祟祠祢祥祧票祭祯祷祸祺禀禁禄禅禊福禚禧禳禹禺离禽禾秀私秃秆秉秋种科秒秕秘租秣秤秦秧秩秫秭积称秸移秽稀稂稃稆程稍税稔稗稚稞 +稠稣稳稷稹稻稼稽稿穆穑穗穰穴究穷穸穹空穿窀突窃窄窆窈窍窑窒窕窖窗窘窜窝窟窠窥窦窨窬窭窳窿立竖站竞竟章竣童竦竭端竹竺竽竿笃笄笆笈笊 +笋笏笑笔笕笙笛笞笠笤笥符笨笪笫第笮笱笳笸笺笼笾筅筇等筋筌筏筐筑筒答策筘筚筛筝筠筢筮筱筲筵筷筹筻签简箅箍箐箔箕算箜箝管箢箦箧箨箩箪 +箫箬箭箱箴箸篁篆篇篌篑篓篙篚篝篡篥篦篪篮篱篷篼篾簇簋簌簏簖簟簦簧簪簸簿籀籁籍米籴类籼籽粉粑粒粕粗粘粜粝粞粟粢粤粥粪粮粱粲粳粹粼粽 +精糁糅糇糈糊糌糍糕糖糗糙糜糟糠糨糯糸系紊素索紧紫累絮絷綦綮縻繁繇纂纛纟纠纡红纣纤纥约级纨纩纪纫纬纭纯纰纱纲纳纵纶纷纸纹纺纽纾线绀 +绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绗绘给绚绛络绝绞统绠绡绢绣绥绦继绨绩绪绫续绮绯绰绱绲绳维绵绶绷绸绺绻综绽绾绿缀缁缂缃缄 +缅缆缇缈缉缋缌缍缎缏缑缒缓缔缕编缗缘缙缚缛缜缝缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵缶缸缺罂罄罅罐网罔罕罗罘罚罟罡罢罨罩 +罪置罱署罴罹罾羁羊羌美羔羚羝羞羟羡群羧羯羰羲羸羹羼羽羿翁翅翊翌翎翔翕翘翟翠翡翥翦翩翮翰翱翳翻翼耀老考耄者耆耋而耍耐耒耔耕耖耗耘耙 +耜耠耢耥耦耧耨耩耪耱耳耵耶耷耸耻耽耿聂聃聆聊聋职聍聒联聘聚聩聪聱聿肀肃肄肆肇肉肋肌肓肖肘肚肛肜肝肟肠股肢肤肥肩肪肫肭肮肯肱育肴肷 +肺肼肽肾肿胀胁胂胃胄胆背胍胎胖胗胙胚胛胜胝胞胡胤胥胧胨胩胪胫胬胭胯胰胱胲胳胴胶胸胺胼能脂脆脉脊脍脎脏脐脑脒脓脔脖脘脚脞脬脯脱脲脶 +脸脾腆腈腊腋腌腐腑腓腔腕腙腚腠腥腧腩腭腮腰腱腴腹腺腻腼腽腾腿膀膂膈膊膏膑膘膛膜膝膣膦膨膪膳膺膻臀臁臂臃臆臊臌臣臧自臬臭至致臻臼臾 +舀舁舂舄舅舆舌舍舐舒舔舛舜舞舟舡舢舣舨航舫般舭舯舰舱舳舴舵舶舷舸船舻舾艄艇艉艋艏艘艚艟艨艮良艰色艳艴艹艺艽艾艿节芄芈芊芋芍芎芏芑 +芒芗芘芙芜芝芟芡芤芥芦芨芩芪芫芬芭芮芯芰花芳芴芷芸芹芽芾苁苄苇苈苊苋苌苍苎苏苑苒苓苔苕苗苘苛苜苞苟苠苡苣苤若苦苫苯英苴苷苹苻茁茂 +范茄茅茆茇茈茉茌茎茏茑茔茕茗茚茛茜茧茨茫茬茭茯茱茳茴茵茶茸茹茺茼荀荃荆荇草荏荐荑荒荔荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药荷荸 +荻荼荽莅莆莉莎莒莓莘莛莜莞莠莨莩莪莫莰莱莲莳莴莶获莸莹莺莼莽菀菁菅菇菊菌菏菔菖菘菜菝菟菠菡菥菩菪菰菱菲菸菹菽萁萃萄萆萋萌萍萎萏萑 +萘萜萝萤营萦萧萨萱萸萼落葆葑著葙葚葛葜葡董葩葫葬葭葱葳葵葶葸葺蒂蒇蒈蒉蒋蒌蒎蒗蒙蒜蒡蒯蒲蒴蒸蒹蒺蒽蒿蓁蓄蓉蓊蓍蓐蓑蓓蓖蓝蓟蓠蓣蓥 +蓦蓬蓰蓼蓿蔌蔑蔓蔗蔚蔟蔡蔫蔬蔷蔸蔹蔺蔻蔼蔽蕃蕈蕉蕊蕖蕙蕞蕤蕨蕲蕴蕹蕺蕻蕾薄薅薇薏薛薜薤薨薪薮薯薰薷薹藁藉藏藐藓藕藜藤藩藻藿蘅蘑蘖 +蘧蘩蘸蘼虍虎虏虐虑虔虚虞虢虫虬虮虱虹虺虻虼虽虾虿蚀蚁蚂蚊蚋蚌蚍蚓蚕蚜蚝蚣蚤蚧蚨蚩蚪蚬蚯蚰蚱蚴蚵蚶蚺蛀蛄蛆蛇蛉蛊蛋蛎蛏蛐蛑蛔蛘蛙蛛 +蛞蛟蛤蛩蛭蛮蛰蛱蛲蛳蛴蛸蛹蛾蜀蜂蜃蜇蜈蜉蜊蜍蜒蜓蜕蜗蜘蜚蜜蜞蜡蜢蜣蜥蜩蜮蜱蜴蜷蜻蜾蜿蝇蝈蝉蝌蝎蝓蝗蝙蝠蝣蝤蝥蝮蝰蝴蝶蝻蝼蝽蝾螂螃 +螅螈螋融螓螗螟螨螫螬螭螯螳螵螺螽蟀蟆蟊蟋蟑蟒蟓蟛蟠蟥蟪蟮蟹蟾蠃蠊蠓蠕蠖蠛蠡蠢蠲蠹蠼血衄衅行衍衔街衙衡衢衣衤补表衩衫衬衮衰衲衷衽衾 +衿袁袂袄袅袈袋袍袒袖袜袢袤被袭袱袷袼裁裂装裆裉裎裒裔裕裘裙裟裢裣裤裥裨裰裱裳裴裸裹裼裾褂褊褐褒褓褙褚褛褡褥褪褫褰褴褶襁襄襞襟襦襻 +西要覃覆见观规觅视觇览觉觊觋觌觎觏觐觑角觖觚觜觞解觥触觫觯觳言訇訾詈詹誉誊誓謇謦警譬讠计订讣认讥讦讧讨让讪讫训议讯记讲讳讴讵讶讷 +许讹论讼讽设访诀证诂诃评诅识诈诉诊诋诌词诎诏译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诫诬语诮误诰诱诲诳说诵诶请诸诹诺读 +诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谘谙谚谛谜谝谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豁豆豇豉豌豕 +豚象豢豪豫豳豸豹豺貂貅貉貊貌貔貘贝贞负贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍 +赎赏赐赓赔赕赖赘赙赚赛赜赝赞赠赡赢赣赤赦赧赫赭走赳赴赵赶起趁趄超越趋趑趔趟趣趱足趴趵趸趺趼趾趿跃跄跆跋跌跎跏跑跖跗跚跛距跞跟跣跤 +跨跪跫跬路跳践跷跸跹跺跻跽踅踉踊踌踏踔踝踞踟踢踣踩踪踬踮踯踱踵踹踺踽蹀蹁蹂蹄蹇蹈蹉蹊蹋蹑蹒蹙蹦蹩蹬蹭蹯蹰蹲蹴蹶蹼蹿躁躅躇躏躐躔躜 +躞身躬躯躲躺軎车轧轨轩轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辁辂较辄辅辆辇辈辉辊辋辍辎辏辐辑输辔辕辖辗辘辙辚辛辜辞辟辣辨辩辫 +辰辱辶边辽达迁迂迄迅过迈迎运近迓返迕还这进远违连迟迢迤迥迦迨迩迪迫迭迮述迳迷迸迹追退送适逃逄逅逆选逊逋逍透逐逑递途逖逗通逛逝逞速 +造逡逢逦逭逮逯逵逶逸逻逼逾遁遂遄遇遍遏遐遑遒道遗遘遛遢遣遥遨遭遮遴遵遽避邀邂邃邈邋邑邓邕邗邙邛邝邡邢那邦邪邬邮邯邰邱邳邴邵邶邸邹 +邺邻邾郁郄郅郇郊郎郏郐郑郓郗郛郜郝郡郢郦郧部郫郭郯郴郸都郾鄂鄄鄙鄞鄢鄣鄯鄱鄹酃酆酉酊酋酌配酎酏酐酒酗酚酝酞酡酢酣酤酥酩酪酬酮酯酰 +酱酲酴酵酶酷酸酹酽酾酿醅醇醉醋醌醍醐醑醒醚醛醢醣醪醭醮醯醴醵醺采釉释里重野量金釜鉴銎銮鋈錾鍪鎏鏊鏖鐾鑫钅钆钇针钉钊钋钌钍钎钏钐钒 +钓钔钕钗钙钚钛钜钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钶钷钸钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铌铍铎铐铑铒铕铖铗铘 +铙铛铜铝铞铟铠铡铢铣铤铥铧铨铩铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐锑锒锓锔锕锖锗锘错锚锛 +锝锞锟锡锢锣锤锥锦锨锩锪锫锬锭键锯锰锱锲锴锵锶锷锸锹锺锻锼锾锿镀镁镂镄镅镆镇镉镊镌镍镎镏镐镑镒镓镔镖镗镘镙镛镜镝镞镟镡镢镣镤镥镦 +镧镨镩镪镫镬镭镯镰镱镲镳镶长门闩闪闫闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼闽闾阀阁阂阃阄阅阆阈阉阊阋阌阍阎阏阐阑阒阔阕阖阗阙阚阜阝队阡 +阢阪阮阱防阳阴阵阶阻阼阽阿陀陂附际陆陇陈陉陋陌降限陔陕陛陟陡院除陧陨险陪陬陲陴陵陶陷隅隆隈隋隍随隐隔隗隘隙障隧隰隳隶隹隼隽难雀雁 +雄雅集雇雉雌雍雎雏雒雕雠雨雩雪雯雳零雷雹雾需霁霄霆震霈霉霍霎霏霓霖霜霞霪霭霰露霸霹霾青靓靖静靛非靠靡面靥革靳靴靶靼鞅鞋鞍鞑鞒鞔鞘 +鞠鞣鞫鞭鞯鞲鞴韦韧韩韪韫韬韭音韵韶页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颌颍颏颐频颓颔颖颗题颚颛颜额颞颟颠颡颢颤颥颦颧风飑 +飒飓飕飘飙飚飞食飧飨餍餐餮饔饕饣饥饧饨饩饪饫饬饭饮饯饰饱饲饴饵饶饷饺饼饽饿馀馁馄馅馆馇馈馊馋馍馏馐馑馒馓馔馕首馗馘香馥馨马驭驮驯 +驰驱驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骄骅骆骇骈骊骋验骏骐骑骒骓骖骗骘骚骛骜骝骞骟骠骡骢骣骤骥骧骨骰骱骶骷骸骺骼髀髁髂髅髋髌髑髓高 +髟髡髦髫髭髯髹髻鬃鬈鬏鬓鬟鬣鬯鬲鬻鬼魁魂魃魄魅魇魈魉魍魏魑魔鱼鱿鲁鲂鲅鲆鲇鲈鲋鲍鲎鲐鲑鲒鲔鲕鲚鲛鲜鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲫鲭鲮 +鲰鲱鲲鲳鲴鲵鲶鲷鲸鲺鲻鲼鲽鳃鳄鳅鳆鳇鳊鳋鳌鳍鳎鳏鳐鳓鳔鳕鳖鳗鳘鳙鳜鳝鳞鳟鳢鸟鸠鸡鸢鸣鸥鸦鸨鸩鸪鸫鸬鸭鸯鸱鸲鸳鸵鸶鸷鸸鸹鸺鸽鸾鸿鹁 +鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹎鹏鹑鹕鹗鹘鹚鹛鹜鹞鹣鹤鹦鹧鹨鹩鹪鹫鹬鹭鹰鹱鹳鹾鹿麂麇麈麋麒麓麝麟麦麴麸麻麽麾黄黉黍黎黏黑黔默黛黜黝黟黠黢 +黥黧黩黪黯黹黻黼黾鼋鼍鼎鼐鼓鼗鼙鼠鼢鼬鼯鼷鼹鼻鼽鼾齄齐齑齿龀龃龄龅龆龇龈龉龊龋龌龙龚龛龟龠 diff --git a/packs/zh-Hans/fonts/zh-hans-cjk/manifest.ini b/packs/zh-Hans/fonts/zh-hans-cjk/manifest.ini new file mode 100644 index 00000000..a4e9fdd4 --- /dev/null +++ b/packs/zh-Hans/fonts/zh-hans-cjk/manifest.ini @@ -0,0 +1,8 @@ +kind=font +id=zh-hans-cjk +display_name=Simplified Chinese CJK +usage=both +estimated_ram_bytes=476075 +source=binfont +file=font.bin +ranges=ranges.txt diff --git a/packs/zh-Hans/fonts/zh-hans-cjk/ranges.txt b/packs/zh-Hans/fonts/zh-hans-cjk/ranges.txt new file mode 100644 index 00000000..682b5711 --- /dev/null +++ b/packs/zh-Hans/fonts/zh-hans-cjk/ranges.txt @@ -0,0 +1 @@ +0xB0,0x3002,0x4E00-0x4E01,0x4E03,0x4E07-0x4E0E,0x4E10-0x4E11,0x4E13-0x4E16,0x4E18-0x4E1E,0x4E22,0x4E24-0x4E25,0x4E27-0x4E28,0x4E2A-0x4E2D,0x4E30,0x4E32,0x4E34,0x4E36,0x4E38-0x4E3B,0x4E3D-0x4E3F,0x4E43,0x4E45,0x4E47-0x4E49,0x4E4B-0x4E50,0x4E52-0x4E54,0x4E56,0x4E58-0x4E59,0x4E5C-0x4E61,0x4E66,0x4E69,0x4E70-0x4E71,0x4E73,0x4E7E,0x4E86,0x4E88-0x4E89,0x4E8B-0x4E8F,0x4E91-0x4E95,0x4E98,0x4E9A-0x4E9B,0x4E9F-0x4EA2,0x4EA4-0x4EA9,0x4EAB-0x4EAE,0x4EB2-0x4EB3,0x4EB5,0x4EBA-0x4EBB,0x4EBF-0x4EC7,0x4EC9-0x4ECB,0x4ECD-0x4ECE,0x4ED1,0x4ED3-0x4ED9,0x4EDD-0x4EDF,0x4EE1,0x4EE3-0x4EE5,0x4EE8,0x4EEA-0x4EEC,0x4EF0,0x4EF2-0x4EF3,0x4EF5-0x4EF7,0x4EFB,0x4EFD,0x4EFF,0x4F01,0x4F09-0x4F0A,0x4F0D-0x4F11,0x4F17-0x4F1B,0x4F1E-0x4F20,0x4F22,0x4F24-0x4F27,0x4F2A-0x4F2B,0x4F2F-0x4F30,0x4F32,0x4F34,0x4F36,0x4F38,0x4F3A,0x4F3C-0x4F3D,0x4F43,0x4F46,0x4F4D-0x4F51,0x4F53,0x4F55,0x4F57-0x4F60,0x4F63-0x4F65,0x4F67,0x4F69,0x4F6C,0x4F6F-0x4F70,0x4F73-0x4F74,0x4F76,0x4F7B-0x4F7C,0x4F7E-0x4F7F,0x4F83-0x4F84,0x4F88-0x4F89,0x4F8B,0x4F8D,0x4F8F,0x4F91,0x4F94,0x4F97,0x4F9B,0x4F9D,0x4FA0,0x4FA3,0x4FA5-0x4FAA,0x4FAC,0x4FAE-0x4FAF,0x4FB5,0x4FBF,0x4FC3-0x4FC5,0x4FCA,0x4FCE-0x4FD1,0x4FD7-0x4FD8,0x4FDA,0x4FDC-0x4FDF,0x4FE1,0x4FE3,0x4FE6,0x4FE8-0x4FEA,0x4FED-0x4FEF,0x4FF1,0x4FF3,0x4FF8,0x4FFA,0x4FFE,0x500C-0x500D,0x500F,0x5012,0x5014,0x5018-0x501A,0x501C,0x501F,0x5021,0x5025-0x5026,0x5028-0x502A,0x502C-0x502E,0x503A,0x503C,0x503E,0x5043,0x5047-0x5048,0x504C,0x504E-0x504F,0x5055,0x505A,0x505C,0x5065,0x506C,0x5076-0x5077,0x507B,0x507E-0x5080,0x5085,0x5088,0x508D,0x50A3,0x50A5,0x50A7-0x50A9,0x50AC,0x50B2,0x50BA-0x50BB,0x50CF,0x50D6,0x50DA,0x50E6-0x50E7,0x50EC-0x50EE,0x50F3,0x50F5,0x50FB,0x5106-0x5107,0x510B,0x5112,0x5121,0x513F-0x5141,0x5143-0x5146,0x5148-0x5149,0x514B,0x514D,0x5151,0x5154-0x5156,0x515A,0x515C,0x5162,0x5165,0x5168,0x516B-0x516E,0x5170-0x5171,0x5173-0x5179,0x517B-0x517D,0x5180-0x5182,0x5185,0x5188-0x5189,0x518C-0x518D,0x5192,0x5195-0x5197,0x5199,0x519B-0x519C,0x51A0,0x51A2,0x51A4-0x51A5,0x51AB-0x51AC,0x51AF-0x51B3,0x51B5-0x51B7,0x51BB-0x51BD,0x51C0,0x51C4,0x51C6-0x51C7,0x51C9,0x51CB-0x51CC,0x51CF,0x51D1,0x51DB,0x51DD,0x51E0-0x51E1,0x51E4,0x51EB,0x51ED,0x51EF-0x51F0,0x51F3,0x51F5-0x51F6,0x51F8-0x51FD,0x51FF-0x5203,0x5206-0x5208,0x520A,0x520D-0x520E,0x5211-0x5212,0x5216-0x521B,0x521D,0x5220,0x5224,0x5228-0x5229,0x522B,0x522D-0x522E,0x5230,0x5233,0x5236-0x523B,0x523D,0x523F-0x5243,0x524A,0x524C-0x524D,0x5250-0x5251,0x5254,0x5256,0x525C,0x525E,0x5261,0x5265,0x5267,0x5269-0x526A,0x526F,0x5272,0x527D,0x527F,0x5281-0x5282,0x5288,0x5290,0x5293,0x529B,0x529D-0x52A3,0x52A8-0x52AD,0x52B1-0x52B3,0x52BE-0x52BF,0x52C3,0x52C7,0x52C9,0x52CB,0x52D0,0x52D2,0x52D6,0x52D8,0x52DF,0x52E4,0x52F0,0x52F9-0x52FA,0x52FE-0x5300,0x5305-0x5306,0x5308,0x530D,0x530F-0x5310,0x5315-0x5317,0x5319-0x531A,0x531D,0x5320-0x5321,0x5323,0x5326,0x532A,0x532E,0x5339-0x533B,0x533E-0x533F,0x5341,0x5343,0x5345,0x5347-0x534A,0x534E-0x534F,0x5351-0x5353,0x5355-0x5357,0x535A,0x535C,0x535E-0x5364,0x5366-0x5367,0x5369,0x536B,0x536E-0x5371,0x5373-0x5375,0x5377-0x5378,0x537A,0x537F,0x5382,0x5384-0x5386,0x5389,0x538B-0x538D,0x5395,0x5398,0x539A,0x539D,0x539F,0x53A2-0x53A3,0x53A5-0x53A6,0x53A8-0x53A9,0x53AE,0x53B6,0x53BB,0x53BF,0x53C1-0x53C2,0x53C8-0x53CD,0x53D1,0x53D4,0x53D6-0x53D9,0x53DB,0x53DF-0x53E0,0x53E3-0x53E6,0x53E8-0x53F3,0x53F5-0x53F9,0x53FB-0x53FD,0x5401,0x5403-0x5404,0x5406,0x5408-0x540A,0x540C-0x5413,0x5415-0x5417,0x541B,0x541D-0x5421,0x5423,0x5426-0x5429,0x542B-0x542F,0x5431-0x5432,0x5434-0x5435,0x5438-0x5439,0x543B-0x543C,0x543E,0x5440,0x5443,0x5446,0x5448,0x544A-0x544B,0x5450,0x5452-0x5459,0x545B-0x545C,0x5462,0x5464,0x5466,0x5468,0x5471-0x5473,0x5475-0x5478,0x547B-0x547D,0x5480,0x5482,0x5484,0x5486,0x548B-0x548C,0x548E-0x5490,0x5492,0x5494-0x5496,0x5499-0x549B,0x549D,0x54A3-0x54A4,0x54A6-0x54AD,0x54AF,0x54B1,0x54B3-0x54B4,0x54B8,0x54BB,0x54BD,0x54BF-0x54C2,0x54C4,0x54C6-0x54C9,0x54CC-0x54D5,0x54D7,0x54D9-0x54DA,0x54DC-0x54DF,0x54E5-0x54EA,0x54ED-0x54EE,0x54F2-0x54F3,0x54FA,0x54FC-0x54FD,0x54FF,0x5501,0x5506-0x5507,0x5509,0x550F-0x5511,0x5514,0x551B,0x5520,0x5522-0x5524,0x5527,0x552A,0x552C,0x552E-0x5531,0x5533,0x5537,0x553C,0x553E-0x553F,0x5541,0x5543-0x5544,0x5546,0x5549-0x554A,0x5550,0x5555-0x5556,0x555C,0x5561,0x5564-0x5567,0x556A,0x556C-0x556E,0x5575-0x5578,0x557B-0x557C,0x557E,0x5580-0x5584,0x5587-0x558B,0x558F,0x5591,0x5594,0x5598-0x5599,0x559C-0x559D,0x559F,0x55A7,0x55B1,0x55B3,0x55B5,0x55B7,0x55B9,0x55BB,0x55BD-0x55BE,0x55C4-0x55C5,0x55C9,0x55CC-0x55CD,0x55D1-0x55D4,0x55D6,0x55DC-0x55DD,0x55DF,0x55E1,0x55E3-0x55E6,0x55E8,0x55EA-0x55EC,0x55EF,0x55F2-0x55F3,0x55F5,0x55F7,0x55FD-0x55FE,0x5600-0x5601,0x5608-0x5609,0x560C,0x560E-0x560F,0x5618,0x561B,0x561E-0x561F,0x5623-0x5624,0x5627,0x562C-0x562D,0x5631-0x5632,0x5634,0x5636,0x5639,0x563B,0x563F,0x564C-0x564E,0x5654,0x5657-0x5659,0x565C,0x5662,0x5664,0x5668-0x566C,0x5671,0x5676,0x567B-0x567C,0x5685-0x5686,0x568E-0x568F,0x5693,0x56A3,0x56AF,0x56B7,0x56BC,0x56CA,0x56D4,0x56D7,0x56DA-0x56DB,0x56DD-0x56E2,0x56E4,0x56EB,0x56ED,0x56F0-0x56F1,0x56F4-0x56F5,0x56F9-0x56FA,0x56FD-0x56FF,0x5703-0x5704,0x5706,0x5708-0x570A,0x571C,0x571F,0x5723,0x5728-0x572A,0x572C-0x5730,0x5733,0x5739-0x573B,0x573E,0x5740,0x5742,0x5747,0x574A,0x574C-0x5751,0x5757,0x575A-0x5761,0x5764,0x5766,0x5768-0x576B,0x576D,0x576F,0x5773,0x5776-0x5777,0x577B-0x577C,0x5782-0x5786,0x578B-0x578C,0x5792-0x5793,0x579B,0x57A0-0x57A4,0x57A6-0x57A7,0x57A9,0x57AB,0x57AD-0x57AE,0x57B2,0x57B4,0x57B8,0x57C2-0x57C3,0x57CB,0x57CE-0x57CF,0x57D2,0x57D4-0x57D5,0x57D8-0x57DA,0x57DD,0x57DF-0x57E0,0x57E4,0x57ED,0x57EF,0x57F4,0x57F8-0x57FA,0x57FD,0x5800,0x5802,0x5806-0x5807,0x580B,0x580D,0x5811,0x5815,0x5819,0x581E,0x5820-0x5821,0x5824,0x582A,0x5830,0x5835,0x5844,0x584C-0x584D,0x5851,0x5854,0x5858,0x585E,0x5865,0x586B-0x586C,0x587E,0x5880-0x5881,0x5883,0x5885,0x5889,0x5892-0x5893,0x5899-0x589A,0x589E-0x589F,0x58A8-0x58A9,0x58BC,0x58C1,0x58C5,0x58D1,0x58D5,0x58E4,0x58EB-0x58EC,0x58EE,0x58F0,0x58F3,0x58F6,0x58F9,0x5902,0x5904,0x5907,0x590D,0x590F,0x5914-0x5916,0x5919-0x591A,0x591C,0x591F,0x5924-0x5925,0x5927,0x5929-0x592B,0x592D-0x592F,0x5931,0x5934,0x5937-0x593A,0x593C,0x5941-0x5942,0x5944,0x5947-0x5949,0x594B,0x594E-0x594F,0x5951,0x5954-0x5958,0x595A,0x5960,0x5962,0x5965,0x5973-0x5974,0x5976,0x5978-0x5979,0x597D,0x5981-0x5984,0x5986-0x5988,0x598A,0x598D,0x5992-0x5993,0x5996-0x5997,0x5999,0x599E,0x59A3-0x59A5,0x59A8-0x59AB,0x59AE-0x59AF,0x59B2,0x59B9,0x59BB,0x59BE,0x59C6,0x59CA-0x59CB,0x59D0-0x59D4,0x59D7-0x59D8,0x59DA,0x59DC-0x59DD,0x59E3,0x59E5,0x59E8,0x59EC,0x59F9,0x59FB,0x59FF,0x5A01,0x5A03-0x5A09,0x5A0C,0x5A11,0x5A13,0x5A18,0x5A1C,0x5A1F-0x5A20,0x5A23,0x5A25,0x5A29,0x5A31-0x5A32,0x5A34,0x5A36,0x5A3C,0x5A40,0x5A46,0x5A49-0x5A4A,0x5A55,0x5A5A,0x5A62,0x5A67,0x5A6A,0x5A74-0x5A77,0x5A7A,0x5A7F,0x5A92,0x5A9A-0x5A9B,0x5AAA,0x5AB2-0x5AB3,0x5AB5,0x5AB8,0x5ABE,0x5AC1-0x5AC2,0x5AC9,0x5ACC,0x5AD2,0x5AD4,0x5AD6,0x5AD8,0x5ADC,0x5AE0-0x5AE1,0x5AE3,0x5AE6,0x5AE9,0x5AEB,0x5AF1,0x5B09,0x5B16-0x5B17,0x5B32,0x5B34,0x5B37,0x5B40,0x5B50-0x5B51,0x5B53-0x5B55,0x5B57-0x5B5D,0x5B5F,0x5B62-0x5B66,0x5B69-0x5B6A,0x5B6C,0x5B70-0x5B71,0x5B73,0x5B75,0x5B7A,0x5B7D,0x5B80-0x5B81,0x5B83-0x5B85,0x5B87-0x5B89,0x5B8B-0x5B8C,0x5B8F,0x5B93,0x5B95,0x5B97-0x5B9E,0x5BA0-0x5BA6,0x5BAA-0x5BAB,0x5BB0,0x5BB3-0x5BB6,0x5BB8-0x5BB9,0x5BBD-0x5BBF,0x5BC2,0x5BC4-0x5BC7,0x5BCC,0x5BD0,0x5BD2-0x5BD3,0x5BDD-0x5BDF,0x5BE1,0x5BE4-0x5BE5,0x5BE8,0x5BEE,0x5BF0,0x5BF8-0x5BFC,0x5BFF,0x5C01,0x5C04,0x5C06,0x5C09-0x5C0A,0x5C0F,0x5C11,0x5C14-0x5C16,0x5C18,0x5C1A,0x5C1C-0x5C1D,0x5C22,0x5C24-0x5C25,0x5C27,0x5C2C,0x5C31,0x5C34,0x5C38-0x5C42,0x5C45,0x5C48-0x5C4B,0x5C4E-0x5C51,0x5C55,0x5C59,0x5C5E,0x5C60-0x5C61,0x5C63,0x5C65-0x5C66,0x5C6E-0x5C6F,0x5C71,0x5C79-0x5C7A,0x5C7F,0x5C81-0x5C82,0x5C88,0x5C8C-0x5C8D,0x5C90-0x5C91,0x5C94,0x5C96-0x5C9C,0x5CA2-0x5CA3,0x5CA9,0x5CAB-0x5CAD,0x5CB1,0x5CB3,0x5CB5,0x5CB7-0x5CB8,0x5CBD,0x5CBF,0x5CC1,0x5CC4,0x5CCB,0x5CD2,0x5CD9,0x5CE1,0x5CE4-0x5CE6,0x5CE8,0x5CEA,0x5CED,0x5CF0,0x5CFB,0x5D02-0x5D03,0x5D06-0x5D07,0x5D0E,0x5D14,0x5D16,0x5D1B,0x5D1E,0x5D24,0x5D26-0x5D27,0x5D29,0x5D2D-0x5D2E,0x5D34,0x5D3D-0x5D3E,0x5D47,0x5D4A-0x5D4C,0x5D58,0x5D5B,0x5D5D,0x5D69,0x5D6B-0x5D6C,0x5D6F,0x5D74,0x5D82,0x5D99,0x5D9D,0x5DB7,0x5DC5,0x5DCD,0x5DDB,0x5DDD-0x5DDE,0x5DE1-0x5DE2,0x5DE5-0x5DE9,0x5DEB,0x5DEE-0x5DEF,0x5DF1-0x5DF4,0x5DF7,0x5DFD-0x5DFE,0x5E01-0x5E03,0x5E05-0x5E06,0x5E08,0x5E0C,0x5E0F-0x5E11,0x5E14-0x5E16,0x5E18-0x5E1D,0x5E26-0x5E27,0x5E2D-0x5E2E,0x5E31,0x5E37-0x5E38,0x5E3B-0x5E3D,0x5E42,0x5E44-0x5E45,0x5E4C,0x5E54-0x5E55,0x5E5B,0x5E5E,0x5E61-0x5E62,0x5E72-0x5E74,0x5E76,0x5E78,0x5E7A-0x5E7D,0x5E7F-0x5E80,0x5E84,0x5E86-0x5E87,0x5E8A-0x5E8B,0x5E8F-0x5E91,0x5E93-0x5E97,0x5E99-0x5E9A,0x5E9C,0x5E9E-0x5EA0,0x5EA5-0x5EA7,0x5EAD,0x5EB3,0x5EB5-0x5EB9,0x5EBE,0x5EC9-0x5ECA,0x5ED1-0x5ED3,0x5ED6,0x5EDB,0x5EE8,0x5EEA,0x5EF4,0x5EF6-0x5EF7,0x5EFA,0x5EFE-0x5F04,0x5F08,0x5F0A-0x5F0B,0x5F0F,0x5F11,0x5F13,0x5F15,0x5F17-0x5F18,0x5F1B,0x5F1F-0x5F20,0x5F25-0x5F27,0x5F29-0x5F2A,0x5F2D,0x5F2F,0x5F31,0x5F39-0x5F3A,0x5F3C,0x5F40,0x5F50,0x5F52-0x5F53,0x5F55-0x5F58,0x5F5D,0x5F61-0x5F62,0x5F64,0x5F66,0x5F69-0x5F6A,0x5F6C-0x5F6D,0x5F70-0x5F71,0x5F73,0x5F77,0x5F79,0x5F7B-0x5F7C,0x5F80-0x5F82,0x5F84-0x5F85,0x5F87-0x5F8C,0x5F90,0x5F92,0x5F95,0x5F97-0x5F99,0x5F9C,0x5FA1,0x5FA8,0x5FAA,0x5FAD-0x5FAE,0x5FB5,0x5FB7,0x5FBC-0x5FBD,0x5FC3-0x5FC6,0x5FC9,0x5FCC-0x5FCD,0x5FCF-0x5FD2,0x5FD6-0x5FD9,0x5FDD,0x5FE0-0x5FE1,0x5FE4,0x5FE7,0x5FEA-0x5FEB,0x5FED-0x5FEE,0x5FF1,0x5FF5,0x5FF8,0x5FFB,0x5FFD-0x6006,0x600A,0x600D-0x600F,0x6012,0x6014-0x6016,0x6019,0x601B-0x601D,0x6020-0x6021,0x6025-0x602B,0x602F,0x6035,0x603B-0x603C,0x603F,0x6041-0x6043,0x604B,0x604D,0x6050,0x6052,0x6055,0x6059-0x605A,0x605D,0x6062-0x6064,0x6067-0x606D,0x606F-0x6070,0x6073,0x6076,0x6078-0x607D,0x607F,0x6083-0x6084,0x6089,0x608C-0x608D,0x6092,0x6094,0x6096,0x609A-0x609B,0x609D,0x609F-0x60A0,0x60A3,0x60A6,0x60A8,0x60AB-0x60AD,0x60AF,0x60B1-0x60B2,0x60B4,0x60B8,0x60BB-0x60BC,0x60C5-0x60C6,0x60CA-0x60CB,0x60D1,0x60D5,0x60D8,0x60DA,0x60DC-0x60DD,0x60DF-0x60E0,0x60E6-0x60E9,0x60EB-0x60F0,0x60F3-0x60F4,0x60F6,0x60F9-0x60FA,0x6100-0x6101,0x6106,0x6108-0x6109,0x610D-0x610F,0x6115,0x611A,0x611F-0x6120,0x6123-0x6124,0x6126-0x6127,0x612B,0x613F,0x6148,0x614A,0x614C,0x614E,0x6151,0x6155,0x615D,0x6162,0x6167-0x6168,0x6170,0x6175,0x6177,0x618B,0x618E,0x6194,0x619D,0x61A7-0x61A9,0x61AC,0x61B7,0x61BE,0x61C2,0x61C8,0x61CA-0x61CB,0x61D1-0x61D2,0x61D4,0x61E6,0x61F5,0x61FF,0x6206,0x6208,0x620A-0x6212,0x6215-0x6218,0x621A-0x621B,0x621F,0x6221-0x6222,0x6224-0x6225,0x622A,0x622C,0x622E,0x6233-0x6234,0x6237,0x623D-0x6241,0x6243,0x6247-0x6249,0x624B-0x624E,0x6251-0x6254,0x6258,0x625B,0x6263,0x6266-0x6267,0x6269-0x6270,0x6273,0x6276,0x6279,0x627C,0x627E-0x6280,0x6284,0x6289-0x628A,0x6291-0x6293,0x6295-0x6298,0x629A-0x629B,0x629F-0x62A2,0x62A4-0x62A5,0x62A8,0x62AB-0x62AC,0x62B1,0x62B5,0x62B9,0x62BB-0x62BD,0x62BF,0x62C2,0x62C4-0x62CA,0x62CC-0x62CE,0x62D0,0x62D2-0x62D4,0x62D6-0x62DC,0x62DF,0x62E2-0x62E3,0x62E5-0x62E9,0x62EC-0x62EF,0x62F1,0x62F3-0x62F4,0x62F6-0x62F7,0x62FC-0x62FF,0x6301-0x6302,0x6307-0x6309,0x630E,0x6311,0x6316,0x631A-0x631B,0x631D-0x6325,0x6328,0x632A-0x632B,0x632F,0x6332,0x6339-0x633A,0x633D,0x6342-0x6343,0x6345-0x6346,0x6349,0x634B-0x6350,0x6355,0x635E-0x635F,0x6361-0x6363,0x6367,0x6369,0x636D-0x636E,0x6371,0x6376-0x6377,0x637A-0x637B,0x6380,0x6382,0x6387-0x638A,0x638C,0x638E-0x6390,0x6392,0x6396,0x6398,0x63A0,0x63A2-0x63A3,0x63A5,0x63A7-0x63AA,0x63AC-0x63AE,0x63B0,0x63B3-0x63B4,0x63B7-0x63B8,0x63BA,0x63BC,0x63BE,0x63C4,0x63C6,0x63C9,0x63CD-0x63D0,0x63D2,0x63D6,0x63DE,0x63E0-0x63E1,0x63E3,0x63E9-0x63EA,0x63ED,0x63F2,0x63F4,0x63F6,0x63F8,0x63FD,0x63FF-0x6402,0x6405,0x640B-0x640C,0x640F-0x6410,0x6413-0x6414,0x641B-0x641C,0x641E,0x6420-0x6421,0x6426,0x642A,0x642C-0x642D,0x6434,0x643A,0x643D,0x643F,0x6441,0x6444-0x6448,0x644A,0x6452,0x6454,0x6458,0x645E,0x6467,0x6469,0x646D,0x6478-0x647A,0x6482,0x6484-0x6485,0x6487,0x6491-0x6492,0x6495-0x6496,0x6499,0x649E,0x64A4,0x64A9,0x64AC-0x64AE,0x64B0,0x64B5,0x64B7-0x64B8,0x64BA,0x64BC,0x64C0,0x64C2,0x64C5,0x64CD-0x64CE,0x64D0,0x64D2,0x64D7-0x64D8,0x64DE,0x64E2,0x64E4,0x64E6,0x6500,0x6509,0x6512,0x6518,0x6525,0x652B,0x652E-0x652F,0x6534-0x6536,0x6538-0x6539,0x653B,0x653E-0x653F,0x6545,0x6548-0x6549,0x654C,0x654F,0x6551,0x6555-0x6556,0x6559,0x655B,0x655D-0x655E,0x6562-0x6563,0x6566,0x656B-0x656C,0x6570,0x6572,0x6574,0x6577,0x6587,0x658B-0x658C,0x6590-0x6591,0x6593,0x6597,0x6599,0x659B-0x659C,0x659F,0x65A1,0x65A4-0x65A5,0x65A7,0x65A9,0x65AB,0x65AD,0x65AF-0x65B0,0x65B9,0x65BC-0x65BD,0x65C1,0x65C3-0x65C6,0x65CB-0x65CC,0x65CE-0x65CF,0x65D2,0x65D6-0x65D7,0x65E0,0x65E2,0x65E5-0x65E9,0x65EC-0x65F1,0x65F6-0x65F7,0x65FA,0x6600,0x6602-0x6603,0x6606,0x660A,0x660C,0x660E-0x660F,0x6613-0x6615,0x6619,0x661D,0x661F-0x6620,0x6625,0x6627-0x6628,0x662D,0x662F,0x6631,0x6634-0x6636,0x663C,0x663E,0x6641,0x6643,0x664B-0x664C,0x664F,0x6652-0x6657,0x665A,0x665F,0x6661,0x6664,0x6666,0x6668,0x666E-0x6670,0x6674,0x6676-0x6677,0x667A,0x667E,0x6682,0x6684,0x6687,0x668C,0x6691,0x6696-0x6697,0x669D,0x66A7-0x66A8,0x66AE,0x66B4,0x66B9,0x66BE,0x66D9,0x66DB-0x66DD,0x66E6,0x66E9,0x66F0,0x66F2-0x66F4,0x66F7,0x66F9,0x66FC,0x66FE-0x6700,0x6708-0x670B,0x670D,0x6710,0x6714-0x6715,0x6717,0x671B,0x671D,0x671F,0x6726,0x6728,0x672A-0x672D,0x672F,0x6731,0x6734-0x6735,0x673A,0x673D,0x6740,0x6742-0x6743,0x6746,0x6748-0x6749,0x674C,0x674E-0x6751,0x6753,0x6756,0x675C,0x675E-0x6761,0x6765,0x6768-0x676A,0x676D,0x676F-0x6770,0x6772-0x6773,0x6775,0x6777,0x677C,0x677E-0x677F,0x6781,0x6784,0x6787,0x6789,0x678B,0x6790,0x6795,0x6797-0x6798,0x679A,0x679C-0x679E,0x67A2-0x67A3,0x67A5,0x67A7-0x67A8,0x67AA-0x67AB,0x67AD,0x67AF-0x67B0,0x67B3,0x67B5-0x67B8,0x67C1,0x67C3-0x67C4,0x67CF-0x67D4,0x67D8-0x67DA,0x67DC-0x67DE,0x67E0,0x67E2,0x67E5,0x67E9,0x67EC,0x67EF-0x67F1,0x67F3-0x67F4,0x67FD,0x67FF-0x6800,0x6805,0x6807-0x680C,0x680E-0x680F,0x6811,0x6813,0x6816-0x6817,0x681D,0x6821,0x6829-0x682A,0x6832-0x6833,0x6837-0x6839,0x683C-0x683E,0x6840-0x6846,0x6848-0x684A,0x684C,0x684E,0x6850-0x6851,0x6853-0x6855,0x6860-0x6869,0x686B,0x6874,0x6876-0x6877,0x6881,0x6883,0x6885-0x6886,0x688F,0x6893,0x6897,0x68A2,0x68A6-0x68A8,0x68AD,0x68AF-0x68B0,0x68B3,0x68B5,0x68C0,0x68C2,0x68C9,0x68CB,0x68CD,0x68D2,0x68D5,0x68D8,0x68DA,0x68E0,0x68E3,0x68EE,0x68F0-0x68F1,0x68F5,0x68F9-0x68FA,0x68FC,0x6901,0x6905,0x690B,0x690D-0x690E,0x6910,0x6912,0x691F-0x6920,0x6924,0x692D,0x6930,0x6934,0x6939,0x693D,0x693F,0x6942,0x6954,0x6957,0x695A,0x695D-0x695E,0x6960,0x6963,0x6966,0x696B,0x696E,0x6971,0x6977-0x6979,0x697C,0x6980,0x6982,0x6984,0x6986-0x6989,0x698D,0x6994-0x6995,0x6998,0x699B-0x699C,0x69A7-0x69A8,0x69AB,0x69AD,0x69B1,0x69B4,0x69B7,0x69BB,0x69C1,0x69CA,0x69CC,0x69CE,0x69D0,0x69D4,0x69DB,0x69DF-0x69E0,0x69ED,0x69F2,0x69FD,0x69FF,0x6A0A,0x6A17-0x6A18,0x6A1F,0x6A21,0x6A28,0x6A2A,0x6A2F,0x6A31,0x6A35,0x6A3D-0x6A3E,0x6A44,0x6A47,0x6A50,0x6A58-0x6A59,0x6A5B,0x6A61,0x6A65,0x6A71,0x6A79,0x6A7C,0x6A80,0x6A84,0x6A8E,0x6A90-0x6A91,0x6A97,0x6AA0,0x6AA9,0x6AAB-0x6AAC,0x6B20-0x6B24,0x6B27,0x6B32,0x6B37,0x6B39-0x6B3A,0x6B3E,0x6B43,0x6B46-0x6B47,0x6B49,0x6B4C,0x6B59,0x6B62-0x6B67,0x6B6A,0x6B79,0x6B7B-0x6B7C,0x6B81-0x6B84,0x6B86-0x6B87,0x6B89-0x6B8B,0x6B8D,0x6B92-0x6B93,0x6B96,0x6B9A-0x6B9B,0x6BA1,0x6BAA,0x6BB3-0x6BB5,0x6BB7,0x6BBF,0x6BC1-0x6BC2,0x6BC5,0x6BCB,0x6BCD,0x6BCF,0x6BD2-0x6BD7,0x6BD9,0x6BDB,0x6BE1,0x6BEA-0x6BEB,0x6BEF,0x6BF3,0x6BF5,0x6BF9,0x6BFD,0x6C05-0x6C07,0x6C0D,0x6C0F-0x6C11,0x6C13-0x6C16,0x6C18-0x6C1B,0x6C1F,0x6C21-0x6C22,0x6C24,0x6C26-0x6C2A,0x6C2E-0x6C30,0x6C32,0x6C34-0x6C35,0x6C38,0x6C3D,0x6C40-0x6C42,0x6C46-0x6C47,0x6C49-0x6C4A,0x6C50,0x6C54-0x6C55,0x6C57,0x6C5B-0x6C61,0x6C64,0x6C68-0x6C6A,0x6C70,0x6C72,0x6C74,0x6C76,0x6C79,0x6C7D-0x6C7E,0x6C81-0x6C83,0x6C85-0x6C86,0x6C88-0x6C89,0x6C8C,0x6C8F-0x6C90,0x6C93-0x6C94,0x6C99,0x6C9B,0x6C9F,0x6CA1,0x6CA3-0x6CA7,0x6CA9-0x6CAB,0x6CAD-0x6CAE,0x6CB1-0x6CB3,0x6CB8-0x6CB9,0x6CBB-0x6CBF,0x6CC4-0x6CC5,0x6CC9-0x6CCA,0x6CCC,0x6CD0,0x6CD3-0x6CD7,0x6CDB,0x6CDE,0x6CE0-0x6CE3,0x6CE5,0x6CE8,0x6CEA-0x6CEB,0x6CEE-0x6CF1,0x6CF3,0x6CF5-0x6CF8,0x6CFA-0x6CFE,0x6D01,0x6D04,0x6D07,0x6D0B-0x6D0C,0x6D0E,0x6D12,0x6D17,0x6D19-0x6D1B,0x6D1E,0x6D25,0x6D27,0x6D2A-0x6D2B,0x6D2E,0x6D31-0x6D33,0x6D35,0x6D39,0x6D3B-0x6D3E,0x6D41,0x6D43,0x6D45-0x6D48,0x6D4A-0x6D4B,0x6D4D-0x6D4F,0x6D51-0x6D54,0x6D59-0x6D5A,0x6D5C,0x6D5E,0x6D60,0x6D63,0x6D66,0x6D69-0x6D6A,0x6D6E-0x6D6F,0x6D74,0x6D77-0x6D78,0x6D7C,0x6D82,0x6D85,0x6D88-0x6D89,0x6D8C,0x6D8E,0x6D91,0x6D93-0x6D95,0x6D9B,0x6D9D-0x6DA1,0x6DA3-0x6DA4,0x6DA6-0x6DAB,0x6DAE-0x6DAF,0x6DB2,0x6DB5,0x6DB8,0x6DBF-0x6DC0,0x6DC4-0x6DC7,0x6DCB-0x6DCC,0x6DD1,0x6DD6,0x6DD8-0x6DD9,0x6DDD-0x6DDE,0x6DE0-0x6DE1,0x6DE4,0x6DE6,0x6DEB-0x6DEC,0x6DEE,0x6DF1,0x6DF3,0x6DF7,0x6DF9,0x6DFB-0x6DFC,0x6E05,0x6E0A,0x6E0C-0x6E0E,0x6E10-0x6E11,0x6E14,0x6E16-0x6E17,0x6E1A,0x6E1D,0x6E20-0x6E21,0x6E23-0x6E25,0x6E29,0x6E2B,0x6E2D,0x6E2F,0x6E32,0x6E34,0x6E38,0x6E3A,0x6E43-0x6E44,0x6E4D-0x6E4E,0x6E53-0x6E54,0x6E56,0x6E58,0x6E5B,0x6E5F,0x6E6B,0x6E6E,0x6E7E-0x6E7F,0x6E83,0x6E85-0x6E86,0x6E89,0x6E8F-0x6E90,0x6E98,0x6E9C,0x6E9F,0x6EA2,0x6EA5,0x6EA7,0x6EAA,0x6EAF,0x6EB1-0x6EB2,0x6EB4,0x6EB6-0x6EB7,0x6EBA-0x6EBB,0x6EBD,0x6EC1-0x6EC2,0x6EC7,0x6ECB,0x6ECF,0x6ED1,0x6ED3-0x6ED5,0x6ED7,0x6EDA,0x6EDE-0x6EE2,0x6EE4-0x6EE6,0x6EE8-0x6EE9,0x6EF4,0x6EF9,0x6F02,0x6F06,0x6F09,0x6F0F,0x6F13-0x6F15,0x6F20,0x6F24,0x6F29-0x6F2B,0x6F2D,0x6F2F,0x6F31,0x6F33,0x6F36,0x6F3E,0x6F46-0x6F47,0x6F4B,0x6F4D,0x6F58,0x6F5C,0x6F5E,0x6F62,0x6F66,0x6F6D-0x6F6E,0x6F72,0x6F74,0x6F78,0x6F7A,0x6F7C,0x6F84,0x6F88-0x6F89,0x6F8C-0x6F8E,0x6F9C,0x6FA1,0x6FA7,0x6FB3,0x6FB6,0x6FB9,0x6FC0,0x6FC2,0x6FC9,0x6FD1-0x6FD2,0x6FDE,0x6FE0-0x6FE1,0x6FEE-0x6FEF,0x7011,0x701A-0x701B,0x7023,0x7035,0x7039,0x704C,0x704F,0x705E,0x706B-0x706D,0x706F-0x7070,0x7075-0x7076,0x7078,0x707C,0x707E-0x7080,0x7085,0x7089-0x708A,0x708E,0x7092,0x7094-0x7096,0x7099,0x709C-0x709D,0x70AB-0x70AF,0x70B1,0x70B3,0x70B7-0x70B9,0x70BB-0x70BD,0x70C0-0x70C3,0x70C8,0x70CA,0x70D8-0x70D9,0x70DB,0x70DF,0x70E4,0x70E6-0x70E9,0x70EB-0x70ED,0x70EF,0x70F7,0x70F9,0x70FD,0x7109-0x710A,0x7110,0x7113,0x7115-0x7116,0x7118-0x711A,0x7126,0x712F-0x7131,0x7136,0x7145,0x714A,0x714C,0x714E,0x715C,0x715E,0x7164,0x7166-0x7168,0x716E,0x7172-0x7173,0x7178,0x717A,0x717D,0x7184,0x718A,0x718F,0x7194,0x7198-0x7199,0x719F-0x71A0,0x71A8,0x71AC,0x71B3,0x71B5,0x71B9,0x71C3,0x71CE,0x71D4-0x71D5,0x71E0,0x71E5,0x71E7,0x71EE,0x71F9,0x7206,0x721D,0x7228,0x722A,0x722C,0x7230-0x7231,0x7235-0x7239,0x723B,0x723D,0x723F,0x7247-0x7248,0x724C-0x724D,0x7252,0x7256,0x7259,0x725B,0x725D,0x725F,0x7261-0x7262,0x7266-0x7267,0x7269,0x726E-0x726F,0x7272,0x7275,0x7279-0x727A,0x727E-0x7281,0x7284,0x728A-0x728B,0x728D,0x728F,0x7292,0x729F,0x72AC-0x72AD,0x72AF-0x72B0,0x72B4,0x72B6-0x72B9,0x72C1-0x72C4,0x72C8,0x72CD-0x72CE,0x72D0,0x72D2,0x72D7,0x72D9,0x72DE,0x72E0-0x72E1,0x72E8-0x72E9,0x72EC-0x72F4,0x72F7-0x72F8,0x72FA-0x72FC,0x7301,0x7303,0x730A,0x730E,0x7313,0x7315-0x7317,0x731B-0x731E,0x7321-0x7322,0x7325,0x7329-0x732C,0x732E,0x7331,0x7334,0x7337-0x7339,0x733E-0x733F,0x734D,0x7350,0x7352,0x7357,0x7360,0x736C-0x736D,0x736F,0x737E,0x7384,0x7387,0x7389,0x738B,0x738E,0x7391,0x7396,0x739B,0x739F,0x73A2,0x73A9,0x73AB,0x73AE-0x73B0,0x73B2-0x73B3,0x73B7,0x73BA-0x73BB,0x73C0,0x73C2,0x73C8-0x73CA,0x73CD,0x73CF-0x73D1,0x73D9,0x73DE,0x73E0,0x73E5,0x73E7,0x73E9,0x73ED,0x73F2,0x7403,0x7405-0x7406,0x7409-0x740A,0x740F-0x7410,0x741A-0x741B,0x7422,0x7425-0x7426,0x7428,0x742A,0x742C,0x742E,0x7430,0x7433-0x7436,0x743C,0x7441,0x7455,0x7457,0x7459-0x745C,0x745E-0x745F,0x746D,0x7470,0x7476-0x7477,0x747E,0x7480-0x7481,0x7483,0x7487,0x748B,0x748E,0x7490,0x749C,0x749E,0x74A7-0x74A9,0x74BA,0x74D2,0x74DC,0x74DE,0x74E0,0x74E2-0x74E4,0x74E6,0x74EE-0x74EF,0x74F4,0x74F6-0x74F7,0x74FF,0x7504,0x750D,0x750F,0x7511,0x7513,0x7518-0x751A,0x751C,0x751F,0x7525,0x7528-0x7529,0x752B-0x752D,0x752F-0x7533,0x7535,0x7537-0x7538,0x753A-0x753B,0x753E,0x7540,0x7545,0x7548,0x754B-0x754C,0x754E-0x754F,0x7554,0x7559-0x755C,0x7565-0x7566,0x756A,0x7572,0x7574,0x7578-0x7579,0x757F,0x7583,0x7586,0x758B,0x758F,0x7591-0x7592,0x7594,0x7596-0x7597,0x7599-0x759A,0x759D,0x759F-0x75A1,0x75A3-0x75A5,0x75AB-0x75AC,0x75AE-0x75B5,0x75B8-0x75B9,0x75BC-0x75BE,0x75C2-0x75C5,0x75C7-0x75CA,0x75CD,0x75D2,0x75D4-0x75D6,0x75D8,0x75DB,0x75DE,0x75E2-0x75E4,0x75E6-0x75E8,0x75EA-0x75EB,0x75F0-0x75F1,0x75F4,0x75F9,0x75FC,0x75FF-0x7601,0x7603,0x7605,0x760A,0x760C,0x7610,0x7615,0x7617-0x7619,0x761B,0x761F-0x7620,0x7622,0x7624-0x7626,0x7629-0x762B,0x762D,0x7630,0x7633-0x7635,0x7638,0x763C,0x763E-0x7640,0x7643,0x764C-0x764D,0x7654,0x7656,0x765C,0x765E,0x7663,0x766B,0x766F,0x7678,0x767B,0x767D-0x767E,0x7682,0x7684,0x7686-0x7688,0x768B,0x768E,0x7691,0x7693,0x7696,0x7699,0x76A4,0x76AE,0x76B1-0x76B2,0x76B4,0x76BF,0x76C2,0x76C5-0x76C6,0x76C8,0x76CA,0x76CD-0x76D2,0x76D4,0x76D6-0x76D8,0x76DB,0x76DF,0x76E5,0x76EE-0x76EF,0x76F1-0x76F2,0x76F4,0x76F8-0x76F9,0x76FC,0x76FE,0x7701,0x7704,0x7707-0x7709,0x770B,0x770D,0x7719-0x771A,0x771F-0x7720,0x7722,0x7726,0x7728-0x7729,0x772D,0x772F,0x7735-0x7738,0x773A,0x773C,0x7740-0x7741,0x7743,0x7747,0x7750-0x7751,0x775A-0x775B,0x7761-0x7763,0x7765-0x7766,0x7768,0x776B-0x776C,0x7779,0x777D-0x7780,0x7784-0x7785,0x778C-0x778E,0x7791-0x7792,0x779F-0x77A0,0x77A2,0x77A5,0x77A7,0x77A9-0x77AA,0x77AC,0x77B0,0x77B3,0x77B5,0x77BB,0x77BD,0x77BF,0x77CD,0x77D7,0x77DB-0x77DC,0x77E2-0x77E3,0x77E5,0x77E7,0x77E9,0x77EB-0x77EE,0x77F3,0x77F6,0x77F8,0x77FD-0x7802,0x7809,0x780C-0x780D,0x7811-0x7812,0x7814,0x7816-0x7818,0x781A,0x781C-0x781D,0x781F,0x7823,0x7825-0x7827,0x7829,0x782C-0x782D,0x7830,0x7834,0x7837-0x783C,0x783E,0x7840,0x7845,0x7847,0x784C,0x784E,0x7850,0x7852,0x7855-0x7857,0x785D,0x786A-0x786E,0x7877,0x787C,0x7887,0x7889,0x788C-0x788E,0x7891,0x7893,0x7897-0x7898,0x789A-0x789C,0x789F,0x78A1,0x78A3,0x78A5,0x78A7,0x78B0-0x78B4,0x78B9,0x78BE,0x78C1,0x78C5,0x78C9-0x78CB,0x78D0,0x78D4-0x78D5,0x78D9,0x78E8,0x78EC,0x78F2,0x78F4,0x78F7,0x78FA,0x7901,0x7905,0x7913,0x791E,0x7924,0x7934,0x793A-0x793C,0x793E,0x7940-0x7941,0x7946,0x7948-0x7949,0x7953,0x7956-0x7957,0x795A-0x7960,0x7962,0x7965,0x7967-0x7968,0x796D,0x796F,0x7977-0x7978,0x797A,0x7980-0x7981,0x7984-0x7985,0x798A,0x798F,0x799A,0x79A7,0x79B3,0x79B9-0x79BB,0x79BD-0x79BE,0x79C0-0x79C1,0x79C3,0x79C6,0x79C9,0x79CB,0x79CD,0x79D1-0x79D2,0x79D5,0x79D8,0x79DF,0x79E3-0x79E4,0x79E6-0x79E7,0x79E9,0x79EB,0x79ED,0x79EF-0x79F0,0x79F8,0x79FB,0x79FD,0x7A00,0x7A02-0x7A03,0x7A06,0x7A0B,0x7A0D-0x7A0E,0x7A14,0x7A17,0x7A1A,0x7A1E,0x7A20,0x7A23,0x7A33,0x7A37,0x7A39,0x7A3B-0x7A3D,0x7A3F,0x7A46,0x7A51,0x7A57,0x7A70,0x7A74,0x7A76-0x7A7A,0x7A7F-0x7A81,0x7A83-0x7A84,0x7A86,0x7A88,0x7A8D,0x7A91-0x7A92,0x7A95-0x7A98,0x7A9C-0x7A9D,0x7A9F-0x7AA0,0x7AA5-0x7AA6,0x7AA8,0x7AAC-0x7AAD,0x7AB3,0x7ABF,0x7ACB,0x7AD6,0x7AD9,0x7ADE-0x7AE0,0x7AE3,0x7AE5-0x7AE6,0x7AED,0x7AEF,0x7AF9-0x7AFA,0x7AFD,0x7AFF,0x7B03-0x7B04,0x7B06,0x7B08,0x7B0A-0x7B0B,0x7B0F,0x7B11,0x7B14-0x7B15,0x7B19,0x7B1B,0x7B1E,0x7B20,0x7B24-0x7B26,0x7B28,0x7B2A-0x7B2C,0x7B2E,0x7B31,0x7B33,0x7B38,0x7B3A,0x7B3C,0x7B3E,0x7B45,0x7B47,0x7B49,0x7B4B-0x7B4C,0x7B4F-0x7B52,0x7B54,0x7B56,0x7B58,0x7B5A-0x7B5B,0x7B5D,0x7B60,0x7B62,0x7B6E,0x7B71-0x7B72,0x7B75,0x7B77,0x7B79,0x7B7B,0x7B7E,0x7B80,0x7B85,0x7B8D,0x7B90,0x7B94-0x7B95,0x7B97,0x7B9C-0x7B9D,0x7BA1-0x7BA2,0x7BA6-0x7BAD,0x7BB1,0x7BB4,0x7BB8,0x7BC1,0x7BC6-0x7BC7,0x7BCC,0x7BD1,0x7BD3,0x7BD9-0x7BDA,0x7BDD,0x7BE1,0x7BE5-0x7BE6,0x7BEA,0x7BEE,0x7BF1,0x7BF7,0x7BFC,0x7BFE,0x7C07,0x7C0B-0x7C0C,0x7C0F,0x7C16,0x7C1F,0x7C26-0x7C27,0x7C2A,0x7C38,0x7C3F-0x7C41,0x7C4D,0x7C73-0x7C74,0x7C7B-0x7C7D,0x7C89,0x7C91-0x7C92,0x7C95,0x7C97-0x7C98,0x7C9C-0x7C9F,0x7CA2,0x7CA4-0x7CA5,0x7CAA,0x7CAE,0x7CB1-0x7CB3,0x7CB9,0x7CBC-0x7CBE,0x7CC1,0x7CC5,0x7CC7-0x7CC8,0x7CCA,0x7CCC-0x7CCD,0x7CD5-0x7CD7,0x7CD9,0x7CDC,0x7CDF-0x7CE0,0x7CE8,0x7CEF,0x7CF8,0x7CFB,0x7D0A,0x7D20,0x7D22,0x7D27,0x7D2B,0x7D2F,0x7D6E,0x7D77,0x7DA6,0x7DAE,0x7E3B,0x7E41,0x7E47,0x7E82,0x7E9B,0x7E9F-0x7EAD,0x7EAF-0x7EB3,0x7EB5-0x7EBA,0x7EBD-0x7ED5,0x7ED7-0x7EE3,0x7EE5-0x7EEB,0x7EED-0x7EF8,0x7EFA-0x7F09,0x7F0B-0x7F0F,0x7F11-0x7F1D,0x7F1F-0x7F36,0x7F38,0x7F3A,0x7F42,0x7F44-0x7F45,0x7F50-0x7F51,0x7F54-0x7F55,0x7F57-0x7F58,0x7F5A,0x7F5F,0x7F61-0x7F62,0x7F68-0x7F6A,0x7F6E,0x7F71-0x7F72,0x7F74,0x7F79,0x7F7E,0x7F81,0x7F8A,0x7F8C,0x7F8E,0x7F94,0x7F9A,0x7F9D-0x7F9F,0x7FA1,0x7FA4,0x7FA7,0x7FAF-0x7FB0,0x7FB2,0x7FB8-0x7FB9,0x7FBC-0x7FBD,0x7FBF,0x7FC1,0x7FC5,0x7FCA,0x7FCC,0x7FCE,0x7FD4-0x7FD5,0x7FD8,0x7FDF-0x7FE1,0x7FE5-0x7FE6,0x7FE9,0x7FEE,0x7FF0-0x7FF1,0x7FF3,0x7FFB-0x7FFC,0x8000-0x8001,0x8003-0x8006,0x800B-0x800D,0x8010,0x8012,0x8014-0x8019,0x801C,0x8020,0x8022,0x8025-0x802A,0x8031,0x8033,0x8035-0x8038,0x803B,0x803D,0x803F,0x8042-0x8043,0x8046,0x804A-0x804D,0x8052,0x8054,0x8058,0x805A,0x8069-0x806A,0x8071,0x807F-0x8080,0x8083-0x8084,0x8086-0x8087,0x8089,0x808B-0x808C,0x8093,0x8096,0x8098,0x809A-0x809D,0x809F-0x80A2,0x80A4-0x80A5,0x80A9-0x80AB,0x80AD-0x80AF,0x80B1-0x80B2,0x80B4,0x80B7,0x80BA,0x80BC-0x80C4,0x80C6,0x80CC-0x80CE,0x80D6-0x80D7,0x80D9-0x80DE,0x80E1,0x80E4-0x80E5,0x80E7-0x80ED,0x80EF-0x80F4,0x80F6,0x80F8,0x80FA,0x80FC-0x80FD,0x8102,0x8106,0x8109-0x810A,0x810D-0x8114,0x8116,0x8118,0x811A,0x811E,0x812C,0x812F,0x8131-0x8132,0x8136,0x8138,0x813E,0x8146,0x8148,0x814A-0x814C,0x8150-0x8151,0x8153-0x8155,0x8159-0x815A,0x8160,0x8165,0x8167,0x8169,0x816D-0x816E,0x8170-0x8171,0x8174,0x8179-0x8180,0x8182,0x8188,0x818A,0x818F,0x8191,0x8198,0x819B-0x819D,0x81A3,0x81A6,0x81A8,0x81AA,0x81B3,0x81BA-0x81BB,0x81C0-0x81C3,0x81C6,0x81CA,0x81CC,0x81E3,0x81E7,0x81EA,0x81EC-0x81ED,0x81F3-0x81F4,0x81FB-0x81FC,0x81FE,0x8200-0x8202,0x8204-0x8206,0x820C-0x820D,0x8210,0x8212,0x8214,0x821B-0x821C,0x821E-0x821F,0x8221-0x8223,0x8228,0x822A-0x822D,0x822F-0x8231,0x8233-0x8239,0x823B,0x823E,0x8244,0x8247,0x8249,0x824B,0x824F,0x8258,0x825A,0x825F,0x8268,0x826E-0x8270,0x8272-0x8274,0x8279-0x827A,0x827D-0x827F,0x8282,0x8284,0x8288,0x828A-0x828B,0x828D-0x828F,0x8291-0x8292,0x8297-0x8299,0x829C-0x829D,0x829F,0x82A1,0x82A4-0x82A6,0x82A8-0x82B1,0x82B3-0x82B4,0x82B7-0x82B9,0x82BD-0x82BE,0x82C1,0x82C4,0x82C7-0x82C8,0x82CA-0x82CF,0x82D1-0x82D5,0x82D7-0x82D8,0x82DB-0x82DC,0x82DE-0x82E1,0x82E3-0x82E6,0x82EB,0x82EF,0x82F1,0x82F4,0x82F7,0x82F9,0x82FB,0x8301-0x8309,0x830C,0x830E-0x830F,0x8311,0x8314-0x8315,0x8317,0x831A-0x831C,0x8327-0x8328,0x832B-0x832D,0x832F,0x8331,0x8333-0x8336,0x8338-0x833A,0x833C,0x8340,0x8343,0x8346-0x8347,0x8349,0x834F-0x8352,0x8354,0x835A-0x835C,0x835E-0x8361,0x8363-0x836F,0x8377-0x8378,0x837B-0x837D,0x8385-0x8386,0x8389,0x838E,0x8392-0x8393,0x8398,0x839B-0x839C,0x839E,0x83A0,0x83A8-0x83AB,0x83B0-0x83B4,0x83B6-0x83BA,0x83BC-0x83BD,0x83C0-0x83C1,0x83C5,0x83C7,0x83CA,0x83CC,0x83CF,0x83D4,0x83D6,0x83D8,0x83DC-0x83DD,0x83DF-0x83E1,0x83E5,0x83E9-0x83EA,0x83F0-0x83F2,0x83F8-0x83F9,0x83FD,0x8401,0x8403-0x8404,0x8406,0x840B-0x840F,0x8411,0x8418,0x841C-0x841D,0x8424-0x8428,0x8431,0x8438,0x843C-0x843D,0x8446,0x8451,0x8457,0x8459-0x845C,0x8461,0x8463,0x8469,0x846B-0x846D,0x8471,0x8473,0x8475-0x8476,0x8478,0x847A,0x8482,0x8487-0x8489,0x848B-0x848C,0x848E,0x8497,0x8499,0x849C,0x84A1,0x84AF,0x84B2,0x84B4,0x84B8-0x84BA,0x84BD,0x84BF,0x84C1,0x84C4,0x84C9-0x84CA,0x84CD,0x84D0-0x84D1,0x84D3,0x84D6,0x84DD,0x84DF-0x84E0,0x84E3,0x84E5-0x84E6,0x84EC,0x84F0,0x84FC,0x84FF,0x850C,0x8511,0x8513,0x8517,0x851A,0x851F,0x8521,0x852B-0x852C,0x8537-0x853D,0x8543,0x8548-0x854A,0x8556,0x8559,0x855E,0x8564,0x8568,0x8572,0x8574,0x8579-0x857B,0x857E,0x8584-0x8585,0x8587,0x858F,0x859B-0x859C,0x85A4,0x85A8,0x85AA,0x85AE-0x85B0,0x85B7,0x85B9,0x85C1,0x85C9,0x85CF-0x85D0,0x85D3,0x85D5,0x85DC,0x85E4,0x85E9,0x85FB,0x85FF,0x8605,0x8611,0x8616,0x8627,0x8629,0x8638,0x863C,0x864D-0x8651,0x8654,0x865A,0x865E,0x8662,0x866B-0x866C,0x866E,0x8671,0x8679-0x8682,0x868A-0x868D,0x8693,0x8695,0x869C-0x869D,0x86A3-0x86A4,0x86A7-0x86AA,0x86AC,0x86AF-0x86B1,0x86B4-0x86B6,0x86BA,0x86C0,0x86C4,0x86C6-0x86C7,0x86C9-0x86CB,0x86CE-0x86D1,0x86D4,0x86D8-0x86D9,0x86DB,0x86DE-0x86DF,0x86E4,0x86E9,0x86ED-0x86EE,0x86F0-0x86F4,0x86F8-0x86F9,0x86FE,0x8700,0x8702-0x8703,0x8707-0x870A,0x870D,0x8712-0x8713,0x8715,0x8717-0x8718,0x871A,0x871C,0x871E,0x8721-0x8723,0x8725,0x8729,0x872E,0x8731,0x8734,0x8737,0x873B,0x873E-0x873F,0x8747-0x8749,0x874C,0x874E,0x8753,0x8757,0x8759,0x8760,0x8763-0x8765,0x876E,0x8770,0x8774,0x8776,0x877B-0x877E,0x8782-0x8783,0x8785,0x8788,0x878B,0x878D,0x8793,0x8797,0x879F,0x87A8,0x87AB-0x87AD,0x87AF,0x87B3,0x87B5,0x87BA,0x87BD,0x87C0,0x87C6,0x87CA-0x87CB,0x87D1-0x87D3,0x87DB,0x87E0,0x87E5,0x87EA,0x87EE,0x87F9,0x87FE,0x8803,0x880A,0x8813,0x8815-0x8816,0x881B,0x8821-0x8822,0x8832,0x8839,0x883C,0x8840,0x8844-0x8845,0x884C-0x884D,0x8854,0x8857,0x8859,0x8861-0x8865,0x8868-0x8869,0x886B-0x886C,0x886E,0x8870,0x8872,0x8877,0x887D-0x887F,0x8881-0x8882,0x8884-0x8885,0x8888,0x888B,0x888D,0x8892,0x8896,0x889C,0x88A2,0x88A4,0x88AB,0x88AD,0x88B1,0x88B7,0x88BC,0x88C1-0x88C2,0x88C5-0x88C6,0x88C9,0x88CE,0x88D2,0x88D4-0x88D5,0x88D8-0x88D9,0x88DF,0x88E2-0x88E5,0x88E8,0x88F0-0x88F1,0x88F3-0x88F4,0x88F8-0x88F9,0x88FC,0x88FE,0x8902,0x890A,0x8910,0x8912-0x8913,0x8919-0x891B,0x8921,0x8925,0x892A-0x892B,0x8930,0x8934,0x8936,0x8941,0x8944,0x895E-0x895F,0x8966,0x897B,0x897F,0x8981,0x8983,0x8986,0x89C1-0x89C2,0x89C4-0x89CC,0x89CE-0x89D2,0x89D6,0x89DA,0x89DC,0x89DE,0x89E3,0x89E5-0x89E6,0x89EB,0x89EF,0x89F3,0x8A00,0x8A07,0x8A3E,0x8A48,0x8A79,0x8A89-0x8A8A,0x8A93,0x8B07,0x8B26,0x8B66,0x8B6C,0x8BA0-0x8BAB,0x8BAD-0x8BB0,0x8BB2-0x8BBA,0x8BBC-0x8BC6,0x8BC8-0x8BCF,0x8BD1-0x8BE9,0x8BEB-0x8C08,0x8C0A-0x8C1D,0x8C1F-0x8C37,0x8C41,0x8C46-0x8C47,0x8C49,0x8C4C,0x8C55,0x8C5A,0x8C61-0x8C62,0x8C6A-0x8C6B,0x8C73,0x8C78-0x8C7A,0x8C82,0x8C85,0x8C89-0x8C8A,0x8C8C,0x8C94,0x8C98,0x8D1D-0x8D1F,0x8D21-0x8D50,0x8D53-0x8D56,0x8D58-0x8D5E,0x8D60-0x8D64,0x8D66-0x8D67,0x8D6B,0x8D6D,0x8D70,0x8D73-0x8D77,0x8D81,0x8D84-0x8D85,0x8D8A-0x8D8B,0x8D91,0x8D94,0x8D9F,0x8DA3,0x8DB1,0x8DB3-0x8DB5,0x8DB8,0x8DBA,0x8DBC,0x8DBE-0x8DBF,0x8DC3-0x8DC4,0x8DC6,0x8DCB-0x8DCC,0x8DCE-0x8DCF,0x8DD1,0x8DD6-0x8DD7,0x8DDA-0x8DDB,0x8DDD-0x8DDF,0x8DE3-0x8DE4,0x8DE8,0x8DEA-0x8DEC,0x8DEF,0x8DF3,0x8DF5,0x8DF7-0x8DFB,0x8DFD,0x8E05,0x8E09-0x8E0A,0x8E0C,0x8E0F,0x8E14,0x8E1D-0x8E1F,0x8E22-0x8E23,0x8E29-0x8E2A,0x8E2C,0x8E2E-0x8E2F,0x8E31,0x8E35,0x8E39-0x8E3A,0x8E3D,0x8E40-0x8E42,0x8E44,0x8E47-0x8E4B,0x8E51-0x8E52,0x8E59,0x8E66,0x8E69,0x8E6C-0x8E6D,0x8E6F-0x8E70,0x8E72,0x8E74,0x8E76,0x8E7C,0x8E7F,0x8E81,0x8E85,0x8E87,0x8E8F-0x8E90,0x8E94,0x8E9C,0x8E9E,0x8EAB-0x8EAC,0x8EAF,0x8EB2,0x8EBA,0x8ECE,0x8F66-0x8F69,0x8F6B-0x8F7F,0x8F81-0x8F8B,0x8F8D-0x8F91,0x8F93-0x8F9C,0x8F9E-0x8F9F,0x8FA3,0x8FA8-0x8FA9,0x8FAB,0x8FB0-0x8FB1,0x8FB6,0x8FB9,0x8FBD-0x8FBE,0x8FC1-0x8FC2,0x8FC4-0x8FC5,0x8FC7-0x8FC8,0x8FCE,0x8FD0-0x8FD1,0x8FD3-0x8FD5,0x8FD8-0x8FD9,0x8FDB-0x8FDF,0x8FE2,0x8FE4-0x8FE6,0x8FE8-0x8FEB,0x8FED-0x8FEE,0x8FF0,0x8FF3,0x8FF7-0x8FF9,0x8FFD,0x9000-0x9006,0x9009-0x900B,0x900D,0x900F-0x9012,0x9014,0x9016-0x9017,0x901A-0x901B,0x901D-0x9022,0x9026,0x902D-0x902F,0x9035-0x9036,0x9038,0x903B-0x903C,0x903E,0x9041-0x9042,0x9044,0x9047,0x904D,0x904F-0x9053,0x9057-0x9058,0x905B,0x9062-0x9063,0x9065,0x9068,0x906D-0x906E,0x9074-0x9075,0x907D,0x907F-0x9080,0x9082-0x9083,0x9088,0x908B,0x9091,0x9093,0x9095,0x9097,0x9099,0x909B,0x909D,0x90A1-0x90A3,0x90A6,0x90AA,0x90AC,0x90AE-0x90B1,0x90B3-0x90B6,0x90B8-0x90BB,0x90BE,0x90C1,0x90C4-0x90C5,0x90C7,0x90CA,0x90CE-0x90D1,0x90D3,0x90D7,0x90DB-0x90DD,0x90E1-0x90E2,0x90E6-0x90E8,0x90EB,0x90ED,0x90EF,0x90F4,0x90F8,0x90FD-0x90FE,0x9102,0x9104,0x9119,0x911E,0x9122-0x9123,0x912F,0x9131,0x9139,0x9143,0x9146,0x9149-0x9150,0x9152,0x9157,0x915A,0x915D-0x915E,0x9161-0x9165,0x9169-0x916A,0x916C,0x916E-0x9172,0x9174-0x9179,0x917D-0x917F,0x9185,0x9187,0x9189,0x918B-0x918D,0x9190-0x9192,0x919A-0x919B,0x91A2-0x91A3,0x91AA,0x91AD-0x91AF,0x91B4-0x91B5,0x91BA,0x91C7,0x91C9-0x91CA,0x91CC-0x91CF,0x91D1,0x91DC,0x9274,0x928E,0x92AE,0x92C8,0x933E,0x936A,0x938F,0x93CA,0x93D6,0x943E,0x946B,0x9485-0x9490,0x9492-0x9495,0x9497,0x9499-0x94C6,0x94C8-0x94CE,0x94D0-0x94D2,0x94D5-0x94D9,0x94DB-0x94E5,0x94E7-0x94FA,0x94FC-0x951B,0x951D-0x951F,0x9521-0x9526,0x9528-0x9532,0x9534-0x953C,0x953E-0x9542,0x9544-0x9547,0x9549-0x954A,0x954C-0x9554,0x9556-0x9559,0x955B-0x955F,0x9561-0x956D,0x956F-0x9573,0x9576,0x957F,0x95E8-0x95EB,0x95ED-0x95FE,0x9600-0x9606,0x9608-0x9612,0x9614-0x9617,0x9619-0x961A,0x961C-0x961D,0x961F,0x9621-0x9622,0x962A,0x962E,0x9631-0x9636,0x963B-0x963D,0x963F-0x9640,0x9642,0x9644-0x9649,0x964B-0x964D,0x9650,0x9654-0x9655,0x965B,0x965F,0x9661-0x9662,0x9664,0x9667-0x966A,0x966C,0x9672,0x9674-0x9677,0x9685-0x9686,0x9688,0x968B,0x968D,0x968F-0x9690,0x9694,0x9697-0x9699,0x969C,0x96A7,0x96B0,0x96B3,0x96B6,0x96B9,0x96BC-0x96BE,0x96C0-0x96C1,0x96C4-0x96C7,0x96C9,0x96CC-0x96CF,0x96D2,0x96D5,0x96E0,0x96E8-0x96EA,0x96EF,0x96F3,0x96F6-0x96F7,0x96F9,0x96FE,0x9700-0x9701,0x9704,0x9706-0x9709,0x970D-0x970F,0x9713,0x9716,0x971C,0x971E,0x972A,0x972D,0x9730,0x9732,0x9738-0x9739,0x973E,0x9752-0x9753,0x9756,0x9759,0x975B,0x975E,0x9760-0x9762,0x9765,0x9769,0x9773-0x9774,0x9776,0x977C,0x9785,0x978B,0x978D,0x9791-0x9792,0x9794,0x9798,0x97A0,0x97A3,0x97AB,0x97AD,0x97AF,0x97B2,0x97B4,0x97E6-0x97E7,0x97E9-0x97ED,0x97F3,0x97F5-0x97F6,0x9875-0x988A,0x988C-0x988D,0x988F-0x9891,0x9893-0x9894,0x9896-0x9898,0x989A-0x98A2,0x98A4-0x98A7,0x98CE,0x98D1-0x98D3,0x98D5,0x98D8-0x98DA,0x98DE-0x98DF,0x98E7-0x98E8,0x990D,0x9910,0x992E,0x9954-0x9955,0x9963,0x9965,0x9967-0x9972,0x9974-0x9977,0x997A,0x997C-0x997D,0x997F-0x9981,0x9984-0x9988,0x998A-0x998B,0x998D,0x998F-0x9999,0x99A5,0x99A8,0x9A6C-0x9A71,0x9A73-0x9A82,0x9A84-0x9A88,0x9A8A-0x9A8C,0x9A8F-0x9A93,0x9A96-0x9A98,0x9A9A-0x9AA5,0x9AA7-0x9AA8,0x9AB0-0x9AB1,0x9AB6-0x9AB8,0x9ABA,0x9ABC,0x9AC0-0x9AC2,0x9AC5,0x9ACB-0x9ACC,0x9AD1,0x9AD3,0x9AD8,0x9ADF,0x9AE1,0x9AE6,0x9AEB,0x9AED,0x9AEF,0x9AF9,0x9AFB,0x9B03,0x9B08,0x9B0F,0x9B13,0x9B1F,0x9B23,0x9B2F,0x9B32,0x9B3B-0x9B3C,0x9B41-0x9B45,0x9B47-0x9B49,0x9B4D,0x9B4F,0x9B51,0x9B54,0x9C7C,0x9C7F,0x9C81-0x9C82,0x9C85-0x9C88,0x9C8B,0x9C8D-0x9C8E,0x9C90-0x9C92,0x9C94-0x9C95,0x9C9A-0x9C9C,0x9C9E-0x9CA9,0x9CAB,0x9CAD-0x9CAE,0x9CB0-0x9CB8,0x9CBA-0x9CBD,0x9CC3-0x9CC7,0x9CCA-0x9CD0,0x9CD3-0x9CD9,0x9CDC-0x9CDF,0x9CE2,0x9E1F-0x9E23,0x9E25-0x9E26,0x9E28-0x9E2D,0x9E2F,0x9E31-0x9E33,0x9E35-0x9E3A,0x9E3D-0x9E3F,0x9E41-0x9E4C,0x9E4E-0x9E4F,0x9E51,0x9E55,0x9E57-0x9E58,0x9E5A-0x9E5C,0x9E5E,0x9E63-0x9E64,0x9E66-0x9E6D,0x9E70-0x9E71,0x9E73,0x9E7E-0x9E7F,0x9E82,0x9E87-0x9E88,0x9E8B,0x9E92-0x9E93,0x9E9D,0x9E9F,0x9EA6,0x9EB4,0x9EB8,0x9EBB,0x9EBD-0x9EBE,0x9EC4,0x9EC9,0x9ECD-0x9ECF,0x9ED1,0x9ED4,0x9ED8,0x9EDB-0x9EDD,0x9EDF-0x9EE0,0x9EE2,0x9EE5,0x9EE7,0x9EE9-0x9EEA,0x9EEF,0x9EF9,0x9EFB-0x9EFC,0x9EFE,0x9F0B,0x9F0D-0x9F0E,0x9F10,0x9F13,0x9F17,0x9F19,0x9F20,0x9F22,0x9F2C,0x9F2F,0x9F37,0x9F39,0x9F3B,0x9F3D-0x9F3E,0x9F44,0x9F50-0x9F51,0x9F7F-0x9F80,0x9F83-0x9F8C,0x9F99-0x9F9B,0x9F9F-0x9FA0 diff --git a/packs/zh-Hans/ime/zh-hans-pinyin/manifest.ini b/packs/zh-Hans/ime/zh-hans-pinyin/manifest.ini new file mode 100644 index 00000000..0227f28b --- /dev/null +++ b/packs/zh-Hans/ime/zh-hans-pinyin/manifest.ini @@ -0,0 +1,4 @@ +kind=ime +id=zh-hans-pinyin +display_name=Pinyin +backend=builtin-pinyin diff --git a/packs/zh-Hans/locales/zh-Hans/manifest.ini b/packs/zh-Hans/locales/zh-Hans/manifest.ini new file mode 100644 index 00000000..000447c5 --- /dev/null +++ b/packs/zh-Hans/locales/zh-Hans/manifest.ini @@ -0,0 +1,8 @@ +kind=locale +id=zh-Hans +display_name=Simplified Chinese +native_name=简体中文 +ui_font_pack=zh-hans-cjk +content_font_pack=zh-hans-cjk +ime_pack=zh-hans-pinyin +strings=strings.tsv diff --git a/packs/zh-Hans/locales/zh-Hans/strings.tsv b/packs/zh-Hans/locales/zh-Hans/strings.tsv new file mode 100644 index 00000000..5d734dc6 --- /dev/null +++ b/packs/zh-Hans/locales/zh-Hans/strings.tsv @@ -0,0 +1,515 @@ +Action 操作 +Add 添加 +Advanced 高级 +Always 常亮 +Altitude 高度 +Altitude Reference 高度参考 +App 应用 +Area Cleared 区域清除 +Auto 自动 +Awaiting teammate 等待队友 +Back 返回 +Base Camp 营地 +Beijing (UTC+8) 北京 (UTC+8) +BLE bridge ready | %s BLE 桥接已就绪 | %s +BLE linked | %s BLE 已连接 | %s +Bluetooth 蓝牙 +Bluetooth Pairing 蓝牙配对 +Bcast 广播 +Bridge error 桥接错误 +Broadcast 广播 +Cancel 取消 +Channel 信道 +Chat 聊天 +Chat unavailable 聊天不可用 +Chinese 中文 +Closes automatically after pairing 配对完成后将自动关闭 +Close 关闭 +Command 指令 +Command: %s 指令:%s +Command: %s %s 指令:%s %s +Command: %s %s %s 指令:%s %s %s +Compare Verification Code 比对验证码 +Compass 指南针 +Connected 已连接 +Connected, handshaking... 已连接,正在握手... +Contacts 联系人 +Contacts (%s) 联系人(%s) +Contacts (LXMF) 联系人(LXMF) +Contacts (LXMF - %.3fMHz) 联系人(LXMF - %.3fMHz) +Contacts (MeshCore) 联系人(MeshCore) +Contacts (Meshtastic - %.3fMHz) 联系人(Meshtastic - %.3fMHz) +Contacts (RNode) 联系人(RNode) +Contacts (RNode - %.3fMHz) 联系人(RNode - %.3fMHz) +Conversation protocol mismatch 会话协议不匹配 +Data Exchange 数据交换 +Debug Logs 调试日志 +Decoding: %d%% 解码中:%d%% +Decoding: 0% 解码中:0% +Delete 删除 +Delete %s? 删除 %s? +Delete contact %s? 删除联系人 %s? +Delete failed 删除失败 +Device: %s 设备:%s +Direct 私聊 +Disabled 已禁用 +Discover 发现 +Display Language 显示语言 +Duplicate name not allowed 名称不能重复 +Edit 编辑 +Edit nickname 编辑昵称 +Emergency SOS 紧急求救 +Energy Sweep 频谱扫描 +English English +English keyboard 英文键盘 +Enter nickname 输入昵称 +Enter number for %s 输入 %s 的校验数字 +Enter the 6-digit number 请输入 6 位数字 +Enter this 6-digit PIN on your phone 请在手机上输入这个 6 位 PIN 码 +Error 错误 +Error: %lu 错误:%lu +FSK Voice FSK 语音 +Failed 失败 +Fix 定位 +Fixed PIN 固定 PIN +Gauge Design (mAh) 电池设计容量 (mAh) +Gauge Full (mAh) 电池满充容量 (mAh) +Good Find 发现目标 +GPS GPS +GPS Status GPS 状态 +HDOP HDOP +Heading starting 航向启动中 +Hide 隐藏 +Host bridge 主机桥接 +Host connected, probing modem... 主机已连接,正在探测调制解调器... +IDLE 空闲 +Ignored 已忽略 +Ignore 忽略 +Ignore unavailable 当前无法忽略 +Ignore update failed 忽略状态更新失败 +Image received 图像已接收 +Info 信息 +Initializing... 初始化中... +Invalid airtime factor 空口系数无效 +Invalid design capacity (mAh) 设计容量 (mAh) 无效 +Invalid frequency offset 频偏无效 +Invalid frequency value 频率值无效 +Invalid full capacity (mAh) 满充容量 (mAh) 无效 +Invalid marker 无效标记 +Invalid MeshCore bandwidth MeshCore 带宽无效 +Invalid MeshCore frequency MeshCore 频率无效 +Invalid number 数字无效 +Invalid RX delay 接收延迟无效 +Key marked trusted 密钥已标记为可信 +Key must be 32 hex or 16 chars 密钥必须为 32 位十六进制或 16 个字符 +Key trust failed 密钥信任失败 +Key Verification 密钥校验 +LINKED 已组网 +LISTEN 监听 +LIVE 实时 +Loading... 加载中... +Location 位置 +Location: %s 位置:%s +Location: %s %s 位置:%s %s +LoRa direct path | %s LoRa 直连 | %s +Low battery - audio disabled 电量低,已禁用音频 +Map 地图 +Map Layer 地图图层 +MESH 网状 +Mesh Status 网状状态 +Mesh unavailable Mesh 不可用 +MeshCore only 仅支持 MeshCore +Message DB cleared 消息数据库已清空 +Messages 消息 +MESSAGES 消息 +Messages and broadcasts show here 消息和广播会显示在这里 +Mini Compass 迷你指南针 +MT send uses slot 0/1 only Meshtastic 发送仅支持槽位 0/1 +N/A 不适用 +Nearby 附近 +Need local GPS 需要本机 GPS +Network 网络 +New 新建 +new activity 有新动态 +Next 下一页 +No GPS fix 没有 GPS 定位 +No image 没有图像 +No messages 暂无消息 +No preview 无预览 +No recent traffic 暂无最近活动 +No response 无响应 +No route selected 未选择路线 +No target 没有目标 +No track files 没有轨迹文件 +Node DB reset 节点数据库已重置 +Node ignored 节点已忽略 +Node ignored and hidden 节点已忽略并隐藏 +Node unignored 已取消忽略节点 +North locked 北向已锁定 +Not set 未设置 +NO 否 +NO FIX 无定位 +Number keyboard 数字键盘 +OFF 关 +OK 确定 +ON 开 +online 在线 +Pairing 配对 +PC Link 电脑连接 +PC Link is unavailable on this target. 当前目标暂不支持电脑连接. +peer lock | turn %03.0f deg 目标锁定 | 转向 %03.0f 度 +Pinyin keyboard 拼音键盘 +Pinyin: %s 拼音:%s +Position 位置 +position stream active | %u in view 定位流活跃 | 可见 %u 颗 +Power 电量 +Press Back 按返回键 +Press RX to start 按 RX 开始 +Primary 主信道 +Protocol 协议 +Protocol switch failed 协议切换失败 +Protocol switched 协议已切换 +quiet net 网络安静 +Random PIN 随机 PIN +Ready 就绪 +RECEIVING 接收中 +Recent Activity 最近活动 +Record 记录 +Recording 记录中 +Region 区域 +Remain: %u 剩余:%u +Reply 回复 +Reply disabled for this protocol 此协议下不能回复 +Reset Mesh Profiles 重置 Mesh 配置 +Reset Node DB 重置节点数据库 +Resetting... 重置中... +Return 返回 +Rally Point 集结点 +RNode Bridge RNode Bridge +RNode modem ready RNode 调制解调器已就绪 +RNode text chat runs on host RNode 文本聊天在主机侧运行 +Route 路线 +Run 执行 +RX RX +RX: %lu TX: %lu 接收:%lu 发送:%lu +RX: 0 TX: 0 接收:0 发送:0 +Save 保存 +Saved: %s 已保存:%s +Scan failed 扫描失败 +Scan Local 本地扫描 +Scanning 5s... 扫描 5 秒中... +Screen Brightness 屏幕亮度 +Screen Timeout 熄屏超时 +SEARCH 搜索中 +Secondary 次信道 +Select a route 请选择路线 +Select a track 请选择轨迹 +Select marker 选择标记 +Select Track 选择轨迹 +Selected: %s 已选:%s +Send 发送 +Send failed 发送失败 +Send this code and compare:\n 发送这个验证码并比对:\n +Send ID Broadcast 广播发送 ID +Send ID Local 本地发送 ID +Sending... 发送中... +Sent 已发送 +Settings 设置 +Setting 设置 +Share Position Marker 分享位置标记 +Share this number:\n 请分享这个数字:\n +Short Name 短名称 +SOLO 单机 +Speaker Volume 提示音音量 +Sky Plot 星图 +SSTV RX READY SSTV 接收就绪 +SSTV start failed SSTV 启动失败 +standing by 待命 +Status 状态 +Stopped 已停止 +Stop 停止 +Stop recording first 请先停止记录 +Submit 提交 +Submit failed 提交失败 +System 系统 +SATELLITE STATUS 卫星状态 +Talk 说话 +TALK 说话 +Team 团队 +TEAM 团队 +Team chat send failed 团队消息发送失败 +Team chat unavailable 团队聊天不可用 +Team chat unavailable in RNode mode RNode 模式下团队聊天不可用 +team member position will appear here 队友位置会显示在这里 +Team keys not ready 团队密钥未就绪 +Team location encode failed 团队位置编码失败 +Text chat unavailable 文本聊天不可用 +threads active 活跃会话 +Time Zone 时区 +Tracker 轨迹 +traffic active 流量活跃 +Trust Key 信任密钥 +U 用 +Unignore 取消忽略 +Unread 未读 +Unknown 未知 +USB Mass Storage USB 大容量存储 +USB Disk USB存储 +USB Bridge USB 桥接 +USB host bridge USB 主机桥接 +USB Bridge is unavailable on this target. 当前目标暂不支持 USB 桥接. +USB CDC KISS modem for Reticulum 面向 Reticulum 的 USB CDC KISS 调制解调器 +USE 用 +Used 已用 +User Name 用户名 +Verification Number 校验数字 +Verification number sent 校验数字已发送 +Vibration 振动 +VOL %d 音量 %d +VOL 80 音量 80 +waiting for better sky view | %u in view 等待更好的天空视野 | 可见 %u 颗 +waiting for module GNSS compass 等待 GNSS/指南针模块 +waiting for traffic 等待流量 +Waiting for host... 等待主机连接... +Waiting for Reticulum host... 等待 Reticulum 主机连接... +WAIT IMU 等待 IMU +Walkie not available 对讲功能不可用 +Walkie start failed 对讲启动失败 +Walkie Talkie 对讲机 +YES 是 +6 digits 6 位数字 +%u threads active %u 个会话活跃 +%ud %u天 +%uh %u小时 +%um %u分 +%umo %u月 +%uy %u年 +Confirm received code:\n 确认收到的验证码:\n +device heading from 9-axis module 设备航向来自 9 轴模块 +heading unlocks after first fix 首次定位后解锁航向 +If it matches, trust the key. 如果一致,请信任此密钥。 +magnetometer heading active 磁力计航向已启用 +magnetometer still warming up 磁力计仍在预热 +needle shows north until target lock 锁定目标前指针指向北方 +nearest peer | %.0f m away 最近队友 | 距离 %.0f 米 +no recent traffic 暂无最近活动 +now 刚刚 +%s layer missing %s 图层缺失 +Active: %s 已激活:%s +Alt: %.0f m 高度:%.0f 米 +Alt: -- m 高度:-- 米 +Base: %s 底图:%s +Battery critical 电池电量危险 +Broadcast (%d) 广播(%d) +COMPLETE 已完成 +Calibrating... 校准中... +Confirm 确认 +Contour: OFF 等高线:关 +Decoder error 解码错误 +Direct (%d) 私聊(%d) +E 东 +ERROR 错误 +Exiting USB... 正在退出 USB... +Hold 原地待命 +ID: !%08lX ID: !%08lX +ID: - ID: - +Key refresh not implemented 暂未实现密钥刷新 +Key verify: %s %s 密钥校验:%s %s +Key verify: confirm %s %s 密钥校验:请确认 %s %s +Key verify: enter number for %s 密钥校验:请为 %s 输入数字 +Key verify: send %s %s 密钥校验:请发送 %s %s +Later 稍后 +Level %d 级别 %d +Listening for SSTV signal... 正在监听 SSTV 信号... +Low battery: %d%% 电量低:%d%% +MODE: %s 模式:%s +MODE: Auto 模式:自动 +Map - %.48s 地图 - %.48s +Map - %s Missing 地图 - %s 缺失 +Map - No Map Data 地图 - 没有地图数据 +Map - No SD Card 地图 - 没有 SD 卡 +Map - no gps data 地图 - 没有 GPS 数据 +Message 消息 +Mic TBD 麦克风功能待定义 +Mic init failed 麦克风初始化失败 +Morse Input 摩斯电码输入 +MoveTo 移动到 +N 北 +NODE INFO 节点信息 +Name required 必须输入名称 +Name too long 名称过长 +No KML routes 没有 KML 路线 +No SD Card 没有 SD 卡 +No routes folder 没有 routes 文件夹 +No tracks yet 暂无轨迹 +Only leader can manage 只有队长可以管理 +Only leader can pair 只有队长可以配对 +Paired successfully 配对成功 +Pairing failed 配对失败 +Pairing init failed 配对初始化失败 +Pairing not available 当前无法配对 +Pairing not ready 配对尚未就绪 +Press Back to exit USB mode 按返回键退出 USB 模式 +Press SPACE to resume 按 SPACE 唤醒屏幕 +RallyTo 集结到 +Route configured 已配置路线 +Route: %s 路线:%s +SD Card Not Found\nPlease insert SD card 未找到 SD 卡\n请插入 SD 卡 +SSTV RECEIVER SSTV 接收器 +Save failed 保存失败 +Scan +%u/%u 扫描新增:+%u/%u +Select level 选择级别 +Selected level: %d 已选级别:%d +Shutting down to protect battery 正在关机以保护电池 +Status: CALIB 状态:校准 +Step 1/2: Tap short dots 第 1/2 步:点击短点 +Step 2/2: Tap long dashes 第 2/2 步:点击长线 +Switch to %s to chat 切换到 %s 以聊天 +Symbol: 符号: +Tap to input, idle 3s to send 点击输入,空闲 3 秒后发送 +Team keys mismatch 团队密钥不匹配 +Team: 团队: +Text: 文本: +Unread: %d 未读:%d +Unread: 0 未读:0 +W 西 +0° Horizon 0° 地平线 +PSK must be 32 hex or 16 chars PSK 必须是 32 位十六进制或 16 个字符 +Restarting... 正在重启... +awaiting position 等待位置 +compass wakes after GPS fix GPS 定位后指南针将启用 +searching satellites 正在搜索卫星 +%s - %s - +%s 0s %s 0秒 +%s %us %s %u秒 +%s %um %s %u分 +%s %uh %s %u小时 +%s %ud %s %u天 +AUTO 自动 +Alt: %ld m 高度:%ld 米 +Alt: - 高度:- +BEST 最佳 +BW: %.0fk 带宽:%.0fk +BW: %.1fk 带宽:%.1fk +BW: - 带宽:- +CAD CAD +CURSOR 游标 +Ch: %.3f 信道:%.3f +Ch: - 信道:- +Hop: %u 跳数:%u +Hop: - 跳数:- +Hop: - / NH: %02X 跳数:- / 下一跳:%02X +ID: !%06lx ID: !%06lx +ID: !%08lx ID: !%08lx +Last heard: 最后收到: +Link 链路 +Link / MeshCore 链路 / MeshCore +Link / Meshtastic 链路 / Meshtastic +MODE: RSSI 模式:RSSI +NOISE %.0f dBm 噪声 %.0f dBm +NOISE -104 dBm 噪声 -104 dBm +PKI: - PKI:- +PKI: known PKI:已知 +PKI: verified PKI:已验证 +RSSI %.0f dBm RSSI %.0f dBm +RSSI -92 dBm RSSI -92 dBm +RSSI: %.0f dBm RSSI:%.0f dBm +RSSI: - RSSI:- +Role: %s 角色:%s +SCAN 扫描 +SF: %u 扩频因子:%u +SF: - 扩频因子:- +SIM 模拟 +SNR +%d SNR +%d +SNR +12 SNR +12 +SNR: %.1f dB SNR:%.1f dB +SNR: - SNR:- +SNR %.0f 信噪比 %.0f +SNR - 信噪比 - +SNR Good 信噪比良好 +SNR Weak 信噪比较弱 +STEP %.0fk | BW %.0fk 步进 %.0fk | 带宽 %.0fk +STEP %.1fk | BW %.1fk 步进 %.1fk | 带宽 %.1fk +STEP -- | BW -- 步进 -- | 带宽 -- +STOP 停止 +Slot 槽位 +Slot %u 槽位 %u +SUB-GHz SCAN Sub-GHz 扫描 +Actions 操作 +Actions: %s 操作:%s +Access to team data revoked 队伍数据访问已撤销 +Capability: 能力: +Channel Actions 频道操作 +Confirm Kick 确认移除 +Create Team 创建队伍 +ELEV 仰角 +Join Team 加入队伍 +In View 可见 +Keep devices within 5m 请将设备保持在 5 米内 +KeyDist 密钥分发 +KeyId: %u 密钥 ID:%u +KeyId: -- 密钥 ID:-- +Keys 密钥 +Kick 移除 +Kick Member 移除成员 +Last paired: %s 最近配对:%s +Last seen %uh ago 上次在线 %u 小时前 +Last seen %um ago 上次在线 %u 分钟前 +Last seen -- 上次在线:-- +Last update %us ago 上次更新 %u 秒前 +Last update -- 上次更新:-- +Last update 0s ago 上次更新 0 秒前 +Leader 队长 +Leader online 队长在线 +Leave 离开 +Leave team? 离开队伍? +Manage 管理 +Member: %s 成员:%s +Members 成员 +Members: %u 成员数:%u +Members: %u Online: %u 成员:%u 在线:%u +Modem Bridge 调制解调器桥接 +No members yet 暂无成员 +Not Used 未使用 +Offline 离线 +Online 在线 +Online: %u 在线:%u +Pair Again 再次配对 +Pair Member 配对成员 +Pairing in progress 正在配对 +Prev 上一页 +Remove %s from team? 将 %s 移出队伍? +Request Keys 请求密钥 +Retry 重试 +Security Round: %u 安全轮次:%u +Security Round: -- 安全轮次:-- +Security: OK (Round %u) 安全状态:正常(轮次 %u) +Security: OK (Round --) 安全状态:正常(轮次 --) +Seen %ud %u 天前 +Seen %uh %u 小时前 +Seen %um %u 分钟前 +Send failed (keys not ready) 发送失败(密钥未就绪) +Status: %s 状态:%s +Target: %s 目标:%s +Team / %s 队伍 / %s +Team Actions 队伍操作 +Team Health 队伍状态 +Team Status 队伍状态 +Team: %s 队伍:%s +This clears local keys. 这将清除本地密钥。 +This will update the security round.\nThe member will no longer receive\nteam messages or waypoints. 这将更新安全轮次。\n该成员将不再接收\n队伍消息或航点。 +Transfer 移交 +Transfer Leader 移交队长 +View Team 查看队伍 +Waiting for new keys... 正在等待新密钥... +Waypoint 航点 +You are no longer in this team 你已不在此队伍中 +You are not in a team 你当前未加入队伍 +- No shared map\n- No team awareness - 无共享地图\n- 无队伍态势 +1 member stale 1 名成员状态过期 +encrypt failed 加密失败 +member 成员 +queue full 队列已满 +unsupported protocol 协议不支持 +Factory Reset 恢复出厂设置 +Clear all settings and restart? 清空全部设置并重启? diff --git a/packs/zh-Hans/package.ini b/packs/zh-Hans/package.ini new file mode 100644 index 00000000..b35e49ce --- /dev/null +++ b/packs/zh-Hans/package.ini @@ -0,0 +1,13 @@ +kind=package +id=zh-Hans +package_type=locale-bundle +version=1.0.0 +display_name=Simplified Chinese +summary=Full Simplified Chinese locale bundle with external CJK font coverage and the built-in Pinyin IME backend. +description=DESCRIPTION.txt +readme=README.md +author=Trail Mate +homepage=https://vicliu624.github.io/trail-mate/ +min_firmware_version=0.1.18-alpha +supported_memory_profiles=standard,extended +tags=language,cjk,chinese,ime diff --git a/packs/zh-Hant/DESCRIPTION.txt b/packs/zh-Hant/DESCRIPTION.txt new file mode 100644 index 00000000..539553b4 --- /dev/null +++ b/packs/zh-Hant/DESCRIPTION.txt @@ -0,0 +1,5 @@ +Traditional Chinese installs the zh-Hant locale together with the zh-hant-cjk external +font pack so the UI and mixed-language content can render Traditional Chinese correctly. + +This bundle is display-focused today. It does not add a dedicated Traditional Chinese IME +yet, but it keeps rendering support fully external to the firmware image. diff --git a/packs/zh-Hant/README.md b/packs/zh-Hant/README.md new file mode 100644 index 00000000..bdd66ec5 --- /dev/null +++ b/packs/zh-Hant/README.md @@ -0,0 +1,85 @@ +# zh-Hant Pack Bundle + +This bundle keeps Traditional Chinese external to the firmware image. + +It contains: + +- locale pack: `zh-Hant` +- font pack: `zh-hant-cjk` + +The locale manifest points to: + +- `ui_font_pack=zh-hant-cjk` +- `content_font_pack=zh-hant-cjk` + +Bundle-level package metadata lives in: + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` + +## Bundle Layout + +- `package.ini` +- `DESCRIPTION.txt` +- `README.md` +- `fonts/zh-hant-cjk/` + - `manifest.ini` + - `build.ini` + - `charset.txt` + - `ranges.txt` + - `font.bin` after generation +- `locales/zh-Hant/` + - `manifest.ini` + - `strings.tsv` + +## Generate The Font Pack + +1. Refresh the subset files: + +```bash +python tools/build_locale_pack_charset.py --pack-root packs/zh-Hant --font-pack-id zh-hant-cjk +``` + +2. Generate `font.bin`: + +```powershell +python tools/generate_binfont_with_lv_font_conv.py ` + --font tools/fonts/NotoSansCJKsc-Regular.otf ` + --charset-file packs/zh-Hant/fonts/zh-hant-cjk/charset.txt ` + --output packs/zh-Hant/fonts/zh-hant-cjk/font.bin ` + --size 16 ` + --bpp 2 ` + --node-exe C:\Users\VicLi\AppData\Local\nodejs22\current\node.exe ` + --no-compress +``` + +Notes: + +- This bundle is display-only for now. It does not declare an IME pack yet. +- `ranges.txt` and `estimated_ram_bytes` let the runtime decide when this pack can be activated or loaded as a supplement. +- `build.ini` is the source-of-truth for generating `font.bin` during Pages/package builds. +- `font.bin` is ignored by Git. Regenerate it whenever the subset changes. + +## Distribution Package + +`python scripts/build_pack_repository.py --pack-root packs --site-root site` produces: + +- `site/assets/packs/zh-Hant-1.0.0.zip` +- `site/data/packs.json` + +The zip is the bundle artifact for a future Extensions downloader. Its `payload/` directory unpacks into `/trailmate/packs/...`. + +## Copy To SD Card + +Copy the bundle contents so the SD card ends up with: + +```text +/trailmate/packs/fonts/zh-hant-cjk/manifest.ini +/trailmate/packs/fonts/zh-hant-cjk/ranges.txt +/trailmate/packs/fonts/zh-hant-cjk/font.bin +/trailmate/packs/locales/zh-Hant/manifest.ini +/trailmate/packs/locales/zh-Hant/strings.tsv +``` + +After reboot, `繁體中文` appears in Settings. The font stays unloaded until the locale is activated or Traditional Chinese content needs coverage. diff --git a/packs/zh-Hant/fonts/zh-hant-cjk/.gitignore b/packs/zh-Hant/fonts/zh-hant-cjk/.gitignore new file mode 100644 index 00000000..2224454d --- /dev/null +++ b/packs/zh-Hant/fonts/zh-hant-cjk/.gitignore @@ -0,0 +1 @@ +font.bin diff --git a/packs/zh-Hant/fonts/zh-hant-cjk/build.ini b/packs/zh-Hant/fonts/zh-hant-cjk/build.ini new file mode 100644 index 00000000..d0889975 --- /dev/null +++ b/packs/zh-Hant/fonts/zh-hant-cjk/build.ini @@ -0,0 +1,4 @@ +font=tools/fonts/NotoSansCJKsc-Regular.otf +size=16 +bpp=2 +no_compress=true diff --git a/packs/zh-Hant/fonts/zh-hant-cjk/charset.txt b/packs/zh-Hant/fonts/zh-hant-cjk/charset.txt new file mode 100644 index 00000000..3de2cd93 --- /dev/null +++ b/packs/zh-Hant/fonts/zh-hant-cjk/charset.txt @@ -0,0 +1,3 @@ +並中交亮人作使停儲入刪動區協原取叭名啟喇回圖團地執增天失字存定尋對就層已常幕度廠建復恢息態應所掃描提換搜操敗數文料新日星時暫最有 +未本止沒活消清源無牙狀現用發盤知確碼示秒稱系絡統網緒線編繁者聊聯腦自英藍藏號螢行覆言訊設語誤證譜議資跡路軌載輯近返連進逾道配重量 +錯鍵閉開關附除隊階隱電震韓音頻顯驗體? diff --git a/packs/zh-Hant/fonts/zh-hant-cjk/manifest.ini b/packs/zh-Hant/fonts/zh-hant-cjk/manifest.ini new file mode 100644 index 00000000..381aa568 --- /dev/null +++ b/packs/zh-Hant/fonts/zh-hant-cjk/manifest.ini @@ -0,0 +1,8 @@ +kind=font +id=zh-hant-cjk +display_name=Traditional Chinese CJK +usage=both +estimated_ram_bytes=10232 +source=binfont +file=font.bin +ranges=ranges.txt diff --git a/packs/zh-Hant/fonts/zh-hant-cjk/ranges.txt b/packs/zh-Hant/fonts/zh-hant-cjk/ranges.txt new file mode 100644 index 00000000..6e6e2066 --- /dev/null +++ b/packs/zh-Hant/fonts/zh-hant-cjk/ranges.txt @@ -0,0 +1 @@ +0x4E26,0x4E2D,0x4EA4,0x4EAE,0x4EBA,0x4F5C,0x4F7F,0x505C,0x5132,0x5165,0x522A,0x52D5,0x5340,0x5354,0x539F,0x53D6,0x53ED,0x540D,0x555F,0x5587,0x56DE,0x5716,0x5718,0x5730,0x57F7,0x589E,0x5929,0x5931,0x5B57-0x5B58,0x5B9A,0x5C0B,0x5C0D,0x5C31,0x5C64,0x5DF2,0x5E38,0x5E55,0x5EA6,0x5EE0,0x5EFA,0x5FA9,0x6062,0x606F,0x614B,0x61C9,0x6240,0x6383,0x63CF-0x63D0,0x63DB,0x641C,0x64CD,0x6557,0x6578,0x6587,0x6599,0x65B0,0x65E5,0x661F,0x6642,0x66AB,0x6700,0x6709,0x672A,0x672C,0x6B62,0x6C92,0x6D3B,0x6D88,0x6E05,0x6E90,0x7121,0x7259,0x72C0,0x73FE,0x7528,0x767C,0x76E4,0x77E5,0x78BA,0x78BC,0x793A,0x79D2,0x7A31,0x7CFB,0x7D61,0x7D71,0x7DB2,0x7DD2,0x7DDA,0x7DE8,0x7E41,0x8005,0x804A,0x806F,0x8166,0x81EA,0x82F1,0x85CD,0x85CF,0x865F,0x87A2,0x884C,0x8986,0x8A00,0x8A0A,0x8A2D,0x8A9E,0x8AA4,0x8B49,0x8B5C,0x8B70,0x8CC7,0x8DE1,0x8DEF,0x8ECC,0x8F09,0x8F2F,0x8FD1,0x8FD4,0x9023,0x9032,0x903E,0x9053,0x914D,0x91CD,0x91CF,0x932F,0x9375,0x9589,0x958B,0x95DC,0x9644,0x9664,0x968A,0x968E,0x96B1,0x96FB,0x9707,0x97D3,0x97F3,0x983B,0x986F,0x9A57,0x9AD4,0xFF1F diff --git a/packs/zh-Hant/locales/zh-Hant/manifest.ini b/packs/zh-Hant/locales/zh-Hant/manifest.ini new file mode 100644 index 00000000..e6b5ac2c --- /dev/null +++ b/packs/zh-Hant/locales/zh-Hant/manifest.ini @@ -0,0 +1,7 @@ +kind=locale +id=zh-Hant +display_name=Traditional Chinese +native_name=繁體中文 +ui_font_pack=zh-hant-cjk +content_font_pack=zh-hant-cjk +strings=strings.tsv diff --git a/packs/zh-Hant/locales/zh-Hant/strings.tsv b/packs/zh-Hant/locales/zh-Hant/strings.tsv new file mode 100644 index 00000000..fe34dae9 --- /dev/null +++ b/packs/zh-Hant/locales/zh-Hant/strings.tsv @@ -0,0 +1,80 @@ +Action 操作 +Add 新增 +Advanced 進階 +Always 常亮 +App 應用 +Auto 自動 +Back 返回 +Bluetooth 藍牙 +Bluetooth Pairing 藍牙配對 +Cancel 取消 +Channel 頻道 +Chat 聊天 +Chinese 中文 +Close 關閉 +Contacts 聯絡人 +Data Exchange 資料交換 +Delete 刪除 +Disabled 已停用 +Discover 發現 +Display Language 顯示語言 +Edit 編輯 +Energy Sweep 頻譜掃描 +English English +English keyboard 英文鍵盤 +Error 錯誤 +Factory Reset 恢復原廠 +GPS GPS +GPS Status GPS 狀態 +Hide 隱藏 +Info 資訊 +Japanese 日文 +Korean 韓文 +Loading... 載入中... +Map 地圖 +Map Layer 地圖圖層 +Nearby 附近 +Network 網路 +New 新建 +No messages 沒有訊息 +No recent traffic 暫無最近活動 +Not set 未設定 +Number keyboard 數字鍵盤 +OFF 關 +OK 確定 +ON 開 +Pairing 配對 +PC Link 電腦連線 +Power 電源 +Protocol 協議 +Ready 就緒 +Recent Activity 最近活動 +Reply 回覆 +Run 執行 +Save 儲存 +Scan failed 掃描失敗 +Scan Local 本地掃描 +Scanning 5s... 掃描 5 秒... +SEARCH 搜尋 +Screen Brightness 螢幕亮度 +Screen Timeout 螢幕逾時 +Settings 設定 +Setting 設定 +Sky Plot 星圖 +Speaker Volume 喇叭音量 +Status 狀態 +Stopped 已停止 +Stop 停止 +Submit 提交 +System 系統 +Team 團隊 +Team Status 團隊狀態 +Time Zone 時區 +Tracker 軌跡 +Traditional Chinese 繁體中文 +Unknown 未知 +USB Disk USB儲存 +User Name 使用者名稱 +Verification Number 驗證號碼 +Vibration 震動 +Clear all settings and restart? 清除所有設定並重新啟動? diff --git a/packs/zh-Hant/package.ini b/packs/zh-Hant/package.ini new file mode 100644 index 00000000..bff56de1 --- /dev/null +++ b/packs/zh-Hant/package.ini @@ -0,0 +1,13 @@ +kind=package +id=zh-Hant +package_type=locale-bundle +version=1.0.0 +display_name=Traditional Chinese +summary=Traditional Chinese locale bundle with external CJK font coverage for UI and mixed-script content rendering. +description=DESCRIPTION.txt +readme=README.md +author=Trail Mate +homepage=https://vicliu624.github.io/trail-mate/ +min_firmware_version=0.1.18-alpha +supported_memory_profiles=constrained,standard,extended +tags=language,cjk,chinese diff --git a/platform/esp/arduino_common/include/lv_conf.h b/platform/esp/arduino_common/include/lv_conf.h index 572c053d..b11f4352 100644 --- a/platform/esp/arduino_common/include/lv_conf.h +++ b/platform/esp/arduino_common/include/lv_conf.h @@ -530,7 +530,7 @@ * * Keep ESP-only fonts declared from ESP-owned paths so NRF builds don't * accidentally pull large bitmap assets into their source lists.*/ -#define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(lv_font_noto_cjk_16_2bpp) +#define LV_FONT_CUSTOM_DECLARE /*Always set a default font*/ #define LV_FONT_DEFAULT &lv_font_montserrat_14 diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/mesh_adapter_router.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/mesh_adapter_router.h index 89af1db5..f58be3f3 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/mesh_adapter_router.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/mesh_adapter_router.h @@ -50,7 +50,7 @@ class MeshAdapterRouter : public IMeshAdapter void applyConfig(const MeshConfig& config) override; void setUserInfo(const char* long_name, const char* short_name) override; void setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) override; - void setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) override; + void setPrivacyConfig(uint8_t encrypt_mode) override; bool isReady() const override; bool pollIncomingRawPacket(uint8_t* out_data, size_t& out_len, size_t max_len) override; void handleRawPacket(const uint8_t* data, size_t size) override; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshcore/meshcore_adapter.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshcore/meshcore_adapter.h index 7c35aac6..ef071d3f 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshcore/meshcore_adapter.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshcore/meshcore_adapter.h @@ -64,7 +64,7 @@ class MeshCoreAdapter : public IMeshAdapter, public IMeshCoreBleBackend void applyConfig(const MeshConfig& config) override; void setUserInfo(const char* long_name, const char* short_name) override; void setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) override; - void setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) override; + void setPrivacyConfig(uint8_t encrypt_mode) override; void setLastRxStats(float rssi, float snr) override; bool isReady() const override; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h index b559125c..46b1b56b 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h @@ -77,7 +77,7 @@ class MtAdapter : public chat::IMeshAdapter void applyConfig(const MeshConfig& config) override; void setUserInfo(const char* long_name, const char* short_name) override; void setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) override; - void setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) override; + void setPrivacyConfig(uint8_t encrypt_mode) override; void setLastRxStats(float rssi, float snr) override; bool isReady() const override; NodeId getNodeId() const override { return node_id_; } @@ -211,7 +211,6 @@ class MtAdapter : public chat::IMeshAdapter uint32_t min_tx_interval_ms_ = 0; uint32_t last_tx_ms_ = 0; uint8_t encrypt_mode_ = 1; - bool pki_enabled_ = false; meshtastic_Routing_Error last_send_error_ = meshtastic_Routing_Error_NONE; bool sendPacket(const PendingSend& pending); diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/flash_store.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/flash_store.h index fb3685f6..1b257a86 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/flash_store.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/flash_store.h @@ -35,6 +35,7 @@ class FlashStore : public IChatStore void clearConversation(const ConversationId& conv) override; void clearAll() override; bool updateMessageStatus(MessageId msg_id, MessageStatus status) override; + bool getMessage(MessageId msg_id, ChatMessage* out) const override; private: struct Record diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/log_store.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/log_store.h index 33bf7650..dc168e40 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/log_store.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/store/log_store.h @@ -50,6 +50,7 @@ class LogStore : public IChatStore void clearConversation(const ConversationId& conv) override; void clearAll() override; bool updateMessageStatus(MessageId msg_id, MessageStatus status) override; + bool getMessage(MessageId msg_id, ChatMessage* out) const override; private: fs::FS* fs_; diff --git a/platform/esp/arduino_common/library.json b/platform/esp/arduino_common/library.json index 1c39180f..875917a6 100644 --- a/platform/esp/arduino_common/library.json +++ b/platform/esp/arduino_common/library.json @@ -7,6 +7,10 @@ "build": { "includeDir": "include", "srcDir": "src", + "srcFilter": [ + "+<*>", + "-" + ], "flags": [ "-I../../../platform/shared/include", "-I../../../modules/ui_shared/include", diff --git a/platform/esp/arduino_common/src/app_config_store.cpp b/platform/esp/arduino_common/src/app_config_store.cpp index 73537ec2..15853df2 100644 --- a/platform/esp/arduino_common/src/app_config_store.cpp +++ b/platform/esp/arduino_common/src/app_config_store.cpp @@ -606,7 +606,6 @@ void log_config_delta(const AppConfig& before, const AppConfig& after) log_change_u8("net_util", before.net_channel_util, after.net_channel_util, any_change); log_change_u8("privacy_encrypt_mode", before.privacy_encrypt_mode, after.privacy_encrypt_mode, any_change); - log_change_bool("privacy_pki", before.privacy_pki, after.privacy_pki, any_change); log_change_u8("privacy_nmea_output", before.privacy_nmea_output, after.privacy_nmea_output, any_change); log_change_u8("privacy_nmea_sentence", before.privacy_nmea_sentence, after.privacy_nmea_sentence, any_change); @@ -669,12 +668,11 @@ void log_config_summary(const char* phase, const AppConfig& config) static_cast(config.map_track_format), bool_label(config.route_enabled), config.route_path); - Serial.printf("[AppCfg][%s][privacy] duty_cycle=%s net_util=%u encrypt=%u pki=%s nmea=%u sentence=%u\n", + Serial.printf("[AppCfg][%s][privacy] duty_cycle=%s net_util=%u encrypt=%u nmea=%u sentence=%u\n", safe_label(phase), bool_label(config.net_duty_cycle), static_cast(config.net_channel_util), static_cast(config.privacy_encrypt_mode), - bool_label(config.privacy_pki), static_cast(config.privacy_nmea_output), static_cast(config.privacy_nmea_sentence)); Serial.printf("[AppCfg][%s][aprs] enabled=%s igate=%s-%u tocall=%s path=%s pos_interval=%u self=%s self_call=%s node_map_len=%u\n", @@ -728,7 +726,6 @@ bool loadAppConfigFromPreferences(AppConfig& config, auto& net_duty_cycle = config.net_duty_cycle; auto& net_channel_util = config.net_channel_util; auto& privacy_encrypt_mode = config.privacy_encrypt_mode; - auto& privacy_pki = config.privacy_pki; auto& privacy_nmea_output = config.privacy_nmea_output; auto& privacy_nmea_sentence = config.privacy_nmea_sentence; auto& route_enabled = config.route_enabled; @@ -933,7 +930,6 @@ bool loadAppConfigFromPreferences(AppConfig& config, net_duty_cycle = get_bool("net_duty_cycle", net_duty_cycle); net_channel_util = get_uchar("net_util", net_channel_util); privacy_encrypt_mode = get_uchar("privacy_encrypt", privacy_encrypt_mode); - privacy_pki = get_bool("privacy_pki", privacy_pki); privacy_nmea_output = get_uchar("privacy_nmea", privacy_nmea_output); privacy_nmea_sentence = get_uchar(kSettingsKeyPrivacyNmeaSentence, privacy_nmea_sentence); route_enabled = get_bool("route_enabled", route_enabled); @@ -1043,7 +1039,6 @@ bool saveAppConfigToPreferences(AppConfig& config, auto& net_duty_cycle = config.net_duty_cycle; auto& net_channel_util = config.net_channel_util; auto& privacy_encrypt_mode = config.privacy_encrypt_mode; - auto& privacy_pki = config.privacy_pki; auto& privacy_nmea_output = config.privacy_nmea_output; auto& privacy_nmea_sentence = config.privacy_nmea_sentence; auto& route_enabled = config.route_enabled; @@ -1214,7 +1209,7 @@ bool saveAppConfigToPreferences(AppConfig& config, put_bool("net_duty_cycle", net_duty_cycle); put_uchar("net_util", net_channel_util); put_uchar("privacy_encrypt", privacy_encrypt_mode); - put_bool("privacy_pki", privacy_pki); + ok = remove_key_logged(prefs, "settings", "privacy_pki", emit_logs) && ok; put_uchar("privacy_nmea", privacy_nmea_output); put_uchar(kSettingsKeyPrivacyNmeaSentence, privacy_nmea_sentence); put_bool("route_enabled", route_enabled); diff --git a/platform/esp/arduino_common/src/chat/infra/mesh_adapter_router.cpp b/platform/esp/arduino_common/src/chat/infra/mesh_adapter_router.cpp index afe306cb..1a5ff087 100644 --- a/platform/esp/arduino_common/src/chat/infra/mesh_adapter_router.cpp +++ b/platform/esp/arduino_common/src/chat/infra/mesh_adapter_router.cpp @@ -181,12 +181,12 @@ void MeshAdapterRouter::setNetworkLimits(bool duty_cycle_enabled, uint8_t util_p } } -void MeshAdapterRouter::setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) +void MeshAdapterRouter::setPrivacyConfig(uint8_t encrypt_mode) { LockGuard lock(mutex_); if (lock.locked()) { - core_.setPrivacyConfig(encrypt_mode, pki_enabled); + core_.setPrivacyConfig(encrypt_mode); } } diff --git a/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp index 6bd89d61..adc216d7 100644 --- a/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp @@ -3241,14 +3241,10 @@ void MeshCoreAdapter::setNetworkLimits(bool duty_cycle_enabled, uint8_t util_per } } -void MeshCoreAdapter::setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) +void MeshCoreAdapter::setPrivacyConfig(uint8_t encrypt_mode) { encrypt_mode_ = encrypt_mode; - pki_enabled_ = pki_enabled; - if (encrypt_mode_ == 0) - { - pki_enabled_ = false; - } + pki_enabled_ = (encrypt_mode_ != 0); if (!pki_enabled_) { key_verify_session_ = KeyVerifySession{}; diff --git a/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp index 9fa5f15b..9d7816f5 100644 --- a/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/meshtastic/mt_adapter.cpp @@ -112,6 +112,15 @@ static const char* portName(uint32_t portnum) } } +bool shouldRequireDirectPki(uint8_t encrypt_mode, uint32_t dest_node, uint32_t portnum) +{ + // Match upstream Meshtastic: direct unicast app traffic uses PKI outside + // cleartext/Ham-style operation, and must not silently fall back to channel crypto. + return encrypt_mode != 0 && + dest_node != kBroadcastNodeId && + allowPkiForPortnum(portnum); +} + void mt_diag_log(const char* fmt, ...) { char buf[192] = {}; @@ -122,6 +131,40 @@ void mt_diag_log(const char* fmt, ...) Serial.print(buf); } +void mt_diag_dropf(const chat::meshtastic::PacketHeaderWire* header, + const char* reason, + const char* fmt = nullptr, + ...) +{ + char detail[96] = {}; + if (fmt && fmt[0] != '\0') + { + va_list args; + va_start(args, fmt); + vsnprintf(detail, sizeof(detail), fmt, args); + va_end(args); + } + + if (header) + { + mt_diag_log("[MT][RX_DROP] reason=%s from=%08lX to=%08lX id=%08lX ch=%u%s%s\n", + reason ? reason : "unknown", + static_cast(header->from), + static_cast(header->to), + static_cast(header->id), + static_cast(header->channel), + detail[0] ? " " : "", + detail); + } + else + { + mt_diag_log("[MT][RX_DROP] reason=%s%s%s\n", + reason ? reason : "unknown", + detail[0] ? " " : "", + detail); + } +} + using chat::meshtastic::computeChannelHash; using chat::meshtastic::expandShortPsk; using chat::meshtastic::hasValidPosition; @@ -329,7 +372,7 @@ bool MtAdapter::sendTextWithId(ChannelId channel, const std::string& text, pending.last_attempt = 0; send_queue_.push(pending); - mt_diag_log("[MT][TX] queue text id=%08lX dest=%08lX ch=%u len=%u\n", + mt_diag_log("[MT][TX] queue text id=%08lX dest=%08lX logical_ch=%u len=%u\n", static_cast(pending.msg_id), static_cast(pending.dest), static_cast(out_channel), @@ -418,11 +461,17 @@ bool MtAdapter::sendAppData(ChannelId channel, uint32_t portnum, const uint8_t* out_payload = data_buffer; size_t out_len = data_size; bool use_pki = false; - if (dest_node != 0xFFFFFFFF && pki_enabled_) + if (shouldRequireDirectPki(encrypt_mode_, dest_node, portnum)) { - if (!pki_ready_ || !allowPkiForPortnum(portnum) || - (node_public_keys_.find(dest_node) == node_public_keys_.end())) + const bool have_dest_key = node_public_keys_.find(dest_node) != node_public_keys_.end(); + if (!pki_ready_ || !have_dest_key) { + const char* reason = !pki_ready_ ? "pki_not_ready" : "pki_key_missing"; + mt_diag_log("[MT][TX_BLOCK] id=%08lX dest=%08lX port=%u reason=%s path=PKI\n", + static_cast(msg_id), + static_cast(dest_node), + static_cast(portnum), + reason); LORA_LOG("[LORA] TX app PKI required but unavailable dest=%08lX port=%u\n", (unsigned long)dest_node, (unsigned)portnum); @@ -433,6 +482,10 @@ bool MtAdapter::sendAppData(ChannelId channel, uint32_t portnum, size_t pki_len = sizeof(pki_buf); if (!encryptPkiPayload(dest_node, msg_id, data_buffer, data_size, pki_buf, &pki_len)) { + mt_diag_log("[MT][TX_BLOCK] id=%08lX dest=%08lX port=%u reason=pki_encrypt_fail path=PKI\n", + static_cast(msg_id), + static_cast(dest_node), + static_cast(portnum)); LORA_LOG("[LORA] TX app PKI encrypt failed dest=%08lX port=%u\n", (unsigned long)dest_node, (unsigned)portnum); @@ -462,6 +515,14 @@ bool MtAdapter::sendAppData(ChannelId channel, uint32_t portnum, } } + mt_diag_log("[MT][TX_ROUTE] id=%08lX dest=%08lX port=%u logical_ch=%u wire_ch=%u path=%s payload=%u\n", + static_cast(msg_id), + static_cast(dest_node), + static_cast(portnum), + static_cast(out_channel), + static_cast(channel_hash), + use_pki ? "PKI" : "CHANNEL", + static_cast(out_len)); if (!buildWirePacket(out_payload, out_len, node_id_, msg_id, dest_node, channel_hash, hop_limit, air_want_ack, psk, psk_len, wire_buffer, &wire_size)) @@ -1266,14 +1327,9 @@ void MtAdapter::setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) } } -void MtAdapter::setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) +void MtAdapter::setPrivacyConfig(uint8_t encrypt_mode) { encrypt_mode_ = encrypt_mode; - pki_enabled_ = pki_enabled; - if (encrypt_mode_ == 0) - { - pki_enabled_ = false; - } } void MtAdapter::setLastRxStats(float rssi, float snr) @@ -1333,12 +1389,18 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) if (!parseWirePacket(data, size, &header, payload, &payload_size)) { std::string raw_hex = toHex(data, size); + mt_diag_log("[MT][RX_DROP] reason=parse_fail len=%u\n", + static_cast(size)); LORA_LOG("[LORA] RX parse fail len=%u hex=%s\n", (unsigned)size, raw_hex.c_str()); return; } + const bool matches_primary_channel = (header.channel == primary_channel_hash_); + const bool matches_secondary_channel = + (secondary_psk_len_ > 0 && header.channel == secondary_channel_hash_); + std::string full_hex = toHex(data, size, size); LORA_LOG("[LORA] RX wire from=%08lX to=%08lX id=%08lX ch=0x%02X flags=0x%02X len=%u\n", (unsigned long)header.from, @@ -1348,33 +1410,31 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) header.flags, (unsigned)payload_size); const char* channel_kind = "UNKNOWN"; - if (header.channel == 0) - { - channel_kind = "PKI"; - } - else if (header.channel == primary_channel_hash_) + if (matches_primary_channel) { channel_kind = "PRIMARY"; } - else if (header.channel == secondary_channel_hash_) + else if (matches_secondary_channel) { channel_kind = "SECONDARY"; } + else if (header.channel == 0) + { + channel_kind = "ZERO_UNMATCHED"; + } LORA_LOG("[LORA] RX channel kind=%s hash=0x%02X\n", channel_kind, header.channel); LORA_LOG("[LORA] RX full packet hex: %s\n", full_hex.c_str()); // Check for duplicates if (dedup_.isDuplicate(header.from, header.id)) { + mt_diag_dropf(&header, "dedup"); LORA_LOG("[LORA] RX dedup from=%08lX id=%08lX\n", (unsigned long)header.from, (unsigned long)header.id); return; // Duplicate, ignore } - // Mark as seen - dedup_.markSeen(header.from, header.id); - chat::RxMeta rx_meta; rx_meta.rx_timestamp_ms = millis(); uint32_t epoch_s = chat::now_epoch_seconds(); @@ -1446,6 +1506,7 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) return; } + mt_diag_dropf(&header, "self_echo"); LORA_LOG("[LORA] RX self drop id=%08lX to=%08lX relay=%08lX ch=%u\n", static_cast(header.id), static_cast(header.to), @@ -1461,15 +1522,179 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) const uint8_t* psk = nullptr; size_t psk_len = 0; - bool unknown_channel = false; + meshtastic_Data decoded = meshtastic_Data_init_default; + ChannelId decoded_channel_id = ChannelId::PRIMARY; + bool used_pki_transport = false; + const bool can_try_pki = + (header.to == node_id_ && header.to != kBroadcastNodeId && payload_size > 12); + const char* last_drop_reason = nullptr; + char last_drop_detail[96] = {}; - if (header.channel == 0) + auto note_drop = [&](const char* reason, const char* detail = nullptr) { - if (header.to != node_id_ || header.to == 0xFFFFFFFF || payload_size <= 12) + last_drop_reason = reason; + if (detail && detail[0] != '\0') { - return; + std::snprintf(last_drop_detail, sizeof(last_drop_detail), "%s", detail); } - if (!pki_ready_) + else + { + last_drop_detail[0] = '\0'; + } + }; + + auto try_decode_candidate = [&](const char* path_name, + const uint8_t* candidate_psk, + size_t candidate_psk_len, + bool candidate_pki, + ChannelId candidate_channel) -> bool + { + uint8_t candidate_plaintext[256]; + size_t candidate_plaintext_len = sizeof(candidate_plaintext); + + if (candidate_pki) + { + if (!pki_ready_) + { + char detail[64]; + std::snprintf(detail, sizeof(detail), "path=%s", path_name); + note_drop("pki_not_ready", detail); + return false; + } + if (!decryptPkiPayload(header.from, + header.id, + payload, + payload_size, + candidate_plaintext, + &candidate_plaintext_len)) + { + char detail[64]; + std::snprintf(detail, sizeof(detail), "path=%s", path_name); + note_drop("pki_decrypt_fail", detail); + return false; + } + } + else if (candidate_psk && candidate_psk_len > 0) + { + if (!decryptPayload(header, + payload, + payload_size, + candidate_psk, + candidate_psk_len, + candidate_plaintext, + &candidate_plaintext_len)) + { + char detail[64]; + std::snprintf(detail, sizeof(detail), "path=%s", path_name); + note_drop("channel_decrypt_fail", detail); + return false; + } + } + else + { + if (payload_size > sizeof(candidate_plaintext)) + { + char detail[64]; + std::snprintf(detail, + sizeof(detail), + "path=%s len=%u", + path_name, + static_cast(payload_size)); + note_drop("payload_too_large", detail); + return false; + } + memcpy(candidate_plaintext, payload, payload_size); + candidate_plaintext_len = payload_size; + } + + meshtastic_Data candidate_decoded = meshtastic_Data_init_default; + pb_istream_t candidate_stream = + pb_istream_from_buffer(candidate_plaintext, candidate_plaintext_len); + if (!pb_decode(&candidate_stream, meshtastic_Data_fields, &candidate_decoded)) + { + char detail[64]; + std::snprintf(detail, sizeof(detail), "path=%s", path_name); + note_drop("data_decode_fail", detail); + return false; + } + + memcpy(plaintext, candidate_plaintext, candidate_plaintext_len); + plaintext_len = candidate_plaintext_len; + decoded = candidate_decoded; + psk = candidate_psk; + psk_len = candidate_psk_len; + decoded_channel_id = candidate_channel; + used_pki_transport = candidate_pki; + if (header.channel == 0) + { + mt_diag_log("[MT][RX_ROUTE] id=%08lX ch=0 path=%s port=%u\n", + static_cast(header.id), + path_name, + static_cast(decoded.portnum)); + } + return true; + }; + + bool decoded_ok = false; + if (matches_primary_channel) + { + decoded_ok = try_decode_candidate("PRIMARY", + primary_psk_, + primary_psk_len_, + false, + ChannelId::PRIMARY); + } + if (!decoded_ok && matches_secondary_channel) + { + decoded_ok = try_decode_candidate("SECONDARY", + secondary_psk_, + secondary_psk_len_, + false, + ChannelId::SECONDARY); + } + if (!decoded_ok && can_try_pki) + { + decoded_ok = try_decode_candidate("PKI", + nullptr, + 0, + true, + matches_secondary_channel ? ChannelId::SECONDARY + : ChannelId::PRIMARY); + } + + if (!decoded_ok) + { + if (!(matches_primary_channel || matches_secondary_channel) && !can_try_pki) + { + char detail[64]; + std::snprintf(detail, + sizeof(detail), + "primary=0x%02X secondary=0x%02X", + static_cast(primary_channel_hash_), + static_cast(secondary_channel_hash_)); + note_drop("unknown_channel", detail); + } + + if (last_drop_reason) + { + mt_diag_dropf(&header, last_drop_reason, "%s", last_drop_detail); + } + else + { + mt_diag_dropf(&header, "decode_failed"); + } + + std::string cipher_hex = toHex(payload, payload_size, payload_size); + if (!(matches_primary_channel || matches_secondary_channel) && !can_try_pki) + { + LORA_LOG("[LORA] RX unknown channel hash=0x%02X from=%08lX id=%08lX len=%u hex=%s (skip decode)\n", + header.channel, + (unsigned long)header.from, + (unsigned long)header.id, + (unsigned)payload_size, + cipher_hex.c_str()); + } + else if (last_drop_reason && std::strcmp(last_drop_reason, "pki_not_ready") == 0) { LORA_LOG("[LORA] RX PKI drop (not ready) from=%08lX id=%08lX len=%u\n", (unsigned long)header.from, @@ -1479,80 +1704,68 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) sendRoutingError(header.from, header.id, primary_channel_hash_, primary_psk_, primary_psk_len_, meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY); - return; } - if (!decryptPkiPayload(header.from, header.id, payload, payload_size, plaintext, &plaintext_len)) + else if (last_drop_reason && std::strcmp(last_drop_reason, "pki_decrypt_fail") == 0) { - std::string cipher_hex = toHex(payload, payload_size); + forgetNodePublicKey(header.from); + sendNodeInfoTo(header.from, true, ChannelId::PRIMARY); + sendRoutingError(header.from, header.id, primary_channel_hash_, + primary_psk_, primary_psk_len_, + meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY); + mt_diag_log("[MT][PKI_RESYNC] node=%08lX action=forget_key+request_nodeinfo\n", + static_cast(header.from)); LORA_LOG("[LORA] RX PKI decrypt fail from=%08lX id=%08lX len=%u hex=%s\n", (unsigned long)header.from, (unsigned long)header.id, (unsigned)payload_size, cipher_hex.c_str()); - return; } - } - else - { - if (header.channel == primary_channel_hash_) + else if (last_drop_reason && std::strcmp(last_drop_reason, "channel_decrypt_fail") == 0) { - psk = primary_psk_; - psk_len = primary_psk_len_; - } - else if (header.channel == secondary_channel_hash_) - { - psk = secondary_psk_; - psk_len = secondary_psk_len_; + LORA_LOG("[LORA] RX decrypt fail id=%08lX ch=0x%02X psk=%u len=%u hex=%s\n", + (unsigned long)header.id, + header.channel, + (unsigned)psk_len, + (unsigned)payload_size, + cipher_hex.c_str()); } else { - std::string cipher_hex = toHex(payload, payload_size); - LORA_LOG("[LORA] RX unknown channel hash=0x%02X from=%08lX id=%08lX len=%u hex=%s (skip decode)\n", - header.channel, - (unsigned long)header.from, + LORA_LOG("[LORA] RX data decode fail id=%08lX len=%u hex=%s\n", (unsigned long)header.id, (unsigned)payload_size, cipher_hex.c_str()); - unknown_channel = true; - } - - if (unknown_channel) - { - return; - } - - if (psk && psk_len > 0) - { - if (!decryptPayload(header, payload, payload_size, psk, psk_len, plaintext, &plaintext_len)) - { - std::string cipher_hex = toHex(payload, payload_size, payload_size); - LORA_LOG("[LORA] RX decrypt fail id=%08lX ch=0x%02X psk=%u len=%u hex=%s\n", - (unsigned long)header.id, - header.channel, - (unsigned)psk_len, - (unsigned)payload_size, - cipher_hex.c_str()); - return; - } - } - else - { - memcpy(plaintext, payload, payload_size); - plaintext_len = payload_size; - } - - // Log decrypted protobuf payload (meshtastic_Data wire format) - if (plaintext_len > 0) - { - std::string protobuf_hex = toHex(plaintext, plaintext_len, plaintext_len); - LORA_LOG("[LORA] RX protobuf hex: %s\n", protobuf_hex.c_str()); } + return; } - meshtastic_Data decoded = meshtastic_Data_init_default; - pb_istream_t stream = pb_istream_from_buffer(plaintext, plaintext_len); - if (pb_decode(&stream, meshtastic_Data_fields, &decoded)) + // Only mark packets as seen after we have successfully identified and decoded them. + // This avoids poisoning dedup for retries when a packet failed due to stale PKI state. + dedup_.markSeen(header.from, header.id); + + if (plaintext_len > 0) { + std::string protobuf_hex = toHex(plaintext, plaintext_len, plaintext_len); + LORA_LOG("[LORA] RX protobuf hex: %s\n", protobuf_hex.c_str()); + } + + { + const bool decoded_is_broadcast = (header.to == kBroadcastNodeId); + const bool decoded_want_response = decoded.want_response || + (decoded.has_bitfield && + ((decoded.bitfield & kBitfieldWantResponseMask) != 0)); + mt_diag_log("[MT][RX_DECODE] from=%08lX to=%08lX id=%08lX ch=%u port=%u(%s) payload=%u bcast=%u resp=%u pki=%u\n", + static_cast(header.from), + static_cast(header.to), + static_cast(header.id), + static_cast(header.channel), + static_cast(decoded.portnum), + portName(decoded.portnum), + static_cast(decoded.payload.size), + decoded_is_broadcast ? 1U : 0U, + decoded_want_response ? 1U : 0U, + used_pki_transport ? 1U : 0U); + LORA_LOG("[LORA] RX data portnum=%u (%s) payload=%u\n", (unsigned)decoded.portnum, portName(decoded.portnum), @@ -1671,11 +1884,19 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) bool published = sys::EventBus::publish(event, 0); if (published) { + mt_diag_log("[MT][RX_NODEINFO] from=%08lX node=%08lX mode=nodeinfo published=1\n", + static_cast(header.from), + static_cast(node_id)); LORA_LOG("[LORA] NodeInfo event published node=%08lX\n", (unsigned long)node_id); } else { + mt_diag_dropf(&header, + "nodeinfo_event_drop", + "node=%08lX pending=%u", + static_cast(node_id), + static_cast(sys::EventBus::pendingCount())); LORA_LOG("[LORA] NodeInfo event dropped node=%08lX pending=%u\n", (unsigned long)node_id, static_cast(sys::EventBus::pendingCount())); @@ -1737,11 +1958,19 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) bool published = sys::EventBus::publish(event, 0); if (published) { + mt_diag_log("[MT][RX_NODEINFO] from=%08lX node=%08lX mode=user published=1\n", + static_cast(header.from), + static_cast(node_id)); LORA_LOG("[LORA] NodeInfo event published node=%08lX\n", (unsigned long)node_id); } else { + mt_diag_dropf(&header, + "user_event_drop", + "node=%08lX pending=%u", + static_cast(node_id), + static_cast(sys::EventBus::pendingCount())); LORA_LOG("[LORA] NodeInfo event dropped node=%08lX pending=%u\n", (unsigned long)node_id, static_cast(sys::EventBus::pendingCount())); @@ -1772,10 +2001,15 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) pb_istream_from_buffer(decoded.payload.bytes, decoded.payload.size); if (pb_decode(&pstream, meshtastic_Position_fields, &pos)) { + mt_diag_log("[MT][RX_POSITION] from=%08lX id=%08lX payload=%u\n", + static_cast(header.from), + static_cast(header.id), + static_cast(decoded.payload.size)); publishPositionEvent(header.from, pos); } else { + mt_diag_dropf(&header, "position_decode_fail"); LORA_LOG("[LORA] RX Position decode fail from=%08lX err=%s\n", (unsigned long)header.from, PB_GET_ERROR(&pstream)); @@ -1844,6 +2078,12 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) meshtastic_KeyVerification kv = meshtastic_KeyVerification_init_default; if (decodeKeyVerificationMessage(plaintext, plaintext_len, &kv)) { + mt_diag_log("[MT][KEY_VERIFY] from=%08lX id=%08lX stage=%s hash1=%u hash2=%u\n", + static_cast(header.from), + static_cast(header.id), + keyVerificationStage(kv), + static_cast(kv.hash1.size), + static_cast(kv.hash2.size)); LORA_LOG("[LORA] RX key verification from=%08lX nonce=%llu hash1=%u hash2=%u stage=%s\n", (unsigned long)header.from, static_cast(kv.nonce), @@ -1870,12 +2110,17 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) } if (!handled) { + mt_diag_log("[MT][KEY_VERIFY] from=%08lX id=%08lX handled=0 stage=%s\n", + static_cast(header.from), + static_cast(header.id), + keyVerificationStage(kv)); LORA_LOG("[LORA] RX key verification ignored stage=%s\n", keyVerificationStage(kv)); } } else { + mt_diag_dropf(&header, "key_verify_decode_fail"); LORA_LOG("[LORA] RX key verification decode fail from=%08lX\n", (unsigned long)header.from); } @@ -1892,8 +2137,7 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) bool is_nodeinfo_port = (decoded.portnum == meshtastic_PortNum_NODEINFO_APP); bool is_position_port = (decoded.portnum == meshtastic_PortNum_POSITION_APP); bool is_traceroute_port = (decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP); - ChannelId channel_id = - (header.channel == secondary_channel_hash_) ? ChannelId::SECONDARY : ChannelId::PRIMARY; + ChannelId channel_id = decoded_channel_id; if (header.channel != 0 && header.from != node_id_) { node_last_channel_[header.from] = channel_id; @@ -2015,48 +2259,67 @@ void MtAdapter::processReceivedPacket(const uint8_t* data, size_t size) if (app_receive_queue_.size() < MAX_APP_QUEUE) { app_receive_queue_.push(incoming); + mt_diag_log("[MT][RX_APP] from=%08lX to=%08lX id=%08lX port=%u(%s) queued=1 depth=%u\n", + static_cast(incoming.from), + static_cast(incoming.to), + static_cast(incoming.packet_id), + static_cast(incoming.portnum), + portName(incoming.portnum), + static_cast(app_receive_queue_.size())); + } + else + { + mt_diag_dropf(&header, + "app_queue_full", + "port=%u depth=%u", + static_cast(incoming.portnum), + static_cast(app_receive_queue_.size())); } } - } - else - { - std::string plain_hex = toHex(plaintext, plaintext_len); - LORA_LOG("[LORA] RX data decode fail id=%08lX err=%s len=%u hex=%s\n", - (unsigned long)header.id, - PB_GET_ERROR(&stream), - (unsigned)plaintext_len, - plain_hex.c_str()); - } - // Decode Data message - MeshIncomingText incoming; - if (decodeTextMessage(plaintext, plaintext_len, &incoming)) - { - // Fill in packet info from header - incoming.from = header.from; - incoming.to = header.to; - incoming.msg_id = header.id; - if (header.channel == secondary_channel_hash_) + if (is_text_port) { - incoming.channel = ChannelId::SECONDARY; - } - else - { - incoming.channel = ChannelId::PRIMARY; - } - incoming.hop_limit = header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; - incoming.encrypted = (psk != nullptr && psk_len > 0); - incoming.rx_meta = rx_meta; + MeshIncomingText incoming; + if (decodeTextPayload(decoded, &incoming)) + { + incoming.from = header.from; + incoming.to = header.to; + incoming.msg_id = header.id; + incoming.channel = decoded_channel_id; + incoming.hop_limit = header.flags & PACKET_FLAGS_HOP_LIMIT_MASK; + incoming.encrypted = used_pki_transport || (psk != nullptr && psk_len > 0); + incoming.rx_meta = rx_meta; - receive_queue_.push(incoming); - LORA_LOG("[LORA] RX text from=%08lX id=%08lX ch=%u len=%u\n", - (unsigned long)incoming.from, - (unsigned long)incoming.msg_id, - static_cast(incoming.channel), - (unsigned)incoming.text.size()); - if (!incoming.text.empty()) - { - LORA_LOG("[LORA] RX text msg='%s'\n", incoming.text.c_str()); + receive_queue_.push(incoming); + mt_diag_log("[MT][RX_TEXT] from=%08lX to=%08lX id=%08lX ch=%u len=%u\n", + static_cast(incoming.from), + static_cast(incoming.to), + static_cast(incoming.msg_id), + static_cast(incoming.channel), + static_cast(incoming.text.size())); + LORA_LOG("[LORA] RX text from=%08lX id=%08lX ch=%u len=%u\n", + (unsigned long)incoming.from, + (unsigned long)incoming.msg_id, + static_cast(incoming.channel), + (unsigned)incoming.text.size()); + if (!incoming.text.empty()) + { + LORA_LOG("[LORA] RX text msg='%s'\n", incoming.text.c_str()); + } + } + else + { + mt_diag_dropf(&header, + "text_decode_fail", + "port=%u payload=%u", + static_cast(decoded.portnum), + static_cast(decoded.payload.size)); + LORA_LOG("[LORA] RX text decode fail from=%08lX id=%08lX port=%u payload=%u\n", + (unsigned long)header.from, + (unsigned long)header.id, + static_cast(decoded.portnum), + static_cast(decoded.payload.size)); + } } } } @@ -2214,17 +2477,24 @@ bool MtAdapter::sendPacket(const PendingSend& pending) bool track_ack = true; bool air_want_ack = shouldSetAirWantAck(dest, track_ack); - // Try PKI encryption for direct messages when key is known + // Upstream Meshtastic requires PKI for direct unicast traffic on + // non-infrastructure ports and rejects legacy channel-encrypted DMs. const uint8_t* payload = data_buffer; size_t payload_len = data_size; const uint8_t* psk = nullptr; size_t psk_len = 0; bool use_pki = false; - if (dest != 0xFFFFFFFF && pki_enabled_) + if (shouldRequireDirectPki(encrypt_mode_, dest, pending.portnum)) { - if (!pki_ready_ || !allowPkiForPortnum(pending.portnum) || - (node_public_keys_.find(dest) == node_public_keys_.end())) + const bool have_dest_key = node_public_keys_.find(dest) != node_public_keys_.end(); + if (!pki_ready_ || !have_dest_key) { + const char* reason = !pki_ready_ ? "pki_not_ready" : "pki_key_missing"; + mt_diag_log("[MT][TX_BLOCK] id=%08lX dest=%08lX port=%u reason=%s path=PKI\n", + static_cast(pending.msg_id), + static_cast(dest), + static_cast(pending.portnum), + reason); LORA_LOG("[LORA] TX text PKI required but unavailable dest=%08lX\n", (unsigned long)dest); return false; @@ -2233,6 +2503,10 @@ bool MtAdapter::sendPacket(const PendingSend& pending) size_t pki_len = sizeof(pki_buf); if (!encryptPkiPayload(dest, pending.msg_id, data_buffer, data_size, pki_buf, &pki_len)) { + mt_diag_log("[MT][TX_BLOCK] id=%08lX dest=%08lX port=%u reason=pki_encrypt_fail path=PKI\n", + static_cast(pending.msg_id), + static_cast(dest), + static_cast(pending.portnum)); LORA_LOG("[LORA] TX text PKI encrypt failed dest=%08lX\n", (unsigned long)dest); return false; @@ -2272,6 +2546,14 @@ bool MtAdapter::sendPacket(const PendingSend& pending) (unsigned)psk_len, (channel_hash == 0) ? 1U : 0U, (unsigned long)dest); + mt_diag_log("[MT][TX_ROUTE] id=%08lX dest=%08lX port=%u logical_ch=%u wire_ch=%u path=%s payload=%u\n", + static_cast(pending.msg_id), + static_cast(dest), + static_cast(pending.portnum), + static_cast(channel), + static_cast(channel_hash), + use_pki ? "PKI" : "CHANNEL", + static_cast(payload_len)); if (!buildWirePacket(payload, payload_len, from_node, pending.msg_id, dest, channel_hash, hop_limit, air_want_ack, @@ -3153,15 +3435,26 @@ bool MtAdapter::decryptPkiPayload(uint32_t from, uint32_t packet_id, { if (!cipher || cipher_len <= 12 || !out_plain || !out_plain_len) { + mt_diag_log("[MT][PKI] decrypt_skip from=%08lX id=%08lX reason=bad_args len=%u\n", + static_cast(from), + static_cast(packet_id), + static_cast(cipher_len)); return false; } if (!pki_ready_) { + mt_diag_log("[MT][PKI] decrypt_skip from=%08lX id=%08lX reason=not_ready len=%u\n", + static_cast(from), + static_cast(packet_id), + static_cast(cipher_len)); return false; } auto it = node_public_keys_.find(from); if (it == node_public_keys_.end()) { + mt_diag_log("[MT][PKI] decrypt_skip from=%08lX id=%08lX reason=key_missing\n", + static_cast(from), + static_cast(packet_id)); LORA_LOG("[LORA] PKI key missing for %08lX\n", (unsigned long)from); sendNodeInfoTo(from, true, ChannelId::PRIMARY); sendRoutingError(from, packet_id, primary_channel_hash_, @@ -3179,6 +3472,9 @@ bool MtAdapter::decryptPkiPayload(uint32_t from, uint32_t packet_id, memcpy(local_priv, pki_private_key_.data(), sizeof(local_priv)); if (!Curve25519::dh2(shared, local_priv)) { + mt_diag_log("[MT][PKI] decrypt_fail from=%08lX id=%08lX reason=dh2\n", + static_cast(from), + static_cast(packet_id)); return false; } @@ -3187,6 +3483,7 @@ bool MtAdapter::decryptPkiPayload(uint32_t from, uint32_t packet_id, const uint8_t* auth = cipher + (cipher_len - 12); uint32_t extra_nonce = 0; memcpy(&extra_nonce, auth + 8, sizeof(extra_nonce)); + std::string key_fp = toHex(it->second.data(), it->second.size(), 8); uint8_t nonce[16]; uint64_t packet_id64 = static_cast(packet_id); @@ -3196,12 +3493,22 @@ bool MtAdapter::decryptPkiPayload(uint32_t from, uint32_t packet_id, if (*out_plain_len < plain_len) { *out_plain_len = plain_len; + mt_diag_log("[MT][PKI] decrypt_skip from=%08lX id=%08lX reason=buffer_small need=%u\n", + static_cast(from), + static_cast(packet_id), + static_cast(plain_len)); return false; } if (!decryptPkiAesCcm(shared, sizeof(shared), nonce, 8, cipher, plain_len, nullptr, 0, auth, out_plain)) { + mt_diag_log("[MT][PKI] decrypt_fail from=%08lX id=%08lX reason=ccm_auth key=%s extra_nonce=%08lX len=%u\n", + static_cast(from), + static_cast(packet_id), + key_fp.c_str(), + static_cast(extra_nonce), + static_cast(cipher_len)); return false; } diff --git a/platform/esp/arduino_common/src/chat/infra/store/flash_store.cpp b/platform/esp/arduino_common/src/chat/infra/store/flash_store.cpp index 00e39c7c..0cf73f81 100644 --- a/platform/esp/arduino_common/src/chat/infra/store/flash_store.cpp +++ b/platform/esp/arduino_common/src/chat/infra/store/flash_store.cpp @@ -280,6 +280,44 @@ bool FlashStore::updateMessageStatus(MessageId msg_id, MessageStatus status) return false; } +bool FlashStore::getMessage(MessageId msg_id, ChatMessage* out) const +{ + if (!ready_ || msg_id == 0) + { + return false; + } + + for (size_t i = 0; i < count_; ++i) + { + size_t idx = (head_ + kMaxMessages - 1 - i) % kMaxMessages; + const Record& rec = records_[idx]; + if (rec.text_len == 0) + { + continue; + } + if (rec.msg_id != msg_id) + { + continue; + } + + if (out) + { + ChatMessage msg; + msg.protocol = static_cast(rec.protocol); + msg.channel = static_cast(rec.channel); + msg.from = rec.from; + msg.peer = rec.peer; + msg.msg_id = rec.msg_id; + msg.timestamp = rec.timestamp; + msg.text.assign(rec.text, rec.text_len); + msg.status = static_cast(rec.status); + *out = msg; + } + return true; + } + return false; +} + void FlashStore::loadFromPrefs() { uint8_t ver = prefs_.getUChar(kKeyVer, 0); diff --git a/platform/esp/arduino_common/src/chat/infra/store/log_store.cpp b/platform/esp/arduino_common/src/chat/infra/store/log_store.cpp index 4701c8ef..562ce6fd 100644 --- a/platform/esp/arduino_common/src/chat/infra/store/log_store.cpp +++ b/platform/esp/arduino_common/src/chat/infra/store/log_store.cpp @@ -432,6 +432,80 @@ bool LogStore::updateMessageStatus(MessageId msg_id, MessageStatus status) return updated; } +bool LogStore::getMessage(MessageId msg_id, ChatMessage* out) const +{ + if (!fs_ || msg_id == 0) + { + return false; + } + + std::vector entries; + if (!const_cast(this)->readIndex(entries)) + { + return false; + } + + for (const auto& entry : entries) + { + ConversationId conv(static_cast(entry.channel), + entry.peer, + static_cast(entry.protocol)); + char path[64]; + buildConversationPath(conv, path, sizeof(path)); + if (!fs_->exists(path)) + { + continue; + } + + File rf = fs_->open(path, FILE_READ); + if (!rf) + { + continue; + } + + FileHeader header{}; + if (!const_cast(this)->loadFileHeader(rf, header)) + { + rf.close(); + continue; + } + + for (uint16_t i = 0; i < header.count; ++i) + { + uint16_t slot = + static_cast((header.head + kMaxMessagesPerConv - header.count + i) % + kMaxMessagesPerConv); + Record rec{}; + if (!const_cast(this)->readRecord(rf, slot, rec)) + { + continue; + } + if (rec.text_len == 0 || rec.msg_id != msg_id) + { + continue; + } + + if (out) + { + ChatMessage msg; + msg.protocol = static_cast(rec.protocol); + msg.channel = static_cast(rec.channel); + msg.from = rec.from; + msg.peer = rec.peer; + msg.msg_id = rec.msg_id; + msg.timestamp = rec.timestamp; + msg.text.assign(rec.text, rec.text_len); + msg.status = static_cast(rec.status); + *out = msg; + } + rf.close(); + return true; + } + rf.close(); + } + return false; +} + bool LogStore::ensureDir() { if (!fs_) return false; diff --git a/platform/esp/arduino_common/src/platform_ui_device_runtime.cpp b/platform/esp/arduino_common/src/platform_ui_device_runtime.cpp index 535e3cd3..db563ab1 100644 --- a/platform/esp/arduino_common/src/platform_ui_device_runtime.cpp +++ b/platform/esp/arduino_common/src/platform_ui_device_runtime.cpp @@ -1,6 +1,8 @@ #include "platform/ui/device_runtime.h" #include "board/BoardBase.h" +#include "esp_heap_caps.h" +#include "esp_ota_ops.h" #include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -33,6 +35,23 @@ BatteryInfo battery_info() return info; } +MemoryStats memory_stats() +{ + MemoryStats stats{}; + stats.ram_total_bytes = heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + stats.ram_free_bytes = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + stats.psram_total_bytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + stats.psram_free_bytes = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + stats.psram_available = stats.psram_total_bytes > 0; + return stats; +} + +const char* firmware_version() +{ + const esp_app_desc_t* desc = esp_ota_get_app_description(); + return (desc && desc->version[0] != '\0') ? desc->version : "unknown"; +} + void handle_low_battery(const BatteryInfo& info) { if (!info.available || info.level < 0) diff --git a/platform/esp/arduino_common/src/platform_ui_wifi_runtime.cpp b/platform/esp/arduino_common/src/platform_ui_wifi_runtime.cpp new file mode 100644 index 00000000..4d10859e --- /dev/null +++ b/platform/esp/arduino_common/src/platform_ui_wifi_runtime.cpp @@ -0,0 +1 @@ +#include "platform/esp/common/wifi_runtime_impl.h" diff --git a/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp b/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp index 9c0974c4..8a4442bd 100644 --- a/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp +++ b/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp @@ -11,6 +11,7 @@ #include "src/misc/cache/instance/lv_image_cache.h" #include "sys/clock.h" #include "ui/page/page_profile.h" +#include "ui/runtime/memory_profile.h" #include "ui/screens/gps/gps_constants.h" #include "ui/support/lvgl_fs_utils.h" #include @@ -183,7 +184,10 @@ size_t tile_record_limit(const TileContext& ctx) size_t tile_decode_cache_limit(const TileContext& ctx) { const size_t desired = std::max(TILE_CACHE_LIMIT, tile_object_cache_limit(ctx)); - return std::min(desired, static_cast(TILE_DECODE_CACHE_SIZE)); + const size_t profile_limit = + std::max(1U, ::ui::runtime::current_memory_profile().max_map_decode_tiles); + return std::min(std::min(desired, profile_limit), + static_cast(TILE_DECODE_CACHE_SIZE)); } const char* major_contour_profile_for_zoom(int z) @@ -1964,10 +1968,15 @@ void cleanup_tiles(TileContext& ctx) reset_tile_runtime(tile); } ctx.tiles->clear(); - // Keep vector capacity and decoded tile cache alive across page exits. - // Late LVGL draw work can still reference image internals for a short time, - // so aggressively freeing image caches here is unsafe. - release_tile_decode_cache_usage(); + if (::ui::runtime::current_memory_profile().retain_map_decode_cache_on_page_exit) + { + // Extended-memory boards can keep decoded tiles hot across page exits. + release_tile_decode_cache_usage(); + } + else + { + clear_tile_decode_cache(); + } g_active_map_source = 0xFF; g_active_contour_enabled = false; g_missing_tile_notice_pending = false; diff --git a/platform/esp/common/include/platform/esp/common/settings_store_impl.h b/platform/esp/common/include/platform/esp/common/settings_store_impl.h index 4c2a327b..528f2286 100644 --- a/platform/esp/common/include/platform/esp/common/settings_store_impl.h +++ b/platform/esp/common/include/platform/esp/common/settings_store_impl.h @@ -21,7 +21,9 @@ struct StorageKeyAlias }; constexpr StorageKeyAlias kStorageKeyAliases[] = { + // Legacy key kept for one-shot locale migration from int -> locale id. {"display_language", "disp_lang"}, + {"display_locale", "disp_locale"}, {"screen_brightness", "screen_bright"}, {"vibration_enabled", "vibe_enabled"}, {"gauge_design_mah", "gauge_dsgn"}, @@ -123,6 +125,12 @@ inline bool commit_and_close(nvs_handle_t handle) return ok; } +inline bool erase_key_if_present(nvs_handle_t handle, const char* storage_key) +{ + const esp_err_t err = nvs_erase_key(handle, storage_key); + return err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND; +} + } // namespace platform::esp::common::settings_store_detail namespace platform::ui::settings_store @@ -248,6 +256,69 @@ void put_uint(const char* ns, const char* key, uint32_t value) bool_label(ok)); } +bool put_string(const char* ns, const char* key, const char* value) +{ + using namespace ::platform::esp::common::settings_store_detail; + + if (!key || !value) + { + return false; + } + + const char* storage_key = resolve_storage_key(key); + if (!validate_storage_key("WRITE", ns, key, storage_key)) + { + return false; + } + + nvs_handle_t handle = 0; + if (!open_namespace(ns, false, &handle)) + { + log_open_failure("WRITE", ns, key, storage_key); + return false; + } + + bool ok = false; + if (value[0] == '\0') + { + ok = erase_key_if_present(handle, storage_key); + if (ok) + { + ok = commit_and_close(handle); + } + else + { + nvs_close(handle); + } + + logf("[CfgStore][REMOVE] ns=%s key=%s storage_key=%s type=string ok=%s\n", + safe_label(ns), + safe_label(key), + safe_label(storage_key), + bool_label(ok)); + return ok; + } + + ok = nvs_set_str(handle, storage_key, value) == ESP_OK; + if (ok) + { + ok = commit_and_close(handle); + } + else + { + nvs_close(handle); + } + + logf("[CfgStore][WRITE] ns=%s key=%s storage_key=%s type=string len=%lu value=%s ok=%s\n", + safe_label(ns), + safe_label(key), + safe_label(storage_key), + static_cast(std::strlen(value)), + safe_label(value), + bool_label(ok)); + return ok; +} + bool put_blob(const char* ns, const char* key, const void* data, std::size_t len) { using namespace ::platform::esp::common::settings_store_detail; @@ -273,7 +344,7 @@ bool put_blob(const char* ns, const char* key, const void* data, std::size_t len bool ok = false; if (len == 0) { - ok = nvs_erase_key(handle, storage_key) == ESP_OK; + ok = erase_key_if_present(handle, storage_key); if (ok) { ok = commit_and_close(handle); @@ -418,6 +489,74 @@ uint32_t get_uint(const char* ns, const char* key, uint32_t default_value) return value; } +bool get_string(const char* ns, const char* key, std::string& out) +{ + using namespace ::platform::esp::common::settings_store_detail; + + out.clear(); + if (!key) + { + return false; + } + + const char* storage_key = resolve_storage_key(key); + if (!validate_storage_key("READ", ns, key, storage_key)) + { + return false; + } + + nvs_handle_t handle = 0; + if (!open_namespace(ns, true, &handle)) + { + log_open_failure("READ", ns, key, storage_key); + return false; + } + + std::size_t len = 0; + const esp_err_t len_err = nvs_get_str(handle, storage_key, nullptr, &len); + if (len_err != ESP_OK || len == 0) + { + logf("[CfgStore][READ] ns=%s key=%s storage_key=%s type=string source=%s len=0\n", + safe_label(ns), + safe_label(key), + safe_label(storage_key), + len_err == ESP_OK ? "empty" : "missing"); + nvs_close(handle); + return false; + } + + std::string buffer(len, '\0'); + std::size_t read = len; + const bool ok = + nvs_get_str(handle, storage_key, buffer.empty() ? nullptr : &buffer[0], &read) == ESP_OK && + read == len; + nvs_close(handle); + + if (!ok) + { + logf("[CfgStore][READ] ns=%s key=%s storage_key=%s type=string source=stored len=%lu ok=false\n", + safe_label(ns), + safe_label(key), + safe_label(storage_key), + static_cast(len)); + return false; + } + + if (!buffer.empty() && buffer.back() == '\0') + { + buffer.pop_back(); + } + out = std::move(buffer); + + logf("[CfgStore][READ] ns=%s key=%s storage_key=%s type=string source=stored len=%lu value=%s ok=true\n", + safe_label(ns), + safe_label(key), + safe_label(storage_key), + static_cast(out.size()), + safe_label(out.c_str())); + return true; +} + bool get_blob(const char* ns, const char* key, std::vector& out) { using namespace ::platform::esp::common::settings_store_detail; @@ -505,7 +644,7 @@ void remove_keys(const char* ns, const char* const* keys, std::size_t key_count) continue; } - const bool ok = nvs_erase_key(handle, storage_key) == ESP_OK; + const bool ok = erase_key_if_present(handle, storage_key); changed = changed || ok; logf("[CfgStore][REMOVE] ns=%s key=%s storage_key=%s ok=%s\n", safe_label(ns), diff --git a/platform/esp/common/include/platform/esp/common/wifi_runtime_impl.h b/platform/esp/common/include/platform/esp/common/wifi_runtime_impl.h new file mode 100644 index 00000000..51a608a2 --- /dev/null +++ b/platform/esp/common/include/platform/esp/common/wifi_runtime_impl.h @@ -0,0 +1,652 @@ +#pragma once + +#include "platform/ui/settings_store.h" +#include "platform/ui/wifi_runtime.h" + +#include +#include +#include +#include + +#include "esp_err.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#if defined(TRAIL_MATE_ESP_BOARD_TAB5) +#include "bsp/m5stack_tab5.h" +#endif + +namespace platform::ui::wifi +{ +namespace +{ + +constexpr const char* kSettingsNs = "settings"; +constexpr const char* kWifiEnabledKey = "wifi_enabled"; +constexpr const char* kWifiSsidKey = "wifi_ssid"; +constexpr const char* kWifiPasswordKey = "wifi_password"; + +struct RuntimeState +{ + bool stack_ready = false; + bool handlers_registered = false; + bool wifi_started = false; + bool wifi_initialized = false; + bool config_cached = false; + bool connected = false; + bool connecting = false; + bool scanning = false; + int rssi = -127; + Config config{}; + char ssid[kMaxSsidLength + 1] = {}; + char ip[kMaxIpLength + 1] = {}; + char message[kMaxStatusMessageLength + 1] = {}; + esp_netif_t* sta_netif = nullptr; + esp_event_handler_instance_t wifi_event_handler = nullptr; + esp_event_handler_instance_t ip_event_handler = nullptr; +}; + +RuntimeState s_runtime{}; + +void copy_bounded(char* out, std::size_t out_len, const char* text) +{ + if (!out || out_len == 0) + { + return; + } + + const char* source = text ? text : ""; + std::snprintf(out, out_len, "%s", source); +} + +void set_status_message(const char* message) +{ + copy_bounded(s_runtime.message, sizeof(s_runtime.message), message); +} + +void cache_config(const Config& config) +{ + s_runtime.config = config; + s_runtime.config_cached = true; +} + +bool read_config_from_store(Config& out) +{ + out = Config{}; + out.enabled = ::platform::ui::settings_store::get_bool(kSettingsNs, kWifiEnabledKey, false); + + std::string value; + if (::platform::ui::settings_store::get_string(kSettingsNs, kWifiSsidKey, value)) + { + copy_bounded(out.ssid, sizeof(out.ssid), value.c_str()); + } + + value.clear(); + if (::platform::ui::settings_store::get_string(kSettingsNs, kWifiPasswordKey, value)) + { + copy_bounded(out.password, sizeof(out.password), value.c_str()); + } + + cache_config(out); + return true; +} + +const Config& current_config() +{ + if (!s_runtime.config_cached) + { + Config config{}; + (void)read_config_from_store(config); + } + return s_runtime.config; +} + +bool has_saved_credentials(const Config& config) +{ + return config.ssid[0] != '\0'; +} + +ConnectionState disconnected_state(bool enabled, bool has_credentials) +{ + if (!enabled) + { + return ConnectionState::Disabled; + } + return has_credentials ? ConnectionState::Idle : ConnectionState::Error; +} + +void clear_connection_details() +{ + s_runtime.connected = false; + s_runtime.connecting = false; + s_runtime.rssi = -127; + s_runtime.ssid[0] = '\0'; + s_runtime.ip[0] = '\0'; +} + +void refresh_runtime_status_message() +{ + const Config& config = s_runtime.config; + + if (!s_runtime.config_cached || !config.enabled) + { + set_status_message("Wi-Fi disabled"); + return; + } + + if (s_runtime.scanning) + { + set_status_message("Scanning..."); + return; + } + + if (s_runtime.connecting) + { + set_status_message("Connecting..."); + return; + } + + if (s_runtime.connected) + { + if (s_runtime.ip[0] != '\0') + { + char buffer[kMaxStatusMessageLength + 1]; + std::snprintf(buffer, + sizeof(buffer), + "Connected %s", + s_runtime.ip); + set_status_message(buffer); + } + else + { + set_status_message("Connected"); + } + return; + } + + if (!has_saved_credentials(config)) + { + set_status_message("Set SSID and password"); + return; + } + + set_status_message("Ready to connect"); +} + +bool ensure_stack_ready() +{ + if (s_runtime.stack_ready) + { + return true; + } + +#if defined(TRAIL_MATE_ESP_BOARD_TAB5) + bsp_set_wifi_power_enable(true); +#endif + + const esp_err_t netif_err = esp_netif_init(); + if (netif_err != ESP_OK && netif_err != ESP_ERR_INVALID_STATE) + { + set_status_message("esp_netif_init failed"); + return false; + } + + const esp_err_t loop_err = esp_event_loop_create_default(); + if (loop_err != ESP_OK && loop_err != ESP_ERR_INVALID_STATE) + { + set_status_message("event loop init failed"); + return false; + } + + if (s_runtime.sta_netif == nullptr) + { + s_runtime.sta_netif = esp_netif_create_default_wifi_sta(); + if (s_runtime.sta_netif == nullptr) + { + set_status_message("STA netif create failed"); + return false; + } + } + + s_runtime.stack_ready = true; + return true; +} + +void wifi_event_handler(void*, + esp_event_base_t event_base, + int32_t event_id, + void* event_data) +{ + if (event_base == WIFI_EVENT) + { + switch (event_id) + { + case WIFI_EVENT_STA_CONNECTED: + s_runtime.connecting = false; + break; + case WIFI_EVENT_STA_DISCONNECTED: + clear_connection_details(); + refresh_runtime_status_message(); + break; + case WIFI_EVENT_SCAN_DONE: + s_runtime.scanning = false; + refresh_runtime_status_message(); + break; + default: + break; + } + } + else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + const auto* got_ip = static_cast(event_data); + s_runtime.connected = true; + s_runtime.connecting = false; + if (got_ip) + { + std::snprintf(s_runtime.ip, + sizeof(s_runtime.ip), + IPSTR, + IP2STR(&got_ip->ip_info.ip)); + } + wifi_ap_record_t ap_info{}; + if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) + { + s_runtime.rssi = ap_info.rssi; + copy_bounded(s_runtime.ssid, + sizeof(s_runtime.ssid), + reinterpret_cast(ap_info.ssid)); + } + refresh_runtime_status_message(); + } +} + +bool ensure_wifi_initialized() +{ + if (s_runtime.wifi_initialized) + { + return true; + } + + if (!ensure_stack_ready()) + { + return false; + } + + wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT(); + const esp_err_t init_err = esp_wifi_init(&config); + if (init_err != ESP_OK && init_err != ESP_ERR_WIFI_INIT_STATE) + { + set_status_message("esp_wifi_init failed"); + return false; + } + + const esp_err_t storage_err = esp_wifi_set_storage(WIFI_STORAGE_RAM); + if (storage_err != ESP_OK) + { + set_status_message("wifi storage failed"); + return false; + } + + const esp_err_t mode_err = esp_wifi_set_mode(WIFI_MODE_STA); + if (mode_err != ESP_OK) + { + set_status_message("wifi mode failed"); + return false; + } + + const esp_err_t ps_err = esp_wifi_set_ps(WIFI_PS_MIN_MODEM); + if (ps_err != ESP_OK) + { + set_status_message("wifi power-save failed"); + return false; + } + + if (!s_runtime.handlers_registered) + { + if (esp_event_handler_instance_register( + WIFI_EVENT, + ESP_EVENT_ANY_ID, + &wifi_event_handler, + nullptr, + &s_runtime.wifi_event_handler) != ESP_OK) + { + set_status_message("wifi event hook failed"); + return false; + } + + if (esp_event_handler_instance_register( + IP_EVENT, + IP_EVENT_STA_GOT_IP, + &wifi_event_handler, + nullptr, + &s_runtime.ip_event_handler) != ESP_OK) + { + set_status_message("ip event hook failed"); + return false; + } + + s_runtime.handlers_registered = true; + } + + s_runtime.wifi_initialized = true; + return true; +} + +bool ensure_wifi_started() +{ + if (s_runtime.wifi_started) + { + return true; + } + + if (!ensure_wifi_initialized()) + { + return false; + } + +#if defined(TRAIL_MATE_ESP_BOARD_TAB5) + bsp_set_wifi_power_enable(true); +#endif + + const esp_err_t start_err = esp_wifi_start(); + if (start_err != ESP_OK && start_err != ESP_ERR_WIFI_CONN) + { + set_status_message("wifi start failed"); + return false; + } + + s_runtime.wifi_started = true; + refresh_runtime_status_message(); + return true; +} + +} // namespace + +bool is_supported() +{ + return true; +} + +bool load_config(Config& out) +{ + return read_config_from_store(out); +} + +bool save_config(const Config& config) +{ + const bool ssid_ok = + ::platform::ui::settings_store::put_string(kSettingsNs, kWifiSsidKey, config.ssid); + const bool password_ok = + ::platform::ui::settings_store::put_string(kSettingsNs, kWifiPasswordKey, config.password); + ::platform::ui::settings_store::put_bool(kSettingsNs, kWifiEnabledKey, config.enabled); + cache_config(config); + refresh_runtime_status_message(); + return ssid_ok && password_ok; +} + +bool apply_enabled(bool enabled) +{ + if (!is_supported()) + { + return false; + } + + if (!enabled) + { + if (s_runtime.wifi_started) + { + (void)esp_wifi_disconnect(); + (void)esp_wifi_stop(); + } +#if defined(TRAIL_MATE_ESP_BOARD_TAB5) + bsp_set_wifi_power_enable(false); +#endif + s_runtime.wifi_started = false; + clear_connection_details(); + set_status_message("Wi-Fi disabled"); + return true; + } + + if (!ensure_wifi_started()) + { + return false; + } + + clear_connection_details(); + refresh_runtime_status_message(); + return true; +} + +bool connect(const Config* override_config) +{ + Config config{}; + if (override_config) + { + config = *override_config; + } + else + { + (void)load_config(config); + } + + if (!config.enabled) + { + set_status_message("Enable Wi-Fi first"); + return false; + } + + if (!has_saved_credentials(config)) + { + set_status_message("SSID is not set"); + return false; + } + + if (!ensure_wifi_started()) + { + return false; + } + + wifi_config_t wifi_config{}; + copy_bounded(reinterpret_cast(wifi_config.sta.ssid), + sizeof(wifi_config.sta.ssid), + config.ssid); + copy_bounded(reinterpret_cast(wifi_config.sta.password), + sizeof(wifi_config.sta.password), + config.password); + wifi_config.sta.threshold.authmode = + config.password[0] == '\0' ? WIFI_AUTH_OPEN : WIFI_AUTH_WPA2_PSK; + wifi_config.sta.pmf_cfg.capable = true; + wifi_config.sta.pmf_cfg.required = false; + + if (esp_wifi_set_config(WIFI_IF_STA, &wifi_config) != ESP_OK) + { + set_status_message("Set Wi-Fi config failed"); + return false; + } + + clear_connection_details(); + s_runtime.connecting = true; + refresh_runtime_status_message(); + + if (esp_wifi_disconnect() != ESP_OK) + { + // Ignore disconnect failures before a fresh connect attempt. + } + if (esp_wifi_connect() != ESP_OK) + { + s_runtime.connecting = false; + set_status_message("Wi-Fi connect failed"); + return false; + } + + constexpr int kConnectTimeoutMs = 15000; + constexpr int kPollMs = 100; + int waited_ms = 0; + while (waited_ms < kConnectTimeoutMs) + { + if (s_runtime.connected) + { + refresh_runtime_status_message(); + return true; + } + vTaskDelay(pdMS_TO_TICKS(kPollMs)); + waited_ms += kPollMs; + } + + s_runtime.connecting = false; + set_status_message("Wi-Fi connect timeout"); + return false; +} + +void disconnect() +{ + if (s_runtime.wifi_started) + { + (void)esp_wifi_disconnect(); + } + clear_connection_details(); + refresh_runtime_status_message(); +} + +bool scan(std::vector& out_results) +{ + out_results.clear(); + + Config config{}; + (void)load_config(config); + if (!config.enabled) + { + set_status_message("Enable Wi-Fi first"); + return false; + } + + if (!ensure_wifi_started()) + { + return false; + } + + s_runtime.scanning = true; + refresh_runtime_status_message(); + + wifi_scan_config_t scan_config{}; + const esp_err_t scan_err = esp_wifi_scan_start(&scan_config, true); + s_runtime.scanning = false; + if (scan_err != ESP_OK) + { + set_status_message("Wi-Fi scan failed"); + return false; + } + + uint16_t ap_count = 0; + if (esp_wifi_scan_get_ap_num(&ap_count) != ESP_OK || ap_count == 0) + { + set_status_message("No Wi-Fi networks found"); + return true; + } + + std::vector records(ap_count); + if (esp_wifi_scan_get_ap_records(&ap_count, records.data()) != ESP_OK) + { + set_status_message("Read scan results failed"); + return false; + } + + out_results.reserve(ap_count); + for (const wifi_ap_record_t& record : records) + { + if (record.ssid[0] == 0) + { + continue; + } + + ScanResult result{}; + copy_bounded(result.ssid, + sizeof(result.ssid), + reinterpret_cast(record.ssid)); + result.rssi = record.rssi; + result.requires_password = record.authmode != WIFI_AUTH_OPEN; + out_results.push_back(result); + } + + std::sort(out_results.begin(), + out_results.end(), + [](const ScanResult& lhs, const ScanResult& rhs) + { + return lhs.rssi > rhs.rssi; + }); + + char buffer[kMaxStatusMessageLength + 1]; + std::snprintf(buffer, + sizeof(buffer), + "Found %u networks", + static_cast(out_results.size())); + set_status_message(buffer); + return true; +} + +Status status() +{ + Status out{}; + out.supported = is_supported(); + if (!out.supported) + { + copy_bounded(out.message, sizeof(out.message), "Wi-Fi unsupported"); + out.state = ConnectionState::Unsupported; + return out; + } + + const Config& config = current_config(); + out.enabled = config.enabled; + out.has_credentials = has_saved_credentials(config); + out.connected = s_runtime.connected; + out.scanning = s_runtime.scanning; + out.rssi = s_runtime.rssi; + copy_bounded(out.ssid, + sizeof(out.ssid), + s_runtime.connected ? s_runtime.ssid : config.ssid); + copy_bounded(out.ip, sizeof(out.ip), s_runtime.ip); + + if (!config.enabled) + { + out.state = ConnectionState::Disabled; + copy_bounded(out.message, sizeof(out.message), "Wi-Fi disabled"); + return out; + } + + if (s_runtime.scanning) + { + out.state = ConnectionState::Scanning; + copy_bounded(out.message, sizeof(out.message), "Scanning..."); + return out; + } + + if (s_runtime.connecting) + { + out.state = ConnectionState::Connecting; + copy_bounded(out.message, sizeof(out.message), "Connecting..."); + return out; + } + + if (s_runtime.connected) + { + out.state = ConnectionState::Connected; + copy_bounded(out.message, sizeof(out.message), s_runtime.message); + return out; + } + + out.state = disconnected_state(config.enabled, out.has_credentials); + if (out.has_credentials) + { + copy_bounded(out.message, sizeof(out.message), "Ready to connect"); + } + else + { + copy_bounded(out.message, sizeof(out.message), "Set SSID and password"); + } + return out; +} + +} // namespace platform::ui::wifi diff --git a/platform/esp/idf_common/src/platform_ui_device_runtime.cpp b/platform/esp/idf_common/src/platform_ui_device_runtime.cpp index e7e4e70f..174569ec 100644 --- a/platform/esp/idf_common/src/platform_ui_device_runtime.cpp +++ b/platform/esp/idf_common/src/platform_ui_device_runtime.cpp @@ -5,6 +5,8 @@ #include "boards/t_display_p4/board_profile.h" #include "boards/tab5/rtc_runtime.h" #include "boards/tab5/tab5_board.h" +#include "esp_heap_caps.h" +#include "esp_ota_ops.h" #include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -49,6 +51,23 @@ BatteryInfo battery_info() return info; } +MemoryStats memory_stats() +{ + MemoryStats stats{}; + stats.ram_total_bytes = heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + stats.ram_free_bytes = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + stats.psram_total_bytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + stats.psram_free_bytes = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + stats.psram_available = stats.psram_total_bytes > 0; + return stats; +} + +const char* firmware_version() +{ + const esp_app_desc_t* desc = esp_ota_get_app_description(); + return (desc && desc->version[0] != '\0') ? desc->version : "unknown"; +} + void handle_low_battery(const BatteryInfo& info) { (void)info; diff --git a/platform/esp/idf_common/src/platform_ui_wifi_runtime.cpp b/platform/esp/idf_common/src/platform_ui_wifi_runtime.cpp new file mode 100644 index 00000000..4d10859e --- /dev/null +++ b/platform/esp/idf_common/src/platform_ui_wifi_runtime.cpp @@ -0,0 +1 @@ +#include "platform/esp/common/wifi_runtime_impl.h" diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h index 0e866625..c35eabc4 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h @@ -33,7 +33,7 @@ class MeshCoreRadioAdapter final : public ::chat::IMeshAdapter, public ::chat::m void applyConfig(const ::chat::MeshConfig& config) override; void setUserInfo(const char* long_name, const char* short_name) override; void setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) override; - void setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) override; + void setPrivacyConfig(uint8_t encrypt_mode) override; bool isReady() const override; ::chat::NodeId getNodeId() const override; ::chat::meshcore::IMeshCoreBleBackend* asMeshCoreBleBackend() override { return this; } diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h index 8471e784..01e8c61a 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/meshtastic_radio_adapter.h @@ -60,7 +60,7 @@ class MeshtasticRadioAdapter final : public ::chat::IMeshAdapter void applyConfig(const ::chat::MeshConfig& config) override; void setUserInfo(const char* long_name, const char* short_name) override; void setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) override; - void setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) override; + void setPrivacyConfig(uint8_t encrypt_mode) override; bool isReady() const override; ::chat::NodeId getNodeId() const override; void setMqttProxySettings(const MqttProxySettings& settings); @@ -258,7 +258,6 @@ class MeshtasticRadioAdapter final : public ::chat::IMeshAdapter size_t last_raw_packet_len_ = 0; bool has_pending_raw_packet_ = false; uint8_t encrypt_mode_ = 1; - bool pki_enabled_ = false; bool pki_ready_ = false; std::array pki_public_key_{}; std::array pki_private_key_{}; diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h index 39000208..7dfffb49 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h @@ -26,6 +26,7 @@ class InternalFsStore final : public ::chat::IChatStore void clearConversation(const ::chat::ConversationId& conv) override; void clearAll() override; bool updateMessageStatus(::chat::MessageId msg_id, ::chat::MessageStatus status) override; + bool getMessage(::chat::MessageId msg_id, ::chat::ChatMessage* out) const override; void flush() override; private: diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp index c973584c..e4301d72 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp @@ -232,10 +232,9 @@ void MeshCoreRadioAdapter::setNetworkLimits(bool duty_cycle_enabled, uint8_t uti (void)util_percent; } -void MeshCoreRadioAdapter::setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) +void MeshCoreRadioAdapter::setPrivacyConfig(uint8_t encrypt_mode) { (void)encrypt_mode; - (void)pki_enabled; } bool MeshCoreRadioAdapter::isReady() const diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp index b100c231..92910a01 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/meshtastic_radio_adapter.cpp @@ -70,6 +70,13 @@ using ::chat::meshtastic::toHex; void logMeshtasticRx(const char* format, ...); uint32_t nowSeconds(); +bool shouldRequireDirectPki(uint8_t encrypt_mode, ::chat::NodeId dest_node, uint32_t portnum) +{ + return encrypt_mode != 0 && + dest_node != kBroadcastNode && + allowPkiForPortnum(portnum); +} + void logMeshtasticRx(const char* format, ...) { char buffer[192] = {}; @@ -513,7 +520,9 @@ bool MeshtasticRadioAdapter::sendTextWithId(::chat::ChannelId channel, const std out_channel = ::chat::ChannelId::PRIMARY; } - if (peer != 0 && pki_enabled_) + if (shouldRequireDirectPki(encrypt_mode_, + (peer == 0) ? kBroadcastNode : peer, + meshtastic_PortNum_TEXT_MESSAGE_APP)) { const ::chat::MessageId packet_id = (forced_msg_id != 0) ? forced_msg_id : next_packet_id_; const bool ok = sendAppData(out_channel, @@ -678,7 +687,7 @@ bool MeshtasticRadioAdapter::sendAppData(::chat::ChannelId channel, uint32_t por size_t wire_payload_len = data_pb_size; bool use_pki = false; bool track_ack = want_ack; - if (wire_dest != kBroadcastNode && pki_enabled_) + if (shouldRequireDirectPki(encrypt_mode_, wire_dest, portnum)) { if (!pki_ready_ || !allowPkiForPortnum(portnum) || !hasPkiKey(wire_dest)) { @@ -821,14 +830,9 @@ void MeshtasticRadioAdapter::setNetworkLimits(bool duty_cycle_enabled, uint8_t u (void)util_percent; } -void MeshtasticRadioAdapter::setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) +void MeshtasticRadioAdapter::setPrivacyConfig(uint8_t encrypt_mode) { encrypt_mode_ = encrypt_mode; - pki_enabled_ = pki_enabled; - if (encrypt_mode_ == 0) - { - pki_enabled_ = false; - } if (!pki_ready_) { pki_ready_ = initPkiKeys(); diff --git a/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp b/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp index a2113408..9cca458a 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp @@ -188,6 +188,33 @@ bool InternalFsStore::updateMessageStatus(::chat::MessageId msg_id, ::chat::Mess return false; } +bool InternalFsStore::getMessage(::chat::MessageId msg_id, ::chat::ChatMessage* out) const +{ + if (msg_id == 0) + { + return false; + } + + for (const auto& pair : conversations_) + { + const auto& storage = pair.second; + for (const auto& entry : storage.messages) + { + if (entry.message.msg_id != msg_id) + { + continue; + } + if (out) + { + *out = entry.message; + } + return true; + } + } + + return false; +} + bool InternalFsStore::ensureFs() const { return path_ && InternalFS.begin(); diff --git a/platform/nrf52/arduino_common/src/platform_ui_settings_store.cpp b/platform/nrf52/arduino_common/src/platform_ui_settings_store.cpp index e124d6c9..bca5946d 100644 --- a/platform/nrf52/arduino_common/src/platform_ui_settings_store.cpp +++ b/platform/nrf52/arduino_common/src/platform_ui_settings_store.cpp @@ -27,6 +27,7 @@ enum class ValueType : uint8_t Bool = 2, Uint = 3, Blob = 4, + String = 5, }; struct FileHeader @@ -76,6 +77,12 @@ std::map>& blobStore() return store; } +std::map& stringStore() +{ + static std::map store; + return store; +} + bool& loadedFlag() { static bool loaded = false; @@ -88,6 +95,7 @@ void clearAllStores() boolStore().clear(); uintStore().clear(); blobStore().clear(); + stringStore().clear(); } template @@ -180,7 +188,7 @@ bool saveToFs() } const uint32_t record_count = static_cast( - intStore().size() + boolStore().size() + uintStore().size() + blobStore().size()); + intStore().size() + boolStore().size() + uintStore().size() + blobStore().size() + stringStore().size()); uint32_t final_size = static_cast(sizeof(FileHeader)); FileHeader header{}; header.record_count = record_count; @@ -247,6 +255,24 @@ bool saveToFs() final_size += static_cast(sizeof(RecordHeader) + entry.first.size() + entry.second.size()); } + for (const auto& entry : stringStore()) + { + if (!writeRecordHeader(file, ValueType::String, entry.first, static_cast(entry.second.size())) || + (!entry.second.empty() && + !writeBytes(file, + reinterpret_cast(entry.second.data()), + entry.second.size()))) + { + file.close(); + Serial.printf("%s string write failed key=%s len=%lu\n", + kLogTag, + entry.first.c_str(), + static_cast(entry.second.size())); + return false; + } + final_size += static_cast(sizeof(RecordHeader) + entry.first.size() + entry.second.size()); + } + const bool trunc_ok = ::platform::nrf52::arduino_common::internal_fs::truncateAfterWrite(file, final_size); file.flush(); file.close(); @@ -297,6 +323,7 @@ void ensureLoaded() std::map loaded_bools; std::map loaded_uints; std::map> loaded_blobs; + std::map loaded_strings; for (uint32_t i = 0; i < header.record_count; ++i) { @@ -361,6 +388,18 @@ void ensureLoaded() loaded_blobs[key] = std::move(value); break; } + case ValueType::String: + { + std::string value(rec.value_len, '\0'); + if (rec.value_len > 0 && + !readBytes(file, reinterpret_cast(&value[0]), rec.value_len)) + { + file.close(); + return; + } + loaded_strings[key] = std::move(value); + break; + } default: { std::vector skip(rec.value_len, 0); @@ -379,6 +418,7 @@ void ensureLoaded() boolStore() = std::move(loaded_bools); uintStore() = std::move(loaded_uints); blobStore() = std::move(loaded_blobs); + stringStore() = std::move(loaded_strings); } } // namespace @@ -419,6 +459,25 @@ void put_uint(const char* ns, const char* key, uint32_t value) (void)saveToFs(); } +bool put_string(const char* ns, const char* key, const char* value) +{ + if (!key || !value) + { + return false; + } + ensureLoaded(); + const std::string scoped = makeScopedKey(ns, key); + if (value[0] == '\0') + { + stringStore().erase(scoped); + } + else + { + stringStore()[scoped] = value; + } + return saveToFs(); +} + bool put_blob(const char* ns, const char* key, const void* data, std::size_t len) { if (!key || (!data && len != 0)) @@ -472,6 +531,23 @@ uint32_t get_uint(const char* ns, const char* key, uint32_t default_value) return it == uintStore().end() ? default_value : it->second; } +bool get_string(const char* ns, const char* key, std::string& out) +{ + out.clear(); + if (!key) + { + return false; + } + ensureLoaded(); + const auto it = stringStore().find(makeScopedKey(ns, key)); + if (it == stringStore().end()) + { + return false; + } + out = it->second; + return true; +} + bool get_blob(const char* ns, const char* key, std::vector& out) { out.clear(); @@ -507,6 +583,7 @@ void remove_keys(const char* ns, const char* const* keys, std::size_t key_count) boolStore().erase(scoped); uintStore().erase(scoped); blobStore().erase(scoped); + stringStore().erase(scoped); } (void)saveToFs(); } @@ -532,6 +609,10 @@ void clear_namespace(const char* ns) { it = (it->first.rfind(prefix, 0) == 0) ? blobStore().erase(it) : std::next(it); } + for (auto it = stringStore().begin(); it != stringStore().end();) + { + it = (it->first.rfind(prefix, 0) == 0) ? stringStore().erase(it) : std::next(it); + } (void)saveToFs(); } diff --git a/platform/nrf52/arduino_common/src/platform_ui_wifi_runtime.cpp b/platform/nrf52/arduino_common/src/platform_ui_wifi_runtime.cpp new file mode 100644 index 00000000..7478a638 --- /dev/null +++ b/platform/nrf52/arduino_common/src/platform_ui_wifi_runtime.cpp @@ -0,0 +1,56 @@ +#include "platform/ui/wifi_runtime.h" + +#include + +namespace platform::ui::wifi +{ + +bool is_supported() +{ + return false; +} + +bool load_config(Config& out) +{ + out = Config{}; + return false; +} + +bool save_config(const Config& config) +{ + (void)config; + return false; +} + +bool apply_enabled(bool enabled) +{ + (void)enabled; + return false; +} + +bool connect(const Config* override_config) +{ + (void)override_config; + return false; +} + +void disconnect() +{ +} + +bool scan(std::vector& out_results) +{ + out_results.clear(); + return false; +} + +Status status() +{ + Status out{}; + out.supported = false; + out.state = ConnectionState::Unsupported; + std::snprintf(out.message, sizeof(out.message), "%s", "Wi-Fi unsupported"); + return out; +} + +} // namespace platform::ui::wifi diff --git a/scripts/build_pack_repository.py b/scripts/build_pack_repository.py new file mode 100644 index 00000000..4ec695bb --- /dev/null +++ b/scripts/build_pack_repository.py @@ -0,0 +1,407 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +import subprocess +import sys +import tempfile +from datetime import datetime, timezone +from pathlib import Path +from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo + + +PACKAGE_SCHEMA_VERSION = 1 +ARCHIVE_LAYOUT = "trailmate-pack-v1" +FIXED_ZIP_DT = (2026, 1, 1, 0, 0, 0) +TOP_LEVEL_PACKAGE_FILES = ("package.ini", "DESCRIPTION.txt", "README.md") +PAYLOAD_GROUPS = ("fonts", "locales", "ime") +PACKAGE_EXCLUDE_NAMES = {".gitignore", "build.ini", "charset.txt"} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Build installable locale-pack zip archives and a package catalog for GitHub Pages." + ) + parser.add_argument("--pack-root", default="packs", help="Repository pack source root") + parser.add_argument("--site-root", default="site", help="Pages site root") + parser.add_argument("--node-exe", default="node", help="Node.js executable for font generation") + parser.add_argument( + "--npm-exe", + default=None, + help="Optional npm executable override for font generation", + ) + parser.add_argument( + "--catalog-path", + default=None, + help="Optional explicit output path for the generated catalog JSON", + ) + return parser.parse_args() + + +def clear_directory_contents(path: Path) -> None: + path.mkdir(parents=True, exist_ok=True) + for child in path.iterdir(): + if child.is_dir(): + shutil.rmtree(child) + else: + child.unlink() + + +def parse_key_value_file(path: Path) -> dict[str, str]: + data: dict[str, str] = {} + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or line.startswith(";"): + continue + if "=" not in line: + continue + key, value = line.split("=", 1) + key = key.strip().lower() + value = value.strip() + if key: + data[key] = value + return data + + +def split_csv(value: str | None) -> list[str]: + if not value: + return [] + return [item.strip() for item in value.split(",") if item.strip()] + + +def parse_int(value: str | None, default: int = 0) -> int: + if value is None or value == "": + return default + return int(value, 10) + + +def parse_bool(value: str | None, default: bool = False) -> bool: + if value is None: + return default + normalized = value.strip().lower() + if normalized in {"1", "true", "yes", "on"}: + return True + if normalized in {"0", "false", "no", "off"}: + return False + return default + + +def build_font_if_missing( + stage_font_dir: Path, + repo_root: Path, + node_exe: str, + npm_exe: str | None, +) -> None: + build_path = stage_font_dir / "build.ini" + font_bin_path = stage_font_dir / "font.bin" + if font_bin_path.exists(): + return + if not build_path.exists(): + raise FileNotFoundError(f"Missing font.bin and build.ini for {stage_font_dir}") + + build = parse_key_value_file(build_path) + font_source = build.get("font") + if not font_source: + raise ValueError(f"build.ini missing font=... in {build_path}") + + charset_file = stage_font_dir / "charset.txt" + if not charset_file.exists(): + raise FileNotFoundError(f"Missing charset.txt for generated font: {charset_file}") + + generator = repo_root / "tools" / "generate_binfont_with_lv_font_conv.py" + if not generator.exists(): + raise FileNotFoundError(f"Missing generator script: {generator}") + + command = [ + sys.executable, + str(generator), + "--font", + str((repo_root / font_source).resolve()), + "--charset-file", + str(charset_file.resolve()), + "--output", + str(font_bin_path.resolve()), + "--size", + str(parse_int(build.get("size"), 16)), + "--bpp", + str(parse_int(build.get("bpp"), 2)), + "--node-exe", + node_exe, + ] + if npm_exe: + command.extend(["--npm-exe", npm_exe]) + if parse_bool(build.get("no_compress"), default=False): + command.append("--no-compress") + if parse_bool(build.get("no_prefilter"), default=False): + command.append("--no-prefilter") + if parse_bool(build.get("no_kerning"), default=False): + command.append("--no-kerning") + + subprocess.run(command, check=True) + + +def stage_bundle( + source_bundle_dir: Path, + repo_root: Path, + node_exe: str, + npm_exe: str | None, +) -> Path: + temp_dir = Path(tempfile.mkdtemp(prefix=f"trailmate-pack-{source_bundle_dir.name}-")) + stage_bundle_dir = temp_dir / source_bundle_dir.name + shutil.copytree( + source_bundle_dir, + stage_bundle_dir, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), + ) + + fonts_root = stage_bundle_dir / "fonts" + if fonts_root.is_dir(): + for font_dir in sorted(path for path in fonts_root.iterdir() if path.is_dir()): + build_font_if_missing(font_dir, repo_root, node_exe, npm_exe) + + return stage_bundle_dir + + +def iter_archive_sources(stage_bundle_dir: Path) -> list[tuple[Path, str]]: + files: list[tuple[Path, str]] = [] + + for name in TOP_LEVEL_PACKAGE_FILES: + source = stage_bundle_dir / name + if source.is_file(): + files.append((source, name)) + + for group in PAYLOAD_GROUPS: + group_dir = stage_bundle_dir / group + if not group_dir.is_dir(): + continue + for source in sorted(path for path in group_dir.rglob("*") if path.is_file()): + if source.name in PACKAGE_EXCLUDE_NAMES: + continue + relative = source.relative_to(stage_bundle_dir).as_posix() + files.append((source, f"payload/{relative}")) + + return files + + +def write_deterministic_zip(files: list[tuple[Path, str]], destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + with ZipFile(destination, "w") as archive: + for source, archive_name in sorted(files, key=lambda item: item[1]): + data = source.read_bytes() + info = ZipInfo(filename=archive_name, date_time=FIXED_ZIP_DT) + info.compress_type = ZIP_DEFLATED + info.external_attr = 0o100644 << 16 + archive.writestr(info, data) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def read_package_text(stage_bundle_dir: Path, relative_path: str | None) -> str: + if not relative_path: + return "" + path = stage_bundle_dir / relative_path + if not path.is_file(): + raise FileNotFoundError(f"Missing package text file: {path}") + return path.read_text(encoding="utf-8").strip() + + +def collect_font_records(stage_bundle_dir: Path) -> list[dict[str, object]]: + records: list[dict[str, object]] = [] + fonts_root = stage_bundle_dir / "fonts" + if not fonts_root.is_dir(): + return records + + for manifest_path in sorted(fonts_root.glob("*/manifest.ini")): + manifest = parse_key_value_file(manifest_path) + records.append( + { + "id": manifest.get("id", manifest_path.parent.name), + "display_name": manifest.get("display_name", manifest_path.parent.name), + "usage": manifest.get("usage", "both"), + "estimated_ram_bytes": parse_int(manifest.get("estimated_ram_bytes"), 0), + } + ) + return records + + +def collect_locale_records(stage_bundle_dir: Path) -> list[dict[str, object]]: + records: list[dict[str, object]] = [] + locales_root = stage_bundle_dir / "locales" + if not locales_root.is_dir(): + return records + + for manifest_path in sorted(locales_root.glob("*/manifest.ini")): + manifest = parse_key_value_file(manifest_path) + records.append( + { + "id": manifest.get("id", manifest_path.parent.name), + "display_name": manifest.get("display_name", manifest_path.parent.name), + "native_name": manifest.get("native_name", manifest.get("display_name", manifest_path.parent.name)), + "ui_font_pack_id": manifest.get("ui_font_pack", ""), + "content_font_pack_id": manifest.get( + "content_font_pack", manifest.get("ui_font_pack", "") + ), + "ime_pack_id": manifest.get("ime_pack", ""), + } + ) + return records + + +def collect_ime_records(stage_bundle_dir: Path) -> list[dict[str, object]]: + records: list[dict[str, object]] = [] + ime_root = stage_bundle_dir / "ime" + if not ime_root.is_dir(): + return records + + for manifest_path in sorted(ime_root.glob("*/manifest.ini")): + manifest = parse_key_value_file(manifest_path) + records.append( + { + "id": manifest.get("id", manifest_path.parent.name), + "display_name": manifest.get("display_name", manifest_path.parent.name), + "backend": manifest.get("backend", "none"), + } + ) + return records + + +def build_package_entry( + stage_bundle_dir: Path, + package_manifest: dict[str, str], + archive_relative_path: str, + archive_path: Path, +) -> dict[str, object]: + fonts = collect_font_records(stage_bundle_dir) + locales = collect_locale_records(stage_bundle_dir) + ime = collect_ime_records(stage_bundle_dir) + + description_text = read_package_text(stage_bundle_dir, package_manifest.get("description")) + readme_path = package_manifest.get("readme") + readme_relative_path = readme_path if readme_path and (stage_bundle_dir / readme_path).is_file() else "" + + return { + "id": package_manifest["id"], + "package_type": package_manifest.get("package_type", "locale-bundle"), + "version": package_manifest["version"], + "display_name": package_manifest.get("display_name", package_manifest["id"]), + "summary": package_manifest.get("summary", ""), + "description": description_text, + "author": package_manifest.get("author", "Trail Mate"), + "homepage": package_manifest.get("homepage", ""), + "min_firmware_version": package_manifest.get("min_firmware_version", ""), + "supported_memory_profiles": split_csv(package_manifest.get("supported_memory_profiles")), + "tags": split_csv(package_manifest.get("tags")), + "provides": { + "locales": locales, + "fonts": fonts, + "ime": ime, + }, + "runtime": { + "estimated_unique_font_ram_bytes": sum( + {record["id"]: int(record["estimated_ram_bytes"]) for record in fonts}.values() + ), + "locale_count": len(locales), + "font_count": len(fonts), + "ime_count": len(ime), + }, + "archive": { + "layout": ARCHIVE_LAYOUT, + "path": archive_relative_path.replace("\\", "/"), + "size_bytes": archive_path.stat().st_size, + "sha256": sha256_file(archive_path), + }, + "files": { + "package_manifest": "package.ini", + "description": package_manifest.get("description", ""), + "readme": readme_relative_path, + }, + } + + +def discover_source_bundles(pack_root: Path) -> list[Path]: + bundles: list[Path] = [] + for candidate in sorted(path for path in pack_root.iterdir() if path.is_dir()): + if (candidate / "package.ini").is_file(): + bundles.append(candidate) + return bundles + + +def build_pack_repository( + pack_root: Path, + site_root: Path, + node_exe: str = "node", + npm_exe: str | None = None, + catalog_path: Path | None = None, +) -> dict[str, object]: + pack_root = pack_root.resolve() + site_root = site_root.resolve() + repo_root = pack_root.parent.resolve() + assets_dir = site_root / "assets" / "packs" + clear_directory_contents(assets_dir) + + entries: list[dict[str, object]] = [] + temp_roots: list[Path] = [] + try: + for bundle_dir in discover_source_bundles(pack_root): + package_manifest = parse_key_value_file(bundle_dir / "package.ini") + if package_manifest.get("kind") != "package": + continue + if "id" not in package_manifest or "version" not in package_manifest: + raise ValueError(f"package.ini missing id/version in {bundle_dir}") + + stage_bundle_dir = stage_bundle(bundle_dir, repo_root, node_exe, npm_exe) + temp_roots.append(stage_bundle_dir.parent) + + archive_name = f"{package_manifest['id']}-{package_manifest['version']}.zip" + archive_path = assets_dir / archive_name + write_deterministic_zip(iter_archive_sources(stage_bundle_dir), archive_path) + + archive_relative_path = archive_path.relative_to(site_root).as_posix() + entries.append( + build_package_entry( + stage_bundle_dir, + package_manifest, + archive_relative_path, + archive_path, + ) + ) + finally: + for temp_root in temp_roots: + shutil.rmtree(temp_root, ignore_errors=True) + + entries.sort(key=lambda entry: str(entry["display_name"]).lower()) + payload = { + "schema_version": PACKAGE_SCHEMA_VERSION, + "archive_layout": ARCHIVE_LAYOUT, + "generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "packages": entries, + } + + output_path = catalog_path or (site_root / "data" / "packs.json") + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + return payload + + +def main() -> int: + args = parse_args() + build_pack_repository( + pack_root=Path(args.pack_root), + site_root=Path(args.site_root), + node_exe=args.node_exe, + npm_exe=args.npm_exe, + catalog_path=Path(args.catalog_path) if args.catalog_path else None, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/prepare_pages_site.py b/scripts/prepare_pages_site.py index 30057101..8d056350 100644 --- a/scripts/prepare_pages_site.py +++ b/scripts/prepare_pages_site.py @@ -1,13 +1,16 @@ from __future__ import annotations import argparse +import http.client import json import os import shutil +import time import urllib.error import urllib.request from pathlib import Path +from build_pack_repository import build_pack_repository from webflash_targets import WEBFLASH_TARGETS SHOWCASE_IMAGES = ( @@ -58,10 +61,32 @@ def github_request(url: str) -> urllib.request.Request: return urllib.request.Request(url, headers=headers) +def open_with_retries( + request: urllib.request.Request, + *, + attempts: int = 3, + timeout: int = 30, +): + last_error: Exception | None = None + for attempt in range(1, attempts + 1): + try: + return urllib.request.urlopen(request, timeout=timeout) + except urllib.error.HTTPError: + raise + except (urllib.error.URLError, TimeoutError, http.client.RemoteDisconnected) as exc: + last_error = exc + if attempt >= attempts: + raise + time.sleep(attempt) + if last_error is not None: + raise last_error + raise RuntimeError("open_with_retries reached an unexpected empty state") + + def fetch_latest_release(repo: str) -> dict | None: url = f"https://api.github.com/repos/{repo}/releases?per_page=10" try: - with urllib.request.urlopen(github_request(url)) as response: + with open_with_retries(github_request(url)) as response: releases = json.load(response) except urllib.error.HTTPError as exc: if exc.code == 404: @@ -75,7 +100,7 @@ def fetch_latest_release(repo: str) -> dict | None: def download(url: str, destination: Path) -> None: - with urllib.request.urlopen(github_request(url)) as response: + with open_with_retries(github_request(url)) as response: destination.write_bytes(response.read()) @@ -134,6 +159,7 @@ def prepare_site(site_root: Path, release: dict | None) -> None: assets_dir.mkdir(parents=True, exist_ok=True) data_dir.mkdir(parents=True, exist_ok=True) copy_showcase_images(site_root) + build_pack_repository(pack_root=Path("packs"), site_root=site_root) if release is None: write_json(data_dir / "latest-release.json", build_unavailable_metadata()) diff --git a/site/data/latest-release.json b/site/data/latest-release.json index 544b7fd7..e201cb24 100644 --- a/site/data/latest-release.json +++ b/site/data/latest-release.json @@ -1,19 +1,19 @@ { "available": true, - "tag_name": "v0.1.16-alpha", - "release_url": "https://github.com/vicliu624/trail-mate/releases/tag/v0.1.16-alpha", - "published_at": "2026-04-17T06:20:17Z", + "tag_name": "v0.1.18-alpha", + "release_url": "https://github.com/vicliu624/trail-mate/releases/tag/v0.1.18-alpha", + "published_at": "2026-04-20T08:07:40Z", "targets": { "tlora-pager-sx1262": { - "available": false, + "available": true, "manifest_path": "manifests/tlora-pager-sx1262.json" }, "tdeck": { - "available": false, + "available": true, "manifest_path": "manifests/tdeck.json" }, "lilygo-twatch-s3": { - "available": false, + "available": true, "manifest_path": "manifests/lilygo-twatch-s3.json" } } diff --git a/site/data/packs.json b/site/data/packs.json new file mode 100644 index 00000000..216beadc --- /dev/null +++ b/site/data/packs.json @@ -0,0 +1,356 @@ +{ + "schema_version": 1, + "archive_layout": "trailmate-pack-v1", + "generated_at": "2026-04-20T16:09:03.769450Z", + "packages": [ + { + "id": "europe-latin-ext", + "package_type": "locale-bundle", + "version": "1.0.0", + "display_name": "European Latin Extended", + "summary": "Shared Latin-Extended font coverage plus German, Spanish, French, Italian, Dutch, Polish, and Portuguese locale packs.", + "description": "European Latin Extended adds a shared external font pack for common accented Latin text\nand installs locale packs for German, Spanish, French, Italian, Dutch, Polish, and\nPortuguese (Portugal).\n\nThis bundle is designed to stay lightweight while still rendering mixed-language contact\nnames, chat content, and localized UI labels cleanly across European languages.", + "author": "Trail Mate", + "homepage": "https://vicliu624.github.io/trail-mate/", + "min_firmware_version": "0.1.18-alpha", + "supported_memory_profiles": [ + "constrained", + "standard", + "extended" + ], + "tags": [ + "language", + "latin", + "europe" + ], + "provides": { + "locales": [ + { + "id": "de", + "display_name": "German", + "native_name": "Deutsch", + "ui_font_pack_id": "latin-ext-eu", + "content_font_pack_id": "latin-ext-eu", + "ime_pack_id": "" + }, + { + "id": "es", + "display_name": "Spanish", + "native_name": "Español", + "ui_font_pack_id": "latin-ext-eu", + "content_font_pack_id": "latin-ext-eu", + "ime_pack_id": "" + }, + { + "id": "fr", + "display_name": "French", + "native_name": "Français", + "ui_font_pack_id": "latin-ext-eu", + "content_font_pack_id": "latin-ext-eu", + "ime_pack_id": "" + }, + { + "id": "it", + "display_name": "Italian", + "native_name": "Italiano", + "ui_font_pack_id": "latin-ext-eu", + "content_font_pack_id": "latin-ext-eu", + "ime_pack_id": "" + }, + { + "id": "nl", + "display_name": "Dutch", + "native_name": "Nederlands", + "ui_font_pack_id": "latin-ext-eu", + "content_font_pack_id": "latin-ext-eu", + "ime_pack_id": "" + }, + { + "id": "pl", + "display_name": "Polish", + "native_name": "Polski", + "ui_font_pack_id": "latin-ext-eu", + "content_font_pack_id": "latin-ext-eu", + "ime_pack_id": "" + }, + { + "id": "pt-PT", + "display_name": "Portuguese (Portugal)", + "native_name": "Português (Portugal)", + "ui_font_pack_id": "latin-ext-eu", + "content_font_pack_id": "latin-ext-eu", + "ime_pack_id": "" + } + ], + "fonts": [ + { + "id": "latin-ext-eu", + "display_name": "Latin Extended EU", + "usage": "both", + "estimated_ram_bytes": 941 + } + ], + "ime": [] + }, + "runtime": { + "estimated_unique_font_ram_bytes": 941, + "locale_count": 7, + "font_count": 1, + "ime_count": 0 + }, + "archive": { + "layout": "trailmate-pack-v1", + "path": "assets/packs/europe-latin-ext-1.0.0.zip", + "size_bytes": 12197, + "sha256": "66aa3e25f3666bb32f638f0eb61a6a130b0f7b74409db0a904fc8bbe05ee7ad4" + }, + "files": { + "package_manifest": "package.ini", + "description": "DESCRIPTION.txt", + "readme": "README.md" + } + }, + { + "id": "ja", + "package_type": "locale-bundle", + "version": "1.0.0", + "display_name": "Japanese", + "summary": "Japanese locale bundle with external CJK font coverage for UI text and mixed-language content.", + "description": "Japanese installs the ja locale together with the ja-cjk external font pack so menu text,\nlabels, and received Japanese content can render without inflating the firmware image.\n\nThis bundle is currently display-only. It does not add a Japanese IME yet, but it keeps\nJapanese coverage available as an installable package on SD storage.", + "author": "Trail Mate", + "homepage": "https://vicliu624.github.io/trail-mate/", + "min_firmware_version": "0.1.18-alpha", + "supported_memory_profiles": [ + "constrained", + "standard", + "extended" + ], + "tags": [ + "language", + "cjk", + "japanese" + ], + "provides": { + "locales": [ + { + "id": "ja", + "display_name": "Japanese", + "native_name": "日本語", + "ui_font_pack_id": "ja-cjk", + "content_font_pack_id": "ja-cjk", + "ime_pack_id": "" + } + ], + "fonts": [ + { + "id": "ja-cjk", + "display_name": "Japanese CJK", + "usage": "both", + "estimated_ram_bytes": 10707 + } + ], + "ime": [] + }, + "runtime": { + "estimated_unique_font_ram_bytes": 10707, + "locale_count": 1, + "font_count": 1, + "ime_count": 0 + }, + "archive": { + "layout": "trailmate-pack-v1", + "path": "assets/packs/ja-1.0.0.zip", + "size_bytes": 11239, + "sha256": "21d00d81dac31d334411d85fafb5483547a16a479b93fc3936682be16138aa45" + }, + "files": { + "package_manifest": "package.ini", + "description": "DESCRIPTION.txt", + "readme": "README.md" + } + }, + { + "id": "ko", + "package_type": "locale-bundle", + "version": "1.0.0", + "display_name": "Korean", + "summary": "Korean locale bundle with external CJK font coverage for UI text and mixed-language content.", + "description": "Korean installs the ko locale together with the ko-cjk external font pack so interface\nlabels and Korean content can render correctly without carrying the glyph set in firmware.\n\nThis bundle is currently display-only. It does not add a Korean IME yet, but it keeps\nKorean support available as an installable SD package.", + "author": "Trail Mate", + "homepage": "https://vicliu624.github.io/trail-mate/", + "min_firmware_version": "0.1.18-alpha", + "supported_memory_profiles": [ + "constrained", + "standard", + "extended" + ], + "tags": [ + "language", + "cjk", + "korean" + ], + "provides": { + "locales": [ + { + "id": "ko", + "display_name": "Korean", + "native_name": "한국어", + "ui_font_pack_id": "ko-cjk", + "content_font_pack_id": "ko-cjk", + "ime_pack_id": "" + } + ], + "fonts": [ + { + "id": "ko-cjk", + "display_name": "Korean CJK", + "usage": "both", + "estimated_ram_bytes": 8143 + } + ], + "ime": [] + }, + "runtime": { + "estimated_unique_font_ram_bytes": 8143, + "locale_count": 1, + "font_count": 1, + "ime_count": 0 + }, + "archive": { + "layout": "trailmate-pack-v1", + "path": "assets/packs/ko-1.0.0.zip", + "size_bytes": 8461, + "sha256": "02bde352d3d071e4e0bbdeea7b546f784e5318a579e8a597fa5e47bb817962f2" + }, + "files": { + "package_manifest": "package.ini", + "description": "DESCRIPTION.txt", + "readme": "README.md" + } + }, + { + "id": "zh-Hans", + "package_type": "locale-bundle", + "version": "1.0.0", + "display_name": "Simplified Chinese", + "summary": "Full Simplified Chinese locale bundle with external CJK font coverage and the built-in Pinyin IME backend.", + "description": "Simplified Chinese installs the zh-Hans locale, the zh-hans-cjk external CJK font pack,\nand the zh-hans-pinyin IME registration used by the built-in Pinyin input backend.\n\nUse this bundle when you want a Chinese interface, Chinese text entry, or reliable\nrendering of Chinese names and chat content while another locale remains active.", + "author": "Trail Mate", + "homepage": "https://vicliu624.github.io/trail-mate/", + "min_firmware_version": "0.1.18-alpha", + "supported_memory_profiles": [ + "standard", + "extended" + ], + "tags": [ + "language", + "cjk", + "chinese", + "ime" + ], + "provides": { + "locales": [ + { + "id": "zh-Hans", + "display_name": "Simplified Chinese", + "native_name": "简体中文", + "ui_font_pack_id": "zh-hans-cjk", + "content_font_pack_id": "zh-hans-cjk", + "ime_pack_id": "zh-hans-pinyin" + } + ], + "fonts": [ + { + "id": "zh-hans-cjk", + "display_name": "Simplified Chinese CJK", + "usage": "both", + "estimated_ram_bytes": 476075 + } + ], + "ime": [ + { + "id": "zh-hans-pinyin", + "display_name": "Pinyin", + "backend": "builtin-pinyin" + } + ] + }, + "runtime": { + "estimated_unique_font_ram_bytes": 476075, + "locale_count": 1, + "font_count": 1, + "ime_count": 1 + }, + "archive": { + "layout": "trailmate-pack-v1", + "path": "assets/packs/zh-Hans-1.0.0.zip", + "size_bytes": 328766, + "sha256": "a7d5623755d962820c8849f7c48aa4c1bb741d30f14985dcae932922f76eee2a" + }, + "files": { + "package_manifest": "package.ini", + "description": "DESCRIPTION.txt", + "readme": "README.md" + } + }, + { + "id": "zh-Hant", + "package_type": "locale-bundle", + "version": "1.0.0", + "display_name": "Traditional Chinese", + "summary": "Traditional Chinese locale bundle with external CJK font coverage for UI and mixed-script content rendering.", + "description": "Traditional Chinese installs the zh-Hant locale together with the zh-hant-cjk external\nfont pack so the UI and mixed-language content can render Traditional Chinese correctly.\n\nThis bundle is display-focused today. It does not add a dedicated Traditional Chinese IME\nyet, but it keeps rendering support fully external to the firmware image.", + "author": "Trail Mate", + "homepage": "https://vicliu624.github.io/trail-mate/", + "min_firmware_version": "0.1.18-alpha", + "supported_memory_profiles": [ + "constrained", + "standard", + "extended" + ], + "tags": [ + "language", + "cjk", + "chinese" + ], + "provides": { + "locales": [ + { + "id": "zh-Hant", + "display_name": "Traditional Chinese", + "native_name": "繁體中文", + "ui_font_pack_id": "zh-hant-cjk", + "content_font_pack_id": "zh-hant-cjk", + "ime_pack_id": "" + } + ], + "fonts": [ + { + "id": "zh-hant-cjk", + "display_name": "Traditional Chinese CJK", + "usage": "both", + "estimated_ram_bytes": 10232 + } + ], + "ime": [] + }, + "runtime": { + "estimated_unique_font_ram_bytes": 10232, + "locale_count": 1, + "font_count": 1, + "ime_count": 0 + }, + "archive": { + "layout": "trailmate-pack-v1", + "path": "assets/packs/zh-Hant-1.0.0.zip", + "size_bytes": 11401, + "sha256": "b1fda6665d46a115f9ed5a63c7d54dea0f4c0c27b157770a89ac3bfdf86d2227" + }, + "files": { + "package_manifest": "package.ini", + "description": "DESCRIPTION.txt", + "readme": "README.md" + } + } + ] +} diff --git a/site/index.html b/site/index.html index 1debb763..0fcad00e 100644 --- a/site/index.html +++ b/site/index.html @@ -37,6 +37,7 @@