Полный комплект документации к 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>
20 KiB
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 Что происходит
На каждом кадре композитор:
- Берёт
last_motion_msдля каждой камеры (обновляется из Frigate MQTTfrigate/events) - Считает «активными» те у кого
(now - last_motion_ms) < motion_ttl_ms(по умолчанию 45 секунд) - Сортирует активных по
priority(число; больше = главнее) - Выбирает template из
templates.jsonпо правилу best-fit: минимальный template сnb_camera_cells >= количество_активных - Если в template'е больше camera-cells, чем активных — лишние заполняются остальными drawable камерами из pool (по приоритету)
- Применяет 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
- Открыть
docker/templates.json(или подмонтированный override) - Добавить блок в
"templates": [...]по схеме выше - Перезапустить 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 Как добавить
- Открыть
docker/mqtt_overlays.json(либо подмонтированный override) - Добавить блок в массив
"overlays": [...] - Перезапустить 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