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..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 @@ -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 '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 b8662c6..8636ad5 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 '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 8a1b0fc..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 @@ -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 '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 7d171e2..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 @@ -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 'callbacks' diff --git a/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh b/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh index dbc9337..6d45a50 100755 --- a/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh +++ b/chirpstack/chirpstack-mqtt-forwarder/files/chirpstack-mqtt-forwarder.sh @@ -21,6 +21,7 @@ configure() { conf_rule_commands "$config_name" conf_rule_metadata "$config_name" + conf_rule_callbacks "$config_name" } conf_rule_concentratord() { @@ -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,40 @@ conf_command_arg() { echo -n "\"$1\"", >>/var/etc/$config_name/chirpstack-mqtt-forwarder.toml } + +conf_rule_callbacks() { + 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 'callbacks' '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 'callbacks' '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 +} 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..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 @@ -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", @@ -173,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; - // DevEUI prefixes + 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; + + // JoinEUI prefixes o = s.option( form.DynamicList, "join_eui_prefix", @@ -183,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")); @@ -216,6 +340,30 @@ return baseclass.extend({ ), ); + // 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, + "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(); }, });