From d1ed47436227e6c3460e9d319495df5293adccd9 Mon Sep 17 00:00:00 2001 From: Jamon Terrell Date: Sat, 22 Feb 2025 12:38:31 -0600 Subject: [PATCH 1/4] added SNR lines for neighborInfo and direct routes in map --- src/core/stores/deviceStore.ts | 20 ++++++ src/core/subscriptions.ts | 6 ++ src/pages/Map/index.tsx | 120 ++++++++++++++++++++++++++++++++- 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index e4cfe823c..353afba91 100644 --- a/src/core/stores/deviceStore.ts +++ b/src/core/stores/deviceStore.ts @@ -38,6 +38,8 @@ export interface Device { activePage: Page; activeNode: number; waypoints: Protobuf.Mesh.Waypoint[]; + neighborInfo: Map; + // currentMetrics: Protobuf.DeviceMetrics; pendingSettingsChanges: boolean; messageDraft: string; unreadCounts: Map; @@ -66,6 +68,10 @@ export interface Device { setActivePage: (page: Page) => void; setActiveNode: (node: number) => void; setPendingSettingsChanges: (state: boolean) => void; + setNeighborInfo: ( + nodeId: number, + neighborInfo: Protobuf.Mesh.NeighborInfo, + ) => void; addChannel: (channel: Protobuf.Channel.Channel) => void; addWaypoint: (waypoint: Protobuf.Mesh.Waypoint) => void; addNodeInfo: (nodeInfo: Protobuf.Mesh.NodeInfo) => void; @@ -145,6 +151,20 @@ export const useDeviceStore = createStore((set, get) => ({ }, pendingSettingsChanges: false, messageDraft: "", + neighborInfo: new Map(), + setNeighborInfo: ( + nodeId: number, + neighborInfo: Protobuf.Mesh.NeighborInfo, + ) => { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (device) { + device.neighborInfo.set(nodeId, neighborInfo); + } + }), + ); + }, nodeErrors: new Map(), unreadCounts: new Map(), nodesMap: new Map(), diff --git a/src/core/subscriptions.ts b/src/core/subscriptions.ts index 35bc89e55..0d09737e7 100644 --- a/src/core/subscriptions.ts +++ b/src/core/subscriptions.ts @@ -9,6 +9,7 @@ export const subscribeAll = ( connection: MeshDevice, messageStore: MessageStore, ) => { + // Set device as a global variable for debugging let myNodeNum = 0; connection.events.onDeviceMetadataPacket.subscribe((metadataPacket) => { @@ -97,6 +98,7 @@ export const subscribeAll = ( }); connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => { + console.log("Trace Route Packet", traceRoutePacket); device.addTraceRoute({ ...traceRoutePacket, }); @@ -142,4 +144,8 @@ export const subscribeAll = ( } } }); + + connection.events.onNeighborInfoPacket.subscribe((neighborInfo) => { + device.setNeighborInfo(neighborInfo.from, neighborInfo.data); + }); }; diff --git a/src/pages/Map/index.tsx b/src/pages/Map/index.tsx index 8b5bbdb23..c8050b614 100644 --- a/src/pages/Map/index.tsx +++ b/src/pages/Map/index.tsx @@ -11,16 +11,37 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { AttributionControl, GeolocateControl, + Layer, Marker, NavigationControl, Popup, ScaleControl, + Source, useMap, } from "react-map-gl/maplibre"; import MapGl from "react-map-gl/maplibre"; import { useNodeFilters } from "@core/hooks/useNodeFilters.ts"; import { FilterControl } from "@pages/Map/FilterControl.tsx"; +// taken from android client these probably should be moved into a shared file +const SNR_GOOD_THRESHOLD = -7; +const SNR_FAIR_THRESHOLD = -15; +const RSSI_GOOD_THRESHOLD = -115; +const RSSI_FAIR_THRESHOLD = -126; +const LINE_GOOD_COLOR = "#00ff00"; +const LINE_FAIR_COLOR = "#ffe600"; +const LINE_BAD_COLOR = "#f7931a"; + +const getSignalColor = (snr: number, rssi?: number) => { + if (snr > SNR_GOOD_THRESHOLD && (rssi == null || rssi > RSSI_GOOD_THRESHOLD)) + return LINE_GOOD_COLOR; + if (snr > SNR_FAIR_THRESHOLD && (rssi == null || rssi > RSSI_FAIR_THRESHOLD)) + return LINE_FAIR_COLOR; + return LINE_BAD_COLOR; +}; + +const DIRECT_NODE_TIMEOUT = 60 * 20; // 60 seconds * ? minutes + type NodePosition = { latitude: number; longitude: number; @@ -34,8 +55,72 @@ const convertToLatLng = (position: { longitude: (position.longitudeI ?? 0) / 1e7, }); +const generateNeighborLines = ( + nodes: { + node: Protobuf.Mesh.NodeInfo; + neighborInfo: Protobuf.Mesh.NeighborInfo; + }[], +) => { + const features = []; + for (const { node, neighborInfo } of nodes) { + const start = convertToLatLng(node.position); + if (!neighborInfo) continue; + for (const neighbor of neighborInfo.neighbors) { + const toNode = nodes.find((n) => n.node.num === neighbor.nodeId)?.node; + if (!toNode) continue; + const end = convertToLatLng(toNode.position); + features.push({ + type: "Feature", + geometry: { + type: "LineString", + coordinates: [ + [start.longitude, start.latitude], + [end.longitude, end.latitude], + ], + }, + properties: { + color: getSignalColor(neighbor.snr), + }, + }); + } + } + + return { + type: "FeatureCollection", + features, + }; +}; +const generateDirectLines = (nodes: Protobuf.Mesh.NodeInfo[]) => { + const features = []; + for (const node of nodes) { + if (!node.position) continue; + if (node.hopsAway > 0) continue; + if (Date.now() / 1000 - node.lastHeard > DIRECT_NODE_TIMEOUT) continue; + const start = convertToLatLng(node.position); + const selfNode = nodes.find((n) => n.isFavorite); + const end = convertToLatLng(selfNode.position); + features.push({ + type: "Feature", + geometry: { + type: "LineString", + coordinates: [ + [start.longitude, start.latitude], + [end.longitude, end.latitude], + ], + }, + properties: { + color: getSignalColor(node.snr), + }, + }); + } + + return { + type: "FeatureCollection", + features, + }; +}; const MapPage = () => { - const { getNodes, waypoints } = useDevice(); + const { getNodes, waypoints, neighborInfo } = useDevice(); const { theme } = useTheme(); const { default: map } = useMap(); @@ -153,6 +238,19 @@ const MapPage = () => { [filteredNodes, handleMarkerClick], ); + const neighborLines = useMemo(() => { + return generateNeighborLines( + validNodes.map((vn) => ({ + node: vn, + neighborInfo: neighborInfo.get(vn.num), + })), + ); + }, [validNodes, neighborInfo]); + + const directLines = useMemo( + () => generateDirectLines(validNodes), + [validNodes], + ); useEffect(() => { map?.on("load", () => { getMapBounds(); @@ -205,6 +303,26 @@ const MapPage = () => { ))} {markers} + + + + + + {selectedNode ? ( Date: Sun, 23 Feb 2025 12:02:54 -0600 Subject: [PATCH 2/4] fixed bug in neighbor lines when self node has no location. --- src/pages/Map/index.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/pages/Map/index.tsx b/src/pages/Map/index.tsx index c8050b614..ab75651ec 100644 --- a/src/pages/Map/index.tsx +++ b/src/pages/Map/index.tsx @@ -91,13 +91,26 @@ const generateNeighborLines = ( }; }; const generateDirectLines = (nodes: Protobuf.Mesh.NodeInfo[]) => { - const features = []; + const selfNode = nodes.find((n) => n.isFavorite); + const features: { + type: string; + geometry: { + type: string; + coordinates: number[][]; + }; + properties: { + color: string; + }; + }[] = []; + + if (!selfNode || !selfNode.position) + return { type: "FeatureCollection", features }; + for (const node of nodes) { if (!node.position) continue; if (node.hopsAway > 0) continue; if (Date.now() / 1000 - node.lastHeard > DIRECT_NODE_TIMEOUT) continue; const start = convertToLatLng(node.position); - const selfNode = nodes.find((n) => n.isFavorite); const end = convertToLatLng(selfNode.position); features.push({ type: "Feature", From 88f5977f1c11c463dfcaa46a1319b90b039888bc Mon Sep 17 00:00:00 2001 From: Jamon Terrell Date: Sun, 11 May 2025 20:14:55 -0500 Subject: [PATCH 3/4] updated with toggle button --- .vscode/settings.json | 2 +- src/core/subscriptions.ts | 4 +- src/pages/Map/index.tsx | 80 +++++++++++++++++++++++++++++---------- 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9045fc0fe..ec937ca08 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,4 +3,4 @@ "deno.suggest.imports.autoDiscover": true, "editor.formatOnSave": true, "editor.defaultFormatter": "denoland.vscode-deno" -} +} \ No newline at end of file diff --git a/src/core/subscriptions.ts b/src/core/subscriptions.ts index 0d09737e7..e2a1bbeb6 100644 --- a/src/core/subscriptions.ts +++ b/src/core/subscriptions.ts @@ -9,7 +9,6 @@ export const subscribeAll = ( connection: MeshDevice, messageStore: MessageStore, ) => { - // Set device as a global variable for debugging let myNodeNum = 0; connection.events.onDeviceMetadataPacket.subscribe((metadataPacket) => { @@ -98,7 +97,6 @@ export const subscribeAll = ( }); connection.events.onTraceRoutePacket.subscribe((traceRoutePacket) => { - console.log("Trace Route Packet", traceRoutePacket); device.addTraceRoute({ ...traceRoutePacket, }); @@ -144,7 +142,7 @@ export const subscribeAll = ( } } }); - + connection.events.onNeighborInfoPacket.subscribe((neighborInfo) => { device.setNeighborInfo(neighborInfo.from, neighborInfo.data); }); diff --git a/src/pages/Map/index.tsx b/src/pages/Map/index.tsx index ab75651ec..da41cccf9 100644 --- a/src/pages/Map/index.tsx +++ b/src/pages/Map/index.tsx @@ -22,6 +22,7 @@ import { import MapGl from "react-map-gl/maplibre"; import { useNodeFilters } from "@core/hooks/useNodeFilters.ts"; import { FilterControl } from "@pages/Map/FilterControl.tsx"; +import { cn } from "@core/utils/cn.ts"; // taken from android client these probably should be moved into a shared file const SNR_GOOD_THRESHOLD = -7; @@ -33,10 +34,16 @@ const LINE_FAIR_COLOR = "#ffe600"; const LINE_BAD_COLOR = "#f7931a"; const getSignalColor = (snr: number, rssi?: number) => { - if (snr > SNR_GOOD_THRESHOLD && (rssi == null || rssi > RSSI_GOOD_THRESHOLD)) + if ( + snr > SNR_GOOD_THRESHOLD && (rssi == null || rssi > RSSI_GOOD_THRESHOLD) + ) { return LINE_GOOD_COLOR; - if (snr > SNR_FAIR_THRESHOLD && (rssi == null || rssi > RSSI_FAIR_THRESHOLD)) + } + if ( + snr > SNR_FAIR_THRESHOLD && (rssi == null || rssi > RSSI_FAIR_THRESHOLD) + ) { return LINE_FAIR_COLOR; + } return LINE_BAD_COLOR; }; @@ -63,11 +70,12 @@ const generateNeighborLines = ( ) => { const features = []; for (const { node, neighborInfo } of nodes) { + if (!node.position) continue; const start = convertToLatLng(node.position); if (!neighborInfo) continue; for (const neighbor of neighborInfo.neighbors) { const toNode = nodes.find((n) => n.node.num === neighbor.nodeId)?.node; - if (!toNode) continue; + if (!toNode || !toNode.position) continue; const end = convertToLatLng(toNode.position); features.push({ type: "Feature", @@ -90,8 +98,11 @@ const generateNeighborLines = ( features, }; }; -const generateDirectLines = (nodes: Protobuf.Mesh.NodeInfo[]) => { - const selfNode = nodes.find((n) => n.isFavorite); +const generateDirectLines = ( + myNode: Protobuf.Mesh.NodeInfo, + nodes: Protobuf.Mesh.NodeInfo[], +) => { + // const selfNode = nodes.find((n) => n.isFavorite); const features: { type: string; geometry: { @@ -103,15 +114,15 @@ const generateDirectLines = (nodes: Protobuf.Mesh.NodeInfo[]) => { }; }[] = []; - if (!selfNode || !selfNode.position) + if (!myNode || !myNode.position) { return { type: "FeatureCollection", features }; + } for (const node of nodes) { - if (!node.position) continue; - if (node.hopsAway > 0) continue; + if (!node.position || node.hopsAway !== 0) continue; if (Date.now() / 1000 - node.lastHeard > DIRECT_NODE_TIMEOUT) continue; const start = convertToLatLng(node.position); - const end = convertToLatLng(selfNode.position); + const end = convertToLatLng(myNode.position); features.push({ type: "Feature", geometry: { @@ -133,7 +144,7 @@ const generateDirectLines = (nodes: Protobuf.Mesh.NodeInfo[]) => { }; }; const MapPage = () => { - const { getNodes, waypoints, neighborInfo } = useDevice(); + const { getNodes, getMyNode, waypoints, neighborInfo } = useDevice(); const { theme } = useTheme(); const { default: map } = useMap(); @@ -143,6 +154,12 @@ const MapPage = () => { Protobuf.Mesh.NodeInfo | null >(null); + const [showSNRLines, setShowSNRLines] = useState(true); + + const toggleSNRLines = () => { + setShowSNRLines((prev) => !prev); + }; + // Filter out nodes without a valid position const validNodes = useMemo( () => @@ -152,7 +169,7 @@ const MapPage = () => { ), [getNodes], ); - + const myNode = useMemo(() => getMyNode(), [getMyNode]); const { filters, defaultState, @@ -180,6 +197,7 @@ const MapPage = () => { setSelectedNode(node); if (map) { + if (!node.position) return; const position = convertToLatLng(node.position); map.easeTo({ center: [position.longitude, position.latitude], @@ -232,6 +250,7 @@ const MapPage = () => { const markers = useMemo( () => filteredNodes.map((node) => { + if (!node.position) return null; const position = convertToLatLng(node.position); return ( { ); const neighborLines = useMemo(() => { + if (!showSNRLines) return { type: "FeatureCollection", features: [] }; return generateNeighborLines( - validNodes.map((vn) => ({ - node: vn, - neighborInfo: neighborInfo.get(vn.num), - })), + filteredNodes + .map((vn) => ({ + node: vn, + neighborInfo: neighborInfo.get(vn.num), + })) + .filter(( + item, + ): item is { + node: Protobuf.Mesh.NodeInfo; + neighborInfo: Protobuf.Mesh.NeighborInfo; + } => item.neighborInfo !== undefined), ); - }, [validNodes, neighborInfo]); + }, [filteredNodes, neighborInfo, showSNRLines]); const directLines = useMemo( - () => generateDirectLines(validNodes), - [validNodes], + () => { + if (!showSNRLines) return { type: "FeatureCollection", features: [] }; + return generateDirectLines(myNode, filteredNodes); + }, + [myNode, filteredNodes, showSNRLines], ); useEffect(() => { map?.on("load", () => { @@ -350,7 +380,19 @@ const MapPage = () => { ) : null} - + Date: Sun, 11 May 2025 20:48:30 -0500 Subject: [PATCH 4/4] remove direct node timeout, since you can use the map filters now --- src/pages/Map/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/Map/index.tsx b/src/pages/Map/index.tsx index da41cccf9..93a958548 100644 --- a/src/pages/Map/index.tsx +++ b/src/pages/Map/index.tsx @@ -47,8 +47,6 @@ const getSignalColor = (snr: number, rssi?: number) => { return LINE_BAD_COLOR; }; -const DIRECT_NODE_TIMEOUT = 60 * 20; // 60 seconds * ? minutes - type NodePosition = { latitude: number; longitude: number; @@ -120,7 +118,6 @@ const generateDirectLines = ( for (const node of nodes) { if (!node.position || node.hopsAway !== 0) continue; - if (Date.now() / 1000 - node.lastHeard > DIRECT_NODE_TIMEOUT) continue; const start = convertToLatLng(node.position); const end = convertToLatLng(myNode.position); features.push({