Phase 10/11 WIP — pool + motion-mode + 8×8 templates (rolled back)

Объединённое состояние работ:
  - Phase 10: source pool, motion-driven layout, Frigate motion_pulse,
    zone-filter в pool, --source / --motion-mode CLI
  - Phase 11 Steps 1-3c: 8×8 микро-сетка, JSON templates с asymmetric
    layouts (tpl_1/3/4/5/6/7/8), reload через ZMQ, auto-labels per camera

В прод отдеплоено и откачено: используем gx/cuframes-composer:0.10 как
baseline. Phase 11 продолжится в branch phase11b-cpp как C++ refactor
с ООП-моделью Cell/Layout/Decoration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 21:24:23 +01:00
parent 2d7fc1e640
commit f8e27b9e85
10 changed files with 1097 additions and 152 deletions
+49
View File
@@ -117,6 +117,55 @@ int cfc_composer_set_layout(cfc_composer_t *comp, const char *layout_name);
* Возвращает NULL если cells выставлены вручную через --cell. */
const char *cfc_composer_current_layout(cfc_composer_t *comp);
/* ── Motion-driven auto layout (Phase 10) ────────────────────────────────
* Composer держит pool из N (до 32) sources с priority. При включённом
* motion-mode на каждом compose'е выбирает active (last_motion_ms < TTL),
* сортирует по priority DESC, и применяет layout по count активных:
* 0 → top-1 by priority в single
* 1 → single
* 2 → dual_horizontal
* 3-4 → quad
* 5 → main_with_strip
* 6 → six_grid
* 7-9 → nine_grid
* 10-16 → sixteen_grid
* Сигналы motion поступают через cfc_composer_motion_pulse() — обычно из
* Frigate MQTT subscriber'а (frigate_mqtt.c). */
/* Добавить источник в pool (отдельно от --cell). Источник создаётся сразу,
* cuframes subscription стартует. Source доступен через motion-relayout.
*
* required_zones (опц.) — colon-separated whitelist для motion-фильтра:
* только события Frigate с current_zones intersect этот список считаются
* motion. Без него все события от frigate_camera считаются motion
* (полезно если камера без zones либо motion и так осмысленный). */
int cfc_composer_add_pool_source(
cfc_composer_t *comp,
const char *cuframes_key, /* "cam-parking" */
const char *frigate_camera, /* "parking_overview" для motion match */
int priority, /* выше = важнее, top попадает в первую ячейку */
const char *required_zones /* "parking_zone:canopy:private_area" или NULL */
);
/* Включить/выключить motion-mode. ttl_ms — сколько камера остаётся
* активной после последнего события (0 → default 45000). */
int cfc_composer_set_motion_mode(cfc_composer_t *comp, int on, int ttl_ms);
/* Узнать, включён ли motion-mode. */
int cfc_composer_get_motion_mode(cfc_composer_t *comp);
/* Зафиксировать факт motion от Frigate-камеры. Используется Frigate MQTT
* subscriber'ом. Если frigate_camera не найдена в pool — no-op.
*
* current_zones — список зон из after.current_zones события. Если у source
* задан required_zones — будет применён фильтр: motion засчитывается только
* если current_zones intersect required_zones. Если required_zones пуст или
* NULL — фильтр выключен. */
int cfc_composer_motion_pulse(cfc_composer_t *comp,
const char *frigate_camera,
const char *const *current_zones,
int n_zones);
/* Получить layout статистику по источникам — для debug / health-репортов. */
typedef struct cfc_composer_health {
int total; /* всего источников */
+5
View File
@@ -16,6 +16,7 @@
#ifndef CUFRAMES_COMPOSER_FRIGATE_MQTT_H
#define CUFRAMES_COMPOSER_FRIGATE_MQTT_H
#include "composer.h"
#include "overlay.h"
#ifdef __cplusplus
@@ -28,6 +29,10 @@ typedef struct cfc_frigate_mqtt_config {
const char *username; /* MQTT user — обычно тот же, что у health */
const char *password;
const char *topic; /* default "frigate/events" */
/* Если задан — каждое new/update event для любой камеры вызывает
* cfc_composer_motion_pulse(composer, after.camera). Это драйвер для
* motion-driven auto-layout (Phase 10). NULL = motion-pulse не шлём. */
cfc_composer_t *composer;
} cfc_frigate_mqtt_config_t;
typedef struct cfc_frigate_mqtt cfc_frigate_mqtt_t;
+80 -16
View File
@@ -1,12 +1,18 @@
/* cuframes-composer — predefined layout templates.
/* cuframes-composer — модель layout'ов на 8×8 микро-сетке (Phase 11).
*
* Phase 9 (task #194): runtime layout switching через ZMQ verb + ONVIF PTZ.
* Output (1920×1080) разбит на 8 cols × 8 rows микроячеек 240×135 px.
* Каждая микроячейка имеет aspect 16:9 — любой квадрат N×N микроячеек
* тоже 16:9 (без растяжения камер).
*
* Layouts описаны normalized (0..1) — масштабируются к фактическому output
* width/height композитора при apply_to_cells(). Список синхронизирован с
* historic'ой `vf_cuda_grid.c` layouts[] (старый FFmpeg patch), что
* обеспечивает совместимость с Python controller'ом и предыдущими ONVIF
* presets.
* Cell template — прямоугольник {col, row, cs, rs} в микроячейках,
* с ролью CAMERA либо WIDGET. Camera-cells получают source из pool
* композитора по polю `order`: order=0 → main (top-priority active),
* order=1 → next и т.д.
*
* Layout template — набор cells одного экрана. Composer хранит таблицу
* built-in templates + опционально загружает override из JSON (Step 7).
*
* Phase 11 Step 1: единственный built-in `tpl_1` = одна cell 8×8 (single).
*
* Лицензия: LGPL-2.1+
*/
@@ -18,25 +24,83 @@
extern "C" {
#endif
#define CFC_LAYOUT_MAX_CELLS 16
#define CFC_LAYOUT_MAX_NAME 32
#define CFC_GRID_COLS 8
#define CFC_GRID_ROWS 8
#define CFC_LAYOUT_MAX_CELLS 32
#define CFC_LAYOUT_MAX_NAME 32
#define CFC_WIDGET_NAME_MAX 32
typedef struct cfc_layout_cell {
float x, y, w, h; /* normalized fraction of output dims */
} cfc_layout_cell_t;
typedef enum cfc_cell_role {
CFC_CELL_CAMERA = 0,
CFC_CELL_WIDGET = 1,
} cfc_cell_role_t;
/* Описание одной cell внутри layout-template'а.
* col, row — top-left угол в микроячейках (0..7)
* cs, rs — span в микроячейках (≥1, col+cs ≤ 8, row+rs ≤ 8)
* role — CAMERA или WIDGET
* order — для CAMERA: порядок placement'а активных камер
* (order=0 получает top-priority); игнорируется для WIDGET
* widget — для WIDGET: имя placeholder'а (текст подписи) */
typedef struct cfc_cell {
int col, row;
int cs, rs;
cfc_cell_role_t role;
int order;
char widget[CFC_WIDGET_NAME_MAX];
} cfc_cell_t;
/* Описание layout-template'а: набор cells + metadata. */
typedef struct cfc_layout {
const char *name;
int nb_cells;
cfc_layout_cell_t cells[CFC_LAYOUT_MAX_CELLS];
char name[CFC_LAYOUT_MAX_NAME];
cfc_cell_t cells[CFC_LAYOUT_MAX_CELLS];
int nb_cells; /* всего cells */
int nb_camera_cells; /* кеш — для best-fit selection */
int priority; /* tie-break при best-fit (выше = победитель) */
} cfc_layout_t;
/* Найти layout по имени; NULL если нет. */
const cfc_layout_t *cfc_layout_find(const char *name);
/* Возвращает указатель на array всех layout'ов + их количество. */
/* Возвращает указатель на массив всех загруженных layout'ов и их количество. */
const cfc_layout_t *cfc_layout_all(int *out_count);
/* Перевести {col,row,cs,rs} в pixel-координаты под output W×H.
* x = col * (W/8), w = cs * (W/8) (выровнено на чётные).
* NV12 требует чётных координат — встроено внутрь. */
void cfc_layout_to_pixels(const cfc_cell_t *cell, int W, int H,
int *out_x, int *out_y, int *out_w, int *out_h);
/* Загрузить templates из JSON-файла. При успехе сменяет реестр; при ошибке
* реестр остаётся прежним (или built-in, если ничего не было).
* Возвращает количество загруженных templates, либо отрицательное число
* (-1 = parse error, -2 = invalid schema, -3 = file not found).
*
* Hot-reload: после успешного вызова cfc_layout_find/cfc_layout_all
* возвращают новые template'ы. Композитор подхватывает на следующем кадре.
*
* JSON schema:
* { "version": 1, "grid_cols": 8, "grid_rows": 8,
* "templates": [
* { "name": "tpl_1", "priority": 0,
* "cells": [
* { "col":0, "row":0, "cs":8, "rs":8,
* "role":"camera", "order":0,
* "widget":"ha_chat" // для role=widget
* }, ...
* ]
* }, ...
* ]
* } */
int cfc_layout_load_file(const char *path);
/* Перезагрузить из последнего успешно загруженного файла. Если файла не
* было — возвращает -1. */
int cfc_layout_reload(void);
/* Получить путь к текущему загруженному файлу (NULL если built-in). */
const char *cfc_layout_loaded_path(void);
#ifdef __cplusplus
}
#endif