mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 01:50:47 -04:00
guh
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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 { 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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user