e76360dbc4
Полный комплект документации к 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>
471 lines
20 KiB
Markdown
471 lines
20 KiB
Markdown
# 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=<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 Пример
|
||
|
||
```bash
|
||
# 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
|
||
```
|
||
|
||
```bash
|
||
# 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:
|
||
|
||
```json
|
||
{"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](developer.md) — внутреннее устройство, расширение
|
||
- [operations.md](operations.md) — build, deploy, troubleshooting
|
||
- README репо: краткий overview
|