From 362871a264f2078b21c0613cc5f1b909a3995f1b Mon Sep 17 00:00:00 2001 From: Evgeny Demchenko Date: Thu, 4 Jun 2026 09:58:47 +0100 Subject: [PATCH] =?UTF-8?q?Phase=2011b:=20=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20temperature=20overlay=20(?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D1=8B=D0=B9-=D0=BD=D0=B8=D0=B6=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20=D1=83=D0=B3=D0=BE=D0=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User: "в предыдущих версиях у нас показывалась температура, можешь сделать в нижнем правом углу оверлей не привязанный к сеткам?". Восстановили прежнее поведение из vf-cuda-grid/controller (dynamic_overlays.py). Новый класс cfc::TempMqttOverlay: - libmosquitto subscriber в отдельном thread'е, auto-reconnect 1→30s - json-c для parse JSON payload — extract поле .temperature (double) - Формат: "%+.1f°C" (например "+18.5°C") - Persistent FreeType text overlay (cfc_overlay_create_text), kept в Composer::overlays_[] backward-compat листе — рендерится поверх Layout - reposition_overlay() — пересчёт x/y после text_size() (right-bottom anchor) C-shim (composer_c_api.cpp) для grid_record.c: - cfc_temp_overlay_start(composer, host, port, user, pw, topic, W, H) - singleton (один temp overlay на процесс, прода-композитору хватит) CLI: --temp-topic="zigbee2mqtt/Температура на улице". MQTT credentials переиспользуются из --mqtt-host/--mqtt-user/--mqtt-pass. Compose override (localhost-infra): --temp-topic=zigbee2mqtt/Температура на улице Co-Authored-By: Claude Opus 4.7 --- examples/grid_record.c | 19 +- .../cuframes_composer/cpp/temp_overlay.hpp | 90 ++++++++++ src/CMakeLists.txt | 2 + src/cpp/temp_overlay.cpp | 167 ++++++++++++++++++ src/cpp/temp_overlay_c_api.cpp | 49 +++++ 5 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 include/cuframes_composer/cpp/temp_overlay.hpp create mode 100644 src/cpp/temp_overlay.cpp create mode 100644 src/cpp/temp_overlay_c_api.cpp diff --git a/examples/grid_record.c b/examples/grid_record.c index 8a792b1..97e54f7 100644 --- a/examples/grid_record.c +++ b/examples/grid_record.c @@ -127,6 +127,7 @@ int main(int argc, char **argv) const char *frigate_mqtt_host = NULL; int frigate_mqtt_port = 1883; const char *frigate_topic = "frigate/events"; + const char *temp_mqtt_topic = NULL; /* "zigbee2mqtt/" — JSON c полем .temperature */ const char *initial_layout = NULL; /* --layout NAME → set_layout после init */ int motion_mode = 0; /* --motion-mode */ int motion_ttl = 45000; /* --motion-ttl ms */ @@ -179,11 +180,12 @@ int main(int argc, char **argv) {"motion-mode", no_argument, 0, 'm'}, /* enable motion-driven auto layout */ {"motion-ttl", required_argument, 0, 'k'}, /* TTL ms (default 45000) */ {"templates", required_argument, 0, 'z'}, /* path to templates.json */ + {"temp-topic", required_argument, 0, 'x'}, /* zigbee2mqtt topic с температурой */ {0, 0, 0, 0}, }; const char *templates_path = NULL; int c; - while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:t:C:M:I:U:P:RF:A:G:T:D:L:S:mk:z:", opts, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:t:C:M:I:U:P:RF:A:G:T:D:L:S:mk:z:x:", opts, NULL)) != -1) { switch (c) { case 'o': out_path = optarg; break; case 'c': @@ -241,6 +243,7 @@ int main(int argc, char **argv) case 'm': motion_mode = 1; break; case 'k': motion_ttl = atoi(optarg); break; case 'z': templates_path = optarg; break; + case 'x': temp_mqtt_topic = optarg; break; case 'S': { if (num_sources >= 32) { fprintf(stderr, "max 32 sources\n"); return 1; @@ -446,6 +449,20 @@ int main(int argc, char **argv) cfc_composer_set_motion_mode(comp, 1, motion_ttl); } + /* Глобальный температурный overlay в правом-нижнем углу — Phase 11b. + * Подписка на zigbee2mqtt topic; при первом сообщении показывается. */ + if (temp_mqtt_topic) { + extern int cfc_temp_overlay_start(cfc_composer_t *, const char *, int, + const char *, const char *, + const char *, int, int); + if (cfc_temp_overlay_start(comp, + mqtt_host ? mqtt_host : "cctv-mosquitto", mqtt_port, + mqtt_user, mqtt_pass, temp_mqtt_topic, + out_w, out_h) != 0) { + fprintf(stderr, "[grid_record] temp overlay start failed\n"); + } + } + /* --layout NAME → applies named layout поверх --cell координат. Удобно * как default для ONVIF PTZ-управляемого composer'а (старт в quad, * далее set_layout через ZMQ). В motion-mode не работает (relayout diff --git a/include/cuframes_composer/cpp/temp_overlay.hpp b/include/cuframes_composer/cpp/temp_overlay.hpp new file mode 100644 index 0000000..894fed1 --- /dev/null +++ b/include/cuframes_composer/cpp/temp_overlay.hpp @@ -0,0 +1,90 @@ +/* TempMqttOverlay — глобальный overlay температуры (Phase 11b). + * + * Не привязан к сеткам/cells: рендерится в фиксированной позиции на output + * frame'е, поверх любого layout'а. Подписывается на MQTT topic, парсит JSON + * payload (поле "temperature"), форматирует "+18.5°C", обновляет persistent + * text overlay. + * + * Реализация: + * - libmosquitto subscriber в собственном thread'е (loop_start, auto-reconnect) + * - json-c для parse JSON payload + * - cfc_overlay_t (overlay.c, FreeType atlas) для рендера + * - text update через cfc_overlay_update_text — rebuild atlas при смене строки + * + * Совместима с прошлой реализацией: + * topic = "zigbee2mqtt/Температура на улице" + * format = "+18.5°C" (signed, 1 decimal) + * + * Лицензия: LGPL-2.1+ + */ + +#ifndef CUFRAMES_COMPOSER_CPP_TEMP_OVERLAY_HPP +#define CUFRAMES_COMPOSER_CPP_TEMP_OVERLAY_HPP + +#include "../overlay.h" + +#include +#include +#include + +/* Forward declarations — namespace глобальный (libmosquitto на C). */ +struct mosquitto; +struct mosquitto_message; + +namespace cfc { + +struct TempOverlayConfig { + std::string host = "cctv-mosquitto"; + int port = 1883; + std::string username; + std::string password; + std::string topic = "zigbee2mqtt/Температура на улице"; + + /* Anchor — позиция overlay'я. Координаты пересчитываются после первого + * получения текста (нужно знать ширину atlas'а). */ + int frame_w = 1920; + int frame_h = 1080; + int margin_right = 32; + int margin_bottom = 24; + int pixel_size = 32; + std::string font_path = "/fonts/DejaVuSans-Bold.ttf"; + int r = 255, g = 255, b = 255; + int alpha = 230; + + /* Префикс перед температурой (например "Улица: "). Пусто — только число. */ + std::string label_prefix; +}; + +class TempMqttOverlay { +public: + explicit TempMqttOverlay(const TempOverlayConfig& cfg); + ~TempMqttOverlay(); + + TempMqttOverlay(const TempMqttOverlay&) = delete; + TempMqttOverlay& operator=(const TempMqttOverlay&) = delete; + + /* Запустить subscriber thread + создать overlay. После start() overlay + * добавляется в композитор через add_overlay(). */ + bool start(); + + /* Опаковый pointer на overlay — Composer хранит в overlays_[] для draw. */ + cfc_overlay_t* overlay() const { return overlay_; } + +private: + static void on_connect(struct mosquitto* m, void* user, int rc); + static void on_message(struct mosquitto* m, void* user, const struct mosquitto_message* msg); + + void handle_payload(const char* payload, std::size_t len); + void update_text(const std::string& text); + void reposition_overlay(); + + TempOverlayConfig cfg_; + struct mosquitto* mosq_ = nullptr; + cfc_overlay_t* overlay_ = nullptr; + std::atomic running_{false}; + std::string last_text_; /* кеш для idempotent update */ +}; + +} // namespace cfc + +#endif /* CUFRAMES_COMPOSER_CPP_TEMP_OVERLAY_HPP */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 361e0f8..56acda6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,8 @@ set(COMPOSER_SOURCES_CPP cpp/composer.cpp cpp/composer_c_api.cpp cpp/layouts_c_api.cpp + cpp/temp_overlay.cpp + cpp/temp_overlay_c_api.cpp ) set(COMPOSER_SOURCES_CU cugrid/cugrid.cu diff --git a/src/cpp/temp_overlay.cpp b/src/cpp/temp_overlay.cpp new file mode 100644 index 0000000..b4c9b24 --- /dev/null +++ b/src/cpp/temp_overlay.cpp @@ -0,0 +1,167 @@ +/* TempMqttOverlay — реализация (Phase 11b). */ + +#include "../../include/cuframes_composer/cpp/temp_overlay.hpp" +#include "../../include/cuframes_composer/overlay.h" + +#include +#include + +#include +#include + +namespace cfc { + +TempMqttOverlay::TempMqttOverlay(const TempOverlayConfig& cfg) : cfg_(cfg) +{ + mosquitto_lib_init(); +} + +TempMqttOverlay::~TempMqttOverlay() +{ + running_.store(false); + if (mosq_) { + mosquitto_disconnect(mosq_); + mosquitto_loop_stop(mosq_, true); + mosquitto_destroy(mosq_); + mosq_ = nullptr; + } + /* Overlay уничтожается композером (composer owns overlays_[]). */ +} + +void TempMqttOverlay::on_connect(struct mosquitto* m, void* user, int rc) +{ + auto* self = static_cast(user); + if (rc == 0) { + std::fprintf(stderr, "[cfc/temp] connected, subscribe '%s'\n", + self->cfg_.topic.c_str()); + mosquitto_subscribe(m, nullptr, self->cfg_.topic.c_str(), 0); + } else { + std::fprintf(stderr, "[cfc/temp] connect failed: %s\n", + mosquitto_connack_string(rc)); + } +} + +void TempMqttOverlay::on_message(struct mosquitto*, void* user, + const struct mosquitto_message* msg) +{ + auto* self = static_cast(user); + if (!msg || !msg->payload || msg->payloadlen <= 0) return; + self->handle_payload(static_cast(msg->payload), + static_cast(msg->payloadlen)); +} + +void TempMqttOverlay::handle_payload(const char* payload, std::size_t len) +{ + /* Payload не nul-terminated. */ + std::string buf(payload, len); + + struct json_object* root = json_tokener_parse(buf.c_str()); + if (!root) return; + + struct json_object* jt = nullptr; + json_object_object_get_ex(root, "temperature", &jt); + if (!jt) { json_object_put(root); return; } + double t = json_object_get_double(jt); + json_object_put(root); + + char fmt[32]; + std::snprintf(fmt, sizeof(fmt), "%+.1f°C", t); + std::string text = cfg_.label_prefix + std::string(fmt); + update_text(text); +} + +void TempMqttOverlay::update_text(const std::string& text) +{ + if (text == last_text_) return; + last_text_ = text; + if (!overlay_) return; + + cfc_overlay_text_config_t tc{}; + tc.font_path = cfg_.font_path.c_str(); + tc.text = text.c_str(); + tc.pixel_size = cfg_.pixel_size; + tc.x = 0; tc.y = 0; /* выставится в reposition_overlay */ + tc.r = cfg_.r; tc.g = cfg_.g; tc.b = cfg_.b; + tc.extra_alpha = cfg_.alpha; + tc.visible = 1; + cfc_overlay_update_text(overlay_, &tc); + reposition_overlay(); + std::fprintf(stderr, "[cfc/temp] update: '%s'\n", text.c_str()); +} + +void TempMqttOverlay::reposition_overlay() +{ + if (!overlay_) return; + int w = 0, h = 0; + cfc_overlay_text_size(overlay_, &w, &h); + if (w <= 0 || h <= 0) return; + + int x = cfg_.frame_w - w - cfg_.margin_right; + int y = cfg_.frame_h - h - cfg_.margin_bottom; + x &= ~1; y &= ~1; + if (x < 0) x = 0; + if (y < 0) y = 0; + + cfc_overlay_text_config_t tc{}; + tc.font_path = cfg_.font_path.c_str(); + tc.text = last_text_.c_str(); + tc.pixel_size = cfg_.pixel_size; + tc.x = x; tc.y = y; + tc.r = cfg_.r; tc.g = cfg_.g; tc.b = cfg_.b; + tc.extra_alpha = cfg_.alpha; + tc.visible = 1; + cfc_overlay_update_text(overlay_, &tc); +} + +bool TempMqttOverlay::start() +{ + /* Создаём persistent text overlay (изначально скрытый — visible=0 + * пока не пришла температура). */ + cfc_overlay_text_config_t tc{}; + tc.font_path = cfg_.font_path.c_str(); + tc.text = "—"; /* placeholder, заменится при первом MQTT */ + tc.pixel_size = cfg_.pixel_size; + tc.x = cfg_.frame_w - 100; + tc.y = cfg_.frame_h - 50; + tc.r = cfg_.r; tc.g = cfg_.g; tc.b = cfg_.b; + tc.extra_alpha = cfg_.alpha; + tc.visible = 0; + if (cfc_overlay_create_text(&tc, &overlay_) != 0) { + std::fprintf(stderr, "[cfc/temp] create_text failed (font: %s)\n", + cfg_.font_path.c_str()); + return false; + } + cfc_overlay_set_id(overlay_, "temp_outside"); + + /* MQTT subscriber. */ + char cid[64]; + std::snprintf(cid, sizeof(cid), "composer-temp-%p", static_cast(this)); + mosq_ = mosquitto_new(cid, true, this); + if (!mosq_) return false; + if (!cfg_.username.empty()) { + mosquitto_username_pw_set(mosq_, cfg_.username.c_str(), + cfg_.password.empty() ? nullptr : cfg_.password.c_str()); + } + mosquitto_connect_callback_set(mosq_, &TempMqttOverlay::on_connect); + mosquitto_message_callback_set(mosq_, &TempMqttOverlay::on_message); + mosquitto_reconnect_delay_set(mosq_, 1, 30, true); + + int r = mosquitto_connect_async(mosq_, cfg_.host.c_str(), cfg_.port, 60); + if (r != MOSQ_ERR_SUCCESS) { + std::fprintf(stderr, "[cfc/temp] connect_async failed: %s\n", + mosquitto_strerror(r)); + return false; + } + r = mosquitto_loop_start(mosq_); + if (r != MOSQ_ERR_SUCCESS) { + std::fprintf(stderr, "[cfc/temp] loop_start failed: %s\n", + mosquitto_strerror(r)); + return false; + } + running_.store(true); + std::fprintf(stderr, "[cfc/temp] subscriber запущен: %s:%d topic=%s\n", + cfg_.host.c_str(), cfg_.port, cfg_.topic.c_str()); + return true; +} + +} // namespace cfc diff --git a/src/cpp/temp_overlay_c_api.cpp b/src/cpp/temp_overlay_c_api.cpp new file mode 100644 index 0000000..7f5fcf8 --- /dev/null +++ b/src/cpp/temp_overlay_c_api.cpp @@ -0,0 +1,49 @@ +/* C wrapper для TempMqttOverlay — позволяет grid_record.c (C) поднять + * температурный overlay через простые extern "C" функции. + */ + +#include "../../include/cuframes_composer/cpp/temp_overlay.hpp" +#include "../../include/cuframes_composer/composer.h" + +#include +#include + +namespace { +/* singleton — один overlay на процесс grid_record. Если потребуется + * несколько — расширим, но для прода-композитора одной температуры хватит. */ +std::unique_ptr g_temp; +} + +extern "C" { + +int cfc_temp_overlay_start(cfc_composer_t* composer, + const char* mqtt_host, int mqtt_port, + const char* mqtt_user, const char* mqtt_pass, + const char* topic, + int frame_w, int frame_h) +{ + if (!composer || !mqtt_host || !topic) return -1; + cfc::TempOverlayConfig cfg; + cfg.host = mqtt_host; + cfg.port = mqtt_port > 0 ? mqtt_port : 1883; + if (mqtt_user) cfg.username = mqtt_user; + if (mqtt_pass) cfg.password = mqtt_pass; + cfg.topic = topic; + cfg.frame_w = frame_w; + cfg.frame_h = frame_h; + + g_temp = std::make_unique(cfg); + if (!g_temp->start()) { + g_temp.reset(); + return -1; + } + cfc_composer_add_overlay(composer, g_temp->overlay()); + return 0; +} + +void cfc_temp_overlay_stop(void) +{ + g_temp.reset(); +} + +} // extern "C"