feat: mqtt adapter monitor daemon

This commit is contained in:
CyberMind-FR 2025-12-29 14:44:49 +01:00
parent 54e0b5df6c
commit 1a61dfb260
10 changed files with 267 additions and 19 deletions

View File

@ -4,6 +4,7 @@
- Introduced SecuBox cascade layout helper (CSS + JS) and migrated SecuNav + MQTT tabs to the new layered system. - Introduced SecuBox cascade layout helper (CSS + JS) and migrated SecuNav + MQTT tabs to the new layered system.
- MQTT Bridge now exposes Zigbee/SMSC USB2134B presets with dmesg hints, tty detection, and documentation updates. - MQTT Bridge now exposes Zigbee/SMSC USB2134B presets with dmesg hints, tty detection, and documentation updates.
- New `mqtt-bridge-monitor` daemon keeps adapter metadata (port/bus/health) synced and logs detection events for SecuBox.
- Unified Monitoring + Modules filters and Help view with SecuNav styling. - Unified Monitoring + Modules filters and Help view with SecuNav styling.
- Added Bonus tab to navbar, refreshed alerts action buttons, removed legacy hero blocks. - Added Bonus tab to navbar, refreshed alerts action buttons, removed legacy hero blocks.
- Verified on router (scp + cache reset) and tagged release v0.5.0-A. - Verified on router (scp + cache reset) and tagged release v0.5.0-A.

View File

@ -13,5 +13,5 @@
- Provide rules to forward payloads into SecuBox Alerts. - Provide rules to forward payloads into SecuBox Alerts.
4. **Profiles** 4. **Profiles**
- Promote detected presets into editable device entries (auto-populate `/etc/config/mqtt-bridge`). - Allow LuCI to edit adapter entries (enable/disable, rename, override serial port).
- Support multiple adapters simultaneously and expose health metrics per profile. - Surface per-adapter health metrics/uptime graphs and expose actions (rescan, reset).

View File

@ -4,6 +4,7 @@
- Scaffolded `luci-app-mqtt-bridge` with SecuBox-themed views (overview/devices/settings). - Scaffolded `luci-app-mqtt-bridge` with SecuBox-themed views (overview/devices/settings).
- Added RPC backend (`luci.mqtt-bridge`) and UCI defaults for broker/bridge stats. - Added RPC backend (`luci.mqtt-bridge`) and UCI defaults for broker/bridge stats.
- Added Zigbee/SMSC USB2134B preset detection (USB VID/PID scan, tty hinting, LuCI cards + docs). - Added Zigbee/SMSC USB2134B preset detection (USB VID/PID scan, tty hinting, LuCI cards + docs).
- Added `/usr/sbin/mqtt-bridge-monitor` + init.d service to keep adapter sections (port/bus/health) in sync.
## In Progress ## In Progress
- Flesh out real USB discovery and MQTT client integration. - Flesh out real USB discovery and MQTT client integration.

View File

@ -67,6 +67,17 @@ Match the reported Bus/Device numbers with `/sys/bus/usb/devices/*/busnum` and `
Once the tty node is confirmed, update `/etc/config/mqtt-bridge` and restart the bridge service to bind Zigbee traffic to the MQTT topics defined in the Settings tab. Once the tty node is confirmed, update `/etc/config/mqtt-bridge` and restart the bridge service to bind Zigbee traffic to the MQTT topics defined in the Settings tab.
## Adapter monitor daemon
The package now installs a lightweight watcher (`/usr/sbin/mqtt-bridge-monitor`) that keeps SecuBox informed about attached adapters:
- Configured via `config monitor 'monitor'` (interval in seconds) and `config adapter '...'` sections inside `/etc/config/mqtt-bridge`.
- Managed with the standard init script: `service mqtt-bridge start|stop|status`.
- Writes state transitions to the system log (`logread -e mqtt-bridge-monitor`).
- Updates each adapter section with `detected`, `port`, `bus`, `device`, `health`, and `last_seen`, which the LuCI Devices tab now surfaces.
Use `uci show mqtt-bridge.adapter` to inspect the persisted metadata, or `ubus call luci.mqtt-bridge status` to see the JSON payload consumed by the UI.
## Next steps ## Next steps
- Add real daemon integration with Mosquitto. - Add real daemon integration with Mosquitto.

View File

@ -20,6 +20,10 @@ USB-aware MQTT orchestrator for SecuBox routers. The application discovers USB s
The LuCI views depend on the SecuBox theme bundle included in `luci-theme-secubox`. The LuCI views depend on the SecuBox theme bundle included in `luci-theme-secubox`.
## Daemon / Monitor
`/usr/sbin/mqtt-bridge-monitor` (started via `/etc/init.d/mqtt-bridge`) polls configured adapter presets, logs plug/unplug events, and updates `/etc/config/mqtt-bridge` with `detected`, `port`, `bus`, `device`, and `health` metadata. The Devices view consumes those values to surface Zigbee/serial presets along with `dmesg` hints for `/dev/tty*` alignment.
## Development Notes ## Development Notes
See `.codex/apps/mqtt-bridge/WIP.md` for current tasks and `.codex/apps/mqtt-bridge/TODO.md` for backlog/high-level goals. See `.codex/apps/mqtt-bridge/WIP.md` for current tasks and `.codex/apps/mqtt-bridge/TODO.md` for backlog/high-level goals.

View File

@ -26,7 +26,7 @@ return view.extend({
E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }),
Nav.renderTabs('devices'), Nav.renderTabs('devices'),
this.renderStats(status), this.renderStats(status),
this.renderProfiles(status.profiles || []), this.renderProfiles(status.adapters || [], status.profiles || []),
E('div', { 'class': 'mb-card' }, [ E('div', { 'class': 'mb-card' }, [
E('div', { 'class': 'mb-card-header' }, [ E('div', { 'class': 'mb-card-header' }, [
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '🔌'), _('USB & Sensors')]), E('div', { 'class': 'mb-card-title' }, [E('span', {}, '🔌'), _('USB & Sensors')]),
@ -41,8 +41,10 @@ return view.extend({
]); ]);
}, },
renderProfiles: function(profiles) { renderProfiles: function(adapters, liveProfiles) {
var items = profiles || []; var primary = (adapters && adapters.length) ? adapters : [];
var fallback = (liveProfiles && liveProfiles.length) ? liveProfiles : [];
var items = primary.length ? primary : fallback;
var cards = items.length ? items.map(this.renderProfile.bind(this)) : var cards = items.length ? items.map(this.renderProfile.bind(this)) :
[E('p', { 'style': 'color:var(--mb-muted);' }, [E('p', { 'style': 'color:var(--mb-muted);' },
_('No presets detected yet. Connect a Zigbee adapter or review the documentation below.'))]; _('No presets detected yet. Connect a Zigbee adapter or review the documentation below.'))];
@ -62,13 +64,22 @@ return view.extend({
}, },
renderProfile: function(profile) { renderProfile: function(profile) {
var detected = profile.detected; var detected = profile.detected === true || profile.detected === 1 || profile.detected === '1';
var meta = [ var meta = [
(profile.vendor && profile.product) ? _('VID:PID ') + profile.vendor + ':' + profile.product : null, (profile.vendor && profile.product) ? _('VID:PID ') + profile.vendor + ':' + profile.product : null,
profile.bus ? _('Bus ') + profile.bus : null, profile.bus ? _('Bus ') + profile.bus : null,
profile.device ? _('Device ') + profile.device : null, profile.device ? _('Device ') + profile.device : null,
profile.port ? _('Port ') + profile.port : null profile.port ? _('Port ') + profile.port : null
].filter(Boolean); ].filter(Boolean);
var statusParts = [];
if (detected)
statusParts.push(_('Detected'));
else
statusParts.push(_('Waiting'));
if (profile.health)
statusParts.push(profile.health);
if (profile.last_seen)
statusParts.push(_('Last seen ') + profile.last_seen);
return E('div', { 'class': 'mb-profile-card' }, [ return E('div', { 'class': 'mb-profile-card' }, [
E('div', { 'class': 'mb-profile-header' }, [ E('div', { 'class': 'mb-profile-header' }, [
@ -80,7 +91,7 @@ return view.extend({
]), ]),
E('span', { E('span', {
'class': 'mb-profile-status' + (detected ? ' online' : '') 'class': 'mb-profile-status' + (detected ? ' online' : '')
}, detected ? _('Detected') : _('Waiting')) }, statusParts.join(' • '))
]), ]),
profile.notes ? E('p', { 'class': 'mb-profile-notes' }, profile.notes) : null profile.notes ? E('p', { 'class': 'mb-profile-notes' }, profile.notes) : null
]); ]);

View File

@ -8,6 +8,22 @@ config bridge 'bridge'
option base_topic 'secubox/+/state' option base_topic 'secubox/+/state'
option retention '7' option retention '7'
config monitor 'monitor'
option interval '10'
config adapter 'zigbee_usb2134'
option enabled '1'
option preset 'zigbee_usb2134'
option title 'SMSC USB2134B'
option vendor '0424'
option product '2134'
option notes 'Bus 003 Device 002: ID 0424:2134 SMSC USB2134B'
option detected '0'
option port ''
option bus ''
option device ''
option health 'unknown'
config stats 'stats' config stats 'stats'
option clients '0' option clients '0'
option mps '0' option mps '0'

View File

@ -0,0 +1,12 @@
#!/bin/sh /etc/rc.common
START=95
USE_PROCD=1
SERVICE_NAME="mqtt-bridge-monitor"
start_service() {
procd_open_instance
procd_set_param command /usr/sbin/mqtt-bridge-monitor
procd_set_param respawn 0 5 5
procd_close_instance
}

View File

@ -72,6 +72,47 @@ append_zigbee_profile() {
fi fi
} }
add_adapter_json() {
local section="$1"
local enabled vendor product title preset notes detected port bus device health last_seen
config_get enabled "$section" enabled "1"
config_get vendor "$section" vendor
config_get product "$section" product
config_get title "$section" title
config_get preset "$section" preset
config_get notes "$section" notes
config_get detected "$section" detected
config_get port "$section" port
config_get bus "$section" bus
config_get device "$section" device
config_get health "$section" health
config_get last_seen "$section" last_seen
json_add_object
json_add_string "id" "$section"
[ -n "$title" ] && json_add_string "label" "$title"
[ -n "$preset" ] && json_add_string "preset" "$preset"
[ -n "$vendor" ] && json_add_string "vendor" "$vendor"
[ -n "$product" ] && json_add_string "product" "$product"
json_add_boolean "enabled" "${enabled:-0}"
json_add_boolean "detected" "${detected:-0}"
[ -n "$port" ] && json_add_string "port" "$port"
[ -n "$bus" ] && json_add_string "bus" "$bus"
[ -n "$device" ] && json_add_string "device" "$device"
[ -n "$health" ] && json_add_string "health" "$health"
[ -n "$last_seen" ] && json_add_string "last_seen" "$last_seen"
[ -n "$notes" ] && json_add_string "notes" "$notes"
json_close_object
}
append_configured_adapters() {
json_add_array "adapters"
config_load mqtt-bridge
config_foreach add_adapter_json adapter
json_close_array
}
status() { status() {
json_init json_init
json_add_string "broker" "$(uci -q get mqtt-bridge.broker.host || echo 'localhost')" json_add_string "broker" "$(uci -q get mqtt-bridge.broker.host || echo 'localhost')"
@ -112,20 +153,22 @@ status() {
done done
json_close_array json_close_array
json_add_object "settings" json_add_object "settings"
json_add_string "host" "$(uci -q get mqtt-bridge.broker.host || echo '127.0.0.1')" json_add_string "host" "$(uci -q get mqtt-bridge.broker.host || echo '127.0.0.1')"
json_add_int "port" "$(uci -q get mqtt-bridge.broker.port || echo 1883)" json_add_int "port" "$(uci -q get mqtt-bridge.broker.port || echo 1883)"
json_add_string "username" "$(uci -q get mqtt-bridge.broker.username || echo '')" json_add_string "username" "$(uci -q get mqtt-bridge.broker.username || echo '')"
json_add_string "password" "" json_add_string "password" ""
json_add_string "base_topic" "$(uci -q get mqtt-bridge.bridge.base_topic || echo 'secubox/+/state')" json_add_string "base_topic" "$(uci -q get mqtt-bridge.bridge.base_topic || echo 'secubox/+/state')"
json_add_int "retention" "$(uci -q get mqtt-bridge.bridge.retention || echo 7)" json_add_int "retention" "$(uci -q get mqtt-bridge.bridge.retention || echo 7)"
json_close_object json_close_object
json_add_array "profiles" json_add_array "profiles"
append_zigbee_profile append_zigbee_profile
json_close_array json_close_array
json_dump append_configured_adapters
json_dump
} }
list_devices() { list_devices() {

View File

@ -0,0 +1,149 @@
#!/bin/sh
#
# MQTT Bridge monitor daemon
# Scans configured USB adapters/presets and updates UCI with live metadata.
. /lib/functions.sh
UCI_NAMESPACE="mqtt-bridge"
LOGTAG="mqtt-bridge-monitor"
SCAN_INTERVAL=10
COMMIT_NEEDED=0
log_msg() {
logger -t "$LOGTAG" "$*"
}
find_usb_device() {
local vendor="$1"
local product="$2"
local dev
for dev in /sys/bus/usb/devices/*; do
[ -f "$dev/idVendor" ] || continue
[ -f "$dev/idProduct" ] || continue
local idVendor idProduct
idVendor="$(cat "$dev/idVendor" 2>/dev/null)"
idProduct="$(cat "$dev/idProduct" 2>/dev/null)"
[ "$idVendor" = "$vendor" ] || continue
[ "$idProduct" = "$product" ] || continue
echo "$dev"
return 0
done
return 1
}
find_usb_tty() {
local base="$1"
local path node
for path in "$base" "$base"/* "$base"/*/*; do
[ -d "$path/tty" ] || continue
for node in "$path"/tty/*; do
[ -e "$node" ] || continue
local tty
tty="$(basename "$node")"
[ -e "/dev/$tty" ] && { echo "/dev/$tty"; return 0; }
done
done
return 1
}
set_option_if_changed() {
local section="$1"
local key="$2"
local value="$3"
local current
current="$(uci -q get ${UCI_NAMESPACE}.adapter.${section}.${key} 2>/dev/null)"
[ "$current" = "$value" ] && return
uci set ${UCI_NAMESPACE}.adapter.${section}.${key}="$value"
COMMIT_NEEDED=1
}
clear_option_if_needed() {
local section="$1"
local key="$2"
local current
current="$(uci -q get ${UCI_NAMESPACE}.adapter.${section}.${key} 2>/dev/null)"
[ -z "$current" ] && return
uci delete ${UCI_NAMESPACE}.adapter.${section}.${key}
COMMIT_NEEDED=1
}
update_adapter_section() {
local section="$1"
local enabled vendor product title preset
config_get enabled "$section" enabled "1"
config_get vendor "$section" vendor
config_get product "$section" product
config_get preset "$section" preset
config_get title "$section" title
if [ "$enabled" != "1" ]; then
set_option_if_changed "$section" detected "0"
set_option_if_changed "$section" health "disabled"
return
fi
if [ -z "$vendor" ] || [ -z "$product" ]; then
set_option_if_changed "$section" detected "0"
set_option_if_changed "$section" health "unknown"
return
fi
local dev_path
dev_path="$(find_usb_device "$vendor" "$product")" || dev_path=""
local prev_detected
prev_detected="$(uci -q get ${UCI_NAMESPACE}.adapter.${section}.detected 2>/dev/null)"
if [ -n "$dev_path" ]; then
local bus devnum port ts
bus="$(cat "$dev_path/busnum" 2>/dev/null)"
devnum="$(cat "$dev_path/devnum" 2>/dev/null)"
port="$(find_usb_tty "$dev_path")"
ts="$(date -Iseconds)"
set_option_if_changed "$section" detected "1"
set_option_if_changed "$section" health "online"
[ -n "$bus" ] && set_option_if_changed "$section" bus "$bus"
[ -n "$devnum" ] && set_option_if_changed "$section" device "$devnum"
if [ -n "$port" ]; then
set_option_if_changed "$section" port "$port"
else
clear_option_if_needed "$section" port
fi
set_option_if_changed "$section" last_seen "$ts"
if [ "$prev_detected" != "1" ]; then
log_msg "Adapter $section ($title) detected on bus $bus dev $devnum $port"
fi
else
set_option_if_changed "$section" detected "0"
set_option_if_changed "$section" health "missing"
clear_option_if_needed "$section" port
clear_option_if_needed "$section" bus
clear_option_if_needed "$section" device
if [ "$prev_detected" = "1" ]; then
log_msg "Adapter $section ($title) disconnected"
fi
fi
}
scan_loop() {
while true; do
COMMIT_NEEDED=0
config_load "$UCI_NAMESPACE"
local interval
config_get interval monitor interval
[ -n "$interval" ] && SCAN_INTERVAL="$interval"
config_foreach update_adapter_section adapter
if [ "$COMMIT_NEEDED" -eq 1 ]; then
uci commit "$UCI_NAMESPACE"
fi
sleep "$SCAN_INTERVAL"
done
}
scan_loop