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/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..e2a1bbeb6 100644 --- a/src/core/subscriptions.ts +++ b/src/core/subscriptions.ts @@ -142,4 +142,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..93a958548 100644 --- a/src/pages/Map/index.tsx +++ b/src/pages/Map/index.tsx @@ -11,15 +11,41 @@ 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"; +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; +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; +}; type NodePosition = { latitude: number; @@ -34,8 +60,88 @@ 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) { + 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 || !toNode.position) 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 = ( + myNode: Protobuf.Mesh.NodeInfo, + nodes: Protobuf.Mesh.NodeInfo[], +) => { + // const selfNode = nodes.find((n) => n.isFavorite); + const features: { + type: string; + geometry: { + type: string; + coordinates: number[][]; + }; + properties: { + color: string; + }; + }[] = []; + + if (!myNode || !myNode.position) { + return { type: "FeatureCollection", features }; + } + + for (const node of nodes) { + if (!node.position || node.hopsAway !== 0) continue; + const start = convertToLatLng(node.position); + const end = convertToLatLng(myNode.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, getMyNode, waypoints, neighborInfo } = useDevice(); const { theme } = useTheme(); const { default: map } = useMap(); @@ -45,6 +151,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( () => @@ -54,7 +166,7 @@ const MapPage = () => { ), [getNodes], ); - + const myNode = useMemo(() => getMyNode(), [getMyNode]); const { filters, defaultState, @@ -82,6 +194,7 @@ const MapPage = () => { setSelectedNode(node); if (map) { + if (!node.position) return; const position = convertToLatLng(node.position); map.easeTo({ center: [position.longitude, position.latitude], @@ -134,6 +247,7 @@ const MapPage = () => { const markers = useMemo( () => filteredNodes.map((node) => { + if (!node.position) return null; const position = convertToLatLng(node.position); return ( { [filteredNodes, handleMarkerClick], ); + const neighborLines = useMemo(() => { + if (!showSNRLines) return { type: "FeatureCollection", features: [] }; + return generateNeighborLines( + 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), + ); + }, [filteredNodes, neighborInfo, showSNRLines]); + + const directLines = useMemo( + () => { + if (!showSNRLines) return { type: "FeatureCollection", features: [] }; + return generateDirectLines(myNode, filteredNodes); + }, + [myNode, filteredNodes, showSNRLines], + ); useEffect(() => { map?.on("load", () => { getMapBounds(); @@ -205,6 +343,26 @@ const MapPage = () => { ))} {markers} + + + + + + {selectedNode ? ( { ) : null} - +