diff --git a/gs/frontend/mcc/package-lock.json b/gs/frontend/mcc/package-lock.json index d99df6328..c0925a0cc 100644 --- a/gs/frontend/mcc/package-lock.json +++ b/gs/frontend/mcc/package-lock.json @@ -20,10 +20,12 @@ "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.13", "@tanstack/react-table": "^8.21.3", + "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.544.0", "react": "^19.1.1", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.1.1", "react-router-dom": "^7.8.2", "react-toastify": "^11.0.5", @@ -495,6 +497,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -541,6 +544,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1170,6 +1174,7 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" }, @@ -1323,6 +1328,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2616,6 +2627,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2788,6 +2800,7 @@ "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2798,6 +2811,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2808,6 +2822,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2858,6 +2873,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -3225,6 +3241,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3405,6 +3422,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3494,6 +3512,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -3824,6 +3855,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4405,6 +4437,7 @@ "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@asamuzakjp/dom-selector": "^6.7.2", "cssstyle": "^5.3.1", @@ -5071,6 +5104,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5154,15 +5188,27 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5666,6 +5712,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5810,6 +5857,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5938,6 +5986,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6052,6 +6101,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/gs/frontend/mcc/package.json b/gs/frontend/mcc/package.json index c2126e2b2..57bdcc6f4 100644 --- a/gs/frontend/mcc/package.json +++ b/gs/frontend/mcc/package.json @@ -23,10 +23,12 @@ "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.13", "@tanstack/react-table": "^8.21.3", + "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.544.0", "react": "^19.1.1", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.1.1", "react-router-dom": "^7.8.2", "react-toastify": "^11.0.5", diff --git a/gs/frontend/mcc/src/App.tsx b/gs/frontend/mcc/src/App.tsx index 6614a8675..01d2f8712 100644 --- a/gs/frontend/mcc/src/App.tsx +++ b/gs/frontend/mcc/src/App.tsx @@ -8,6 +8,7 @@ import Dashboard from "./pages/Dashboard"; import AROAdmin from "./pages/AROAdmin"; import LiveSession from "./pages/LiveSession"; import Login from "./pages/Login"; +import Telemetry from "./pages/Telemetry/Telemetry"; import PageNotFound from "./components/PageNotFound"; /** @@ -25,6 +26,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/gs/frontend/mcc/src/pages/Telemetry/Telemetry.tsx b/gs/frontend/mcc/src/pages/Telemetry/Telemetry.tsx new file mode 100644 index 000000000..bb2abbb07 --- /dev/null +++ b/gs/frontend/mcc/src/pages/Telemetry/Telemetry.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { + telemetryTypes, + telemetrySubtypes, + telemetryData, + type TelemetryDataType, +} from "../../utils/mockTelemetryData"; // TODO: Create API to fetch real telemetry data +import TypeSelector from "./components/TypeSelector"; +import SubtypeSelector from "./components/SubtypeSelector"; +import Table from "../../components/Table"; +import { createColumnHelper } from "@tanstack/react-table"; +import TelemetryChart from "./components/TelemetryChart"; +import ThesholdSelector from "./components/ThesholdSelector"; + +const columnHelper = createColumnHelper(); + +// TODO: Update columns as per real telemetry data structure when available +const columns = [ + columnHelper.accessor("session", { + header: "Session", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("telemetry", { + header: "Telemetry", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("type", { + header: "Type", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("packet", { + header: "Packet", + cell: (info) => info.getValue(), + }), +]; + +/** + * @brief Page for displaying telemetry data in a table and chart with options to select telemetry type, subtype, and threshold. + */ +function Telemetry() { + const [type, setType] = React.useState(`${telemetryTypes[0]}`); + const [subTypeList, setSubTypeList] = React.useState( + telemetrySubtypes[type] + ); + const [threshold, setThreshold] = React.useState(0); + const [selectedSubTypeList, setSelectedSubTypeList] = + React.useState(subTypeList); + + const selectedData = telemetryData[type].filter((row) => selectedSubTypeList.includes(row.type)); + + return ( +
+
+ + +
+
+ +
+ + + + + + ); +} + +export default Telemetry; diff --git a/gs/frontend/mcc/src/pages/Telemetry/components/SubtypeSelector.tsx b/gs/frontend/mcc/src/pages/Telemetry/components/SubtypeSelector.tsx new file mode 100644 index 000000000..4cd1271a5 --- /dev/null +++ b/gs/frontend/mcc/src/pages/Telemetry/components/SubtypeSelector.tsx @@ -0,0 +1,41 @@ +/** + * @brief Component for selecting telemetry subtype(s) available for a given telemetry type + */ +function SubtypeSelector({ + subTypeList, + selectedSubTypeList, + setSelectedSubTypeList, +}: { + subTypeList: string[]; + selectedSubTypeList: string[]; + setSelectedSubTypeList: (selectedSubTypeList: string[]) => void; +}) { + return ( +
+ {subTypeList.map((st) => ( +
+ { + if (e.target.checked) { + if (!selectedSubTypeList.includes(st)) { + setSelectedSubTypeList([...selectedSubTypeList, st]); + console.log(selectedSubTypeList); + } + } else { + setSelectedSubTypeList(selectedSubTypeList.filter((item) => item !== st)); + } + }} + /> + +
+ ))} +
+ ); +} + +export default SubtypeSelector; diff --git a/gs/frontend/mcc/src/pages/Telemetry/components/TelemetryChart.tsx b/gs/frontend/mcc/src/pages/Telemetry/components/TelemetryChart.tsx new file mode 100644 index 000000000..4f09aa6fc --- /dev/null +++ b/gs/frontend/mcc/src/pages/Telemetry/components/TelemetryChart.tsx @@ -0,0 +1,72 @@ +import { Chart } from 'react-chartjs-2'; +import { + Chart as ChartJS, + LineElement, + PointElement, + LinearScale, + Title, + CategoryScale, + Legend, + Tooltip, +} from 'chart.js'; + +ChartJS.register(LineElement, PointElement, LinearScale, Title, CategoryScale, Legend, Tooltip); + +import type { TelemetryDataType } from '../../../utils/mockTelemetryData'; + +type TelemetryChartProps = { + type: string; + telemetryData: TelemetryDataType[]; +}; + +const COLORS = [ + 'rgba(255, 99, 132, 1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)', +]; + +/** + * @brief Component for displaying selected telemetry data of the as a line chart. + */ +function TelemetryChart({ type, telemetryData }: TelemetryChartProps) { + const allTimestamps = telemetryData[0]?.datapoints.map(d => d.timestamp) || []; + + const data = { + labels: allTimestamps, + datasets: telemetryData.map((entry, idx) => { + const subtype = entry.type; + return { + label: subtype, + data: entry.datapoints.map(d => d.value), + borderColor: COLORS[idx % COLORS.length], + backgroundColor: COLORS[idx % COLORS.length], + fill: false, + tension: 0.2, + }; + }), + }; + + const options = { + responsive: true, + plugins: { + legend: { position: 'top' as const }, + title: { display: true, text: `${type} Telemetry` }, + }, + // TODO: Update axes titles and formatting to match real telemetry data + scales: { + x: { title: { display: true, text: 'Timestamp' } }, + y: { title: { display: true, text: 'Value' } }, + }, + }; + + return ( +
+ +
+ ); +} + +export default TelemetryChart; diff --git a/gs/frontend/mcc/src/pages/Telemetry/components/ThesholdSelector.tsx b/gs/frontend/mcc/src/pages/Telemetry/components/ThesholdSelector.tsx new file mode 100644 index 000000000..a5089ada9 --- /dev/null +++ b/gs/frontend/mcc/src/pages/Telemetry/components/ThesholdSelector.tsx @@ -0,0 +1,25 @@ +type ThesholdSelectorProps = { + threshold: string | number; + setThreshold: (value: string | number) => void; +}; + +/* +* @brief Component for selecting threshold value +*/ +function ThesholdSelector({ threshold, setThreshold }: ThesholdSelectorProps) { + return ( +
+ Threshold: + setThreshold(e.target.value)} + className="border border-card rounded px-2 py-1 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30" + min={0} + step={0.01} + /> +
+ ); +} + +export default ThesholdSelector diff --git a/gs/frontend/mcc/src/pages/Telemetry/components/TypeSelector.tsx b/gs/frontend/mcc/src/pages/Telemetry/components/TypeSelector.tsx new file mode 100644 index 000000000..b1536a167 --- /dev/null +++ b/gs/frontend/mcc/src/pages/Telemetry/components/TypeSelector.tsx @@ -0,0 +1,52 @@ +import { + telemetrySubtypes, + telemetryTypes, +} from "../../../utils/mockTelemetryData"; + +/** + * + * @brief Component for selecting telemetry type, and updates subtype list accordingly + * + */ +function TypeSelector({ + type, + setType, + setSubType, + setSelectedSubTypeList, +}: { + type: string; + setType: (type: string) => void; + setSubType: (subType: string[]) => void; + setSelectedSubTypeList: (selectedSubTypeList: string[]) => void; +}) { + return ( +
+
+ {telemetryTypes.map((t, i) => ( + <> + + {i !== telemetryTypes.length - 1 && ( + | + )} + + ))} +
+
+ ); +} + +export default TypeSelector; diff --git a/gs/frontend/mcc/src/utils/mockTelemetryData.ts b/gs/frontend/mcc/src/utils/mockTelemetryData.ts new file mode 100644 index 000000000..79a04f28c --- /dev/null +++ b/gs/frontend/mcc/src/utils/mockTelemetryData.ts @@ -0,0 +1,142 @@ +// Mock telemetry data for testing and developing the telemetry page. +// This includes telemetry types, subtypes, and sample data points. +// Currently, format of this data may not match real telemetry data structure - refactor as needed. + +export const telemetryTypes = ["Current", "Voltage", "Motor"]; + +export const currentSubtypes = ["3v3", "5v", "12v"]; +export const voltageSubtypes = ["3v3", "5v", "7v"]; +export const motorSubtypes = ["Motor A", "Motor B", "Motor C"]; + +export const telemetrySubtypes: { [key: string]: string[] } = { + Current: currentSubtypes, + Voltage: voltageSubtypes, + Motor: motorSubtypes, +}; + + +export type TelemetryDataType = { + session: string; + telemetry: string; + type: string; // e.g. '3v3', '5v', 'Motor A' + packet: string; + datapoints: Array<{ timestamp: number; value: number }>; +}; + + +export const telemetryData: { [type: string]: TelemetryDataType[] } = { + Current: [ + { + session: "Session A", + telemetry: "Telemetry 1", + type: "3v3", + packet: "Packet Data 1", + datapoints: [ + { timestamp: 1, value: 0.5 }, + { timestamp: 2, value: 0.7 }, + { timestamp: 3, value: 0.6 }, + { timestamp: 4, value: 0.8 }, + ], + }, + { + session: "Session B", + telemetry: "Telemetry 2", + type: "5v", + packet: "Packet Data 2", + datapoints: [ + { timestamp: 1, value: 1.2 }, + { timestamp: 2, value: 1.1 }, + { timestamp: 3, value: 1.3 }, + { timestamp: 4, value: 1.4 }, + ], + }, + { + session: "Session C", + telemetry: "Telemetry 3", + type: "12v", + packet: "Packet Data 3", + datapoints: [ + { timestamp: 1, value: 2.0 }, + { timestamp: 2, value: 2.1 }, + { timestamp: 3, value: 2.2 }, + { timestamp: 4, value: 2.3 }, + ], + }, + ], + Voltage: [ + { + session: "Session D", + telemetry: "Telemetry 4", + type: "3v3", + packet: "Packet Data 4", + datapoints: [ + { timestamp: 1, value: 3.3 }, + { timestamp: 2, value: 3.2 }, + { timestamp: 3, value: 3.4 }, + { timestamp: 4, value: 3.3 }, + ], + }, + { + session: "Session E", + telemetry: "Telemetry 5", + type: "5v", + packet: "Packet Data 5", + datapoints: [ + { timestamp: 1, value: 5.0 }, + { timestamp: 2, value: 5.1 }, + { timestamp: 3, value: 5.0 }, + { timestamp: 4, value: 5.2 }, + ], + }, + { + session: "Session F", + telemetry: "Telemetry 6", + type: "7v", + packet: "Packet Data 6", + datapoints: [ + { timestamp: 1, value: 7.0 }, + { timestamp: 2, value: 7.1 }, + { timestamp: 3, value: 7.2 }, + { timestamp: 4, value: 7.1 }, + ], + }, + ], + Motor: [ + { + session: "Session G", + telemetry: "Telemetry 7", + type: "Motor A", + packet: "Packet Data 7", + datapoints: [ + { timestamp: 1, value: 100 }, + { timestamp: 2, value: 110 }, + { timestamp: 3, value: 120 }, + { timestamp: 4, value: 130 }, + ], + }, + { + session: "Session H", + telemetry: "Telemetry 8", + type: "Motor B", + packet: "Packet Data 8", + datapoints: [ + { timestamp: 1, value: 90 }, + { timestamp: 2, value: 95 }, + { timestamp: 3, value: 92 }, + { timestamp: 4, value: 98 }, + ], + }, + { + session: "Session I", + telemetry: "Telemetry 9", + type: "Motor C", + packet: "Packet Data 9", + datapoints: [ + { timestamp: 1, value: 80 }, + { timestamp: 2, value: 85 }, + { timestamp: 3, value: 88 }, + { timestamp: 4, value: 87 }, + ], + }, + ], +}; diff --git a/gs/frontend/mcc/src/utils/nav-links.ts b/gs/frontend/mcc/src/utils/nav-links.ts index cf2cd65e1..49e3b4d56 100644 --- a/gs/frontend/mcc/src/utils/nav-links.ts +++ b/gs/frontend/mcc/src/utils/nav-links.ts @@ -24,4 +24,8 @@ export const NAVIGATION_LINKS: NavLink[] = [ text: "Live Sessions", url: "/live-sessions", }, + { + text: "Telemetry", + url: "/telemetry", + }, ];