forked from iarv/meshtastic-map
Compare commits
7 Commits
Collect-ed
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fd6730e0d | ||
|
|
1748079708 | ||
|
|
4a4b5fb7f3 | ||
|
|
dc9a45a62a | ||
|
|
f3154cb97b | ||
|
|
57c10383e2 | ||
|
|
f690bb65a7 |
@@ -1096,7 +1096,7 @@ client.on("message", async (topic, message) => {
|
||||
|
||||
// Extract edges from neighbour info
|
||||
try {
|
||||
const fromNodeId = envelope.packet.from;
|
||||
const toNodeId = envelope.packet.from;
|
||||
const neighbors = neighbourInfo.neighbors || [];
|
||||
const packetId = envelope.packet.id;
|
||||
const channelId = envelope.channelId;
|
||||
@@ -1115,7 +1115,7 @@ client.on("message", async (topic, message) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const toNodeId = neighbour.nodeId;
|
||||
const fromNodeId = neighbour.nodeId;
|
||||
const snr = neighbour.snr;
|
||||
|
||||
// Fetch node positions from Node table
|
||||
|
||||
@@ -1303,29 +1303,6 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- configNodesDisconnectedAgeInSeconds -->
|
||||
<div class="p-2">
|
||||
<label class="block text-sm font-medium text-gray-900">Nodes Disconnected Age</label>
|
||||
<div class="text-xs text-gray-600 mb-2">Nodes that have not uplinked to MQTT in this time will show as blue icons. Reload to update map.</div>
|
||||
<select v-model="configNodesDisconnectedAgeInSeconds" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||
<option value="900">15 minutes</option>
|
||||
<option value="1800">30 minutes</option>
|
||||
<option value="2700">45 minutes</option>
|
||||
<option value="3600">1 hour</option>
|
||||
<option value="7200">2 hours</option>
|
||||
<option value="10800">3 hours</option>
|
||||
<option value="21600">6 hours</option>
|
||||
<option value="43200">12 hours</option>
|
||||
<option value="86400">24 hours</option>
|
||||
<option value="172800">2 days</option>
|
||||
<option value="259200">3 days</option>
|
||||
<option value="345600">4 days</option>
|
||||
<option value="432000">5 days</option>
|
||||
<option value="518400">6 days</option>
|
||||
<option value="604800">7 days</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- configNodesOfflineAgeInSeconds -->
|
||||
<div class="p-2">
|
||||
<label class="block text-sm font-medium text-gray-900">Nodes Offline Age</label>
|
||||
@@ -1374,8 +1351,8 @@
|
||||
|
||||
<!-- configConnectionsTimePeriodInSeconds -->
|
||||
<div class="p-2">
|
||||
<label class="block text-sm font-medium text-gray-900">Connections Time Period</label>
|
||||
<div class="text-xs text-gray-600 mb-2">Edges within this time period are shown in the Connections layer. Reload to update map.</div>
|
||||
<label class="block text-sm font-medium text-gray-900">Connections Max Age</label>
|
||||
<div class="text-xs text-gray-600 mb-2">Edges from traceroutes and neighbour info within this time period are shown in the Connections layer. Reload to update map.</div>
|
||||
<select v-model="configConnectionsTimePeriodInSeconds" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||
<option value="300">5 minutes</option>
|
||||
<option value="900">15 minutes</option>
|
||||
@@ -1402,6 +1379,31 @@
|
||||
<div class="text-xs text-gray-600">Colors the connection lines by the average SNR in the worst direction. Reload to update map.</div>
|
||||
</div>
|
||||
|
||||
<!-- configConnectionsBidirectionalOnly -->
|
||||
<div class="p-2">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input type="checkbox" v-model="configConnectionsBidirectionalOnly" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300" required>
|
||||
</div>
|
||||
<label class="ml-2 text-sm font-medium text-gray-900">Bidirectional Connections Only</label>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">Only show connections where data flows in both directions. Reload to update map.</div>
|
||||
</div>
|
||||
|
||||
<!-- configConnectionsMinSnrDb -->
|
||||
<div class="p-2">
|
||||
<label class="block text-sm font-medium text-gray-900">Connections Minimum SNR (dB)</label>
|
||||
<div class="text-xs text-gray-600 mb-2">Only show connections where at least one direction has SNR above this threshold. Leave empty to show all connections. Reload to update map.</div>
|
||||
<input type="number" v-model="configConnectionsMinSnrDb" placeholder="e.g. -10" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||
<div class="mt-2 flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input type="checkbox" v-model="configConnectionsBidirectionalMinSnr" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300" required>
|
||||
</div>
|
||||
<label class="ml-2 text-sm font-medium text-gray-900">Bidirectional Minimum SNR</label>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 ml-6">If checked, all existing directions must meet the minimum SNR threshold (both directions if bidirectional, single direction if unidirectional).</div>
|
||||
</div>
|
||||
|
||||
<!-- configConnectionsMaxDistanceInMeters -->
|
||||
<div class="p-2">
|
||||
<label class="block text-sm font-medium text-gray-900">Connections Max Distance (meters)</label>
|
||||
@@ -1657,20 +1659,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1719,8 +1707,8 @@
|
||||
|
||||
function getConfigConnectionsTimePeriodInSeconds() {
|
||||
const value = localStorage.getItem("config_connections_time_period_in_seconds");
|
||||
// default to 24 hours if unset
|
||||
return value != null ? parseInt(value) : 86400;
|
||||
// default to 7 days if unset
|
||||
return value != null ? parseInt(value) : 604800;
|
||||
}
|
||||
|
||||
function setConfigConnectionsTimePeriodInSeconds(value) {
|
||||
@@ -1740,6 +1728,51 @@
|
||||
return localStorage.setItem("config_connections_colored_lines", value);
|
||||
}
|
||||
|
||||
function getConfigConnectionsBidirectionalOnly() {
|
||||
const value = localStorage.getItem("config_connections_bidirectional_only");
|
||||
// disable bidirectional filter by default
|
||||
if(value === null){
|
||||
return false;
|
||||
}
|
||||
return value === "true";
|
||||
}
|
||||
|
||||
function setConfigConnectionsBidirectionalOnly(value) {
|
||||
return localStorage.setItem("config_connections_bidirectional_only", value);
|
||||
}
|
||||
|
||||
function getConfigConnectionsMinSnrDb() {
|
||||
const value = localStorage.getItem("config_connections_min_snr_db");
|
||||
// default to null (unset)
|
||||
if(value === null || value === ""){
|
||||
return null;
|
||||
}
|
||||
const parsed = parseFloat(value);
|
||||
return isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
function setConfigConnectionsMinSnrDb(value) {
|
||||
if(value === null || value === "" || value === undefined){
|
||||
return localStorage.removeItem("config_connections_min_snr_db");
|
||||
}
|
||||
// Convert to string for localStorage (handles both number and string inputs)
|
||||
const stringValue = typeof value === "number" ? value.toString() : String(value);
|
||||
return localStorage.setItem("config_connections_min_snr_db", stringValue);
|
||||
}
|
||||
|
||||
function getConfigConnectionsBidirectionalMinSnr() {
|
||||
const value = localStorage.getItem("config_connections_bidirectional_min_snr");
|
||||
// disable bidirectional minimum SNR by default
|
||||
if(value === null){
|
||||
return false;
|
||||
}
|
||||
return value === "true";
|
||||
}
|
||||
|
||||
function setConfigConnectionsBidirectionalMinSnr(value) {
|
||||
return localStorage.setItem("config_connections_bidirectional_min_snr", value);
|
||||
}
|
||||
|
||||
function isMobile() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
@@ -1754,7 +1787,6 @@
|
||||
isShowingAnnouncement: this.shouldShowAnnouncement(),
|
||||
|
||||
configNodesMaxAgeInSeconds: window.getConfigNodesMaxAgeInSeconds(),
|
||||
configNodesDisconnectedAgeInSeconds: window.getConfigNodesDisconnectedAgeInSeconds(),
|
||||
configNodesOfflineAgeInSeconds: window.getConfigNodesOfflineAgeInSeconds(),
|
||||
configWaypointsMaxAgeInSeconds: window.getConfigWaypointsMaxAgeInSeconds(),
|
||||
configConnectionsMaxDistanceInMeters: window.getConfigConnectionsMaxDistanceInMeters(),
|
||||
@@ -1764,6 +1796,9 @@
|
||||
configTemperatureFormat: window.getConfigTemperatureFormat(),
|
||||
configConnectionsTimePeriodInSeconds: window.getConfigConnectionsTimePeriodInSeconds(),
|
||||
configConnectionsColoredLines: window.getConfigConnectionsColoredLines(),
|
||||
configConnectionsBidirectionalOnly: window.getConfigConnectionsBidirectionalOnly(),
|
||||
configConnectionsMinSnrDb: window.getConfigConnectionsMinSnrDb(),
|
||||
configConnectionsBidirectionalMinSnr: window.getConfigConnectionsBidirectionalMinSnr(),
|
||||
|
||||
isShowingHardwareModels: false,
|
||||
hardwareModelStats: null,
|
||||
@@ -2654,9 +2689,6 @@
|
||||
configNodesMaxAgeInSeconds() {
|
||||
window.setConfigNodesMaxAgeInSeconds(this.configNodesMaxAgeInSeconds);
|
||||
},
|
||||
configNodesDisconnectedAgeInSeconds() {
|
||||
window.setConfigNodesDisconnectedAgeInSeconds(this.configNodesDisconnectedAgeInSeconds);
|
||||
},
|
||||
configNodesOfflineAgeInSeconds() {
|
||||
window.setConfigNodesOfflineAgeInSeconds(this.configNodesOfflineAgeInSeconds);
|
||||
},
|
||||
@@ -2684,6 +2716,15 @@
|
||||
configConnectionsColoredLines() {
|
||||
window.setConfigConnectionsColoredLines(this.configConnectionsColoredLines);
|
||||
},
|
||||
configConnectionsBidirectionalOnly() {
|
||||
window.setConfigConnectionsBidirectionalOnly(this.configConnectionsBidirectionalOnly);
|
||||
},
|
||||
configConnectionsMinSnrDb() {
|
||||
window.setConfigConnectionsMinSnrDb(this.configConnectionsMinSnrDb);
|
||||
},
|
||||
configConnectionsBidirectionalMinSnr() {
|
||||
window.setConfigConnectionsBidirectionalMinSnr(this.configConnectionsBidirectionalMinSnr);
|
||||
},
|
||||
deviceMetricsTimeRange() {
|
||||
this.loadNodeDeviceMetrics(this.selectedNode.node_id);
|
||||
},
|
||||
@@ -2867,7 +2908,7 @@
|
||||
},
|
||||
"Overlays": {
|
||||
"Legend": legendLayerGroup,
|
||||
"Backbone Connection": backboneConnectionsLayerGroup,
|
||||
"Backbone Connections": backboneConnectionsLayerGroup,
|
||||
"Connections": connectionsLayerGroup,
|
||||
"Waypoints": waypointsLayerGroup,
|
||||
"Position History": nodePositionHistoryLayerGroup,
|
||||
@@ -3255,6 +3296,48 @@
|
||||
|
||||
if (!otherNode || !otherNodeMarker) continue;
|
||||
|
||||
// Apply bidirectional filter
|
||||
const configConnectionsBidirectionalOnly = getConfigConnectionsBidirectionalOnly();
|
||||
if(configConnectionsBidirectionalOnly){
|
||||
const hasDirectionAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null;
|
||||
const hasDirectionBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null;
|
||||
if(!hasDirectionAB || !hasDirectionBA){
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply minimum SNR filter
|
||||
const configConnectionsMinSnrDb = getConfigConnectionsMinSnrDb();
|
||||
if(configConnectionsMinSnrDb != null){
|
||||
const snrAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null ? connection.direction_ab.avg_snr_db : null;
|
||||
const snrBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null ? connection.direction_ba.avg_snr_db : null;
|
||||
|
||||
const configConnectionsBidirectionalMinSnr = getConfigConnectionsBidirectionalMinSnr();
|
||||
let hasSnrAboveThreshold;
|
||||
|
||||
if(configConnectionsBidirectionalMinSnr){
|
||||
// Bidirectional mode: ALL existing directions must meet threshold
|
||||
const directionsToCheck = [];
|
||||
if(snrAB != null) directionsToCheck.push(snrAB);
|
||||
if(snrBA != null) directionsToCheck.push(snrBA);
|
||||
|
||||
if(directionsToCheck.length === 0){
|
||||
// No SNR data in either direction, skip
|
||||
hasSnrAboveThreshold = false;
|
||||
} else {
|
||||
// All existing directions must be above threshold
|
||||
hasSnrAboveThreshold = directionsToCheck.every(snr => snr > configConnectionsMinSnrDb);
|
||||
}
|
||||
} else {
|
||||
// Default mode: EITHER direction has SNR above threshold
|
||||
hasSnrAboveThreshold = (snrAB != null && snrAB > configConnectionsMinSnrDb) || (snrBA != null && snrBA > configConnectionsMinSnrDb);
|
||||
}
|
||||
|
||||
if(!hasSnrAboveThreshold){
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate distance
|
||||
const distanceInMeters = nodeMarker.getLatLng().distanceTo(otherNodeMarker.getLatLng()).toFixed(2);
|
||||
const configConnectionsMaxDistanceInMeters = getConfigConnectionsMaxDistanceInMeters();
|
||||
@@ -3340,13 +3423,6 @@
|
||||
return string.replace(/</g, "<").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) {
|
||||
|
||||
@@ -3416,12 +3492,6 @@
|
||||
zIndexOffset = -1000;
|
||||
}
|
||||
|
||||
// determine if node was recently heard uplinking packets to mqtt
|
||||
//const nodeHasUplinkedToMqttRecently = hasNodeUplinkedToMqttRecently(node);
|
||||
//if(nodeHasUplinkedToMqttRecently){
|
||||
// icon = iconMqttConnected;
|
||||
//}
|
||||
|
||||
// To not have overlapping nodes.
|
||||
var latJitter = 0;
|
||||
var lonJitter = 0;
|
||||
@@ -3665,6 +3735,48 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply bidirectional filter
|
||||
const configConnectionsBidirectionalOnly = getConfigConnectionsBidirectionalOnly();
|
||||
if(configConnectionsBidirectionalOnly){
|
||||
const hasDirectionAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null;
|
||||
const hasDirectionBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null;
|
||||
if(!hasDirectionAB || !hasDirectionBA){
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply minimum SNR filter
|
||||
const configConnectionsMinSnrDb = getConfigConnectionsMinSnrDb();
|
||||
if(configConnectionsMinSnrDb != null){
|
||||
const snrAB = connection.direction_ab && connection.direction_ab.avg_snr_db != null ? connection.direction_ab.avg_snr_db : null;
|
||||
const snrBA = connection.direction_ba && connection.direction_ba.avg_snr_db != null ? connection.direction_ba.avg_snr_db : null;
|
||||
|
||||
const configConnectionsBidirectionalMinSnr = getConfigConnectionsBidirectionalMinSnr();
|
||||
let hasSnrAboveThreshold;
|
||||
|
||||
if(configConnectionsBidirectionalMinSnr){
|
||||
// Bidirectional mode: ALL existing directions must meet threshold
|
||||
const directionsToCheck = [];
|
||||
if(snrAB != null) directionsToCheck.push(snrAB);
|
||||
if(snrBA != null) directionsToCheck.push(snrBA);
|
||||
|
||||
if(directionsToCheck.length === 0){
|
||||
// No SNR data in either direction, skip
|
||||
hasSnrAboveThreshold = false;
|
||||
} else {
|
||||
// All existing directions must be above threshold
|
||||
hasSnrAboveThreshold = directionsToCheck.every(snr => snr > configConnectionsMinSnrDb);
|
||||
}
|
||||
} else {
|
||||
// Default mode: EITHER direction has SNR above threshold
|
||||
hasSnrAboveThreshold = (snrAB != null && snrAB > configConnectionsMinSnrDb) || (snrBA != null && snrBA > configConnectionsMinSnrDb);
|
||||
}
|
||||
|
||||
if(!hasSnrAboveThreshold){
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate distance between nodes
|
||||
const distanceInMeters = nodeAMarker.getLatLng().distanceTo(nodeBMarker.getLatLng()).toFixed(2);
|
||||
|
||||
@@ -3710,7 +3822,7 @@
|
||||
event.target.closeTooltip();
|
||||
});
|
||||
|
||||
// If both nodes are backbone nodes, also add to backbone layer group with arrows
|
||||
// If both nodes are backbone nodes, also add to backbone layer group
|
||||
if (nodeA.is_backbone && nodeB.is_backbone) {
|
||||
const backboneLine = L.polyline([
|
||||
nodeAMarker.getLatLng(),
|
||||
@@ -3719,13 +3831,6 @@
|
||||
color: lineColor,
|
||||
opacity: 0.75,
|
||||
weight: 3,
|
||||
}).arrowheads({
|
||||
size: '10px',
|
||||
fill: true,
|
||||
offsets: {
|
||||
start: '25px',
|
||||
end: '25px',
|
||||
},
|
||||
}).addTo(backboneConnectionsLayerGroup);
|
||||
|
||||
backboneLine.bindTooltip(tooltip, {
|
||||
@@ -3784,7 +3889,7 @@
|
||||
}
|
||||
tooltip += `<br/>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}`;
|
||||
tooltip += `<br/>${positionHistory.latitude}, ${positionHistory.longitude}`;
|
||||
tooltip += `<br/>Heard on: ${moment(new Date(positionHistory.created_at)).format("DD/MM/YYYY hh:mm A")}`;
|
||||
tooltip += `<br/>Heard on: ${moment(new Date(positionHistory.created_at)).format("YYYY-MM-DD HH:mm")}`;
|
||||
|
||||
// add gateway info if available
|
||||
if(positionHistory.gateway_id){
|
||||
@@ -3967,24 +4072,11 @@
|
||||
|
||||
function getTooltipContentForNode(node) {
|
||||
|
||||
// determine if node was recently heard uplinking packets to mqtt
|
||||
const nodeHasUplinkedToMqttRecently = hasNodeUplinkedToMqttRecently(node);
|
||||
var mqttStatus = `<span class="text-blue-700">Disconnected</span>`;
|
||||
if(node.mqtt_connection_state_updated_at){
|
||||
var mqttStatusUpdatedAt = moment(new Date(node.mqtt_connection_state_updated_at)).fromNow();
|
||||
if(nodeHasUplinkedToMqttRecently){
|
||||
mqttStatus = `<span><span class="text-green-700">Connected</span> (${mqttStatusUpdatedAt})</span>`;
|
||||
} else {
|
||||
mqttStatus = `<span><span class="text-blue-700">Disconnected</span> (${mqttStatusUpdatedAt})</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
var loraFrequencyRange = getRegionFrequencyRange(node.region_name);
|
||||
|
||||
var tooltip = `<img class="mb-4 w-40 mx-auto" src="/images/devices/${node.hardware_model_name}.png" onerror="this.classList.add('hidden')"/>` +
|
||||
`<b>${escapeString(node.long_name)}</b>` +
|
||||
`<br/>Short Name: ${escapeString(node.short_name)}` +
|
||||
`<br/>MQTT: ${mqttStatus}` +
|
||||
(node.num_online_local_nodes != null ? `<br/>Local Nodes Online: ${node.num_online_local_nodes}` : '') +
|
||||
(node.position_precision != null && node.position_precision !== 32 ? `<br/>Position Precision: ${formatPositionPrecision(node.position_precision)}` : '') +
|
||||
`<br/><br/>Role: ${node.role_name}` +
|
||||
@@ -4021,6 +4113,7 @@
|
||||
tooltip += `<br/><br/>ID: ${node.node_id}`;
|
||||
tooltip += `<br/>Hex ID: ${node.node_id_hex}`;
|
||||
tooltip += `<br/>Updated: ${moment(new Date(node.updated_at)).fromNow()}`;
|
||||
tooltip += (node.mqtt_connection_state_updated_at ? `<br/>MQTT Updated: ${moment(new Date(node.mqtt_connection_state_updated_at)).fromNow()}` : '');
|
||||
tooltip += (node.neighbours_updated_at ? `<br/>Neighbours Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '');
|
||||
tooltip += (node.position_updated_at ? `<br/>Position Updated: ${moment(new Date(node.position_updated_at)).fromNow()}` : '');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user