Расширение Glyph Push для динамического текста

Этот документ определяет версию 1 расширения протокола glyph_push. Расширение позволяет серверу отправлять растровые глифы, которых нет в установленном на устройстве текстовом шрифте. Оно одинаково применимо к транспортам WebSocket и MQTT/UDP, поскольку объявление возможностей и обработка входящего JSON реализованы в общем слое протокола.

Расширение лишь дополняет отрисовку текста. Оно не меняет семантику текста, аудио 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-значение признаком отсутствия поддержки. Поле v в каждой отправляемой нагрузке несёт версию расширения.

Объект text_font точно описывает данные шрифта, установленного на устройстве:

ПолеТипЗначение
bundlestringЯвный идентификатор набора шрифтов. Меняется при изменении метрик глифов, поведения отрисовки, наборов символов или совместимости на уровне протокола.
charsetstringУстановленный набор символов. Устройства версии 1 сообщают basic или common.
sizenumberПрофиль размера в пикселях текстового шрифта, который использует прошивка.
bppnumberЧисло бит на пиксель в растре текстового шрифта, сейчас 1 или 4.

basic — это шрифт, скомпонованный с прошивкой. Стандартные assets XiaoZhi сообщают common после загрузки общего шрифта из раздела assets. Сервер должен использовать значения из сообщения hello каждого устройства, а не выводить их из модели платы.

Пакет assets, доставляемый по OTA, может заменить текстовый шрифт на другой по размеру, bpp, набору символов или семейству. Прошивка по-прежнему загружает любой структурно корректный шрифт CBIN. Если пакет содержит и полные поля text_font_meta, устройство объявляет эти действующие значения времени выполнения и проверяет по ним присылаемые глифы. Устаревший или пользовательский пакет без совместимых метаданных глифов продолжает использовать свой шрифт, эмодзи, цвета и фон, но объявляет glyph_push: false и опускает text_font. Так предотвращаются несовместимые резервные глифы без ограничения кастомизации темы.

2. Нагрузка с глифами от сервера

Сервер может добавить объект glyph_push в любое из этих сообщений от сервера к устройству:

  • сообщение TTS с "type": "tts" и "state": "sentence_start";
  • сообщение STT с "type": "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-encoded bitmap>"
      }
    ]
  }
}

Заголовок нагрузки должен точно соответствовать возможностям устройства:

ПолеТребование
vДолжно быть 1.
bundleДолжно совпадать с text_font.bundle.
sizeДолжно совпадать с text_font.size.
bppДолжно совпадать с text_font.bpp.
glyphsОтправляемая частичная партия глифов, не более 64 элементов.

Каждый элемент использует собственные метрики растрового шрифта LVGL:

ПолеЗначение
codepointКодовая точка Unicode от 1 до 0x10FFFF.
adv_wГоризонтальный шаг в единицах с фиксированной запятой LVGL с четырьмя дробными битами (один пиксель — 16 единиц).
box_w, box_hРазмеры растра в пикселях. Каждый размер — от 0 до 64.
ofs_x, ofs_yЗнаковые 16-битные смещения глифа относительно базовой линии текста и позиции курсора.
bitmapBase64-кодирование несжатого простого растра LVGL.

Длина декодированного растра должна быть в точности равна:

ceil(box_w * box_h * bpp / 8)

Растр должен иметь ту же простую раскладку с нулевым шагом строки (stride), что и соответствующий полный набор шрифта Noto в формате CBIN. Серверу следует извлекать растр и метрики прямо из этого профиля CBIN и пересылать их, а не растеризовать посторонний шрифт в момент запроса.

Сумма длин декодированных растров в одной нагрузке не должна превышать 64 КиБ. Если недопустимы заголовок, любой из глифов или растр, устройство отклоняет всю нагрузку с глифами, но всё равно показывает текст сообщения установленными шрифтами. Устройство с PSRAM может также использовать резервные глифы, кэшированные предыдущими сообщениями.

3. Алгоритм выбора глифов на сервере

Для каждого соединения, которое объявляет glyph_push: true, серверу следует:

  1. Найти полный набор шрифтов, заданный text_font.bundle.
  2. Выбрать профиль CBIN, соответствующий text_font.size и text_font.bpp.
  3. Декодировать текст сообщения в кодовые точки Unicode.
  4. Удалить управляющие символы, дубликаты и кодовые точки, уже входящие в text_font.charset.
  5. Извлечь оставшиеся глифы из полного набора.
  6. Применить ограничения на сообщение и добавить к текстовому сообщению один объект glyph_push.
  7. Опустить glyph_push, если недостающих глифов нет.

Установленный текстовый шрифт просматривается раньше динамического резервного шрифта. Поэтому присланные глифы заполняют недостающие кодовые точки, но не переопределяют глифы из basic или common.

Полный набор шрифтов может использоваться всеми соединениями устройств в процессе сервера. Работа на каждое соединение сводится к тому, чтобы по кортежу возможностей (bundle, charset, size, bpp) определить, каких глифов не хватает и какой профиль читать.

4. Поведение кэша на устройстве

Сначала вставляются все глифы одного сообщения, затем выполняется единственная пересборка резервного шрифта. Устройство никогда не пересобирает шрифт для каждого глифа отдельно.

На устройстве с инициализированной PSRAM:

  • память под растр, cmap, дескриптор и записи кэша выделяется в PSRAM;
  • глифы сохраняются между сообщениями;
  • в кэше помещается не более 256 глифов и 64 КиБ декодированных растровых данных;
  • при превышении любого из пределов вытесняются записи, дольше всего не вставлявшиеся и не обновлявшиеся.

На устройстве без PSRAM:

  • память берётся из внутренней RAM;
  • сохраняется только партия глифов текущего сообщения;
  • следующее текстовое сообщение заменяет или очищает предыдущую партию.

Это различие не влияет на протокол. Сервер может отправлять глифы, нужные каждому сообщению, не зная, есть ли у устройства PSRAM.

5. Совместимость и версии

Сервер не должен отправлять глифы, если выполняется любое из условий:

  • features.glyph_push отсутствует или не поддерживается сервером;
  • у сервера нет объявленного набора (bundle);
  • ни один профиль полного шрифта не соответствует объявленным size и bpp;
  • данные глифов не удовлетворяют правилам проверки версии 1.

Откат автоматический: сообщения без glyph_push и сообщения, нагрузка с глифами которых отклонена, обрабатываются как обычно.

Когда генератор шрифтов меняет метрики, раскладку растра, исходные шрифты, наборы символов или поведение отрисовки, публикуйте новый явный идентификатор набора. Не отдавайте глифы одного набора под идентификатором другого, даже если size и bpp случайно совпали.

6. Требования безопасности

Нагрузка с глифами — недоверенный сетевой ввод. Реализации обязаны проверить нагрузку целиком, прежде чем изменять работающий шрифт, ограничить и число элементов, и декодированный размер, проверить длину после декодирования base64 и отклонять недопустимые кодовые точки и метрики. Серверам также следует ограничивать собственный объём работы на сообщение и не отправлять глифы, уже покрытые объявленным charset.

Источник: docs/glyph-push.md в 78/xiaozhi-esp32