This commit is contained in:
legop3
2025-11-17 18:39:21 -05:00
parent 9b552fb857
commit 2a942ff55c
10 changed files with 193 additions and 20 deletions
+6 -5
View File
@@ -1,11 +1,12 @@
package roverd package roverd
type helloMessage struct { type helloMessage struct {
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Battery BatteryConfig `json:"battery"` Battery BatteryConfig `json:"battery"`
MaxWheelSpeed int `json:"maxWheelSpeed"` MaxWheelSpeed int `json:"maxWheelSpeed"`
Media MediaConfig `json:"media"` Media MediaConfig `json:"media"`
CameraServo CameraServoConfig `json:"cameraServo,omitempty"`
} }
type sensorMessage struct { type sensorMessage struct {
+1
View File
@@ -70,6 +70,7 @@ func (c *WSClient) sendHello(ctx context.Context, conn *websocket.Conn) error {
Battery: c.cfg.Battery, Battery: c.cfg.Battery,
MaxWheelSpeed: c.cfg.MaxWheelMMs, MaxWheelSpeed: c.cfg.MaxWheelMMs,
Media: c.cfg.Media, Media: c.cfg.Media,
CameraServo: c.cfg.CameraServo,
} }
return writeJSON(ctx, conn, msg) return writeJSON(ctx, conn, msg)
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title> <title>webui</title>
<script type="module" crossorigin src="/assets/index-BvgUoEqO.js"></script> <script type="module" crossorigin src="/assets/index-OLyWk2-K.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DwQ38q3M.css"> <link rel="stylesheet" crossorigin href="/assets/index-Dk59njim.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+88
View File
@@ -0,0 +1,88 @@
import { useDriveControl } from '../context/DriveControlContext.jsx';
function formatDegrees(value) {
if (typeof value !== 'number' || Number.isNaN(value)) return '—';
return `${value > 0 ? '+' : ''}${value.toFixed(1)}°`;
}
export default function CameraServoPanel() {
const { roverId, servo } = useDriveControl();
const config = servo?.config;
const enabled = Boolean(roverId && servo?.enabled && config);
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
const value =
typeof servo?.angle === 'number'
? servo.angle
: typeof config?.homeAngle === 'number'
? config.homeAngle
: (min + max) / 2;
if (!enabled) return null;
const step =
typeof config?.nudgeDegrees === 'number' && config.nudgeDegrees > 0
? config.nudgeDegrees
: 1;
const handleSlider = (event) => {
const next = Number.parseFloat(event.target.value);
if (Number.isNaN(next)) return;
servo.setAngle(next);
};
const handleNudge = (direction) => {
const delta = step * direction;
servo.nudge(delta);
};
return (
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
<div className="flex items-center justify-between text-sm text-slate-300">
<span>Camera Tilt</span>
<span className="font-mono text-sm text-slate-100">{formatDegrees(value)}</span>
</div>
<div className="mt-2">
<input
type="range"
className="w-full accent-emerald-400"
min={min}
max={max}
step={0.5}
value={value}
onChange={handleSlider}
/>
<div className="mt-1 flex justify-between text-xs text-slate-400">
<span>{formatDegrees(min)}</span>
<span>{formatDegrees(max)}</span>
</div>
</div>
<div className="mt-2 flex gap-1 text-sm">
<button
type="button"
className="flex-1 rounded bg-slate-800 px-1 py-0.5 text-slate-100 hover:bg-slate-700"
onClick={() => handleNudge(-1)}
>
Tilt Down
</button>
<button
type="button"
className="flex-1 rounded bg-slate-700 px-1 py-0.5 text-slate-100 hover:bg-slate-600"
onClick={() => servo.goHome()}
>
Center
</button>
<button
type="button"
className="flex-1 rounded bg-slate-800 px-1 py-0.5 text-slate-100 hover:bg-slate-700"
onClick={() => handleNudge(1)}
>
Tilt Up
</button>
</div>
<p className="mt-1 text-xs text-slate-400">
Step: {formatDegrees(step)} · Pin GPIO {config?.pin}
</p>
</section>
);
}
+2
View File
@@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import TelemetryPanel from './TelemetryPanel.jsx'; import TelemetryPanel from './TelemetryPanel.jsx';
import DrivePanel from './DrivePanel.jsx'; import DrivePanel from './DrivePanel.jsx';
import CameraServoPanel from './CameraServoPanel.jsx';
import RoomCameraPanel from './RoomCameraPanel.jsx'; import RoomCameraPanel from './RoomCameraPanel.jsx';
import AdvancedSettings from './AdvancedSettings.jsx'; import AdvancedSettings from './AdvancedSettings.jsx';
import HelpPanel from './HelpPanel.jsx'; import HelpPanel from './HelpPanel.jsx';
@@ -51,6 +52,7 @@ export default function RightPaneTabs({ layout }) {
{active === 'telemetry' && ( {active === 'telemetry' && (
<div className="space-y-1"> <div className="space-y-1">
<DrivePanel /> <DrivePanel />
<CameraServoPanel />
<TelemetryPanel /> <TelemetryPanel />
</div> </div>
)} )}
+82 -1
View File
@@ -1,6 +1,6 @@
/* global Buffer */ /* global Buffer */
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSocket } from '../context/SocketContext.jsx'; import { useSocket } from '../context/SocketContext.jsx';
import { useSession } from '../context/SessionContext.jsx'; import { useSession } from '../context/SessionContext.jsx';
@@ -96,6 +96,7 @@ export function useDriveControls() {
const keysRef = useRef(new Set()); const keysRef = useRef(new Set());
const lastSpeedsRef = useRef({ left: 0, right: 0 }); const lastSpeedsRef = useRef({ left: 0, right: 0 });
const [currentSpeeds, setCurrentSpeeds] = useState(() => ({ left: 0, right: 0 })); const [currentSpeeds, setCurrentSpeeds] = useState(() => ({ left: 0, right: 0 }));
const [servoAngle, setServoAngle] = useState(null);
const emitCommand = useCallback( const emitCommand = useCallback(
(payload, cb) => { (payload, cb) => {
@@ -105,6 +106,18 @@ export function useDriveControls() {
[socket, roverId], [socket, roverId],
); );
const currentRosterEntry = useMemo(() => {
if (!roverId || !Array.isArray(session?.roster)) return null;
return session.roster.find((entry) => String(entry.id) === String(roverId)) || null;
}, [roverId, session?.roster]);
const servoConfig = useMemo(() => {
if (!currentRosterEntry?.cameraServo || !currentRosterEntry.cameraServo.enabled) {
return null;
}
return currentRosterEntry.cameraServo;
}, [currentRosterEntry]);
const enableSensorStream = useCallback(() => { const enableSensorStream = useCallback(() => {
if (!roverId) return; if (!roverId) return;
emitCommand({ emitCommand({
@@ -205,6 +218,60 @@ export function useDriveControls() {
[roverId, sendMotorPwm], [roverId, sendMotorPwm],
); );
const sendServoCommand = useCallback(
(payload) => {
if (!roverId || !servoConfig) return;
emitCommand({
type: 'servo',
data: { servo: payload },
});
},
[emitCommand, roverId, servoConfig],
);
const applyServoAngle = useCallback(
(angle) => {
if (!servoConfig) return;
const min = typeof servoConfig.minAngle === 'number' ? servoConfig.minAngle : -45;
const max = typeof servoConfig.maxAngle === 'number' ? servoConfig.maxAngle : 45;
const clamped = clamp(angle, min, max);
setServoAngle(clamped);
sendServoCommand({ angle: clamped });
},
[sendServoCommand, servoConfig],
);
const nudgeServo = useCallback(
(delta) => {
if (!servoConfig) return;
const min = typeof servoConfig.minAngle === 'number' ? servoConfig.minAngle : -45;
const max = typeof servoConfig.maxAngle === 'number' ? servoConfig.maxAngle : 45;
const midpoint = (min + max) / 2;
const step =
typeof delta === 'number' && !Number.isNaN(delta) && delta !== 0
? delta
: servoConfig.nudgeDegrees || 1;
const baseline =
typeof servoAngle === 'number'
? servoAngle
: servoConfig.homeAngle ?? midpoint;
applyServoAngle(baseline + step);
},
[applyServoAngle, servoAngle, servoConfig],
);
useEffect(() => {
if (!servoConfig) {
setServoAngle(null);
return;
}
const min = typeof servoConfig.minAngle === 'number' ? servoConfig.minAngle : -45;
const max = typeof servoConfig.maxAngle === 'number' ? servoConfig.maxAngle : 45;
const initial =
typeof servoConfig.homeAngle === 'number' ? servoConfig.homeAngle : (min + max) / 2;
setServoAngle(clamp(initial, min, max));
}, [servoConfig, roverId]);
useEffect(() => { useEffect(() => {
if (!roverId) { if (!roverId) {
keysRef.current.clear(); keysRef.current.clear();
@@ -249,5 +316,19 @@ export function useDriveControls() {
seekDock, seekDock,
setAuxMotors, setAuxMotors,
driveWithVector, driveWithVector,
servo: {
enabled: Boolean(roverId && servoConfig),
config: servoConfig,
angle:
typeof servoAngle === 'number'
? servoAngle
: servoConfig?.homeAngle ?? servoConfig?.minAngle ?? 0,
setAngle: applyServoAngle,
nudge: nudgeServo,
goHome: () => {
if (!servoConfig) return;
applyServoAngle(servoConfig.homeAngle ?? 0);
},
},
}; };
} }