diff --git a/.codex/WIP.md b/.codex/WIP.md index 6ae7b105..7285c98a 100644 --- a/.codex/WIP.md +++ b/.codex/WIP.md @@ -4,6 +4,7 @@ - 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. +- 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. - 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. diff --git a/.codex/apps/mqtt-bridge/TODO.md b/.codex/apps/mqtt-bridge/TODO.md index 6900ae9f..cb1ed618 100644 --- a/.codex/apps/mqtt-bridge/TODO.md +++ b/.codex/apps/mqtt-bridge/TODO.md @@ -13,5 +13,5 @@ - Provide rules to forward payloads into SecuBox Alerts. 4. **Profiles** - - Promote detected presets into editable device entries (auto-populate `/etc/config/mqtt-bridge`). - - Support multiple adapters simultaneously and expose health metrics per profile. + - Allow LuCI to edit adapter entries (enable/disable, rename, override serial port). + - Surface per-adapter health metrics/uptime graphs and expose actions (rescan, reset). diff --git a/.codex/apps/mqtt-bridge/WIP.md b/.codex/apps/mqtt-bridge/WIP.md index ac35b6d0..1e320411 100644 --- a/.codex/apps/mqtt-bridge/WIP.md +++ b/.codex/apps/mqtt-bridge/WIP.md @@ -4,6 +4,7 @@ - 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 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 - Flesh out real USB discovery and MQTT client integration. diff --git a/DOCS/MQTT_BRIDGE.md b/DOCS/MQTT_BRIDGE.md index 87c1b38a..48a36bf5 100644 --- a/DOCS/MQTT_BRIDGE.md +++ b/DOCS/MQTT_BRIDGE.md @@ -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. +## 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 - Add real daemon integration with Mosquitto. diff --git a/luci-app-mqtt-bridge/README.md b/luci-app-mqtt-bridge/README.md index 8bb9c80d..4d941c67 100644 --- a/luci-app-mqtt-bridge/README.md +++ b/luci-app-mqtt-bridge/README.md @@ -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`. +## 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 See `.codex/apps/mqtt-bridge/WIP.md` for current tasks and `.codex/apps/mqtt-bridge/TODO.md` for backlog/high-level goals. diff --git a/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/devices.js b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/devices.js index 60bb7005..add0f09a 100644 --- a/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/devices.js +++ b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/devices.js @@ -26,7 +26,7 @@ return view.extend({ E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }), Nav.renderTabs('devices'), this.renderStats(status), - this.renderProfiles(status.profiles || []), + this.renderProfiles(status.adapters || [], status.profiles || []), E('div', { 'class': 'mb-card' }, [ E('div', { 'class': 'mb-card-header' }, [ E('div', { 'class': 'mb-card-title' }, [E('span', {}, '🔌'), _('USB & Sensors')]), @@ -41,8 +41,10 @@ return view.extend({ ]); }, - renderProfiles: function(profiles) { - var items = profiles || []; + renderProfiles: function(adapters, liveProfiles) { + 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)) : [E('p', { 'style': 'color:var(--mb-muted);' }, _('No presets detected yet. Connect a Zigbee adapter or review the documentation below.'))]; @@ -62,13 +64,22 @@ return view.extend({ }, renderProfile: function(profile) { - var detected = profile.detected; + var detected = profile.detected === true || profile.detected === 1 || profile.detected === '1'; var meta = [ (profile.vendor && profile.product) ? _('VID:PID ') + profile.vendor + ':' + profile.product : null, profile.bus ? _('Bus ') + profile.bus : null, profile.device ? _('Device ') + profile.device : null, profile.port ? _('Port ') + profile.port : null ].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' }, [ E('div', { 'class': 'mb-profile-header' }, [ @@ -80,7 +91,7 @@ return view.extend({ ]), E('span', { 'class': 'mb-profile-status' + (detected ? ' online' : '') - }, detected ? _('Detected') : _('Waiting')) + }, statusParts.join(' • ')) ]), profile.notes ? E('p', { 'class': 'mb-profile-notes' }, profile.notes) : null ]); diff --git a/luci-app-mqtt-bridge/root/etc/config/mqtt-bridge b/luci-app-mqtt-bridge/root/etc/config/mqtt-bridge index 57bf0fa9..807d82e9 100644 --- a/luci-app-mqtt-bridge/root/etc/config/mqtt-bridge +++ b/luci-app-mqtt-bridge/root/etc/config/mqtt-bridge @@ -8,6 +8,22 @@ config bridge 'bridge' option base_topic 'secubox/+/state' 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' option clients '0' option mps '0' diff --git a/luci-app-mqtt-bridge/root/etc/init.d/mqtt-bridge b/luci-app-mqtt-bridge/root/etc/init.d/mqtt-bridge new file mode 100755 index 00000000..920d89ae --- /dev/null +++ b/luci-app-mqtt-bridge/root/etc/init.d/mqtt-bridge @@ -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 +} diff --git a/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge b/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge index b97137e9..934fad01 100755 --- a/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge +++ b/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge @@ -72,6 +72,47 @@ append_zigbee_profile() { 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() { json_init json_add_string "broker" "$(uci -q get mqtt-bridge.broker.host || echo 'localhost')" @@ -112,20 +153,22 @@ status() { done json_close_array - json_add_object "settings" - 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_string "username" "$(uci -q get mqtt-bridge.broker.username || echo '')" - json_add_string "password" "" - 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_close_object +json_add_object "settings" +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_string "username" "$(uci -q get mqtt-bridge.broker.username || echo '')" +json_add_string "password" "" +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_close_object - json_add_array "profiles" - append_zigbee_profile - json_close_array +json_add_array "profiles" +append_zigbee_profile +json_close_array - json_dump +append_configured_adapters + +json_dump } list_devices() { diff --git a/luci-app-mqtt-bridge/root/usr/sbin/mqtt-bridge-monitor b/luci-app-mqtt-bridge/root/usr/sbin/mqtt-bridge-monitor new file mode 100755 index 00000000..07613ced --- /dev/null +++ b/luci-app-mqtt-bridge/root/usr/sbin/mqtt-bridge-monitor @@ -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