mirror of
https://github.com/Roslund/meshtastic-map.git
synced 2026-05-09 14:55:29 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f305bdbb3d | |||
| c765d63804 | |||
| 0ac1f796da | |||
| 3cccf80d07 | |||
| 8fd6730e0d | |||
| 1748079708 | |||
| 4a4b5fb7f3 | |||
| dc9a45a62a | |||
| f3154cb97b | |||
| 57c10383e2 | |||
| f690bb65a7 |
+1
-1
@@ -58,7 +58,7 @@ services:
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 6
|
||||
start_interval: 5s
|
||||
start_period: 5s
|
||||
|
||||
volumes:
|
||||
database_data:
|
||||
|
||||
Generated
+9
-12
@@ -10,7 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.16.2",
|
||||
"command-line-args": "^6.0.1",
|
||||
"command-line-args": "^6.0.2",
|
||||
"command-line-usage": "^7.0.3",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
@@ -69,7 +69,6 @@
|
||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -1763,9 +1762,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/array-back": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz",
|
||||
"integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz",
|
||||
"integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.17"
|
||||
@@ -2010,7 +2009,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001737",
|
||||
"electron-to-chromium": "^1.5.211",
|
||||
@@ -2372,15 +2370,15 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/command-line-args": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz",
|
||||
"integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.2.tgz",
|
||||
"integrity": "sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-back": "^6.2.2",
|
||||
"array-back": "^6.2.3",
|
||||
"find-replace": "^5.0.2",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"typical": "^7.2.0"
|
||||
"typical": "^7.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
@@ -5150,7 +5148,6 @@
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.16.2",
|
||||
"@prisma/engines": "6.16.2"
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.16.2",
|
||||
"command-line-args": "^6.0.1",
|
||||
"command-line-args": "^6.0.2",
|
||||
"command-line-usage": "^7.0.3",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
+14
-2
@@ -547,10 +547,22 @@ app.get('/api/v1/nodes/:nodeId/traceroutes', async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// get latest traceroutes
|
||||
// get latest traceroutes, deduplicated by packet_id
|
||||
// We want replies where want_response is false and it will be "to" the
|
||||
// requester.
|
||||
const traceroutes = await prisma.$queryRaw`SELECT * FROM traceroutes WHERE want_response = false and \`to\` = ${node.node_id} and gateway_id is not null order by id desc limit ${count}`;
|
||||
// Deduplicate by packet_id, keeping the latest traceroute (highest id) for each packet_id
|
||||
const traceroutes = await prisma.$queryRaw`
|
||||
SELECT t1.*
|
||||
FROM traceroutes t1
|
||||
INNER JOIN (
|
||||
SELECT packet_id, MAX(id) as max_id
|
||||
FROM traceroutes
|
||||
WHERE want_response = false and \`to\` = ${node.node_id} and gateway_id is not null
|
||||
GROUP BY packet_id
|
||||
) t2 ON t1.packet_id = t2.packet_id AND t1.id = t2.max_id
|
||||
ORDER BY t1.id DESC
|
||||
LIMIT ${count}
|
||||
`;
|
||||
|
||||
res.json({
|
||||
traceroutes: traceroutes.map((trace) => {
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+182
-85
@@ -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,
|
||||
@@ -3183,7 +3224,7 @@
|
||||
|
||||
}
|
||||
|
||||
function getTerrainProfileImage(node1, node2) {
|
||||
function getTerrainProfileImage(url,node1, node2) {
|
||||
|
||||
// line colour between nodes
|
||||
const lineColour = "0000FF"; // blue
|
||||
@@ -3201,7 +3242,7 @@
|
||||
const node2ElevationMSL = node2.altitude ?? "";
|
||||
|
||||
// generate terrain profile image url
|
||||
return "https://heywhatsthat.com/bin/profile-0904.cgi?" + new URLSearchParams({
|
||||
return url + new URLSearchParams({
|
||||
src: "meshtastic.liamcottle.net",
|
||||
axes: 1, // include grid lines and a scale
|
||||
metric: 1, // show metric units
|
||||
@@ -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;
|
||||
@@ -3636,9 +3706,13 @@
|
||||
}
|
||||
|
||||
// Add terrain profile image
|
||||
const terrainImageUrl = getTerrainProfileImage(nodeA, nodeB);
|
||||
tooltip += `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`;
|
||||
const terrainImageUrl = getTerrainProfileImage("https://heywhatsthat.com/bin/profile-0904.cgi?", nodeA, nodeB);
|
||||
tooltip += `<br/><br/>Terrain image from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`;
|
||||
tooltip += `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
|
||||
|
||||
const terrainImageUrl2 = getTerrainProfileImage("https://tjenavadärdetdär.se/?", nodeA, nodeB);
|
||||
tooltip += `<br/>Terrain image from <a href="https://tjenavadärdetdär.se" target="_blank">TjenaVadÄrDetDär.se</a>`;
|
||||
tooltip += `<br/><a href="${terrainImageUrl2}" target="_blank"><img src="${terrainImageUrl2}" width="100%"></a>`;
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
@@ -3665,6 +3739,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 +3826,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 +3835,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 +3893,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 +4076,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 +4117,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