From 8c0b155833e0390e84a348711eb8107edf0d4214 Mon Sep 17 00:00:00 2001 From: Alexis DEVLEESCHAUWER Date: Wed, 2 Sep 2026 15:57:30 +0000 Subject: [PATCH 1/5] mqtt-forwarder: add keep_alive/reconnect_interval, crc filters and callbacks options --- .../files/chirpstack-mqtt-forwarder.sh | 73 +++++++++++++++++-- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh b/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh index dbc9337..eac314d 100755 --- a/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh +++ b/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh @@ -18,6 +18,7 @@ configure() { config_foreach conf_rule_concentratord "concentratord" "$config_name" config_foreach conf_rule_mqtt "mqtt" "$config_name" config_foreach conf_rule_filters "filters" "$config_name" + config_foreach conf_rule_callbacks "callbacks" "$config_name" conf_rule_commands "$config_name" conf_rule_metadata "$config_name" @@ -47,7 +48,7 @@ conf_rule_concentratord() { conf_rule_mqtt() { local cfg="$1" local config_name="$2" - local topic_prefix json server username password qos clean_session client_id ca_cert tls_cert tls_key + local topic_prefix json server username password qos clean_session keep_alive_interval client_id ca_cert tls_cert tls_key reconnect_interval config_get topic_prefix $cfg topic_prefix config_get json $cfg json @@ -57,9 +58,11 @@ conf_rule_mqtt() { config_get qos $cfg qos config_get_bool clean_session $cfg clean_session config_get client_id $cfg client_id + config_get keep_alive_interval $cfg keep_alive_interval config_get ca_cert $cfg ca_cert config_get tls_cert $cfg tls_cert config_get tls_key $cfg tls_key + config_get reconnect_interval $cfg reconnect_interval if [ "$json" = "1" ]; then json="true" @@ -73,6 +76,10 @@ conf_rule_mqtt() { clean_session="false" fi + if [ "$keep_alive_interval" = "" ]; then + keep_alive_interval="30s" + fi + if [ "$ca_cert" != "" ]; then echo "$ca_cert" >/var/etc/$config_name/ca.pem ca_cert="/var/etc/$config_name/ca.pem" @@ -98,26 +105,39 @@ conf_rule_mqtt() { qos=$qos clean_session=$clean_session client_id="$client_id" + keep_alive_interval="$keep_alive_interval" ca_cert="$ca_cert" tls_cert="$tls_cert" tls_key="$tls_key" EOF + + if [ "$reconnect_interval" != "" ]; then + echo "reconnect_interval=\"$reconnect_interval\"" >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml + fi + } conf_rule_filters() { local cfg="$1" local config_name="$2" + local forward_crc_ok forward_crc_invalid forward_crc_missing lorawan_only - config_get lorawan_only $cfg lorawan_only + config_get_bool forward_crc_ok $cfg forward_crc_ok true + config_get_bool forward_crc_invalid $cfg forward_crc_invalid false + config_get_bool forward_crc_missing $cfg forward_crc_missing false + config_get_bool lorawan_only $cfg lorawan_only - if [ "$lorawan_only" = "1" ]; then - lorawan_only="true" - else - lorawan_only="false" - fi + # convert uci bool (1 or 0) to toml bool (true or false) + [ "$forward_crc_invalid" = "1" ] && forward_crc_invalid="true" || forward_crc_invalid="false" + [ "$forward_crc_missing" = "1" ] && forward_crc_missing="true" || forward_crc_missing="false" + [ "$forward_crc_ok" = "1" ] && forward_crc_ok="true" || forward_crc_ok="false" + [ "$lorawan_only" = "1" ] && lorawan_only="true" || lorawan_only="false" cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF [backend.filters] + forward_crc_ok=$forward_crc_ok + forward_crc_invalid=$forward_crc_invalid + forward_crc_missing=$forward_crc_missing lorawan_only=$lorawan_only dev_addr_prefixes=[ EOF @@ -173,7 +193,6 @@ conf_rule_metadata() { EOF config_foreach conf_command "metadata" "$config_name" - } # Foreach config 'type' 'key' @@ -237,3 +256,41 @@ conf_command_arg() { echo -n "\"$1\"", >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml } + +conf_rule_callbacks() { + local cfg="$1" + local config_name="$2" + + cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF + [callbacks] + EOF + + # on_mqtt_connected + cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF + on_mqtt_connected=[ + EOF + + config_list_foreach $cfg on_mqtt_connected conf_rule_callbacks_on_mqtt "$config_name" + + cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF + ] + EOF + + # on_mqtt_connection_error + cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF + on_mqtt_connection_error=[ + EOF + + config_list_foreach $cfg on_mqtt_connection_error conf_rule_callbacks_on_mqtt "$config_name" + + cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF + ] + EOF +} + +conf_rule_callbacks_on_mqtt() { + local config_name="$2" + cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF + "$1", + EOF +} From 9338d2238265d257f710048fe025d5d7d21364d9 Mon Sep 17 00:00:00 2001 From: Alexis DEVLEESCHAUWER Date: Fri, 4 Sep 2026 07:29:46 +0000 Subject: [PATCH 2/5] mqtt-forwarder: add LuCI UI for keep_alive/reconnect_interval, crc filters and callbacks options --- .../resources/chirpstack-mqtt-forwarder.js | 105 +++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js b/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js index fab1857..1d9a832 100644 --- a/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js +++ b/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js @@ -79,6 +79,50 @@ return baseclass.extend({ return "Enter a valid MQTT server address."; }; + // keep alive interval + o = s.option( + form.Value, + "keep_alive_interval", + _("Keep Alive Interval"), + _( + "Defines the maximum time that should pass without communication between the client and server", + ), + ); + o.optional = true; + o.validate = function (section_id, value) { + if (!value) { + return true; + } + + if (/^\d+[smhd]$/.test(value.trim())) { + return true; + } + + return "Enter a valid duration (e.g. 30s, 5m, 1h, 1d)"; + }; + + // reconnect_interval + o = s.option( + form.Value, + "reconnect_interval", + _("Reconnect Interval"), + _( + "defines the reconnection interval to the MQTT broker in case of network issues", + ), + ); + o.optional = true; + o.validate = function (section_id, value) { + if (!value) { + return true; + } + + if (/^\d+[smhd]$/.test(value.trim())) { + return true; + } + + return "Enter a valid duration (e.g. 30s, 5m, 1h, 1d)"; + }; + // username o = s.option( form.Value, @@ -154,6 +198,42 @@ return baseclass.extend({ s = m.section(form.TypedSection, "filters", _("Filter configuration")); s.anonymous = true; + // forward_crc_ok + o = s.option( + form.Flag, + "forward_crc_ok", + _("Forward CRC ok"), + _( + "Forward LoRaWAN frame when CRC is valid", + ), + ); + o.default = o.enabled; + o.rmempty = false; + + // forward_crc_invalid + o = s.option( + form.Flag, + "forward_crc_invalid", + _("Forward CRC invalid"), + _( + "Forward LoRaWAN frame when CRC is invalid", + ), + ); + o.default = o.disabled; + o.rmempty = false; + + // forward_crc_missing + o = s.option( + form.Flag, + "forward_crc_missing", + _("Forward CRC missing"), + _( + "Forward LoRaWAN frame when CRC is missing", + ), + ); + o.default = o.disabled; + o.rmempty = false; + // LoRaWAN only o = s.option( form.Flag, @@ -164,7 +244,7 @@ return baseclass.extend({ ), ); - // DevAddr prefixs + // DevAddr prefixes o = s.option( form.DynamicList, "dev_addr_prefix", @@ -216,6 +296,27 @@ return baseclass.extend({ ), ); + s = m.section(form.TypedSection, "callbacks", _("Callbacks")); + s.anonymous = true; + + s.option( + form.DynamicList, + "on_mqtt_connected", + _("On mqtt Connected"), + _( + "Commands triggered when MQTT connected", + ), + ); + + s.option( + form.DynamicList, + "on_mqtt_connection_error", + _("On mqtt Connection Error"), + _( + "Commands triggered when MQTT Connection is in Error", + ), + ); + return m.render(); }, -}); +}); \ No newline at end of file From 6deb18ee7467b15ede8bd69e119c65b208187d75 Mon Sep 17 00:00:00 2001 From: Alexis DEVLEESCHAUWER Date: Fri, 4 Sep 2026 10:58:38 +0000 Subject: [PATCH 3/5] feat: add validator for DevAddr and JoinEUI --- .../resources/chirpstack-mqtt-forwarder.js | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js b/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js index 1d9a832..26d3407 100644 --- a/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js +++ b/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js @@ -253,8 +253,30 @@ return baseclass.extend({ "Filter uplinks based on the configured DevAddr prefixes (e.g. '0000ff00/24'). If no filters have been configured, filtering is disabled.", ), ); + o.validate = function (section_id, value) { + if (value == null || value === "") + return true; + + var parts = value.split("/"); + + if (parts.length !== 2) + return _("Expected the format {hex}/{mask}, e.g. 0000ff00/24"); + + if (!/^[0-9a-fA-F]{8}$/.test(parts[0])) + return _("Invalid address '%s'/ must be exactly 8 hexadecimal digits").format(parts[0]); + + if (!/^\d+$/.test(parts[1])) + return _("Invalid mask '/%s' must be a number").format(parts[1]); + + // +parts[1] trick to convert str to number + if (+parts[1] > 32) + return _("Invalid mask '/%s' must be between 0 and 32").format(parts[1]); + + return true; + }; + o.rmempty = true; - // DevEUI prefixes + // JoinEUI prefixes o = s.option( form.DynamicList, "join_eui_prefix", @@ -263,6 +285,28 @@ return baseclass.extend({ "Filter join-requests based on the configured JoinEUI prefixes (e.g. '0000ff0000000000/24'). If no filters have been configured, filtering is disabled.", ), ); + o.validate = function (section_id, value) { + if (value == null || value === "") + return true; + + var parts = value.split("/"); + + if (parts.length !== 2) + return _("Expected the format {hex}/{mask}, e.g. 0000ff0000000000/24"); + + if (!/^[0-9a-fA-F]{16}$/.test(parts[0])) + return _("Invalid address '%s'/ must be exactly 16 hexadecimal digits").format(parts[0]); + + if (!/^\d+$/.test(parts[1])) + return _("Invalid mask '/%s' must be a number").format(parts[1]); + + // +parts[1] trick to convert str to number + if (+parts[1] > 64) + return _("Invalid mask '/%s' must be between 0 and 64").format(parts[1]); + + return true; + }; + o.rmempty = true; // Commands s = m.section(form.TypedSection, "commands", _("Commands")); @@ -319,4 +363,4 @@ return baseclass.extend({ return m.render(); }, -}); \ No newline at end of file +}); From dd1e1f919e415714654aa87360d1059fdf091e57 Mon Sep 17 00:00:00 2001 From: Alexis DEVLEESCHAUWER Date: Mon, 7 Sep 2026 09:34:39 +0000 Subject: [PATCH 4/5] fix: added empty default config for callbacks in init chirpstack-mqtt-forwarder-mesh.config --- .../files/chirpstack-mqtt-forwarder-mesh.config | 2 ++ .../files/chirpstack-mqtt-forwarder.config | 2 ++ .../files/chirpstack-mqtt-forwarder-slot1.config | 2 ++ .../files/chirpstack-mqtt-forwarder-slot2.config | 2 ++ 4 files changed, 8 insertions(+) diff --git a/chirpstack/chirpstack-mqtt-forwarder-mesh/files/chirpstack-mqtt-forwarder-mesh.config b/chirpstack/chirpstack-mqtt-forwarder-mesh/files/chirpstack-mqtt-forwarder-mesh.config index 97d3844..3ae736d 100644 --- a/chirpstack/chirpstack-mqtt-forwarder-mesh/files/chirpstack-mqtt-forwarder-mesh.config +++ b/chirpstack/chirpstack-mqtt-forwarder-mesh/files/chirpstack-mqtt-forwarder-mesh.config @@ -11,3 +11,5 @@ config filters config concentratord option event_url 'ipc:///tmp/gateway_relay_event' option command_url 'ipc:///tmp/gateway_relay_command' + +config callbacks \ No newline at end of file diff --git a/chirpstack/chirpstack-mqtt-forwarder-single/files/chirpstack-mqtt-forwarder.config b/chirpstack/chirpstack-mqtt-forwarder-single/files/chirpstack-mqtt-forwarder.config index b8662c6..7c94330 100644 --- a/chirpstack/chirpstack-mqtt-forwarder-single/files/chirpstack-mqtt-forwarder.config +++ b/chirpstack/chirpstack-mqtt-forwarder-single/files/chirpstack-mqtt-forwarder.config @@ -7,3 +7,5 @@ config mqtt option qos '0' config filters + +config callbacks \ No newline at end of file diff --git a/chirpstack/chirpstack-mqtt-forwarder-slot1/files/chirpstack-mqtt-forwarder-slot1.config b/chirpstack/chirpstack-mqtt-forwarder-slot1/files/chirpstack-mqtt-forwarder-slot1.config index 8a1b0fc..eb5ba0c 100644 --- a/chirpstack/chirpstack-mqtt-forwarder-slot1/files/chirpstack-mqtt-forwarder-slot1.config +++ b/chirpstack/chirpstack-mqtt-forwarder-slot1/files/chirpstack-mqtt-forwarder-slot1.config @@ -11,3 +11,5 @@ config filters config concentratord option event_url 'ipc:///tmp/concentratord_slot1_event' option command_url 'ipc:///tmp/concentratord_slot1_command' + +config callbacks \ No newline at end of file diff --git a/chirpstack/chirpstack-mqtt-forwarder-slot2/files/chirpstack-mqtt-forwarder-slot2.config b/chirpstack/chirpstack-mqtt-forwarder-slot2/files/chirpstack-mqtt-forwarder-slot2.config index 7d171e2..f563828 100644 --- a/chirpstack/chirpstack-mqtt-forwarder-slot2/files/chirpstack-mqtt-forwarder-slot2.config +++ b/chirpstack/chirpstack-mqtt-forwarder-slot2/files/chirpstack-mqtt-forwarder-slot2.config @@ -11,3 +11,5 @@ config filters config concentratord option event_url 'ipc:///tmp/concentratord_slot2_event' option command_url 'ipc:///tmp/concentratord_slot2_command' + +config callbacks \ No newline at end of file From dd914bb9ed59262bc165ac8b260f044aac3980a4 Mon Sep 17 00:00:00 2001 From: Alexis DEVLEESCHAUWER Date: Mon, 7 Sep 2026 12:32:48 +0000 Subject: [PATCH 5/5] feat: use NamedSection instead TypedSection for callbacks --- .../files/chirpstack-mqtt-forwarder-mesh.config | 2 +- .../files/chirpstack-mqtt-forwarder.config | 2 +- .../files/chirpstack-mqtt-forwarder-slot1.config | 2 +- .../files/chirpstack-mqtt-forwarder-slot2.config | 2 +- .../files/chirpstack-mqtt-forwarder.sh | 7 +++---- .../luci-static/resources/chirpstack-mqtt-forwarder.js | 7 +++++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/chirpstack/chirpstack-mqtt-forwarder-mesh/files/chirpstack-mqtt-forwarder-mesh.config b/chirpstack/chirpstack-mqtt-forwarder-mesh/files/chirpstack-mqtt-forwarder-mesh.config index 3ae736d..b0d2f16 100644 --- a/chirpstack/chirpstack-mqtt-forwarder-mesh/files/chirpstack-mqtt-forwarder-mesh.config +++ b/chirpstack/chirpstack-mqtt-forwarder-mesh/files/chirpstack-mqtt-forwarder-mesh.config @@ -12,4 +12,4 @@ config concentratord option event_url 'ipc:///tmp/gateway_relay_event' option command_url 'ipc:///tmp/gateway_relay_command' -config callbacks \ No newline at end of file +config callbacks 'callbacks' diff --git a/chirpstack/chirpstack-mqtt-forwarder-single/files/chirpstack-mqtt-forwarder.config b/chirpstack/chirpstack-mqtt-forwarder-single/files/chirpstack-mqtt-forwarder.config index 7c94330..8636ad5 100644 --- a/chirpstack/chirpstack-mqtt-forwarder-single/files/chirpstack-mqtt-forwarder.config +++ b/chirpstack/chirpstack-mqtt-forwarder-single/files/chirpstack-mqtt-forwarder.config @@ -8,4 +8,4 @@ config mqtt config filters -config callbacks \ No newline at end of file +config callbacks 'callbacks' diff --git a/chirpstack/chirpstack-mqtt-forwarder-slot1/files/chirpstack-mqtt-forwarder-slot1.config b/chirpstack/chirpstack-mqtt-forwarder-slot1/files/chirpstack-mqtt-forwarder-slot1.config index eb5ba0c..4b1397e 100644 --- a/chirpstack/chirpstack-mqtt-forwarder-slot1/files/chirpstack-mqtt-forwarder-slot1.config +++ b/chirpstack/chirpstack-mqtt-forwarder-slot1/files/chirpstack-mqtt-forwarder-slot1.config @@ -12,4 +12,4 @@ config concentratord option event_url 'ipc:///tmp/concentratord_slot1_event' option command_url 'ipc:///tmp/concentratord_slot1_command' -config callbacks \ No newline at end of file +config callbacks 'callbacks' diff --git a/chirpstack/chirpstack-mqtt-forwarder-slot2/files/chirpstack-mqtt-forwarder-slot2.config b/chirpstack/chirpstack-mqtt-forwarder-slot2/files/chirpstack-mqtt-forwarder-slot2.config index f563828..0904616 100644 --- a/chirpstack/chirpstack-mqtt-forwarder-slot2/files/chirpstack-mqtt-forwarder-slot2.config +++ b/chirpstack/chirpstack-mqtt-forwarder-slot2/files/chirpstack-mqtt-forwarder-slot2.config @@ -12,4 +12,4 @@ config concentratord option event_url 'ipc:///tmp/concentratord_slot2_event' option command_url 'ipc:///tmp/concentratord_slot2_command' -config callbacks \ No newline at end of file +config callbacks 'callbacks' diff --git a/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh b/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh index eac314d..6d45a50 100755 --- a/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh +++ b/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh @@ -18,10 +18,10 @@ configure() { config_foreach conf_rule_concentratord "concentratord" "$config_name" config_foreach conf_rule_mqtt "mqtt" "$config_name" config_foreach conf_rule_filters "filters" "$config_name" - config_foreach conf_rule_callbacks "callbacks" "$config_name" conf_rule_commands "$config_name" conf_rule_metadata "$config_name" + conf_rule_callbacks "$config_name" } conf_rule_concentratord() { @@ -258,7 +258,6 @@ conf_command_arg() { } conf_rule_callbacks() { - local cfg="$1" local config_name="$2" cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF @@ -270,7 +269,7 @@ conf_rule_callbacks() { on_mqtt_connected=[ EOF - config_list_foreach $cfg on_mqtt_connected conf_rule_callbacks_on_mqtt "$config_name" + config_list_foreach 'callbacks' 'on_mqtt_connected' conf_rule_callbacks_on_mqtt "$config_name" cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF ] @@ -281,7 +280,7 @@ conf_rule_callbacks() { on_mqtt_connection_error=[ EOF - config_list_foreach $cfg on_mqtt_connection_error conf_rule_callbacks_on_mqtt "$config_name" + config_list_foreach 'callbacks' 'on_mqtt_connection_error' conf_rule_callbacks_on_mqtt "$config_name" cat >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml <<-EOF ] diff --git a/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js b/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js index 26d3407..ca476ac 100644 --- a/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js +++ b/chirpstack/luci-app-chirpstack-mqtt-forwarder/htdocs/luci-static/resources/chirpstack-mqtt-forwarder.js @@ -340,8 +340,11 @@ return baseclass.extend({ ), ); - s = m.section(form.TypedSection, "callbacks", _("Callbacks")); - s.anonymous = true; + // force create section callbacks if it's missing + if (!uci.get(mqttForwarderConfig, "callbacks")) + uci.add(mqttForwarderConfig, "callbacks", "callbacks"); + + s = m.section(form.NamedSection, "callbacks", "callbacks", _("Callbacks")); s.option( form.DynamicList,