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:
+18
-1
@@ -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 */
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user