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/public/marker-icon.png b/public/marker-icon.png deleted file mode 100644 index 4fb5f40..0000000 Binary files a/public/marker-icon.png and /dev/null differ 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/BreadCrumbTrail.tsx b/src/components/BreadCrumbTrail.tsx index d4c842b..6345b2f 100644 --- a/src/components/BreadCrumbTrail.tsx +++ b/src/components/BreadCrumbTrail.tsx @@ -12,13 +12,6 @@ interface Breadcrumb { timestamp: number; } -const breadcrumbIcon = L.icon({ - iconUrl: '../../public/marker-icon.png', - iconSize: [10, 10], - iconAnchor: [5, 5], - popupAnchor: [0, -5], -}); - const BreadcrumbTrail: React.FC = () => { 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()} -
-
-
- ))}
{ 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/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/MapView.tsx b/src/components/panels/MapView.tsx index 63e3597..e0cd1b3 100644 --- a/src/components/panels/MapView.tsx +++ b/src/components/panels/MapView.tsx @@ -31,7 +31,7 @@ const MapView: React.FC = ({offline}) => { return ( = { 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
; 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, }, ]; 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: + + + +