From fce131b72c18f070c6b100bbb3b04899e09c2c36 Mon Sep 17 00:00:00 2001 From: Alejandro Mora Date: Fri, 10 Apr 2026 20:27:46 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(plugin):=20=E2=9C=A8=20add=20mqtt=20pl?= =?UTF-8?q?ugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give the ability to lcd4linux listen to MQTT topics --- .github/workflows/ci.yml | 2 + .github/workflows/release.yml | 2 + Makefile.am | 1 + debian/control | 3 +- plugin.c | 11 + plugin_mqtt.c | 462 ++++++++++++++++++++++++++++++++++ plugins.m4 | 22 +- rpm/lcd4linux-ax206.spec | 2 + 8 files changed, 502 insertions(+), 3 deletions(-) create mode 100644 plugin_mqtt.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60387af..d5b9594 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: build-essential debhelper autoconf automake libtool libtool-bin pkg-config \ libusb-1.0-0-dev libgd-dev libvncserver-dev \ libx11-dev libxext-dev libxpm-dev \ + libmosquitto-dev \ git dpkg-buildpackage -us -uc -b echo "=== .deb build successful ===" @@ -77,6 +78,7 @@ jobs: dnf install -y gcc make autoconf automake libtool pkgconfig \ libusbx-devel gd-devel libvncserver-devel \ libX11-devel libXext-devel libXpm-devel \ + mosquitto-devel \ rpm-build git systemd-rpm-macros gettext-devel mkdir -p /root/rpmbuild/{SOURCES,SPECS} tar czf /root/rpmbuild/SOURCES/lcd4linux-ax206-0.11.0.tar.gz \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c0bb58..c3e2ec3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,7 @@ jobs: build-essential debhelper autoconf automake libtool libtool-bin pkg-config \ libusb-1.0-0-dev libgd-dev libvncserver-dev \ libx11-dev libxext-dev libxpm-dev \ + libmosquitto-dev \ git # Update changelog version from tag sed -i "1s/([^)]*)/('"${{ steps.version.outputs.version }}"'-1)/" debian/changelog @@ -95,6 +96,7 @@ jobs: dnf install -y gcc make autoconf automake libtool pkgconfig \ libusbx-devel gd-devel libvncserver-devel \ libX11-devel libXext-devel libXpm-devel \ + mosquitto-devel \ rpm-build git systemd-rpm-macros gettext-devel VERSION='"${{ steps.version.outputs.version }}"' # RPM Version field forbids "-"; replace with "." (sorts lower, correct for pre-releases) diff --git a/Makefile.am b/Makefile.am index bc5eadf..691e88c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -162,6 +162,7 @@ plugin_loadavg.c \ plugin_meminfo.c \ plugin_mpd.c \ plugin_mpris_dbus.c \ +plugin_mqtt.c \ plugin_mysql.c \ plugin_netdev.c \ plugin_netinfo.c \ diff --git a/debian/control b/debian/control index 9543fea..97612f6 100644 --- a/debian/control +++ b/debian/control @@ -13,7 +13,8 @@ Build-Depends: debhelper-compat (= 13), libvncserver-dev, libx11-dev, libxext-dev, - libxpm-dev + libxpm-dev, + libmosquitto-dev Standards-Version: 4.6.0 Homepage: https://github.com/amd989/lcd4linux-ax206 diff --git a/plugin.c b/plugin.c index 98c9b7e..063d417 100644 --- a/plugin.c +++ b/plugin.c @@ -120,6 +120,9 @@ char *Plugins[] = { #ifdef PLUGIN_MPRIS_DBUS "mpris_dbus", #endif +#ifdef PLUGIN_MQTT + "mqtt", +#endif #ifdef PLUGIN_MYSQL "mysql", #endif @@ -230,6 +233,8 @@ int plugin_init_mpd(void); void plugin_exit_mpd(void); int plugin_init_mpris_dbus(void); void plugin_exit_mpris_dbus(void); +int plugin_init_mqtt(void); +void plugin_exit_mqtt(void); int plugin_init_mysql(void); void plugin_exit_mysql(void); int plugin_init_netdev(void); @@ -357,6 +362,9 @@ int plugin_init(void) #ifdef PLUGIN_MPRIS_DBUS plugin_init_mpris_dbus(); #endif +#ifdef PLUGIN_MQTT + plugin_init_mqtt(); +#endif #ifdef PLUGIN_MYSQL plugin_init_mysql(); #endif @@ -478,6 +486,9 @@ void plugin_exit(void) #ifdef PLUGIN_MPRIS_DBUS plugin_exit_mpris_dbus(); #endif +#ifdef PLUGIN_MQTT + plugin_exit_mqtt(); +#endif #ifdef PLUGIN_MYSQL plugin_exit_mysql(); #endif diff --git a/plugin_mqtt.c b/plugin_mqtt.c new file mode 100644 index 0000000..efcd8de --- /dev/null +++ b/plugin_mqtt.c @@ -0,0 +1,462 @@ +/* $Id$ + * $URL$ + * + * MQTT subscriber plugin + * + * Copyright (C) 2026 Alejandro Mora + * + * This file is part of LCD4Linux. + * + * LCD4Linux is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * LCD4Linux is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + */ + +/* + * exported functions: + * + * int plugin_init_mqtt (void) + * adds MQTT subscriber functions + * + */ + + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "debug.h" +#include "plugin.h" +#include "hash.h" +#include "cfg.h" +#include "thread.h" +#include "qprintf.h" + + +#define MQTT_MAX_TOPICS 64 +#define MQTT_MAX_TOPIC_LEN 256 +#define MQTT_MAX_PAYLOAD_LEN 768 + +static char Section[] = "Plugin:MQTT"; + +typedef struct { + char topic[MQTT_MAX_TOPIC_LEN]; + char payload[MQTT_MAX_PAYLOAD_LEN]; + long tv_sec; + long tv_usec; + int active; +} MQTT_SLOT; + +typedef struct { + int count; + int connected; + MQTT_SLOT slots[MQTT_MAX_TOPICS]; +} MQTT_SHM; + +/* IPC resources */ +static int mqtt_mutex = 0; +static int mqtt_shmid = -1; +static pid_t mqtt_pid = -1; +static MQTT_SHM *mqtt_shm = NULL; +static HASH MQTT; + +/* Configuration */ +static char mqtt_broker[256]; +static int mqtt_port; +static char mqtt_user[128]; +static char mqtt_pass[128]; +static char mqtt_clientid[128]; +static char mqtt_cafile[512]; +static char mqtt_certfile[512]; +static char mqtt_keyfile[512]; +static char *mqtt_topics[MQTT_MAX_TOPICS]; +static int mqtt_topic_count = 0; + + +static int configure_mqtt(void) +{ + char *s; + int i; + char key[16]; + + s = cfg_get(Section, "broker", "localhost"); + strncpy(mqtt_broker, s, sizeof(mqtt_broker) - 1); + mqtt_broker[sizeof(mqtt_broker) - 1] = '\0'; + free(s); + + if (cfg_number(Section, "port", 1883, 1, 65535, &mqtt_port) < 0) { + info("[MQTT] no '%s.port' entry from %s, using default 1883", Section, cfg_source()); + mqtt_port = 1883; + } + + s = cfg_get(Section, "user", ""); + strncpy(mqtt_user, s, sizeof(mqtt_user) - 1); + mqtt_user[sizeof(mqtt_user) - 1] = '\0'; + free(s); + + s = cfg_get(Section, "password", ""); + strncpy(mqtt_pass, s, sizeof(mqtt_pass) - 1); + mqtt_pass[sizeof(mqtt_pass) - 1] = '\0'; + free(s); + + s = cfg_get(Section, "clientid", ""); + strncpy(mqtt_clientid, s, sizeof(mqtt_clientid) - 1); + mqtt_clientid[sizeof(mqtt_clientid) - 1] = '\0'; + free(s); + + s = cfg_get(Section, "cafile", ""); + strncpy(mqtt_cafile, s, sizeof(mqtt_cafile) - 1); + mqtt_cafile[sizeof(mqtt_cafile) - 1] = '\0'; + free(s); + + s = cfg_get(Section, "certfile", ""); + strncpy(mqtt_certfile, s, sizeof(mqtt_certfile) - 1); + mqtt_certfile[sizeof(mqtt_certfile) - 1] = '\0'; + free(s); + + s = cfg_get(Section, "keyfile", ""); + strncpy(mqtt_keyfile, s, sizeof(mqtt_keyfile) - 1); + mqtt_keyfile[sizeof(mqtt_keyfile) - 1] = '\0'; + free(s); + + /* Read topics: topic1, topic2, ... topicN */ + mqtt_topic_count = 0; + for (i = 0; i < MQTT_MAX_TOPICS; i++) { + qprintf(key, sizeof(key), "topic%d", i + 1); + s = cfg_get(Section, key, NULL); + if (s == NULL || *s == '\0') { + if (s) + free(s); + break; + } + mqtt_topics[i] = s; + mqtt_topic_count++; + } + + info("[MQTT] broker=%s:%d topics=%d", mqtt_broker, mqtt_port, mqtt_topic_count); + return 0; +} + + +/* Mosquitto callbacks — run in child process */ + +static void on_connect(struct mosquitto *mosq, void *userdata, int rc) +{ + MQTT_SHM *shm = (MQTT_SHM *) userdata; + int i; + + if (rc != 0) { + error("[MQTT] connection refused: %s", mosquitto_connack_string(rc)); + return; + } + + info("[MQTT] connected to %s:%d", mqtt_broker, mqtt_port); + + /* Subscribe to all configured topics */ + for (i = 0; i < mqtt_topic_count; i++) { + if (mosquitto_subscribe(mosq, NULL, mqtt_topics[i], 0) != MOSQ_ERR_SUCCESS) { + error("[MQTT] failed to subscribe to '%s'", mqtt_topics[i]); + } else { + info("[MQTT] subscribed to '%s'", mqtt_topics[i]); + } + } + + mutex_lock(mqtt_mutex); + shm->connected = 1; + mutex_unlock(mqtt_mutex); +} + + +static void on_disconnect(struct mosquitto *mosq __attribute__((unused)), void *userdata, int rc) +{ + MQTT_SHM *shm = (MQTT_SHM *) userdata; + + if (rc != 0) { + info("[MQTT] unexpected disconnect (rc=%d), reconnecting...", rc); + } else { + info("[MQTT] disconnected"); + } + + mutex_lock(mqtt_mutex); + shm->connected = 0; + mutex_unlock(mqtt_mutex); +} + + +static void on_message(struct mosquitto *mosq __attribute__((unused)), void *userdata, + const struct mosquitto_message *msg) +{ + MQTT_SHM *shm = (MQTT_SHM *) userdata; + struct timeval now; + int i, slot; + int payload_len; + + if (msg->topic == NULL) + return; + + gettimeofday(&now, NULL); + + mutex_lock(mqtt_mutex); + + /* Find existing slot for this topic */ + slot = -1; + for (i = 0; i < shm->count; i++) { + if (shm->slots[i].active && strcmp(shm->slots[i].topic, msg->topic) == 0) { + slot = i; + break; + } + } + + /* Allocate new slot if not found */ + if (slot < 0) { + if (shm->count >= MQTT_MAX_TOPICS) { + mutex_unlock(mqtt_mutex); + error("[MQTT] slot table full, dropping message on '%s'", msg->topic); + return; + } + slot = shm->count; + shm->count++; + shm->slots[slot].active = 1; + strncpy(shm->slots[slot].topic, msg->topic, MQTT_MAX_TOPIC_LEN - 1); + shm->slots[slot].topic[MQTT_MAX_TOPIC_LEN - 1] = '\0'; + } + + /* Copy payload */ + payload_len = msg->payloadlen; + if (payload_len > MQTT_MAX_PAYLOAD_LEN - 1) + payload_len = MQTT_MAX_PAYLOAD_LEN - 1; + + if (msg->payload != NULL && payload_len > 0) { + memcpy(shm->slots[slot].payload, msg->payload, payload_len); + } + shm->slots[slot].payload[payload_len] = '\0'; + + /* Record timestamp */ + shm->slots[slot].tv_sec = now.tv_sec; + shm->slots[slot].tv_usec = now.tv_usec; + + mutex_unlock(mqtt_mutex); +} + + +/* Background thread — runs mosquitto event loop in child process */ + +static void mqtt_thread(void *data) +{ + MQTT_SHM *shm = (MQTT_SHM *) data; + struct mosquitto *mosq; + + mosquitto_lib_init(); + + mosq = mosquitto_new(mqtt_clientid[0] ? mqtt_clientid : NULL, true, shm); + if (mosq == NULL) { + error("[MQTT] mosquitto_new() failed: %s", strerror(errno)); + return; + } + + /* Set credentials if configured */ + if (mqtt_user[0]) { + mosquitto_username_pw_set(mosq, mqtt_user, mqtt_pass[0] ? mqtt_pass : NULL); + } + + /* Set TLS if CA file is configured */ + if (mqtt_cafile[0]) { + int tls_rc = mosquitto_tls_set(mosq, + mqtt_cafile, + NULL, + mqtt_certfile[0] ? mqtt_certfile : NULL, + mqtt_keyfile[0] ? mqtt_keyfile : NULL, + NULL); + if (tls_rc != MOSQ_ERR_SUCCESS) { + error("[MQTT] TLS configuration failed: %s", mosquitto_strerror(tls_rc)); + } + } + + /* Set callbacks */ + mosquitto_connect_callback_set(mosq, on_connect); + mosquitto_disconnect_callback_set(mosq, on_disconnect); + mosquitto_message_callback_set(mosq, on_message); + + /* Automatic reconnection: 1s initial, 5s max, no exponential backoff */ + mosquitto_reconnect_delay_set(mosq, 1, 5, false); + + /* Connect to broker */ + if (mosquitto_connect(mosq, mqtt_broker, mqtt_port, 60) != MOSQ_ERR_SUCCESS) { + error("[MQTT] initial connection to %s:%d failed: %s", mqtt_broker, mqtt_port, strerror(errno)); + /* mosquitto_loop_forever will retry */ + } + + /* Block forever — handles reconnection automatically */ + mosquitto_loop_forever(mosq, -1, 1); + + /* Only reached on fatal error */ + mosquitto_destroy(mosq); + mosquitto_lib_cleanup(); +} + + +/* Transfer a topic's value from SHM into the local HASH table */ + +static int mqtt_sync(const char *topic) +{ + int i, age; + + age = hash_age(&MQTT, topic); + + /* Rate-limit: skip SHM read if less than 10ms since last sync */ + if (age > 0 && age <= 10) + return 0; + + mutex_lock(mqtt_mutex); + for (i = 0; i < mqtt_shm->count; i++) { + if (mqtt_shm->slots[i].active && strcmp(mqtt_shm->slots[i].topic, topic) == 0) { + hash_put(&MQTT, topic, mqtt_shm->slots[i].payload); + mutex_unlock(mqtt_mutex); + return 0; + } + } + mutex_unlock(mqtt_mutex); + + /* Topic not found in SHM — may not have received a message yet */ + if (age < 0) { + hash_put(&MQTT, topic, ""); + } + + return 0; +} + + +/* Plugin functions — called by the expression evaluator in the parent process */ + +static void my_mqtt_topic(RESULT * result, RESULT * arg1) +{ + char *topic = R2S(arg1); + char *val; + + mqtt_sync(topic); + + val = hash_get(&MQTT, topic, NULL); + if (val == NULL) + val = ""; + + SetResult(&result, R_STRING, val); +} + + +static void my_mqtt_age(RESULT * result, RESULT * arg1) +{ + char *topic = R2S(arg1); + double age; + + mqtt_sync(topic); + + age = (double) hash_age(&MQTT, topic); + + SetResult(&result, R_NUMBER, &age); +} + + +static void my_mqtt_connected(RESULT * result) +{ + double val; + + mutex_lock(mqtt_mutex); + val = (double) mqtt_shm->connected; + mutex_unlock(mqtt_mutex); + + SetResult(&result, R_NUMBER, &val); +} + + +int plugin_init_mqtt(void) +{ + configure_mqtt(); + + if (mqtt_topic_count == 0) { + info("[MQTT] no topics configured, plugin inactive"); + return 0; + } + + hash_create(&MQTT); + + /* Create IPC resources */ + mqtt_mutex = mutex_create(); + + mqtt_shmid = shm_create((void **) &mqtt_shm, sizeof(MQTT_SHM)); + if (mqtt_shmid < 0) { + error("[MQTT] shared memory allocation failed"); + mutex_destroy(mqtt_mutex); + return -1; + } + memset(mqtt_shm, 0, sizeof(MQTT_SHM)); + + /* Initialize mosquitto library before fork */ + mosquitto_lib_init(); + + /* Create background thread (fork) for MQTT event loop */ + mqtt_pid = thread_create("mqtt-sub", mqtt_thread, mqtt_shm); + if (mqtt_pid < 0) { + error("[MQTT] failed to create subscriber thread"); + shm_destroy(mqtt_shmid, mqtt_shm); + mutex_destroy(mqtt_mutex); + return -1; + } + + AddFunction("mqtt::topic", 1, my_mqtt_topic); + AddFunction("mqtt::age", 1, my_mqtt_age); + AddFunction("mqtt::connected", 0, my_mqtt_connected); + + return 0; +} + + +void plugin_exit_mqtt(void) +{ + int i; + + if (mqtt_pid > 0) { + thread_destroy(mqtt_pid); + mqtt_pid = -1; + } + + if (mqtt_mutex != 0) { + mutex_destroy(mqtt_mutex); + mqtt_mutex = 0; + } + + if (mqtt_shm != NULL) { + shm_destroy(mqtt_shmid, mqtt_shm); + mqtt_shm = NULL; + mqtt_shmid = -1; + } + + hash_destroy(&MQTT); + + for (i = 0; i < mqtt_topic_count; i++) { + if (mqtt_topics[i]) { + free(mqtt_topics[i]); + mqtt_topics[i] = NULL; + } + } + mqtt_topic_count = 0; + + mosquitto_lib_cleanup(); +} diff --git a/plugins.m4 b/plugins.m4 index b37c11e..7e339ff 100644 --- a/plugins.m4 +++ b/plugins.m4 @@ -55,7 +55,7 @@ for plugin in $plugins; do [available plugins:] [ apm,asterisk,button_exec,cpuinfo,dbus,diskstats,dvb,exec,event,] [ fifo,file,gps,hddtemp,huawei,i2c_sensors,iconv,imon,isdn,kvv,] - [ loadavg,meminfo,mpd,mpris_dbus,mysql,netdev,netinfo,pop3,ppp,] + [ loadavg,meminfo,mpd,mpris_dbus,mqtt,mysql,netdev,netinfo,pop3,ppp,] [ proc_stat,python,qnaplog,raspi,sample,seti,statfs,uname,uptime,] [ w1retap,wireless,xmms]) AC_MSG_ERROR([run ./configure --with-plugins=...]) @@ -84,6 +84,7 @@ for plugin in $plugins; do PLUGIN_MEMINFO="yes" PLUGIN_MPD="yes" PLUGIN_MPRIS_DBUS="yes" + PLUGIN_MQTT="yes" PLUGIN_MYSQL="yes" PLUGIN_NETDEV="yes" PLUGIN_NETINFO="yes" @@ -126,6 +127,7 @@ for plugin in $plugins; do PLUGIN_MEMINFO="no" PLUGIN_MPD="no" PLUGIN_MPRIS_DBUS="no" + PLUGIN_MQTT="no" PLUGIN_MYSQL="no" PLUGIN_NETDEV="no" PLUGIN_NETINFO="no" @@ -212,7 +214,10 @@ for plugin in $plugins; do ;; mpris_dbus) PLUGIN_MPRIS_DBUS=$val - ;; + ;; + mqtt) + PLUGIN_MQTT=$val + ;; mysql) PLUGIN_MYSQL=$val ;; @@ -510,6 +515,19 @@ if test "$PLUGIN_MPRIS_DBUS" = "yes"; then fi +# MQTT (Mosquitto) +if test "$PLUGIN_MQTT" = "yes"; then + PKG_CHECK_MODULES(MOSQUITTO, libmosquitto >= 1.0, HAVE_MOSQUITTO="yes", HAVE_MOSQUITTO="no") + if test "x$HAVE_MOSQUITTO" == "xyes"; then + PLUGINS="$PLUGINS plugin_mqtt.o" + PLUGINLIBS="$PLUGINLIBS $MOSQUITTO_LIBS" + CPPFLAGS="$CPPFLAGS $MOSQUITTO_CFLAGS" + AC_DEFINE(PLUGIN_MQTT,1,[mqtt plugin]) + else + AC_MSG_WARN(libmosquitto not found: mqtt plugin disabled) + fi +fi + # MySQL if test "$PLUGIN_MYSQL" = "yes"; then AC_CHECK_HEADERS(mysql/mysql.h, [has_mysql_header="true"], [has_mysql_header="false"]) diff --git a/rpm/lcd4linux-ax206.spec b/rpm/lcd4linux-ax206.spec index 7962819..ee5c553 100644 --- a/rpm/lcd4linux-ax206.spec +++ b/rpm/lcd4linux-ax206.spec @@ -19,11 +19,13 @@ BuildRequires: libX11-devel BuildRequires: libXext-devel BuildRequires: libXpm-devel BuildRequires: gettext-devel +BuildRequires: mosquitto-devel Requires: libusbx Requires: gd Requires: libvncserver Requires: libX11 +Requires: mosquitto %description LCD4Linux fork focused on AX206 USB LCD displays (AIDA64 3.5" USB displays) From 7a8c1e159d97f31e56a60525beece8c27548c40a Mon Sep 17 00:00:00 2001 From: Alejandro Mora Date: Fri, 10 Apr 2026 21:36:50 -0400 Subject: [PATCH 2/2] fix: freebsd issue --- plugins.m4 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins.m4 b/plugins.m4 index 7e339ff..f15d3b5 100644 --- a/plugins.m4 +++ b/plugins.m4 @@ -431,6 +431,13 @@ if test "$PLUGIN_ICONV" = "yes"; then AC_CHECK_FUNC(iconv, [ PLUGINS="$PLUGINS plugin_iconv.o" AC_DEFINE(PLUGIN_ICONV,1,[iconv charset converter plugin]) + dnl On FreeBSD, GNU libiconv installed from ports renames iconv symbols + dnl in its headers (iconv_open -> libiconv_open, etc.) but iconv is also + dnl present in libc, so AC_CHECK_FUNC succeeds without -liconv. The port + dnl headers then cause link failures. Always link -liconv on FreeBSD. + if test "$is_freebsd" = "true"; then + PLUGINLIBS="$PLUGINLIBS -liconv" + fi ], [ AC_CHECK_LIB(iconv, iconv, [ PLUGINS="$PLUGINS plugin_iconv.o"