Phase 4: ZMQ control plane + MQTT health publisher

Phase 4a — control plane через ZMQ REP socket с JSON-командами.
Позволяет операторам менять text overlay'и runtime'ом без рестарта.
Live-validated: команды set_text label-parking → "⚠ ВНИМАНИЕ ⚠"
и set_text label-gate → "Машина у ворот" отрабатывают, текст
обновляется в потоке RTSP без перерывов.

Phase 4b — MQTT health publisher через libmosquitto. Каждые 10с в
composer/<instance>/health публикуется JSON {active,stale,dead,total,
uptime_s} с retain=true. Опционально публикуется HA MQTT discovery
config — четыре сенсора (active/stale/dead/total) появляются в HA
автоматически с expire_after=30s.

Содержимое:

- include/cuframes_composer/overlay.h — cfc_overlay_set_id/get_id/get_type
  для lookup'а через control plane.
- include/cuframes_composer/composer.h — cfc_composer_find_overlay(id).
- include/cuframes_composer/control.h — cfc_control_config_t (endpoint +
  composer + cuda_ctx для text rebuild в worker thread).
- src/control.c — ZMQ REP socket в фоновом потоке + zmq_poll с 200мс
  timeout'ом для проверки stop_flag. JSON-диспатчер для команд ping /
  health / set_text / set_visible / list_overlays. cuCtxSetCurrent
  на старте worker'а — без этого update_text валится на cuMemAlloc.
- include/cuframes_composer/health.h — cfc_health_config_t (host/port/
  user/pass + topic_prefix/instance + interval + publish_discovery).
- src/health.c — mosquitto_connect_async + loop_start + background
  thread с publish health каждые N секунд + один разовый publish_discovery
  для HA.
- examples/grid_record — флаги --control tcp://0.0.0.0:5599,
  --mqtt host[:port], --mqtt-instance NAME, --mqtt-user/--mqtt-pass.
  Для text overlay'ев — prefix "id=NAME:" в --text задаёт control-plane ID.

CMakeLists.txt — find_library(zmq), find_library(json-c),
find_library(mosquitto) + linkage всех трёх.

Production TODO: создать отдельного MQTT user'а для composer'а вместо
переиспользования frigate creds (используется только в smoke).
This commit is contained in:
2026-06-03 06:20:38 +01:00
parent e02998cea7
commit 636b70b64c
11 changed files with 748 additions and 6 deletions
+8
View File
@@ -84,6 +84,14 @@ int cfc_composer_compose(
* Порядок добавления = z-order (последний рисуется поверх). Лимит — 64. */
int cfc_composer_add_overlay(cfc_composer_t *comp, cfc_overlay_t *ov);
/* Найти overlay по ID (если был задан через cfc_overlay_set_id). Возвращает
* NULL если не найден. Thread-safe — composer держит overlays в массиве,
* пока add/destroy не пересекаются с lookup'ом — но control plane вызывает
* это из своего потока, draw — из своего; они оба только читают список,
* а update полей overlay'я делается через cfc_overlay_update_text и пр.
* (содержимое overlay'я под mutex'ом не лежит, нужно лочиться вызывающему). */
cfc_overlay_t *cfc_composer_find_overlay(cfc_composer_t *comp, const char *id);
/* Получить layout статистику по источникам — для debug / health-репортов. */
typedef struct cfc_composer_health {
int total; /* всего источников */
+56
View File
@@ -0,0 +1,56 @@
/* cuframes-composer — control plane через ZMQ + JSON.
*
* Слушает JSON-команды на ZMQ REP socket, диспатчит их в composer/overlay.
* Используется операторами для:
* - изменения text overlay'я live ("NO SIGNAL" → "RECORDING")
* - toggle visible/alpha без рестарта
* - health-репортов (active/stale/dead) для observability и автоматики
*
* Протокол:
* Запрос: JSON-объект {"cmd": "<команда>", ...}
* Ответ: {"ok": true, ...} либо {"error": "<сообщение>"}
*
* Команды Phase 4a:
* {"cmd": "ping"} → {"ok":true,"pong":1}
* {"cmd": "health"} → {"ok":true,"active":N,"stale":M,"dead":K}
* {"cmd": "list_overlays"} → {"ok":true,"overlays":[{"id":"...","type":N},...]}
* {"cmd": "set_text", "id": "...",
* "text": "...", "r":255,"g":255,"b":255,"visible":1}
* → {"ok":true}
* {"cmd": "set_visible", "id": "...",
* "visible": 0|1} → {"ok":true}
*
* Сервер работает в фоновом потоке. REP socket — блокирующий, но мы используем
* zmq_poll с таймаутом для проверки stop_flag'а.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CONTROL_H
#define CUFRAMES_COMPOSER_CONTROL_H
#include "composer.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct cfc_control_config {
const char *bind_endpoint; /* "tcp://0.0.0.0:5599" или "ipc:///run/composer.sock" */
cfc_composer_t *composer; /* для health-запросов и lookup overlay'ев */
CUcontext cuda_ctx; /* push'нется в control thread (для cuMemAlloc в text rebuild) */
} cfc_control_config_t;
typedef struct cfc_control cfc_control_t;
/* Создать control plane сервер и запустить background thread. */
int cfc_control_create(const cfc_control_config_t *cfg, cfc_control_t **out);
/* Остановить и освободить. */
int cfc_control_destroy(cfc_control_t *ctl);
#ifdef __cplusplus
}
#endif
#endif /* CUFRAMES_COMPOSER_CONTROL_H */
+49
View File
@@ -0,0 +1,49 @@
/* cuframes-composer — MQTT health publisher для observability.
*
* Подключается к MQTT-брокеру, периодически публикует JSON со статистикой
* композитора (active/stale/dead источников, framerate, uptime).
*
* Топик: <prefix>/<instance>/health QoS 1, retain=true.
* Так же при старте публикует HA discovery config — для автоматического
* появления сенсора в Home Assistant. expire_after короткий (30 секунд) —
* если поток упал, HA подсветит сенсор «Unavailable» спустя 30с.
*
* Lifecycle:
* create: mosquitto_new + connect_async + начать background thread
* loop: каждые interval_sec секунд → mosquitto_publish + mosquitto_loop
* destroy: stop_flag + DISCONNECT + cleanup
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_HEALTH_H
#define CUFRAMES_COMPOSER_HEALTH_H
#include "composer.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct cfc_health_config {
const char *host; /* "cctv-mosquitto" / "192.168.88.23" */
int port; /* 1883 без TLS */
const char *username; /* nullable */
const char *password; /* nullable */
const char *topic_prefix; /* "composer" (топик: composer/<instance>/health) */
const char *instance; /* "cfc-grid" — уникальный ID composer'а */
int interval_sec; /* 10 = публикуем раз в 10 секунд */
cfc_composer_t *composer; /* читает get_health() */
int publish_discovery; /* 1 = опубликовать HA discovery config при старте */
} cfc_health_config_t;
typedef struct cfc_health cfc_health_t;
int cfc_health_create(const cfc_health_config_t *cfg, cfc_health_t **out);
int cfc_health_destroy(cfc_health_t *h);
#ifdef __cplusplus
}
#endif
#endif /* CUFRAMES_COMPOSER_HEALTH_H */
+11
View File
@@ -47,6 +47,17 @@ typedef enum cfc_overlay_type {
typedef struct cfc_overlay cfc_overlay_t;
/* Назначить overlay'ю короткий ID для lookup через control plane (Phase 4).
* ID копируется внутрь overlay'я (макс 31 символ). Без ID overlay тоже работает,
* но управлять им runtime'ом нельзя. Можно вызвать после create. */
int cfc_overlay_set_id(cfc_overlay_t *ov, const char *id);
/* Получить ID overlay'я (NULL если не задан). */
const char *cfc_overlay_get_id(const cfc_overlay_t *ov);
/* Получить тип overlay'я. */
cfc_overlay_type_t cfc_overlay_get_type(const cfc_overlay_t *ov);
/* Параметры BORDER overlay'я. */
typedef struct cfc_overlay_border_config {
int x, y, w, h; /* прямоугольник в full-res пикселях */