mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
wooo!!!!
This commit is contained in:
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-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-BY461TBg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DzG-Bp9_.css">
|
||||
<script type="module" crossorigin src="/assets/index-Bg3-HsWj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B89HtNlO.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+11
-4
@@ -6,7 +6,12 @@ import MobileControls, {
|
||||
MobileLeftColumn,
|
||||
MobileRightColumn,
|
||||
} from './components/MobileControls.jsx';
|
||||
import { ControlSystemProvider, KeyboardInputManager, GamepadInputManager } from './controls/index.js';
|
||||
import {
|
||||
ControlSystemProvider,
|
||||
KeyboardInputManager,
|
||||
GamepadInputManager,
|
||||
useControlSystem,
|
||||
} from './controls/index.js';
|
||||
import RoomCameraPanel from './components/RoomCameraPanel.jsx';
|
||||
import LogPanel from './components/LogPanel.jsx';
|
||||
import DriverVideoPanel from './components/DriverVideoPanel.jsx';
|
||||
@@ -84,12 +89,14 @@ function MobileFeatureTabs({
|
||||
showTelemetry = true,
|
||||
}) {
|
||||
const { session } = useSession();
|
||||
const { state: controlState } = useControlSystem();
|
||||
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false });
|
||||
const vipDotClass = session?.isVerified ? 'bg-emerald-400' : 'bg-amber-400';
|
||||
const ownRoverId = String(session?.assignment?.roverId || '').trim();
|
||||
const ownAudioForward = ownRoverId ? session?.audioForward?.[ownRoverId] : null;
|
||||
const pttActive = Boolean(controlState?.mic?.pttActive);
|
||||
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
|
||||
const vipMicActive = Boolean(
|
||||
ownAudioForward?.source === 'mic-whip' &&
|
||||
(ownAudioForward?.state === 'starting' || ownAudioForward?.state === 'playing'),
|
||||
ownRoverId && session?.isVerified && (openMicEnabled || pttActive),
|
||||
);
|
||||
return (
|
||||
<section className="panel text-base">
|
||||
|
||||
@@ -18,6 +18,7 @@ import HornControl from './HornControl.jsx';
|
||||
import CameraTiltControl from './CameraTiltControl.jsx';
|
||||
import VipPanel from './VipPanel.jsx';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
|
||||
function TopDownMapPanel() {
|
||||
const {
|
||||
@@ -111,12 +112,14 @@ function DriveDockPanel() {
|
||||
|
||||
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
const { session } = useSession();
|
||||
const { state: controlState } = useControlSystem();
|
||||
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false });
|
||||
const vipDotClass = session?.isVerified ? 'bg-emerald-400' : 'bg-red-600';
|
||||
const ownRoverId = String(session?.assignment?.roverId || '').trim();
|
||||
const ownAudioForward = ownRoverId ? session?.audioForward?.[ownRoverId] : null;
|
||||
const pttActive = Boolean(controlState?.mic?.pttActive);
|
||||
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
|
||||
const vipMicActive = Boolean(
|
||||
ownAudioForward?.source === 'mic-whip' &&
|
||||
(ownAudioForward?.state === 'starting' || ownAudioForward?.state === 'playing'),
|
||||
ownRoverId && session?.isVerified && (openMicEnabled || pttActive),
|
||||
);
|
||||
return (
|
||||
<section className="panel text-base">
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function VipPanel() {
|
||||
<section className="surface w-full">
|
||||
<div className="mx-auto flex w-full max-w-md flex-col items-center space-y-0.5 text-center">
|
||||
<p className="text-sm text-emerald-300">Verification complete</p>
|
||||
<p className="text-xs text-slate-400">VIP audio controls are unlocked for your assigned rover.</p>
|
||||
<p className="text-xs text-slate-400">VIP controls are unlocked for your assigned rover.</p>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
@@ -64,34 +64,25 @@ export default function VipPanel() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isVerified ? (
|
||||
<VipAudioUploadCard
|
||||
ownRoverId={ownRoverId}
|
||||
audioForwardByRover={session?.audioForward || {}}
|
||||
playUploadedAudio={playUploadedAudio}
|
||||
stopUploadedAudio={stopUploadedAudio}
|
||||
startMicWhip={startMicWhip}
|
||||
readyMicWhip={readyMicWhip}
|
||||
stopMicWhip={stopMicWhip}
|
||||
/>
|
||||
) : (
|
||||
<section className="surface h-full">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center text-xs text-slate-400">
|
||||
Verify your account to unlock audio upload and microphone forwarding.
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="surface h-full">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-slate-200">VIP Notes</p>
|
||||
<ul className="space-y-0.25 text-xs text-slate-400">
|
||||
<li>Audio forwarding is locked to your currently assigned rover.</li>
|
||||
<li>Losing control or turn ownership stops active forwarding.</li>
|
||||
<li>PTT works page-wide using your keybind (default: m).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<div className="lg:col-span-2">
|
||||
{isVerified ? (
|
||||
<VipAudioUploadCard
|
||||
ownRoverId={ownRoverId}
|
||||
audioForwardByRover={session?.audioForward || {}}
|
||||
playUploadedAudio={playUploadedAudio}
|
||||
stopUploadedAudio={stopUploadedAudio}
|
||||
startMicWhip={startMicWhip}
|
||||
readyMicWhip={readyMicWhip}
|
||||
stopMicWhip={stopMicWhip}
|
||||
/>
|
||||
) : (
|
||||
<section className="surface h-full">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center text-xs text-slate-400">
|
||||
Verify your account to unlock VIP controls.
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<VipIdentityCard
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { fieldClass } from './constants.js';
|
||||
import { useControlSystem } from '../../controls/index.js';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
|
||||
const MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
|
||||
const TARGET_SAMPLE_RATE = 16000;
|
||||
@@ -154,14 +155,17 @@ function waitForOutboundAudioFlow(pc, timeoutMs = 6000) {
|
||||
});
|
||||
}
|
||||
|
||||
function StatusLight({ label, active, detail = '' }) {
|
||||
function StatusIndicator({ label, active, detail = '' }) {
|
||||
return (
|
||||
<div className="surface-muted flex items-center justify-between px-0.5 py-0.25 text-xs">
|
||||
<span className="text-slate-300">{label}</span>
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<span className={`h-2 w-2 rounded-full ${active ? 'bg-emerald-400' : 'bg-slate-600'}`} aria-hidden="true" />
|
||||
<span className={active ? 'text-emerald-300' : 'text-slate-500'}>{detail || (active ? 'on' : 'off')}</span>
|
||||
</span>
|
||||
<div
|
||||
className={`rounded-md border px-0.5 py-0.5 text-xs transition-colors ${
|
||||
active
|
||||
? 'border-emerald-300/80 bg-emerald-500/20 text-emerald-200'
|
||||
: 'border-slate-700 bg-slate-900/50 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">{label}</div>
|
||||
<div className="text-[0.72rem] opacity-90">{detail || (active ? 'active' : 'idle')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -176,10 +180,12 @@ export default function VipAudioUploadCard({
|
||||
stopMicWhip,
|
||||
}) {
|
||||
const { state: controlState } = useControlSystem();
|
||||
const { value: vipAudio, save: saveVipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false });
|
||||
|
||||
const roverId = String(ownRoverId || '').trim();
|
||||
const [selectedUpload, setSelectedUpload] = useState(null);
|
||||
const [working, setWorking] = useState(false);
|
||||
const [openMicEnabled, setOpenMicEnabled] = useState(false);
|
||||
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
|
||||
const [micState, setMicState] = useState('idle');
|
||||
const [message, setMessage] = useState('');
|
||||
const streamRef = useRef(null);
|
||||
@@ -204,7 +210,8 @@ export default function VipAudioUploadCard({
|
||||
selectedForwardState?.source === 'mic-whip' &&
|
||||
(selectedForwardState?.state === 'starting' || selectedForwardState?.state === 'playing'),
|
||||
);
|
||||
const micTalking = Boolean(audioTrackRef.current && (openMicEnabled || pttActive));
|
||||
const micHot = Boolean(audioTrackRef.current && (openMicEnabled || pttActive));
|
||||
const whipLinkActive = micState === 'live' || micState === 'starting';
|
||||
|
||||
const handleUploadPlay = async () => {
|
||||
if (!roverId) {
|
||||
@@ -427,63 +434,76 @@ export default function VipAudioUploadCard({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="surface h-full">
|
||||
<div className="grid h-full gap-0.5 grid-rows-[auto_auto_auto_1fr_auto]">
|
||||
<p className="text-sm text-slate-200 text-center">VIP Audio Forwarding</p>
|
||||
<div className="grid gap-0.5 grid-cols-1 lg:grid-cols-2">
|
||||
<section className="surface h-full">
|
||||
<div className="grid h-full gap-0.5 grid-rows-[auto_auto_1fr_auto]">
|
||||
<p className="text-sm text-slate-200 text-center">Audio Upload</p>
|
||||
|
||||
<label className="mx-auto grid w-full max-w-sm gap-0.5 text-xs text-slate-300 text-center">
|
||||
<span>Audio file (mp3 / wav / ogg)</span>
|
||||
<input
|
||||
className={`${fieldClass} text-center`}
|
||||
type="file"
|
||||
accept=".mp3,.wav,.ogg,audio/mpeg,audio/wav,audio/ogg"
|
||||
disabled={working || !roverId}
|
||||
onChange={(event) => setSelectedUpload(event.target.files?.[0] || null)}
|
||||
/>
|
||||
</label>
|
||||
<label className="mx-auto grid w-full max-w-sm gap-0.5 text-xs text-slate-300 text-center">
|
||||
<span>Audio file (mp3 / wav / ogg)</span>
|
||||
<input
|
||||
className={`${fieldClass} text-center`}
|
||||
type="file"
|
||||
accept=".mp3,.wav,.ogg,audio/mpeg,audio/wav,audio/ogg"
|
||||
disabled={working || !roverId}
|
||||
onChange={(event) => setSelectedUpload(event.target.files?.[0] || null)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{selectedUpload ? (
|
||||
<div className="surface-muted mx-auto w-full max-w-sm text-xs text-slate-300 text-center">
|
||||
{selectedUpload.name} ({selectedUpload.size} bytes)
|
||||
<div className="mx-auto w-full max-w-sm text-center">
|
||||
{selectedUpload ? (
|
||||
<div className="surface-muted text-xs text-slate-300">
|
||||
{selectedUpload.name} ({selectedUpload.size} bytes)
|
||||
</div>
|
||||
) : (
|
||||
<div className="surface-muted text-xs text-slate-500">No file selected</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex justify-center gap-0.5">
|
||||
<button type="button" className="button-dark text-sm" disabled={working || !roverId} onClick={handleUploadPlay}>
|
||||
{working ? 'Working...' : 'Play Upload'}
|
||||
</button>
|
||||
<button type="button" className="button-dark text-sm" disabled={working || !roverId} onClick={handleUploadStop}>
|
||||
Stop
|
||||
</button>
|
||||
<div className="flex justify-center gap-0.5">
|
||||
<button type="button" className="button-dark text-sm" disabled={working || !roverId} onClick={handleUploadPlay}>
|
||||
{working ? 'Working...' : 'Play Upload'}
|
||||
</button>
|
||||
<button type="button" className="button-dark text-sm" disabled={working || !roverId} onClick={handleUploadStop}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid gap-0.5 lg:grid-cols-2">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-slate-300 text-center">Audio Path Status</p>
|
||||
<StatusLight label="Forward pipe" active={pipelineConnected} detail={pipelineConnected ? 'connected' : 'offline'} />
|
||||
<StatusLight label="Upload playing" active={uploadPlaying} detail={uploadPlaying ? 'active' : 'idle'} />
|
||||
<StatusLight label="Mic relay" active={micRelayActive} detail={micRelayActive ? 'active' : 'idle'} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs text-slate-300 text-center">Microphone</p>
|
||||
<label className="surface-muted flex items-center justify-center gap-0.5 px-0.5 py-0.25 text-xs text-slate-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={openMicEnabled}
|
||||
disabled={!roverId}
|
||||
onChange={(event) => setOpenMicEnabled(Boolean(event.target.checked))}
|
||||
/>
|
||||
<span>Open mic</span>
|
||||
</label>
|
||||
<StatusLight label="WHIP link" active={micState === 'live' || micState === 'starting'} detail={micState} />
|
||||
<StatusLight label="PTT transmit" active={micTalking} detail={micTalking ? 'talking' : 'muted'} />
|
||||
<section className="surface h-full">
|
||||
<div className="grid h-full gap-0.5 grid-rows-[auto_auto_1fr]">
|
||||
<p className="text-sm text-slate-200 text-center">Live Microphone</p>
|
||||
<label className="surface-muted mx-auto flex w-full max-w-sm items-center justify-center gap-0.5 px-0.5 py-0.5 text-xs text-slate-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={openMicEnabled}
|
||||
disabled={!roverId}
|
||||
onChange={(event) => saveVipAudio((current) => ({ ...(current || {}), openMicEnabled: Boolean(event.target.checked) }))}
|
||||
/>
|
||||
<span>Open mic</span>
|
||||
</label>
|
||||
<div className="grid gap-0.5 content-start">
|
||||
<StatusIndicator label="WHIP link" active={whipLinkActive} detail={micState} />
|
||||
<StatusIndicator label="Mic hot" active={micHot} detail={micHot ? 'transmitting' : 'muted'} />
|
||||
<div className="surface-muted text-center text-xs text-slate-400">PTT key: {controlState?.keymap?.micPtt?.[0] || 'm'} (hold)</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{message ? <div className="text-xs text-slate-400 text-center">{message}</div> : null}
|
||||
</div>
|
||||
</section>
|
||||
<section className="surface lg:col-span-2">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-slate-200 text-center">Audio Status</p>
|
||||
<div className="grid gap-0.5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<StatusIndicator label="Forward pipe" active={pipelineConnected} detail={pipelineConnected ? 'connected' : 'offline'} />
|
||||
<StatusIndicator label="Upload playback" active={uploadPlaying} detail={uploadPlaying ? 'playing' : 'idle'} />
|
||||
<StatusIndicator label="Mic relay" active={micRelayActive} detail={micRelayActive ? 'active' : 'idle'} />
|
||||
<StatusIndicator label="WHIP transport" active={whipLinkActive} detail={micState} />
|
||||
<StatusIndicator label="Mic transmit" active={micHot} detail={micHot ? 'hot' : 'muted'} />
|
||||
</div>
|
||||
{message ? <div className="text-xs text-slate-400 text-center">{message}</div> : null}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user