動態文字 Glyph Push 擴充

本文件定義 glyph_push 協議擴充的版本 1。伺服器可以透過該擴充向裝置下發本地文字 字型檔中缺失的點陣圖 glyph。能力宣告和 JSON 訊息處理位於公共協議層,因此 WebSocket 與 MQTT/UDP 使用完全相同的擴充格式。

該擴充只補充文字渲染能力,不改變訊息中的文字、TTS 音訊或 STT 語義。

1. 能力宣告

裝置在客戶端 hello 訊息中宣告能力:

{
  "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 描述裝置實際安裝的文字字型檔:

欄位型別含義
bundlestring顯式的字型 bundle 標識。glyph 度量、渲染方式、字符集或線格式變化時需要更換。
charsetstring已安裝字符集。版本 1 裝置報告 basic 或 common。
sizenumber韌體使用的文字字型畫素規格。
bppnumber字型點陣圖的每畫素位數,目前為 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 訊息。

示例:

{
  "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 編碼的點陣圖>"
      }
    ]
  }
}

payload 頭必須與裝置能力完全匹配:

欄位要求
v必須為 1。
bundle必須等於 text_font.bundle。
size必須等於 text_font.size。
bpp必須等於 text_font.bpp。
glyphs本次增量推送的 glyph 陣列,最多 64 項。

每個 item 使用 LVGL 原生點陣圖字型度量:

欄位含義
codepoint1 到 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 編碼。

解碼後的點陣圖長度必須嚴格等於:

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。

來源:78/xiaozhi-esp32 的 docs/glyph-push_zh.md