From 5ae4f22d9ed2ea19883c045d1932a05f2731fb79 Mon Sep 17 00:00:00 2001 From: Connor Needham <129120300+ConnorNeed@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:49:18 +0000 Subject: [PATCH 1/4] fix: make the trail a line instead of a sequence of dots --- public/marker-icon.png | Bin 9456 -> 0 bytes src/components/BreadCrumbTrail.tsx | 17 ----------------- src/components/panels/MapView.tsx | 2 +- 3 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 public/marker-icon.png diff --git a/public/marker-icon.png b/public/marker-icon.png deleted file mode 100644 index 4fb5f40948bde2018be9944f714555ec4230afce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9456 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 W003ms{}PS { const { ros, connectionStatus } = useROS(); const { addWaypoint } = useWaypoints(); @@ -26,7 +19,6 @@ const BreadcrumbTrail: React.FC = () => { const [paused, setPaused] = useState(false); const [lastFix, setLastFix] = useState(null); - // i hate react useEffect(() => { if (!ros) return; @@ -104,15 +96,6 @@ const BreadcrumbTrail: React.FC = () => { color="yellow" /> )} - {breadcrumbs.map((breadcrumb, index) => ( - - -
- Recorded at: {new Date(breadcrumb.timestamp).toLocaleTimeString()} -
-
-
- ))}
= ({offline}) => { return ( Date: Mon, 9 Feb 2026 19:49:35 +0000 Subject: [PATCH 2/4] fix: use rocket M9 api to get bandwidth info --- package-lock.json | 140 ++++++++++++++++++ package.json | 3 + src/app/dashboard/api/route.ts | 60 ++++---- .../panels/NetworkHealthTelemetryPanel.tsx | 4 +- 4 files changed, 178 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dc79f2..c62cd8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,12 @@ "@blueprintjs/icons": "^5.21.0", "@react-three/drei": "^9.121.4", "@react-three/fiber": "^8.17.14", + "fetch-cookie": "^3.2.0", "js-cookie": "^3.0.5", "leaflet": "^1.9.4", "next": "15.1.6", "next-runtime-env": "^3.3.0", + "node-fetch": "^3.3.2", "ping": "^1.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -26,6 +28,7 @@ "recharts": "^2.15.1", "roslib": "^1.4.1", "three": "^0.173.0", + "tough-cookie": "^6.0.0", "uuid": "^13.0.0", "webrtc-adapter": "^9.0.1" }, @@ -2479,6 +2482,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3506,6 +3518,39 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-cookie": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-3.2.0.tgz", + "integrity": "sha512-n61pQIxP25C6DRhcJxn7BDzgHP/+S56Urowb5WFxtcRMpU6drqXD90xjyAsVQYsNSNNVbaCcYY1DuHsdkZLuiA==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^6.0.0" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -3592,6 +3637,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5077,6 +5134,44 @@ "tslib": "^2.0.3" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/normalize.css": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", @@ -6118,6 +6213,12 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6736,6 +6837,24 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tldts": { + "version": "7.0.22", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.22.tgz", + "integrity": "sha512-nqpKFC53CgopKPjT6Wfb6tpIcZXHcI6G37hesvikhx0EmUGPkZrujRyAjgnmp1SHNgpQfKVanZ+KfpANFt2Hxw==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.22" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.22", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.22.tgz", + "integrity": "sha512-KgbTDC5wzlL6j/x6np6wCnDSMUq4kucHNm00KXPbfNzmllCmtmvtykJHfmgdHntwIeupW04y8s1N/43S1PkQDw==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6749,6 +6868,18 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/troika-three-text": { "version": "0.52.3", "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.3.tgz", @@ -7077,6 +7208,15 @@ "loose-envify": "^1.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webgl-constants": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", diff --git a/package.json b/package.json index 0e1fbba..c91f813 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ "@blueprintjs/icons": "^5.21.0", "@react-three/drei": "^9.121.4", "@react-three/fiber": "^8.17.14", + "fetch-cookie": "^3.2.0", "js-cookie": "^3.0.5", "leaflet": "^1.9.4", "next": "15.1.6", "next-runtime-env": "^3.3.0", + "node-fetch": "^3.3.2", "ping": "^1.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -27,6 +29,7 @@ "recharts": "^2.15.1", "roslib": "^1.4.1", "three": "^0.173.0", + "tough-cookie": "^6.0.0", "uuid": "^13.0.0", "webrtc-adapter": "^9.0.1" }, diff --git a/src/app/dashboard/api/route.ts b/src/app/dashboard/api/route.ts index b4c9131..edf3109 100644 --- a/src/app/dashboard/api/route.ts +++ b/src/app/dashboard/api/route.ts @@ -1,8 +1,15 @@ +import fetch from 'node-fetch'; +import fetchCookie from 'fetch-cookie'; +import { CookieJar } from 'tough-cookie'; + +const jar = new CookieJar(); +const fetchWithCookies = fetchCookie(fetch, jar); + 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 +const hosts = ['192.168.0.2', '192.168.0.3', '192.168.0.55']; // Add more hosts here as needed const ping = require('ping'); @@ -34,36 +41,37 @@ async function pingHosts(hosts: string[]): Promise<{ [key: string]: number }> { // Authenticates with the base station async function authenticate() { - const response = await fetch(`http://${baseStationIP}/api/auth`, { + const formData = new URLSearchParams(); + formData.append('uri', '/index.cgi'); + formData.append('username', USERNAME); + formData.append('password', PASSWORD); + + const response = await fetchWithCookies(`http://${baseStationIP}/login.cgi`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - username: USERNAME, - password: PASSWORD, - }), - credentials: 'include', + body: formData, + redirect: 'manual', }); - if (!response.ok) { - throw new Error('Authentication failed'); - } - const setCookie = response.headers.getSetCookie()[0] - return {cookie: setCookie} + if (response.status !== 302) { + throw new Error(`Authentication failed: ${response.status}`); + } } // Fetches status JSON from the base station -async function fetchStatus(cookie: string) { - const response = await fetch(`http://${baseStationIP}/status.cgi`, { +async function fetchStatus() { + const response = await fetchWithCookies(`http://${baseStationIP}/status.cgi`, { method: 'GET', - credentials: 'include', - headers: { - 'Cookie': cookie - } + redirect: 'manual', }); + + + if (response.status === 302) { + await authenticate(); + throw new Error('Not authenticated (redirected to login)'); + } + if (!response.ok) { - throw new Error('Failed to fetch status, error code: ' + response.status); + throw new Error(`Failed to fetch status: ${response.status}`); } return response.json(); } @@ -78,11 +86,9 @@ export async function GET(request: Request) { // 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); - - uplinkCapacity = status.wireless?.polling?.ucap ?? 0; - downlinkCapacity = status.wireless?.polling?.dcap ?? 0; + const status : any = await fetchStatus(); + uplinkCapacity = status.wireless?.txrate ?? 0; + downlinkCapacity = status.wireless?.rxrate ?? 0; uplinkThroughput = status.wireless?.throughput?.tx ?? 0; downlinkThroughput = status.wireless?.throughput?.rx ?? 0; } catch (error: any) { diff --git a/src/components/panels/NetworkHealthTelemetryPanel.tsx b/src/components/panels/NetworkHealthTelemetryPanel.tsx index eb5f52e..07ebbf3 100644 --- a/src/components/panels/NetworkHealthTelemetryPanel.tsx +++ b/src/components/panels/NetworkHealthTelemetryPanel.tsx @@ -95,12 +95,12 @@ const NetworkHealthTelemetryPanel: React.FC = () => { { name: "Uplink", Throughput: Math.round(stats.uplinkThroughput / 10) / 100, - Capacity: stats.uplinkCapacity / 1000, + Capacity: stats.uplinkCapacity, }, { name: "Downlink", Throughput: Math.round(stats.downlinkThroughput / 10) / 100, - Capacity: stats.downlinkCapacity / 1000, + Capacity: stats.downlinkCapacity, }, ]; From f96d47e6fe7817692966b1ae65c393a953d5d1be Mon Sep 17 00:00:00 2001 From: Connor Needham <129120300+ConnorNeed@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:49:51 +0000 Subject: [PATCH 3/4] fix: small video control touch ups for display and functionality --- src/components/SrtStats.tsx | 11 +-- src/components/panels/VideoControls.tsx | 103 ++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/src/components/SrtStats.tsx b/src/components/SrtStats.tsx index a393e06..8c0f1f5 100644 --- a/src/components/SrtStats.tsx +++ b/src/components/SrtStats.tsx @@ -18,13 +18,6 @@ const formatNumber = (ms: number | null | undefined) => { return ms.toLocaleString(); }; -const formatSecondsMs = (sec: number | null | undefined) => { - if (sec === null || sec === undefined) return "—"; - const ms = sec * 1000.0; - if (!Number.isFinite(ms)) return "—"; - return ms >= 10 ? `${ms.toFixed(0)} ms` : `${ms.toFixed(1)} ms`; -}; - const formatBandwidth = (bps: number | null | undefined) => { if (bps === null || bps === undefined) return "—"; if (!Number.isFinite(bps)) return "—"; @@ -55,7 +48,7 @@ const SrtStats: React.FC = () => { const topic = new ROSLIB.Topic({ ros, - name: "/srt_node/stats", + name: "/srt_node/srt_stats", messageType: "interfaces/msg/SrtStats", }); @@ -135,7 +128,7 @@ const SrtStats: React.FC = () => { >
RTT - {formatSecondsMs(stats?.rtt)} + {formatNumber(stats?.rtt)}
diff --git a/src/components/panels/VideoControls.tsx b/src/components/panels/VideoControls.tsx index e56c813..6c1a7fb 100644 --- a/src/components/panels/VideoControls.tsx +++ b/src/components/panels/VideoControls.tsx @@ -52,10 +52,73 @@ const VideoControls: React.FC = () => { ); }; - const onRestart = () => {}; + const onRestart = () => { + if (!ros || rosStatus !== "connected") return; + const topic = new ROSLIB.Topic({ + ros, + name: "/all_video/restart_pipeline", + messageType: "std_msgs/msg/Empty", + }); + topic.publish(new ROSLIB.Message({})); + console.log("Stream restart triggered"); + }; const onSnapshot = () => {}; const onPanoramic = () => {}; + const setLatency = (latency: number) => { + if (!ros || rosStatus !== "connected") return; + + const setParamsClient = new ROSLIB.Service({ + ros, + name: "/srt_node/set_parameters", + serviceType: "rcl_interfaces/srv/SetParameters", + }); + + const request = new ROSLIB.ServiceRequest({ + parameters: [ + { + name: "latency", + value: { + type: 2, + integer_value: latency, + }, + }, + ], + }); + + setParamsClient.callService(request, (result) => { + console.log("Set parameters response:", result); + }); + }; + + const setFramerate = (framerate: number) => { + if (!ros || rosStatus !== "connected") return; + const setParamsClient = new ROSLIB.Service({ + ros, + name: "/srt_node/set_parameters", + serviceType: "rcl_interfaces/srv/SetParameters", + }); + const request = new ROSLIB.ServiceRequest({ + parameters: [ + { + name: "target_framerate", + value: { + type: 2, + integer_value: framerate, + }, + }, + ], + }); + setParamsClient.callService(request, (result) => { + if (result.results && result.results[0].successful) { + console.log(`Framerate successfully set to ${framerate} fps`); + } else { + console.error("Failed to set framerate", result); + } + }); + console.log(`Setting framerate to: ${framerate} fps`); + }; + const connected = !!ros && rosStatus === "connected"; const buttonStyle = (enabled: boolean) => ({ @@ -121,15 +184,45 @@ const VideoControls: React.FC = () => { display: "flex", alignItems: "center", gap: "0.4rem", - marginLeft: "0.5rem", padding: "0.2rem 0.5rem", backgroundColor: "#222", borderRadius: "4px", - border: "1px dashed #555" + border: "1px solid #555" }}> - BITRATE: + Bitrate: + - + + +
+
+ Latency: + + + +
+
+ Framerate: + + + +
From fafd41c02f426b3dcae0e0182b31522891f37241 Mon Sep 17 00:00:00 2001 From: Connor Needham <129120300+ConnorNeed@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:50:01 +0000 Subject: [PATCH 4/4] feat: add antenna control pannel --- src/components/panels/AntennaControlPanel.tsx | 196 ++++++++++++++++++ src/components/panels/MosaicDashboard.tsx | 18 +- 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/components/panels/AntennaControlPanel.tsx diff --git a/src/components/panels/AntennaControlPanel.tsx b/src/components/panels/AntennaControlPanel.tsx new file mode 100644 index 0000000..080247b --- /dev/null +++ b/src/components/panels/AntennaControlPanel.tsx @@ -0,0 +1,196 @@ +'use client'; +import React, { useEffect, useRef, useState } from 'react'; +import ROSLIB from 'roslib'; +import { useROS } from '@/ros/ROSContext'; + +const AntennaControlPanel: React.FC = () => { + const { ros } = useROS(); + + const [disabled, setDisabled] = useState(false); + const [leftHeld, setLeftHeld] = useState(false); + const [rightHeld, setRightHeld] = useState(false); + + const topicRef = useRef(null); + const intervalRef = useRef(null); + + // Create/cleanup topic when ROS connection changes + useEffect(() => { + if (!ros) { + topicRef.current = null; + return; + } + + topicRef.current = new ROSLIB.Topic({ + ros, + name: '/antenna_control', + messageType: 'std_msgs/Float32', + }); + + return () => { + try { + topicRef.current?.unadvertise(); + } catch { + // ignore + } + topicRef.current = null; + }; + }, [ros]); + + // Determine what value should be published right now + const computeValue = () => { + if (disabled) return 0.0; + if (leftHeld && !rightHeld) return -0.5; + if (rightHeld && !leftHeld) return 0.5; + return 0.0; // neither held OR both held + }; + + // Start/stop the 100ms publish loop + useEffect(() => { + if (!ros || !topicRef.current) return; + + // Clear any previous loop + if (intervalRef.current) { + window.clearInterval(intervalRef.current); + intervalRef.current = null; + } + + // Publish immediately, then every 100ms + const publishNow = () => { + const value = computeValue(); + topicRef.current?.publish(new ROSLIB.Message({ data: value })); + }; + + publishNow(); + intervalRef.current = window.setInterval(publishNow, 100); + + return () => { + if (intervalRef.current) { + window.clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ros, disabled, leftHeld, rightHeld]); + + // If user disables controls, clear held state so it goes to 0 cleanly + useEffect(() => { + if (disabled) { + setLeftHeld(false); + setRightHeld(false); + } + }, [disabled]); + + const setHeld = (side: 'left' | 'right', held: boolean) => { + if (disabled) return; + if (side === 'left') setLeftHeld(held); + else setRightHeld(held); + }; + + const btnDisabled = disabled || !ros; + + return ( +
+
+ + + +
+ + + + +
+ ); +}; + +export default AntennaControlPanel; diff --git a/src/components/panels/MosaicDashboard.tsx b/src/components/panels/MosaicDashboard.tsx index 674fd8c..d984e79 100644 --- a/src/components/panels/MosaicDashboard.tsx +++ b/src/components/panels/MosaicDashboard.tsx @@ -19,6 +19,7 @@ import NetworkHealthTelemetryPanel from './NetworkHealthTelemetryPanel'; import VideoControls from './VideoControls'; import MotorStatusPanel from './MotorStatusPanel'; import { v4 as uuidv4 } from 'uuid'; +import AntennaControlPanel from './AntennaControlPanel'; type TileType = | 'mapView' @@ -29,7 +30,8 @@ type TileType = | 'orientationDisplay' | 'goalSetter' | 'networkHealthMonitor' - | 'MotorStatusPanel'; + | 'MotorStatusPanel' + | 'antennaControlPanel'; type TileId = `${TileType}:${string}`; @@ -43,6 +45,7 @@ const TILE_DISPLAY_NAMES: Record = { goalSetter: 'Nav2', networkHealthMonitor: 'Connection Health', MotorStatusPanel: 'motor', + antennaControlPanel: 'Antenna Control', }; const ALL_TILE_TYPES: TileType[] = [ @@ -55,6 +58,7 @@ const ALL_TILE_TYPES: TileType[] = [ 'gasSensor', 'goalSetter', 'MotorStatusPanel', + 'antennaControlPanel', ]; function makeTileId(type: TileType): TileId { @@ -331,6 +335,18 @@ const MosaicDashboard: React.FC = () => { ); + case 'antennaControlPanel': + return ( + + title={TILE_DISPLAY_NAMES[type]} + path={path} + additionalControls={ + + } + > + + + ); default: return
Unknown tile
;