Phase 11b: глобальный temperature overlay (правый-нижний угол)

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 09:58:47 +01:00
parent 24d398526e
commit 362871a264
5 changed files with 326 additions and 1 deletions
+18 -1
View File
@@ -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/<sensor>" — 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
@@ -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 <atomic>
#include <string>
#include <thread>
/* 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<bool> running_{false};
std::string last_text_; /* кеш для idempotent update */
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_TEMP_OVERLAY_HPP */
+2
View File
@@ -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
+167
View File
@@ -0,0 +1,167 @@
/* TempMqttOverlay — реализация (Phase 11b). */
#include "../../include/cuframes_composer/cpp/temp_overlay.hpp"
#include "../../include/cuframes_composer/overlay.h"
#include <json-c/json.h>
#include <mosquitto.h>
#include <cstdio>
#include <cstring>
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<TempMqttOverlay*>(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<TempMqttOverlay*>(user);
if (!msg || !msg->payload || msg->payloadlen <= 0) return;
self->handle_payload(static_cast<const char*>(msg->payload),
static_cast<std::size_t>(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<void*>(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
+49
View File
@@ -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 <memory>
#include <string>
namespace {
/* singleton — один overlay на процесс grid_record. Если потребуется
* несколько — расширим, но для прода-композитора одной температуры хватит. */
std::unique_ptr<cfc::TempMqttOverlay> 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<cfc::TempMqttOverlay>(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"