Phase 11b: MQTT-overlays конфигурируются через JSON (вместо --temp-topic)

User: "оверлеев которые выводят какую-то инфу из MQTT может быть
бесконечно много. Надо делать настраиваемым через json/yaml конфиг".

Заменили захардкоженный TempMqttOverlay на generic MqttOverlay:

  - cpp/mqtt_overlay.hpp/.cpp: класс MqttOverlayItem (один topic + один
    text overlay) + MqttOverlayManager (контейнер). Загрузка из JSON.
  - cpp/mqtt_overlay_c_api.cpp: extern "C" обёртка для grid_record.c.
  - docker/mqtt_overlays.json: default config с temp_outside примером.
  - grid_record.c: --mqtt-overlays=PATH (заменил --temp-topic).
  - src/CMakeLists.txt: temp_overlay* удалены, mqtt_overlay* добавлены.

JSON schema:
  {
    "overlays": [
      {
        "id": "temp_outside",
        "topic": "zigbee2mqtt/Температура на улице",
        "json_field": "temperature",      // пусто = raw payload string
        "format": "%+.1f°C",                // printf
        "anchor": "right-bottom",           // right-top, left-bottom, ...
        "margin_x": 32, "margin_y": 24,
        "pixel_size": 32,
        "color": [255, 255, 255], "alpha": 230,
        "font_path": "/fonts/DejaVuSans-Bold.ttf"
      }
    ]
  }

MqttBrokerCfg делятся между всеми overlays (one connect_async per item
но shared credentials). Добавление новых overlays = редактирование JSON +
restart cfc-grid (hot-reload через ZMQ — Phase 12).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 10:07:05 +01:00
parent 362871a264
commit 75271436f7
9 changed files with 491 additions and 321 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"version": 1,
"_doc": "MQTT-driven text overlays. Каждый блок = одна MQTT-подписка + persistent text overlay в фиксированной позиции на output frame'е. Не привязан к layout cells. anchor: right-bottom/right-top/left-bottom/left-top/center. format: printf-style для extracted значения (для double — \"%+.1f°C\"). json_field пустой → raw payload как string.",
"overlays": [
{
"id": "temp_outside",
"topic": "zigbee2mqtt/Температура на улице",
"json_field": "temperature",
"format": "%+.1f°C",
"anchor": "right-bottom",
"margin_x": 32,
"margin_y": 24,
"pixel_size": 32,
"color": [255, 255, 255],
"alpha": 230,
"font_path": "/fonts/DejaVuSans-Bold.ttf"
}
]
}
+14 -13
View File
@@ -127,7 +127,7 @@ int main(int argc, char **argv)
const char *frigate_mqtt_host = NULL; const char *frigate_mqtt_host = NULL;
int frigate_mqtt_port = 1883; int frigate_mqtt_port = 1883;
const char *frigate_topic = "frigate/events"; const char *frigate_topic = "frigate/events";
const char *temp_mqtt_topic = NULL; /* "zigbee2mqtt/<sensor>" — JSON c полем .temperature */ const char *mqtt_overlays_path = NULL; /* JSON-конфиг MQTT-driven text overlays */
const char *initial_layout = NULL; /* --layout NAME → set_layout после init */ const char *initial_layout = NULL; /* --layout NAME → set_layout после init */
int motion_mode = 0; /* --motion-mode */ int motion_mode = 0; /* --motion-mode */
int motion_ttl = 45000; /* --motion-ttl ms */ int motion_ttl = 45000; /* --motion-ttl ms */
@@ -180,7 +180,7 @@ int main(int argc, char **argv)
{"motion-mode", no_argument, 0, 'm'}, /* enable motion-driven auto layout */ {"motion-mode", no_argument, 0, 'm'}, /* enable motion-driven auto layout */
{"motion-ttl", required_argument, 0, 'k'}, /* TTL ms (default 45000) */ {"motion-ttl", required_argument, 0, 'k'}, /* TTL ms (default 45000) */
{"templates", required_argument, 0, 'z'}, /* path to templates.json */ {"templates", required_argument, 0, 'z'}, /* path to templates.json */
{"temp-topic", required_argument, 0, 'x'}, /* zigbee2mqtt topic с температурой */ {"mqtt-overlays", required_argument, 0, 'x'}, /* path to MQTT overlays JSON */
{0, 0, 0, 0}, {0, 0, 0, 0},
}; };
const char *templates_path = NULL; const char *templates_path = NULL;
@@ -243,7 +243,7 @@ int main(int argc, char **argv)
case 'm': motion_mode = 1; break; case 'm': motion_mode = 1; break;
case 'k': motion_ttl = atoi(optarg); break; case 'k': motion_ttl = atoi(optarg); break;
case 'z': templates_path = optarg; break; case 'z': templates_path = optarg; break;
case 'x': temp_mqtt_topic = optarg; break; case 'x': mqtt_overlays_path = optarg; break;
case 'S': { case 'S': {
if (num_sources >= 32) { if (num_sources >= 32) {
fprintf(stderr, "max 32 sources\n"); return 1; fprintf(stderr, "max 32 sources\n"); return 1;
@@ -449,18 +449,19 @@ int main(int argc, char **argv)
cfc_composer_set_motion_mode(comp, 1, motion_ttl); cfc_composer_set_motion_mode(comp, 1, motion_ttl);
} }
/* Глобальный температурный overlay в правом-нижнем углу — Phase 11b. /* Глобальные MQTT-driven overlays (температура и т.п.) — JSON-конфиг.
* Подписка на zigbee2mqtt topic; при первом сообщении показывается. */ * Каждая запись = MQTT subscribe + persistent text overlay. См.
if (temp_mqtt_topic) { * include/cuframes_composer/cpp/mqtt_overlay.hpp для schema. */
extern int cfc_temp_overlay_start(cfc_composer_t *, const char *, int, if (mqtt_overlays_path) {
extern int cfc_mqtt_overlays_load(cfc_composer_t *, const char *,
const char *, int,
const char *, const char *, const char *, const char *,
const char *, int, int); int, int);
if (cfc_temp_overlay_start(comp, int n = cfc_mqtt_overlays_load(comp, mqtt_overlays_path,
mqtt_host ? mqtt_host : "cctv-mosquitto", mqtt_port, mqtt_host ? mqtt_host : "cctv-mosquitto", mqtt_port,
mqtt_user, mqtt_pass, temp_mqtt_topic, mqtt_user, mqtt_pass,
out_w, out_h) != 0) { out_w, out_h);
fprintf(stderr, "[grid_record] temp overlay start failed\n"); fprintf(stderr, "[grid_record] mqtt overlays loaded: %d\n", n);
}
} }
/* --layout NAME → applies named layout поверх --cell координат. Удобно /* --layout NAME → applies named layout поверх --cell координат. Удобно
@@ -0,0 +1,113 @@
/* MqttOverlay — generic MQTT-driven text overlay (Phase 11b).
*
* Каждый overlay = одна MQTT-подписка + один persistent text overlay.
* Конфиг загружается из JSON-файла (mqtt_overlays.json):
* {
* "overlays": [
* { "id": "temp_outside",
* "topic": "zigbee2mqtt/Температура на улице",
* "json_field": "temperature", // если payload JSON; пусто — raw string
* "format": "%+.1f°C", // printf для extracted значения
* "anchor": "right-bottom", // right-top, left-bottom, ...
* "margin_x": 32, "margin_y": 24,
* "pixel_size": 32,
* "color": [255, 255, 255], "alpha": 230,
* "font_path": "/fonts/DejaVuSans-Bold.ttf"
* }, ...
* ]
* }
*
* Менеджер (MqttOverlayManager) держит vector<MqttOverlayItem>, поднимает
* MQTT-клиентов и добавляет overlays в композер. Hot-reload через
* reload_from_file() — пересоздаёт всех subscribers и overlays.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_MQTT_OVERLAY_HPP
#define CUFRAMES_COMPOSER_CPP_MQTT_OVERLAY_HPP
#include "../overlay.h"
#include <atomic>
#include <memory>
#include <string>
#include <vector>
struct mosquitto;
struct mosquitto_message;
namespace cfc {
struct MqttOverlayCfg {
std::string id;
std::string topic;
std::string json_field; /* если payload JSON; пусто = raw string */
std::string format = "%s"; /* printf-formatted (для double — "%+.1f°C") */
std::string anchor = "right-bottom"; /* right-top, left-bottom, ... */
int margin_x = 32, margin_y = 24;
int pixel_size = 32;
int r = 255, g = 255, b = 255;
int alpha = 230;
std::string font_path = "/fonts/DejaVuSans-Bold.ttf";
};
struct MqttBrokerCfg {
std::string host = "cctv-mosquitto";
int port = 1883;
std::string username;
std::string password;
};
class MqttOverlayItem {
public:
MqttOverlayItem(const MqttOverlayCfg& cfg, const MqttBrokerCfg& broker,
int frame_w, int frame_h);
~MqttOverlayItem();
MqttOverlayItem(const MqttOverlayItem&) = delete;
MqttOverlayItem& operator=(const MqttOverlayItem&) = delete;
bool start();
cfc_overlay_t* overlay() const { return overlay_; }
const std::string& id() const { return cfg_.id; }
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();
MqttOverlayCfg cfg_;
MqttBrokerCfg broker_;
int frame_w_, frame_h_;
struct mosquitto* mosq_ = nullptr;
cfc_overlay_t* overlay_ = nullptr;
std::atomic<bool> running_{false};
std::string last_text_;
};
class MqttOverlayManager {
public:
explicit MqttOverlayManager(const MqttBrokerCfg& broker) : broker_(broker) {}
~MqttOverlayManager() = default;
/* Загрузить overlays из JSON-файла. Возвращает количество созданных. */
int load_from_file(const std::string& path, int frame_w, int frame_h);
/* Pointers на overlays для регистрации в композере. */
std::vector<cfc_overlay_t*> overlay_handles() const;
void clear();
int size() const { return static_cast<int>(items_.size()); }
private:
MqttBrokerCfg broker_;
std::vector<std::unique_ptr<MqttOverlayItem>> items_;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_MQTT_OVERLAY_HPP */
@@ -1,90 +0,0 @@
/* 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 -2
View File
@@ -37,8 +37,8 @@ set(COMPOSER_SOURCES_CPP
cpp/composer.cpp cpp/composer.cpp
cpp/composer_c_api.cpp cpp/composer_c_api.cpp
cpp/layouts_c_api.cpp cpp/layouts_c_api.cpp
cpp/temp_overlay.cpp cpp/mqtt_overlay.cpp
cpp/temp_overlay_c_api.cpp cpp/mqtt_overlay_c_api.cpp
) )
set(COMPOSER_SOURCES_CU set(COMPOSER_SOURCES_CU
cugrid/cugrid.cu cugrid/cugrid.cu
+297
View File
@@ -0,0 +1,297 @@
/* MqttOverlay — реализация (Phase 11b). */
#include "../../include/cuframes_composer/cpp/mqtt_overlay.hpp"
#include <json-c/json.h>
#include <mosquitto.h>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
namespace cfc {
// ── MqttOverlayItem ──────────────────────────────────────────────────────
MqttOverlayItem::MqttOverlayItem(const MqttOverlayCfg& cfg,
const MqttBrokerCfg& broker,
int frame_w, int frame_h)
: cfg_(cfg), broker_(broker), frame_w_(frame_w), frame_h_(frame_h)
{
mosquitto_lib_init();
}
MqttOverlayItem::~MqttOverlayItem()
{
running_.store(false);
if (mosq_) {
mosquitto_disconnect(mosq_);
mosquitto_loop_stop(mosq_, true);
mosquitto_destroy(mosq_);
mosq_ = nullptr;
}
/* Overlay ownership — Composer; не уничтожаем. */
}
void MqttOverlayItem::on_connect(struct mosquitto* m, void* user, int rc)
{
auto* self = static_cast<MqttOverlayItem*>(user);
if (rc == 0) {
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] connected, subscribe '%s'\n",
self->cfg_.id.c_str(), self->cfg_.topic.c_str());
mosquitto_subscribe(m, nullptr, self->cfg_.topic.c_str(), 0);
} else {
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] connect failed: %s\n",
self->cfg_.id.c_str(), mosquitto_connack_string(rc));
}
}
void MqttOverlayItem::on_message(struct mosquitto*, void* user,
const struct mosquitto_message* msg)
{
auto* self = static_cast<MqttOverlayItem*>(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 MqttOverlayItem::handle_payload(const char* payload, std::size_t len)
{
std::string buf(payload, len);
std::string text;
if (!cfg_.json_field.empty()) {
struct json_object* root = json_tokener_parse(buf.c_str());
if (!root) return;
struct json_object* v = nullptr;
json_object_object_get_ex(root, cfg_.json_field.c_str(), &v);
if (!v) { json_object_put(root); return; }
char tmp[128];
/* Если значение numeric — извлекаем как double, форматируем
* printf'ом. Если string — как %s. */
if (json_object_is_type(v, json_type_double) ||
json_object_is_type(v, json_type_int)) {
double d = json_object_get_double(v);
std::snprintf(tmp, sizeof(tmp), cfg_.format.c_str(), d);
} else {
const char* s = json_object_get_string(v);
std::snprintf(tmp, sizeof(tmp), cfg_.format.c_str(), s ? s : "");
}
json_object_put(root);
text = tmp;
} else {
/* Raw payload — format должен быть "%s" или совместимый. */
char tmp[256];
std::snprintf(tmp, sizeof(tmp), cfg_.format.c_str(), buf.c_str());
text = tmp;
}
update_text(text);
}
void MqttOverlayItem::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;
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/mqtt-overlay/%s] '%s'\n",
cfg_.id.c_str(), text.c_str());
}
void MqttOverlayItem::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 = 0, y = 0;
if (cfg_.anchor == "right-bottom") {
x = frame_w_ - w - cfg_.margin_x;
y = frame_h_ - h - cfg_.margin_y;
} else if (cfg_.anchor == "right-top") {
x = frame_w_ - w - cfg_.margin_x;
y = cfg_.margin_y;
} else if (cfg_.anchor == "left-bottom") {
x = cfg_.margin_x;
y = frame_h_ - h - cfg_.margin_y;
} else if (cfg_.anchor == "left-top") {
x = cfg_.margin_x;
y = cfg_.margin_y;
} else if (cfg_.anchor == "center") {
x = (frame_w_ - w) / 2;
y = (frame_h_ - h) / 2;
} else {
x = cfg_.margin_x; y = cfg_.margin_y;
}
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 MqttOverlayItem::start()
{
/* Persistent text overlay. */
cfc_overlay_text_config_t tc{};
tc.font_path = cfg_.font_path.c_str();
tc.text = "";
tc.pixel_size = cfg_.pixel_size;
tc.x = 0; tc.y = 0;
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/mqtt-overlay/%s] create_text failed (font '%s')\n",
cfg_.id.c_str(), cfg_.font_path.c_str());
return false;
}
cfc_overlay_set_id(overlay_, cfg_.id.c_str());
/* MQTT subscriber. */
char cid[64];
std::snprintf(cid, sizeof(cid), "composer-overlay-%s-%p",
cfg_.id.c_str(), static_cast<void*>(this));
mosq_ = mosquitto_new(cid, true, this);
if (!mosq_) return false;
if (!broker_.username.empty()) {
mosquitto_username_pw_set(mosq_, broker_.username.c_str(),
broker_.password.empty() ? nullptr : broker_.password.c_str());
}
mosquitto_connect_callback_set(mosq_, &MqttOverlayItem::on_connect);
mosquitto_message_callback_set(mosq_, &MqttOverlayItem::on_message);
mosquitto_reconnect_delay_set(mosq_, 1, 30, true);
int r = mosquitto_connect_async(mosq_, broker_.host.c_str(), broker_.port, 60);
if (r != MOSQ_ERR_SUCCESS) {
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] connect_async failed: %s\n",
cfg_.id.c_str(), mosquitto_strerror(r));
return false;
}
r = mosquitto_loop_start(mosq_);
if (r != MOSQ_ERR_SUCCESS) {
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] loop_start failed: %s\n",
cfg_.id.c_str(), mosquitto_strerror(r));
return false;
}
running_.store(true);
return true;
}
// ── MqttOverlayManager ───────────────────────────────────────────────────
namespace {
const char* jstr(struct json_object* o, const char* k, const char* def = "")
{
struct json_object* v;
if (!json_object_object_get_ex(o, k, &v)) return def;
return json_object_get_string(v);
}
int jint(struct json_object* o, const char* k, int def)
{
struct json_object* v;
if (!json_object_object_get_ex(o, k, &v)) return def;
return json_object_get_int(v);
}
} // namespace
int MqttOverlayManager::load_from_file(const std::string& path, int W, int H)
{
std::ifstream f(path);
if (!f.is_open()) {
std::fprintf(stderr, "[cfc/mqtt-overlay] %s: open failed\n", path.c_str());
return -3;
}
std::stringstream ss; ss << f.rdbuf();
std::string buf = ss.str();
struct json_object* root = json_tokener_parse(buf.c_str());
if (!root) {
std::fprintf(stderr, "[cfc/mqtt-overlay] %s: parse failed\n", path.c_str());
return -1;
}
struct json_object* jarr = nullptr;
if (!json_object_object_get_ex(root, "overlays", &jarr) ||
!json_object_is_type(jarr, json_type_array)) {
std::fprintf(stderr, "[cfc/mqtt-overlay] %s: 'overlays' missing\n", path.c_str());
json_object_put(root);
return -2;
}
clear();
int n = static_cast<int>(json_object_array_length(jarr));
for (int i = 0; i < n; i++) {
struct json_object* jo = json_object_array_get_idx(jarr, i);
if (!jo) continue;
MqttOverlayCfg cfg;
cfg.id = jstr(jo, "id", "");
cfg.topic = jstr(jo, "topic", "");
cfg.json_field = jstr(jo, "json_field", "");
cfg.format = jstr(jo, "format", "%s");
cfg.anchor = jstr(jo, "anchor", "right-bottom");
cfg.margin_x = jint(jo, "margin_x", 32);
cfg.margin_y = jint(jo, "margin_y", 24);
cfg.pixel_size = jint(jo, "pixel_size", 32);
cfg.alpha = jint(jo, "alpha", 230);
const char* fp = jstr(jo, "font_path", "");
if (*fp) cfg.font_path = fp;
struct json_object* jcolor = nullptr;
if (json_object_object_get_ex(jo, "color", &jcolor) &&
json_object_is_type(jcolor, json_type_array) &&
json_object_array_length(jcolor) >= 3) {
cfg.r = json_object_get_int(json_object_array_get_idx(jcolor, 0));
cfg.g = json_object_get_int(json_object_array_get_idx(jcolor, 1));
cfg.b = json_object_get_int(json_object_array_get_idx(jcolor, 2));
}
if (cfg.id.empty() || cfg.topic.empty()) {
std::fprintf(stderr, "[cfc/mqtt-overlay] entry[%d] без id/topic — skip\n", i);
continue;
}
auto item = std::make_unique<MqttOverlayItem>(cfg, broker_, W, H);
if (item->start()) items_.push_back(std::move(item));
}
json_object_put(root);
std::fprintf(stderr, "[cfc/mqtt-overlay] %s: started %zu overlays\n",
path.c_str(), items_.size());
return static_cast<int>(items_.size());
}
std::vector<cfc_overlay_t*> MqttOverlayManager::overlay_handles() const
{
std::vector<cfc_overlay_t*> v;
v.reserve(items_.size());
for (auto& i : items_) v.push_back(i->overlay());
return v;
}
void MqttOverlayManager::clear()
{
items_.clear();
}
} // namespace cfc
+45
View File
@@ -0,0 +1,45 @@
/* C wrapper для MqttOverlayManager (Phase 11b). */
#include "../../include/cuframes_composer/composer.h"
#include "../../include/cuframes_composer/cpp/mqtt_overlay.hpp"
#include <memory>
namespace {
std::unique_ptr<cfc::MqttOverlayManager> g_mgr;
}
extern "C" {
int cfc_mqtt_overlays_load(cfc_composer_t* composer,
const char* path,
const char* mqtt_host, int mqtt_port,
const char* mqtt_user, const char* mqtt_pass,
int frame_w, int frame_h)
{
if (!composer || !path) return -1;
cfc::MqttBrokerCfg br;
if (mqtt_host) br.host = mqtt_host;
if (mqtt_port > 0) br.port = mqtt_port;
if (mqtt_user) br.username = mqtt_user;
if (mqtt_pass) br.password = mqtt_pass;
g_mgr = std::make_unique<cfc::MqttOverlayManager>(br);
int n = g_mgr->load_from_file(path, frame_w, frame_h);
if (n <= 0) {
g_mgr.reset();
return n;
}
for (cfc_overlay_t* ov : g_mgr->overlay_handles()) {
cfc_composer_add_overlay(composer, ov);
}
return n;
}
void cfc_mqtt_overlays_stop(void)
{
g_mgr.reset();
}
} // extern "C"
-167
View File
@@ -1,167 +0,0 @@
/* 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
@@ -1,49 +0,0 @@
/* 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"