mirror of
https://github.com/dpup/meshstream.git
synced 2026-03-28 17:42:37 +01:00
Fix node types
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -2,5 +2,6 @@
|
|||||||
"cSpell.words": ["meshstream", "Motherlode", "mqtt"],
|
"cSpell.words": ["meshstream", "Motherlode", "mqtt"],
|
||||||
"protoc": {
|
"protoc": {
|
||||||
"options": ["-Iproto"]
|
"options": ["-Iproto"]
|
||||||
}
|
},
|
||||||
|
"makefile.configureOnOpen": false
|
||||||
}
|
}
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -17,7 +17,7 @@ const (
|
|||||||
mqttBroker = "mqtt.bayme.sh"
|
mqttBroker = "mqtt.bayme.sh"
|
||||||
mqttUsername = "meshdev"
|
mqttUsername = "meshdev"
|
||||||
mqttPassword = "large4cats"
|
mqttPassword = "large4cats"
|
||||||
mqttTopicPrefix = "msh/US/bayarea"
|
mqttTopicPrefix = "msh/US/CA/Motherlode"
|
||||||
|
|
||||||
// Web server configuration
|
// Web server configuration
|
||||||
serverHost = "localhost"
|
serverHost = "localhost"
|
||||||
|
|||||||
6
web/CLAUDE.md
Normal file
6
web/CLAUDE.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
- `make web-build` to compile the frontend
|
||||||
|
- `make web-lint` to run lint
|
||||||
|
- `make web-test` to run unit tests
|
||||||
|
|
||||||
|
- Avoid using `any` type
|
||||||
|
- Use existing UI components when possible.
|
||||||
5650
web/pnpm-lock.yaml
generated
5650
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,8 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
import { useAppSelector } from "../../hooks";
|
import { useAppSelector } from "../../hooks";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { NodeData, GatewayData } from "../../store/slices/aggregatorSlice";
|
||||||
|
import { Position } from "../../lib/types";
|
||||||
|
|
||||||
interface NetworkMapProps {
|
interface NetworkMapProps {
|
||||||
/** Height of the map in CSS units */
|
/** Height of the map in CSS units */
|
||||||
@@ -18,7 +20,7 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
|||||||
const markersRef = useRef<Record<string, google.maps.Marker>>({});
|
const markersRef = useRef<Record<string, google.maps.Marker>>({});
|
||||||
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null);
|
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null);
|
||||||
const boundsRef = useRef<google.maps.LatLngBounds>(new google.maps.LatLngBounds());
|
const boundsRef = useRef<google.maps.LatLngBounds>(new google.maps.LatLngBounds());
|
||||||
const [nodesWithPosition, setNodesWithPosition] = useState<any[]>([]);
|
const [nodesWithPosition, setNodesWithPosition] = useState<MapNode[]>([]);
|
||||||
const animatingNodesRef = useRef<Record<string, number>>({});
|
const animatingNodesRef = useRef<Record<string, number>>({});
|
||||||
|
|
||||||
// Get nodes data from the store
|
// Get nodes data from the store
|
||||||
@@ -68,7 +70,7 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
|||||||
}, [latestPacket]);
|
}, [latestPacket]);
|
||||||
|
|
||||||
// Function to animate a node marker when it receives a packet
|
// Function to animate a node marker when it receives a packet
|
||||||
function animateNodeMarker(nodeId: number) {
|
function animateNodeMarker(nodeId: number): void {
|
||||||
const key = `node-${nodeId}`;
|
const key = `node-${nodeId}`;
|
||||||
const marker = markersRef.current[key];
|
const marker = markersRef.current[key];
|
||||||
const node = nodesWithPosition.find(n => n.id === nodeId);
|
const node = nodesWithPosition.find(n => n.id === nodeId);
|
||||||
@@ -109,7 +111,7 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Helper function to initialize the map
|
// Helper function to initialize the map
|
||||||
function initializeMap(element: HTMLDivElement) {
|
function initializeMap(element: HTMLDivElement): void {
|
||||||
const mapOptions: google.maps.MapOptions = {
|
const mapOptions: google.maps.MapOptions = {
|
||||||
zoom: 10,
|
zoom: 10,
|
||||||
mapTypeId: google.maps.MapTypeId.HYBRID,
|
mapTypeId: google.maps.MapTypeId.HYBRID,
|
||||||
@@ -166,7 +168,7 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to update node markers on the map
|
// Helper function to update node markers on the map
|
||||||
function updateNodeMarkers(nodes: any[], navigate: any) {
|
function updateNodeMarkers(nodes: MapNode[], navigate: ReturnType<typeof useNavigate>): void {
|
||||||
if (!mapInstanceRef.current) return;
|
if (!mapInstanceRef.current) return;
|
||||||
|
|
||||||
// Clear the bounds for recalculation
|
// Clear the bounds for recalculation
|
||||||
@@ -224,8 +226,12 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new marker
|
// Create a new marker
|
||||||
function createMarker(node: any, position: google.maps.LatLngLiteral,
|
function createMarker(
|
||||||
nodeName: string, navigate: any) {
|
node: MapNode,
|
||||||
|
position: google.maps.LatLngLiteral,
|
||||||
|
nodeName: string,
|
||||||
|
navigate: ReturnType<typeof useNavigate>
|
||||||
|
): void {
|
||||||
if (!mapInstanceRef.current || !infoWindowRef.current) return;
|
if (!mapInstanceRef.current || !infoWindowRef.current) return;
|
||||||
|
|
||||||
const key = `node-${node.id}`;
|
const key = `node-${node.id}`;
|
||||||
@@ -246,20 +252,24 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update an existing marker
|
// Update an existing marker
|
||||||
function updateMarker(node: any, position: google.maps.LatLngLiteral) {
|
function updateMarker(node: MapNode, position: google.maps.LatLngLiteral): void {
|
||||||
const key = `node-${node.id}`;
|
const key = `node-${node.id}`;
|
||||||
markersRef.current[key].setPosition(position);
|
markersRef.current[key].setPosition(position);
|
||||||
markersRef.current[key].setIcon(getMarkerIcon(node));
|
markersRef.current[key].setIcon(getMarkerIcon(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show info window for a node
|
// Show info window for a node
|
||||||
function showInfoWindow(node: any, marker: google.maps.Marker, navigate: any) {
|
function showInfoWindow(
|
||||||
|
node: MapNode,
|
||||||
|
marker: google.maps.Marker,
|
||||||
|
navigate: ReturnType<typeof useNavigate>
|
||||||
|
): void {
|
||||||
if (!infoWindowRef.current || !mapInstanceRef.current) return;
|
if (!infoWindowRef.current || !mapInstanceRef.current) return;
|
||||||
|
|
||||||
const nodeName = node.shortName || node.longName ||
|
const nodeName = node.shortName || node.longName ||
|
||||||
`${node.isGateway ? 'Gateway' : 'Node'} ${node.id.toString(16)}`;
|
`${node.isGateway ? 'Gateway' : 'Node'} ${node.id.toString(16)}`;
|
||||||
|
|
||||||
const secondsAgo = Math.floor(Date.now() / 1000) - node.lastHeard;
|
const secondsAgo = node.lastHeard ? Math.floor(Date.now() / 1000) - node.lastHeard : 0;
|
||||||
let lastSeenText = formatLastSeen(secondsAgo);
|
let lastSeenText = formatLastSeen(secondsAgo);
|
||||||
|
|
||||||
const infoContent = `
|
const infoContent = `
|
||||||
@@ -292,7 +302,7 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
|||||||
const link = document.getElementById(`view-node-${node.id}`);
|
const link = document.getElementById(`view-node-${node.id}`);
|
||||||
if (link) {
|
if (link) {
|
||||||
link.addEventListener('click', () => {
|
link.addEventListener('click', () => {
|
||||||
navigate({ to: `/node/$nodeId`, params: { nodeId: node.id.toString() } });
|
navigate({ to: `/node/$nodeId`, params: { nodeId: node.id.toString(16) } });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -307,70 +317,100 @@ export const NetworkMap: React.FC<NetworkMapProps> = ({ height = "600px" }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Define interface for nodes with position data for map display
|
||||||
|
interface MapNode {
|
||||||
|
id: number;
|
||||||
|
position: Position & {
|
||||||
|
latitudeI: number; // Override to make required
|
||||||
|
longitudeI: number; // Override to make required
|
||||||
|
};
|
||||||
|
isGateway: boolean;
|
||||||
|
gatewayId?: string;
|
||||||
|
shortName?: string;
|
||||||
|
longName?: string;
|
||||||
|
lastHeard?: number;
|
||||||
|
messageCount: number;
|
||||||
|
textMessageCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to determine if a node has valid position data
|
// Helper function to determine if a node has valid position data
|
||||||
function hasValidPosition(node: any) {
|
function hasValidPosition(node: NodeData): boolean {
|
||||||
return node.position &&
|
return Boolean(
|
||||||
node.position.latitudeI !== undefined &&
|
node.position &&
|
||||||
node.position.longitudeI !== undefined;
|
node.position.latitudeI !== undefined &&
|
||||||
|
node.position.longitudeI !== undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a list of nodes that have position data
|
// Get a list of nodes that have position data
|
||||||
function getNodesWithPosition(nodes: any, gateways: any) {
|
function getNodesWithPosition(
|
||||||
const nodesMap = new Map(); // Use a Map to avoid duplicates
|
nodes: Record<number, NodeData>,
|
||||||
|
gateways: Record<string, GatewayData>
|
||||||
|
): MapNode[] {
|
||||||
|
const nodesMap = new Map<number, MapNode>(); // Use a Map to avoid duplicates
|
||||||
|
|
||||||
// Regular nodes
|
// Regular nodes
|
||||||
Object.entries(nodes).forEach(([nodeIdStr, nodeData]) => {
|
Object.entries(nodes).forEach(([nodeIdStr, nodeData]) => {
|
||||||
if (hasValidPosition(nodeData)) {
|
if (hasValidPosition(nodeData)) {
|
||||||
const nodeId = parseInt(nodeIdStr);
|
const nodeId = parseInt(nodeIdStr);
|
||||||
|
const position = nodeData.position as MapNode['position'];
|
||||||
nodesMap.set(nodeId, {
|
nodesMap.set(nodeId, {
|
||||||
...nodeData,
|
...nodeData,
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
isGateway: false
|
isGateway: !!nodeData.isGateway,
|
||||||
|
position,
|
||||||
|
messageCount: nodeData.messageCount || 0,
|
||||||
|
textMessageCount: nodeData.textMessageCount || 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gateways with position data
|
// Gateways - we need to find the corresponding node for each gateway
|
||||||
Object.entries(gateways).forEach(([gatewayId, gatewayData]) => {
|
Object.entries(gateways).forEach(([gatewayId, gatewayData]) => {
|
||||||
// Extract node ID from gateway ID (removing the '!' prefix)
|
// Extract node ID from gateway ID (removing the '!' prefix)
|
||||||
const nodeId = parseInt(gatewayId.substring(1), 16);
|
const nodeId = parseInt(gatewayId.substring(1), 16);
|
||||||
|
|
||||||
// First priority: Use gateway's mapReport position if available
|
// First priority: Check if we already have the node with a mapReport
|
||||||
if (gatewayData.mapReport &&
|
// (since mapReport is stored on NodeData, not GatewayData)
|
||||||
gatewayData.mapReport.latitudeI !== undefined &&
|
const nodeWithMapReport = nodes[nodeId];
|
||||||
gatewayData.mapReport.longitudeI !== undefined) {
|
|
||||||
|
if (
|
||||||
nodesMap.set(nodeId, {
|
nodeWithMapReport?.mapReport &&
|
||||||
...(nodesMap.get(nodeId) || {}), // Keep existing node data if any
|
nodeWithMapReport.mapReport.latitudeI !== undefined &&
|
||||||
id: nodeId,
|
nodeWithMapReport.mapReport.longitudeI !== undefined
|
||||||
isGateway: true,
|
) {
|
||||||
gatewayId: gatewayId,
|
// Use mapReport position from the node data if we haven't already added this node
|
||||||
position: {
|
if (!nodesMap.has(nodeId)) {
|
||||||
latitudeI: gatewayData.mapReport.latitudeI,
|
nodesMap.set(nodeId, {
|
||||||
longitudeI: gatewayData.mapReport.longitudeI,
|
id: nodeId,
|
||||||
precisionBits: gatewayData.mapReport.positionPrecision
|
isGateway: true,
|
||||||
},
|
gatewayId: gatewayId,
|
||||||
// Include other gateway data
|
position: {
|
||||||
lastHeard: gatewayData.lastHeard || (nodesMap.get(nodeId)?.lastHeard),
|
latitudeI: nodeWithMapReport.mapReport.latitudeI!,
|
||||||
messageCount: gatewayData.messageCount || (nodesMap.get(nodeId)?.messageCount || 0),
|
longitudeI: nodeWithMapReport.mapReport.longitudeI!,
|
||||||
textMessageCount: gatewayData.textMessageCount || (nodesMap.get(nodeId)?.textMessageCount || 0),
|
precisionBits: nodeWithMapReport.mapReport.positionPrecision,
|
||||||
shortName: gatewayData.shortName || (nodesMap.get(nodeId)?.shortName),
|
time: nodeWithMapReport.lastHeard || Math.floor(Date.now() / 1000)
|
||||||
longName: gatewayData.longName || (nodesMap.get(nodeId)?.longName)
|
},
|
||||||
});
|
// Include other data
|
||||||
|
lastHeard: nodeWithMapReport.lastHeard,
|
||||||
|
messageCount: nodeWithMapReport.messageCount || gatewayData.messageCount || 0,
|
||||||
|
textMessageCount: nodeWithMapReport.textMessageCount || gatewayData.textMessageCount || 0,
|
||||||
|
shortName: nodeWithMapReport.shortName,
|
||||||
|
longName: nodeWithMapReport.longName
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Second priority: Mark existing node as gateway if it already has position data
|
// Second priority: Mark existing node as gateway if it already has position data
|
||||||
else if (nodesMap.has(nodeId)) {
|
else if (nodesMap.has(nodeId)) {
|
||||||
const existingNode = nodesMap.get(nodeId);
|
const existingNode = nodesMap.get(nodeId)!;
|
||||||
nodesMap.set(nodeId, {
|
nodesMap.set(nodeId, {
|
||||||
...existingNode,
|
...existingNode,
|
||||||
isGateway: true,
|
isGateway: true,
|
||||||
gatewayId: gatewayId,
|
gatewayId: gatewayId,
|
||||||
// Merge other data
|
// Update data from gateway information
|
||||||
lastHeard: Math.max(existingNode.lastHeard || 0, gatewayData.lastHeard || 0),
|
lastHeard: Math.max(existingNode.lastHeard || 0, gatewayData.lastHeard || 0),
|
||||||
messageCount: existingNode.messageCount || gatewayData.messageCount || 0,
|
messageCount: existingNode.messageCount || gatewayData.messageCount || 0,
|
||||||
textMessageCount: existingNode.textMessageCount || gatewayData.textMessageCount || 0,
|
textMessageCount: existingNode.textMessageCount || gatewayData.textMessageCount || 0
|
||||||
shortName: existingNode.shortName || gatewayData.shortName,
|
|
||||||
longName: existingNode.longName || gatewayData.longName
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -378,8 +418,18 @@ function getNodesWithPosition(nodes: any, gateways: any) {
|
|||||||
return Array.from(nodesMap.values());
|
return Array.from(nodesMap.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface for marker icon configuration
|
||||||
|
interface MarkerIconConfig {
|
||||||
|
path: number;
|
||||||
|
scale: number;
|
||||||
|
fillColor: string;
|
||||||
|
fillOpacity: number;
|
||||||
|
strokeColor: string;
|
||||||
|
strokeWeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Get marker icon for a node
|
// Get marker icon for a node
|
||||||
function getMarkerIcon(node: any, isAnimating: boolean = false) {
|
function getMarkerIcon(node: MapNode, isAnimating: boolean = false): MarkerIconConfig {
|
||||||
return {
|
return {
|
||||||
path: google.maps.SymbolPath.CIRCLE,
|
path: google.maps.SymbolPath.CIRCLE,
|
||||||
scale: isAnimating ? 14 : 10, // Increase size during animation
|
scale: isAnimating ? 14 : 10, // Increase size during animation
|
||||||
@@ -391,7 +441,7 @@ function getMarkerIcon(node: any, isAnimating: boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Format the "last seen" text
|
// Format the "last seen" text
|
||||||
function formatLastSeen(secondsAgo: number) {
|
function formatLastSeen(secondsAgo: number): string {
|
||||||
if (secondsAgo < 60) {
|
if (secondsAgo < 60) {
|
||||||
return `${secondsAgo} seconds ago`;
|
return `${secondsAgo} seconds ago`;
|
||||||
} else if (secondsAgo < 3600) {
|
} else if (secondsAgo < 3600) {
|
||||||
|
|||||||
37
web/src/types/google-maps.d.ts
vendored
37
web/src/types/google-maps.d.ts
vendored
@@ -6,18 +6,43 @@ declare namespace google {
|
|||||||
mapDiv: Element,
|
mapDiv: Element,
|
||||||
opts?: MapOptions
|
opts?: MapOptions
|
||||||
);
|
);
|
||||||
|
setZoom(zoom: number): void;
|
||||||
|
getZoom(): number | undefined;
|
||||||
|
fitBounds(bounds: LatLngBounds): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Marker {
|
class Marker {
|
||||||
constructor(opts?: MarkerOptions);
|
constructor(opts?: MarkerOptions);
|
||||||
|
setMap(map: Map | null): void;
|
||||||
|
setPosition(position: LatLngLiteral): void;
|
||||||
|
setIcon(icon: any): void;
|
||||||
|
addListener(event: string, handler: Function): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Circle {
|
class Circle {
|
||||||
constructor(opts?: CircleOptions);
|
constructor(opts?: CircleOptions);
|
||||||
|
setMap(map: Map | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InfoWindow {
|
||||||
|
constructor(opts?: InfoWindowOptions);
|
||||||
|
setContent(content: string): void;
|
||||||
|
open(map?: Map, anchor?: Marker): void;
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LatLngBounds {
|
||||||
|
constructor();
|
||||||
|
extend(point: LatLngLiteral): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LatLngLiteral {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MapOptions {
|
interface MapOptions {
|
||||||
center?: { lat: number; lng: number };
|
center?: LatLngLiteral;
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
mapTypeId?: string;
|
mapTypeId?: string;
|
||||||
mapTypeControl?: boolean;
|
mapTypeControl?: boolean;
|
||||||
@@ -28,10 +53,11 @@ declare namespace google {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MarkerOptions {
|
interface MarkerOptions {
|
||||||
position?: { lat: number; lng: number };
|
position?: LatLngLiteral;
|
||||||
map?: Map;
|
map?: Map;
|
||||||
title?: string;
|
title?: string;
|
||||||
icon?: any;
|
icon?: any;
|
||||||
|
zIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CircleOptions {
|
interface CircleOptions {
|
||||||
@@ -41,10 +67,15 @@ declare namespace google {
|
|||||||
fillColor?: string;
|
fillColor?: string;
|
||||||
fillOpacity?: number;
|
fillOpacity?: number;
|
||||||
map?: Map;
|
map?: Map;
|
||||||
center?: { lat: number; lng: number };
|
center?: LatLngLiteral;
|
||||||
radius?: number;
|
radius?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InfoWindowOptions {
|
||||||
|
content?: string;
|
||||||
|
position?: LatLngLiteral;
|
||||||
|
}
|
||||||
|
|
||||||
const MapTypeId: {
|
const MapTypeId: {
|
||||||
ROADMAP: string;
|
ROADMAP: string;
|
||||||
SATELLITE: string;
|
SATELLITE: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user