# cfc-grid — руководство пользователя > Аудитория: администратор инсталляции. Кто настраивает камеры, layout'ы, > overlay'и, смотрит RTSP-поток на TV или в браузере. Не правит C++ код. > > Если ты разработчик и хочешь добавить новый тип ячейки/декорации/виджета — > см. [developer.md](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 Схема ```json { "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: ```bash 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 Схема ```json { "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`: ```yaml 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=,frigate=,priority=[,zones=::...] ``` - `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=,,,,,,,[,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 Пример ```bash # Python python3 <