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:
@@ -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
@@ -127,7 +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 *mqtt_overlays_path = NULL; /* JSON-конфиг MQTT-driven text overlays */
|
||||
const char *initial_layout = NULL; /* --layout NAME → set_layout после init */
|
||||
int motion_mode = 0; /* --motion-mode */
|
||||
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-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 с температурой */
|
||||
{"mqtt-overlays", required_argument, 0, 'x'}, /* path to MQTT overlays JSON */
|
||||
{0, 0, 0, 0},
|
||||
};
|
||||
const char *templates_path = NULL;
|
||||
@@ -243,7 +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 'x': mqtt_overlays_path = optarg; break;
|
||||
case 'S': {
|
||||
if (num_sources >= 32) {
|
||||
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);
|
||||
}
|
||||
|
||||
/* Глобальный температурный overlay в правом-нижнем углу — Phase 11b.
|
||||
* Подписка на zigbee2mqtt topic; при первом сообщении показывается. */
|
||||
if (temp_mqtt_topic) {
|
||||
extern int cfc_temp_overlay_start(cfc_composer_t *, const char *, int,
|
||||
/* Глобальные MQTT-driven overlays (температура и т.п.) — JSON-конфиг.
|
||||
* Каждая запись = MQTT subscribe + persistent text overlay. См.
|
||||
* include/cuframes_composer/cpp/mqtt_overlay.hpp для schema. */
|
||||
if (mqtt_overlays_path) {
|
||||
extern int cfc_mqtt_overlays_load(cfc_composer_t *, const char *,
|
||||
const char *, int,
|
||||
const char *, const char *,
|
||||
const char *, int, int);
|
||||
if (cfc_temp_overlay_start(comp,
|
||||
int, int);
|
||||
int n = cfc_mqtt_overlays_load(comp, mqtt_overlays_path,
|
||||
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");
|
||||
}
|
||||
mqtt_user, mqtt_pass,
|
||||
out_w, out_h);
|
||||
fprintf(stderr, "[grid_record] mqtt overlays loaded: %d\n", n);
|
||||
}
|
||||
|
||||
/* --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
@@ -37,8 +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
|
||||
cpp/mqtt_overlay.cpp
|
||||
cpp/mqtt_overlay_c_api.cpp
|
||||
)
|
||||
set(COMPOSER_SOURCES_CU
|
||||
cugrid/cugrid.cu
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user