From d6f5c2c42f18b34be6a5b1cfdd9c79bce054e13a Mon Sep 17 00:00:00 2001 From: = Date: Sun, 11 Jan 2026 23:39:38 -0500 Subject: [PATCH 1/9] - still needs testing on rover (tested only on my local machine) but seems to work! - added ping library [https://www.npmjs.com/package/ping] to package,json - toast notifications to be done at a later date --- package-lock.json | 10 ++ package.json | 1 + src/app/dashboard/api/route.ts | 68 +++++++- .../panels/NetworkHealthTelemetryPanel.tsx | 159 +++++++++--------- 4 files changed, 151 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index da93818..142b1f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "leaflet": "^1.9.4", "next": "15.1.6", "next-runtime-env": "^3.3.0", + "ping": "^1.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hot-toast": "^2.6.0", @@ -5358,6 +5359,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/ping": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ping/-/ping-1.0.0.tgz", + "integrity": "sha512-3dxdgGtV+7P/EVo42JhkGSomeO/0GGicSz3mI/yK+AI+VGNAOfakw5XfcbGI4IjyBY+ZZwvuRXdhnNF2uliKew==", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/pngparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pngparse/-/pngparse-2.0.1.tgz", diff --git a/package.json b/package.json index ec55702..3795dde 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "leaflet": "^1.9.4", "next": "15.1.6", "next-runtime-env": "^3.3.0", + "ping": "^1.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hot-toast": "^2.6.0", diff --git a/src/app/dashboard/api/route.ts b/src/app/dashboard/api/route.ts index 22188b2..2fa378f 100644 --- a/src/app/dashboard/api/route.ts +++ b/src/app/dashboard/api/route.ts @@ -2,6 +2,36 @@ const USERNAME = 'ubnt'; const PASSWORD = 'samitherover'; const baseStationIP = '192.168.0.2'; +const hosts = ['192.168.0.2', '172.19.228.1']; // Add more hosts here as needed (must match hosts in NetworkHealthTelemetryPanel.tsx) + +const ping = require('ping'); + +// Ping configuration +const pingConfig = { + timeout: 5, +}; + +// Function to ping multiple hosts and return RTT times in dictionary format +async function pingHosts(hosts: string[]): Promise<{ [key: string]: number }> { + const results: { [key: string]: number } = {}; + + const pingPromises = hosts.map(async (host) => { + try { + const res = await ping.promise.probe(host, pingConfig); + if (res.alive) { + results[host] = parseFloat(res.time as string); + } else { + results[host] = -1; + } + } catch (error) { + results[host] = -1; + } + }); + + await Promise.all(pingPromises); + return results; +} + // Authenticates with the base station async function authenticate() { const response = await fetch(`http://${baseStationIP}/api/auth`, { @@ -39,25 +69,45 @@ async function fetchStatus(cookie: string) { } export async function GET(request: Request) { + // Initialize response data with defaults + let uplinkCapacity = 0; + let downlinkCapacity = 0; + let uplinkThroughput = 0; + let downlinkThroughput = 0; + let baseStationError: string | null = null; + + // Try to fetch base station data, but don't fail if it's unavailable try { const authStatus = await authenticate(); - - const status = await fetchStatus(authStatus.cookie); - // Extract only the needed fields for the frontend - const uplinkCapacity = status.wireless?.polling?.ucap ?? 0; - const downlinkCapacity = status.wireless?.polling?.dcap ?? 0; - const uplinkThroughput = status.wireless?.throughput?.tx ?? 0; - const downlinkThroughput = status.wireless?.throughput?.rx ?? 0; + uplinkCapacity = status.wireless?.polling?.ucap ?? 0; + downlinkCapacity = status.wireless?.polling?.dcap ?? 0; + uplinkThroughput = status.wireless?.throughput?.tx ?? 0; + downlinkThroughput = status.wireless?.throughput?.rx ?? 0; + } catch (error: any) { + baseStationError = error.message; + console.warn('Failed to fetch base station data:', error.message); + } + + // Always ping hosts, regardless of base station status + try { + const pings = await pingHosts(hosts); return Response.json({ uplinkCapacity, downlinkCapacity, uplinkThroughput, downlinkThroughput, + pings, + baseStationError, // Include error info in response }); } catch (error: any) { - return new Response(JSON.stringify({ error: error.message }), { status: 500 }); + console.error('Failed to ping hosts:', error); // put toast notifications here LATER + return Response.json( + { error: 'Failed to ping hosts: ' + error.message }, + { status: 500 } + ); } -} \ No newline at end of file +} + diff --git a/src/components/panels/NetworkHealthTelemetryPanel.tsx b/src/components/panels/NetworkHealthTelemetryPanel.tsx index 7496694..05d8775 100644 --- a/src/components/panels/NetworkHealthTelemetryPanel.tsx +++ b/src/components/panels/NetworkHealthTelemetryPanel.tsx @@ -1,74 +1,83 @@ import React, { useEffect, useState } from "react"; -import { - BarChart, - Bar, - XAxis, - YAxis, - Tooltip, - ResponsiveContainer, - Cell, - LabelList, -} from "recharts"; + +const dotStyle = (up: boolean): React.CSSProperties => ({ + width: 12, + height: 12, + borderRadius: "50%", + display: "inline-block", + backgroundColor: up ? "#22c55e" : "#ef4444", +}); + +const hosts = ["192.168.0.2", "172.19.228.1"]; // Add more hosts here as needed (must match hosts in route.ts) +const hostnames: { [key: string]: string } = {"192.168.0.2": "Base"}; // set nicknames here + const NetworkHealthTelemetryPanel: React.FC = () => { const [stats, setStats] = useState({ - uplinkThroughput: 0, - downlinkThroughput: 0, - uplinkCapacity: 100, - downlinkCapacity: 100, + uplinkRttMs: 0, + downlinkRttMs: 0, + uplinkUp: false, + downlinkUp: false, }); + const [pings, setPings] = useState<{ [key: string]: number }>({}); + const [baseStationError, setBaseStationError] = useState(null); + + console.log('pings state:', pings); + useEffect(() => { let interval: NodeJS.Timeout; - // Polling function to fetch data from the API route + const poll = async () => { try { - const response = await fetch("/dashboard/api", { + const response = await fetch("/dashboard/api", { method: "GET", + headers: { + "Content-Type": "application/json", + } }); if (!response.ok) { - console.error("Failed to fetch status from API route"); + console.error(`API returned ${response.status}: ${response.statusText}`); return; } - const data = await response.json(); - const uplinkCapacity = data.uplinkCapacity ?? 0; - const downlinkCapacity = data.downlinkCapacity ?? 0; - const uplinkThroughput = data.uplinkThroughput?? 0; - const downlinkThroughput = data.downlinkThroughput ?? 0; + const data = await response.json(); setStats({ - uplinkThroughput, - downlinkThroughput, - uplinkCapacity, - downlinkCapacity, + uplinkRttMs: data.uplinkRttMs ?? 0, + downlinkRttMs: data.downlinkRttMs ?? 0, + uplinkUp: Boolean(data.uplinkUp), + downlinkUp: Boolean(data.downlinkUp), }); + // Set ping results for each host + if (data.pings) { + setPings(data.pings); + } + // Track base station errors + if (data.baseStationError) { + setBaseStationError(data.baseStationError); + } else { + setBaseStationError(null); + } } catch (error) { console.error("Polling error:", error); } }; - // Initial poll + start interval poll(); interval = setInterval(poll, 1000); - // Cleanup on unmount - return () => { - if (interval) clearInterval(interval); - }; + return () => clearInterval(interval); }, []); - // rounded throughput kbps / 10 to get 2 decimal places, then divided by 100 to finish conversion to Mbps - const data = [ - { - name: "Uplink", - Throughput: Math.round(stats.uplinkThroughput / 10) / 100, - Capacity: stats.uplinkCapacity / 1000, - }, - { - name: "Downlink", - Throughput: Math.round(stats.downlinkThroughput / 10) / 100, - Capacity: stats.downlinkCapacity / 1000, - }, + const rows = [ + // Add ping results for each host + ...hosts.map((host) => ({ + + name: hostnames[host] ?? host, + rttMs: pings[host] ?? 0, + up: pings[host] !== -1 && pings[host] !== undefined, + + })), ]; return ( @@ -83,42 +92,36 @@ const NetworkHealthTelemetryPanel: React.FC = () => { flexDirection: "column", }} > +
+ + + + + + + + -
- - - Math.floor(dataMax * 1.2)]} - stroke="#ccc" - tick={{ fill: "#ccc" }} - /> - - - - - - {data.map((entry, index) => ( - 0.85 - ? "#ef4444" - : "#3b82f6" - } - /> - ))} - - - +
+ {rows.map((r) => ( + + + + + + ))} + +
+ Name + + RTT (ms) + + Status +
{r.name} + {r.rttMs === -1 ? "Offline" : r.rttMs} + + +
); From 1a5ffa4a7365f1973d800ff579fb0de890faf893 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 14 Jan 2026 22:40:16 -0500 Subject: [PATCH 2/9] - Timeout duration changed to 1s - hosts are now solely taken from route.js --- src/app/dashboard/api/route.ts | 4 ++-- src/components/panels/NetworkHealthTelemetryPanel.tsx | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/app/dashboard/api/route.ts b/src/app/dashboard/api/route.ts index 2fa378f..c5dd128 100644 --- a/src/app/dashboard/api/route.ts +++ b/src/app/dashboard/api/route.ts @@ -2,13 +2,13 @@ const USERNAME = 'ubnt'; const PASSWORD = 'samitherover'; const baseStationIP = '192.168.0.2'; -const hosts = ['192.168.0.2', '172.19.228.1']; // Add more hosts here as needed (must match hosts in NetworkHealthTelemetryPanel.tsx) +const hosts = ['192.168.0.2', '172.19.228.1']; // Add more hosts here as needed const ping = require('ping'); // Ping configuration const pingConfig = { - timeout: 5, + timeout: 1, // timeout set to 1 second }; // Function to ping multiple hosts and return RTT times in dictionary format diff --git a/src/components/panels/NetworkHealthTelemetryPanel.tsx b/src/components/panels/NetworkHealthTelemetryPanel.tsx index 05d8775..debf9a4 100644 --- a/src/components/panels/NetworkHealthTelemetryPanel.tsx +++ b/src/components/panels/NetworkHealthTelemetryPanel.tsx @@ -8,9 +8,6 @@ const dotStyle = (up: boolean): React.CSSProperties => ({ backgroundColor: up ? "#22c55e" : "#ef4444", }); -const hosts = ["192.168.0.2", "172.19.228.1"]; // Add more hosts here as needed (must match hosts in route.ts) -const hostnames: { [key: string]: string } = {"192.168.0.2": "Base"}; // set nicknames here - const NetworkHealthTelemetryPanel: React.FC = () => { const [stats, setStats] = useState({ uplinkRttMs: 0, @@ -71,9 +68,9 @@ const NetworkHealthTelemetryPanel: React.FC = () => { const rows = [ // Add ping results for each host - ...hosts.map((host) => ({ + ...Object.keys(pings).map((host) => ({ - name: hostnames[host] ?? host, + name: host, rttMs: pings[host] ?? 0, up: pings[host] !== -1 && pings[host] !== undefined, From 934b1d2baf4ccb144ebdb43f38bee0893efd4262 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 15 Jan 2026 01:43:27 -0500 Subject: [PATCH 3/9] - original graph added back into the panel - basically merged the two versions together - removed a LOT of unnecessary padding and made graph shorter - should be able to see ~ 5 devices status w/ graph now - as always needs testing with rover before merging --- .../panels/NetworkHealthTelemetryPanel.tsx | 96 +++++++++++++++++-- 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/src/components/panels/NetworkHealthTelemetryPanel.tsx b/src/components/panels/NetworkHealthTelemetryPanel.tsx index debf9a4..eb5f52e 100644 --- a/src/components/panels/NetworkHealthTelemetryPanel.tsx +++ b/src/components/panels/NetworkHealthTelemetryPanel.tsx @@ -1,4 +1,14 @@ import React, { useEffect, useState } from "react"; +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + Cell, + LabelList, +} from "recharts"; const dotStyle = (up: boolean): React.CSSProperties => ({ width: 12, @@ -10,10 +20,10 @@ const dotStyle = (up: boolean): React.CSSProperties => ({ const NetworkHealthTelemetryPanel: React.FC = () => { const [stats, setStats] = useState({ - uplinkRttMs: 0, - downlinkRttMs: 0, - uplinkUp: false, - downlinkUp: false, + uplinkThroughput: 0, + downlinkThroughput: 0, + uplinkCapacity: 100, + downlinkCapacity: 100, }); const [pings, setPings] = useState<{ [key: string]: number }>({}); @@ -39,11 +49,15 @@ const NetworkHealthTelemetryPanel: React.FC = () => { const data = await response.json(); + const uplinkCapacity = data.uplinkCapacity ?? 0; + const downlinkCapacity = data.downlinkCapacity ?? 0; + const uplinkThroughput = data.uplinkThroughput?? 0; + const downlinkThroughput = data.downlinkThroughput ?? 0; setStats({ - uplinkRttMs: data.uplinkRttMs ?? 0, - downlinkRttMs: data.downlinkRttMs ?? 0, - uplinkUp: Boolean(data.uplinkUp), - downlinkUp: Boolean(data.downlinkUp), + uplinkThroughput, + downlinkThroughput, + uplinkCapacity, + downlinkCapacity, }); // Set ping results for each host if (data.pings) { @@ -77,19 +91,32 @@ const NetworkHealthTelemetryPanel: React.FC = () => { })), ]; + const data = [ + { + name: "Uplink", + Throughput: Math.round(stats.uplinkThroughput / 10) / 100, + Capacity: stats.uplinkCapacity / 1000, + }, + { + name: "Downlink", + Throughput: Math.round(stats.downlinkThroughput / 10) / 100, + Capacity: stats.downlinkCapacity / 1000, + }, + ]; + return (
-
+
@@ -120,7 +147,56 @@ const NetworkHealthTelemetryPanel: React.FC = () => {
+
+ +
+ + + Math.floor(dataMax * 1.2)]} + stroke="#ccc" + tick={{ fill: "#ccc" }} + /> + + + + + + {data.map((entry, index) => ( + 0.85 + ? "#ef4444" + : "#3b82f6" + } + /> + ))} + + + +
+
); }; From 8488e4f3aa52fa08f418add6c4764919824e8a35 Mon Sep 17 00:00:00 2001 From: Jack Wong <120768324+jackcwong2007@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:18:24 -0500 Subject: [PATCH 4/9] removed comment --- src/app/dashboard/api/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/dashboard/api/route.ts b/src/app/dashboard/api/route.ts index c5dd128..b4c9131 100644 --- a/src/app/dashboard/api/route.ts +++ b/src/app/dashboard/api/route.ts @@ -103,7 +103,7 @@ export async function GET(request: Request) { baseStationError, // Include error info in response }); } catch (error: any) { - console.error('Failed to ping hosts:', error); // put toast notifications here LATER + console.error('Failed to ping hosts:', error); return Response.json( { error: 'Failed to ping hosts: ' + error.message }, { status: 500 } From 092ab7e5142fe78d3e4562fa546a1e325ddca40a Mon Sep 17 00:00:00 2001 From: = Date: Sat, 24 Jan 2026 00:33:56 -0500 Subject: [PATCH 5/9] Added MotorStatusPanel, compact table to show information on four motors - Add panel in ROS2 Dashboard - Wasn't sure whether to modify OrientationDisplayPanel or make new panel, although new panel is based off of old panel's code so should be easy to merge if necessary - Also unsure about units for velocity and current (just used SI units) - As always, needs testing on rover before merging --- src/components/panels/MosaicDashboard.tsx | 18 ++- src/components/panels/MotorStatusPanel.tsx | 151 +++++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/components/panels/MotorStatusPanel.tsx diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index a1d4be6..36e99e4 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -17,6 +17,7 @@ import GoalSetterPanel from './GoalSetterPanel'; import GasSensor from './GasSensor'; import NetworkHealthTelemetryPanel from './NetworkHealthTelemetryPanel'; import VideoControls from './VideoControls'; +import MotorStatusPanel from './MotorStatusPanel'; type TileType = | 'mapView' @@ -26,7 +27,8 @@ type TileType = | 'gasSensor' | 'orientationDisplay' | 'goalSetter' - | 'networkHealthMonitor'; + | 'networkHealthMonitor' + | 'MotorStatusPanel'; type TileId = `${TileType}:${string}`; @@ -39,6 +41,7 @@ const TILE_DISPLAY_NAMES: Record = { orientationDisplay: 'Rover Orientation', goalSetter: 'Nav2', networkHealthMonitor: 'Connection Health', + MotorStatusPanel: 'motor', }; const ALL_TILE_TYPES: TileType[] = [ @@ -50,6 +53,7 @@ const ALL_TILE_TYPES: TileType[] = [ 'waypointList', 'gasSensor', 'goalSetter', + 'MotorStatusPanel', ]; function makeTileId(type: TileType): TileId { @@ -314,6 +318,18 @@ const MosaicDashboard: React.FC = () => { ); + case 'MotorStatusPanel': + return ( + + title={TILE_DISPLAY_NAMES[type]} + path={path} + additionalControls={ + + } + > + + + ); default: return
Unknown tile
; diff --git a/src/components/panels/MotorStatusPanel.tsx b/src/components/panels/MotorStatusPanel.tsx new file mode 100644 index 0000000..1ae3fc4 --- /dev/null +++ b/src/components/panels/MotorStatusPanel.tsx @@ -0,0 +1,151 @@ +'use client'; +import React, { useEffect, useRef, useState } from 'react'; +import * as THREE from 'three'; +import ROSLIB from 'roslib'; +import { useROS } from '@/ros/ROSContext'; + +type MotorStatus = { + velocity: number; + temperature: number; + output_current: number; +}; + +const MotorStatusPanel: React.FC = () => { + const { ros } = useROS(); + const containerRef = useRef(null); + const cubeRef = useRef(null); + + const [motorStats, setMotorStats] = useState<{ + fl: MotorStatus | null; + fr: MotorStatus | null; + rl: MotorStatus | null; + rr: MotorStatus | null; + }>({ + fl: null, + fr: null, + rl: null, + rr: null, + }); + + + useEffect(() => { + if (!ros) return; + + const motors = { + fl: '/frontLeft/status', + fr: '/frontRight/status', + rl: '/backLeft/status', + rr: '/backRight/status', + }; + + const subscriptions = Object.entries(motors).map(([key, topicName]) => { + const topic = new ROSLIB.Topic({ + ros, + name: topicName, + messageType: 'ros_phoenix/msg/MotorStatus', + throttle_rate: 100, + }); + + const handler = (msg: any) => { + setMotorStats(prev => ({ + ...prev, + [key]: { + velocity: msg.velocity, + temperature: msg.temperature, + output_current: msg.output_current, + }, + })); + }; + + topic.subscribe(handler); + return () => topic.unsubscribe(handler); + }); + + return () => subscriptions.forEach(unsub => unsub()); + }, [ros]); + + + + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MotorVel.Temp.Curr.
FLeft{motorStats.fl?.velocity.toFixed(2) || '-'}{motorStats.fl?.temperature.toFixed(2) || '-'}°C{motorStats.fl?.output_current.toFixed(2) || '-'}A
FRight{motorStats.fr?.velocity.toFixed(2) || '-'}{motorStats.fr?.temperature.toFixed(2) || '-'}°C{motorStats.fr?.output_current.toFixed(2) || '-'}A
BLeft{motorStats.rl?.velocity.toFixed(2) || '-'}{motorStats.rl?.temperature.toFixed(2) || '-'}°C{motorStats.rl?.output_current.toFixed(2) || '-'}A
BRight{motorStats.rr?.velocity.toFixed(2) || '-'}{motorStats.rr?.temperature.toFixed(2) || '-'}°C{motorStats.rr?.output_current.toFixed(2) || '-'}A
+ + +
+ ); +}; + +export default MotorStatusPanel; From 1556bcf65d511fcdf5eb2aa7136f9ad5b4c6c385 Mon Sep 17 00:00:00 2001 From: ConnorN Date: Mon, 26 Jan 2026 19:20:54 -0500 Subject: [PATCH 6/9] switch to using uuid lib --- package-lock.json | 22 ++++++++++++++++++---- package.json | 1 + src/components/panels/MosaicDashboard.tsx | 3 ++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 142b1f7..2dc79f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "recharts": "^2.15.1", "roslib": "^1.4.1", "three": "^0.173.0", + "uuid": "^13.0.0", "webrtc-adapter": "^9.0.1" }, "devDependencies": { @@ -5701,6 +5702,19 @@ "react-dnd": "^16.0.1" } }, + "node_modules/react-mosaic-component/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/react-popper": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", @@ -7011,16 +7025,16 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/vary": { diff --git a/package.json b/package.json index 3795dde..0e1fbba 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "recharts": "^2.15.1", "roslib": "^1.4.1", "three": "^0.173.0", + "uuid": "^13.0.0", "webrtc-adapter": "^9.0.1" }, "devDependencies": { diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index 36e99e4..674fd8c 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -18,6 +18,7 @@ import GasSensor from './GasSensor'; import NetworkHealthTelemetryPanel from './NetworkHealthTelemetryPanel'; import VideoControls from './VideoControls'; import MotorStatusPanel from './MotorStatusPanel'; +import { v4 as uuidv4 } from 'uuid'; type TileType = | 'mapView' @@ -57,7 +58,7 @@ const ALL_TILE_TYPES: TileType[] = [ ]; function makeTileId(type: TileType): TileId { - const uid = crypto.randomUUID(); + const uid = uuidv4(); return `${type}:${uid}`; } From 2b794e6f1261dad418c7b879036084762e733b83 Mon Sep 17 00:00:00 2001 From: Jetson Date: Mon, 26 Jan 2026 19:41:43 -0500 Subject: [PATCH 7/9] feat: use mapping from list --- src/components/panels/MotorStatusPanel.tsx | 103 +++++++++------------ 1 file changed, 44 insertions(+), 59 deletions(-) diff --git a/src/components/panels/MotorStatusPanel.tsx b/src/components/panels/MotorStatusPanel.tsx index 1ae3fc4..1319a90 100644 --- a/src/components/panels/MotorStatusPanel.tsx +++ b/src/components/panels/MotorStatusPanel.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useRef, useState } from 'react'; -import * as THREE from 'three'; + +import React, { useEffect, useState } from 'react'; import ROSLIB from 'roslib'; import { useROS } from '@/ros/ROSContext'; @@ -10,38 +10,36 @@ type MotorStatus = { output_current: number; }; +const MOTORS = { + fl: { label: 'FLeft', topic: '/frontLeft/status' }, + fr: { label: 'FRight', topic: '/frontRight/status' }, + rl: { label: 'BLeft', topic: '/backLeft/status' }, + rr: { label: 'BRight', topic: '/backRight/status' }, +} as const; + +type MotorKey = keyof typeof MOTORS; + const MotorStatusPanel: React.FC = () => { const { ros } = useROS(); - const containerRef = useRef(null); - const cubeRef = useRef(null); - - const [motorStats, setMotorStats] = useState<{ - fl: MotorStatus | null; - fr: MotorStatus | null; - rl: MotorStatus | null; - rr: MotorStatus | null; - }>({ + + const [motorStats, setMotorStats] = useState< + Record + >({ fl: null, fr: null, rl: null, rr: null, }); - useEffect(() => { if (!ros) return; - const motors = { - fl: '/frontLeft/status', - fr: '/frontRight/status', - rl: '/backLeft/status', - rr: '/backRight/status', - }; - - const subscriptions = Object.entries(motors).map(([key, topicName]) => { - const topic = new ROSLIB.Topic({ + const unsubscribers = ( + Object.entries(MOTORS) as [MotorKey, typeof MOTORS[MotorKey]][] + ).map(([key, { topic }]) => { + const rosTopic = new ROSLIB.Topic({ ros, - name: topicName, + name: topic, messageType: 'ros_phoenix/msg/MotorStatus', throttle_rate: 100, }); @@ -57,16 +55,13 @@ const MotorStatusPanel: React.FC = () => { })); }; - topic.subscribe(handler); - return () => topic.unsubscribe(handler); + rosTopic.subscribe(handler); + return () => rosTopic.unsubscribe(handler); }); - return () => subscriptions.forEach(unsub => unsub()); + return () => unsubscribers.forEach(unsub => unsub()); }, [ros]); - - - return (
@@ -79,30 +74,20 @@ const MotorStatusPanel: React.FC = () => { - - - - - - - - - - - - - - - - - - - - - - - - + {(Object.entries(MOTORS) as [MotorKey, typeof MOTORS[MotorKey]][]).map( + ([key, { label }]) => { + const stat = motorStats[key]; + + return ( + + + + + + + ); + } + )}
FLeft{motorStats.fl?.velocity.toFixed(2) || '-'}{motorStats.fl?.temperature.toFixed(2) || '-'}°C{motorStats.fl?.output_current.toFixed(2) || '-'}A
FRight{motorStats.fr?.velocity.toFixed(2) || '-'}{motorStats.fr?.temperature.toFixed(2) || '-'}°C{motorStats.fr?.output_current.toFixed(2) || '-'}A
BLeft{motorStats.rl?.velocity.toFixed(2) || '-'}{motorStats.rl?.temperature.toFixed(2) || '-'}°C{motorStats.rl?.output_current.toFixed(2) || '-'}A
BRight{motorStats.rr?.velocity.toFixed(2) || '-'}{motorStats.rr?.temperature.toFixed(2) || '-'}°C{motorStats.rr?.output_current.toFixed(2) || '-'}A
{label}{stat ? stat.velocity.toFixed(2) : '-'}{stat ? `${stat.temperature.toFixed(2)}°C` : '-'}{stat ? `${stat.output_current.toFixed(2)}A` : '-'}
@@ -117,29 +102,29 @@ const MotorStatusPanel: React.FC = () => { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; overflow: auto; } - h3 { - margin: 0 0 0.25rem 0; - font-size: 1.25rem; - text-align: center; - border-bottom: 1px solid #444; - } + .motor-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; } + .motor-table thead { background: #2d2d2d; border-bottom: 2px solid #444; } + .motor-table th { text-align: left; font-weight: 600; - color: #f1f1f1; + padding: 0.5rem; } + .motor-table td { + padding: 0.5rem; border-bottom: 1px solid #333; } + .motor-table tbody tr:hover { background-color: #262626; } From f5bdb2e19bc1815022860ad2c5222d8db0b171e1 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 26 Jan 2026 21:47:28 -0500 Subject: [PATCH 8/9] removed padding to save space --- src/components/panels/MotorStatusPanel.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/panels/MotorStatusPanel.tsx b/src/components/panels/MotorStatusPanel.tsx index 1319a90..9bdab32 100644 --- a/src/components/panels/MotorStatusPanel.tsx +++ b/src/components/panels/MotorStatusPanel.tsx @@ -95,7 +95,6 @@ const MotorStatusPanel: React.FC = () => { .motor-panel { background: #1e1e1e; color: #f1f1f1; - border-radius: 8px; height: 100%; display: flex; flex-direction: column; @@ -117,11 +116,10 @@ const MotorStatusPanel: React.FC = () => { .motor-table th { text-align: left; font-weight: 600; - padding: 0.5rem; + } .motor-table td { - padding: 0.5rem; border-bottom: 1px solid #333; } From 6bfe853f275223a816b9da1dab5fdde877a1b8a7 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 26 Jan 2026 22:11:50 -0500 Subject: [PATCH 9/9] Removed unnecessary/unused code in OrientationDisplayPanel - Removed everything and all references to do with Motor status (in new MotorStatusPanel) - Don't think I broke anything, no errors when testing on local station. --- .../panels/OrientationDisplayPanel.tsx | 128 ------------------ 1 file changed, 128 deletions(-) diff --git a/src/components/panels/OrientationDisplayPanel.tsx b/src/components/panels/OrientationDisplayPanel.tsx index f828c85..5b766b9 100644 --- a/src/components/panels/OrientationDisplayPanel.tsx +++ b/src/components/panels/OrientationDisplayPanel.tsx @@ -4,30 +4,11 @@ import * as THREE from 'three'; import ROSLIB from 'roslib'; import { useROS } from '@/ros/ROSContext'; -type MotorStatus = { - velocity: number; - temperature: number; - output_current: number; - bus_voltage: number; -}; - const OrientationDisplayPanel: React.FC = () => { const { ros } = useROS(); const containerRef = useRef(null); const cubeRef = useRef(null); - const [motorStats, setMotorStats] = useState<{ - fl: MotorStatus | null; - fr: MotorStatus | null; - rl: MotorStatus | null; - rr: MotorStatus | null; - }>({ - fl: null, - fr: null, - rl: null, - rr: null, - }); - useEffect(() => { if (!containerRef.current) return; @@ -121,95 +102,10 @@ const OrientationDisplayPanel: React.FC = () => { return () => imuTopic.unsubscribe(handleIMU); }, [ros]); - useEffect(() => { - if (!ros) return; - - const motors = { - fl: '/frontLeft/status', - fr: '/frontRight/status', - rl: '/backLeft/status', - rr: '/backRight/status', - }; - - const subscriptions = Object.entries(motors).map(([key, topicName]) => { - const topic = new ROSLIB.Topic({ - ros, - name: topicName, - messageType: 'ros_phoenix/msg/MotorStatus', - throttle_rate: 100, - }); - - const handler = (msg: any) => { - setMotorStats(prev => ({ - ...prev, - [key]: { - velocity: msg.velocity, - temperature: msg.temperature, - output_current: msg.output_current, - bus_voltage: msg.bus_voltage, - }, - })); - }; - - topic.subscribe(handler); - return () => topic.unsubscribe(handler); - }); - - return () => subscriptions.forEach(unsub => unsub()); - }, [ros]); - - const renderBar = (value: number, color: string, clamp: number) => { - const percentage = Math.min(Math.max(value / clamp, 0), 1) * 100; - return ( -
-
-
- ); - }; - - const renderMotorInfo = (label: string, data: MotorStatus | null) => { - if (!data) return
{label}: waiting for data...
; - return ( -
- {label}
-
{data.velocity < 0 ? renderBar(Math.abs(data.velocity), '#f00', 20) : renderBar(data.velocity, '#0f0', 20)} {data.velocity.toFixed(2)} m/s
-
{renderBar(data.output_current, '#ff0', 6)} {data.output_current.toFixed(2)} A
- Temp: {data.temperature.toFixed(1)} °C
-
- ); - }; - - const getBusVoltage = () => { - const voltages = Object.values(motorStats) - .filter((stat): stat is MotorStatus => stat !== null) - .map(stat => stat.bus_voltage); - - if (voltages.length === 0) return null; - - const avg = voltages.reduce((sum, v) => sum + v, 0) / voltages.length; - return avg.toFixed(2); - }; return (
-
{getBusVoltage() ? `Bus Voltage: ${getBusVoltage()} V` : 'Waiting for voltage...'}
-
{renderMotorInfo('Front Left', motorStats.fl)}
-
{renderMotorInfo('Front Right', motorStats.fr)}
-
{renderMotorInfo('Rear Left', motorStats.rl)}
-
{renderMotorInfo('Rear Right', motorStats.rr)}
);