diff --git a/prisma/migrations/20240828105009_remove_mqtt_connection_state_column/migration.sql b/prisma/migrations/20240828105009_remove_mqtt_connection_state_column/migration.sql new file mode 100644 index 0000000..03dd68b --- /dev/null +++ b/prisma/migrations/20240828105009_remove_mqtt_connection_state_column/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `mqtt_connection_state` on the `nodes` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE `nodes` DROP COLUMN `mqtt_connection_state`; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e656dc5..eeb0cb4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -48,7 +48,7 @@ model Node { neighbours Json? neighbours_updated_at DateTime? - mqtt_connection_state String? + // this column tracks when an mqtt gateway node uplinked a packet mqtt_connection_state_updated_at DateTime? created_at DateTime @default(now()) diff --git a/src/mqtt.js b/src/mqtt.js index 6b8d86f..d8939d4 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -605,36 +605,6 @@ client.on("connect", () => { client.on("message", async (topic, message) => { try { - // handle node status - if(topic.includes("/stat/!")){ - try { - - // get node id and status - const nodeIdHex = topic.split("/").pop(); - const mqttConnectionState = message.toString(); - - // convert node id hex to int value - const nodeId = convertHexIdToNumericId(nodeIdHex); - - // update mqtt connection state for node - await prisma.node.updateMany({ - where: { - node_id: nodeId, - }, - data: { - mqtt_connection_state: mqttConnectionState, - mqtt_connection_state_updated_at: new Date(), - }, - }); - - // no need to continue with this mqtt message - return; - - } catch(e) { - console.error(e); - } - } - // decode service envelope const envelope = ServiceEnvelope.decode(message); if(!envelope.packet){ @@ -661,6 +631,20 @@ client.on("message", async (topic, message) => { } } + // track when a node last gated a packet to mqtt + try { + await prisma.node.updateMany({ + where: { + node_id: convertHexIdToNumericId(envelope.gatewayId), + }, + data: { + mqtt_connection_state_updated_at: new Date(), + }, + }); + } catch(e) { + // don't care if updating mqtt timestamp fails + } + // attempt to decrypt encrypted packets const isEncrypted = envelope.packet.encrypted?.length > 0; if(isEncrypted){ diff --git a/src/public/index.html b/src/public/index.html index b6f2340..8ed838c 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -1485,6 +1485,29 @@ + +
+ +
Nodes that have not uplinked to MQTT in this time will show as blue icons. Reload to update map.
+ +
+
@@ -1777,6 +1800,20 @@ } } + function getConfigNodesDisconnectedAgeInSeconds() { + // default to showing nodes as recently uplinked if heard in the last 30 minutes + const value = localStorage.getItem("config_nodes_disconnected_age_in_seconds"); + return value != null ? parseInt(value) : 1800; + } + + function setConfigNodesDisconnectedAgeInSeconds(value) { + if(value != null){ + return localStorage.setItem("config_nodes_disconnected_age_in_seconds", value); + } else { + return localStorage.removeItem("config_nodes_disconnected_age_in_seconds"); + } + } + function getConfigNodesOfflineAgeInSeconds() { const value = localStorage.getItem("config_nodes_offline_age_in_seconds"); return value != null ? parseInt(value) : null; @@ -1836,6 +1873,7 @@ isShowingAnnouncement: this.shouldShowAnnouncement(), configNodesMaxAgeInSeconds: window.getConfigNodesMaxAgeInSeconds(), + configNodesDisconnectedAgeInSeconds: window.getConfigNodesDisconnectedAgeInSeconds(), configNodesOfflineAgeInSeconds: window.getConfigNodesOfflineAgeInSeconds(), configWaypointsMaxAgeInSeconds: window.getConfigWaypointsMaxAgeInSeconds(), configNeighboursMaxDistanceInMeters: window.getConfigNeighboursMaxDistanceInMeters(), @@ -2791,6 +2829,9 @@ configNodesMaxAgeInSeconds() { window.setConfigNodesMaxAgeInSeconds(this.configNodesMaxAgeInSeconds); }, + configNodesDisconnectedAgeInSeconds() { + window.setConfigNodesDisconnectedAgeInSeconds(this.configNodesDisconnectedAgeInSeconds); + }, configNodesOfflineAgeInSeconds() { window.setConfigNodesOfflineAgeInSeconds(this.configNodesOfflineAgeInSeconds); }, @@ -3455,6 +3496,14 @@ return string.replace(//g, ">"); } + // determine if node was recently heard uplinking packets to mqtt + function hasNodeUplinkedToMqttRecently(node) { + const now = moment(); + const configNodesDisconnectedAgeInSeconds = getConfigNodesDisconnectedAgeInSeconds(); + const millisecondsSinceNodeLastUplinkedToMqtt = now.diff(moment(node.mqtt_connection_state_updated_at)); + return millisecondsSinceNodeLastUplinkedToMqtt < configNodesDisconnectedAgeInSeconds * 1000; + } + function onNodesUpdated(updatedNodes) { // clear nodes cache @@ -3500,9 +3549,6 @@ // icon based on mqtt connection state var icon = iconMqttDisconnected; - if(node.mqtt_connection_state === "online"){ - icon = iconMqttConnected; - } // use offline icon for nodes older than configured node offline age const now = moment(); @@ -3514,12 +3560,18 @@ } } + // determine if node was recently heard uplinking packets to mqtt + const nodeHasUplinkedToMqttRecently = hasNodeUplinkedToMqttRecently(node); + if(nodeHasUplinkedToMqttRecently){ + icon = iconMqttConnected; + } + // create node marker var marker = L.marker([node.latitude, longitude], { icon: icon, tagName: node.node_id, // we want to show online nodes above offline, but without needing to use separate layer groups - zIndexOffset: node.mqtt_connection_state === "online" ? 1000 : -1000, + zIndexOffset: nodeHasUplinkedToMqttRecently ? 1000 : -1000, }).on('click', function(event) { // close tooltip on click to prevent tooltip and popup showing at same time event.target.closeTooltip(); @@ -3935,15 +3987,16 @@ function getTooltipContentForNode(node) { - // human friendly connection state - var mqttStatus = ""; - var mqttStatusLastUpdated = node.mqtt_connection_state_updated_at ? `(${moment(new Date(node.mqtt_connection_state_updated_at)).fromNow()})` : ""; - if(node.mqtt_connection_state === "online"){ - mqttStatus = `Connected ${mqttStatusLastUpdated}`; - } else if(node.mqtt_connection_state === "offline"){ - mqttStatus = `Disconnected ${mqttStatusLastUpdated}`; - } else { - mqttStatus = `Disconnected`; + // determine if node was recently heard uplinking packets to mqtt + const nodeHasUplinkedToMqttRecently = hasNodeUplinkedToMqttRecently(node); + var mqttStatus = `Disconnected`; + if(node.mqtt_connection_state_updated_at){ + var mqttStatusUpdatedAt = moment(new Date(node.mqtt_connection_state_updated_at)).fromNow(); + if(nodeHasUplinkedToMqttRecently){ + mqttStatus = `Connected (${mqttStatusUpdatedAt})`; + } else { + mqttStatus = `Disconnected (${mqttStatusUpdatedAt})`; + } } var loraFrequencyRange = getRegionFrequencyRange(node.region_name);