From 6e8208e54cbb3cc8ef20bbcfd09b05796f495188 Mon Sep 17 00:00:00 2001 From: Ahnaf Kamal Date: Fri, 2 Oct 2026 00:22:23 -0400 Subject: [PATCH 1/3] motor current panel --- src/components/panels/MosaicDashboard.tsx | 2 + .../panels/MotorCurrentGraphPanel.tsx | 223 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/components/panels/MotorCurrentGraphPanel.tsx diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index 0bf130c..2571027 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -30,6 +30,7 @@ import MorseTransmissionPanel from './MorseTransmissionPanel'; import RtpStats from './RtpStats'; import TopicEchoPanel from './TopicEchoPanel'; import DriveThrottlePanel from './DriveThrottlePanel'; +import MotorCurrentGraphPanel from './MotorCurrentGraphPanel'; import { ROVER_IP } from '@/constants'; @@ -70,6 +71,7 @@ const TILE_REGISTRY = { headlightControlPanel: { title: 'Headlights', render: () => }, morseTransmissionPanel: { title: 'Morse Transmission', render: () => }, echoPanel: { title: 'Topic Echo', render: () => }, + motorCurrentGraphPanel: { title: 'Motor Current', render: () => }, } satisfies Record; type TileType = keyof typeof TILE_REGISTRY; diff --git a/src/components/panels/MotorCurrentGraphPanel.tsx b/src/components/panels/MotorCurrentGraphPanel.tsx new file mode 100644 index 0000000..7cacd6a --- /dev/null +++ b/src/components/panels/MotorCurrentGraphPanel.tsx @@ -0,0 +1,223 @@ +'use client'; + +import React, { useEffect, useRef, useState } from 'react'; +import ROSLIB from 'roslib'; +import html2canvas from 'html2canvas'; +import { useROS } from '@/ros/ROSContext'; +import { + ResponsiveContainer, + LineChart, + Line, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + Legend, +} from 'recharts'; + +const WINDOW_MS = 60 * 1000; +const DEFAULT_MAX_AMPS = 12; + +const CURRENT_MOTORS = { + fl_d: { label: 'FL Drive', topic: '/Left_front_wheel_joint/status', color: '#4da3ff', dashed: false }, + fr_d: { label: 'FR Drive', topic: '/Right_front_wheel_joint/status', color: '#ff7a45', dashed: false }, + rl_d: { label: 'BL Drive', topic: '/Left_back_wheel_joint/status', color: '#4cd964', dashed: false }, + rr_d: { label: 'BR Drive', topic: '/Right_back_wheel_joint/status', color: '#c77dff', dashed: false }, + fl_s: { label: 'FL Steer', topic: '/Left_front_wheel_arm_joint/status', color: '#4da3ff', dashed: true }, + fr_s: { label: 'FR Steer', topic: '/Right_front_wheel_arm_joint/status', color: '#ff7a45', dashed: true }, + rl_s: { label: 'BL Steer', topic: '/Left_back_wheel_arm_joint/status', color: '#4cd964', dashed: true }, + rr_s: { label: 'BR Steer', topic: '/Right_back_wheel_arm_joint/status', color: '#c77dff', dashed: true }, +} as const; + +type MotorKey = keyof typeof CURRENT_MOTORS; +const KEYS = Object.keys(CURRENT_MOTORS) as MotorKey[]; + +type Point = { time: number; value: number }; +type CurrentHistory = Record; + +const emptyCurrentHistory= () => Object.fromEntries(KEYS.map((key) => [key, []])) as unknown as CurrentHistory; + +const MotorCurrentGraphPanel: React.FC = () => { + const { ros } = useROS(); + const [currentHistory, setCurrentHistory] = useState(emptyCurrentHistory); + const containerRef = useRef(null); + + useEffect(() => { + if (!ros) return; + + const unsubscribers = KEYS.map((key) => { + const topic = new ROSLIB.Topic({ + ros, + name: CURRENT_MOTORS[key].topic, + messageType: 'ros_phoenix/msg/MotorStatus', + throttle_rate: 100, + }); + + const handler = (msg: any) => { + const value = Number(msg.output_current); + if (!Number.isFinite(value)) return; + + const now = Date.now(); + const cutoff = now - WINDOW_MS; + + setCurrentHistory((prev) => { + const next = {} as CurrentHistory; + KEYS.forEach((k) => { + next[k] = prev[k].filter((p) => p.time >= cutoff); + }); + next[key].push({ time: now, value }); + return next; + }); + }; + + topic.subscribe(handler); + return () => topic.unsubscribe(handler); + }); + + return () => unsubscribers.forEach((unsub) => unsub()); + }, [ros]); + + const formatTime = (time: number) => + new Date(time).toLocaleTimeString([], { + minute: '2-digit', + second: '2-digit', + }); + + const downloadPNG = async () => { + if (!containerRef.current) return; + + const canvas = await html2canvas(containerRef.current, { + backgroundColor: '#181818', + }); + + const link = document.createElement('a'); + link.download = 'motor-currents.png'; + link.href = canvas.toDataURL('image/png'); + link.click(); + }; + + return ( +
+
+

Motor Currents

+
+ +
+
+ +
+ + + + + + + Number.isFinite(dataMax) ? Math.max(DEFAULT_MAX_AMPS, Math.ceil(dataMax)) : DEFAULT_MAX_AMPS, + ]} + unit="A" + tick={{ fill: '#aaa', fontSize: 10 }} + axisLine={{ stroke: '#444' }} + tickLine={{ stroke: '#444' }} + width={55} + /> + + [`${Number(value).toFixed(2)} A`, name]} + labelFormatter={(value) => formatTime(Number(value))} + contentStyle={{ + background: '#222', + border: '1px solid #444', + borderRadius: '8px', + color: '#fff', + }} + /> + + + + {KEYS.map((key) => ( + + ))} + + +
+ + +
+ ); +}; + +export default MotorCurrentGraphPanel; From 416531c55bdaf2930ad111ed6ada3abbfaaf6a69 Mon Sep 17 00:00:00 2001 From: Ahnaf Kamal Date: Thu, 8 Oct 2026 00:24:17 -0400 Subject: [PATCH 2/3] coments --- .../panels/MotorCurrentGraphPanel.tsx | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/src/components/panels/MotorCurrentGraphPanel.tsx b/src/components/panels/MotorCurrentGraphPanel.tsx index 7cacd6a..720b9c8 100644 --- a/src/components/panels/MotorCurrentGraphPanel.tsx +++ b/src/components/panels/MotorCurrentGraphPanel.tsx @@ -15,8 +15,8 @@ import { Legend, } from 'recharts'; -const WINDOW_MS = 60 * 1000; -const DEFAULT_MAX_AMPS = 12; +const WINDOW_SEC = 60; +const DEFAULT_MAX_AMPS = 2; const CURRENT_MOTORS = { fl_d: { label: 'FL Drive', topic: '/Left_front_wheel_joint/status', color: '#4da3ff', dashed: false }, @@ -32,15 +32,16 @@ const CURRENT_MOTORS = { type MotorKey = keyof typeof CURRENT_MOTORS; const KEYS = Object.keys(CURRENT_MOTORS) as MotorKey[]; -type Point = { time: number; value: number }; +type Point = { time: number; value: number }; // time = seconds since the panel opened type CurrentHistory = Record; -const emptyCurrentHistory= () => Object.fromEntries(KEYS.map((key) => [key, []])) as unknown as CurrentHistory; +const emptyCurrentHistory = () => Object.fromEntries(KEYS.map((key) => [key, []])) as unknown as CurrentHistory; const MotorCurrentGraphPanel: React.FC = () => { const { ros } = useROS(); const [currentHistory, setCurrentHistory] = useState(emptyCurrentHistory); const containerRef = useRef(null); + const startTime = useRef(Date.now()); useEffect(() => { if (!ros) return; @@ -50,15 +51,14 @@ const MotorCurrentGraphPanel: React.FC = () => { ros, name: CURRENT_MOTORS[key].topic, messageType: 'ros_phoenix/msg/MotorStatus', - throttle_rate: 100, }); const handler = (msg: any) => { const value = Number(msg.output_current); if (!Number.isFinite(value)) return; - const now = Date.now(); - const cutoff = now - WINDOW_MS; + const now = (Date.now() - startTime.current) / 1000; + const cutoff = now - WINDOW_SEC; setCurrentHistory((prev) => { const next = {} as CurrentHistory; @@ -77,14 +77,8 @@ const MotorCurrentGraphPanel: React.FC = () => { return () => unsubscribers.forEach((unsub) => unsub()); }, [ros]); - const formatTime = (time: number) => - new Date(time).toLocaleTimeString([], { - minute: '2-digit', - second: '2-digit', - }); - - const downloadPNG = async () => { - if (!containerRef.current) return; + const downloadPNG = async () => { + if (!containerRef.current) return; const canvas = await html2canvas(containerRef.current, { backgroundColor: '#181818', @@ -103,24 +97,21 @@ const MotorCurrentGraphPanel: React.FC = () => {
- - -
+
+
- `${Math.round(t)}s`} tick={{ fill: '#aaa', fontSize: 10 }} axisLine={{ stroke: '#444' }} tickLine={{ stroke: '#444' }} minTickGap={30} /> - { tickLine={{ stroke: '#444' }} width={55} /> - [`${Number(value).toFixed(2)} A`, name]} - labelFormatter={(value) => formatTime(Number(value))} + labelFormatter={(value) => `${Number(value).toFixed(1)}s`} contentStyle={{ background: '#222', border: '1px solid #444', @@ -143,9 +133,7 @@ const MotorCurrentGraphPanel: React.FC = () => { color: '#fff', }} /> - - {KEYS.map((key) => ( Date: Sun, 11 Oct 2026 10:06:11 -0400 Subject: [PATCH 3/3] comments --- src/components/panels/MotorCurrentGraphPanel.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/panels/MotorCurrentGraphPanel.tsx b/src/components/panels/MotorCurrentGraphPanel.tsx index 720b9c8..9b941df 100644 --- a/src/components/panels/MotorCurrentGraphPanel.tsx +++ b/src/components/panels/MotorCurrentGraphPanel.tsx @@ -32,7 +32,7 @@ const CURRENT_MOTORS = { type MotorKey = keyof typeof CURRENT_MOTORS; const KEYS = Object.keys(CURRENT_MOTORS) as MotorKey[]; -type Point = { time: number; value: number }; // time = seconds since the panel opened +type Point = { time: number; value: number }; // time = Date.now() when the message arrived (ms) type CurrentHistory = Record; const emptyCurrentHistory = () => Object.fromEntries(KEYS.map((key) => [key, []])) as unknown as CurrentHistory; @@ -41,7 +41,6 @@ const MotorCurrentGraphPanel: React.FC = () => { const { ros } = useROS(); const [currentHistory, setCurrentHistory] = useState(emptyCurrentHistory); const containerRef = useRef(null); - const startTime = useRef(Date.now()); useEffect(() => { if (!ros) return; @@ -57,8 +56,8 @@ const MotorCurrentGraphPanel: React.FC = () => { const value = Number(msg.output_current); if (!Number.isFinite(value)) return; - const now = (Date.now() - startTime.current) / 1000; - const cutoff = now - WINDOW_SEC; + const now = Date.now(); + const cutoff = now - WINDOW_SEC * 1000; setCurrentHistory((prev) => { const next = {} as CurrentHistory; @@ -106,7 +105,7 @@ const MotorCurrentGraphPanel: React.FC = () => { dataKey="time" type="number" domain={['dataMin', 'dataMax']} - tickFormatter={(t) => `${Math.round(t)}s`} + tickFormatter={(t) => `${Math.round((Date.now() - t) / 1000)}s ago`} tick={{ fill: '#aaa', fontSize: 10 }} axisLine={{ stroke: '#444' }} tickLine={{ stroke: '#444' }} @@ -125,7 +124,7 @@ const MotorCurrentGraphPanel: React.FC = () => { /> [`${Number(value).toFixed(2)} A`, name]} - labelFormatter={(value) => `${Number(value).toFixed(1)}s`} + labelFormatter={(value) => `${((Date.now() - Number(value)) / 1000).toFixed(1)}s ago`} contentStyle={{ background: '#222', border: '1px solid #444',