mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
guh
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
package roverd
|
||||
|
||||
type helloMessage struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Battery BatteryConfig `json:"battery"`
|
||||
MaxWheelSpeed int `json:"maxWheelSpeed"`
|
||||
Media MediaConfig `json:"media"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Battery BatteryConfig `json:"battery"`
|
||||
MaxWheelSpeed int `json:"maxWheelSpeed"`
|
||||
Media MediaConfig `json:"media"`
|
||||
CameraServo CameraServoConfig `json:"cameraServo,omitempty"`
|
||||
}
|
||||
|
||||
type sensorMessage struct {
|
||||
|
||||
@@ -70,6 +70,7 @@ func (c *WSClient) sendHello(ctx context.Context, conn *websocket.Conn) error {
|
||||
Battery: c.cfg.Battery,
|
||||
MaxWheelSpeed: c.cfg.MaxWheelMMs,
|
||||
Media: c.cfg.Media,
|
||||
CameraServo: c.cfg.CameraServo,
|
||||
}
|
||||
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
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>webui</title>
|
||||
<script type="module" crossorigin src="/assets/index-BvgUoEqO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DwQ38q3M.css">
|
||||
<script type="module" crossorigin src="/assets/index-OLyWk2-K.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dk59njim.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import TelemetryPanel from './TelemetryPanel.jsx';
|
||||
import DrivePanel from './DrivePanel.jsx';
|
||||
import CameraServoPanel from './CameraServoPanel.jsx';
|
||||
import RoomCameraPanel from './RoomCameraPanel.jsx';
|
||||
import AdvancedSettings from './AdvancedSettings.jsx';
|
||||
import HelpPanel from './HelpPanel.jsx';
|
||||
@@ -51,6 +52,7 @@ export default function RightPaneTabs({ layout }) {
|
||||
{active === 'telemetry' && (
|
||||
<div className="space-y-1">
|
||||
<DrivePanel />
|
||||
<CameraServoPanel />
|
||||
<TelemetryPanel />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* global Buffer */
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
|
||||
@@ -96,6 +96,7 @@ export function useDriveControls() {
|
||||
const keysRef = useRef(new Set());
|
||||
const lastSpeedsRef = useRef({ left: 0, right: 0 });
|
||||
const [currentSpeeds, setCurrentSpeeds] = useState(() => ({ left: 0, right: 0 }));
|
||||
const [servoAngle, setServoAngle] = useState(null);
|
||||
|
||||
const emitCommand = useCallback(
|
||||
(payload, cb) => {
|
||||
@@ -105,6 +106,18 @@ export function useDriveControls() {
|
||||
[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(() => {
|
||||
if (!roverId) return;
|
||||
emitCommand({
|
||||
@@ -205,6 +218,60 @@ export function useDriveControls() {
|
||||
[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(() => {
|
||||
if (!roverId) {
|
||||
keysRef.current.clear();
|
||||
@@ -249,5 +316,19 @@ export function useDriveControls() {
|
||||
seekDock,
|
||||
setAuxMotors,
|
||||
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);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user