diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..f8dc1cb7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,94 @@ +# AGENTS.md + +## Project + +XiaoZhi is an ESP-IDF C/C++ voice-assistant firmware supporting many chips, boards, displays, audio devices, and network transports. A build selects exactly one board implementation. + +Use ESP-IDF v6.0.2 when possible. IDF 5.5.x is retained only for documented legacy boards. + +## Architecture + +- `main/application.*`: main event loop, protocol lifecycle, and high-level behavior. +- `main/device_state_machine.*`: legal runtime state transitions. +- `main/boards/common/`: board interfaces and reusable hardware/network helpers. +- `main/boards/**/`: board-specific pins, initialization, and build variants. +- `main/audio/`: codecs, audio tasks, engines, wake words, and queues. +- `main/protocols/`: transport-neutral API plus WebSocket and MQTT/UDP. +- `main/display/` and `main/led/`: reusable UI implementations. +- `main/mcp_server.*`: common device-side MCP tools and dispatch. +- `main/Kconfig.projbuild`: board and feature configuration. +- `main/CMakeLists.txt`: source, board, locale, font, and asset selection. +- `scripts/release.py`: canonical board/variant build entry point. + +Read the closest existing implementation before adding a new one. Prefer the narrowest owning layer; do not put board-specific behavior into core modules. + +## Required Rules + +- Preserve unrelated worktree changes and keep patches focused. +- A build must export exactly one board factory through `DECLARE_BOARD(...)`. +- Never alter an existing board's pins to support different hardware. Add a uniquely named board or release variant; board identity affects OTA compatibility. +- Core code depends on `Board` interfaces, never a concrete board class or board `config.h`. +- Treat camera, backlight, display, LED, battery, and similar capabilities as optional. +- Change runtime state through `Application::SetDeviceState()` and the state machine. +- Callbacks may run outside the main task. Schedule application mutations with `Application::Schedule()` or event bits. +- Do not block the main event loop or audio tasks. Avoid unbounded queues and repeated large allocations in audio paths. +- Keep shared message semantics in `Protocol`; verify both transports when changing its contract. +- Validate network input and preserve `cJSON` ownership. NVS keys are persistent API and require migration when changed. +- Guard target-specific features with Kconfig/component rules. Do not assume every target has PSRAM or S3/P4 resources. +- Do not manually edit generated/vendor output: `build/`, `releases/`, `managed_components/`, `components/`, `sdkconfig*`, `main/assets/lang_config.h`, or generated mmap headers. +- Format only touched C/C++ files with the repository `.clang-format`; avoid unrelated mass formatting. + +## Boards and Configuration + +Board selection is a coupled chain: + +`config.json` -> `scripts/release.py` -> `main/Kconfig.projbuild` -> `main/CMakeLists.txt` -> board source and `config.h`. + +When adding a board or variant, update every relevant link in that chain. Include a unique board identity, correct chip target, flash/partition settings, exactly one `DECLARE_BOARD`, and board documentation. Follow `docs/custom-board.md`. + +## Commands + +Source the intended ESP-IDF environment first: + +```sh +source /path/to/esp-idf/export.sh +idf.py --version +``` + +```sh +# Discover exact board and variant names +python3 scripts/release.py --list-boards + +# Canonical variant build +python3 scripts/release.py --name + +# Host-side release tests +python3 -m unittest discover -s scripts/tests -v + +# Format/check touched files +clang-format -i +clang-format --dry-run -Werror +``` + +The release script changes local `sdkconfig` and build state. Do not assume the build directory still represents a previous target. + +## Validation + +- Board-only change: build affected variants and smoke-test changed hardware. +- Core, common-board, audio, protocol, display, dependency, Kconfig, or CMake change: run host tests and build representative affected chip/network paths. +- Protocol changes: verify WebSocket and MQTT/UDP when shared behavior changes. +- Audio changes: verify capture, playback, wake/VAD, interruption, reconnect, and applicable AEC modes. +- UI/assets changes: verify applicable no-display/OLED/LVGL paths and partition size. +- Always report what was tested and what still needs physical hardware. A successful build is not hardware validation. + +## Authoritative Documentation + +- Overview and SDK policy: `README.md` +- SDK compatibility: `docs/esp-idf-6-migration.md` +- Board guide: `docs/custom-board.md` +- Audio design: `main/audio/README.md` +- Code style: `docs/code_style.md` +- Protocols: `docs/websocket.md`, `docs/mqtt-udp.md`, `docs/mcp-protocol.md` +- CI matrix: `.github/workflows/build.yml` + +Keep detailed or fast-changing information in those files, not here. Add a nested `AGENTS.md` only when a subsystem needs specialized instructions. diff --git a/docs/glyph-push.md b/docs/glyph-push.md new file mode 100644 index 00000000..64678bec --- /dev/null +++ b/docs/glyph-push.md @@ -0,0 +1,185 @@ +# Dynamic Text Glyph Push Extension + +This document defines version 1 of the `glyph_push` protocol extension. The extension lets a server +send bitmap glyphs that are missing from a device's installed text font. It applies equally to the +WebSocket and MQTT/UDP transports because capability advertisement and incoming JSON handling are +implemented in the shared protocol layer. + +The extension supplements text rendering only. It does not change the text, TTS audio, or STT +semantics of the containing message. + +## 1. Capability advertisement + +The device advertises support in its client `hello` message: + +```json +{ + "type": "hello", + "version": 1, + "features": { + "mcp": true, + "glyph_push": true + }, + "text_font": { + "bundle": "noto-v1", + "charset": "common", + "size": 20, + "bpp": 4 + } +} +``` + +`features.glyph_push` indicates support for this extension. A server must treat a missing or false +value as unsupported. The `v` field in each pushed payload carries the extension version. + +The `text_font` object describes the exact font data installed on the device: + +| Field | Type | Meaning | +|---|---|---| +| `bundle` | string | Explicit font bundle identifier. It changes when glyph metrics, rendering behavior, character sets, or wire compatibility change. | +| `charset` | string | Installed character set. Version 1 devices report `basic` or `common`. | +| `size` | number | Text font pixel profile used by the firmware. | +| `bpp` | number | Bits per pixel of the text font bitmap, currently `1` or `4`. | + +`basic` is the font linked into the firmware. The standard XiaoZhi assets report `common` after +loading their common font from the assets partition. The server must use the values from each +device's hello message rather than inferring them from the board model. + +An OTA assets package may replace the text font with a different size, bpp, character set, or font +family. The firmware still loads any structurally valid CBIN font. When the package also provides +complete `text_font_meta` fields, the device advertises those active runtime values and validates +glyph pushes against them. A legacy or custom package without compatible glyph metadata continues +to use its custom font, emoji, colors, and background, but advertises `glyph_push: false` and omits +`text_font`. This prevents incompatible fallback glyphs without restricting theme customization. + +## 2. Server glyph payload + +The server may attach a `glyph_push` object to either of these server-to-device messages: + +- a TTS message with `"type": "tts"` and `"state": "sentence_start"`; +- an STT message with `"type": "stt"`. + +Example: + +```json +{ + "type": "tts", + "state": "sentence_start", + "text": "𠮷野家", + "glyph_push": { + "v": 1, + "bundle": "noto-v1", + "size": 20, + "bpp": 4, + "glyphs": [ + { + "codepoint": 134071, + "adv_w": 320, + "box_w": 20, + "box_h": 20, + "ofs_x": 0, + "ofs_y": 0, + "bitmap": "" + } + ] + } +} +``` + +The payload header must match the device capability exactly: + +| Field | Requirement | +|---|---| +| `v` | Must be `1`. | +| `bundle` | Must equal `text_font.bundle`. | +| `size` | Must equal `text_font.size`. | +| `bpp` | Must equal `text_font.bpp`. | +| `glyphs` | The partial glyph batch being pushed, containing at most 64 entries. | + +Each item uses the LVGL native bitmap-font metrics: + +| Field | Meaning | +|---|---| +| `codepoint` | Unicode code point from `1` through `0x10FFFF`. | +| `adv_w` | Horizontal advance in LVGL fixed-point units with four fractional bits (one pixel is 16 units). | +| `box_w`, `box_h` | Bitmap dimensions in pixels. Each dimension must be from 0 through 64. | +| `ofs_x`, `ofs_y` | Signed 16-bit glyph offsets relative to the text baseline and cursor position. | +| `bitmap` | Base64 encoding of the uncompressed LVGL plain bitmap. | + +The decoded bitmap length must be exactly: + +```text +ceil(box_w * box_h * bpp / 8) +``` + +The bitmap must use the same plain, zero-stride layout as the matching Noto full-bundle CBIN font. +Servers should extract and forward the bitmap and metrics directly from that CBIN profile instead of +rasterizing an unrelated font at request time. + +The sum of decoded bitmap lengths in one payload must not exceed 64 KiB. If any header, glyph, or +bitmap is invalid, the device rejects the entire glyph payload but still displays the message text +using its installed fonts. A PSRAM device may also use fallback glyphs cached by earlier messages. + +## 3. Server selection algorithm + +For every connection that advertises `glyph_push: true`, the server should: + +1. Resolve the full font bundle identified by `text_font.bundle`. +2. Select the CBIN profile matching `text_font.size` and `text_font.bpp`. +3. Decode the message text into Unicode code points. +4. Remove control characters, duplicates, and code points already present in + `text_font.charset`. +5. Extract the remaining glyphs from the full bundle. +6. Apply the per-message limits and attach one `glyph_push` object to the text message. +7. Omit `glyph_push` when no missing glyph is available. + +The installed text font is searched before the dynamic fallback font. Pushed glyphs therefore fill +missing code points; they do not override glyphs in `basic` or `common`. + +The full font bundle may be shared by all device connections in a server process. Per-connection +work is limited to using the capability tuple `(bundle, charset, size, bpp)` to select which glyphs +are missing and which profile to read. + +## 4. Device cache behavior + +All glyphs in one message are inserted first, followed by a single fallback-font rebuild. The device +never rebuilds the font once per glyph. + +On a device with initialized PSRAM: + +- bitmap, cmap, descriptor, and cache-entry storage is allocated in PSRAM; +- glyphs are retained across messages; +- the cache holds at most 256 glyphs and 64 KiB of decoded bitmap data; +- the least recently inserted or updated entries are evicted when a limit is exceeded. + +On a device without PSRAM: + +- storage uses internal RAM; +- only the current message's glyph batch is retained; +- the next text message replaces or clears the previous batch. + +This distinction does not affect the protocol. A server can send the glyphs needed by each message +without knowing whether the device has PSRAM. + +## 5. Compatibility and versioning + +The server must not send glyphs when any of these conditions is true: + +- `features.glyph_push` is absent or not supported by the server; +- the server does not have the advertised bundle; +- no full-font profile matches the advertised size and bpp; +- the glyph data cannot satisfy the version 1 validation rules. + +Fallback is automatic: messages without `glyph_push`, and messages whose glyph payload is rejected, +are still processed normally. + +When the font generator changes metrics, bitmap layout, source fonts, character sets, or rendering +behavior, publish a new explicit bundle identifier. Do not serve glyphs from one bundle under +another bundle's identifier even if their size and bpp happen to match. + +## 6. Security requirements + +Glyph payloads are untrusted network input. Implementations must validate the complete payload before +mutating a live font, bound both item count and decoded size, verify base64 decoded length, and reject +invalid code points or metrics. Servers should also bound their own per-message work and avoid +sending glyphs already covered by the advertised charset. diff --git a/docs/glyph-push_zh.md b/docs/glyph-push_zh.md new file mode 100644 index 00000000..d1318a1a --- /dev/null +++ b/docs/glyph-push_zh.md @@ -0,0 +1,174 @@ +# 动态文字 Glyph Push 扩展 + +本文档定义 `glyph_push` 协议扩展的版本 1。服务器可以通过该扩展向设备下发本地文字 +字库中缺失的位图 glyph。能力声明和 JSON 消息处理位于公共协议层,因此 WebSocket 与 +MQTT/UDP 使用完全相同的扩展格式。 + +该扩展只补充文字渲染能力,不改变消息中的文本、TTS 音频或 STT 语义。 + +## 1. 能力声明 + +设备在客户端 `hello` 消息中声明能力: + +```json +{ + "type": "hello", + "version": 1, + "features": { + "mcp": true, + "glyph_push": true + }, + "text_font": { + "bundle": "noto-v1", + "charset": "common", + "size": 20, + "bpp": 4 + } +} +``` + +`features.glyph_push` 表示设备支持该扩展。字段缺失或为 false 时,服务器必须视为设备不支持。 +每次推送 payload 中的 `v` 字段负责表达扩展版本。 + +`text_font` 描述设备实际安装的文字字库: + +| 字段 | 类型 | 含义 | +|---|---|---| +| `bundle` | string | 显式的字体 bundle 标识。glyph 度量、渲染方式、字符集或线格式变化时需要更换。 | +| `charset` | string | 已安装字符集。版本 1 设备报告 `basic` 或 `common`。 | +| `size` | number | 固件使用的文字字体像素规格。 | +| `bpp` | number | 字体位图的每像素位数,目前为 `1` 或 `4`。 | + +`basic` 是链接进固件的字库。标准小智 assets 从分区加载 common 字库后会报告 `common`。 +服务器必须使用每个连接在 hello 中报告的实际值,不能根据板型推测。 + +OTA assets 可以把文字字体替换为不同字号、bpp、字符集或字体家族的 CBIN 字体。固件仍会 +加载任何结构有效的 CBIN 字体;如果 assets 同时提供完整的 `text_font_meta`,设备会声明当前 +实际使用的运行时字体参数,并据此校验 glyph push。旧版或自定义 assets 缺少兼容的 glyph +metadata 时,自定义字体、表情、颜色和背景仍然正常使用,但设备会声明 `glyph_push: false` +并省略 `text_font`,从而只禁止不兼容的 fallback glyph,不限制主题定制能力。 + +## 2. 服务器下发格式 + +服务器可以在以下消息中附加 `glyph_push` 对象: + +- `"type": "tts"`、`"state": "sentence_start"` 的 TTS 消息; +- `"type": "stt"` 的 STT 消息。 + +示例: + +```json +{ + "type": "tts", + "state": "sentence_start", + "text": "𠮷野家", + "glyph_push": { + "v": 1, + "bundle": "noto-v1", + "size": 20, + "bpp": 4, + "glyphs": [ + { + "codepoint": 134071, + "adv_w": 320, + "box_w": 20, + "box_h": 20, + "ofs_x": 0, + "ofs_y": 0, + "bitmap": "" + } + ] + } +} +``` + +payload 头必须与设备能力完全匹配: + +| 字段 | 要求 | +|---|---| +| `v` | 必须为 `1`。 | +| `bundle` | 必须等于 `text_font.bundle`。 | +| `size` | 必须等于 `text_font.size`。 | +| `bpp` | 必须等于 `text_font.bpp`。 | +| `glyphs` | 本次增量推送的 glyph 数组,最多 64 项。 | + +每个 item 使用 LVGL 原生位图字体度量: + +| 字段 | 含义 | +|---|---| +| `codepoint` | `1` 到 `0x10FFFF` 的 Unicode code point。 | +| `adv_w` | 带 4 位小数的 LVGL 定点水平 advance,16 个单位等于 1 像素。 | +| `box_w`、`box_h` | 位图宽高,单项范围为 0 到 64 像素。 | +| `ofs_x`、`ofs_y` | 相对于文字基线和光标位置的有符号 16 位偏移。 | +| `bitmap` | 未压缩 LVGL plain 位图的 Base64 编码。 | + +解码后的位图长度必须严格等于: + +```text +ceil(box_w * box_h * bpp / 8) +``` + +位图必须与对应 Noto full bundle CBIN 字体使用相同的 plain、零 stride 布局。服务器应直接 +提取相同 profile 的 CBIN 位图和度量,不应临时使用其他字体重新光栅化。 + +单条 payload 的解码位图总长度不得超过 64 KiB。任意头字段、glyph 或位图无效时,设备会 +拒绝整批 glyph,但仍使用本地字体显示消息文本;有 PSRAM 的设备还可能使用此前缓存的 +fallback glyph。 + +## 3. 服务器选择流程 + +对于声明 `glyph_push: true` 的每个连接,服务器应: + +1. 根据 `text_font.bundle` 找到对应 full 字体 bundle。 +2. 根据 `text_font.size` 和 `text_font.bpp` 选择 CBIN profile。 +3. 将消息文本解码为 Unicode code point。 +4. 去掉控制字符、重复字符及 `text_font.charset` 已包含的字符。 +5. 从 full bundle 提取剩余 glyph。 +6. 执行单消息限制,并将一个 `glyph_push` 对象附加到文字消息。 +7. 没有可用的缺失 glyph 时省略 `glyph_push`。 + +设备先查询本地文字字体,再查询动态 fallback。因此 glyph push 只补充缺字,不会覆盖 +`basic` 或 `common` 中已有的 glyph。 + +同一服务器进程中的所有设备连接可以共享 full 字体 bundle 服务。每个连接只需根据 +`(bundle, charset, size, bpp)` 能力组合判断缺字并选择 profile。 + +## 4. 设备缓存行为 + +同一消息中的全部 glyph 会先加入缓存,随后只执行一次 fallback 字体 rebuild,不会每加入 +一个 glyph 就 rebuild 一次。 + +设备有已初始化的 PSRAM 时: + +- bitmap、cmap、descriptor 和缓存条目存放在 PSRAM; +- glyph 跨消息保留; +- 缓存上限为 256 个 glyph 和 64 KiB 解码位图; +- 超限时淘汰最早插入或更新的条目。 + +设备没有 PSRAM 时: + +- 数据使用内部 RAM; +- 只保留当前消息的 glyph 批次; +- 下一条文字消息会替换或清空上一批 glyph。 + +该差异不改变协议。服务器无需知道设备是否有 PSRAM,可以为每条消息发送它所需的 glyph。 + +## 5. 兼容与版本管理 + +以下任一条件成立时,服务器不得发送 glyph: + +- `features.glyph_push` 缺失、为 false,或服务器不支持该扩展; +- 服务器没有设备声明的 bundle; +- 找不到匹配 size 和 bpp 的 full 字体 profile; +- glyph 数据不满足版本 1 的校验要求。 + +兼容降级是自动的:没有 `glyph_push`,或 glyph payload 被拒绝时,设备仍会正常处理消息。 + +字体生成器的度量、位图布局、源字体、字符集或渲染方式变化时,应发布新的显式 bundle +标识。即使 size 和 bpp 相同,也不能用一个 bundle 的标识下发另一个 bundle 的 glyph。 + +## 6. 安全要求 + +glyph payload 是不可信网络输入。实现必须先完整校验 payload,再修改正在使用的字体;限制 +item 数量和解码总长度;验证 Base64 解码长度;拒绝非法 code point 或度量。服务器也应限制 +单消息处理量,避免重复发送设备声明字符集已经包含的 glyph。 diff --git a/docs/mqtt-udp.md b/docs/mqtt-udp.md index 086d5d5e..0a991f41 100644 --- a/docs/mqtt-udp.md +++ b/docs/mqtt-udp.md @@ -80,7 +80,14 @@ The device connects to the broker using: "transport": "udp", "features": { "mcp": true, - "aec": true + "aec": true, + "glyph_push": true + }, + "text_font": { + "bundle": "noto-v1", + "charset": "common", + "size": 20, + "bpp": 4 }, "audio_params": { "format": "opus", @@ -92,6 +99,8 @@ The device connects to the broker using: ``` `features.mcp` is always set; `features.aec` is set when `CONFIG_USE_SERVER_AEC` is enabled. +`features.glyph_push` and `text_font` advertise the shared dynamic text-glyph extension described in +[Dynamic Text Glyph Push Extension](glyph-push.md). #### 3.2.2 Server -> Device diff --git a/docs/mqtt-udp_zh.md b/docs/mqtt-udp_zh.md index 478e4669..708015d1 100644 --- a/docs/mqtt-udp_zh.md +++ b/docs/mqtt-udp_zh.md @@ -78,7 +78,14 @@ sequenceDiagram "version": 3, "transport": "udp", "features": { - "mcp": true + "mcp": true, + "glyph_push": true + }, + "text_font": { + "bundle": "noto-v1", + "charset": "common", + "size": 20, + "bpp": 4 }, "audio_params": { "format": "opus", @@ -89,6 +96,9 @@ sequenceDiagram } ``` +`features.glyph_push` 和 `text_font` 声明 WebSocket 与 MQTT/UDP 共用的动态文字扩展,详见 +[动态文字 Glyph Push 扩展](glyph-push_zh.md)。 + #### 3.2.2 服务器响应 Hello ```json @@ -390,4 +400,4 @@ MQTT + UDP 混合协议通过以下设计实现高效的音视频通信: - **自动恢复**:支持连接断开后的自动重连 - **性能优化**:UDP 传输保证音频数据的实时性 -该协议适用于对实时性要求较高的语音交互场景,但需要在网络复杂度和传输性能之间做出权衡。 \ No newline at end of file +该协议适用于对实时性要求较高的语音交互场景,但需要在网络复杂度和传输性能之间做出权衡。 diff --git a/docs/websocket.md b/docs/websocket.md index d9d06ad1..d7cdefb4 100644 --- a/docs/websocket.md +++ b/docs/websocket.md @@ -27,7 +27,14 @@ This document describes the WebSocket communication protocol between the device "version": 1, "features": { "mcp": true, - "aec": true + "aec": true, + "glyph_push": true + }, + "text_font": { + "bundle": "noto-v1", + "charset": "common", + "size": 20, + "bpp": 4 }, "transport": "websocket", "audio_params": { @@ -39,6 +46,7 @@ This document describes the WebSocket communication protocol between the device } ``` - `features` is optional and generated from compile-time configuration. For example, `"mcp": true` means the device supports MCP, and `"aec": true` is emitted when `CONFIG_USE_SERVER_AEC` is enabled. + - `"glyph_push": true` and `text_font` advertise the optional dynamic text-glyph extension. See [Dynamic Text Glyph Push Extension](glyph-push.md). - `frame_duration` matches `OPUS_FRAME_DURATION_MS` (typically 60 ms). 4. **Server replies with "hello"** diff --git a/docs/websocket_zh.md b/docs/websocket_zh.md index fb335f88..41e03d57 100644 --- a/docs/websocket_zh.md +++ b/docs/websocket_zh.md @@ -26,7 +26,14 @@ "type": "hello", "version": 1, "features": { - "mcp": true + "mcp": true, + "glyph_push": true + }, + "text_font": { + "bundle": "noto-v1", + "charset": "common", + "size": 20, + "bpp": 4 }, "transport": "websocket", "audio_params": { @@ -38,6 +45,7 @@ } ``` - 其中 `features` 字段为可选,内容根据设备编译配置自动生成。例如:`"mcp": true` 表示支持 MCP 协议。 + - `"glyph_push": true` 和 `text_font` 声明可选的动态文字 glyph 扩展,详见[动态文字 Glyph Push 扩展](glyph-push_zh.md)。 - `frame_duration` 的值对应 `OPUS_FRAME_DURATION_MS`(例如 60ms)。 4. **服务器回复 "hello"** diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ca3c4487..d77ecc75 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -14,6 +14,7 @@ set(SOURCES "audio/audio_codec.cc" "led/circular_strip.cc" "led/gpio_led.cc" "display/display.cc" + "display/text_glyph.cc" "display/lcd_display.cc" "display/oled_display.cc" "display/lvgl_display/lvgl_display.cc" @@ -21,12 +22,14 @@ set(SOURCES "audio/audio_codec.cc" "display/lvgl_display/emoji_collection.cc" "display/lvgl_display/lvgl_theme.cc" "display/lvgl_display/lvgl_font.cc" + "display/lvgl_display/dynamic_glyph_cache.cc" "display/lvgl_display/lvgl_image.cc" "display/lvgl_display/gif/lvgl_gif.cc" "display/lvgl_display/gif/gifdec.c" "display/lvgl_display/jpg/image_to_jpeg.cpp" "display/lvgl_display/jpg/jpeg_to_image.c" "protocols/protocol.cc" + "protocols/text_glyph_payload.cc" "protocols/mqtt_protocol.cc" "protocols/websocket_protocol.cc" "mcp_server.cc" @@ -81,8 +84,8 @@ function(find_component_by_pattern PATTERN COMPONENT_VAR PATH_VAR) endfunction() # Set default BUILTIN_TEXT_FONT and BUILTIN_ICON_FONT -set(BUILTIN_TEXT_FONT font_puhui_14_1) -set(BUILTIN_ICON_FONT font_awesome_14_1) +set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) +set(BUILTIN_ICON_FONT font_material_symbols_14_1) set(EMOTE_RESOLUTION "320_240") @@ -90,193 +93,193 @@ set(EMOTE_RESOLUTION "320_240") # Set default assets if the board uses partition table V2 if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI) set(BOARD_TYPE "bread-compact-wifi") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307) set(BOARD_TYPE "bread-compact-ml307") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_NT26) set(BOARD_TYPE "bread-compact-nt26") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32) set(BOARD_TYPE "bread-compact-esp32") elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32_LCD) set(BOARD_TYPE "bread-compact-esp32-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_DF_K10) set(BOARD_TYPE "df-k10") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_DF_S3_AI_CAM) set(BOARD_TYPE "df-s3-ai-cam") elseif(CONFIG_BOARD_TYPE_ESP_BOX_3) set(BOARD_TYPE "esp-box-3") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) set(EMOTE_RESOLUTION "320_240") elseif(CONFIG_BOARD_TYPE_ESP_BOX) set(BOARD_TYPE "esp-box") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) set(EMOTE_RESOLUTION "320_240") elseif(CONFIG_BOARD_TYPE_ESP_BOX_LITE) set(BOARD_TYPE "esp-box-lite") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_2) set(BOARD_TYPE "kevin-box-2") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_KEVIN_C3) set(BOARD_TYPE "kevin-c3") elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V3_DEV) set(BOARD_TYPE "kevin-sp-v3-dev") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V4_DEV) set(BOARD_TYPE "kevin-sp-v4-dev") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD) set(BOARD_TYPE "kevin-yuying-313lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_S3) set(BOARD_TYPE "lichuang-dev") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_QUANDONG_S3_DEV) set(BOARD_TYPE "quandong-s3-dev") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_C3) set(BOARD_TYPE "lichuang-c3-dev") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_RYMCU_BIGSMART) set(MANUFACTURER "rymcu") set(BOARD_TYPE "bigsmart") set(BOARD_NAME "rymcu-bigsmart") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_EDA_TV_PRO) set(MANUFACTURER "lceda-course-examples") set(BOARD_TYPE "eda-tv-pro") elseif(CONFIG_BOARD_TYPE_EDA_ROBOT_PRO) set(MANUFACTURER "lceda-course-examples") set(BOARD_TYPE "eda-robot-pro") - set(BUILTIN_TEXT_FONT font_puhui_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) elseif(CONFIG_BOARD_TYPE_EDA_SUPER_BEAR) set(MANUFACTURER "lceda-course-examples") set(BOARD_TYPE "eda-super-bear") elseif(CONFIG_BOARD_TYPE_MAGICLICK_S3_2P4) set(BOARD_TYPE "magiclick-2p4") - set(BUILTIN_TEXT_FONT font_noto_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_MAGICLICK_S3_2P5) set(BOARD_TYPE "magiclick-2p5") - set(BUILTIN_TEXT_FONT font_noto_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3) set(BOARD_TYPE "magiclick-c3") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3_V2) set(BOARD_TYPE "magiclick-c3-v2") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_M5STACK_STOPWATCH) set(BOARD_TYPE "m5stack-stopwatch") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_S3) set(BOARD_TYPE "m5stack-core-s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_M5STACK_STICK_S3) set(BOARD_TYPE "m5stack-stick-s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_TAB5) set(BOARD_TYPE "m5stack-tab5") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3_ECHO_BASE) set(BOARD_TYPE "atoms3-echo-base") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3R_ECHO_BASE) set(BOARD_TYPE "atoms3r-echo-base") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3R_ECHO_PYRAMID) set(BOARD_TYPE "atoms3r-echo-pyramid") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3R_CAM_M12_ECHO_BASE) set(BOARD_TYPE "atoms3r-cam-m12-echo-base") elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_ECHOS3R) set(BOARD_TYPE "atom-echos3r") elseif(CONFIG_BOARD_TYPE_M5STACK_CARDPUTER_ADV) set(BOARD_TYPE "m5stack-cardputer-adv") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_MATRIX_ECHO_BASE) set(BOARD_TYPE "atommatrix-echo-base") elseif(CONFIG_BOARD_TYPE_XMINI_C3_V3) set(BOARD_TYPE "xmini-c3-v3") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_XMINI_C3_4G) set(BOARD_TYPE "xmini-c3-4g") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_XMINI_C3) set(BOARD_TYPE "xmini-c3") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_ESP_KORVO2_V3) set(BOARD_TYPE "esp32s3-korvo2-v3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ESP_KORVO2_V3_RNDIS) set(BOARD_TYPE "esp32s3-korvo2-v3-rndis") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT) set(BOARD_TYPE "esp-sparkbot") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ESP_SPOT_S3) set(BOARD_TYPE "esp-spot") elseif(CONFIG_BOARD_TYPE_ESP_SPOT_C5) @@ -287,584 +290,584 @@ elseif(CONFIG_BOARD_TYPE_ESP_HI) set(DEFAULT_ASSETS_EXTRA_FILES "${CMAKE_BINARY_DIR}/emoji") elseif(CONFIG_BOARD_TYPE_ESP_VOCAT) set(BOARD_TYPE "esp-vocat") - set(BUILTIN_TEXT_FONT font_puhui_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) set(EMOTE_RESOLUTION "360_360") set(ESP_VOCAT_ASSETS_STAGING "${CMAKE_BINARY_DIR}/esp-vocat-assets") set(EMOTE_EXTERNAL_PATH "${ESP_VOCAT_ASSETS_STAGING}") elseif(CONFIG_BOARD_TYPE_ESP_SENSAIRSHUTTLE) set(BOARD_TYPE "esp-sensairshuttle") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_TOUCH_LCD_3_5) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-touch-lcd-3.5") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_CAM_XXXX) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-cam") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_AUDIO_BOARD) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-audio-board") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_8) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-amoled-1.8") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_8_v2) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-amoled-1.8-v2") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_1_8) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c6-touch-amoled-1.8") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_2_06) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-amoled-2.06") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_2_06) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c6-touch-amoled-2.06") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_2_16) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c6-touch-amoled-2.16") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_2_16) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-amoled-2.16") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_43C) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-amoled-1.43c") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4B) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-4b") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_4_3C) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-4.3c") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_7C) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-7c") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-amoled-1.75") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_75C) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-amoled-1.75") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_83) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-1.83") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_RGB_MATRIX) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-rgb-matrix") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85C) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-1.85c") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85B) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-1.85b") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_85) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-1.85") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_46) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-1.46") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_5) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-3.5") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_5B) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-3.5b") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_ePaper_1_54_v1) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-epaper-1.54") elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_ePaper_1_54_v2) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-epaper-1.54") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_ePaper_3_97) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-epaper-3.97") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_RLCD_4_2) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-rlcd-4.2") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_3_49) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-3.49") - set(LVGL_TEXT_FONT font_puhui_basic_30_4) - set(LVGL_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_LCD_1_54) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-lcd-1.54") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_LCD_0_85) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-lcd-0.85") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C5_TOUCH_LCD_1_69) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c5-touch-lcd-1.69") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_LCD_1_69) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c6-lcd-1.69") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_LCD_1_83) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c6-touch-lcd-1.83") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_1_43) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c6-touch-amoled-1.43") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_AMOLED_1_32) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c6-touch-amoled-1.32") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_TOUCH_LCD_1_54) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c6-touch-lcd-1.54") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_C6_LCD_0_85) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-c6-lcd-0.85") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_S3_TOUCH_AMOLED_1_32) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-s3-touch-amoled-1.32") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_NANO) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-nano") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4B) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4_3) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_5) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_7B) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_3_4C) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_3_5) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd-3.5") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_4C) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_7) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_8) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_WIFI6_TOUCH_LCD_10_1) set(MANUFACTURER "waveshare") set(BOARD_TYPE "esp32-p4-wifi6-touch-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ESP_P4_FUNCTION_EV_BOARD) set(BOARD_TYPE "esp-p4-function-ev-board") elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD) set(BOARD_TYPE "bread-compact-wifi-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_TUDOUZI) set(BOARD_TYPE "tudouzi") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_LILYGO_T_CIRCLE_S3) set(BOARD_TYPE "lilygo-t-circle-s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_0_V1_1) set(BOARD_TYPE "lilygo-t-cameraplus-s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_2) set(BOARD_TYPE "lilygo-t-cameraplus-s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA) set(BOARD_TYPE "lilygo-t-display-s3-pro-mvsrlora") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA_NO_BATTERY) set(BOARD_TYPE "lilygo-t-display-s3-pro-mvsrlora") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3) set(BOARD_TYPE "movecall-moji-esp32s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI2_ESP32C5) set(BOARD_TYPE "movecall-moji2-esp32c5") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_MOVECALL_CUICAN_ESP32S3) set(BOARD_TYPE "movecall-cuican-esp32s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3) set(BOARD_TYPE "atk-dnesp32s3") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX) set(BOARD_TYPE "atk-dnesp32s3-box") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX0) set(BOARD_TYPE "atk-dnesp32s3-box0") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_WIFI) set(BOARD_TYPE "atk-dnesp32s3-box2-wifi") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_4G) set(BOARD_TYPE "atk-dnesp32s3-box2-4g") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_WIFI) set(BOARD_TYPE "atk-dnesp32s3m-wifi") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_4G) set(BOARD_TYPE "atk-dnesp32s3m-4g") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX3) set(BOARD_TYPE "atk-dnesp32s3-box3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_DU_CHATX) set(BOARD_TYPE "du-chatx") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_TAIJI_PI_S3) set(BOARD_TYPE "taiji-pi-s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_85TFT_WIFI) set(BOARD_TYPE "xingzhi-cube-0.85tft-wifi") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_85TFT_ML307) set(BOARD_TYPE "xingzhi-cube-0.85tft-ml307") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_96OLED_WIFI) set(BOARD_TYPE "xingzhi-cube-0.96oled-wifi") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_0_96OLED_ML307) set(BOARD_TYPE "xingzhi-cube-0.96oled-ml307") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_1_54TFT_WIFI) set(BOARD_TYPE "xingzhi-cube-1.54tft-wifi") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_XINGZHI_CUBE_1_54TFT_ML307) set(BOARD_TYPE "xingzhi-cube-1.54tft-ml307") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_XINGZHI_METAL_1_54_WIFI) set(BOARD_TYPE "xingzhi-metal-1.54-wifi") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_XINGZHI_ABS_2_0) set(BOARD_TYPE "xingzhi-abs-2.0") - set(BUILTIN_TEXT_FONT font_noto_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_SEEED_STUDIO_SENSECAP_WATCHER) set(BOARD_TYPE "sensecap-watcher") - set(BUILTIN_TEXT_FONT font_noto_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION noto-emoji_128) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_128) elseif(CONFIG_BOARD_TYPE_DOIT_S3_AIBOX) set(BOARD_TYPE "doit-s3-aibox") elseif(CONFIG_BOARD_TYPE_MIXGO_NOVA) set(BOARD_TYPE "mixgo-nova") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_GENJUTECH_S3_1_54TFT) set(BOARD_TYPE "genjutech-s3-1.54tft") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_CGC) set(BOARD_TYPE "esp32-cgc") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_CGC_144) set(BOARD_TYPE "esp32-cgc-144") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_ESP_S3_LCD_EV_Board) set(BOARD_TYPE "esp-s3-lcd-ev-board") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ESP_S3_LCD_EV_Board_2) set(BOARD_TYPE "esp-s3-lcd-ev-board-2") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_1_54TFT_WIFI) set(BOARD_TYPE "zhengchen-1.54tft-wifi") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_MINSI_K08_DUAL) set(BOARD_TYPE "minsi-k08-dual") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_1_54TFT_ML307) set(BOARD_TYPE "zhengchen-1.54tft-ml307") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_CAM) set(BOARD_TYPE "zhengchen-cam") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_CAM_ML307) set(BOARD_TYPE "zhengchen-cam-ml307") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_SPOTPEAR_ESP32_S3_1_54_MUMA) set(BOARD_TYPE "sp-esp32-s3-1.54-muma") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_SPOTPEAR_ESP32_S3_1_28_BOX) set(BOARD_TYPE "sp-esp32-s3-1.28-box") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_OTTO_ROBOT) set(BOARD_TYPE "otto-robot") - set(BUILTIN_TEXT_FONT font_puhui_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) set(DEFAULT_EMOJI_COLLECTION otto-gif) elseif(CONFIG_BOARD_TYPE_ELECTRON_BOT) set(BOARD_TYPE "electron-bot") - set(BUILTIN_TEXT_FONT font_puhui_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) set(DEFAULT_EMOJI_COLLECTION otto-gif) elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_CAM) set(BOARD_TYPE "bread-compact-wifi-s3cam") - set(BUILTIN_TEXT_FONT font_puhui_basic_16_4) - set(BUILTIN_ICON_FONT font_awesome_16_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_16_4) + set(BUILTIN_ICON_FONT font_material_symbols_16_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_JIUCHUAN) set(BOARD_TYPE "jiuchuan-s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_LABPLUS_MPYTHON_V3) set(BOARD_TYPE "labplus-mpython-v3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_LABPLUS_LEDONG_V2) set(BOARD_TYPE "labplus-ledong-v2") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_SURFER_C3_1_14TFT) set(BOARD_TYPE "surfer-c3-1.14tft") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_YUNLIAO_S3) set(BOARD_TYPE "yunliao-s3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_WIRELESS_TAG_WTP4C5MP07S) set(BOARD_TYPE "wireless-tag-wtp4c5mp07s") - set(BUILTIN_TEXT_FONT font_puhui_basic_30_4) - set(BUILTIN_ICON_FONT font_awesome_30_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_30_4) + set(BUILTIN_ICON_FONT font_material_symbols_30_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_AIPI_LITE) set(BOARD_TYPE "aipi-lite") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) - set(DEFAULT_EMOJI_COLLECTION twemoji_32) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_32) elseif(CONFIG_BOARD_TYPE_HU_087) set(BOARD_TYPE "hu-087") - set(BUILTIN_TEXT_FONT font_puhui_basic_14_1) - set(BUILTIN_ICON_FONT font_awesome_14_1) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_14_1) + set(BUILTIN_ICON_FONT font_material_symbols_14_1) elseif(CONFIG_BOARD_TYPE_Freenove_ESP32S3_DISPLAY_2_8_LCD) set(BOARD_TYPE "freenove-esp32s3-display-2.8-lcd") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) elseif(CONFIG_BOARD_TYPE_AI_VOX3) set(BOARD_TYPE "nulllab-ai-vox-v3") - set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) - set(BUILTIN_ICON_FONT font_awesome_20_4) - set(DEFAULT_EMOJI_COLLECTION twemoji_64) + set(BUILTIN_TEXT_FONT font_noto_sans_basic_20_4) + set(BUILTIN_ICON_FONT font_material_symbols_20_4) + set(DEFAULT_EMOJI_COLLECTION noto-color-emoji_64) endif() if(MANUFACTURER) @@ -1072,6 +1075,7 @@ idf_component_register(SRCS ${SOURCES} efuse bt fatfs + xiaozhi-fonts ${MAIN_PRIV_REQUIRES_EXTRA} ) @@ -1080,9 +1084,16 @@ idf_component_register(SRCS ${SOURCES} if(NOT BOARD_NAME) set(BOARD_NAME ${BOARD_TYPE}) endif() +string(REGEX MATCH "^font_noto_sans_basic_([0-9]+)_([0-9]+)$" _font_match "${BUILTIN_TEXT_FONT}") +if(NOT _font_match) + message(FATAL_ERROR "Unsupported built-in text font name: ${BUILTIN_TEXT_FONT}") +endif() +set(TEXT_FONT_SIZE "${CMAKE_MATCH_1}") +set(TEXT_FONT_BPP "${CMAKE_MATCH_2}") target_compile_definitions(${COMPONENT_LIB} PRIVATE BOARD_TYPE=\"${BOARD_TYPE}\" BOARD_NAME=\"${BOARD_NAME}\" PRIVATE BUILTIN_TEXT_FONT=${BUILTIN_TEXT_FONT} BUILTIN_ICON_FONT=${BUILTIN_ICON_FONT} + PRIVATE TEXT_FONT_SIZE=${TEXT_FONT_SIZE} TEXT_FONT_BPP=${TEXT_FONT_BPP} ) # Add generation rules @@ -1109,9 +1120,9 @@ if(ESP_SR_COMPONENT_PATH) endif() # Find xiaozhi-fonts component dynamically -find_component_by_pattern("xiaozhi-fonts" XIAOZHI_FONTS_COMPONENT XIAOZHI_FONTS_COMPONENT_PATH) -if(XIAOZHI_FONTS_COMPONENT_PATH) - set(XIAOZHI_FONTS_PATH "${XIAOZHI_FONTS_COMPONENT_PATH}") +find_component_by_pattern("xiaozhi-fonts" NOTO_FONTS_COMPONENT NOTO_FONTS_COMPONENT_PATH) +if(NOTO_FONTS_COMPONENT_PATH) + set(NOTO_FONTS_PATH "${NOTO_FONTS_COMPONENT_PATH}") endif() if(CONFIG_BOARD_TYPE_ESP_VOCAT) @@ -1184,6 +1195,10 @@ endif() function(build_default_assets_bin) # Set output path for generated assets.bin set(GENERATED_ASSETS_BIN "${CMAKE_BINARY_DIR}/generated_assets.bin") + set(DEFAULT_ASSETS_DEPENDENCIES + "${SDKCONFIG}" + "${PROJECT_DIR}/scripts/build_default_assets.py" + ) # Prepare arguments for build script set(BUILD_ARGS @@ -1194,11 +1209,23 @@ function(build_default_assets_bin) # Add builtin text font if defined if(BUILTIN_TEXT_FONT) list(APPEND BUILD_ARGS "--builtin_text_font" "${BUILTIN_TEXT_FONT}") + string(REPLACE "_basic_" "_common_" COMMON_TEXT_FONT "${BUILTIN_TEXT_FONT}") + set(COMMON_TEXT_FONT_FILE "${NOTO_FONTS_PATH}/cbin/${COMMON_TEXT_FONT}.bin") + if(EXISTS "${COMMON_TEXT_FONT_FILE}") + list(APPEND DEFAULT_ASSETS_DEPENDENCIES "${COMMON_TEXT_FONT_FILE}") + endif() + if(EXISTS "${NOTO_FONTS_PATH}/manifest.json") + list(APPEND DEFAULT_ASSETS_DEPENDENCIES "${NOTO_FONTS_PATH}/manifest.json") + endif() endif() # Add default emoji collection if defined if(DEFAULT_EMOJI_COLLECTION) list(APPEND BUILD_ARGS "--emoji_collection" "${DEFAULT_EMOJI_COLLECTION}") + file(GLOB DEFAULT_EMOJI_FILES CONFIGURE_DEPENDS + "${NOTO_FONTS_PATH}/png/${DEFAULT_EMOJI_COLLECTION}/*" + ) + list(APPEND DEFAULT_ASSETS_DEPENDENCIES ${DEFAULT_EMOJI_FILES}) endif() # Add default assets extra files if defined @@ -1207,15 +1234,13 @@ function(build_default_assets_bin) endif() list(APPEND BUILD_ARGS "--esp_sr_model_path" "${ESP_SR_MODEL_PATH}") - list(APPEND BUILD_ARGS "--xiaozhi_fonts_path" "${XIAOZHI_FONTS_PATH}") + list(APPEND BUILD_ARGS "--noto_fonts_path" "${NOTO_FONTS_PATH}") # Create custom command to build assets add_custom_command( OUTPUT ${GENERATED_ASSETS_BIN} COMMAND python ${PROJECT_DIR}/scripts/build_default_assets.py ${BUILD_ARGS} - DEPENDS - ${SDKCONFIG} - ${PROJECT_DIR}/scripts/build_default_assets.py + DEPENDS ${DEFAULT_ASSETS_DEPENDENCIES} COMMENT "Building default assets.bin based on configuration" VERBATIM ) diff --git a/main/application.cc b/main/application.cc index 8615b5ab..d3c1d5f9 100644 --- a/main/application.cc +++ b/main/application.cc @@ -1,25 +1,24 @@ #include "application.h" +#include "assets.h" +#include "assets/lang_config.h" +#include "audio_codec.h" #include "board.h" #include "display.h" -#include "system_info.h" -#include "audio_codec.h" -#include "mqtt_protocol.h" -#include "websocket_protocol.h" -#include "assets/lang_config.h" #include "mcp_server.h" -#include "assets.h" +#include "mqtt_protocol.h" #include "settings.h" +#include "system_info.h" +#include "text_glyph_payload.h" +#include "websocket_protocol.h" -#include -#include -#include #include +#include #include -#include +#include +#include #define TAG "Application" - Application::Application() { event_group_ = xEventGroupCreate(); @@ -33,16 +32,16 @@ Application::Application() { aec_mode_ = kAecOff; #endif - esp_timer_create_args_t clock_timer_args = { - .callback = [](void* arg) { - Application* app = (Application*)arg; - xEventGroupSetBits(app->event_group_, MAIN_EVENT_CLOCK_TICK); - }, - .arg = this, - .dispatch_method = ESP_TIMER_TASK, - .name = "clock_timer", - .skip_unhandled_events = true - }; + esp_timer_create_args_t clock_timer_args = {.callback = + [](void* arg) { + Application* app = (Application*)arg; + xEventGroupSetBits(app->event_group_, + MAIN_EVENT_CLOCK_TICK); + }, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "clock_timer", + .skip_unhandled_events = true}; esp_timer_create(&clock_timer_args, &clock_timer_handle_); } @@ -54,9 +53,7 @@ Application::~Application() { vEventGroupDelete(event_group_); } -bool Application::SetDeviceState(DeviceState state) { - return state_machine_.TransitionTo(state); -} +bool Application::SetDeviceState(DeviceState state) { return state_machine_.TransitionTo(state); } void Application::Initialize() { auto& board = Board::GetInstance(); @@ -104,7 +101,7 @@ void Application::Initialize() { // Set network event callback for UI updates and network state handling board.SetNetworkEventCallback([this](NetworkEvent event, const std::string& data) { auto display = Board::GetInstance().GetDisplay(); - + switch (event) { case NetworkEvent::Scanning: display->ShowNotification(Lang::Strings::SCANNING_WIFI, 30000); @@ -144,13 +141,16 @@ void Application::Initialize() { display->SetStatus(Lang::Strings::DETECTING_MODULE); break; case NetworkEvent::ModemErrorNoSim: - Alert(Lang::Strings::ERROR, Lang::Strings::PIN_ERROR, "triangle_exclamation", Lang::Sounds::OGG_ERR_PIN); + Alert(Lang::Strings::ERROR, Lang::Strings::PIN_ERROR, "warning", + Lang::Sounds::OGG_ERR_PIN); break; case NetworkEvent::ModemErrorRegDenied: - Alert(Lang::Strings::ERROR, Lang::Strings::REG_ERROR, "triangle_exclamation", Lang::Sounds::OGG_ERR_REG); + Alert(Lang::Strings::ERROR, Lang::Strings::REG_ERROR, "warning", + Lang::Sounds::OGG_ERR_REG); break; case NetworkEvent::ModemErrorInitFailed: - Alert(Lang::Strings::ERROR, Lang::Strings::MODEM_INIT_ERROR, "triangle_exclamation", Lang::Sounds::OGG_EXCLAMATION); + Alert(Lang::Strings::ERROR, Lang::Strings::MODEM_INIT_ERROR, "warning", + Lang::Sounds::OGG_EXCLAMATION); break; case NetworkEvent::ModemErrorTimeout: display->SetStatus(Lang::Strings::REGISTERING_NETWORK); @@ -169,28 +169,20 @@ void Application::Run() { // Set the priority of the main task to 10 vTaskPrioritySet(nullptr, 10); - const EventBits_t ALL_EVENTS = - MAIN_EVENT_SCHEDULE | - MAIN_EVENT_SEND_AUDIO | - MAIN_EVENT_WAKE_WORD_DETECTED | - MAIN_EVENT_VAD_CHANGE | - MAIN_EVENT_CLOCK_TICK | - MAIN_EVENT_ERROR | - MAIN_EVENT_NETWORK_CONNECTED | - MAIN_EVENT_NETWORK_DISCONNECTED | - MAIN_EVENT_TOGGLE_CHAT | - MAIN_EVENT_START_LISTENING | - MAIN_EVENT_STOP_LISTENING | - MAIN_EVENT_ACTIVATION_DONE | - MAIN_EVENT_STATE_CHANGED | - MAIN_EVENT_PLAYBACK_DRAINED; + const EventBits_t ALL_EVENTS = + MAIN_EVENT_SCHEDULE | MAIN_EVENT_SEND_AUDIO | MAIN_EVENT_WAKE_WORD_DETECTED | + MAIN_EVENT_VAD_CHANGE | MAIN_EVENT_CLOCK_TICK | MAIN_EVENT_ERROR | + MAIN_EVENT_NETWORK_CONNECTED | MAIN_EVENT_NETWORK_DISCONNECTED | MAIN_EVENT_TOGGLE_CHAT | + MAIN_EVENT_START_LISTENING | MAIN_EVENT_STOP_LISTENING | MAIN_EVENT_ACTIVATION_DONE | + MAIN_EVENT_STATE_CHANGED | MAIN_EVENT_PLAYBACK_DRAINED; while (true) { auto bits = xEventGroupWaitBits(event_group_, ALL_EVENTS, pdTRUE, pdFALSE, portMAX_DELAY); if (bits & MAIN_EVENT_ERROR) { SetDeviceState(kDeviceStateIdle); - Alert(Lang::Strings::ERROR, last_error_message_.c_str(), "circle_xmark", Lang::Sounds::OGG_EXCLAMATION); + Alert(Lang::Strings::ERROR, last_error_message_.c_str(), "cancel", + Lang::Sounds::OGG_EXCLAMATION); } if (bits & MAIN_EVENT_NETWORK_CONNECTED) { @@ -238,7 +230,8 @@ void Application::Run() { // stall the Opus codec task (it waits for queue space), which in // turn deadlocks the whole audio input pipeline, as no new // MAIN_EVENT_SEND_AUDIO event would ever be triggered again. - while (audio_service_.PopPacketFromSendQueue()); + while (audio_service_.PopPacketFromSendQueue()) + ; break; } } @@ -268,7 +261,7 @@ void Application::Run() { clock_ticks_++; auto display = Board::GetInstance().GetDisplay(); display->UpdateStatusBar(); - + // Print debug info every 10 seconds if (clock_ticks_ % 10 == 0) { SystemInfo::PrintHeapStats(); @@ -291,12 +284,14 @@ void Application::HandleNetworkConnectedEvent() { return; } - xTaskCreate([](void* arg) { - Application* app = static_cast(arg); - app->ActivationTask(); - app->activation_task_handle_ = nullptr; - vTaskDelete(NULL); - }, "activation", 4096 * 2, this, 2, &activation_task_handle_); + xTaskCreate( + [](void* arg) { + Application* app = static_cast(arg); + app->ActivationTask(); + app->activation_task_handle_ = nullptr; + vTaskDelete(NULL); + }, + "activation", 4096 * 2, this, 2, &activation_task_handle_); } // Update the status bar immediately to show the network state @@ -307,7 +302,8 @@ void Application::HandleNetworkConnectedEvent() { void Application::HandleNetworkDisconnectedEvent() { // Close current conversation when network disconnected auto state = GetDeviceState(); - if (state == kDeviceStateConnecting || state == kDeviceStateListening || state == kDeviceStateSpeaking) { + if (state == kDeviceStateConnecting || state == kDeviceStateListening || + state == kDeviceStateSpeaking) { ESP_LOGI(TAG, "Closing audio channel due to network disconnection"); protocol_->CloseAudioChannel(); } @@ -373,7 +369,7 @@ void Application::CheckAssetsVersion() { ESP_LOGW(TAG, "Assets partition is disabled for board %s", BOARD_NAME); return; } - + Settings settings("assets", true); // Check if there is a new assets need to be downloaded std::string download_url = settings.GetString("download_url"); @@ -383,27 +379,29 @@ void Application::CheckAssetsVersion() { char message[256]; snprintf(message, sizeof(message), Lang::Strings::FOUND_NEW_ASSETS, download_url.c_str()); - Alert(Lang::Strings::LOADING_ASSETS, message, "cloud_arrow_down", Lang::Sounds::OGG_UPGRADE); - + Alert(Lang::Strings::LOADING_ASSETS, message, "cloud_download", Lang::Sounds::OGG_UPGRADE); + // Wait for the audio service to be idle for 3 seconds vTaskDelay(pdMS_TO_TICKS(3000)); SetDeviceState(kDeviceStateUpgrading); board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); display->SetChatMessage("system", Lang::Strings::PLEASE_WAIT); - bool success = assets.Download(download_url, [this, display](int progress, size_t speed) -> void { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024); - Schedule([display, message = std::string(buffer)]() { - display->SetChatMessage("system", message.c_str()); + bool success = + assets.Download(download_url, [this, display](int progress, size_t speed) -> void { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024); + Schedule([display, message = std::string(buffer)]() { + display->SetChatMessage("system", message.c_str()); + }); }); - }); board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); vTaskDelay(pdMS_TO_TICKS(1000)); if (!success) { - Alert(Lang::Strings::ERROR, Lang::Strings::DOWNLOAD_ASSETS_FAILED, "circle_xmark", Lang::Sounds::OGG_EXCLAMATION); + Alert(Lang::Strings::ERROR, Lang::Strings::DOWNLOAD_ASSETS_FAILED, "cancel", + Lang::Sounds::OGG_EXCLAMATION); vTaskDelay(pdMS_TO_TICKS(2000)); SetDeviceState(kDeviceStateActivating); return; @@ -413,13 +411,13 @@ void Application::CheckAssetsVersion() { // Apply assets assets.Apply(); display->SetChatMessage("system", ""); - display->SetEmotion("microchip_ai"); + display->SetEmotion("robot_2"); } void Application::CheckNewVersion() { const int MAX_RETRY = 10; int retry_count = 0; - int retry_delay = 10; // Initial retry delay in seconds + int retry_delay = 10; // Initial retry delay in seconds auto& board = Board::GetInstance(); while (true) { @@ -435,27 +433,30 @@ void Application::CheckNewVersion() { } char error_message[128]; - snprintf(error_message, sizeof(error_message), "code=%d, url=%s", err, ota_->GetCheckVersionUrl().c_str()); + snprintf(error_message, sizeof(error_message), "code=%d, url=%s", err, + ota_->GetCheckVersionUrl().c_str()); char buffer[256]; - snprintf(buffer, sizeof(buffer), Lang::Strings::CHECK_NEW_VERSION_FAILED, retry_delay, error_message); - Alert(Lang::Strings::ERROR, buffer, "cloud_slash", Lang::Sounds::OGG_EXCLAMATION); + snprintf(buffer, sizeof(buffer), Lang::Strings::CHECK_NEW_VERSION_FAILED, retry_delay, + error_message); + Alert(Lang::Strings::ERROR, buffer, "cloud_off", Lang::Sounds::OGG_EXCLAMATION); - ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", retry_delay, retry_count, MAX_RETRY); + ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", retry_delay, + retry_count, MAX_RETRY); for (int i = 0; i < retry_delay; i++) { vTaskDelay(pdMS_TO_TICKS(1000)); if (GetDeviceState() == kDeviceStateIdle) { break; } } - retry_delay *= 2; // Double the retry delay + retry_delay *= 2; // Double the retry delay continue; } retry_count = 0; - retry_delay = 10; // Reset retry delay + retry_delay = 10; // Reset retry delay if (ota_->HasNewVersion()) { if (UpgradeFirmware(ota_->GetFirmwareUrl(), ota_->GetFirmwareVersion())) { - return; // This line will never be reached after reboot + return; // This line will never be reached after reboot } // If upgrade failed, continue to normal operation } @@ -507,29 +508,29 @@ void Application::InitializeProtocol() { protocol_ = std::make_unique(); } - protocol_->OnConnected([this]() { - DismissAlert(); - }); + protocol_->OnConnected([this]() { DismissAlert(); }); protocol_->OnNetworkError([this](const std::string& message) { last_error_message_ = message; xEventGroupSetBits(event_group_, MAIN_EVENT_ERROR); }); - + protocol_->OnIncomingAudio([this](std::unique_ptr packet) { if (GetDeviceState() == kDeviceStateSpeaking) { audio_service_.PushPacketToDecodeQueue(std::move(packet)); } }); - + protocol_->OnAudioChannelOpened([this, codec, &board]() { board.SetPowerSaveLevel(PowerSaveLevel::PERFORMANCE); if (protocol_->server_sample_rate() != codec->output_sample_rate()) { - ESP_LOGW(TAG, "Server sample rate %d does not match device output sample rate %d, resampling may cause distortion", - protocol_->server_sample_rate(), codec->output_sample_rate()); + ESP_LOGW(TAG, + "Server sample rate %d does not match device output sample rate %d, " + "resampling may cause distortion", + protocol_->server_sample_rate(), codec->output_sample_rate()); } }); - + protocol_->OnAudioChannelClosed([this, &board]() { board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); Schedule([this]() { @@ -538,12 +539,19 @@ void Application::InitializeProtocol() { SetDeviceState(kDeviceStateIdle); }); }); - + protocol_->OnIncomingJson([this, display](const cJSON* root) { // Parse JSON data auto type = cJSON_GetObjectItem(root, "type"); + if (!cJSON_IsString(type)) { + ESP_LOGW(TAG, "Incoming JSON message has no type"); + return; + } if (strcmp(type->valuestring, "tts") == 0) { auto state = cJSON_GetObjectItem(root, "state"); + if (!cJSON_IsString(state)) { + return; + } if (strcmp(state->valuestring, "start") == 0) { Schedule([this]() { aborted_ = false; @@ -562,8 +570,15 @@ void Application::InitializeProtocol() { } else if (strcmp(state->valuestring, "sentence_start") == 0) { auto text = cJSON_GetObjectItem(root, "text"); if (cJSON_IsString(text)) { + std::vector glyphs; + uint8_t bpp = 0; + if (!TextGlyphPayload::Parse(root, glyphs, bpp)) { + glyphs.clear(); + } ESP_LOGI(TAG, "<< %s", text->valuestring); - Schedule([display, message = std::string(text->valuestring)]() { + Schedule([display, message = std::string(text->valuestring), + glyphs = std::move(glyphs), bpp]() { + display->AddTextGlyphs(glyphs, bpp); display->SetChatMessage("assistant", message.c_str()); }); } @@ -571,8 +586,15 @@ void Application::InitializeProtocol() { } else if (strcmp(type->valuestring, "stt") == 0) { auto text = cJSON_GetObjectItem(root, "text"); if (cJSON_IsString(text)) { + std::vector glyphs; + uint8_t bpp = 0; + if (!TextGlyphPayload::Parse(root, glyphs, bpp)) { + glyphs.clear(); + } ESP_LOGI(TAG, ">> %s", text->valuestring); - Schedule([display, message = std::string(text->valuestring)]() { + Schedule([display, message = std::string(text->valuestring), + glyphs = std::move(glyphs), bpp]() { + display->AddTextGlyphs(glyphs, bpp); display->SetChatMessage("user", message.c_str()); }); } @@ -594,9 +616,7 @@ void Application::InitializeProtocol() { ESP_LOGI(TAG, "System command: %s", command->valuestring); if (strcmp(command->valuestring, "reboot") == 0) { // Do a reboot if user requests a OTA update - Schedule([this]() { - Reboot(); - }); + Schedule([this]() { Reboot(); }); } else { ESP_LOGW(TAG, "Unknown system command: %s", command->valuestring); } @@ -606,7 +626,8 @@ void Application::InitializeProtocol() { auto message = cJSON_GetObjectItem(root, "message"); auto emotion = cJSON_GetObjectItem(root, "emotion"); if (cJSON_IsString(status) && cJSON_IsString(message) && cJSON_IsString(emotion)) { - Alert(status->valuestring, message->valuestring, emotion->valuestring, Lang::Sounds::OGG_VIBRATION); + Alert(status->valuestring, message->valuestring, emotion->valuestring, + Lang::Sounds::OGG_VIBRATION); } else { ESP_LOGW(TAG, "Alert command requires status, message and emotion"); } @@ -615,9 +636,10 @@ void Application::InitializeProtocol() { auto payload = cJSON_GetObjectItem(root, "payload"); ESP_LOGI(TAG, "Received custom message: %s", cJSON_PrintUnformatted(root)); if (cJSON_IsObject(payload)) { - Schedule([this, display, payload_str = std::string(cJSON_PrintUnformatted(payload))]() { - display->SetChatMessage("system", payload_str.c_str()); - }); + Schedule( + [this, display, payload_str = std::string(cJSON_PrintUnformatted(payload))]() { + display->SetChatMessage("system", payload_str.c_str()); + }); } else { ESP_LOGW(TAG, "Invalid custom message format: missing payload"); } @@ -626,7 +648,7 @@ void Application::InitializeProtocol() { ESP_LOGW(TAG, "Unknown message type: %s", type->valuestring); } }); - + protocol_->Start(); } @@ -635,32 +657,27 @@ void Application::ShowActivationCode(const std::string& code, const std::string& char digit; const std::string_view& sound; }; - static const std::array digit_sounds{{ - digit_sound{'0', Lang::Sounds::OGG_0}, - digit_sound{'1', Lang::Sounds::OGG_1}, - digit_sound{'2', Lang::Sounds::OGG_2}, - digit_sound{'3', Lang::Sounds::OGG_3}, - digit_sound{'4', Lang::Sounds::OGG_4}, - digit_sound{'5', Lang::Sounds::OGG_5}, - digit_sound{'6', Lang::Sounds::OGG_6}, - digit_sound{'7', Lang::Sounds::OGG_7}, - digit_sound{'8', Lang::Sounds::OGG_8}, - digit_sound{'9', Lang::Sounds::OGG_9} - }}; + static const std::array digit_sounds{ + {digit_sound{'0', Lang::Sounds::OGG_0}, digit_sound{'1', Lang::Sounds::OGG_1}, + digit_sound{'2', Lang::Sounds::OGG_2}, digit_sound{'3', Lang::Sounds::OGG_3}, + digit_sound{'4', Lang::Sounds::OGG_4}, digit_sound{'5', Lang::Sounds::OGG_5}, + digit_sound{'6', Lang::Sounds::OGG_6}, digit_sound{'7', Lang::Sounds::OGG_7}, + digit_sound{'8', Lang::Sounds::OGG_8}, digit_sound{'9', Lang::Sounds::OGG_9}}}; // This sentence uses 9KB of SRAM, so we need to wait for it to finish Alert(Lang::Strings::ACTIVATION, message.c_str(), "link", Lang::Sounds::OGG_ACTIVATION); for (const auto& digit : code) { auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(), - [digit](const digit_sound& ds) { return ds.digit == digit; }); + [digit](const digit_sound& ds) { return ds.digit == digit; }); if (it != digit_sounds.end()) { audio_service_.PlaySound(it->sound); } } } -void Application::Alert(const char* status, const char* message, const char* emotion, const std::string_view& sound) { +void Application::Alert(const char* status, const char* message, const char* emotion, + const std::string_view& sound) { ESP_LOGW(TAG, "Alert [%s] %s: %s", emotion, status, message); auto display = Board::GetInstance().GetDisplay(); display->SetStatus(status); @@ -680,21 +697,15 @@ void Application::DismissAlert() { } } -void Application::ToggleChatState() { - xEventGroupSetBits(event_group_, MAIN_EVENT_TOGGLE_CHAT); -} +void Application::ToggleChatState() { xEventGroupSetBits(event_group_, MAIN_EVENT_TOGGLE_CHAT); } -void Application::StartListening() { - xEventGroupSetBits(event_group_, MAIN_EVENT_START_LISTENING); -} +void Application::StartListening() { xEventGroupSetBits(event_group_, MAIN_EVENT_START_LISTENING); } -void Application::StopListening() { - xEventGroupSetBits(event_group_, MAIN_EVENT_STOP_LISTENING); -} +void Application::StopListening() { xEventGroupSetBits(event_group_, MAIN_EVENT_STOP_LISTENING); } void Application::HandleToggleChatEvent() { auto state = GetDeviceState(); - + if (state == kDeviceStateActivating) { SetDeviceState(kDeviceStateIdle); return; @@ -718,9 +729,7 @@ void Application::HandleToggleChatEvent() { if (!protocol_->IsAudioChannelOpened()) { SetDeviceState(kDeviceStateConnecting); // Schedule to let the state change be processed first (UI update) - Schedule([this, mode]() { - ContinueOpenAudioChannel(mode); - }); + Schedule([this, mode]() { ContinueOpenAudioChannel(mode); }); return; } SetListeningMode(mode); @@ -743,6 +752,9 @@ void Application::ContinueOpenAudioChannel(ListeningMode mode) { if (!protocol_->IsAudioChannelOpened()) { if (!protocol_->OpenAudioChannel()) { + // Return to idle so the device is not stuck in the connecting + // state (not every failure path reports a network error) + SetDeviceState(kDeviceStateIdle); return; } } @@ -752,7 +764,7 @@ void Application::ContinueOpenAudioChannel(ListeningMode mode) { void Application::HandleStartListeningEvent() { auto state = GetDeviceState(); - + if (state == kDeviceStateActivating) { SetDeviceState(kDeviceStateIdle); return; @@ -766,14 +778,12 @@ void Application::HandleStartListeningEvent() { ESP_LOGE(TAG, "Protocol not initialized"); return; } - + if (state == kDeviceStateIdle) { if (!protocol_->IsAudioChannelOpened()) { SetDeviceState(kDeviceStateConnecting); // Schedule to let the state change be processed first (UI update) - Schedule([this]() { - ContinueOpenAudioChannel(kListeningModeManualStop); - }); + Schedule([this]() { ContinueOpenAudioChannel(kListeningModeManualStop); }); return; } SetListeningMode(kListeningModeManualStop); @@ -785,7 +795,7 @@ void Application::HandleStartListeningEvent() { void Application::HandleStopListeningEvent() { auto state = GetDeviceState(); - + if (state == kDeviceStateAudioTesting) { audio_service_.EnableAudioTesting(false); SetDeviceState(kDeviceStateWifiConfiguring); @@ -808,24 +818,12 @@ void Application::HandleWakeWordDetectedEvent() { ESP_LOGI(TAG, "Wake word detected: %s (state: %d)", wake_word.c_str(), (int)state); if (state == kDeviceStateIdle) { - audio_service_.EncodeWakeWord(); - auto wake_word = audio_service_.GetLastWakeWord(); - - if (!protocol_->IsAudioChannelOpened()) { - SetDeviceState(kDeviceStateConnecting); - // Schedule to let the state change be processed first (UI update), - // then continue with OpenAudioChannel which may block for ~1 second - Schedule([this, wake_word]() { - ContinueWakeWordInvoke(wake_word); - }); - return; - } - // Channel already opened, continue directly - ContinueWakeWordInvoke(wake_word); + BeginWakeWordInvoke(wake_word); } else if (state == kDeviceStateSpeaking || state == kDeviceStateListening) { AbortSpeaking(kAbortReasonWakeWordDetected); // Clear send queue to avoid sending residues to server - while (audio_service_.PopPacketFromSendQueue()); + while (audio_service_.PopPacketFromSendQueue()) + ; if (state == kDeviceStateListening) { protocol_->SendStartListening(GetDefaultListeningMode()); @@ -844,6 +842,30 @@ void Application::HandleWakeWordDetectedEvent() { } } +void Application::BeginWakeWordInvoke(const std::string& wake_word) { + // Must run in the main task with the device in idle state + audio_service_.EncodeWakeWord(); + + // Always pass through the connecting state, even if the audio channel is + // already opened. ContinueWakeWordInvoke() rejects any other state, so + // skipping this transition would silently drop the wake word invocation. + if (!SetDeviceState(kDeviceStateConnecting)) { + // Wake word detection was stopped by the detection itself; restore it + // so the device does not become unresponsive to wake words. + audio_service_.EnableWakeWordDetection(true); + return; + } + + if (!protocol_->IsAudioChannelOpened()) { + // Schedule to let the state change be processed first (UI update), + // then continue with OpenAudioChannel which may block for ~1 second + Schedule([this, wake_word]() { ContinueWakeWordInvoke(wake_word); }); + return; + } + // Channel already opened, continue directly + ContinueWakeWordInvoke(wake_word); +} + void Application::ContinueWakeWordInvoke(const std::string& wake_word) { // Check state again in case it was changed during scheduling if (GetDeviceState() != kDeviceStateConnecting) { @@ -856,7 +878,10 @@ void Application::ContinueWakeWordInvoke(const std::string& wake_word) { if (!protocol_->IsAudioChannelOpened()) { if (!protocol_->OpenAudioChannel()) { - audio_service_.EnableWakeWordDetection(true); + // Return to idle so the device is not stuck in the connecting + // state (not every failure path reports a network error), and + // wake word detection is re-enabled by the idle state handler. + SetDeviceState(kDeviceStateIdle); return; } } @@ -889,13 +914,13 @@ void Application::HandleStateChangedEvent() { auto display = board.GetDisplay(); auto led = board.GetLed(); led->OnStateChanged(); - + switch (new_state) { case kDeviceStateUnknown: case kDeviceStateIdle: display->SetStatus(Lang::Strings::STANDBY); - display->ClearChatMessages(); // Clear messages first - display->SetEmotion("neutral"); // Then set emotion (wechat mode checks child count) + display->ClearChatMessages(); // Clear messages first + display->SetEmotion("neutral"); // Then set emotion (wechat mode checks child count) audio_service_.EnableVoiceProcessing(false); audio_service_.EnableWakeWordDetection(true); break; @@ -1025,7 +1050,8 @@ bool Application::UpgradeFirmware(const std::string& url, const std::string& ver } ESP_LOGI(TAG, "Starting firmware upgrade from URL: %s", upgrade_url.c_str()); - Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "download", Lang::Sounds::OGG_UPGRADE); + Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "download", + Lang::Sounds::OGG_UPGRADE); vTaskDelay(pdMS_TO_TICKS(3000)); SetDeviceState(kDeviceStateUpgrading); @@ -1047,17 +1073,19 @@ bool Application::UpgradeFirmware(const std::string& url, const std::string& ver if (!upgrade_success) { // Upgrade failed, restart audio service and continue running - ESP_LOGE(TAG, "Firmware upgrade failed, restarting audio service and continuing operation..."); - audio_service_.Start(); // Restart audio service - board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); // Restore power save level - Alert(Lang::Strings::ERROR, Lang::Strings::UPGRADE_FAILED, "circle_xmark", Lang::Sounds::OGG_EXCLAMATION); + ESP_LOGE(TAG, + "Firmware upgrade failed, restarting audio service and continuing operation..."); + audio_service_.Start(); // Restart audio service + board.SetPowerSaveLevel(PowerSaveLevel::LOW_POWER); // Restore power save level + Alert(Lang::Strings::ERROR, Lang::Strings::UPGRADE_FAILED, "cancel", + Lang::Sounds::OGG_EXCLAMATION); vTaskDelay(pdMS_TO_TICKS(3000)); return false; } else { // Upgrade success, reboot immediately ESP_LOGI(TAG, "Firmware upgrade successful, rebooting..."); display->SetChatMessage("system", "Upgrade successful, rebooting..."); - vTaskDelay(pdMS_TO_TICKS(1000)); // Brief pause to show message + vTaskDelay(pdMS_TO_TICKS(1000)); // Brief pause to show message Reboot(); return true; } @@ -1069,25 +1097,18 @@ void Application::WakeWordInvoke(const std::string& wake_word) { } auto state = GetDeviceState(); - - if (state == kDeviceStateIdle) { - audio_service_.EncodeWakeWord(); - if (!protocol_->IsAudioChannelOpened()) { - SetDeviceState(kDeviceStateConnecting); - // Schedule to let the state change be processed first (UI update) - Schedule([this, wake_word]() { - ContinueWakeWordInvoke(wake_word); - }); - return; - } - // Channel already opened, continue directly - ContinueWakeWordInvoke(wake_word); - } else if (state == kDeviceStateSpeaking) { - Schedule([this]() { - AbortSpeaking(kAbortReasonNone); + if (state == kDeviceStateIdle) { + // May be called from outside the main task (e.g. board button + // callbacks), so schedule the invocation instead of running it here + Schedule([this, wake_word]() { + if (GetDeviceState() == kDeviceStateIdle) { + BeginWakeWordInvoke(wake_word); + } }); - } else if (state == kDeviceStateListening) { + } else if (state == kDeviceStateSpeaking) { + Schedule([this]() { AbortSpeaking(kAbortReasonNone); }); + } else if (state == kDeviceStateListening) { Schedule([this]() { if (protocol_) { protocol_->CloseAudioChannel(); @@ -1119,7 +1140,7 @@ void Application::RegisterMcpBroadcastCallback(std::functionSendMcpMessage(payload); } @@ -1135,18 +1156,18 @@ void Application::SetAecMode(AecMode mode) { auto& board = Board::GetInstance(); auto display = board.GetDisplay(); switch (aec_mode_) { - case kAecOff: - audio_service_.EnableDeviceAec(false); - display->ShowNotification(Lang::Strings::RTC_MODE_OFF); - break; - case kAecOnServerSide: - audio_service_.EnableDeviceAec(false); - display->ShowNotification(Lang::Strings::RTC_MODE_ON); - break; - case kAecOnDeviceSide: - audio_service_.EnableDeviceAec(true); - display->ShowNotification(Lang::Strings::RTC_MODE_ON); - break; + case kAecOff: + audio_service_.EnableDeviceAec(false); + display->ShowNotification(Lang::Strings::RTC_MODE_OFF); + break; + case kAecOnServerSide: + audio_service_.EnableDeviceAec(false); + display->ShowNotification(Lang::Strings::RTC_MODE_ON); + break; + case kAecOnDeviceSide: + audio_service_.EnableDeviceAec(true); + display->ShowNotification(Lang::Strings::RTC_MODE_ON); + break; } // If the AEC mode is changed, close the audio channel @@ -1156,9 +1177,7 @@ void Application::SetAecMode(AecMode mode) { }); } -void Application::PlaySound(const std::string_view& sound) { - audio_service_.PlaySound(sound); -} +void Application::PlaySound(const std::string_view& sound) { audio_service_.PlaySound(sound); } void Application::ResetProtocol() { Schedule([this]() { @@ -1170,4 +1189,3 @@ void Application::ResetProtocol() { protocol_.reset(); }); } - diff --git a/main/application.h b/main/application.h index 464140f0..9d828ca8 100644 --- a/main/application.h +++ b/main/application.h @@ -160,6 +160,7 @@ private: void HandleActivationDoneEvent(); void HandleWakeWordDetectedEvent(); void ContinueOpenAudioChannel(ListeningMode mode); + void BeginWakeWordInvoke(const std::string& wake_word); void ContinueWakeWordInvoke(const std::string& wake_word); void StartListeningAudio(); void ConfigureWakeWordForListening(); diff --git a/main/assets.cc b/main/assets.cc index 93e95213..1c01e336 100644 --- a/main/assets.cc +++ b/main/assets.cc @@ -1,33 +1,37 @@ #include "assets.h" +#include "application.h" #include "board.h" #include "display.h" -#include "application.h" -#include "lvgl_theme.h" #include "emote_display.h" #include "expression_emote.h" +#include "lvgl_theme.h" #if HAVE_LVGL -#include "display/lcd_display.h" #include +#include "display/lcd_display.h" +#include "display/lvgl_display/lvgl_display.h" #endif +#include #include #include -#include #include +#include +#include #define TAG "Assets" #define PARTITION_LABEL "assets" struct mmap_assets_table { - char asset_name[32]; /*!< Name of the asset */ - uint32_t asset_size; /*!< Size of the asset */ - uint32_t asset_offset; /*!< Offset of the asset */ - uint16_t asset_width; /*!< Width of the asset */ - uint16_t asset_height; /*!< Height of the asset */ + char asset_name[32]; /*!< Name of the asset */ + uint32_t asset_size; /*!< Size of the asset */ + uint32_t asset_offset; /*!< Offset of the asset */ + uint16_t asset_width; /*!< Width of the asset */ + uint16_t asset_height; /*!< Height of the asset */ }; Assets::Assets() { + UseBuiltInTextFontCapability(); #if HAVE_LVGL strategy_ = std::make_unique(); #else @@ -37,12 +41,11 @@ Assets::Assets() { InitializePartition(); } -Assets::~Assets() { - UnApplyPartition(); -} +Assets::~Assets() { UnApplyPartition(); } bool Assets::FindPartition(Assets* assets) { - assets->partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, PARTITION_LABEL); + assets->partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, + PARTITION_LABEL); if (assets->partition_ == nullptr) { ESP_LOGI(TAG, "No assets partition found"); return false; @@ -59,11 +62,24 @@ bool Assets::InitializePartition() { } void Assets::UnApplyPartition() { + UseBuiltInTextFontCapability(); if (strategy_) { strategy_->UnApplyPartition(this); } } +void Assets::UseBuiltInTextFontCapability() { + text_font_capability_ = { + .glyph_push = true, + .bundle = NOTO_FONT_BUNDLE_ID, + .charset = "basic", + .size = TEXT_FONT_SIZE, + .bpp = TEXT_FONT_BPP, + }; +} + +void Assets::DisableTextFontGlyphPush() { text_font_capability_ = {}; } + bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) { return strategy_ ? strategy_->GetAssetData(this, name, ptr, size) : false; } @@ -140,11 +156,14 @@ bool Assets::LvglStrategy::InitializePartition(Assets* assets) { ESP_LOGI(TAG, "The storage free size is %ld KB", storage_size / 1024); ESP_LOGI(TAG, "The partition size is %ld KB", assets->partition_->size / 1024); if (storage_size < assets->partition_->size) { - ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, assets->partition_->size / 1024); + ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", + storage_size / 1024, assets->partition_->size / 1024); return false; } - esp_err_t err = esp_partition_mmap(assets->partition_, 0, assets->partition_->size, ESP_PARTITION_MMAP_DATA, (const void**)&mmap_root_, &mmap_handle_); + esp_err_t err = + esp_partition_mmap(assets->partition_, 0, assets->partition_->size, ESP_PARTITION_MMAP_DATA, + (const void**)&mmap_root_, &mmap_handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to mmap assets partition: %s", esp_err_to_name(err)); return false; @@ -157,7 +176,8 @@ bool Assets::LvglStrategy::InitializePartition(Assets* assets) { uint32_t stored_len = *(uint32_t*)(mmap_root_ + 8); if (stored_len > assets->partition_->size - 12) { - ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, assets->partition_->size); + ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", + stored_len, assets->partition_->size); return false; } @@ -167,7 +187,8 @@ bool Assets::LvglStrategy::InitializePartition(Assets* assets) { ESP_LOGI(TAG, "The checksum calculation time is %d ms", int((end_time - start_time) / 1000)); if (calculated_checksum != stored_chksum) { - ESP_LOGE(TAG, "The calculated checksum (0x%lx) does not match the stored checksum (0x%lx)", calculated_checksum, stored_chksum); + ESP_LOGE(TAG, "The calculated checksum (0x%lx) does not match the stored checksum (0x%lx)", + calculated_checksum, stored_chksum); return false; } @@ -175,10 +196,9 @@ bool Assets::LvglStrategy::InitializePartition(Assets* assets) { for (uint32_t i = 0; i < stored_files; i++) { auto item = (const mmap_assets_table*)(mmap_root_ + 12 + i * sizeof(mmap_assets_table)); - auto asset = Asset{ - .size = static_cast(item->asset_size), - .offset = static_cast(12 + sizeof(mmap_assets_table) * stored_files + item->asset_offset) - }; + auto asset = Asset{.size = static_cast(item->asset_size), + .offset = static_cast( + 12 + sizeof(mmap_assets_table) * stored_files + item->asset_offset)}; assets_[item->asset_name] = asset; } return checksum_valid_; @@ -192,17 +212,19 @@ void Assets::LvglStrategy::UnApplyPartition(Assets* assets) { } checksum_valid_ = false; assets_.clear(); - (void)assets; // Unused parameter + (void)assets; // Unused parameter } -bool Assets::LvglStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) { +bool Assets::LvglStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, + size_t& size) { auto asset = assets_.find(name); if (asset == assets_.end()) { return false; } auto data = (const char*)(mmap_root_ + asset->second.offset); if (data[0] != 'Z' || data[1] != 'Z') { - ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], data[1]); + ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], + data[1]); return false; } @@ -228,7 +250,8 @@ bool Assets::LvglStrategy::Apply(Assets* assets, bool refresh_display_theme) { cJSON* version = cJSON_GetObjectItem(root, "version"); if (cJSON_IsNumber(version)) { if (version->valuedouble > 1) { - ESP_LOGE(TAG, "The assets version %d is not supported, please upgrade the firmware", version->valueint); + ESP_LOGE(TAG, "The assets version %d is not supported, please upgrade the firmware", + version->valueint); return false; } } @@ -244,15 +267,39 @@ bool Assets::LvglStrategy::Apply(Assets* assets, bool refresh_display_theme) { std::string fonts_text_file = font->valuestring; if (assets->GetAssetData(fonts_text_file, ptr, size)) { auto text_font = std::make_shared(ptr); - if (text_font->font() == nullptr) { - ESP_LOGE(TAG, "Failed to load fonts.bin"); - return false; - } - if (light_theme != nullptr) { - light_theme->set_text_font(text_font); - } - if (dark_theme != nullptr) { - dark_theme->set_text_font(text_font); + auto display = dynamic_cast(Board::GetInstance().GetDisplay()); + if (text_font->font() == nullptr || display == nullptr || + !display->SetTextFont(text_font)) { + ESP_LOGW(TAG, "Ignoring invalid text font asset %s", fonts_text_file.c_str()); + } else { + assets->DisableTextFontGlyphPush(); + + cJSON* metadata = cJSON_GetObjectItem(root, "text_font_meta"); + cJSON* charset = cJSON_GetObjectItem(metadata, "charset"); + cJSON* font_size = cJSON_GetObjectItem(metadata, "size"); + cJSON* font_bpp = cJSON_GetObjectItem(metadata, "bpp"); + cJSON* bundle = cJSON_GetObjectItem(metadata, "bundle"); + bool supports_glyph_push = + cJSON_IsString(charset) && + (std::strcmp(charset->valuestring, "basic") == 0 || + std::strcmp(charset->valuestring, "common") == 0) && + cJSON_IsNumber(font_size) && font_size->valueint > 0 && + font_size->valuedouble == font_size->valueint && cJSON_IsNumber(font_bpp) && + font_bpp->valuedouble == font_bpp->valueint && + (font_bpp->valueint == 1 || font_bpp->valueint == 4) && + font_bpp->valueint == text_font->bpp() && cJSON_IsString(bundle) && + bundle->valuestring[0] != '\0' && std::strlen(bundle->valuestring) <= 64; + if (supports_glyph_push) { + assets->text_font_capability_ = { + .glyph_push = true, + .bundle = bundle->valuestring, + .charset = charset->valuestring, + .size = font_size->valueint, + .bpp = font_bpp->valueint, + }; + } else { + ESP_LOGW(TAG, "Loaded custom text font without compatible glyph push metadata"); + } } } else { ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str()); @@ -269,12 +316,14 @@ bool Assets::LvglStrategy::Apply(Assets* assets, bool refresh_display_theme) { cJSON* name = cJSON_GetObjectItem(emoji, "name"); cJSON* file = cJSON_GetObjectItem(emoji, "file"); cJSON* eaf = cJSON_GetObjectItem(emoji, "eaf"); - if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL== eaf)) { + if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL == eaf)) { if (!assets->GetAssetData(file->valuestring, ptr, size)) { - ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring); + ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, + file->valuestring); continue; } - custom_emoji_collection->AddEmoji(name->valuestring, new LvglRawImage(ptr, size)); + custom_emoji_collection->AddEmoji(name->valuestring, + new LvglRawImage(ptr, size)); } } } @@ -284,6 +333,7 @@ bool Assets::LvglStrategy::Apply(Assets* assets, bool refresh_display_theme) { if (dark_theme != nullptr) { dark_theme->set_emoji_collection(custom_emoji_collection); } + Board::GetInstance().GetDisplay()->SetEmojiCollection(custom_emoji_collection); } cJSON* skin = cJSON_GetObjectItem(root, "skin"); @@ -297,12 +347,15 @@ bool Assets::LvglStrategy::Apply(Assets* assets, bool refresh_display_theme) { light_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring)); } if (cJSON_IsString(background_color)) { - light_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring)); - light_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring)); + light_theme->set_background_color( + LvglTheme::ParseColor(background_color->valuestring)); + light_theme->set_chat_background_color( + LvglTheme::ParseColor(background_color->valuestring)); } if (cJSON_IsString(background_image)) { if (!assets->GetAssetData(background_image->valuestring, ptr, size)) { - ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring); + ESP_LOGE(TAG, "The background image file %s is not found", + background_image->valuestring); return false; } auto background_image = std::make_shared(ptr); @@ -318,12 +371,15 @@ bool Assets::LvglStrategy::Apply(Assets* assets, bool refresh_display_theme) { dark_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring)); } if (cJSON_IsString(background_color)) { - dark_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring)); - dark_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring)); + dark_theme->set_background_color( + LvglTheme::ParseColor(background_color->valuestring)); + dark_theme->set_chat_background_color( + LvglTheme::ParseColor(background_color->valuestring)); } if (cJSON_IsString(background_image)) { if (!assets->GetAssetData(background_image->valuestring, ptr, size)) { - ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring); + ESP_LOGE(TAG, "The background image file %s is not found", + background_image->valuestring); return false; } auto background_image = std::make_shared(ptr); @@ -352,11 +408,11 @@ bool Assets::LvglStrategy::Apply(Assets* assets, bool refresh_display_theme) { } } } - + cJSON_Delete(root); return true; } -#endif // HAVE_LVGL +#endif // HAVE_LVGL bool Assets::EmoteStrategy::InitializePartition(Assets* assets) { assets->partition_valid_ = false; @@ -371,12 +427,14 @@ bool Assets::EmoteStrategy::InitializePartition(Assets* assets) { if (emote_display && emote_display->GetEmoteHandle() != nullptr) { const emote_data_t data = { .type = EMOTE_SOURCE_PARTITION, - .source = { - .partition_label = PARTITION_LABEL, - }, - .flags = { - .mmap_enable = true, //must be true here!!! - }, + .source = + { + .partition_label = PARTITION_LABEL, + }, + .flags = + { + .mmap_enable = true, // must be true here!!! + }, }; ret = emote_mount_assets(emote_display->GetEmoteHandle(), &data); } else { @@ -392,16 +450,18 @@ void Assets::EmoteStrategy::UnApplyPartition(Assets* assets) { if (emote_display && emote_display->GetEmoteHandle() != nullptr) { emote_unmount_assets(emote_display->GetEmoteHandle()); } - (void)assets; // Unused parameter + (void)assets; // Unused parameter } -bool Assets::EmoteStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) { +bool Assets::EmoteStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr, + size_t& size) { auto display = Board::GetInstance().GetDisplay(); auto* emote_display = dynamic_cast(display); if (emote_display && emote_display->GetEmoteHandle() != nullptr) { const uint8_t* data = nullptr; size_t data_size = 0; - if (ESP_OK == emote_get_asset_data_by_name(emote_display->GetEmoteHandle(), name.c_str(), &data, &data_size)) { + if (ESP_OK == emote_get_asset_data_by_name(emote_display->GetEmoteHandle(), name.c_str(), + &data, &data_size)) { ptr = const_cast(static_cast(data)); size = data_size; return true; @@ -409,7 +469,7 @@ bool Assets::EmoteStrategy::GetAssetData(Assets* assets, const std::string& name ESP_LOGE(TAG, "Failed to get asset data by name: %s", name.c_str()); return false; } - (void)assets; // Unused parameter + (void)assets; // Unused parameter return false; } @@ -425,7 +485,8 @@ bool Assets::EmoteStrategy::Apply(Assets* assets, bool refresh_display_theme) { return true; } -bool Assets::Download(std::string url, std::function progress_callback) { +bool Assets::Download(std::string url, + std::function progress_callback) { ESP_LOGI(TAG, "Downloading new version of assets from %s", url.c_str()); auto network = Board::GetInstance().GetNetwork(); @@ -449,25 +510,24 @@ bool Assets::Download(std::string url, std::function partition_->size) { - ESP_LOGE(TAG, "Assets file size (%u) is larger than partition size (%lu)", - content_length, partition_->size); + ESP_LOGE(TAG, "Assets file size (%u) is larger than partition size (%lu)", content_length, + partition_->size); return false; } constexpr size_t HEADER_SIZE = 12; if (content_length < HEADER_SIZE) { - ESP_LOGE(TAG, "Content length (%u) is smaller than header size (%u)", - content_length, HEADER_SIZE); + ESP_LOGE(TAG, "Content length (%u) is smaller than header size (%u)", content_length, + HEADER_SIZE); return false; } const size_t SECTOR_SIZE = esp_partition_get_main_flash_sector_size(); using BufferPtr = std::unique_ptr; - BufferPtr buffer( - static_cast(heap_caps_malloc(SECTOR_SIZE, MALLOC_CAP_INTERNAL)), - &heap_caps_free); + BufferPtr buffer(static_cast(heap_caps_malloc(SECTOR_SIZE, MALLOC_CAP_INTERNAL)), + &heap_caps_free); if (!buffer) { ESP_LOGE(TAG, "Failed to allocate buffer"); @@ -482,8 +542,7 @@ bool Assets::Download(std::string url, std::function partition_->size) { - ESP_LOGE(TAG, "Sector end (%u) exceeds partition size (%lu)", - sector_end, partition_->size); + ESP_LOGE(TAG, "Sector end (%u) exceeds partition size (%lu)", sector_end, + partition_->size); erase_failed = true; break; } - ESP_LOGD(TAG, "Erasing sector %u (offset: %u, size: %u)", - current_sector, sector_start, SECTOR_SIZE); + ESP_LOGD(TAG, "Erasing sector %u (offset: %u, size: %u)", current_sector, + sector_start, SECTOR_SIZE); esp_err_t err = esp_partition_erase_range(partition_, sector_start, SECTOR_SIZE); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to erase sector %u at offset %u: %s", - current_sector, sector_start, esp_err_to_name(err)); + ESP_LOGE(TAG, "Failed to erase sector %u at offset %u: %s", current_sector, + sector_start, esp_err_to_name(err)); erase_failed = true; break; } @@ -563,15 +622,13 @@ bool Assets::Download(std::string url, std::function= 1000000 || (header_collected + total_written) == content_length) { + if (esp_timer_get_time() - last_calc_time >= 1000000 || + (header_collected + total_written) == content_length) { size_t progress = (header_collected + total_written) * 100 / content_length; size_t speed = recent_written; - ESP_LOGI(TAG, "Progress: %u%% (%u/%u), Speed: %u B/s, Sectors erased: %u", - progress, - (unsigned int)(header_collected + total_written), - (unsigned int)content_length, - (unsigned int)speed, - (unsigned int)current_sector); + ESP_LOGI(TAG, "Progress: %u%% (%u/%u), Speed: %u B/s, Sectors erased: %u", progress, + (unsigned int)(header_collected + total_written), (unsigned int)content_length, + (unsigned int)speed, (unsigned int)current_sector); if (progress_callback) { progress_callback(progress, speed); @@ -602,7 +659,9 @@ bool Assets::Download(std::string url, std::function #include #include +#include -#include #include +#include #include #include #include @@ -20,6 +20,14 @@ struct Asset { size_t offset; }; +struct TextFontCapability { + bool glyph_push = false; + std::string bundle; + std::string charset; + int size = 0; + int bpp = 0; +}; + class Assets { public: static Assets& GetInstance() { @@ -28,12 +36,14 @@ public: } ~Assets(); - bool Download(std::string url, std::function progress_callback); + bool Download(std::string url, + std::function progress_callback); bool Apply(bool refresh_display_theme = true); bool GetAssetData(const std::string& name, void*& ptr, size_t& size); inline bool partition_valid() const { return partition_valid_; } inline std::string default_assets_url() const { return default_assets_url_; } + inline TextFontCapability text_font_capability() const { return text_font_capability_; } private: Assets(); @@ -44,22 +54,27 @@ private: void UnApplyPartition(); static bool FindPartition(Assets* assets); static bool LoadSrmodelsFromIndex(Assets* assets, cJSON* root = nullptr); - + void UseBuiltInTextFontCapability(); + void DisableTextFontGlyphPush(); + class AssetStrategy { public: virtual ~AssetStrategy() = default; virtual bool Apply(Assets* assets, bool refresh_display_theme = true) = 0; virtual bool InitializePartition(Assets* assets) = 0; virtual void UnApplyPartition(Assets* assets) = 0; - virtual bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) = 0; + virtual bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, + size_t& size) = 0; }; - + class LvglStrategy : public AssetStrategy { public: bool Apply(Assets* assets, bool refresh_display_theme = true) override; bool InitializePartition(Assets* assets) override; void UnApplyPartition(Assets* assets) override; - bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) override; + bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, + size_t& size) override; + private: static uint32_t CalculateChecksum(const char* data, uint32_t length); std::map assets_; @@ -67,15 +82,16 @@ private: const char* mmap_root_ = nullptr; bool checksum_valid_ = false; }; - + class EmoteStrategy : public AssetStrategy { public: bool Apply(Assets* assets, bool refresh_display_theme = true) override; bool InitializePartition(Assets* assets) override; void UnApplyPartition(Assets* assets) override; - bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, size_t& size) override; + bool GetAssetData(Assets* assets, const std::string& name, void*& ptr, + size_t& size) override; }; - + // Strategy instance std::unique_ptr strategy_; @@ -83,6 +99,7 @@ protected: const esp_partition_t* partition_ = nullptr; bool partition_valid_ = false; std::string default_assets_url_; + TextFontCapability text_font_capability_; srmodel_list_t* models_list_ = nullptr; }; diff --git a/main/boards/atk-dnesp32s3-box3/atk_dnesp32s3_box3.cc b/main/boards/atk-dnesp32s3-box3/atk_dnesp32s3_box3.cc index 7bb1064b..5fbe7532 100644 --- a/main/boards/atk-dnesp32s3-box3/atk_dnesp32s3_box3.cc +++ b/main/boards/atk-dnesp32s3-box3/atk_dnesp32s3_box3.cc @@ -30,8 +30,8 @@ #define TAG "atk_dnesp32s3_box3" -LV_FONT_DECLARE(font_puhui_20_4); -LV_FONT_DECLARE(font_awesome_20_4); +LV_FONT_DECLARE(font_noto_sans_basic_20_4); +LV_FONT_DECLARE(font_material_symbols_20_4); class atk_dnesp32s3_box3 : public WifiBoard { private: diff --git a/main/boards/atoms3-echo-base/atoms3_echo_base.cc b/main/boards/atoms3-echo-base/atoms3_echo_base.cc index 40e4c7c1..c061477d 100644 --- a/main/boards/atoms3-echo-base/atoms3_echo_base.cc +++ b/main/boards/atoms3-echo-base/atoms3_echo_base.cc @@ -132,7 +132,7 @@ private: display_->SetupUI(); display_->SetStatus(Lang::Strings::ERROR); - display_->SetEmotion("triangle_exclamation"); + display_->SetEmotion("warning"); display_->SetChatMessage("system", "Echo Base\nnot connected"); while (1) { diff --git a/main/boards/atoms3r-echo-base/atoms3r_echo_base.cc b/main/boards/atoms3r-echo-base/atoms3r_echo_base.cc index e29d102e..c1b5c721 100644 --- a/main/boards/atoms3r-echo-base/atoms3r_echo_base.cc +++ b/main/boards/atoms3r-echo-base/atoms3r_echo_base.cc @@ -199,7 +199,7 @@ private: display_->SetupUI(); display_->SetStatus(Lang::Strings::ERROR); - display_->SetEmotion("triangle_exclamation"); + display_->SetEmotion("warning"); display_->SetChatMessage("system", "Echo Base\nnot connected"); while (1) { diff --git a/main/boards/atoms3r-echo-pyramid/atoms3r_echo_pyramid.cc b/main/boards/atoms3r-echo-pyramid/atoms3r_echo_pyramid.cc index 80f691ba..3c0b29c2 100644 --- a/main/boards/atoms3r-echo-pyramid/atoms3r_echo_pyramid.cc +++ b/main/boards/atoms3r-echo-pyramid/atoms3r_echo_pyramid.cc @@ -642,7 +642,7 @@ private: display_->SetupUI(); display_->SetStatus(Lang::Strings::ERROR); - display_->SetEmotion("triangle_exclamation"); + display_->SetEmotion("warning"); display_->SetChatMessage("system", "Echo Pyramid\nnot connected"); while (1) { diff --git a/main/boards/common/ethernet_board.cc b/main/boards/common/ethernet_board.cc index 6427e196..98cf9f60 100644 --- a/main/boards/common/ethernet_board.cc +++ b/main/boards/common/ethernet_board.cc @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include #include @@ -221,7 +221,7 @@ NetworkInterface* EthernetBoard::GetNetwork() { } const char* EthernetBoard::GetNetworkStateIcon() { - return connected_ ? FONT_AWESOME_SIGNAL_STRONG : FONT_AWESOME_SIGNAL_OFF; + return connected_ ? MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR : MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR_OFF; } std::string EthernetBoard::GetEthernetMacAddress() { diff --git a/main/boards/common/ml307_board.cc b/main/boards/common/ml307_board.cc index d204bd7e..9a4b7ec1 100644 --- a/main/boards/common/ml307_board.cc +++ b/main/boards/common/ml307_board.cc @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include static const char *TAG = "Ml307Board"; @@ -146,23 +146,23 @@ NetworkInterface* Ml307Board::GetNetwork() { const char* Ml307Board::GetNetworkStateIcon() { if (modem_ == nullptr || !modem_->network_ready()) { - return FONT_AWESOME_SIGNAL_OFF; + return MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR_OFF; } int csq = modem_->GetCsq(); if (csq == -1) { - return FONT_AWESOME_SIGNAL_OFF; + return MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR_OFF; } else if (csq >= 0 && csq <= 9) { - return FONT_AWESOME_SIGNAL_WEAK; + return MATERIAL_SYMBOLS_SIGNAL_CELLULAR_ALT_1_BAR; } else if (csq >= 10 && csq <= 14) { - return FONT_AWESOME_SIGNAL_FAIR; + return MATERIAL_SYMBOLS_SIGNAL_CELLULAR_ALT_2_BAR; } else if (csq >= 15 && csq <= 19) { - return FONT_AWESOME_SIGNAL_GOOD; + return MATERIAL_SYMBOLS_SIGNAL_CELLULAR_ALT; } else if (csq >= 20 && csq <= 31) { - return FONT_AWESOME_SIGNAL_STRONG; + return MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR; } ESP_LOGW(TAG, "Invalid CSQ: %d", csq); - return FONT_AWESOME_SIGNAL_OFF; + return MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR_OFF; } std::string Ml307Board::GetBoardJson() { diff --git a/main/boards/common/nt26_board.cc b/main/boards/common/nt26_board.cc index 9279869b..3a62fee8 100644 --- a/main/boards/common/nt26_board.cc +++ b/main/boards/common/nt26_board.cc @@ -3,7 +3,7 @@ #include "application.h" #include "audio_codec.h" #include -#include +#include #include #define TAG "Nt26Board" @@ -146,21 +146,21 @@ NetworkInterface* Nt26Board::GetNetwork() { const char* Nt26Board::GetNetworkStateIcon() { if (modem_ == nullptr || !modem_->IsInitialized()) { - return FONT_AWESOME_SIGNAL_OFF; + return MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR_OFF; } int csq = modem_->GetSignalStrength(); if (csq == 99 || csq == -1) { - return FONT_AWESOME_SIGNAL_OFF; + return MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR_OFF; } else if (csq >= 0 && csq <= 9) { - return FONT_AWESOME_SIGNAL_WEAK; + return MATERIAL_SYMBOLS_SIGNAL_CELLULAR_ALT_1_BAR; } else if (csq >= 10 && csq <= 14) { - return FONT_AWESOME_SIGNAL_FAIR; + return MATERIAL_SYMBOLS_SIGNAL_CELLULAR_ALT_2_BAR; } else if (csq >= 15 && csq <= 19) { - return FONT_AWESOME_SIGNAL_GOOD; + return MATERIAL_SYMBOLS_SIGNAL_CELLULAR_ALT; } else if (csq >= 20 && csq <= 31) { - return FONT_AWESOME_SIGNAL_STRONG; + return MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR; } - return FONT_AWESOME_SIGNAL_OFF; + return MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR_OFF; } void Nt26Board::SetPowerSaveLevel(PowerSaveLevel level) { diff --git a/main/boards/common/rndis_board.cc b/main/boards/common/rndis_board.cc index c9c5219d..64af8d5e 100644 --- a/main/boards/common/rndis_board.cc +++ b/main/boards/common/rndis_board.cc @@ -10,7 +10,7 @@ #include #include #include -#include +#include #if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 @@ -176,7 +176,7 @@ NetworkInterface* RndisBoard::GetNetwork() { } const char* RndisBoard::GetNetworkStateIcon() { - return FONT_AWESOME_SIGNAL_STRONG; + return MATERIAL_SYMBOLS_ANDROID_CELL_4_BAR; } std::string RndisBoard::GetBoardJson() { @@ -244,4 +244,4 @@ std::string RndisBoard::GetDeviceStatusJson() { cJSON_Delete(root); return result; } -#endif // CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 \ No newline at end of file +#endif // CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 diff --git a/main/boards/common/wifi_board.cc b/main/boards/common/wifi_board.cc index 83e4ced9..2883221e 100644 --- a/main/boards/common/wifi_board.cc +++ b/main/boards/common/wifi_board.cc @@ -13,7 +13,7 @@ #include #include -#include +#include #include #include #include @@ -259,19 +259,19 @@ const char* WifiBoard::GetNetworkStateIcon() { auto& wifi = WifiManager::GetInstance(); if (wifi.IsConfigMode()) { - return FONT_AWESOME_WIFI; + return MATERIAL_SYMBOLS_WIFI; } if (!wifi.IsConnected()) { - return FONT_AWESOME_WIFI_SLASH; + return MATERIAL_SYMBOLS_WIFI_OFF; } int rssi = wifi.GetRssi(); if (rssi >= -65) { - return FONT_AWESOME_WIFI; + return MATERIAL_SYMBOLS_WIFI; } else if (rssi >= -75) { - return FONT_AWESOME_WIFI_FAIR; + return MATERIAL_SYMBOLS_WIFI_2_BAR; } - return FONT_AWESOME_WIFI_WEAK; + return MATERIAL_SYMBOLS_WIFI_1_BAR; } std::string WifiBoard::GetBoardJson() { diff --git a/main/boards/esp-box-3/README.md b/main/boards/esp-box-3/README.md index 103f7323..8f38abaf 100644 --- a/main/boards/esp-box-3/README.md +++ b/main/boards/esp-box-3/README.md @@ -58,6 +58,9 @@ ESP-BOX-3 支持多种不同的 UI 显示风格,通过 menuconfig 配置选择 https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-esp-box-3.bin ``` + 这是保留表情动画和模型的历史资源包;新版固件会忽略其中没有 Noto 元数据的旧 Puhui + 字体,并使用内置 Noto basic 字体。待新的 Noto 资源包上传后可直接替换此 URL。 + ##### 默认消息风格 (Enable default message style) - **配置选项**: `USE_DEFAULT_MESSAGE_STYLE` (默认) - **特点**: 使用标准的消息显示界面 diff --git a/main/boards/esp-vocat/README.md b/main/boards/esp-vocat/README.md index fd3613c3..614faf26 100644 --- a/main/boards/esp-vocat/README.md +++ b/main/boards/esp-vocat/README.md @@ -49,6 +49,9 @@ ESP-VoCat 支持多种不同的 UI 显示风格,通过 menuconfig 配置选择 https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-echoear.bin ``` + 这是保留表情动画和模型的历史资源包;新版固件会忽略其中没有 Noto 元数据的旧 Puhui + 字体,并使用内置 Noto basic 字体。待新的 Noto 资源包上传后可直接替换此 URL。 + ##### 默认消息风格 (Enable default message style) - **配置选项**: `USE_DEFAULT_MESSAGE_STYLE` (默认) - **特点**: 使用标准的消息显示界面 @@ -79,4 +82,4 @@ idf.py build ```bash idf.py flash -``` \ No newline at end of file +``` diff --git a/main/boards/m5stack-tab5/m5stack_tab5.cc b/main/boards/m5stack-tab5/m5stack_tab5.cc index dfe2ff87..291c82ea 100644 --- a/main/boards/m5stack-tab5/m5stack_tab5.cc +++ b/main/boards/m5stack-tab5/m5stack_tab5.cc @@ -3,7 +3,6 @@ #include "display/lcd_display.h" #include "esp_lcd_ili9881c.h" #include "esp_lcd_st7123.h" -#include "font_emoji.h" #include "application.h" #include "button.h" #include "config.h" diff --git a/main/boards/quandong-s3-dev/HARDWARE_DIFF.md b/main/boards/quandong-s3-dev/HARDWARE_DIFF.md index 43d9a44e..c1d17a06 100644 --- a/main/boards/quandong-s3-dev/HARDWARE_DIFF.md +++ b/main/boards/quandong-s3-dev/HARDWARE_DIFF.md @@ -16,7 +16,7 @@ | 板载 LED | 无 | GPIO48 单色 | | MCP 外设示例 | 无 | `LampController`(GPIO18) | | 背光 | PWM on GPIO45 | OLED 无背光 | -| 字体 / emoji 资源 | `font_noto_basic_20_4` + `font_awesome_20_4` + `noto-emoji_128` | `font_puhui_basic_14_1` + `font_awesome_14_1`(无 emoji) | +| 字体 / emoji 资源 | `font_noto_sans_basic_20_4` + `font_material_symbols_20_4` + `noto-color-emoji_64` | `font_noto_sans_basic_14_1` + `font_material_symbols_14_1`(无 emoji) | --- @@ -144,8 +144,8 @@ InitializeTools() // 注册 LampController(MCP 外设) | 板子 | text font | icon font | emoji collection | |---|---|---|---| -| quandong-s3-dev | `font_noto_basic_20_4` | `font_awesome_20_4` | `noto-emoji_128` | -| bread-compact-wifi | `font_puhui_basic_14_1` | `font_awesome_14_1` | (无) | +| quandong-s3-dev | `font_noto_sans_basic_20_4` | `font_material_symbols_20_4` | `noto-color-emoji_64` | +| bread-compact-wifi | `font_noto_sans_basic_14_1` | `font_material_symbols_14_1` | (无) | → quandong 走的是和 `lichuang-dev`、`esp-box-3` 等 240×320 LCD 板同档资源;OLED 板因为像素少,用 14 像素位图字体即可,emoji 也加载不动。 diff --git a/main/boards/sp-esp32-s3-1.28-box/sp-esp32-s3-1.28-box.cc b/main/boards/sp-esp32-s3-1.28-box/sp-esp32-s3-1.28-box.cc index 447aa26c..bb1923e1 100644 --- a/main/boards/sp-esp32-s3-1.28-box/sp-esp32-s3-1.28-box.cc +++ b/main/boards/sp-esp32-s3-1.28-box/sp-esp32-s3-1.28-box.cc @@ -29,8 +29,8 @@ #define TAG "Spotpear_ESP32_S3_1_28_BOX" -LV_FONT_DECLARE(font_puhui_16_4); -LV_FONT_DECLARE(font_awesome_16_4); +LV_FONT_DECLARE(font_noto_sans_basic_16_4); +LV_FONT_DECLARE(font_material_symbols_16_4); class Cst816d : public I2cDevice { diff --git a/main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.cc b/main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.cc index 2b9570c0..929e6f56 100644 --- a/main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.cc +++ b/main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.cc @@ -2,11 +2,12 @@ #include #include #include -#include +#include #include #include #include #include +#include #include "application.h" #include "assets/lang_config.h" #include "board.h" @@ -15,12 +16,9 @@ #include "hub75.h" // 声明中文字体 -LV_FONT_DECLARE(font_puhui_14_1); +LV_FONT_DECLARE(font_noto_sans_basic_14_1); LV_FONT_DECLARE(BUILTIN_ICON_FONT); -LV_FONT_DECLARE(font_awesome_30_4); - -// 声明32x32 Emoji集合 -class Twemoji32; +LV_FONT_DECLARE(font_material_symbols_30_4); struct Hub75Context { Hub75Driver driver; @@ -41,7 +39,7 @@ bool UseBuiltInEmotionIcon(const char* emotion) { if (emotion == nullptr) { return false; } - if (strcmp(emotion, "microchip_ai") == 0) { + if (strcmp(emotion, "robot_2") == 0) { return true; } if (strcmp(emotion, "link") == 0) { @@ -270,9 +268,6 @@ void CustomMatrixDisplay::SetupUI() { } Display::SetupUI(); - // 初始化 Emoji 资源(用于 SetEmotion) - emoji_collection_ = std::make_shared(); - const int ui_width_px = width_; const int ui_height_px = height_; @@ -301,7 +296,7 @@ void CustomMatrixDisplay::SetupUI() { lv_label_set_long_mode(status_label_, LV_LABEL_LONG_CLIP); lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_RIGHT, 0); lv_obj_set_style_text_color(status_label_, lv_color_white(), 0); - lv_obj_set_style_text_font(status_label_, &font_puhui_14_1, 0); + lv_obj_set_style_text_font(status_label_, &font_noto_sans_basic_14_1, 0); lv_obj_align(status_label_, LV_ALIGN_TOP_RIGHT, 0, -1); status_text_ = "初始化"; RefreshStatusLabelLocked(); @@ -312,7 +307,7 @@ void CustomMatrixDisplay::SetupUI() { lv_label_set_long_mode(message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); lv_obj_set_style_text_align(message_label_, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_color(message_label_, lv_color_white(), 0); - lv_obj_set_style_text_font(message_label_, &font_puhui_14_1, 0); + lv_obj_set_style_text_font(message_label_, &font_noto_sans_basic_14_1, 0); lv_obj_align(message_label_, LV_ALIGN_BOTTOM_MID, 0, 0); lv_label_set_text(message_label_, "hi 小智"); @@ -322,13 +317,18 @@ void CustomMatrixDisplay::SetupUI() { lv_obj_add_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN); emotion_icon_label_ = lv_label_create(main_container_); - lv_label_set_text(emotion_icon_label_, FONT_AWESOME_MICROCHIP_AI); - lv_obj_set_style_text_font(emotion_icon_label_, &font_awesome_30_4, 0); + lv_label_set_text(emotion_icon_label_, MATERIAL_SYMBOLS_ROBOT_2); + lv_obj_set_style_text_font(emotion_icon_label_, &font_material_symbols_30_4, 0); lv_obj_set_style_text_color(emotion_icon_label_, lv_color_white(), 0); lv_obj_align(emotion_icon_label_, LV_ALIGN_CENTER, 0, -1); lv_obj_remove_flag(emotion_icon_label_, LV_OBJ_FLAG_HIDDEN); } +void CustomMatrixDisplay::SetEmojiCollection(std::shared_ptr collection) { + DisplayLockGuard lock(this); + emoji_collection_ = std::move(collection); +} + void CustomMatrixDisplay::SetEmotion(const char* emotion) { DisplayLockGuard lock(this); @@ -358,7 +358,7 @@ void CustomMatrixDisplay::SetEmotion(const char* emotion) { if (emotion_icon_label_ == nullptr) { return; } - lv_label_set_text(emotion_icon_label_, FONT_AWESOME_MICROCHIP_AI); + lv_label_set_text(emotion_icon_label_, MATERIAL_SYMBOLS_ROBOT_2); SetObjectVisible(emotion_icon_label_, true); } diff --git a/main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.h b/main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.h index 150fc6ce..ed5f2ab7 100644 --- a/main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.h +++ b/main/boards/waveshare/esp32-s3-rgb-matrix/rgb_matrix_display.h @@ -16,6 +16,7 @@ public: void SetBrightness(uint8_t brightness_0_100); virtual void SetEmotion(const char* emotion) override; + virtual void SetEmojiCollection(std::shared_ptr collection) override; virtual void SetChatMessage(const char* role, const char* content) override; virtual void SetStatus(const char* status) override; virtual void ShowNotification(const char* notification, int duration_ms = 3000) override; diff --git a/main/boards/waveshare/esp32-s3-touch-lcd-7c/esp32_s3_touch_lcd_7c.cc b/main/boards/waveshare/esp32-s3-touch-lcd-7c/esp32_s3_touch_lcd_7c.cc index 3886fa72..0d1452ff 100644 --- a/main/boards/waveshare/esp32-s3-touch-lcd-7c/esp32_s3_touch_lcd_7c.cc +++ b/main/boards/waveshare/esp32-s3-touch-lcd-7c/esp32_s3_touch_lcd_7c.cc @@ -1,6 +1,5 @@ #include "wifi_board.h" #include "display/lcd_display.h" -// #include "font_awesome_symbols.h" #include "codecs/box_audio_codec.h" #include "waveshare_audio_codec.h" diff --git a/main/boards/xingzhi-abs-2.0/customlcddisplay.h b/main/boards/xingzhi-abs-2.0/customlcddisplay.h index b0422dc0..8711f9f2 100644 --- a/main/boards/xingzhi-abs-2.0/customlcddisplay.h +++ b/main/boards/xingzhi-abs-2.0/customlcddisplay.h @@ -4,8 +4,8 @@ #include "display/lcd_display.h" #include "lvgl_theme.h" -LV_FONT_DECLARE(font_puhui_16_4); -LV_FONT_DECLARE(font_awesome_16_4); +LV_FONT_DECLARE(font_noto_sans_basic_16_4); +LV_FONT_DECLARE(font_material_symbols_16_4); class CustomLcdDisplay : public SpiLcdDisplay { private: @@ -91,4 +91,4 @@ public: } }; -#endif \ No newline at end of file +#endif diff --git a/main/boards/zhengchen-cam-ml307/zhengchen_cam_board_ml307.cc b/main/boards/zhengchen-cam-ml307/zhengchen_cam_board_ml307.cc index 9d6f0cc6..2c2250ea 100644 --- a/main/boards/zhengchen-cam-ml307/zhengchen_cam_board_ml307.cc +++ b/main/boards/zhengchen-cam-ml307/zhengchen_cam_board_ml307.cc @@ -28,8 +28,8 @@ //控制器初始化函数声明 void InitializeMCPController(); -LV_FONT_DECLARE(font_puhui_20_4); -LV_FONT_DECLARE(font_awesome_20_4); +LV_FONT_DECLARE(font_noto_sans_basic_20_4); +LV_FONT_DECLARE(font_material_symbols_20_4); class Pca9557 : public I2cDevice { public: diff --git a/main/boards/zhengchen-cam/zhengchen_cam_board.cc b/main/boards/zhengchen-cam/zhengchen_cam_board.cc index c7bc9eff..8cc28ec1 100644 --- a/main/boards/zhengchen-cam/zhengchen_cam_board.cc +++ b/main/boards/zhengchen-cam/zhengchen_cam_board.cc @@ -28,8 +28,8 @@ //控制器初始化函数声明 void InitializeMCPController(); -LV_FONT_DECLARE(font_puhui_20_4); -LV_FONT_DECLARE(font_awesome_20_4); +LV_FONT_DECLARE(font_noto_sans_basic_20_4); +LV_FONT_DECLARE(font_material_symbols_20_4); class Pca9557 : public I2cDevice { public: diff --git a/main/display/display.cc b/main/display/display.cc index 50fc1c52..16549611 100644 --- a/main/display/display.cc +++ b/main/display/display.cc @@ -1,30 +1,24 @@ -#include +#include "display.h" #include -#include +#include #include #include -#include - -#include "display.h" -#include "board.h" +#include #include "application.h" -#include "audio_codec.h" -#include "settings.h" #include "assets/lang_config.h" +#include "audio_codec.h" +#include "board.h" +#include "settings.h" #define TAG "Display" -Display::Display() { -} +Display::Display() {} -Display::~Display() { -} +Display::~Display() {} -void Display::SetStatus(const char* status) { - ESP_LOGW(TAG, "SetStatus: %s", status); -} +void Display::SetStatus(const char* status) { ESP_LOGW(TAG, "SetStatus: %s", status); } -void Display::ShowNotification(const std::string ¬ification, int duration_ms) { +void Display::ShowNotification(const std::string& notification, int duration_ms) { ShowNotification(notification.c_str(), duration_ms); } @@ -32,13 +26,9 @@ void Display::ShowNotification(const char* notification, int duration_ms) { ESP_LOGW(TAG, "ShowNotification: %s", notification); } -void Display::UpdateStatusBar(bool update_all) { -} +void Display::UpdateStatusBar(bool update_all) {} - -void Display::SetEmotion(const char* emotion) { - ESP_LOGW(TAG, "SetEmotion: %s", emotion); -} +void Display::SetEmotion(const char* emotion) { ESP_LOGW(TAG, "SetEmotion: %s", emotion); } void Display::SetChatMessage(const char* role, const char* content) { ESP_LOGW(TAG, "Role:%s", role); @@ -55,6 +45,4 @@ void Display::SetTheme(Theme* theme) { settings.SetString("theme", theme->name()); } -void Display::SetPowerSaveMode(bool on) { - ESP_LOGW(TAG, "SetPowerSaveMode: %d", on); -} +void Display::SetPowerSaveMode(bool on) { ESP_LOGW(TAG, "SetPowerSaveMode: %d", on); } diff --git a/main/display/display.h b/main/display/display.h index 9e73014e..63b7affe 100644 --- a/main/display/display.h +++ b/main/display/display.h @@ -2,18 +2,21 @@ #define DISPLAY_H #include "emoji_collection.h" +#include "text_glyph.h" #ifndef CONFIG_USE_EMOTE_MESSAGE_STYLE #define HAVE_LVGL 1 #include #endif -#include #include #include +#include -#include #include +#include +#include +#include class Theme { public: @@ -21,6 +24,7 @@ public: virtual ~Theme() = default; inline std::string name() const { return name_; } + private: std::string name_; }; @@ -32,7 +36,7 @@ public: virtual void SetStatus(const char* status); virtual void ShowNotification(const char* notification, int duration_ms = 3000); - virtual void ShowNotification(const std::string ¬ification, int duration_ms = 3000); + virtual void ShowNotification(const std::string& notification, int duration_ms = 3000); virtual void SetEmotion(const char* emotion); virtual void SetChatMessage(const char* role, const char* content); virtual void ClearChatMessages(); @@ -40,9 +44,10 @@ public: virtual Theme* GetTheme() { return current_theme_; } virtual void UpdateStatusBar(bool update_all = false); virtual void SetPowerSaveMode(bool on); - virtual void SetupUI() { - setup_ui_called_ = true; - } + virtual bool AddTextGlyphs(const std::vector& glyphs, uint8_t bpp) { return false; } + virtual void ClearTextGlyphs() {} + virtual void SetEmojiCollection(std::shared_ptr) {} + virtual void SetupUI() { setup_ui_called_ = true; } inline int width() const { return width_; } inline int height() const { return height_; } @@ -60,27 +65,22 @@ protected: virtual void Unlock() = 0; }; - class DisplayLockGuard { public: - DisplayLockGuard(Display *display) : display_(display) { + DisplayLockGuard(Display* display) : display_(display) { if (!display_->Lock(30000)) { ESP_LOGE("Display", "Failed to lock display"); } } - ~DisplayLockGuard() { - display_->Unlock(); - } + ~DisplayLockGuard() { display_->Unlock(); } private: - Display *display_; + Display* display_; }; class NoDisplay : public Display { private: - virtual bool Lock(int timeout_ms = 0) override { - return true; - } + virtual bool Lock(int timeout_ms = 0) override { return true; } virtual void Unlock() override {} }; diff --git a/main/display/lcd_display.cc b/main/display/lcd_display.cc index be236bb8..9fdf0b9f 100644 --- a/main/display/lcd_display.cc +++ b/main/display/lcd_display.cc @@ -1,18 +1,19 @@ #include "lcd_display.h" -#include "gif/lvgl_gif.h" -#include "settings.h" -#include "lvgl_theme.h" #include "assets/lang_config.h" +#include "gif/lvgl_gif.h" +#include "lvgl_theme.h" +#include "settings.h" -#include -#include -#include -#include #include +#include #include #include -#include +#include +#include #include +#include +#include +#include #include "board.h" @@ -20,12 +21,14 @@ LV_FONT_DECLARE(BUILTIN_TEXT_FONT); LV_FONT_DECLARE(BUILTIN_ICON_FONT); -LV_FONT_DECLARE(font_awesome_30_4); +LV_FONT_DECLARE(font_material_symbols_30_4); +LV_FONT_DECLARE(font_noto_emoji_30_4); void LcdDisplay::InitializeLcdThemes() { auto text_font = std::make_shared(&BUILTIN_TEXT_FONT); auto icon_font = std::make_shared(&BUILTIN_ICON_FONT); - auto large_icon_font = std::make_shared(&font_awesome_30_4); + auto large_icon_font = std::make_shared(&font_material_symbols_30_4); + auto emoji_font = std::make_shared(&font_noto_emoji_30_4); // light theme auto light_theme = new LvglTheme("light"); @@ -41,6 +44,7 @@ void LcdDisplay::InitializeLcdThemes() { light_theme->set_text_font(text_font); light_theme->set_icon_font(icon_font); light_theme->set_large_icon_font(large_icon_font); + light_theme->set_emoji_font(emoji_font); // dark theme auto dark_theme = new LvglTheme("dark"); @@ -56,13 +60,15 @@ void LcdDisplay::InitializeLcdThemes() { dark_theme->set_text_font(text_font); dark_theme->set_icon_font(icon_font); dark_theme->set_large_icon_font(large_icon_font); + dark_theme->set_emoji_font(emoji_font); auto& theme_manager = LvglThemeManager::GetInstance(); theme_manager.RegisterTheme("light", light_theme); theme_manager.RegisterTheme("dark", dark_theme); } -LcdDisplay::LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height) +LcdDisplay::LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, + int height) : panel_io_(panel_io), panel_(panel) { width_ = width; height_ = height; @@ -77,10 +83,11 @@ LcdDisplay::LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_ // Create a timer to hide the preview image esp_timer_create_args_t preview_timer_args = { - .callback = [](void* arg) { - LcdDisplay* display = static_cast(arg); - display->SetPreviewImage(nullptr); - }, + .callback = + [](void* arg) { + LcdDisplay* display = static_cast(arg); + display->SetPreviewImage(nullptr); + }, .arg = this, .dispatch_method = ESP_TIMER_TASK, .name = "preview_timer", @@ -90,9 +97,9 @@ LcdDisplay::LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_ } SpiLcdDisplay::SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, - int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy) + int width, int height, int offset_x, int offset_y, bool mirror_x, + bool mirror_y, bool swap_xy) : LcdDisplay(panel_io, panel, width, height) { - // draw white std::vector buffer(width_, 0xFFFF); for (int y = 0; y < height_; y++) { @@ -144,20 +151,22 @@ SpiLcdDisplay::SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_h .hres = static_cast(width_), .vres = static_cast(height_), .monochrome = false, - .rotation = { - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y, - }, + .rotation = + { + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + }, .color_format = LV_COLOR_FORMAT_RGB565, - .flags = { - .buff_dma = 1, - .buff_spiram = 0, - .sw_rotate = 0, - .swap_bytes = 1, - .full_refresh = 0, - .direct_mode = 0, - }, + .flags = + { + .buff_dma = 1, + .buff_spiram = 0, + .sw_rotate = 0, + .swap_bytes = 1, + .full_refresh = 0, + .direct_mode = 0, + }, }; display_ = lvgl_port_add_disp(&display_cfg); @@ -171,13 +180,11 @@ SpiLcdDisplay::SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_h } } - // RGB LCD implementation RgbLcdDisplay::RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, - int width, int height, int offset_x, int offset_y, - bool mirror_x, bool mirror_y, bool swap_xy) + int width, int height, int offset_x, int offset_y, bool mirror_x, + bool mirror_y, bool swap_xy) : LcdDisplay(panel_io, panel, width, height) { - // draw white std::vector buffer(width_, 0xFFFF); for (int y = 0; y < height_; y++) { @@ -201,42 +208,41 @@ RgbLcdDisplay::RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_h .double_buffer = true, .hres = static_cast(width_), .vres = static_cast(height_), - .rotation = { - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y, - }, - .flags = { - .buff_dma = 1, - .swap_bytes = 0, - .full_refresh = 1, - .direct_mode = 1, - }, + .rotation = + { + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + }, + .flags = + { + .buff_dma = 1, + .swap_bytes = 0, + .full_refresh = 1, + .direct_mode = 1, + }, }; - const lvgl_port_display_rgb_cfg_t rgb_cfg = { - .flags = { - .bb_mode = true, - .avoid_tearing = true, - } - }; - + const lvgl_port_display_rgb_cfg_t rgb_cfg = {.flags = { + .bb_mode = true, + .avoid_tearing = true, + }}; + display_ = lvgl_port_add_disp_rgb(&display_cfg, &rgb_cfg); if (display_ == nullptr) { ESP_LOGE(TAG, "Failed to add RGB display"); return; } - + if (offset_x != 0 || offset_y != 0) { lv_display_set_offset(display_, offset_x, offset_y); } } MipiLcdDisplay::MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, - int width, int height, int offset_x, int offset_y, - bool mirror_x, bool mirror_y, bool swap_xy) + int width, int height, int offset_x, int offset_y, bool mirror_x, + bool mirror_y, bool swap_xy) : LcdDisplay(panel_io, panel, width, height) { - ESP_LOGI(TAG, "Initialize LVGL library"); lv_init(); @@ -255,23 +261,23 @@ MipiLcdDisplay::MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel .vres = static_cast(height_), .monochrome = false, /* Rotation values must be same as used in esp_lcd for initial settings of the screen */ - .rotation = { - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y, - }, - .flags = { - .buff_dma = true, - .buff_spiram =false, - .sw_rotate = true, - }, + .rotation = + { + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + }, + .flags = + { + .buff_dma = true, + .buff_spiram = false, + .sw_rotate = true, + }, }; - const lvgl_port_display_dsi_cfg_t dpi_cfg = { - .flags = { - .avoid_tearing = false, - } - }; + const lvgl_port_display_dsi_cfg_t dpi_cfg = {.flags = { + .avoid_tearing = false, + }}; display_ = lvgl_port_add_disp_dsi(&disp_cfg, &dpi_cfg); if (display_ == nullptr) { ESP_LOGE(TAG, "Failed to add display"); @@ -285,13 +291,13 @@ MipiLcdDisplay::MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel LcdDisplay::~LcdDisplay() { SetPreviewImage(nullptr); - + // Clean up GIF controller if (gif_controller_) { gif_controller_->Stop(); gif_controller_.reset(); } - + if (preview_timer_ != nullptr) { esp_timer_stop(preview_timer_); esp_timer_delete(preview_timer_); @@ -342,13 +348,9 @@ LcdDisplay::~LcdDisplay() { } } -bool LcdDisplay::Lock(int timeout_ms) { - return lvgl_port_lock(timeout_ms); -} +bool LcdDisplay::Lock(int timeout_ms) { return lvgl_port_lock(timeout_ms); } -void LcdDisplay::Unlock() { - lvgl_port_unlock(); -} +void LcdDisplay::Unlock() { lvgl_port_unlock(); } #if CONFIG_USE_WECHAT_MESSAGE_STYLE void LcdDisplay::SetupUI() { @@ -357,7 +359,7 @@ void LcdDisplay::SetupUI() { ESP_LOGW(TAG, "SetupUI() called multiple times, skipping duplicate call"); return; } - + Display::SetupUI(); // Mark SetupUI as called DisplayLockGuard lock(this); @@ -395,7 +397,8 @@ void LcdDisplay::SetupUI() { lv_obj_set_style_pad_left(top_bar_, lvgl_theme->spacing(4), 0); lv_obj_set_style_pad_right(top_bar_, lvgl_theme->spacing(4), 0); lv_obj_set_flex_flow(top_bar_, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); lv_obj_set_scrollbar_mode(top_bar_, LV_SCROLLBAR_MODE_OFF); // Left icon @@ -411,7 +414,8 @@ void LcdDisplay::SetupUI() { lv_obj_set_style_border_width(right_icons, 0, 0); lv_obj_set_style_pad_all(right_icons, 0, 0); lv_obj_set_flex_flow(right_icons, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); mute_label_ = lv_label_create(right_icons); lv_label_set_text(mute_label_, ""); @@ -435,7 +439,7 @@ void LcdDisplay::SetupUI() { lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0); lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF); lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0); // Use absolute positioning - lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_ + lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_ notification_label_ = lv_label_create(status_bar_); lv_obj_set_width(notification_label_, LV_HOR_RES * 0.8); @@ -452,7 +456,7 @@ void LcdDisplay::SetupUI() { lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0); lv_label_set_text(status_label_, Lang::Strings::INITIALIZING); lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0); - + /* Content - Chat area */ content_ = lv_obj_create(container_); lv_obj_set_style_radius(content_, 0, 0); @@ -460,16 +464,17 @@ void LcdDisplay::SetupUI() { lv_obj_set_flex_grow(content_, 1); lv_obj_set_style_pad_all(content_, lvgl_theme->spacing(4), 0); lv_obj_set_style_border_width(content_, 0, 0); - lv_obj_set_style_bg_color(content_, lvgl_theme->chat_background_color(), 0); // Background for chat area + lv_obj_set_style_bg_color(content_, lvgl_theme->chat_background_color(), + 0); // Background for chat area // Enable scrolling for chat content lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF); lv_obj_set_scroll_dir(content_, LV_DIR_VER); - + // Create a flex container for chat messages lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); - lv_obj_set_style_pad_row(content_, lvgl_theme->spacing(4), 0); // Space between messages + lv_obj_set_style_pad_row(content_, lvgl_theme->spacing(4), 0); // Space between messages // We'll create chat messages dynamically in SetChatMessage chat_message_label_ = nullptr; @@ -487,32 +492,37 @@ void LcdDisplay::SetupUI() { lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); emoji_image_ = lv_img_create(screen); - lv_obj_align(emoji_image_, LV_ALIGN_TOP_MID, 0, text_font->line_height + lvgl_theme->spacing(8)); + lv_obj_align(emoji_image_, LV_ALIGN_TOP_MID, 0, + text_font->line_height + lvgl_theme->spacing(8)); // Display AI logo while booting emoji_label_ = lv_label_create(screen); lv_obj_center(emoji_label_); lv_obj_set_style_text_font(emoji_label_, large_icon_font, 0); lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0); - lv_label_set_text(emoji_label_, FONT_AWESOME_MICROCHIP_AI); + lv_label_set_text(emoji_label_, MATERIAL_SYMBOLS_ROBOT_2); } #if CONFIG_IDF_TARGET_ESP32P4 -#define MAX_MESSAGES 40 +#define MAX_MESSAGES 40 #else -#define MAX_MESSAGES 20 +#define MAX_MESSAGES 20 #endif void LcdDisplay::SetChatMessage(const char* role, const char* content) { if (!setup_ui_called_) { - ESP_LOGW(TAG, "SetChatMessage('%s', '%s') called before SetupUI() - message will be lost!", role, content); + ESP_LOGW(TAG, "SetChatMessage('%s', '%s') called before SetupUI() - message will be lost!", + role, content); } DisplayLockGuard lock(this); if (content_ == nullptr) { if (setup_ui_called_) { - ESP_LOGW(TAG, "SetChatMessage('%s', '%s') failed: content_ is nullptr (SetupUI() was called but container not created)", role, content); + ESP_LOGW(TAG, + "SetChatMessage('%s', '%s') failed: content_ is nullptr (SetupUI() was called " + "but container not created)", + role, content); } return; } - + // Check if message count exceeds limit uint32_t child_count = lv_obj_get_child_cnt(content_); if (child_count >= MAX_MESSAGES) { @@ -531,21 +541,24 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { } } } - - // Collapse system messages (if it's a system message, check if the last message is also a system message) + + // Collapse system messages (if it's a system message, check if the last message is also a + // system message) if (strcmp(role, "system") == 0) { // Refresh child count to get accurate count after potential deletion above child_count = lv_obj_get_child_cnt(content_); if (child_count > 0) { // Get the last message container lv_obj_t* last_container = lv_obj_get_child(content_, child_count - 1); - if (last_container != nullptr && lv_obj_is_valid(last_container) && lv_obj_get_child_cnt(last_container) > 0) { + if (last_container != nullptr && lv_obj_is_valid(last_container) && + lv_obj_get_child_cnt(last_container) > 0) { // Get the bubble inside the container lv_obj_t* last_bubble = lv_obj_get_child(last_container, 0); if (last_bubble != nullptr && lv_obj_is_valid(last_bubble)) { // Check if bubble type is system message void* bubble_type_ptr = lv_obj_get_user_data(last_bubble); - if (bubble_type_ptr != nullptr && strcmp((const char*)bubble_type_ptr, "system") == 0) { + if (bubble_type_ptr != nullptr && + strcmp((const char*)bubble_type_ptr, "system") == 0) { // If the last message is also a system message, delete it lv_obj_del(last_container); } @@ -558,7 +571,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { } // Avoid empty message boxes - if(strlen(content) == 0) { + if (strlen(content) == 0) { return; } @@ -574,16 +587,16 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { // Create the message text lv_obj_t* msg_text = lv_label_create(msg_bubble); lv_label_set_text(msg_text, content); - + // Calculate bubble width constraints lv_coord_t max_width = LV_HOR_RES * 85 / 100 - 16; // 85% of screen width - lv_coord_t min_width = 20; - + lv_coord_t min_width = 20; + // Let LVGL calculate the natural text width first lv_obj_set_width(msg_text, LV_SIZE_CONTENT); lv_obj_update_layout(msg_text); lv_coord_t text_width = lv_obj_get_width(msg_text); - + // Ensure text width is not less than minimum width if (text_width < min_width) { text_width = min_width; @@ -591,7 +604,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { // Constrain to max width lv_coord_t bubble_width = (text_width < max_width) ? text_width : max_width; - + // Set message text width lv_obj_set_width(msg_text, bubble_width); lv_label_set_long_mode(msg_text, LV_LABEL_LONG_WRAP); @@ -607,14 +620,14 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0); // Set text color for contrast lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0); - + // Set custom attribute to mark bubble type lv_obj_set_user_data(msg_bubble, (void*)"user"); - + // Set appropriate width for content lv_obj_set_width(msg_bubble, LV_SIZE_CONTENT); lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT); - + // Don't grow lv_obj_set_style_flex_grow(msg_bubble, 0, 0); } else if (strcmp(role, "assistant") == 0) { @@ -623,14 +636,14 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0); // Set text color for contrast lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0); - + // Set custom attribute to mark bubble type lv_obj_set_user_data(msg_bubble, (void*)"assistant"); - + // Set appropriate width for content lv_obj_set_width(msg_bubble, LV_SIZE_CONTENT); lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT); - + // Don't grow lv_obj_set_style_flex_grow(msg_bubble, 0, 0); } else if (strcmp(role, "system") == 0) { @@ -639,36 +652,36 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0); // Set text color for contrast lv_obj_set_style_text_color(msg_text, lvgl_theme->system_text_color(), 0); - + // Set custom attribute to mark bubble type lv_obj_set_user_data(msg_bubble, (void*)"system"); - + // Set appropriate width for content lv_obj_set_width(msg_bubble, LV_SIZE_CONTENT); lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT); - + // Don't grow lv_obj_set_style_flex_grow(msg_bubble, 0, 0); } - + // Create a full-width container for user messages to ensure right alignment if (strcmp(role, "user") == 0) { // Create a full-width container lv_obj_t* container = lv_obj_create(content_); lv_obj_set_width(container, LV_HOR_RES); lv_obj_set_height(container, LV_SIZE_CONTENT); - + // Make container transparent and borderless lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(container, 0, 0); lv_obj_set_style_pad_all(container, 0, 0); - + // Move the message bubble into this container lv_obj_set_parent(msg_bubble, container); - + // Right align the bubble in the container lv_obj_align(msg_bubble, LV_ALIGN_RIGHT_MID, -25, 0); - + // Auto-scroll to this container lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON); } else if (strcmp(role, "system") == 0) { @@ -676,11 +689,11 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { lv_obj_t* container = lv_obj_create(content_); lv_obj_set_width(container, LV_HOR_RES); lv_obj_set_height(container, LV_SIZE_CONTENT); - + lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(container, 0, 0); lv_obj_set_style_pad_all(container, 0, 0); - + lv_obj_set_parent(msg_bubble, container); lv_obj_align(msg_bubble, LV_ALIGN_CENTER, 0, 0); lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON); @@ -692,7 +705,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) { // Auto-scroll to the message bubble lv_obj_scroll_to_view_recursive(msg_bubble, LV_ANIM_ON); } - + // Store reference to the latest message label chat_message_label_ = msg_text; } @@ -706,7 +719,7 @@ void LcdDisplay::SetPreviewImage(std::unique_ptr image) { if (image == nullptr) { return; } - + auto lvgl_theme = static_cast(current_theme_); // Create a message bubble for image preview lv_obj_t* img_bubble = lv_obj_create(content_); @@ -714,21 +727,21 @@ void LcdDisplay::SetPreviewImage(std::unique_ptr image) { lv_obj_set_scrollbar_mode(img_bubble, LV_SCROLLBAR_MODE_OFF); lv_obj_set_style_border_width(img_bubble, 0, 0); lv_obj_set_style_pad_all(img_bubble, lvgl_theme->spacing(4), 0); - + // Set image bubble background color (similar to system message) lv_obj_set_style_bg_color(img_bubble, lvgl_theme->assistant_bubble_color(), 0); lv_obj_set_style_bg_opa(img_bubble, LV_OPA_70, 0); - + // Set custom attribute to mark bubble type lv_obj_set_user_data(img_bubble, (void*)"image"); // Create the image object inside the bubble lv_obj_t* preview_image = lv_image_create(img_bubble); - + // Calculate appropriate size for the image - lv_coord_t max_width = LV_HOR_RES * 70 / 100; // 70% of screen width - lv_coord_t max_height = LV_VER_RES * 50 / 100; // 50% of screen height - + lv_coord_t max_width = LV_HOR_RES * 70 / 100; // 70% of screen width + lv_coord_t max_height = LV_VER_RES * 50 / 100; // 50% of screen height + // Calculate zoom factor to fit within maximum dimensions auto img_dsc = image->image_dsc(); lv_coord_t img_width = img_dsc->header.w; @@ -736,44 +749,49 @@ void LcdDisplay::SetPreviewImage(std::unique_ptr image) { if (img_width == 0 || img_height == 0) { img_width = max_width; img_height = max_height; - ESP_LOGW(TAG, "Invalid image dimensions: %ld x %ld, using default dimensions: %ld x %ld", img_width, img_height, max_width, max_height); + ESP_LOGW(TAG, "Invalid image dimensions: %ld x %ld, using default dimensions: %ld x %ld", + img_width, img_height, max_width, max_height); } - + lv_coord_t zoom_w = (max_width * 256) / img_width; lv_coord_t zoom_h = (max_height * 256) / img_height; lv_coord_t zoom = (zoom_w < zoom_h) ? zoom_w : zoom_h; - + // Ensure zoom doesn't exceed 256 (100%) - if (zoom > 256) zoom = 256; - + if (zoom > 256) + zoom = 256; + // Set image properties lv_image_set_src(preview_image, img_dsc); lv_image_set_scale(preview_image, zoom); - + // Add event handler to clean up LvglImage when image is deleted // We need to transfer ownership of the unique_ptr to the event callback - LvglImage* raw_image = image.release(); // Release ownership of smart pointer - lv_obj_add_event_cb(preview_image, [](lv_event_t* e) { - LvglImage* img = (LvglImage*)lv_event_get_user_data(e); - if (img != nullptr) { - delete img; // Properly release memory by deleting LvglImage object - } - }, LV_EVENT_DELETE, (void*)raw_image); - + LvglImage* raw_image = image.release(); // Release ownership of smart pointer + lv_obj_add_event_cb( + preview_image, + [](lv_event_t* e) { + LvglImage* img = (LvglImage*)lv_event_get_user_data(e); + if (img != nullptr) { + delete img; // Properly release memory by deleting LvglImage object + } + }, + LV_EVENT_DELETE, (void*)raw_image); + // Calculate actual scaled image dimensions lv_coord_t scaled_width = (img_width * zoom) / 256; lv_coord_t scaled_height = (img_height * zoom) / 256; - + // Set bubble size to be 16 pixels larger than the image (8 pixels on each side) lv_obj_set_width(img_bubble, scaled_width + 16); lv_obj_set_height(img_bubble, scaled_height + 16); - + // Don't grow in flex layout lv_obj_set_style_flex_grow(img_bubble, 0, 0); - + // Center the image within the bubble lv_obj_center(preview_image); - + // Left align the image bubble like assistant messages lv_obj_align(img_bubble, LV_ALIGN_LEFT_MID, 0, 0); @@ -786,18 +804,18 @@ void LcdDisplay::ClearChatMessages() { if (content_ == nullptr) { return; } - + // Use lv_obj_clean to delete all children of content_ (chat message bubbles) lv_obj_clean(content_); - + // Reset chat_message_label_ as it has been deleted chat_message_label_ = nullptr; - + // Show the centered AI logo (emoji_label_) again if (emoji_label_ != nullptr) { lv_obj_remove_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN); } - + ESP_LOGI(TAG, "Chat messages cleared"); } #else @@ -807,7 +825,7 @@ void LcdDisplay::SetupUI() { ESP_LOGW(TAG, "SetupUI() called multiple times, skipping duplicate call"); return; } - + Display::SetupUI(); // Mark SetupUI as called DisplayLockGuard lock(this); LvglTheme* lvgl_theme = static_cast(current_theme_); @@ -840,7 +858,7 @@ void LcdDisplay::SetupUI() { emoji_label_ = lv_label_create(emoji_box_); lv_obj_set_style_text_font(emoji_label_, large_icon_font, 0); lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0); - lv_label_set_text(emoji_label_, FONT_AWESOME_MICROCHIP_AI); + lv_label_set_text(emoji_label_, MATERIAL_SYMBOLS_ROBOT_2); emoji_image_ = lv_img_create(emoji_box_); lv_obj_center(emoji_image_); @@ -865,7 +883,8 @@ void LcdDisplay::SetupUI() { lv_obj_set_style_pad_left(top_bar_, lvgl_theme->spacing(4), 0); lv_obj_set_style_pad_right(top_bar_, lvgl_theme->spacing(4), 0); lv_obj_set_flex_flow(top_bar_, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); lv_obj_set_scrollbar_mode(top_bar_, LV_SCROLLBAR_MODE_OFF); lv_obj_align(top_bar_, LV_ALIGN_TOP_MID, 0, 0); @@ -882,7 +901,8 @@ void LcdDisplay::SetupUI() { lv_obj_set_style_border_width(right_icons, 0, 0); lv_obj_set_style_pad_all(right_icons, 0, 0); lv_obj_set_flex_flow(right_icons, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); mute_label_ = lv_label_create(right_icons); lv_label_set_text(mute_label_, ""); @@ -906,7 +926,7 @@ void LcdDisplay::SetupUI() { lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0); lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF); lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0); // Use absolute positioning - lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_ + lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_ notification_label_ = lv_label_create(status_bar_); lv_obj_set_width(notification_label_, LV_HOR_RES * 0.75); @@ -976,7 +996,8 @@ void LcdDisplay::SetupUI() { lv_anim_set_delay(&a, 1000); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN); - lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN); + lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), + LV_PART_MAIN); lv_obj_add_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); // Hide until there is content #endif @@ -986,7 +1007,7 @@ void LcdDisplay::SetupUI() { lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, -lvgl_theme->spacing(4)); lv_obj_set_style_bg_color(low_battery_popup_, lvgl_theme->low_battery_color(), 0); lv_obj_set_style_radius(low_battery_popup_, lvgl_theme->spacing(4), 0); - + low_battery_label_ = lv_label_create(low_battery_popup_); lv_label_set_text(low_battery_label_, Lang::Strings::BATTERY_NEED_CHARGE); lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0); @@ -1032,15 +1053,20 @@ void LcdDisplay::SetPreviewImage(std::unique_ptr image) { void LcdDisplay::SetChatMessage(const char* role, const char* content) { if (!setup_ui_called_) { - ESP_LOGW(TAG, "SetChatMessage('%s', '%s') called before SetupUI() - message will be lost!", role, content); + ESP_LOGW(TAG, "SetChatMessage('%s', '%s') called before SetupUI() - message will be lost!", + role, content); } DisplayLockGuard lock(this); if (chat_message_label_ == nullptr) { if (setup_ui_called_) { - ESP_LOGW(TAG, "SetChatMessage('%s', '%s') failed: chat_message_label_ is nullptr (SetupUI() was called but label not created)", role, content); + ESP_LOGW(TAG, + "SetChatMessage('%s', '%s') failed: chat_message_label_ is nullptr (SetupUI() " + "was called but label not created)", + role, content); } return; } + lv_anim_delete(chat_message_label_, nullptr); lv_label_set_text(chat_message_label_, content); // Show bottom_bar_ only when there is content (and subtitle is not globally hidden) if (bottom_bar_ != nullptr) { @@ -1073,11 +1099,15 @@ void LcdDisplay::ClearChatMessages() { void LcdDisplay::SetEmotion(const char* emotion) { if (!setup_ui_called_) { - ESP_LOGW(TAG, "SetEmotion('%s') called before SetupUI() - emotion will not be displayed!", emotion); + ESP_LOGW(TAG, "SetEmotion('%s') called before SetupUI() - emotion will not be displayed!", + emotion); } if (emoji_image_ == nullptr) { if (setup_ui_called_) { - ESP_LOGW(TAG, "SetEmotion('%s') failed: emoji_image_ is nullptr (SetupUI() was called but emoji image not created)", emotion); + ESP_LOGW(TAG, + "SetEmotion('%s') failed: emoji_image_ is nullptr (SetupUI() was called but " + "emoji image not created)", + emotion); } return; } @@ -1085,13 +1115,20 @@ void LcdDisplay::SetEmotion(const char* emotion) { auto emoji_collection = static_cast(current_theme_)->emoji_collection(); auto image = emoji_collection != nullptr ? emoji_collection->GetEmojiImage(emotion) : nullptr; if (image == nullptr) { - const char* utf8 = font_awesome_get_utf8(emotion); + auto lvgl_theme = static_cast(current_theme_); + const char* utf8 = noto_emoji_get_utf8(emotion); + const lv_font_t* emotion_font = lvgl_theme->emoji_font()->font(); + if (utf8 == nullptr) { + utf8 = material_symbols_get_utf8(emotion); + emotion_font = lvgl_theme->large_icon_font()->font(); + } if (utf8 != nullptr && emoji_label_ != nullptr) { DisplayLockGuard lock(this); if (gif_controller_) { gif_controller_->Stop(); gif_controller_.reset(); } + lv_obj_set_style_text_font(emoji_label_, emotion_font, 0); lv_label_set_text(emoji_label_, utf8); lv_obj_add_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN); @@ -1109,17 +1146,16 @@ void LcdDisplay::SetEmotion(const char* emotion) { if (image->IsGif()) { // Create new GIF controller gif_controller_ = std::make_unique(image->image_dsc()); - + if (gif_controller_->IsLoaded()) { // Set up frame update callback - gif_controller_->SetFrameCallback([this]() { - lv_image_set_src(emoji_image_, gif_controller_->image_dsc()); - }); - + gif_controller_->SetFrameCallback( + [this]() { lv_image_set_src(emoji_image_, gif_controller_->image_dsc()); }); + // Set initial frame and start animation lv_image_set_src(emoji_image_, gif_controller_->image_dsc()); gif_controller_->Start(); - + // Show GIF, hide others lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN); @@ -1142,7 +1178,7 @@ void LcdDisplay::SetEmotion(const char* emotion) { gif_controller_->Stop(); gif_controller_.reset(); } - + lv_obj_add_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN); } @@ -1151,9 +1187,9 @@ void LcdDisplay::SetEmotion(const char* emotion) { void LcdDisplay::SetTheme(Theme* theme) { DisplayLockGuard lock(this); - + auto lvgl_theme = static_cast(theme); - + // Get the active screen lv_obj_t* screen = lv_screen_active(); @@ -1183,13 +1219,13 @@ void LcdDisplay::SetTheme(Theme* theme) { lv_obj_set_style_bg_image_src(container_, nullptr, 0); lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0); } - + // Update top bar background color with 50% opacity if (top_bar_ != nullptr) { lv_obj_set_style_bg_opa(top_bar_, LV_OPA_50, 0); lv_obj_set_style_bg_color(top_bar_, lvgl_theme->background_color(), 0); } - + // Update status bar elements lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0); @@ -1207,10 +1243,11 @@ void LcdDisplay::SetTheme(Theme* theme) { uint32_t child_count = lv_obj_get_child_cnt(content_); for (uint32_t i = 0; i < child_count; i++) { lv_obj_t* obj = lv_obj_get_child(content_, i); - if (obj == nullptr) continue; - + if (obj == nullptr) + continue; + lv_obj_t* bubble = nullptr; - + // Check if this object is a container or bubble // If it's a container (user or system message), get its child as bubble // If it's a bubble (assistant message), use it directly @@ -1229,28 +1266,29 @@ void LcdDisplay::SetTheme(Theme* theme) { // No child elements, might be other UI elements, skip continue; } - - if (bubble == nullptr) continue; - + + if (bubble == nullptr) + continue; + // Use saved user data to identify bubble type void* bubble_type_ptr = lv_obj_get_user_data(bubble); if (bubble_type_ptr != nullptr) { const char* bubble_type = static_cast(bubble_type_ptr); - + // Apply correct color based on bubble type if (strcmp(bubble_type, "user") == 0) { lv_obj_set_style_bg_color(bubble, lvgl_theme->user_bubble_color(), 0); } else if (strcmp(bubble_type, "assistant") == 0) { - lv_obj_set_style_bg_color(bubble, lvgl_theme->assistant_bubble_color(), 0); + lv_obj_set_style_bg_color(bubble, lvgl_theme->assistant_bubble_color(), 0); } else if (strcmp(bubble_type, "system") == 0) { lv_obj_set_style_bg_color(bubble, lvgl_theme->system_bubble_color(), 0); } else if (strcmp(bubble_type, "image") == 0) { lv_obj_set_style_bg_color(bubble, lvgl_theme->system_bubble_color(), 0); } - + // Update border color lv_obj_set_style_border_color(bubble, lvgl_theme->border_color(), 0); - + // Update text color for the message if (lv_obj_get_child_cnt(bubble) > 0) { lv_obj_t* text = lv_obj_get_child(bubble, 0); @@ -1272,18 +1310,18 @@ void LcdDisplay::SetTheme(Theme* theme) { if (chat_message_label_ != nullptr) { lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0); } - + if (emoji_label_ != nullptr) { lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0); } - + // Update bottom bar background color with 50% opacity if (bottom_bar_ != nullptr) { lv_obj_set_style_bg_opa(bottom_bar_, LV_OPA_50, 0); lv_obj_set_style_bg_color(bottom_bar_, lvgl_theme->background_color(), 0); } #endif - + // Update low battery popup lv_obj_set_style_bg_color(low_battery_popup_, lvgl_theme->low_battery_color(), 0); @@ -1294,14 +1332,15 @@ void LcdDisplay::SetTheme(Theme* theme) { void LcdDisplay::SetHideSubtitle(bool hide) { DisplayLockGuard lock(this); hide_subtitle_ = hide; - + // Immediately update UI visibility based on the setting if (bottom_bar_ != nullptr) { if (hide) { lv_obj_add_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); } else { // Only show if there is actual content to display - const char* text = (chat_message_label_ != nullptr) ? lv_label_get_text(chat_message_label_) : nullptr; + const char* text = + (chat_message_label_ != nullptr) ? lv_label_get_text(chat_message_label_) : nullptr; if (text != nullptr && text[0] != '\0') { lv_obj_remove_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN); } diff --git a/main/display/lcd_display.h b/main/display/lcd_display.h index e5be55e3..235c110c 100644 --- a/main/display/lcd_display.h +++ b/main/display/lcd_display.h @@ -1,24 +1,21 @@ #ifndef LCD_DISPLAY_H #define LCD_DISPLAY_H -#include "lvgl_display.h" #include "gif/lvgl_gif.h" +#include "lvgl_display.h" #include #include -#include - #include #include #define PREVIEW_IMAGE_DURATION_MS 5000 - class LcdDisplay : public LvglDisplay { protected: esp_lcd_panel_io_handle_t panel_io_ = nullptr; esp_lcd_panel_handle_t panel_ = nullptr; - + lv_draw_buf_t draw_buf_; lv_obj_t* top_bar_ = nullptr; lv_obj_t* status_bar_ = nullptr; @@ -42,8 +39,9 @@ protected: protected: // Add protected constructor - LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height); - + LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, + int height); + public: ~LcdDisplay(); virtual void SetEmotion(const char* emotion) override; @@ -53,7 +51,7 @@ public: virtual void SetupUI() override; // Add theme switching function virtual void SetTheme(Theme* theme) override; - + // Set whether to hide chat messages/subtitles void SetHideSubtitle(bool hide); }; @@ -61,25 +59,25 @@ public: // SPI LCD display class SpiLcdDisplay : public LcdDisplay { public: - SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, - int width, int height, int offset_x, int offset_y, - bool mirror_x, bool mirror_y, bool swap_xy); + SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, + int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, + bool swap_xy); }; // RGB LCD display class RgbLcdDisplay : public LcdDisplay { public: - RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, - int width, int height, int offset_x, int offset_y, - bool mirror_x, bool mirror_y, bool swap_xy); + RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, + int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, + bool swap_xy); }; // MIPI LCD display class MipiLcdDisplay : public LcdDisplay { public: - MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, - int width, int height, int offset_x, int offset_y, - bool mirror_x, bool mirror_y, bool swap_xy); + MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, + int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, + bool swap_xy); }; -#endif // LCD_DISPLAY_H +#endif // LCD_DISPLAY_H diff --git a/main/display/lvgl_display/dynamic_glyph_cache.cc b/main/display/lvgl_display/dynamic_glyph_cache.cc new file mode 100644 index 00000000..287439ec --- /dev/null +++ b/main/display/lvgl_display/dynamic_glyph_cache.cc @@ -0,0 +1,181 @@ +#include "dynamic_glyph_cache.h" + +#include +#include +#include + +#define TAG "DynamicGlyphCache" + +DynamicGlyphCache::DynamicGlyphCache() : retain_between_batches_(TextGlyphStorageUsesPsram()) {} + +lv_font_t* DynamicGlyphCache::EnsureFont(const lv_font_t* base_font, uint8_t bpp) { + if (base_font == nullptr || (bpp != 1 && bpp != 4)) { + return nullptr; + } + bool needs_rebuild = !initialized_; + if (initialized_ && bpp_ != bpp) { + entries_.clear(); + use_counter_ = 0; + needs_rebuild = true; + } + bpp_ = bpp; + font_.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt; + font_.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt; + font_.release_glyph = nullptr; + font_.line_height = base_font->line_height; + font_.base_line = base_font->base_line; + font_.subpx = LV_FONT_SUBPX_NONE; + font_.kerning = LV_FONT_KERNING_NONE; + font_.static_bitmap = 0; + font_.underline_position = base_font->underline_position; + font_.underline_thickness = base_font->underline_thickness; + font_.fallback = nullptr; + font_.user_data = nullptr; + font_.dsc = &dsc_; + initialized_ = true; + if (needs_rebuild) { + Rebuild(); + } + return &font_; +} + +size_t DynamicGlyphCache::BitmapBytes() const { + size_t total = 0; + for (const auto& entry : entries_) { + total += entry.bitmap.size(); + } + return total; +} + +bool DynamicGlyphCache::AddGlyphs(const std::vector& glyphs) { + if (!initialized_ || glyphs.empty()) { + return false; + } + + bool changed = false; + if (!retain_between_batches_) { + changed = !entries_.empty(); + entries_.clear(); + use_counter_ = 0; + } + size_t bitmap_bytes = BitmapBytes(); + for (const auto& glyph : glyphs) { + const size_t expected = (static_cast(glyph.box_w) * glyph.box_h * bpp_ + 7) / 8; + if (glyph.codepoint == 0 || glyph.codepoint > 0x10FFFF || glyph.bitmap.size() != expected) { + ESP_LOGW(TAG, "Rejected glyph U+%04lX", static_cast(glyph.codepoint)); + continue; + } + + auto existing = std::find_if( + entries_.begin(), entries_.end(), + [&glyph](const Entry& entry) { return entry.codepoint == glyph.codepoint; }); + if (existing != entries_.end()) { + bitmap_bytes -= existing->bitmap.size(); + } else { + entries_.emplace_back(); + existing = std::prev(entries_.end()); + } + existing->codepoint = glyph.codepoint; + existing->adv_w = glyph.adv_w; + existing->box_w = glyph.box_w; + existing->box_h = glyph.box_h; + existing->ofs_x = glyph.ofs_x; + existing->ofs_y = glyph.ofs_y; + existing->bitmap.assign(glyph.bitmap.begin(), glyph.bitmap.end()); + existing->last_use = ++use_counter_; + bitmap_bytes += glyph.bitmap.size(); + changed = true; + } + + while (entries_.size() > kMaxGlyphs || bitmap_bytes > kMaxBitmapBytes) { + auto oldest = std::min_element( + entries_.begin(), entries_.end(), + [](const Entry& a, const Entry& b) { return a.last_use < b.last_use; }); + if (oldest == entries_.end()) { + break; + } + bitmap_bytes -= oldest->bitmap.size(); + entries_.erase(oldest); + } + if (changed) { + Rebuild(); + } + return changed; +} + +void DynamicGlyphCache::Clear() { + if (retain_between_batches_) { + entries_.clear(); + } else { + TextGlyphVector().swap(entries_); + TextGlyphVector().swap(bitmap_blob_); + TextGlyphVector().swap(unicode_list_); + TextGlyphVector().swap(glyph_dsc_); + TextGlyphVector().swap(cmaps_); + } + use_counter_ = 0; + Rebuild(); +} + +void DynamicGlyphCache::Rebuild() { + std::sort(entries_.begin(), entries_.end(), + [](const Entry& a, const Entry& b) { return a.codepoint < b.codepoint; }); + + bitmap_blob_.clear(); + unicode_list_.clear(); + glyph_dsc_.clear(); + cmaps_.clear(); + glyph_dsc_.push_back(lv_font_fmt_txt_glyph_dsc_t{}); + + struct Range { + uint32_t start; + uint32_t list_begin; + uint32_t last; + uint16_t count; + }; + TextGlyphVector ranges; + for (const auto& entry : entries_) { + if (ranges.empty() || entry.codepoint - ranges.back().start > 0xFFFE) { + ranges.push_back(Range{entry.codepoint, static_cast(unicode_list_.size()), + entry.codepoint, 0}); + } + auto& range = ranges.back(); + unicode_list_.push_back(static_cast(entry.codepoint - range.start)); + range.last = entry.codepoint; + ++range.count; + + lv_font_fmt_txt_glyph_dsc_t glyph_dsc{}; + glyph_dsc.bitmap_index = bitmap_blob_.size(); + glyph_dsc.adv_w = entry.adv_w; + glyph_dsc.box_w = entry.box_w; + glyph_dsc.box_h = entry.box_h; + glyph_dsc.ofs_x = entry.ofs_x; + glyph_dsc.ofs_y = entry.ofs_y; + glyph_dsc_.push_back(glyph_dsc); + bitmap_blob_.insert(bitmap_blob_.end(), entry.bitmap.begin(), entry.bitmap.end()); + } + + for (const auto& range : ranges) { + lv_font_fmt_txt_cmap_t cmap{}; + cmap.range_start = range.start; + cmap.range_length = static_cast(range.last - range.start + 1); + cmap.glyph_id_start = static_cast(range.list_begin + 1); + cmap.unicode_list = unicode_list_.data() + range.list_begin; + cmap.glyph_id_ofs_list = nullptr; + cmap.list_length = range.count; + cmap.type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY; + cmaps_.push_back(cmap); + } + + dsc_.glyph_bitmap = bitmap_blob_.empty() ? nullptr : bitmap_blob_.data(); + dsc_.glyph_dsc = glyph_dsc_.data(); + dsc_.cmaps = cmaps_.empty() ? nullptr : cmaps_.data(); + dsc_.kern_dsc = nullptr; + dsc_.kern_scale = 0; + dsc_.cmap_num = cmaps_.size(); + dsc_.bpp = bpp_; + dsc_.kern_classes = 0; + dsc_.bitmap_format = LV_FONT_FMT_TXT_PLAIN; + dsc_.stride = 0; + font_.dsc = &dsc_; +} diff --git a/main/display/lvgl_display/dynamic_glyph_cache.h b/main/display/lvgl_display/dynamic_glyph_cache.h new file mode 100644 index 00000000..a2007303 --- /dev/null +++ b/main/display/lvgl_display/dynamic_glyph_cache.h @@ -0,0 +1,49 @@ +#ifndef DYNAMIC_GLYPH_CACHE_H +#define DYNAMIC_GLYPH_CACHE_H + +#include "display.h" + +#include +#include +#include +#include + +class DynamicGlyphCache { +public: + static constexpr size_t kMaxGlyphs = 256; + static constexpr size_t kMaxBitmapBytes = 64 * 1024; + + DynamicGlyphCache(); + lv_font_t* EnsureFont(const lv_font_t* base_font, uint8_t bpp); + bool AddGlyphs(const std::vector& glyphs); + void Clear(); + +private: + struct Entry { + uint32_t codepoint = 0; + uint32_t adv_w = 0; + uint16_t box_w = 0; + uint16_t box_h = 0; + int16_t ofs_x = 0; + int16_t ofs_y = 0; + TextGlyphVector bitmap; + uint64_t last_use = 0; + }; + + void Rebuild(); + size_t BitmapBytes() const; + + bool initialized_ = false; + bool retain_between_batches_ = false; + uint8_t bpp_ = 0; + uint64_t use_counter_ = 0; + lv_font_t font_{}; + lv_font_fmt_txt_dsc_t dsc_{}; + TextGlyphVector entries_; + TextGlyphVector bitmap_blob_; + TextGlyphVector unicode_list_; + TextGlyphVector glyph_dsc_; + TextGlyphVector cmaps_; +}; + +#endif diff --git a/main/display/lvgl_display/emoji_collection.cc b/main/display/lvgl_display/emoji_collection.cc index c9a1a605..08692603 100644 --- a/main/display/lvgl_display/emoji_collection.cc +++ b/main/display/lvgl_display/emoji_collection.cc @@ -1,8 +1,6 @@ #include "emoji_collection.h" #include -#include -#include #define TAG "EmojiCollection" @@ -26,98 +24,3 @@ EmojiCollection::~EmojiCollection() { } emoji_collection_.clear(); } - -// These are declared in xiaozhi-fonts/src/font_emoji_32.c -extern const lv_image_dsc_t emoji_1f636_32; // neutral -extern const lv_image_dsc_t emoji_1f642_32; // happy -extern const lv_image_dsc_t emoji_1f606_32; // laughing -extern const lv_image_dsc_t emoji_1f602_32; // funny -extern const lv_image_dsc_t emoji_1f614_32; // sad -extern const lv_image_dsc_t emoji_1f620_32; // angry -extern const lv_image_dsc_t emoji_1f62d_32; // crying -extern const lv_image_dsc_t emoji_1f60d_32; // loving -extern const lv_image_dsc_t emoji_1f633_32; // embarrassed -extern const lv_image_dsc_t emoji_1f62f_32; // surprised -extern const lv_image_dsc_t emoji_1f631_32; // shocked -extern const lv_image_dsc_t emoji_1f914_32; // thinking -extern const lv_image_dsc_t emoji_1f609_32; // winking -extern const lv_image_dsc_t emoji_1f60e_32; // cool -extern const lv_image_dsc_t emoji_1f60c_32; // relaxed -extern const lv_image_dsc_t emoji_1f924_32; // delicious -extern const lv_image_dsc_t emoji_1f618_32; // kissy -extern const lv_image_dsc_t emoji_1f60f_32; // confident -extern const lv_image_dsc_t emoji_1f634_32; // sleepy -extern const lv_image_dsc_t emoji_1f61c_32; // silly -extern const lv_image_dsc_t emoji_1f644_32; // confused - -Twemoji32::Twemoji32() { - AddEmoji("neutral", new LvglSourceImage(&emoji_1f636_32)); - AddEmoji("happy", new LvglSourceImage(&emoji_1f642_32)); - AddEmoji("laughing", new LvglSourceImage(&emoji_1f606_32)); - AddEmoji("funny", new LvglSourceImage(&emoji_1f602_32)); - AddEmoji("sad", new LvglSourceImage(&emoji_1f614_32)); - AddEmoji("angry", new LvglSourceImage(&emoji_1f620_32)); - AddEmoji("crying", new LvglSourceImage(&emoji_1f62d_32)); - AddEmoji("loving", new LvglSourceImage(&emoji_1f60d_32)); - AddEmoji("embarrassed", new LvglSourceImage(&emoji_1f633_32)); - AddEmoji("surprised", new LvglSourceImage(&emoji_1f62f_32)); - AddEmoji("shocked", new LvglSourceImage(&emoji_1f631_32)); - AddEmoji("thinking", new LvglSourceImage(&emoji_1f914_32)); - AddEmoji("winking", new LvglSourceImage(&emoji_1f609_32)); - AddEmoji("cool", new LvglSourceImage(&emoji_1f60e_32)); - AddEmoji("relaxed", new LvglSourceImage(&emoji_1f60c_32)); - AddEmoji("delicious", new LvglSourceImage(&emoji_1f924_32)); - AddEmoji("kissy", new LvglSourceImage(&emoji_1f618_32)); - AddEmoji("confident", new LvglSourceImage(&emoji_1f60f_32)); - AddEmoji("sleepy", new LvglSourceImage(&emoji_1f634_32)); - AddEmoji("silly", new LvglSourceImage(&emoji_1f61c_32)); - AddEmoji("confused", new LvglSourceImage(&emoji_1f644_32)); -} - - -// These are declared in xiaozhi-fonts/src/font_emoji_64.c -extern const lv_image_dsc_t emoji_1f636_64; // neutral -extern const lv_image_dsc_t emoji_1f642_64; // happy -extern const lv_image_dsc_t emoji_1f606_64; // laughing -extern const lv_image_dsc_t emoji_1f602_64; // funny -extern const lv_image_dsc_t emoji_1f614_64; // sad -extern const lv_image_dsc_t emoji_1f620_64; // angry -extern const lv_image_dsc_t emoji_1f62d_64; // crying -extern const lv_image_dsc_t emoji_1f60d_64; // loving -extern const lv_image_dsc_t emoji_1f633_64; // embarrassed -extern const lv_image_dsc_t emoji_1f62f_64; // surprised -extern const lv_image_dsc_t emoji_1f631_64; // shocked -extern const lv_image_dsc_t emoji_1f914_64; // thinking -extern const lv_image_dsc_t emoji_1f609_64; // winking -extern const lv_image_dsc_t emoji_1f60e_64; // cool -extern const lv_image_dsc_t emoji_1f60c_64; // relaxed -extern const lv_image_dsc_t emoji_1f924_64; // delicious -extern const lv_image_dsc_t emoji_1f618_64; // kissy -extern const lv_image_dsc_t emoji_1f60f_64; // confident -extern const lv_image_dsc_t emoji_1f634_64; // sleepy -extern const lv_image_dsc_t emoji_1f61c_64; // silly -extern const lv_image_dsc_t emoji_1f644_64; // confused - -Twemoji64::Twemoji64() { - AddEmoji("neutral", new LvglSourceImage(&emoji_1f636_64)); - AddEmoji("happy", new LvglSourceImage(&emoji_1f642_64)); - AddEmoji("laughing", new LvglSourceImage(&emoji_1f606_64)); - AddEmoji("funny", new LvglSourceImage(&emoji_1f602_64)); - AddEmoji("sad", new LvglSourceImage(&emoji_1f614_64)); - AddEmoji("angry", new LvglSourceImage(&emoji_1f620_64)); - AddEmoji("crying", new LvglSourceImage(&emoji_1f62d_64)); - AddEmoji("loving", new LvglSourceImage(&emoji_1f60d_64)); - AddEmoji("embarrassed", new LvglSourceImage(&emoji_1f633_64)); - AddEmoji("surprised", new LvglSourceImage(&emoji_1f62f_64)); - AddEmoji("shocked", new LvglSourceImage(&emoji_1f631_64)); - AddEmoji("thinking", new LvglSourceImage(&emoji_1f914_64)); - AddEmoji("winking", new LvglSourceImage(&emoji_1f609_64)); - AddEmoji("cool", new LvglSourceImage(&emoji_1f60e_64)); - AddEmoji("relaxed", new LvglSourceImage(&emoji_1f60c_64)); - AddEmoji("delicious", new LvglSourceImage(&emoji_1f924_64)); - AddEmoji("kissy", new LvglSourceImage(&emoji_1f618_64)); - AddEmoji("confident", new LvglSourceImage(&emoji_1f60f_64)); - AddEmoji("sleepy", new LvglSourceImage(&emoji_1f634_64)); - AddEmoji("silly", new LvglSourceImage(&emoji_1f61c_64)); - AddEmoji("confused", new LvglSourceImage(&emoji_1f644_64)); -} diff --git a/main/display/lvgl_display/emoji_collection.h b/main/display/lvgl_display/emoji_collection.h index c9eac054..7ffc5da7 100644 --- a/main/display/lvgl_display/emoji_collection.h +++ b/main/display/lvgl_display/emoji_collection.h @@ -6,9 +6,8 @@ #include #include -#include #include - +#include // Define interface for emoji collection class EmojiCollection { @@ -21,14 +20,4 @@ private: std::map emoji_collection_; }; -class Twemoji32 : public EmojiCollection { -public: - Twemoji32(); -}; - -class Twemoji64 : public EmojiCollection { -public: - Twemoji64(); -}; - #endif diff --git a/main/display/lvgl_display/lvgl_display.cc b/main/display/lvgl_display/lvgl_display.cc index 46668ddf..44137df7 100644 --- a/main/display/lvgl_display/lvgl_display.cc +++ b/main/display/lvgl_display/lvgl_display.cc @@ -1,29 +1,33 @@ -#include #include -#include +#include +#include #include #include -#include +#include -#include "lvgl_display.h" -#include "board.h" #include "application.h" -#include "audio_codec.h" -#include "settings.h" #include "assets/lang_config.h" +#include "audio_codec.h" +#include "board.h" +#include "dynamic_glyph_cache.h" #include "jpg/image_to_jpeg.h" +#include "lvgl_display.h" +#include "lvgl_theme.h" +#include "settings.h" #define TAG "Display" LvglDisplay::LvglDisplay() { + dynamic_glyph_cache_ = std::make_unique(); // Notification timer esp_timer_create_args_t notification_timer_args = { - .callback = [](void *arg) { - LvglDisplay *display = static_cast(arg); - DisplayLockGuard lock(display); - lv_obj_add_flag(display->notification_label_, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(display->status_label_, LV_OBJ_FLAG_HIDDEN); - }, + .callback = + [](void* arg) { + LvglDisplay* display = static_cast(arg); + DisplayLockGuard lock(display); + lv_obj_add_flag(display->notification_label_, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(display->status_label_, LV_OBJ_FLAG_HIDDEN); + }, .arg = this, .dispatch_method = ESP_TIMER_TASK, .name = "notification_timer", @@ -40,6 +44,73 @@ LvglDisplay::LvglDisplay() { } } +bool LvglDisplay::SetTextFont(std::shared_ptr text_font) { + if (text_font == nullptr || text_font->font() == nullptr) { + return false; + } + + DisplayLockGuard lock(this); + auto& theme_manager = LvglThemeManager::GetInstance(); + auto light_theme = theme_manager.GetTheme("light"); + auto dark_theme = theme_manager.GetTheme("dark"); + if (light_theme == nullptr && dark_theme == nullptr) { + return false; + } + + // LVGL styles keep raw lv_font_t pointers. Keep the previous font owners alive until the + // current theme has rebound every style to the new font. + auto previous_light_font = light_theme != nullptr ? light_theme->text_font() : nullptr; + auto previous_dark_font = dark_theme != nullptr ? dark_theme->text_font() : nullptr; + if (light_theme != nullptr) { + light_theme->set_text_font(text_font); + } + if (dark_theme != nullptr) { + dark_theme->set_text_font(text_font); + } + if (current_theme_ != nullptr) { + SetTheme(current_theme_); + } + previous_light_font.reset(); + previous_dark_font.reset(); + return true; +} + +bool LvglDisplay::AddTextGlyphs(const std::vector& glyphs, uint8_t bpp) { + if (dynamic_glyph_cache_ == nullptr) { + return false; + } + if (glyphs.empty()) { + if (!TextGlyphStorageUsesPsram()) { + ClearTextGlyphs(); + } + return false; + } + if (bpp != 1 && bpp != 4) { + return false; + } + + DisplayLockGuard lock(this); + auto theme = dynamic_cast(current_theme_); + if (theme == nullptr || theme->text_font() == nullptr) { + return false; + } + + auto fallback = dynamic_glyph_cache_->EnsureFont(theme->text_font()->font(), bpp); + if (fallback == nullptr) { + return false; + } + theme->text_font()->SetFallback(fallback); + return dynamic_glyph_cache_->AddGlyphs(glyphs); +} + +void LvglDisplay::ClearTextGlyphs() { + if (dynamic_glyph_cache_ == nullptr) { + return; + } + DisplayLockGuard lock(this); + dynamic_glyph_cache_->Clear(); +} + LvglDisplay::~LvglDisplay() { if (notification_timer_ != nullptr) { esp_timer_stop(notification_timer_); @@ -61,7 +132,7 @@ LvglDisplay::~LvglDisplay() { if (battery_label_ != nullptr) { lv_obj_del(battery_label_); } - if( low_battery_popup_ != nullptr ) { + if (low_battery_popup_ != nullptr) { lv_obj_del(low_battery_popup_); } if (pm_lock_ != nullptr) { @@ -76,7 +147,10 @@ void LvglDisplay::SetStatus(const char* status) { DisplayLockGuard lock(this); if (status_label_ == nullptr) { if (setup_ui_called_) { - ESP_LOGW(TAG, "SetStatus('%s') failed: status_label_ is nullptr (SetupUI() was called but label not created)", status); + ESP_LOGW(TAG, + "SetStatus('%s') failed: status_label_ is nullptr (SetupUI() was called but " + "label not created)", + status); } return; } @@ -87,18 +161,22 @@ void LvglDisplay::SetStatus(const char* status) { last_status_update_time_ = std::chrono::system_clock::now(); } -void LvglDisplay::ShowNotification(const std::string ¬ification, int duration_ms) { +void LvglDisplay::ShowNotification(const std::string& notification, int duration_ms) { ShowNotification(notification.c_str(), duration_ms); } void LvglDisplay::ShowNotification(const char* notification, int duration_ms) { if (!setup_ui_called_) { - ESP_LOGW(TAG, "ShowNotification('%s') called before SetupUI() - message will be lost!", notification); + ESP_LOGW(TAG, "ShowNotification('%s') called before SetupUI() - message will be lost!", + notification); } DisplayLockGuard lock(this); if (notification_label_ == nullptr) { if (setup_ui_called_) { - ESP_LOGW(TAG, "ShowNotification('%s') failed: notification_label_ is nullptr (SetupUI() was called but label not created)", notification); + ESP_LOGW(TAG, + "ShowNotification('%s') failed: notification_label_ is nullptr (SetupUI() was " + "called but label not created)", + notification); } return; } @@ -125,7 +203,7 @@ void LvglDisplay::UpdateStatusBar(bool update_all) { // Update icon if mute state changes if (codec->output_volume() == 0 && !muted_) { muted_ = true; - lv_label_set_text(mute_label_, FONT_AWESOME_VOLUME_XMARK); + lv_label_set_text(mute_label_, MATERIAL_SYMBOLS_VOLUME_OFF); } else if (codec->output_volume() > 0 && muted_) { muted_ = false; lv_label_set_text(mute_label_, ""); @@ -134,7 +212,8 @@ void LvglDisplay::UpdateStatusBar(bool update_all) { // Update time if (app.GetDeviceState() == kDeviceStateIdle) { - if (last_status_update_time_ + std::chrono::seconds(10) < std::chrono::system_clock::now()) { + if (last_status_update_time_ + std::chrono::seconds(10) < + std::chrono::system_clock::now()) { // Set status to clock "HH:MM" time_t now = time(NULL); struct tm* tm = localtime(&now); @@ -156,17 +235,22 @@ void LvglDisplay::UpdateStatusBar(bool update_all) { const char* icon = nullptr; if (board.GetBatteryLevel(battery_level, charging, discharging)) { if (charging) { - icon = FONT_AWESOME_BATTERY_BOLT; + icon = MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_BOLT; } else { const char* levels[] = { - FONT_AWESOME_BATTERY_EMPTY, // 0-19% - FONT_AWESOME_BATTERY_QUARTER, // 20-39% - FONT_AWESOME_BATTERY_HALF, // 40-59% - FONT_AWESOME_BATTERY_THREE_QUARTERS, // 60-79% - FONT_AWESOME_BATTERY_FULL, // 80-99% - FONT_AWESOME_BATTERY_FULL, // 100% + MATERIAL_SYMBOLS_BATTERY_ANDROID_0, + MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_1, + MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_2, + MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_3, + MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_4, + MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_5, + MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_6, + MATERIAL_SYMBOLS_BATTERY_ANDROID_FRAME_FULL, }; - icon = levels[battery_level / 20]; + int level_index = battery_level <= 0 + ? 0 + : (battery_level >= 100 ? 7 : 1 + ((battery_level - 1) * 6 / 99)); + icon = levels[level_index]; } DisplayLockGuard lock(this); if (battery_label_ != nullptr && battery_icon_ != icon) { @@ -177,16 +261,16 @@ void LvglDisplay::UpdateStatusBar(bool update_all) { // Check low battery popup only when clock tick event is triggered // Because when initializing, the battery level is not ready yet. if (low_battery_popup_ != nullptr && !update_all) { - if (strcmp(icon, FONT_AWESOME_BATTERY_EMPTY) == 0 && discharging) { - if (lv_obj_has_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN)) { // Show if low battery popup is hidden + if (strcmp(icon, MATERIAL_SYMBOLS_BATTERY_ANDROID_0) == 0 && discharging) { + if (lv_obj_has_flag(low_battery_popup_, + LV_OBJ_FLAG_HIDDEN)) { // Show if low battery popup is hidden lv_obj_remove_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); - app.Schedule([&app]() { - app.PlaySound(Lang::Sounds::OGG_LOW_BATTERY); - }); + app.Schedule([&app]() { app.PlaySound(Lang::Sounds::OGG_LOW_BATTERY); }); } } else { // Hide the low battery popup when the battery is not empty - if (!lv_obj_has_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN)) { // Hide if low battery popup is shown + if (!lv_obj_has_flag(low_battery_popup_, + LV_OBJ_FLAG_HIDDEN)) { // Hide if low battery popup is shown lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); } } @@ -199,13 +283,11 @@ void LvglDisplay::UpdateStatusBar(bool update_all) { // Don't read 4G network status during firmware upgrade to avoid occupying UART resources auto device_state = Application::GetInstance().GetDeviceState(); static const std::vector allowed_states = { - kDeviceStateIdle, - kDeviceStateStarting, - kDeviceStateWifiConfiguring, - kDeviceStateListening, - kDeviceStateActivating, + kDeviceStateIdle, kDeviceStateStarting, kDeviceStateWifiConfiguring, + kDeviceStateListening, kDeviceStateActivating, }; - if (std::find(allowed_states.begin(), allowed_states.end(), device_state) != allowed_states.end()) { + if (std::find(allowed_states.begin(), allowed_states.end(), device_state) != + allowed_states.end()) { icon = board.GetNetworkStateIcon(); if (network_label_ != nullptr && icon != nullptr && network_icon_ != icon) { DisplayLockGuard lock(this); @@ -218,8 +300,7 @@ void LvglDisplay::UpdateStatusBar(bool update_all) { esp_pm_lock_release(pm_lock_); } -void LvglDisplay::SetPreviewImage(std::unique_ptr image) { -} +void LvglDisplay::SetPreviewImage(std::unique_ptr image) {} void LvglDisplay::SetPowerSaveMode(bool on) { if (on) { @@ -253,14 +334,17 @@ bool LvglDisplay::SnapshotToJpeg(std::string& jpeg_data, int quality) { jpeg_data.clear(); // Use callback-based JPEG encoder to further save memory - bool ret = image_to_jpeg_cb((uint8_t*)draw_buffer->data, draw_buffer->data_size, draw_buffer->header.w, draw_buffer->header.h, V4L2_PIX_FMT_RGB565, quality, - [](void *arg, size_t index, const void *data, size_t len) -> size_t { - std::string* output = static_cast(arg); - if (data && len > 0) { - output->append(static_cast(data), len); - } - return len; - }, &jpeg_data); + bool ret = + image_to_jpeg_cb((uint8_t*)draw_buffer->data, draw_buffer->data_size, draw_buffer->header.w, + draw_buffer->header.h, V4L2_PIX_FMT_RGB565, quality, + [](void* arg, size_t index, const void* data, size_t len) -> size_t { + std::string* output = static_cast(arg); + if (data && len > 0) { + output->append(static_cast(data), len); + } + return len; + }, + &jpeg_data); if (!ret) { ESP_LOGE(TAG, "Failed to convert image to JPEG"); } diff --git a/main/display/lvgl_display/lvgl_display.h b/main/display/lvgl_display/lvgl_display.h index 226a6ff4..4b393167 100644 --- a/main/display/lvgl_display/lvgl_display.h +++ b/main/display/lvgl_display/lvgl_display.h @@ -4,13 +4,17 @@ #include "display.h" #include "lvgl_image.h" -#include -#include #include #include +#include +#include -#include #include +#include +#include + +class DynamicGlyphCache; +class LvglFont; class LvglDisplay : public Display { public: @@ -19,35 +23,38 @@ public: virtual void SetStatus(const char* status); virtual void ShowNotification(const char* notification, int duration_ms = 3000); - virtual void ShowNotification(const std::string ¬ification, int duration_ms = 3000); + virtual void ShowNotification(const std::string& notification, int duration_ms = 3000); virtual void SetPreviewImage(std::unique_ptr image); virtual void UpdateStatusBar(bool update_all = false); virtual void SetPowerSaveMode(bool on); virtual bool SnapshotToJpeg(std::string& jpeg_data, int quality = 80); + virtual bool AddTextGlyphs(const std::vector& glyphs, uint8_t bpp) override; + virtual void ClearTextGlyphs() override; + bool SetTextFont(std::shared_ptr text_font); protected: esp_pm_lock_handle_t pm_lock_ = nullptr; - lv_display_t *display_ = nullptr; + lv_display_t* display_ = nullptr; - lv_obj_t *network_label_ = nullptr; - lv_obj_t *status_label_ = nullptr; - lv_obj_t *notification_label_ = nullptr; - lv_obj_t *mute_label_ = nullptr; - lv_obj_t *battery_label_ = nullptr; + lv_obj_t* network_label_ = nullptr; + lv_obj_t* status_label_ = nullptr; + lv_obj_t* notification_label_ = nullptr; + lv_obj_t* mute_label_ = nullptr; + lv_obj_t* battery_label_ = nullptr; lv_obj_t* low_battery_popup_ = nullptr; lv_obj_t* low_battery_label_ = nullptr; - + const char* battery_icon_ = nullptr; const char* network_icon_ = nullptr; bool muted_ = false; std::chrono::system_clock::time_point last_status_update_time_; esp_timer_handle_t notification_timer_ = nullptr; + std::unique_ptr dynamic_glyph_cache_; friend class DisplayLockGuard; virtual bool Lock(int timeout_ms = 0) = 0; virtual void Unlock() = 0; }; - #endif diff --git a/main/display/lvgl_display/lvgl_font.h b/main/display/lvgl_display/lvgl_font.h index d539dc01..e6b356f2 100644 --- a/main/display/lvgl_display/lvgl_font.h +++ b/main/display/lvgl_display/lvgl_font.h @@ -2,29 +2,40 @@ #include - class LvglFont { public: virtual const lv_font_t* font() const = 0; + virtual void SetFallback(const lv_font_t* fallback) = 0; virtual ~LvglFont() = default; }; // Built-in font class LvglBuiltInFont : public LvglFont { public: - LvglBuiltInFont(const lv_font_t* font) : font_(font) {} - virtual const lv_font_t* font() const override { return font_; } + LvglBuiltInFont(const lv_font_t* font) : font_(*font) {} + virtual const lv_font_t* font() const override { return &font_; } + virtual void SetFallback(const lv_font_t* fallback) override { font_.fallback = fallback; } private: - const lv_font_t* font_; + lv_font_t font_{}; }; - class LvglCBinFont : public LvglFont { public: LvglCBinFont(void* data); virtual ~LvglCBinFont(); virtual const lv_font_t* font() const override { return font_; } + uint8_t bpp() const { + if (font_ == nullptr || font_->dsc == nullptr) { + return 0; + } + return static_cast(font_->dsc)->bpp; + } + virtual void SetFallback(const lv_font_t* fallback) override { + if (font_ != nullptr) { + font_->fallback = fallback; + } + } private: lv_font_t* font_; diff --git a/main/display/lvgl_display/lvgl_image.h b/main/display/lvgl_display/lvgl_image.h index 0bbf39be..f3bc1b35 100644 --- a/main/display/lvgl_display/lvgl_image.h +++ b/main/display/lvgl_display/lvgl_image.h @@ -2,7 +2,6 @@ #include - // Wrap around lv_img_dsc_t class LvglImage { public: @@ -11,7 +10,6 @@ public: virtual ~LvglImage() = default; }; - class LvglRawImage : public LvglImage { public: LvglRawImage(void* data, size_t size); @@ -32,22 +30,14 @@ private: lv_img_dsc_t* image_dsc_ = nullptr; }; -class LvglSourceImage : public LvglImage { -public: - LvglSourceImage(const lv_img_dsc_t* image_dsc) : image_dsc_(image_dsc) {} - virtual const lv_img_dsc_t* image_dsc() const override { return image_dsc_; } - -private: - const lv_img_dsc_t* image_dsc_; -}; - class LvglAllocatedImage : public LvglImage { public: LvglAllocatedImage(void* data, size_t size); - LvglAllocatedImage(void* data, size_t size, int width, int height, int stride, int color_format); + LvglAllocatedImage(void* data, size_t size, int width, int height, int stride, + int color_format); virtual ~LvglAllocatedImage(); virtual const lv_img_dsc_t* image_dsc() const override { return &image_dsc_; } private: lv_img_dsc_t image_dsc_; -}; \ No newline at end of file +}; diff --git a/main/display/lvgl_display/lvgl_theme.h b/main/display/lvgl_display/lvgl_theme.h index 85527a9a..f59992c5 100644 --- a/main/display/lvgl_display/lvgl_theme.h +++ b/main/display/lvgl_display/lvgl_theme.h @@ -1,16 +1,15 @@ #pragma once #include "display.h" -#include "lvgl_image.h" -#include "lvgl_font.h" #include "emoji_collection.h" +#include "lvgl_font.h" +#include "lvgl_image.h" #include -#include #include +#include #include - class LvglTheme : public Theme { public: static lv_color_t ParseColor(const std::string& color); @@ -32,22 +31,36 @@ public: inline std::shared_ptr text_font() const { return text_font_; } inline std::shared_ptr icon_font() const { return icon_font_; } inline std::shared_ptr large_icon_font() const { return large_icon_font_; } + inline std::shared_ptr emoji_font() const { return emoji_font_; } inline int spacing(int scale) const { return spacing_ * scale; } inline void set_background_color(lv_color_t background) { background_color_ = background; } inline void set_text_color(lv_color_t text) { text_color_ = text; } - inline void set_chat_background_color(lv_color_t chat_background) { chat_background_color_ = chat_background; } + inline void set_chat_background_color(lv_color_t chat_background) { + chat_background_color_ = chat_background; + } inline void set_user_bubble_color(lv_color_t user_bubble) { user_bubble_color_ = user_bubble; } - inline void set_assistant_bubble_color(lv_color_t assistant_bubble) { assistant_bubble_color_ = assistant_bubble; } - inline void set_system_bubble_color(lv_color_t system_bubble) { system_bubble_color_ = system_bubble; } + inline void set_assistant_bubble_color(lv_color_t assistant_bubble) { + assistant_bubble_color_ = assistant_bubble; + } + inline void set_system_bubble_color(lv_color_t system_bubble) { + system_bubble_color_ = system_bubble; + } inline void set_system_text_color(lv_color_t system_text) { system_text_color_ = system_text; } inline void set_border_color(lv_color_t border) { border_color_ = border; } inline void set_low_battery_color(lv_color_t low_battery) { low_battery_color_ = low_battery; } - inline void set_background_image(std::shared_ptr background_image) { background_image_ = background_image; } - inline void set_emoji_collection(std::shared_ptr emoji_collection) { emoji_collection_ = emoji_collection; } + inline void set_background_image(std::shared_ptr background_image) { + background_image_ = background_image; + } + inline void set_emoji_collection(std::shared_ptr emoji_collection) { + emoji_collection_ = emoji_collection; + } inline void set_text_font(std::shared_ptr text_font) { text_font_ = text_font; } inline void set_icon_font(std::shared_ptr icon_font) { icon_font_ = icon_font; } - inline void set_large_icon_font(std::shared_ptr large_icon_font) { large_icon_font_ = large_icon_font; } + inline void set_large_icon_font(std::shared_ptr large_icon_font) { + large_icon_font_ = large_icon_font; + } + inline void set_emoji_font(std::shared_ptr emoji_font) { emoji_font_ = emoji_font; } private: int spacing_ = 2; @@ -70,12 +83,12 @@ private: std::shared_ptr text_font_ = nullptr; std::shared_ptr icon_font_ = nullptr; std::shared_ptr large_icon_font_ = nullptr; + std::shared_ptr emoji_font_ = nullptr; // Emoji collection std::shared_ptr emoji_collection_ = nullptr; }; - class LvglThemeManager { public: static LvglThemeManager& GetInstance() { diff --git a/main/display/oled_display.cc b/main/display/oled_display.cc index 2083a504..70849bd8 100644 --- a/main/display/oled_display.cc +++ b/main/display/oled_display.cc @@ -1,36 +1,40 @@ #include "oled_display.h" #include "assets/lang_config.h" -#include "lvgl_theme.h" #include "lvgl_font.h" +#include "lvgl_theme.h" -#include #include +#include -#include #include +#include #include -#include +#include +#include #define TAG "OledDisplay" LV_FONT_DECLARE(BUILTIN_TEXT_FONT); LV_FONT_DECLARE(BUILTIN_ICON_FONT); -LV_FONT_DECLARE(font_awesome_30_1); +LV_FONT_DECLARE(font_material_symbols_30_1); +LV_FONT_DECLARE(font_noto_emoji_30_1); OledDisplay::OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, - int width, int height, bool mirror_x, bool mirror_y) + int width, int height, bool mirror_x, bool mirror_y) : panel_io_(panel_io), panel_(panel) { width_ = width; height_ = height; auto text_font = std::make_shared(&BUILTIN_TEXT_FONT); auto icon_font = std::make_shared(&BUILTIN_ICON_FONT); - auto large_icon_font = std::make_shared(&font_awesome_30_1); - + auto large_icon_font = std::make_shared(&font_material_symbols_30_1); + auto emoji_font = std::make_shared(&font_noto_emoji_30_1); + auto dark_theme = new LvglTheme("dark"); dark_theme->set_text_font(text_font); dark_theme->set_icon_font(icon_font); dark_theme->set_large_icon_font(large_icon_font); + dark_theme->set_emoji_font(emoji_font); auto& theme_manager = LvglThemeManager::GetInstance(); theme_manager.RegisterTheme("dark", dark_theme); @@ -56,18 +60,20 @@ OledDisplay::OledDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handl .hres = static_cast(width_), .vres = static_cast(height_), .monochrome = true, - .rotation = { - .swap_xy = false, - .mirror_x = mirror_x, - .mirror_y = mirror_y, - }, - .flags = { - .buff_dma = 1, - .buff_spiram = 0, - .sw_rotate = 0, - .full_refresh = 0, - .direct_mode = 0, - }, + .rotation = + { + .swap_xy = false, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + }, + .flags = + { + .buff_dma = 1, + .buff_spiram = 0, + .sw_rotate = 0, + .full_refresh = 0, + .direct_mode = 0, + }, }; display_ = lvgl_port_add_disp(&display_cfg); @@ -86,7 +92,7 @@ void OledDisplay::SetupUI() { ESP_LOGW(TAG, "SetupUI() called multiple times, skipping duplicate call"); return; } - + Display::SetupUI(); // Mark SetupUI as called if (height_ == 64) { SetupUI_128x64(); @@ -135,13 +141,9 @@ OledDisplay::~OledDisplay() { lvgl_port_deinit(); } -bool OledDisplay::Lock(int timeout_ms) { - return lvgl_port_lock(timeout_ms); -} +bool OledDisplay::Lock(int timeout_ms) { return lvgl_port_lock(timeout_ms); } -void OledDisplay::Unlock() { - lvgl_port_unlock(); -} +void OledDisplay::Unlock() { lvgl_port_unlock(); } void OledDisplay::SetChatMessage(const char* role, const char* content) { DisplayLockGuard lock(this); @@ -153,6 +155,7 @@ void OledDisplay::SetChatMessage(const char* role, const char* content) { std::string content_str = content; std::replace(content_str.begin(), content_str.end(), '\n', ' '); + lv_anim_delete(chat_message_label_, nullptr); if (content_right_ == nullptr) { lv_label_set_text(chat_message_label_, content_str.c_str()); } else { @@ -193,7 +196,8 @@ void OledDisplay::SetupUI_128x64() { lv_obj_set_style_border_width(top_bar_, 0, 0); lv_obj_set_style_pad_all(top_bar_, 0, 0); lv_obj_set_flex_flow(top_bar_, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); lv_obj_set_scrollbar_mode(top_bar_, LV_SCROLLBAR_MODE_OFF); network_label_ = lv_label_create(top_bar_); @@ -206,7 +210,8 @@ void OledDisplay::SetupUI_128x64() { lv_obj_set_style_border_width(right_icons, 0, 0); lv_obj_set_style_pad_all(right_icons, 0, 0); lv_obj_set_flex_flow(right_icons, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); mute_label_ = lv_label_create(right_icons); lv_label_set_text(mute_label_, ""); @@ -225,7 +230,7 @@ void OledDisplay::SetupUI_128x64() { lv_obj_set_style_pad_all(status_bar_, 0, 0); lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF); lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0); // Use absolute positioning - lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_ + lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_ notification_label_ = lv_label_create(status_bar_); lv_obj_set_width(notification_label_, LV_HOR_RES); @@ -258,7 +263,7 @@ void OledDisplay::SetupUI_128x64() { emotion_label_ = lv_label_create(content_left_); lv_obj_set_style_text_font(emotion_label_, large_icon_font, 0); - lv_label_set_text(emotion_label_, FONT_AWESOME_MICROCHIP_AI); + lv_label_set_text(emotion_label_, MATERIAL_SYMBOLS_ROBOT_2); lv_obj_center(emotion_label_); lv_obj_set_style_pad_top(emotion_label_, 8, 0); @@ -282,7 +287,8 @@ void OledDisplay::SetupUI_128x64() { lv_anim_set_delay(&a, 1000); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN); - lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN); + lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), + LV_PART_MAIN); low_battery_popup_ = lv_obj_create(screen); lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF); @@ -325,7 +331,7 @@ void OledDisplay::SetupUI_128x32() { emotion_label_ = lv_label_create(content_); lv_obj_set_style_text_font(emotion_label_, large_icon_font, 0); - lv_label_set_text(emotion_label_, FONT_AWESOME_MICROCHIP_AI); + lv_label_set_text(emotion_label_, MATERIAL_SYMBOLS_ROBOT_2); lv_obj_center(emotion_label_); /* Right side */ @@ -381,19 +387,28 @@ void OledDisplay::SetupUI_128x32() { lv_anim_set_delay(&a, 1000); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN); - lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN); + lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), + LV_PART_MAIN); } void OledDisplay::SetEmotion(const char* emotion) { - const char* utf8 = font_awesome_get_utf8(emotion); + auto lvgl_theme = static_cast(current_theme_); + const char* utf8 = noto_emoji_get_utf8(emotion); + const lv_font_t* emotion_font = lvgl_theme->emoji_font()->font(); + if (utf8 == nullptr) { + utf8 = material_symbols_get_utf8(emotion); + emotion_font = lvgl_theme->large_icon_font()->font(); + } DisplayLockGuard lock(this); if (emotion_label_ == nullptr) { return; } if (utf8 != nullptr) { + lv_obj_set_style_text_font(emotion_label_, emotion_font, 0); lv_label_set_text(emotion_label_, utf8); } else { - lv_label_set_text(emotion_label_, FONT_AWESOME_NEUTRAL); + lv_obj_set_style_text_font(emotion_label_, lvgl_theme->emoji_font()->font(), 0); + lv_label_set_text(emotion_label_, NOTO_EMOJI_NEUTRAL); } } diff --git a/main/display/text_glyph.cc b/main/display/text_glyph.cc new file mode 100644 index 00000000..4bec2765 --- /dev/null +++ b/main/display/text_glyph.cc @@ -0,0 +1,3 @@ +#include "text_glyph.h" + +bool TextGlyphStorageUsesPsram() { return heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0; } diff --git a/main/display/text_glyph.h b/main/display/text_glyph.h new file mode 100644 index 00000000..258d054b --- /dev/null +++ b/main/display/text_glyph.h @@ -0,0 +1,67 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +bool TextGlyphStorageUsesPsram(); + +template +class TextGlyphAllocator { +public: + using value_type = T; + using is_always_equal = std::true_type; + + TextGlyphAllocator() noexcept = default; + + template + TextGlyphAllocator(const TextGlyphAllocator&) noexcept {} + + T* allocate(size_t count) { + if (count == 0) { + return nullptr; + } + if (count > std::numeric_limits::max() / sizeof(T)) { + std::abort(); + } + uint32_t caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; + if (TextGlyphStorageUsesPsram()) { + caps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + } + auto ptr = static_cast(heap_caps_malloc(count * sizeof(T), caps)); + if (ptr == nullptr) { + std::abort(); + } + return ptr; + } + + void deallocate(T* ptr, size_t) noexcept { heap_caps_free(ptr); } +}; + +template +bool operator==(const TextGlyphAllocator&, const TextGlyphAllocator&) { + return true; +} + +template +bool operator!=(const TextGlyphAllocator&, const TextGlyphAllocator&) { + return false; +} + +template +using TextGlyphVector = std::vector>; + +struct TextGlyph { + uint32_t codepoint = 0; + uint32_t adv_w = 0; + uint16_t box_w = 0; + uint16_t box_h = 0; + int16_t ofs_x = 0; + int16_t ofs_y = 0; + TextGlyphVector bitmap; +}; diff --git a/main/idf_component.yml b/main/idf_component.yml index 997d37c7..0de1a371 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -30,7 +30,7 @@ dependencies: version: ==0.6.0 rules: - if: target not in [esp32] - 78/xiaozhi-fonts: ~1.6.0 + 78/xiaozhi-fonts: ~2.0.0 espressif/led_strip: ~3.0.2 espressif/esp_codec_dev: ~1.5.6 espressif/esp-sr: ~2.4.6 diff --git a/main/protocols/mqtt_protocol.cc b/main/protocols/mqtt_protocol.cc index b769e66b..9809d5ae 100644 --- a/main/protocols/mqtt_protocol.cc +++ b/main/protocols/mqtt_protocol.cc @@ -1,11 +1,11 @@ #include "mqtt_protocol.h" -#include "board.h" #include "application.h" +#include "board.h" #include "settings.h" #include -#include #include +#include #include "assets/lang_config.h" #define TAG "MQTT" @@ -15,19 +15,20 @@ MqttProtocol::MqttProtocol() { // Initialize reconnect timer esp_timer_create_args_t reconnect_timer_args = { - .callback = [](void* arg) { - MqttProtocol* protocol = (MqttProtocol*)arg; - auto& app = Application::GetInstance(); - if (app.GetDeviceState() == kDeviceStateIdle) { - ESP_LOGI(TAG, "Reconnecting to MQTT server"); - auto alive = protocol->alive_; // Capture alive flag - app.Schedule([protocol, alive]() { - if (*alive) { - protocol->StartMqttClient(false); - } - }); - } - }, + .callback = + [](void* arg) { + MqttProtocol* protocol = (MqttProtocol*)arg; + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateIdle) { + ESP_LOGI(TAG, "Reconnecting to MQTT server"); + auto alive = protocol->alive_; // Capture alive flag + app.Schedule([protocol, alive]() { + if (*alive) { + protocol->StartMqttClient(false); + } + }); + } + }, .arg = this, }; esp_timer_create(&reconnect_timer_args, &reconnect_timer_); @@ -35,19 +36,21 @@ MqttProtocol::MqttProtocol() { MqttProtocol::~MqttProtocol() { ESP_LOGI(TAG, "MqttProtocol deinit"); - + // Mark as dead first to prevent any pending scheduled tasks from executing *alive_ = false; - + if (reconnect_timer_ != nullptr) { esp_timer_stop(reconnect_timer_); esp_timer_delete(reconnect_timer_); } + std::unique_ptr udp; { std::lock_guard lock(channel_mutex_); - udp_.reset(); + udp = std::move(udp_); } + udp.reset(); mqtt_.reset(); { @@ -57,15 +60,13 @@ MqttProtocol::~MqttProtocol() { aes_key_id_ = PSA_KEY_ID_NULL; } } - + if (event_group_handle_ != nullptr) { vEventGroupDelete(event_group_handle_); } } -bool MqttProtocol::Start() { - return StartMqttClient(false); -} +bool MqttProtocol::Start() { return StartMqttClient(false); } bool MqttProtocol::StartMqttClient(bool report_error) { if (mqtt_ != nullptr) { @@ -97,7 +98,8 @@ bool MqttProtocol::StartMqttClient(bool report_error) { if (on_disconnected_ != nullptr) { on_disconnected_(); } - ESP_LOGI(TAG, "MQTT disconnected, schedule reconnect in %d seconds", MQTT_RECONNECT_INTERVAL_MS / 1000); + ESP_LOGI(TAG, "MQTT disconnected, schedule reconnect in %d seconds", + MQTT_RECONNECT_INTERVAL_MS / 1000); esp_timer_start_once(reconnect_timer_, MQTT_RECONNECT_INTERVAL_MS * 1000); }); @@ -125,7 +127,8 @@ bool MqttProtocol::StartMqttClient(bool report_error) { ParseServerHello(root); } else if (strcmp(type->valuestring, "goodbye") == 0) { auto session_id = cJSON_GetObjectItem(root, "session_id"); - ESP_LOGI(TAG, "Received goodbye message, session_id: %s", cJSON_IsString(session_id) ? session_id->valuestring : "null"); + ESP_LOGI(TAG, "Received goodbye message, session_id: %s", + cJSON_IsString(session_id) ? session_id->valuestring : "null"); if (cJSON_IsString(session_id) && session_id_ == session_id->valuestring) { auto alive = alive_; // Capture alive flag Application::GetInstance().Schedule([this, alive]() { @@ -198,8 +201,8 @@ bool MqttProtocol::SendAudio(std::unique_ptr packet) { encrypted.resize(aes_nonce_.size() + packet->payload.size()); memcpy(encrypted.data(), nonce.data(), nonce.size()); - if (!CryptAesCtr(reinterpret_cast(packet->payload.data()), packet->payload.size(), - reinterpret_cast(nonce.data()), + if (!CryptAesCtr(reinterpret_cast(packet->payload.data()), + packet->payload.size(), reinterpret_cast(nonce.data()), reinterpret_cast(&encrypted[nonce.size()]))) { ESP_LOGE(TAG, "Failed to encrypt audio data"); return false; @@ -209,10 +212,12 @@ bool MqttProtocol::SendAudio(std::unique_ptr packet) { } void MqttProtocol::CloseAudioChannel(bool send_goodbye) { + std::unique_ptr udp; { std::lock_guard lock(channel_mutex_); - udp_.reset(); + udp = std::move(udp_); } + udp.reset(); ESP_LOGI(TAG, "Closing audio channel, send_goodbye: %d", send_goodbye); @@ -249,7 +254,8 @@ bool MqttProtocol::OpenAudioChannel() { } // 等待服务器响应 - EventBits_t bits = xEventGroupWaitBits(event_group_handle_, MQTT_PROTOCOL_SERVER_HELLO_EVENT, pdTRUE, pdFALSE, pdMS_TO_TICKS(10000)); + EventBits_t bits = xEventGroupWaitBits(event_group_handle_, MQTT_PROTOCOL_SERVER_HELLO_EVENT, + pdTRUE, pdFALSE, pdMS_TO_TICKS(10000)); if (!(bits & MQTT_PROTOCOL_SERVER_HELLO_EVENT)) { ESP_LOGE(TAG, "Failed to receive server hello"); SetError(Lang::Strings::SERVER_TIMEOUT); @@ -284,8 +290,7 @@ bool MqttProtocol::OpenAudioChannel() { sequence = ntohl(sequence); if (data.size() != kAudioHeaderSize + payload_len) { ESP_LOGE(TAG, "Audio payload length mismatch: header=%u, datagram=%zu", - static_cast(payload_len), - data.size() - kAudioHeaderSize); + static_cast(payload_len), data.size() - kAudioHeaderSize); return; } @@ -310,7 +315,8 @@ bool MqttProtocol::OpenAudioChannel() { packet->frame_duration = server_frame_duration_; packet->timestamp = timestamp; packet->payload.resize(decrypted_size); - if (!CryptAesCtr(encrypted, decrypted_size, nonce, reinterpret_cast(packet->payload.data()))) { + if (!CryptAesCtr(encrypted, decrypted_size, nonce, + reinterpret_cast(packet->payload.data()))) { ESP_LOGE(TAG, "Failed to decrypt audio data"); return; } @@ -354,6 +360,7 @@ std::string MqttProtocol::GetHelloMessage() { #endif cJSON_AddBoolToObject(features, "mcp", true); cJSON_AddItemToObject(root, "features", features); + AddTextFontCapabilities(root); cJSON* audio_params = cJSON_CreateObject(); cJSON_AddStringToObject(audio_params, "format", "opus"); cJSON_AddNumberToObject(audio_params, "sample_rate", 16000); @@ -411,12 +418,13 @@ void MqttProtocol::ParseServerHello(const cJSON* root) { const int udp_port = port->valueint; // auto encryption = cJSON_GetObjectItem(udp, "encryption")->valuestring; - // ESP_LOGI(TAG, "UDP server: %s, port: %d, encryption: %s", udp_server_.c_str(), udp_port_, encryption); + // ESP_LOGI(TAG, "UDP server: %s, port: %d, encryption: %s", udp_server_.c_str(), udp_port_, + // encryption); std::string aes_nonce; std::string aes_key; if (!DecodeHexString(nonce_item->valuestring, aes_nonce) || - !DecodeHexString(key_item->valuestring, aes_key) || - aes_nonce.size() != 16 || aes_key.size() != 16) { + !DecodeHexString(key_item->valuestring, aes_key) || aes_nonce.size() != 16 || + aes_key.size() != 16) { ESP_LOGE(TAG, "Invalid AES key or nonce length"); return; } @@ -458,9 +466,11 @@ void MqttProtocol::ParseServerHello(const cJSON* root) { xEventGroupSetBits(event_group_handle_, MQTT_PROTOCOL_SERVER_HELLO_EVENT); } -bool MqttProtocol::CryptAesCtr(const uint8_t* input, size_t input_size, const uint8_t* nonce, uint8_t* output) { +bool MqttProtocol::CryptAesCtr(const uint8_t* input, size_t input_size, const uint8_t* nonce, + uint8_t* output) { std::lock_guard lock(crypto_mutex_); - if (aes_key_id_ == PSA_KEY_ID_NULL || input == nullptr || nonce == nullptr || output == nullptr) { + if (aes_key_id_ == PSA_KEY_ID_NULL || input == nullptr || nonce == nullptr || + output == nullptr) { return false; } @@ -490,9 +500,12 @@ bool MqttProtocol::CryptAesCtr(const uint8_t* input, size_t input_size, const ui } static inline int CharToHex(char c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; return -1; } diff --git a/main/protocols/protocol.cc b/main/protocols/protocol.cc index 470cc915..f57720df 100644 --- a/main/protocols/protocol.cc +++ b/main/protocols/protocol.cc @@ -1,14 +1,34 @@ #include "protocol.h" +#include "assets.h" #include #define TAG "Protocol" +void Protocol::AddTextFontCapabilities(cJSON* root) { + auto capability = Assets::GetInstance().text_font_capability(); + cJSON* features = cJSON_GetObjectItem(root, "features"); + if (cJSON_IsObject(features)) { + cJSON_AddBoolToObject(features, "glyph_push", capability.glyph_push); + } + + if (!capability.glyph_push) { + return; + } + cJSON* font = cJSON_CreateObject(); + cJSON_AddStringToObject(font, "bundle", capability.bundle.c_str()); + cJSON_AddStringToObject(font, "charset", capability.charset.c_str()); + cJSON_AddNumberToObject(font, "size", capability.size); + cJSON_AddNumberToObject(font, "bpp", capability.bpp); + cJSON_AddItemToObject(root, "text_font", font); +} + void Protocol::OnIncomingJson(std::function callback) { on_incoming_json_ = callback; } -void Protocol::OnIncomingAudio(std::function packet)> callback) { +void Protocol::OnIncomingAudio( + std::function packet)> callback) { on_incoming_audio_ = callback; } @@ -24,13 +44,9 @@ void Protocol::OnNetworkError(std::function ca on_network_error_ = callback; } -void Protocol::OnConnected(std::function callback) { - on_connected_ = callback; -} +void Protocol::OnConnected(std::function callback) { on_connected_ = callback; } -void Protocol::OnDisconnected(std::function callback) { - on_disconnected_ = callback; -} +void Protocol::OnDisconnected(std::function callback) { on_disconnected_ = callback; } void Protocol::SetError(const std::string& message) { error_occurred_ = true; @@ -49,8 +65,9 @@ void Protocol::SendAbortSpeaking(AbortReason reason) { } void Protocol::SendWakeWordDetected(const std::string& wake_word) { - std::string json = "{\"session_id\":\"" + session_id_ + - "\",\"type\":\"listen\",\"state\":\"detect\",\"text\":\"" + wake_word + "\"}"; + std::string json = "{\"session_id\":\"" + session_id_ + + "\",\"type\":\"listen\",\"state\":\"detect\",\"text\":\"" + wake_word + + "\"}"; SendText(json); } @@ -69,12 +86,14 @@ void Protocol::SendStartListening(ListeningMode mode) { } void Protocol::SendStopListening() { - std::string message = "{\"session_id\":\"" + session_id_ + "\",\"type\":\"listen\",\"state\":\"stop\"}"; + std::string message = + "{\"session_id\":\"" + session_id_ + "\",\"type\":\"listen\",\"state\":\"stop\"}"; SendText(message); } void Protocol::SendMcpMessage(const std::string& payload) { - std::string message = "{\"session_id\":\"" + session_id_ + "\",\"type\":\"mcp\",\"payload\":" + payload + "}"; + std::string message = + "{\"session_id\":\"" + session_id_ + "\",\"type\":\"mcp\",\"payload\":" + payload + "}"; SendText(message); } diff --git a/main/protocols/protocol.h b/main/protocols/protocol.h index 28ef604f..988f1fe0 100644 --- a/main/protocols/protocol.h +++ b/main/protocols/protocol.h @@ -2,9 +2,9 @@ #define PROTOCOL_H #include -#include -#include #include +#include +#include #include struct AudioStreamPacket { @@ -30,30 +30,21 @@ struct BinaryProtocol3 { uint8_t payload[]; } __attribute__((packed)); -enum AbortReason { - kAbortReasonNone, - kAbortReasonWakeWordDetected -}; +enum AbortReason { kAbortReasonNone, kAbortReasonWakeWordDetected }; enum ListeningMode { kListeningModeAutoStop, kListeningModeManualStop, - kListeningModeRealtime // 需要 AEC 支持 + kListeningModeRealtime // 需要 AEC 支持 }; class Protocol { public: virtual ~Protocol() = default; - inline int server_sample_rate() const { - return server_sample_rate_; - } - inline int server_frame_duration() const { - return server_frame_duration_; - } - inline const std::string& session_id() const { - return session_id_; - } + inline int server_sample_rate() const { return server_sample_rate_; } + inline int server_frame_duration() const { return server_frame_duration_; } + inline const std::string& session_id() const { return session_id_; } void OnIncomingAudio(std::function packet)> callback); void OnIncomingJson(std::function callback); @@ -92,7 +83,7 @@ protected: virtual bool SendText(const std::string& text) = 0; virtual void SetError(const std::string& message); virtual bool IsTimeout() const; + static void AddTextFontCapabilities(cJSON* root); }; -#endif // PROTOCOL_H - +#endif // PROTOCOL_H diff --git a/main/protocols/text_glyph_payload.cc b/main/protocols/text_glyph_payload.cc new file mode 100644 index 00000000..21ab7c46 --- /dev/null +++ b/main/protocols/text_glyph_payload.cc @@ -0,0 +1,102 @@ +#include "text_glyph_payload.h" +#include "assets.h" + +#include +#include + +#include +#include + +#define TAG "TextGlyphPayload" + +namespace TextGlyphPayload { + +bool Parse(const cJSON* root, std::vector& result, uint8_t& bpp) { + auto payload = cJSON_GetObjectItem(root, "glyph_push"); + if (!cJSON_IsObject(payload)) { + return true; + } + auto version = cJSON_GetObjectItem(payload, "v"); + auto bundle = cJSON_GetObjectItem(payload, "bundle"); + auto size = cJSON_GetObjectItem(payload, "size"); + auto wire_bpp = cJSON_GetObjectItem(payload, "bpp"); + auto glyphs = cJSON_GetObjectItem(payload, "glyphs"); + auto capability = Assets::GetInstance().text_font_capability(); + if (!capability.glyph_push || !cJSON_IsNumber(version) || version->valueint != 1 || + !cJSON_IsString(bundle) || capability.bundle != bundle->valuestring || + !cJSON_IsNumber(size) || size->valuedouble != capability.size || + !cJSON_IsNumber(wire_bpp) || wire_bpp->valuedouble != capability.bpp || + !cJSON_IsArray(glyphs) || cJSON_GetArraySize(glyphs) > 64) { + ESP_LOGW(TAG, "Rejected incompatible glyph payload"); + return false; + } + + const uint8_t parsed_bpp = static_cast(wire_bpp->valueint); + std::vector parsed; + parsed.reserve(cJSON_GetArraySize(glyphs)); + size_t total_bitmap_bytes = 0; + cJSON* item = nullptr; + cJSON_ArrayForEach (item, glyphs) { + auto codepoint = cJSON_GetObjectItem(item, "codepoint"); + auto adv_w = cJSON_GetObjectItem(item, "adv_w"); + auto box_w = cJSON_GetObjectItem(item, "box_w"); + auto box_h = cJSON_GetObjectItem(item, "box_h"); + auto ofs_x = cJSON_GetObjectItem(item, "ofs_x"); + auto ofs_y = cJSON_GetObjectItem(item, "ofs_y"); + auto bitmap = cJSON_GetObjectItem(item, "bitmap"); + if (!cJSON_IsNumber(codepoint) || codepoint->valuedouble <= 0 || + codepoint->valuedouble > 0x10FFFF || !cJSON_IsNumber(adv_w) || adv_w->valuedouble < 0 || + adv_w->valuedouble > UINT32_MAX || !cJSON_IsNumber(box_w) || box_w->valueint < 0 || + box_w->valueint > 64 || !cJSON_IsNumber(box_h) || box_h->valueint < 0 || + box_h->valueint > 64 || !cJSON_IsNumber(ofs_x) || ofs_x->valueint < INT16_MIN || + ofs_x->valueint > INT16_MAX || !cJSON_IsNumber(ofs_y) || ofs_y->valueint < INT16_MIN || + ofs_y->valueint > INT16_MAX || !cJSON_IsString(bitmap)) { + ESP_LOGW(TAG, "Rejected malformed glyph"); + return false; + } + + const size_t expected = + (static_cast(box_w->valueint) * box_h->valueint * parsed_bpp + 7) / 8; + total_bitmap_bytes += expected; + if (total_bitmap_bytes > 64 * 1024) { + ESP_LOGW(TAG, "Rejected oversized glyph payload"); + return false; + } + + TextGlyph glyph; + glyph.codepoint = static_cast(codepoint->valuedouble); + glyph.adv_w = static_cast(adv_w->valuedouble); + glyph.box_w = static_cast(box_w->valueint); + glyph.box_h = static_cast(box_h->valueint); + glyph.ofs_x = static_cast(ofs_x->valueint); + glyph.ofs_y = static_cast(ofs_y->valueint); + glyph.bitmap.resize(expected); + const size_t encoded_length = strlen(bitmap->valuestring); + if (encoded_length > ((expected + 2) / 3) * 4 + 4) { + ESP_LOGW(TAG, "Rejected oversized base64 bitmap"); + return false; + } + if (expected == 0) { + if (encoded_length != 0) { + return false; + } + parsed.push_back(std::move(glyph)); + continue; + } + size_t decoded = 0; + int rc = mbedtls_base64_decode(glyph.bitmap.data(), glyph.bitmap.size(), &decoded, + reinterpret_cast(bitmap->valuestring), + encoded_length); + if (rc != 0 || decoded != expected) { + ESP_LOGW(TAG, "Rejected invalid bitmap for U+%04lX", + static_cast(glyph.codepoint)); + return false; + } + parsed.push_back(std::move(glyph)); + } + result = std::move(parsed); + bpp = parsed_bpp; + return true; +} + +} // namespace TextGlyphPayload diff --git a/main/protocols/text_glyph_payload.h b/main/protocols/text_glyph_payload.h new file mode 100644 index 00000000..5c869b67 --- /dev/null +++ b/main/protocols/text_glyph_payload.h @@ -0,0 +1,14 @@ +#pragma once + +#include "display/text_glyph.h" + +#include + +#include +#include + +namespace TextGlyphPayload { + +bool Parse(const cJSON* root, std::vector& result, uint8_t& bpp); + +} // namespace TextGlyphPayload diff --git a/main/protocols/websocket_protocol.cc b/main/protocols/websocket_protocol.cc index 4b74a5e2..9586c6d0 100644 --- a/main/protocols/websocket_protocol.cc +++ b/main/protocols/websocket_protocol.cc @@ -1,24 +1,20 @@ #include "websocket_protocol.h" -#include "board.h" -#include "system_info.h" #include "application.h" +#include "board.h" #include "settings.h" +#include "system_info.h" -#include -#include #include #include +#include +#include #include "assets/lang_config.h" #define TAG "WS" -WebsocketProtocol::WebsocketProtocol() { - event_group_handle_ = xEventGroupCreate(); -} +WebsocketProtocol::WebsocketProtocol() { event_group_handle_ = xEventGroupCreate(); } -WebsocketProtocol::~WebsocketProtocol() { - vEventGroupDelete(event_group_handle_); -} +WebsocketProtocol::~WebsocketProtocol() { vEventGroupDelete(event_group_handle_); } bool WebsocketProtocol::Start() { // Only connect to server when audio channel is needed @@ -123,8 +119,7 @@ bool WebsocketProtocol::OpenAudioChannel() { .sample_rate = server_sample_rate_, .frame_duration = server_frame_duration_, .timestamp = bp2->timestamp, - .payload = std::vector(payload, payload + bp2->payload_size) - })); + .payload = std::vector(payload, payload + bp2->payload_size)})); } else if (version_ == 3) { BinaryProtocol3* bp3 = (BinaryProtocol3*)data; bp3->type = bp3->type; @@ -134,15 +129,13 @@ bool WebsocketProtocol::OpenAudioChannel() { .sample_rate = server_sample_rate_, .frame_duration = server_frame_duration_, .timestamp = 0, - .payload = std::vector(payload, payload + bp3->payload_size) - })); + .payload = std::vector(payload, payload + bp3->payload_size)})); } else { on_incoming_audio_(std::make_unique(AudioStreamPacket{ .sample_rate = server_sample_rate_, .frame_duration = server_frame_duration_, .timestamp = 0, - .payload = std::vector((uint8_t*)data, (uint8_t*)data + len) - })); + .payload = std::vector((uint8_t*)data, (uint8_t*)data + len)})); } } } else { @@ -186,7 +179,9 @@ bool WebsocketProtocol::OpenAudioChannel() { } // Wait for server hello - EventBits_t bits = xEventGroupWaitBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT, pdTRUE, pdFALSE, pdMS_TO_TICKS(10000)); + EventBits_t bits = + xEventGroupWaitBits(event_group_handle_, WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT, pdTRUE, + pdFALSE, pdMS_TO_TICKS(10000)); if (!(bits & WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT)) { ESP_LOGE(TAG, "Failed to receive server hello"); SetError(Lang::Strings::SERVER_TIMEOUT); @@ -211,6 +206,7 @@ std::string WebsocketProtocol::GetHelloMessage() { #endif cJSON_AddBoolToObject(features, "mcp", true); cJSON_AddItemToObject(root, "features", features); + AddTextFontCapabilities(root); cJSON_AddStringToObject(root, "transport", "websocket"); cJSON* audio_params = cJSON_CreateObject(); cJSON_AddStringToObject(audio_params, "format", "opus"); diff --git a/scripts/build_default_assets.py b/scripts/build_default_assets.py index db6b65d2..6d4cacb3 100755 --- a/scripts/build_default_assets.py +++ b/scripts/build_default_assets.py @@ -13,6 +13,7 @@ Usage: import argparse import io import os +import re import shutil import sys import json @@ -273,7 +274,8 @@ def process_extra_files(extra_files_dir, assets_dir): return extra_files_list -def generate_index_json(assets_dir, srmodels, text_font, emoji_collection, extra_files=None, multinet_model_info=None): +def generate_index_json(assets_dir, srmodels, text_font, emoji_collection, extra_files=None, + multinet_model_info=None, font_bundle_id=None): """Generate index.json file""" index_data = { "version": 1 @@ -283,7 +285,22 @@ def generate_index_json(assets_dir, srmodels, text_font, emoji_collection, extra index_data["srmodels"] = srmodels if text_font: + if not font_bundle_id: + raise ValueError("font_bundle_id is required when packaging a text font") index_data["text_font"] = text_font + stem = os.path.splitext(text_font)[0] + marker = '_common_' + if marker in stem: + match = re.fullmatch(r"(\d+)_(\d+)", stem.split(marker, 1)[1]) + if not match: + raise ValueError(f"invalid Noto text font name: {text_font}") + text_font_meta = { + "charset": "common", + "size": int(match.group(1)), + "bpp": int(match.group(2)), + "bundle": font_bundle_id, + } + index_data["text_font_meta"] = text_font_meta if emoji_collection: index_data["emoji_collection"] = emoji_collection @@ -663,7 +680,7 @@ def get_multinet_model_paths(model_names, esp_sr_model_path): return valid_paths -def get_text_font_path(builtin_text_font, xiaozhi_fonts_path): +def get_text_font_path(builtin_text_font, noto_fonts_path): """ Get the text font path if needed Returns the font file path or None if no font is needed @@ -671,13 +688,9 @@ def get_text_font_path(builtin_text_font, xiaozhi_fonts_path): if not builtin_text_font or 'basic' not in builtin_text_font: return None - # Convert from basic to common font name - # e.g., font_puhui_basic_16_4 -> font_puhui_common_16_4.bin - if builtin_text_font.startswith('font_noto_'): - font_name = builtin_text_font.replace('basic', 'qwen') + '.bin' - else: - font_name = builtin_text_font.replace('basic', 'common') + '.bin' - font_path = os.path.join(xiaozhi_fonts_path, 'cbin', font_name) + # basic is linked into firmware; common is loaded from the assets partition. + font_name = builtin_text_font.replace('_basic_', '_common_') + '.bin' + font_path = os.path.join(noto_fonts_path, 'cbin', font_name) if os.path.exists(font_path): return font_path @@ -686,14 +699,13 @@ def get_text_font_path(builtin_text_font, xiaozhi_fonts_path): return None -def get_emoji_collection_path(default_emoji_collection, xiaozhi_fonts_path, project_root=None): +def get_emoji_collection_path(default_emoji_collection, noto_fonts_path, project_root=None): """ Get the emoji collection path if needed Returns the emoji directory path or None if no emoji collection is needed Supports: - - PNG emoji collections from xiaozhi-fonts (e.g., emojis_32, twemoji_64) - - GIF emoji collections from xiaozhi-fonts (e.g., noto-emoji_128, noto-emoji_64) + - PNG emoji collections from noto-fonts (e.g., noto-color-emoji_32) - Otto GIF emoji collection (otto-gif) """ if not default_emoji_collection: @@ -713,13 +725,13 @@ def get_emoji_collection_path(default_emoji_collection, xiaozhi_fonts_path, proj print("Warning: project_root not provided, cannot locate otto-gif collection") return None - # Try PNG emoji collections first (e.g., emojis_32, twemoji_64) - emoji_path = os.path.join(xiaozhi_fonts_path, 'png', default_emoji_collection) + # Try PNG emoji collections first. + emoji_path = os.path.join(noto_fonts_path, 'png', default_emoji_collection) if os.path.exists(emoji_path): return emoji_path # Try GIF emoji collections (e.g., noto-emoji_128, noto-emoji_64, noto-emoji_32) - emoji_path = os.path.join(xiaozhi_fonts_path, 'gif', default_emoji_collection) + emoji_path = os.path.join(noto_fonts_path, 'gif', default_emoji_collection) if os.path.exists(emoji_path): return emoji_path @@ -727,7 +739,9 @@ def get_emoji_collection_path(default_emoji_collection, xiaozhi_fonts_path, proj return None -def build_assets_integrated(wakenet_model_paths, multinet_model_paths, text_font_path, emoji_collection_path, extra_files_path, output_path, multinet_model_info=None): +def build_assets_integrated(wakenet_model_paths, multinet_model_paths, text_font_path, + emoji_collection_path, extra_files_path, output_path, + multinet_model_info=None, font_bundle_id=None): """ Build assets using integrated functions (no external dependencies) """ @@ -751,7 +765,8 @@ def build_assets_integrated(wakenet_model_paths, multinet_model_paths, text_font extra_files = process_extra_files(extra_files_path, assets_dir) if extra_files_path else None # Generate index.json - generate_index_json(assets_dir, srmodels, text_font, emoji_collection, extra_files, multinet_model_info) + generate_index_json(assets_dir, srmodels, text_font, emoji_collection, extra_files, + multinet_model_info, font_bundle_id) # Generate config.json for packing config_path = generate_config_json(temp_build_dir, assets_dir) @@ -791,17 +806,17 @@ def build_assets_integrated(wakenet_model_paths, multinet_model_paths, text_font def main(): parser = argparse.ArgumentParser(description='Build default assets based on configuration') parser.add_argument('--sdkconfig', required=True, help='Path to sdkconfig file') - parser.add_argument('--builtin_text_font', help='Builtin text font name (e.g., font_puhui_basic_16_4)') - parser.add_argument('--emoji_collection', help='Default emoji collection name (e.g., emojis_32)') + parser.add_argument('--builtin_text_font', help='Builtin text font name (e.g., font_noto_sans_basic_16_4)') + parser.add_argument('--emoji_collection', help='Default emoji collection name (e.g., noto-color-emoji_32)') parser.add_argument('--output', required=True, help='Output path for assets.bin') parser.add_argument('--esp_sr_model_path', help='Path to ESP-SR model directory') - parser.add_argument('--xiaozhi_fonts_path', help='Path to xiaozhi-fonts component directory') + parser.add_argument('--noto_fonts_path', help='Path to noto-fonts component directory') parser.add_argument('--extra_files', help='Path to extra files directory to be included in assets') args = parser.parse_args() # Set default paths if not provided - if not args.esp_sr_model_path or not args.xiaozhi_fonts_path: + if not args.esp_sr_model_path or not args.noto_fonts_path: # Calculate project root from script location script_dir = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(script_dir) @@ -809,8 +824,8 @@ def main(): if not args.esp_sr_model_path: args.esp_sr_model_path = os.path.join(project_root, "managed_components", "espressif__esp-sr", "model") - if not args.xiaozhi_fonts_path: - args.xiaozhi_fonts_path = os.path.join(project_root, "components", "xiaozhi-fonts") + if not args.noto_fonts_path: + args.noto_fonts_path = os.path.join(project_root, "components", "noto-fonts") print("Building default assets...") print(f" sdkconfig: {args.sdkconfig}") @@ -854,13 +869,20 @@ def main(): print(f" multinet models: {', '.join(multinet_model_names)} (will be packaged)") # Get text font path if needed - text_font_path = get_text_font_path(args.builtin_text_font, args.xiaozhi_fonts_path) + text_font_path = get_text_font_path(args.builtin_text_font, args.noto_fonts_path) + font_bundle_id = None + if text_font_path: + manifest_path = os.path.join(args.noto_fonts_path, "manifest.json") + with open(manifest_path, 'r', encoding='utf-8') as f: + font_bundle_id = json.load(f).get("bundle_id") + if not isinstance(font_bundle_id, str) or not font_bundle_id: + raise ValueError("noto-fonts manifest.json must define bundle_id") # Get emoji collection path if needed # Calculate project root from script location for otto-gif support script_dir = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(script_dir) - emoji_collection_path = get_emoji_collection_path(args.emoji_collection, args.xiaozhi_fonts_path, project_root) + emoji_collection_path = get_emoji_collection_path(args.emoji_collection, args.noto_fonts_path, project_root) # Get extra files path if provided extra_files_path = args.extra_files @@ -901,8 +923,9 @@ def main(): return # Build the assets - success = build_assets_integrated(wakenet_model_paths, multinet_model_paths, text_font_path, emoji_collection_path, - extra_files_path, args.output, multinet_model_info) + success = build_assets_integrated( + wakenet_model_paths, multinet_model_paths, text_font_path, emoji_collection_path, + extra_files_path, args.output, multinet_model_info, font_bundle_id) if not success: sys.exit(1) diff --git a/scripts/spiffs_assets/README.md b/scripts/spiffs_assets/README.md index 9cd0f924..ad2a428b 100644 --- a/scripts/spiffs_assets/README.md +++ b/scripts/spiffs_assets/README.md @@ -39,14 +39,14 @@ # 完整参数示例 ./build.py \ --wakenet_model ../../managed_components/espressif__esp-sr/model/wakenet_model/wn9_nihaoxiaozhi_tts \ - --text_font ../../components/xiaozhi-fonts/build/font_puhui_common_20_4.bin \ - --emoji_collection ../../components/xiaozhi-fonts/build/emojis_64/ + --text_font ../../components/noto-fonts/cbin/font_noto_sans_common_20_4.bin \ + --emoji_collection ../../components/noto-fonts/png/noto-color-emoji_64/ # 仅处理字体文件 -./build.py --text_font ../../components/xiaozhi-fonts/build/font_puhui_common_20_4.bin +./build.py --text_font ../../components/noto-fonts/cbin/font_noto_sans_common_20_4.bin # 仅处理表情符号 -./build.py --emoji_collection ../../components/xiaozhi-fonts/build/emojis_64/ +./build.py --emoji_collection ../../components/noto-fonts/png/noto-color-emoji_64/ ``` ## 工作流程 diff --git a/scripts/spiffs_assets/build.py b/scripts/spiffs_assets/build.py index 47069d23..bf030671 100644 --- a/scripts/spiffs_assets/build.py +++ b/scripts/spiffs_assets/build.py @@ -9,8 +9,8 @@ Usage: Example: ./build.py --wakenet_model ../../managed_components/espressif__esp-sr/model/wakenet_model/wn9_nihaoxiaozhi_tts \ - --text_font ../../components/xiaozhi-fonts/build/font_puhui_common_20_4.bin \ - --emoji_collection ../../components/xiaozhi-fonts/build/emojis_64/ + --text_font ../../components/noto-fonts/cbin/font_noto_sans_common_20_4.bin \ + --emoji_collection ../../components/noto-fonts/png/noto-color-emoji_64/ """ import os @@ -397,4 +397,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/scripts/spiffs_assets/build_all.py b/scripts/spiffs_assets/build_all.py index 33efbbcd..80296045 100644 --- a/scripts/spiffs_assets/build_all.py +++ b/scripts/spiffs_assets/build_all.py @@ -8,7 +8,7 @@ This script calls build.py with different combinations of: - emoji_collections And generates assets.bin files with names like: -wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-emojis_32.bin +wn9_nihaoxiaozhi_tts-font_noto_sans_common_20_4-noto-color-emoji_32.bin """ import os @@ -40,11 +40,11 @@ def build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_di cmd.extend(["--wakenet_model", wakenet_path]) if text_font != "none": - text_font_path = os.path.join("../../components/78__xiaozhi-fonts/cbin", f"{text_font}.bin") + text_font_path = os.path.join("../../components/noto-fonts/cbin", f"{text_font}.bin") cmd.extend(["--text_font", text_font_path]) if emoji_collection != "none": - emoji_path = os.path.join("../../components/xiaozhi-fonts/build", emoji_collection) + emoji_path = os.path.join("../../components/noto-fonts/png", emoji_collection) cmd.extend(["--emoji_collection", emoji_path]) print(f"\n正在构建: {wakenet_model}-{text_font}-{emoji_collection}") @@ -87,16 +87,16 @@ def main(): text_fonts = [ "none", - "font_puhui_common_14_1", - "font_puhui_common_16_4", - "font_puhui_common_20_4", - "font_puhui_common_30_4", + "font_noto_sans_common_14_1", + "font_noto_sans_common_16_4", + "font_noto_sans_common_20_4", + "font_noto_sans_common_30_4", ] emoji_collections = [ "none", - "emojis_32", - "emojis_64", + "noto-color-emoji_32", + "noto-color-emoji_64", ] # Get script directory @@ -145,4 +145,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/scripts/tests/test_build_default_assets.py b/scripts/tests/test_build_default_assets.py new file mode 100644 index 00000000..47a5ab53 --- /dev/null +++ b/scripts/tests/test_build_default_assets.py @@ -0,0 +1,42 @@ +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +SPEC = importlib.util.spec_from_file_location( + "build_default_assets", ROOT / "scripts" / "build_default_assets.py" +) +BUILD = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(BUILD) + + +class BuildDefaultAssetsTest(unittest.TestCase): + def test_text_font_metadata_uses_bundle_charset_size_and_bpp(self): + with tempfile.TemporaryDirectory() as directory: + assets = Path(directory) + BUILD.generate_index_json( + str(assets), + None, + "font_noto_sans_common_20_4.bin", + None, + font_bundle_id="noto-v1", + ) + index = json.loads((assets / "index.json").read_text(encoding="utf-8")) + self.assertEqual( + index["text_font_meta"], + {"charset": "common", "size": 20, "bpp": 4, "bundle": "noto-v1"}, + ) + + def test_text_font_requires_bundle(self): + with tempfile.TemporaryDirectory() as directory: + with self.assertRaises(ValueError): + BUILD.generate_index_json( + directory, None, "font_noto_sans_common_20_4.bin", None + ) + + +if __name__ == "__main__": + unittest.main()