* feat: migrate firmware to xiaozhi-fonts 2.0.0 * fix: restart scrolling subtitles on text change * docs: add AGENTS.md for project overview, architecture, rules, and commands - Introduced AGENTS.md to document the XiaoZhi voice-assistant firmware architecture, required rules for development, board configuration process, and validation steps. - Included detailed sections on project structure, commands for building and testing, and authoritative documentation references. * feat: finalize glyph push protocol * fix: remove stale emoji font include * build: use xiaozhi-fonts 2.0.0 * fix: decouple custom fonts from glyph push * refactor: improve wake word invocation handling - Introduced BeginWakeWordInvoke method to streamline the wake word processing flow. - Updated HandleWakeWordDetectedEvent to utilize the new method, ensuring proper state transitions and scheduling. - Enhanced error handling to prevent the device from getting stuck in the connecting state. - Refactored audio channel management to improve responsiveness during wake word detection. --------- Co-authored-by: Xiaoxia <terrence.huang@tenclass.com>
7.3 KiB
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:
{
"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:
{
"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": "<base64-encoded 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:
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:
- Resolve the full font bundle identified by
text_font.bundle. - Select the CBIN profile matching
text_font.sizeandtext_font.bpp. - Decode the message text into Unicode code points.
- Remove control characters, duplicates, and code points already present in
text_font.charset. - Extract the remaining glyphs from the full bundle.
- Apply the per-message limits and attach one
glyph_pushobject to the text message. - Omit
glyph_pushwhen 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_pushis 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.