mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
stateful night vision rework
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -81,6 +81,12 @@ func (n *NightVisionLight) HandleAction(action string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NightVisionLight) NightVisionOn() bool {
|
||||||
|
n.mu.Lock()
|
||||||
|
defer n.mu.Unlock()
|
||||||
|
return !n.on
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NightVisionLight) setLocked(on bool) error {
|
func (n *NightVisionLight) setLocked(on bool) error {
|
||||||
if err := n.line.SetValue(boolToGPIO(on)); err != nil {
|
if err := n.line.SetValue(boolToGPIO(on)); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ func (n *NightVisionLight) Close() {}
|
|||||||
func (n *NightVisionLight) HandleAction(action string) error {
|
func (n *NightVisionLight) HandleAction(action string) error {
|
||||||
return fmt.Errorf("night vision not supported in dummy build")
|
return fmt.Errorf("night vision not supported in dummy build")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NightVisionLight) NightVisionOn() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,7 +184,13 @@ func (c *WSClient) dispatch(ctx context.Context, msg *inboundMessage) error {
|
|||||||
if c.nightVision == nil {
|
if c.nightVision == nil {
|
||||||
return fmt.Errorf("night vision disabled")
|
return fmt.Errorf("night vision disabled")
|
||||||
}
|
}
|
||||||
return c.nightVision.HandleAction(msg.NightVision.Action)
|
if err := c.nightVision.HandleAction(msg.NightVision.Action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.emitEvent("nightVision.state", map[string]any{
|
||||||
|
"nightVisionOn": c.nightVision.NightVisionOn(),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
case msg.Song != nil:
|
case msg.Song != nil:
|
||||||
slot := 0
|
slot := 0
|
||||||
if msg.Song.Slot != nil {
|
if msg.Song.Slot != nil {
|
||||||
|
|||||||
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
@@ -11,8 +11,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-sxx9hCHI.js"></script>
|
<script type="module" crossorigin src="/assets/index-DCabXYAC.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-VF8LslAQ.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-lg9md4T4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ function handleMessage(roverId, msg) {
|
|||||||
roverManager.handleSensorFrame(roverId, msg);
|
roverManager.handleSensorFrame(roverId, msg);
|
||||||
break;
|
break;
|
||||||
case 'event':
|
case 'event':
|
||||||
|
if (msg.event === 'nightVision.state' && typeof msg.data?.nightVisionOn === 'boolean') {
|
||||||
|
roverManager.setNightVisionState(roverId, msg.data.nightVisionOn);
|
||||||
|
break;
|
||||||
|
}
|
||||||
sendAlert({ color: ALERT_COLOR, title: `${roverId} event`, message: msg.event });
|
sendAlert({ color: ALERT_COLOR, title: `${roverId} event`, message: msg.event });
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -69,7 +73,11 @@ roverWSS.on('connection', (ws) => {
|
|||||||
} else if (msg.type === 'ack') {
|
} else if (msg.type === 'ack') {
|
||||||
handleAck(msg);
|
handleAck(msg);
|
||||||
} else if (msg.type === 'event') {
|
} else if (msg.type === 'event') {
|
||||||
sendAlert({ color: ALERT_COLOR, title: `${roverId}`, message: msg.event });
|
if (msg.event === 'nightVision.state' && typeof msg.data?.nightVisionOn === 'boolean') {
|
||||||
|
roverManager.setNightVisionState(roverId, msg.data.nightVisionOn);
|
||||||
|
} else {
|
||||||
|
sendAlert({ color: ALERT_COLOR, title: `${roverId}`, message: msg.event });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ function ensureRecord(id) {
|
|||||||
locked: false,
|
locked: false,
|
||||||
lockReason: null,
|
lockReason: null,
|
||||||
batteryState: null,
|
batteryState: null,
|
||||||
|
nightVisionState: null,
|
||||||
room: `rover:${id}`,
|
room: `rover:${id}`,
|
||||||
lastSeen: Date.now(),
|
lastSeen: Date.now(),
|
||||||
lastMovementAt: Date.now(),
|
lastMovementAt: Date.now(),
|
||||||
@@ -57,6 +58,13 @@ function upsertRover(meta, ws) {
|
|||||||
record.meta = meta;
|
record.meta = meta;
|
||||||
record.ws = ws;
|
record.ws = ws;
|
||||||
record.lastSeen = Date.now();
|
record.lastSeen = Date.now();
|
||||||
|
if (record.nightVisionState == null && meta?.nightVision?.enabled) {
|
||||||
|
const ledOn = Boolean(meta.nightVision.initialOn);
|
||||||
|
record.nightVisionState = {
|
||||||
|
nightVisionOn: !ledOn,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
rovers.set(id, record);
|
rovers.set(id, record);
|
||||||
spectatorSockets.forEach((socketId) => {
|
spectatorSockets.forEach((socketId) => {
|
||||||
const sock = io.sockets.sockets.get(socketId);
|
const sock = io.sockets.sockets.get(socketId);
|
||||||
@@ -143,7 +151,9 @@ function getRoster() {
|
|||||||
media: record.meta?.media,
|
media: record.meta?.media,
|
||||||
cameraServo: record.meta?.cameraServo,
|
cameraServo: record.meta?.cameraServo,
|
||||||
audio: record.meta?.audio,
|
audio: record.meta?.audio,
|
||||||
nightVision: record.meta?.nightVision,
|
nightVision: record.meta?.nightVision
|
||||||
|
? { ...record.meta.nightVision, state: record.nightVisionState }
|
||||||
|
: record.meta?.nightVision,
|
||||||
locked: record.locked,
|
locked: record.locked,
|
||||||
lockReason: record.lockReason,
|
lockReason: record.lockReason,
|
||||||
lastSeen: record.lastSeen,
|
lastSeen: record.lastSeen,
|
||||||
@@ -154,6 +164,18 @@ function broadcastRoster() {
|
|||||||
io.emit('rovers', getRoster());
|
io.emit('rovers', getRoster());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setNightVisionState(roverId, nightVisionOn) {
|
||||||
|
const record = rovers.get(roverId);
|
||||||
|
if (!record) return;
|
||||||
|
if (typeof nightVisionOn !== 'boolean') return;
|
||||||
|
record.nightVisionState = {
|
||||||
|
nightVisionOn,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
broadcastRoster();
|
||||||
|
managerEvents.emit('rover', { roverId, action: 'nightVision', record });
|
||||||
|
}
|
||||||
|
|
||||||
function computeBatteryState(record, sensors) {
|
function computeBatteryState(record, sensors) {
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
if (!sensors) return record.batteryState;
|
if (!sensors) return record.batteryState;
|
||||||
@@ -557,6 +579,7 @@ module.exports = {
|
|||||||
lockRover,
|
lockRover,
|
||||||
getRoster,
|
getRoster,
|
||||||
broadcastRoster,
|
broadcastRoster,
|
||||||
|
setNightVisionState,
|
||||||
handleSensorFrame,
|
handleSensorFrame,
|
||||||
requestControl,
|
requestControl,
|
||||||
releaseControl,
|
releaseControl,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useControlSystem } from '../controls/index.js';
|
import { useControlSystem } from '../controls/index.js';
|
||||||
|
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
||||||
|
import NightVisionControl from './NightVisionControl.jsx';
|
||||||
|
|
||||||
const SLIDER_THROTTLE_MS = 150;
|
const SLIDER_THROTTLE_MS = 150;
|
||||||
|
|
||||||
@@ -10,11 +12,15 @@ function formatDegrees(value) {
|
|||||||
|
|
||||||
export default function CameraServoPanel() {
|
export default function CameraServoPanel() {
|
||||||
const {
|
const {
|
||||||
state: { roverId, camera },
|
state: { roverId, camera, keymap },
|
||||||
actions: { setServoAngle, nudgeServo, goServoHome },
|
pipeline,
|
||||||
|
actions: { setServoAngle, nudgeServo, goServoHome, setNightVision },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const config = camera?.config;
|
const config = camera?.config;
|
||||||
const enabled = Boolean(roverId && camera?.enabled && config);
|
const enabled = Boolean(roverId && camera?.enabled && config);
|
||||||
|
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||||
|
const nightVisionState = pipeline?.nightVisionState;
|
||||||
|
const nightVisionKey = formatKeyLabel(keymap?.nightVisionToggle?.[0]);
|
||||||
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
||||||
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
||||||
const value =
|
const value =
|
||||||
@@ -24,7 +30,7 @@ export default function CameraServoPanel() {
|
|||||||
? config.homeAngle
|
? config.homeAngle
|
||||||
: (min + max) / 2;
|
: (min + max) / 2;
|
||||||
|
|
||||||
if (!enabled) return null;
|
if (!enabled && !nightVisionAvailable) return null;
|
||||||
|
|
||||||
const [pendingAngle, setPendingAngle] = useState(value);
|
const [pendingAngle, setPendingAngle] = useState(value);
|
||||||
const throttleRef = useRef(null);
|
const throttleRef = useRef(null);
|
||||||
@@ -80,30 +86,48 @@ export default function CameraServoPanel() {
|
|||||||
nudgeServo(delta);
|
nudgeServo(delta);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNightVisionToggle = (nextOn) => {
|
||||||
|
if (!nightVisionAvailable) return;
|
||||||
|
setNightVision(nextOn);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel-section space-y-0.5 text-base">
|
<section className="panel-section space-y-0.5 text-base">
|
||||||
<div className="flex items-center justify-between text-sm text-slate-300">
|
{enabled && (
|
||||||
<span>Camera Tilt</span>
|
<>
|
||||||
<span className="font-mono text-sm text-slate-100">{formatDegrees(value)}</span>
|
<div className="flex items-center justify-between text-sm text-slate-300">
|
||||||
</div>
|
<span>Camera Tilt</span>
|
||||||
<div>
|
<span className="font-mono text-sm text-slate-100">{formatDegrees(value)}</span>
|
||||||
<input
|
</div>
|
||||||
type="range"
|
<div>
|
||||||
className="w-full accent-emerald-400"
|
<input
|
||||||
min={min}
|
type="range"
|
||||||
max={max}
|
className="w-full accent-emerald-400"
|
||||||
step={0.5}
|
min={min}
|
||||||
value={pendingAngle}
|
max={max}
|
||||||
onChange={handleSlider}
|
step={0.5}
|
||||||
onMouseUp={commitSlider}
|
value={pendingAngle}
|
||||||
onTouchEnd={commitSlider}
|
onChange={handleSlider}
|
||||||
onPointerUp={commitSlider}
|
onMouseUp={commitSlider}
|
||||||
|
onTouchEnd={commitSlider}
|
||||||
|
onPointerUp={commitSlider}
|
||||||
|
/>
|
||||||
|
<div className="mt-0 flex justify-between text-xs text-slate-400">
|
||||||
|
<span>{formatDegrees(min)}</span>
|
||||||
|
<span>{formatDegrees(max)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{nightVisionAvailable && (
|
||||||
|
<NightVisionControl
|
||||||
|
nightVisionOn={nightVisionState?.nightVisionOn}
|
||||||
|
disabled={!roverId}
|
||||||
|
onToggle={handleNightVisionToggle}
|
||||||
|
keyLabel={nightVisionKey}
|
||||||
|
className={enabled ? 'mt-1' : ''}
|
||||||
/>
|
/>
|
||||||
<div className="mt-0 flex justify-between text-xs text-slate-400">
|
)}
|
||||||
<span>{formatDegrees(min)}</span>
|
|
||||||
<span>{formatDegrees(max)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <div className="flex gap-0.5 text-sm">
|
{/* <div className="flex gap-0.5 text-sm">
|
||||||
<button type="button" className="flex-1 button-dark" onClick={() => handleNudge(-1)}>
|
<button type="button" className="flex-1 button-dark" onClick={() => handleNudge(-1)}>
|
||||||
Tilt Down
|
Tilt Down
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
import { useControlSystem } from '../controls/index.js';
|
import { useControlSystem } from '../controls/index.js';
|
||||||
import { clampUnit } from '../controls/controlMath.js';
|
import { clampUnit } from '../controls/controlMath.js';
|
||||||
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
|
import DriveDockAction, { useDriveDockState } from './DriveDockAction.jsx';
|
||||||
|
import NightVisionControl from './NightVisionControl.jsx';
|
||||||
|
|
||||||
const SOURCE = 'mobile-joystick';
|
const SOURCE = 'mobile-joystick';
|
||||||
const JOYSTICK_RADIUS = 80;
|
const JOYSTICK_RADIUS = 80;
|
||||||
@@ -134,7 +135,7 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
const {
|
const {
|
||||||
state: { roverId, camera },
|
state: { roverId, camera },
|
||||||
pipeline,
|
pipeline,
|
||||||
actions: { setDriveVector, registerInputState, setServoAngle, toggleNightVision },
|
actions: { setDriveVector, registerInputState, setServoAngle, setNightVision },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const driveDockState = useDriveDockState(roverId);
|
const driveDockState = useDriveDockState(roverId);
|
||||||
const dockedNotDriving = driveDockState.docked && !driveDockState.driving;
|
const dockedNotDriving = driveDockState.docked && !driveDockState.driving;
|
||||||
@@ -142,6 +143,7 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
const cameraConfig = camera?.config;
|
const cameraConfig = camera?.config;
|
||||||
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
||||||
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||||
|
const nightVisionState = pipeline?.nightVisionState;
|
||||||
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
|
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
|
||||||
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
||||||
const cameraValue =
|
const cameraValue =
|
||||||
@@ -198,6 +200,14 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
setServoAngle(next);
|
setServoAngle(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNightVisionToggle = useCallback(
|
||||||
|
(nextOn) => {
|
||||||
|
if (!nightVisionAvailable) return;
|
||||||
|
setNightVision(nextOn);
|
||||||
|
},
|
||||||
|
[nightVisionAvailable, setNightVision],
|
||||||
|
);
|
||||||
|
|
||||||
const fillClass = dockedNotDriving ? 'max-h-screen self-start' : '';
|
const fillClass = dockedNotDriving ? 'max-h-screen self-start' : '';
|
||||||
const containerClass = `flex h-full flex-col gap-0.5 text-slate-100 ${fillClass}`;
|
const containerClass = `flex h-full flex-col gap-0.5 text-slate-100 ${fillClass}`;
|
||||||
|
|
||||||
@@ -206,16 +216,6 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
<DriveDockAction layout="mobile" expand={dockedNotDriving} driveDockState={driveDockState} />
|
<DriveDockAction layout="mobile" expand={dockedNotDriving} driveDockState={driveDockState} />
|
||||||
{!dockedNotDriving ? (
|
{!dockedNotDriving ? (
|
||||||
<>
|
<>
|
||||||
{nightVisionAvailable && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleNightVision()}
|
|
||||||
disabled={disabled}
|
|
||||||
className="bg-amber-600 px-0.5 py-1 text-sm font-semibold text-amber-50 transition hover:bg-amber-500 disabled:opacity-40"
|
|
||||||
>
|
|
||||||
Toggle Night Vision
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{cameraEnabled && (
|
{cameraEnabled && (
|
||||||
<div className="bg-zinc-950 p-0.5 text-xs">
|
<div className="bg-zinc-950 p-0.5 text-xs">
|
||||||
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
|
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
|
||||||
@@ -233,6 +233,14 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{nightVisionAvailable && (
|
||||||
|
<NightVisionControl
|
||||||
|
nightVisionOn={nightVisionState?.nightVisionOn}
|
||||||
|
disabled={disabled}
|
||||||
|
onToggle={handleNightVisionToggle}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<FloatingJoystick
|
<FloatingJoystick
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
layout={layout}
|
layout={layout}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
function isBoolean(value) {
|
||||||
|
return typeof value === 'boolean';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NightVisionControl({
|
||||||
|
nightVisionOn,
|
||||||
|
disabled,
|
||||||
|
onToggle,
|
||||||
|
keyLabel,
|
||||||
|
className = '',
|
||||||
|
}) {
|
||||||
|
const [optimistic, setOptimistic] = useState(
|
||||||
|
isBoolean(nightVisionOn) ? nightVisionOn : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isBoolean(nightVisionOn)) {
|
||||||
|
setOptimistic(nightVisionOn);
|
||||||
|
}
|
||||||
|
}, [nightVisionOn]);
|
||||||
|
|
||||||
|
const hasState = isBoolean(optimistic);
|
||||||
|
const displayOn = hasState ? optimistic : false;
|
||||||
|
const statusLabel = hasState ? (displayOn ? 'On' : 'Off') : '—';
|
||||||
|
const statusClasses = displayOn
|
||||||
|
? 'bg-emerald-600 text-emerald-50'
|
||||||
|
: 'bg-slate-700 text-slate-200';
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
const next = hasState ? !displayOn : true;
|
||||||
|
setOptimistic(next);
|
||||||
|
onToggle?.(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonClasses = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
'flex w-full items-center justify-between rounded bg-zinc-950 px-1 py-0.5 text-xs text-slate-300',
|
||||||
|
'transition hover:bg-zinc-900 disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' '),
|
||||||
|
[className],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleToggle}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-pressed={displayOn}
|
||||||
|
className={buttonClasses}
|
||||||
|
>
|
||||||
|
<span className="text-slate-400">Night Vision</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className={`rounded px-1 py-0.5 text-[0.65rem] font-semibold ${statusClasses}`}>
|
||||||
|
{statusLabel}
|
||||||
|
</span>
|
||||||
|
{keyLabel ? (
|
||||||
|
<span className="rounded bg-slate-800 px-1 py-0.5 text-[0.6rem] font-semibold text-slate-200">
|
||||||
|
{keyLabel}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -312,10 +312,23 @@ export function ControlSystemProvider({ children }) {
|
|||||||
[pipeline],
|
[pipeline],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setNightVision = useCallback(
|
||||||
|
(nightVisionOn) => {
|
||||||
|
if (!pipeline.nightVision) return;
|
||||||
|
if (typeof nightVisionOn === 'boolean') {
|
||||||
|
const action = nightVisionOn ? 'off' : 'on';
|
||||||
|
pipeline.sendNightVision(action);
|
||||||
|
} else {
|
||||||
|
pipeline.sendNightVision('toggle');
|
||||||
|
}
|
||||||
|
recordControlIntent();
|
||||||
|
},
|
||||||
|
[pipeline, recordControlIntent],
|
||||||
|
);
|
||||||
|
|
||||||
const toggleNightVision = useCallback(() => {
|
const toggleNightVision = useCallback(() => {
|
||||||
pipeline.sendNightVision('toggle');
|
setNightVision();
|
||||||
recordControlIntent();
|
}, [setNightVision]);
|
||||||
}, [pipeline, recordControlIntent]);
|
|
||||||
|
|
||||||
const setSongNote = useCallback(
|
const setSongNote = useCallback(
|
||||||
(note) => {
|
(note) => {
|
||||||
@@ -354,6 +367,7 @@ export function ControlSystemProvider({ children }) {
|
|||||||
stopAllMotion,
|
stopAllMotion,
|
||||||
sendOiCommand,
|
sendOiCommand,
|
||||||
setSensorStream,
|
setSensorStream,
|
||||||
|
setNightVision,
|
||||||
toggleNightVision,
|
toggleNightVision,
|
||||||
updateKeyBinding,
|
updateKeyBinding,
|
||||||
resetKeyBindings,
|
resetKeyBindings,
|
||||||
@@ -376,6 +390,7 @@ export function ControlSystemProvider({ children }) {
|
|||||||
stopAllMotion,
|
stopAllMotion,
|
||||||
sendOiCommand,
|
sendOiCommand,
|
||||||
setSensorStream,
|
setSensorStream,
|
||||||
|
setNightVision,
|
||||||
toggleNightVision,
|
toggleNightVision,
|
||||||
updateKeyBinding,
|
updateKeyBinding,
|
||||||
resetKeyBindings,
|
resetKeyBindings,
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export function useCommandPipeline(options = {}) {
|
|||||||
return rosterEntry.nightVision;
|
return rosterEntry.nightVision;
|
||||||
}, [rosterEntry]);
|
}, [rosterEntry]);
|
||||||
|
|
||||||
|
const nightVisionState = useMemo(() => rosterEntry?.nightVision?.state ?? null, [rosterEntry]);
|
||||||
|
|
||||||
const emitCommand = useCallback(
|
const emitCommand = useCallback(
|
||||||
(payload, cb) => {
|
(payload, cb) => {
|
||||||
if (!roverId) return;
|
if (!roverId) return;
|
||||||
@@ -202,6 +204,7 @@ export function useCommandPipeline(options = {}) {
|
|||||||
rosterEntry,
|
rosterEntry,
|
||||||
servoConfig,
|
servoConfig,
|
||||||
nightVision,
|
nightVision,
|
||||||
|
nightVisionState,
|
||||||
emitCommand,
|
emitCommand,
|
||||||
enableSensorStream,
|
enableSensorStream,
|
||||||
sendDriveDirect,
|
sendDriveDirect,
|
||||||
@@ -217,6 +220,7 @@ export function useCommandPipeline(options = {}) {
|
|||||||
rosterEntry,
|
rosterEntry,
|
||||||
servoConfig,
|
servoConfig,
|
||||||
nightVision,
|
nightVision,
|
||||||
|
nightVisionState,
|
||||||
emitCommand,
|
emitCommand,
|
||||||
enableSensorStream,
|
enableSensorStream,
|
||||||
sendDriveDirect,
|
sendDriveDirect,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getPadSignature,
|
getPadSignature,
|
||||||
} from './gamepadBindings.js';
|
} from './gamepadBindings.js';
|
||||||
import { subscribeGamepadHub } from './gamepadHub.js';
|
import { subscribeGamepadHub } from './gamepadHub.js';
|
||||||
|
import { isTextEntryActive } from './inputFocusUtils.js';
|
||||||
|
|
||||||
const SOURCE = 'gamepad';
|
const SOURCE = 'gamepad';
|
||||||
const ZERO_VECTOR = { x: 0, y: 0, boost: false };
|
const ZERO_VECTOR = { x: 0, y: 0, boost: false };
|
||||||
@@ -154,6 +155,21 @@ export default function GamepadInputManager() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isTextEntryActive()) {
|
||||||
|
if (!areVectorsEqual(lastVectorRef.current, ZERO_VECTOR)) {
|
||||||
|
lastVectorRef.current = ZERO_VECTOR;
|
||||||
|
setDriveVector(ZERO_VECTOR, { source: SOURCE });
|
||||||
|
}
|
||||||
|
if (!areAuxEqual(lastAuxRef.current, ZERO_AUX)) {
|
||||||
|
lastAuxRef.current = ZERO_AUX;
|
||||||
|
setAuxMotors(ZERO_AUX);
|
||||||
|
}
|
||||||
|
buttonStateRef.current = new Map();
|
||||||
|
reverseStateRef.current = { main: false, side: false };
|
||||||
|
registerInputState(SOURCE, { connected: true, blocked: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ensureProfile(activePad);
|
ensureProfile(activePad);
|
||||||
const signature = activePad.signature;
|
const signature = activePad.signature;
|
||||||
const profile =
|
const profile =
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useControlSystem } from '../ControlContext.jsx';
|
|||||||
import { useChat } from '../../context/ChatContext.jsx';
|
import { useChat } from '../../context/ChatContext.jsx';
|
||||||
import { useSession } from '../../context/SessionContext.jsx';
|
import { useSession } from '../../context/SessionContext.jsx';
|
||||||
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
|
import { normalizeKeymapEntries, tokensForEvent } from '../keymapUtils.js';
|
||||||
|
import { isTextInputElement } from './inputFocusUtils.js';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
|
||||||
import {
|
import {
|
||||||
@@ -55,15 +56,7 @@ function mapTiltIntervalToSpeed(interval) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldIgnoreEvent(event) {
|
function shouldIgnoreEvent(event) {
|
||||||
const target = event.target;
|
return isTextInputElement(event?.target);
|
||||||
if (!target) return false;
|
|
||||||
const tag = target.tagName;
|
|
||||||
return (
|
|
||||||
tag === 'INPUT' ||
|
|
||||||
tag === 'TEXTAREA' ||
|
|
||||||
target.isContentEditable ||
|
|
||||||
tag === 'SELECT'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindingActive(bindingSet, keys) {
|
function bindingActive(bindingSet, keys) {
|
||||||
@@ -395,12 +388,12 @@ export default function KeyboardInputManager() {
|
|||||||
resetAll();
|
resetAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||||
window.addEventListener('keyup', handleKeyUp);
|
window.addEventListener('keyup', handleKeyUp, { capture: true });
|
||||||
window.addEventListener('blur', handleBlur);
|
window.addEventListener('blur', handleBlur);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||||
window.removeEventListener('keyup', handleKeyUp);
|
window.removeEventListener('keyup', handleKeyUp, { capture: true });
|
||||||
window.removeEventListener('blur', handleBlur);
|
window.removeEventListener('blur', handleBlur);
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const TEXT_INPUT_TYPES = new Set([
|
||||||
|
'',
|
||||||
|
'text',
|
||||||
|
'search',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'url',
|
||||||
|
'tel',
|
||||||
|
'number',
|
||||||
|
'date',
|
||||||
|
'datetime-local',
|
||||||
|
'month',
|
||||||
|
'time',
|
||||||
|
'week',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isTextInputElement(target) {
|
||||||
|
if (!target || target.nodeType !== 1) return false;
|
||||||
|
const tag = target.tagName;
|
||||||
|
if (tag === 'TEXTAREA') return true;
|
||||||
|
if (target.isContentEditable) return true;
|
||||||
|
if (tag !== 'INPUT') return false;
|
||||||
|
const type = target.type ? target.type.toLowerCase() : '';
|
||||||
|
return TEXT_INPUT_TYPES.has(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTextEntryActive() {
|
||||||
|
if (typeof document === 'undefined') return false;
|
||||||
|
return isTextInputElement(document.activeElement);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user