diff --git a/src/core/stores/deviceStore.ts b/src/core/stores/deviceStore.ts index e4cfe823..353afba9 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 35bc89e5..0d09737e 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 8b5bbdb2..c8050b61 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 ? (