Files
gx e76360dbc4 docs: руководства пользователя / разработчика / operations (RU+EN)
Полный комплект документации к Phase 11b:

  docs/ru/user.md        — для админа инсталляции (motion-mode, PTZ,
                            templates.json, mqtt_overlays.json, ZMQ verbs)
  docs/ru/developer.md   — архитектура (Cell / Layout / Decoration),
                            как добавить новый Cell/Decoration, ABI shim,
                            algorithms (best-fit + asymmetric hysteresis)
  docs/ru/operations.md  — build (host + jammy + incremental bake),
                            deploy, logs/telemetry, troubleshooting
                            (broken pipe, MQTT-overlay, motion-mode)
  docs/en/*.md           — английская версия всех трёх
  README.md              — переписан с overview + ссылками на docs/

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 22:02:47 +01:00

20 KiB
Raw Permalink Blame History

cfc-grid — руководство пользователя

Аудитория: администратор инсталляции. Кто настраивает камеры, layout'ы, overlay'и, смотрит RTSP-поток на TV или в браузере. Не правит C++ код.

Если ты разработчик и хочешь добавить новый тип ячейки/декорации/виджета — см. developer.md.

1. Что это

cfc-grid — CUDA-композитор, собирающий N камер в один RTSP-поток rtsp://192.168.88.23:554/cfc-grid (1920×1080, H.264, NVENC). Раскладка выбирается автоматически по движению (от Frigate) или вручную через ONVIF-presets с TV.

Параллельно с видеокадром поверх рисуются:

  • Borders — серые рамки 2 px вокруг каждой ячейки
  • Labels — подпись имя prio=N в углу каждой камеры
  • Detection boxes — рамки объектов от Frigate, повторяющие позицию камеры при смене layout
  • MQTT-overlays — текстовые поля привязанные к топикам MQTT (температура, статусы, чаты)

2. Архитектура одной фразой

cuframes-pub-* (на камеру)
       ↓ shared VMM
   cfc-grid (composer) ── ZMQ control ──┐
       ↓ pipe (H.264)                   │
   cfc-grid-ffmpeg (relay) ─→ mediamtx ─┴─→ TV / VLC / Frigate / ...
                                ↑
                            ONVIF discovery от cctv-onvif

Кадры через cuframes идут zero-copy (один VMM-буфер, разделяемый между publisher'ом и composer'ом). Композитор берёт NV12-поверхность, ресайзит/блитит в свой output, добавляет декорации, отдаёт NVENC, NVENC пишет H.264 в pipe, cfc-grid-ffmpeg транскодирует pipe → RTSP к mediamtx.

3. Motion-mode — основной режим работы

3.1 Что происходит

На каждом кадре композитор:

  1. Берёт last_motion_ms для каждой камеры (обновляется из Frigate MQTT frigate/events)
  2. Считает «активными» те у кого (now - last_motion_ms) < motion_ttl_ms (по умолчанию 45 секунд)
  3. Сортирует активных по priority (число; больше = главнее)
  4. Выбирает template из templates.json по правилу best-fit: минимальный template с nb_camera_cells >= количество_активных
  5. Если в template'е больше camera-cells, чем активных — лишние заполняются остальными drawable камерами из pool (по приоритету)
  6. Применяет asymmetric hysteresis: рост числа активных переключает layout мгновенно, уменьшение — ждёт 3 секунды (чтобы не мелькало)

3.2 Что значит «drawable»

Камера исключается из pool если её cfc_source_state_t = CONNECTING, DISCONNECTED или DEAD (cuframes publisher молчит дольше dead_threshold_ms, по умолчанию 5 секунд).

STALE (кадры приходят редко) — считается, рисуется последний доступный кадр.

3.3 Manual override через PTZ

В TV в ONVIF PTZ-presets отображаются имена templates (tpl_1, tpl_3, tpl_4, ..., tpl_16). Нажатие GotoPreset или movement-кнопок:

  • Применяет выбранный layout мгновенно
  • Замораживает motion-mode на 60 секунд
  • По истечении — возвращается в auto-режим

ContinuousMove (стрелки): pan/tilt циклируют по списку presets, zoom-in =tpl_1 (full screen), zoom-out=tpl_16 (4×4 grid).

4. templates.json — раскладка экрана

4.1 Схема

{
  "version": 1,
  "grid_cols": 8,
  "grid_rows": 8,
  "templates": [
    {
      "name": "tpl_N",
      "_desc": "Описание",
      "priority": 0,
      "cells": [
        {
          "col": 0, "row": 0,
          "cs": 4, "rs": 4,
          "role": "camera",
          "order": 0
        },
        {
          "col": 6, "row": 0,
          "cs": 2, "rs": 6,
          "role": "widget",
          "widget": "temp_chart"
        }
      ]
    }
  ]
}

Грид: 8×8 микроячеек (240×135 px каждая = 16:9 на output 1920×1080). Любой квадрат N×N микроячеек тоже 16:9.

Поле Значение
col, row Top-left угол cell в микроячейках (0..7)
cs, rs Размер в микроячейках
role camera либо widget
order Для camera: порядок placement'а активных (0 = главная, обычно крупнейшая cell)
widget Для widget: имя placeholder'а (текст подписи)

4.2 Best-fit selection

Композитор выбирает template для текущего числа активных:

candidates = [t for t in templates if t.nb_camera_cells >= n_active]
pick = min(candidates, key=lambda t: t.nb_camera_cells - n_active)
# При равенстве: больший priority побеждает

Если активных больше чем cell'ов в самом большом template — берётся самый большой, лишние камеры обрезаются (низший priority вылетает).

4.3 Встроенные templates

По умолчанию 9 templates в /opt/templates.json:

Имя Cells Описание
tpl_1 1 cam Одна камера во весь экран
tpl_3 3 cam + 2 widgets Главная 1440×810 слева + 2 превью справа + 2 widget
tpl_4 4 cam Quad 2×2, 960×540 каждая
tpl_5 5 cam + 1 widget Главная + 4 превью справа стопкой + widget снизу
tpl_6 6 cam + 1 widget Главная + 3 правые + 2 нижние + widget
tpl_7 7 cam + 1 widget Главная + 3 правые + 3 нижние + widget
tpl_8 8 cam (1+3+4) Главная + 3 правые + 4 в нижней строке
tpl_9 9 cam + 2 widgets 3×3 главных + widget справа + widget снизу
tpl_16 16 cam 4×4 grid, 480×270 каждая

Подробности — см. docker/templates.json в репо.

4.4 Как добавить свой template

  1. Открыть docker/templates.json (или подмонтированный override)
  2. Добавить блок в "templates": [...] по схеме выше
  3. Перезапустить cfc-grid (либо docker exec cfc-grid sh -c 'kill -HUP 1' когда добавим hot-reload в Phase 12), либо вызвать ZMQ:
mosquitto_pub -h 192.168.88.23 -p 5599 ...   # пока нет CLI, см. operations.md

4.5 Координатная математика

Каждая микроячейка = 1920/8 = 240 px ширина, 1080/8 = 135 px высота (aspect 16:9). Cell {col=2, row=4, cs=4, rs=2}:

  • pixel x = 2 * 240 = 480
  • pixel y = 4 * 135 = 540
  • pixel w = 4 * 240 = 960
  • pixel h = 2 * 135 = 270

Aspect cell = cs/rs * 16/9. Для 16:9 cell'ов cs == rs.

5. mqtt_overlays.json — text overlay'и из MQTT

5.1 Схема

{
  "version": 1,
  "overlays": [
    {
      "id": "temp_outside",
      "topic": "zigbee2mqtt/Температура на улице",
      "json_field": "temperature",
      "format": "%+.1f°C",
      "anchor": "right-bottom",
      "margin_x": 32, "margin_y": 24,
      "pixel_size": 32,
      "color": [255, 255, 255],
      "alpha": 230,
      "bg_alpha": 160,
      "bg_y": 16, "bg_u": 128, "bg_v": 128,
      "bg_pad": 10,
      "placeholder": "—",
      "font_path": "/fonts/DejaVuSans-Bold.ttf"
    }
  ]
}
Поле Описание
id Уникальный идентификатор overlay'я (используется ZMQ для lookup'а)
topic MQTT topic для subscribe (через cctv-mosquitto)
json_field Если payload JSON — имя поля для извлечения значения; пусто = raw payload как строка
format printf-стиль для отформатированного значения (например "%+.1f°C", "%s")
anchor Якорь позиционирования: right-bottom, right-top, left-bottom, left-top, center
margin_x, margin_y Отступ от ближайшего края экрана (px)
pixel_size Размер шрифта в пикселях
color RGB цвет текста
alpha Общая непрозрачность текста (0..255)
bg_alpha Непрозрачность подложки (0..255); 0 = без фона
bg_y, bg_u, bg_v BT.709 limited-range цвет подложки (Y=16..235, UV=16..240); default чёрный
bg_pad Отступ подложки вокруг текста (px)
placeholder Что показывать пока не пришло MQTT-сообщение; пусто = "—"
font_path Путь к шрифту (.ttf/.otf) в контейнере

5.2 Поддерживаемые символы

Шрифт DejaVuSans-Bold.ttf — стандартный из пакета fonts-dejavu. Покрывает Basic Multilingual Plane (latin, cyrillic, базовые символы), включая:

  • (U+276F), (U+270E), (U+27A4), (U+2192)
  • (U+2605), (U+25B6), (U+2709)

Emoji из Supplementary Multilingual Plane (>U+10000) — например 🗣 (U+1F5E3), 🤖 (U+1F916), 💬 (U+1F4AC) — не рендерятся: шрифт не содержит таких глифов. Рисуется placeholder-квадрат.

Чтобы добавить color emoji — нужно подключить Noto Color Emoji и расширить рендерер для COLR/CPAL/SBIX (см. developer.md).

5.3 Примеры

Только число: payload = "23.5" (raw), json_field: "", format: "%s°"

JSON с полем: payload = {"temperature": 23.5, "humidity": 45}, json_field: "temperature", format: "%+.1f°C"

Несколько overlay'ев в правом-верхнем углу столбиком: первый с margin_y: 24, второй с margin_y: 72, третий с margin_y: 120.

5.4 Как добавить

  1. Открыть docker/mqtt_overlays.json (либо подмонтированный override)
  2. Добавить блок в массив "overlays": [...]
  3. Перезапустить cfc-grid

Hot-reload через ZMQ — Phase 12 (reload_overlays verb).

6. CLI флаги composer'а

В compose override docker/cctv/cuframes-composer/docker-compose.override.yml:

command:
  - "--out=/out/grid.h264"          # named pipe для ffmpeg-relay
  - "--fps=25"
  - "--bitrate=6000"                # kbps
  - "--width=1920"
  - "--height=1080"
  - "--intra-refresh"               # вместо IDR-burst'ов (low-latency)
  - "--control=tcp://0.0.0.0:5599"  # ZMQ control plane
  - "--mqtt=cctv-mosquitto:1883"    # MQTT для health-публикации
  - "--mqtt-instance=cfc-grid"
  - "--mqtt-user=composer"
  - "--mqtt-pass=${COMPOSER_MQTT_PASSWORD}"

  # Источники (камеры) — повторяемое
  - "--source=cam-parking,frigate=parking_overview,priority=100,zones=parking_zone:canopy:private_area"
  - "--source=cam-back_yard,frigate=back_yard,priority=70"
  # ...

  - "--motion-mode"                  # включить auto-layout
  - "--motion-ttl=45000"             # ms

  - "--templates=/opt/templates.json"
  - "--mqtt-overlays=/opt/mqtt_overlays.json"

  # Frigate motion-driver и detection boxes
  - "--frigate-mqtt=cctv-mosquitto:1883"
  - "--frigate-topic=frigate/events"
  - "--detection-cell=parking,parking_overview,0,0,960,540,640,480,parking_zone:canopy:private_area"

6.1 --source синтаксис

--source=<cuframes_key>,frigate=<camera>,priority=<N>[,zones=<z1>:<z2>:...]
  • cuframes_key — имя cuframes-publisher'а (например cam-parking)
  • frigate=NAME — имя камеры в Frigate (для матчинга motion-событий)
  • priority=N — целое, больше = главнее
  • zones=... — опциональный whitelist Frigate zones; motion засчитывается только если event.current_zones пересекается со списком (отсев street-флуда)

6.2 --detection-cell синтаксис

--detection-cell=<key>,<frigate_camera>,<x>,<y>,<w>,<h>,<detect_w>,<detect_h>[,zones]
  • key — произвольный идентификатор overlay'я для логов
  • frigate_camera — имя в Frigate (для матчинга event.camera)
  • x,y,w,h — initial geometry (composer пересчитает динамически)
  • detect_w,detect_h — разрешение детектора Frigate (например 640×480)
  • zones — whitelist для bbox

7. ZMQ control plane

Default endpoint: tcp://192.168.88.23:5599. Все verb'ы — JSON request/reply.

7.1 Список verbs

Команда Параметры Что делает
ping Health-check
health {total, active, stale, dead} по pool'у
set_text id, text, r, g, b, x, y, visible Обновить текстовый overlay (для CLI --text=...)
set_visible id, visible Скрыть/показать overlay
list_overlays Список overlay'ев
set_layout name Применить named template (manual override на 60s в motion-mode)
list_layouts Список доступных templates с cells
get_layout Имя текущего template'а
set_motion_mode on, ttl_ms Включить/выключить motion-режим
get_motion_mode Состояние motion-mode
get_template name Полный JSON template'а
reload_templates path? Перезагрузить templates из файла (default — последний путь)

7.2 Пример

# Python
python3 <<EOF
import zmq, json
s = zmq.Context().socket(zmq.REQ)
s.connect("tcp://192.168.88.23:5599")
s.send_json({"cmd": "list_layouts"})
print(json.dumps(s.recv_json(), indent=2, ensure_ascii=False))
EOF
# Force layout
echo '{"cmd":"set_layout","name":"tpl_4"}' | \
  nc -q1 192.168.88.23 5599    # ! REP socket требует ZMQ framing, не голый TCP

Голый nc не работает — REP socket ожидает ZMQ wire-protocol. Используй zmq Python/Go/JS либо mosquitto_pub через RPC-bridge (Phase 12).

8. ONVIF и PTZ

Сервис cctv-onvif биндится на host network, отвечает на WS-Discovery multicast (239.255.255.250:3702) и SOAP-запросы по HTTP :8085.

8.1 Добавление в TV

В клиенте (TV / IP-CamViewer / Synology / Frigate):

  • ONVIF host: 192.168.88.23
  • Port: 8085
  • User / Password: пусто (auth не настроен)

WS-Discovery в LAN 192.168.88.0/24 найдёт устройство cfc-grid (Goldix). RTSP-URL автоматически — rtsp://192.168.88.23:554/cfc-grid.

8.2 PTZ presets

Список (GetPresets): tpl_1, tpl_3, tpl_4, tpl_5, tpl_6, tpl_7, tpl_8, tpl_9, tpl_16.

GotoPreset(name) → composer применяет template + замораживает motion-mode на 60 секунд → auto-возврат.

8.3 PTZ movement (ContinuousMove)

Команда Действие
Pan right / Tilt down Следующий template в списке
Pan left / Tilt up Предыдущий
Zoom in (+) tpl_1 (full screen)
Zoom out () tpl_16 (4×4 grid)

9. Где смотреть RTSP

Способ URL
VLC / mpv / ffplay rtsp://192.168.88.23:554/cfc-grid
Браузер (HLS) http://192.168.88.23:8888/cfc-grid
WebRTC http://192.168.88.23:8889/cfc-grid
OBS / FFmpeg input rtsp://192.168.88.23:554/cfc-grid
ONVIF клиенты через WS-Discovery (см. §8)

10. Известные ограничения

  • Color emoji не рендерятся (нужен Noto Color Emoji + COLR/CPAL поддержка в text renderer — Phase 12)
  • Hot-reload mqtt_overlays.json — нет ZMQ verb'а, нужен restart cfc-grid
  • Per-overlay broker — все MQTT-overlay'и используют общий broker (тот что задан как --mqtt); подписку на сторонний broker отдельно — нет
  • Widget rendering — placeholder (тёмный rect + label), реальные виджеты (graph, chat) — Phase 12+
  • HA Assist в MQTT — архитектурное ограничение HA (см. operations.md §Troubleshooting)

11. FAQ

В: На TV вижу старые имена layouts (quad, dual_horizontal). Что делать?

О: TV закешировал ONVIF-presets. В клиенте удали камеру и добавь заново — он перечитает GetPresets с актуальными именами.

В: Камера парковки в DEAD, но в logs показывает active=3. Почему?

О: cfc_composer_get_health показывает pool-wide state, а motion-active считает по last_motion_ms независимо от source state. DEAD исключается на этапе is_camera_drawable() в compose_motion_relayout.

В: PTZ нажал на TV, рамка переключилась, через минуту вернулась обратно.

О: Это by design — set_layout в motion-mode замораживает auto на 60 секунд (manual_override_duration_ms). Чтобы зафиксировать template надолго — выключи motion-mode целиком через ZMQ:

{"cmd": "set_motion_mode", "on": 0}

В: Хочу подложку у text overlay'я разного цвета.

О: Поля bg_y/bg_u/bg_v в JSON принимают BT.709 limited-range. Чтобы получить красный — Y≈80, U≈90, V≈240. Для голубого — Y≈170, U≈170, V≈100. Калькулятор: https://www.rapidtables.com/convert/color/rgb-to-yuv.html (использовать BT.709 limited).

В: При motion на 5 камерах layout не появляется, остаётся quad.

О: Проверь docker logs cfc-grid | grep "loaded N templates" — там должно быть ≥5 (есть tpl_5..tpl_8, tpl_9, tpl_16). Если нет — templates.json не подгрузился (проверь syntax через jq либо python3 -m json.tool).

В: Frigate-bbox рисуется не на той камере.

О: Проверь --detection-cell — там должен быть frigate_camera который совпадает с event.after.camera. Composer связывает detbox-overlay с pool-entry по frigate_camera (см. cfc_composer::pool::by_frigate_camera).

12. Куда дальше

  • developer.md — внутреннее устройство, расширение
  • operations.md — build, deploy, troubleshooting
  • README репо: краткий overview