Spaces:
Running
Running
<script lang="ts"> | |
import type { Robot } from '../Robot.svelte.js'; | |
import { robotManager } from '../RobotManager.svelte.js'; | |
import { settings } from '$lib/runes/settings.svelte'; | |
import USBCalibrationPanel from '../calibration/USBCalibrationPanel.svelte'; | |
import { USBConsumer } from '../drivers/USBConsumer.js'; | |
import { USBProducer } from '../drivers/USBProducer.js'; | |
interface Props { | |
robot: Robot; | |
workspaceId: string; | |
} | |
let { robot, workspaceId }: Props = $props(); | |
const hasConsumer = $derived(robot.hasConsumer); | |
const outputDriverCount = $derived(robot.outputDriverCount); | |
const consumer = $derived(robot.consumer); | |
const producers = $derived(robot.producers); | |
// Connection configs - using transport server for communication | |
let remoteRobotId = $state(robot.id); | |
let connecting = $state(false); | |
let error = $state<string | null>(null); | |
// USB connection flow state | |
let showUSBCalibration = $state(false); | |
let pendingUSBConnection: 'consumer' | 'producer' | null = $state(null); | |
// Room management state | |
const rooms = $derived(robotManager.rooms); | |
const roomsLoading = $derived(robotManager.roomsLoading); | |
let selectedRoomId = $state(''); | |
let newRoomId = $state(''); | |
let showRoomManagement = $state(true); // Show rooms by default | |
// Auto-load rooms when component loads | |
$effect(() => { | |
if (rooms.length === 0 && !roomsLoading && workspaceId) { | |
refreshRooms(); | |
} | |
}); | |
// Set up calibration completion callback with position sync | |
robot.calibrationManager.onCalibrationCompleteWithPositions((finalPositions) => { | |
console.log('[ConnectionPanel] Calibration complete, syncing robot to final positions'); | |
robot.syncToCalibrationPositions(finalPositions); | |
}); | |
async function connectUSBConsumer() { | |
try { | |
connecting = true; | |
error = null; | |
// Check if calibration is needed | |
if (robot.calibrationManager.needsCalibration) { | |
pendingUSBConnection = 'consumer'; | |
showUSBCalibration = true; | |
return; // Don't proceed with connection yet | |
} | |
await robot.setConsumer({ | |
type: 'usb', | |
baudRate: 1000000 | |
}); | |
} catch (err) { | |
console.error('Failed to connect USB consumer:', err); | |
error = err instanceof Error ? err.message : 'Unknown error'; | |
} finally { | |
connecting = false; | |
} | |
} | |
async function connectRemoteConsumer() { | |
try { | |
connecting = true; | |
error = null; | |
await robot.setConsumer({ | |
type: 'remote', | |
url: settings.transportServerUrl, | |
robotId: remoteRobotId, | |
workspaceId: workspaceId | |
}); | |
} catch (err) { | |
console.error('Failed to connect remote consumer:', err); | |
error = err instanceof Error ? err.message : 'Unknown error'; | |
} finally { | |
connecting = false; | |
} | |
} | |
async function connectUSBProducer() { | |
try { | |
connecting = true; | |
error = null; | |
// Check if calibration is needed | |
if (robot.calibrationManager.needsCalibration) { | |
pendingUSBConnection = 'producer'; | |
showUSBCalibration = true; | |
return; // Don't proceed with connection yet | |
} | |
await robot.addProducer({ | |
type: 'usb', | |
baudRate: 1000000 | |
}); | |
} catch (err) { | |
console.error('Failed to connect USB producer:', err); | |
error = err instanceof Error ? err.message : 'Unknown error'; | |
} finally { | |
connecting = false; | |
} | |
} | |
async function connectRemoteProducer() { | |
try { | |
connecting = true; | |
error = null; | |
await robot.addProducer({ | |
type: 'remote', | |
url: settings.transportServerUrl, | |
robotId: remoteRobotId, | |
workspaceId: workspaceId | |
}); | |
} catch (err) { | |
console.error('Failed to connect remote producer:', err); | |
error = err instanceof Error ? err.message : 'Unknown error'; | |
} finally { | |
connecting = false; | |
} | |
} | |
async function disconnectConsumer() { | |
try { | |
connecting = true; | |
error = null; | |
await robot.removeConsumer(); | |
} catch (err) { | |
console.error('Failed to disconnect consumer:', err); | |
error = err instanceof Error ? err.message : 'Unknown error'; | |
} finally { | |
connecting = false; | |
} | |
} | |
async function disconnectProducer(producerId: string) { | |
try { | |
connecting = true; | |
error = null; | |
await robot.removeProducer(producerId); | |
} catch (err) { | |
console.error('Failed to disconnect producer:', err); | |
error = err instanceof Error ? err.message : 'Unknown error'; | |
} finally { | |
connecting = false; | |
} | |
} | |
// Room management functions | |
async function refreshRooms() { | |
try { | |
await robotManager.refreshRooms(workspaceId); | |
} catch (err) { | |
console.error('Failed to refresh rooms:', err); | |
error = err instanceof Error ? err.message : 'Failed to refresh rooms'; | |
} | |
} | |
async function createRoom() { | |
try { | |
connecting = true; | |
error = null; | |
const result = await robotManager.createRoboticsRoom(workspaceId, newRoomId || undefined); | |
if (result.success) { | |
newRoomId = ''; | |
await refreshRooms(); | |
} else { | |
error = result.error || 'Failed to create room'; | |
} | |
} catch (err) { | |
console.error('Failed to create room:', err); | |
error = err instanceof Error ? err.message : 'Failed to create room'; | |
} finally { | |
connecting = false; | |
} | |
} | |
async function joinRoomAsConsumer() { | |
if (!selectedRoomId) { | |
error = 'Please select a room'; | |
return; | |
} | |
try { | |
connecting = true; | |
error = null; | |
await robotManager.connectConsumerToRoom(workspaceId, robot.id, selectedRoomId); | |
} catch (err) { | |
console.error('Failed to join room as consumer:', err); | |
error = err instanceof Error ? err.message : 'Failed to join room as consumer'; | |
} finally { | |
connecting = false; | |
} | |
} | |
async function joinRoomAsProducer() { | |
if (!selectedRoomId) { | |
error = 'Please select a room'; | |
return; | |
} | |
try { | |
connecting = true; | |
error = null; | |
await robotManager.connectProducerToRoom(workspaceId, robot.id, selectedRoomId); | |
} catch (err) { | |
console.error('Failed to join room as producer:', err); | |
error = err instanceof Error ? err.message : 'Failed to join room as producer'; | |
} finally { | |
connecting = false; | |
} | |
} | |
async function createRoomAndJoinAsProducer() { | |
try { | |
connecting = true; | |
error = null; | |
const result = await robotManager.connectProducerAsProducer(workspaceId, robot.id, newRoomId || undefined); | |
if (result.success) { | |
newRoomId = ''; | |
await refreshRooms(); | |
} else { | |
error = result.error || 'Failed to create room and join as producer'; | |
} | |
} catch (err) { | |
console.error('Failed to create room and join as producer:', err); | |
error = err instanceof Error ? err.message : 'Failed to create room and join as producer'; | |
} finally { | |
connecting = false; | |
} | |
} | |
// Handle calibration completion | |
async function onCalibrationComplete() { | |
showUSBCalibration = false; | |
if (pendingUSBConnection === 'consumer') { | |
await connectUSBConsumer(); | |
} else if (pendingUSBConnection === 'producer') { | |
await connectUSBProducer(); | |
} | |
pendingUSBConnection = null; | |
} | |
function onCalibrationCancel() { | |
showUSBCalibration = false; | |
pendingUSBConnection = null; | |
connecting = false; | |
} | |
</script> | |
<div class="space-y-4"> | |
<!-- Connection panel --> | |
<div class="bg-slate-800 border border-slate-600 rounded-lg p-4 space-y-4"> | |
<h3 class="text-lg font-semibold text-slate-100">Connections - {robot.id}</h3> | |
<!-- Error display --> | |
{#if error} | |
<div class="bg-red-900/20 border border-red-500/30 rounded p-2 text-red-400 text-sm"> | |
{error} | |
</div> | |
{/if} | |
<!-- Room Management Section --> | |
<div class="space-y-2"> | |
<div class="flex items-center justify-between"> | |
<h4 class="text-sm font-medium text-slate-300">Room Management</h4> | |
<button | |
onclick={() => showRoomManagement = !showRoomManagement} | |
class="text-xs text-blue-400 hover:text-blue-300" | |
> | |
{showRoomManagement ? 'Hide' : 'Show'} Rooms | |
</button> | |
</div> | |
{#if showRoomManagement} | |
<div class="bg-slate-700/30 border border-slate-600 rounded p-3 space-y-3"> | |
<!-- Refresh Rooms --> | |
<div class="flex items-center gap-2"> | |
<button | |
onclick={refreshRooms} | |
disabled={roomsLoading || connecting} | |
class="px-3 py-1 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-xs" | |
> | |
{#if roomsLoading} | |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span> | |
Loading... | |
{:else} | |
<span class="icon-[mdi--refresh] size-3 mr-1"></span> | |
Refresh Rooms | |
{/if} | |
</button> | |
<span class="text-xs text-slate-400"> | |
{rooms.length} room{rooms.length !== 1 ? 's' : ''} available | |
</span> | |
</div> | |
<!-- Available Rooms --> | |
<div class="space-y-2"> | |
<span class="text-xs text-slate-400">Available Rooms:</span> | |
<div class="max-h-48 space-y-2 overflow-y-auto"> | |
<!-- Create New Room Option --> | |
<div class="rounded border-2 border-dashed border-green-500/50 bg-green-500/5 p-2"> | |
<div class="space-y-2"> | |
<div class="flex items-center gap-2"> | |
<span class="text-sm font-medium text-green-300">Create New Room</span> | |
</div> | |
<p class="text-xs text-green-400/70"> | |
Create a room for collaboration | |
</p> | |
<input | |
bind:value={newRoomId} | |
placeholder="Room ID (optional)" | |
disabled={connecting} | |
class="w-full px-2 py-1 bg-slate-700 border border-slate-600 rounded text-xs text-slate-100 disabled:opacity-50" | |
/> | |
<div class="flex gap-1"> | |
<button | |
onclick={createRoom} | |
disabled={connecting} | |
class="flex-1 px-2 py-1 bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-xs" | |
> | |
{#if connecting} | |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span> | |
Creating... | |
{:else} | |
Create | |
{/if} | |
</button> | |
<button | |
onclick={createRoomAndJoinAsProducer} | |
disabled={connecting} | |
class="flex-1 px-2 py-1 bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-xs" | |
> | |
{#if connecting} | |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span> | |
Creating... | |
{:else} | |
Create & Join | |
{/if} | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Existing Rooms --> | |
{#if rooms.length === 0} | |
<div class="text-center py-2 text-xs text-slate-500"> | |
{roomsLoading ? 'Loading...' : 'No existing rooms available'} | |
</div> | |
{:else} | |
{#each rooms as room} | |
<div class="rounded border border-slate-600 bg-slate-700/30 p-2"> | |
<div class="mb-2"> | |
<p class="text-sm font-medium text-slate-200 truncate">{room.id}</p> | |
<p class="text-xs text-slate-400">{room.participants?.total || 0} participants</p> | |
</div> | |
<div class="flex gap-1"> | |
<button | |
onclick={() => { | |
selectedRoomId = room.id; | |
joinRoomAsConsumer(); | |
}} | |
disabled={connecting} | |
class="flex-1 px-2 py-1 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-xs" | |
> | |
Join as Consumer | |
</button> | |
<button | |
onclick={() => { | |
selectedRoomId = room.id; | |
joinRoomAsProducer(); | |
}} | |
disabled={connecting} | |
class="flex-1 px-2 py-1 bg-orange-600 hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-xs" | |
> | |
Join as Producer | |
</button> | |
</div> | |
</div> | |
{/each} | |
{/if} | |
</div> | |
</div> | |
</div> | |
{/if} | |
</div> | |
<!-- Consumer Section (Input) - SINGLE --> | |
<div class="space-y-2"> | |
<h4 class="text-sm font-medium text-slate-300">Consumer (Receive Commands) - Single</h4> | |
{#if hasConsumer} | |
<div class="flex items-center justify-between bg-green-900/20 border border-green-500/30 rounded p-2"> | |
<div> | |
<span class="text-green-300 text-sm">{consumer?.name || 'Consumer Active'}</span> | |
<span class="text-xs text-slate-500 ml-2"> | |
{consumer?.status.isConnected ? '🟢 Connected' : '🔴 Disconnected'} | |
</span> | |
</div> | |
<button | |
onclick={disconnectConsumer} | |
disabled={connecting} | |
class="px-3 py-1 bg-red-600 hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-xs" | |
> | |
{connecting ? 'Disconnecting...' : 'Disconnect'} | |
</button> | |
</div> | |
{:else} | |
<div class="space-y-2"> | |
<button | |
onclick={connectUSBConsumer} | |
disabled={connecting} | |
class="w-full py-2 px-3 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-sm" | |
> | |
{connecting ? 'Connecting...' : 'Connect USB Consumer'} | |
</button> | |
<div class="space-y-2"> | |
<div class="flex gap-2"> | |
<input | |
bind:value={settings.transportServerUrl} | |
placeholder="Transport server URL (e.g. http://localhost:8000)" | |
disabled={connecting} | |
class="flex-1 px-3 py-2 bg-slate-700 border border-slate-600 rounded text-sm text-slate-100 disabled:opacity-50" | |
/> | |
<button | |
onclick={connectRemoteConsumer} | |
disabled={connecting} | |
class="px-3 py-2 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-sm whitespace-nowrap" | |
> | |
{connecting ? 'Connecting...' : 'Remote Consumer'} | |
</button> | |
</div> | |
<div class="text-xs text-slate-500"> | |
Remote Consumer: Receive commands from transport server | |
</div> | |
</div> | |
</div> | |
{/if} | |
</div> | |
<!-- Producers Section (Output) - MULTIPLE --> | |
<div class="space-y-2"> | |
<h4 class="text-sm font-medium text-slate-300">Producers (Send Commands) - {outputDriverCount} connected</h4> | |
<div class="space-y-2"> | |
<button | |
onclick={connectUSBProducer} | |
disabled={connecting} | |
class="w-full py-2 px-3 bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-sm" | |
> | |
{connecting ? 'Connecting...' : 'Add USB Producer'} | |
</button> | |
<button | |
onclick={connectRemoteProducer} | |
disabled={connecting} | |
class="w-full py-2 px-3 bg-orange-600 hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-sm" | |
> | |
{connecting ? 'Connecting...' : 'Add Remote Producer'} | |
</button> | |
<div class="text-xs text-slate-500"> | |
Remote Producer: Send commands to transport server. Uses Robot ID: {remoteRobotId} | |
</div> | |
</div> | |
<!-- Connected Producers List --> | |
{#each producers as producer} | |
<div class="flex items-center justify-between bg-slate-700/50 border border-slate-600 rounded p-2"> | |
<div> | |
<span class="text-slate-300 text-sm">{producer.name}</span> | |
<span class="text-xs text-slate-500 ml-2"> | |
{producer.status.isConnected ? '🟢 Connected' : '🔴 Disconnected'} | |
</span> | |
</div> | |
<button | |
onclick={() => disconnectProducer(producer.id)} | |
disabled={connecting} | |
class="px-2 py-1 bg-red-600 hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-xs" | |
> | |
{connecting ? 'Removing...' : 'Remove'} | |
</button> | |
</div> | |
{/each} | |
</div> | |
<!-- Robot ID Config --> | |
<div class="pt-2 border-t border-slate-600"> | |
<span class="text-xs text-slate-400">Robot ID for Remote Connections:</span> | |
<input | |
bind:value={remoteRobotId} | |
disabled={connecting} | |
class="w-full px-3 py-1 bg-slate-700 border border-slate-600 rounded text-sm text-slate-100 mt-1 disabled:opacity-50" | |
/> | |
</div> | |
</div> | |
<!-- USB Calibration Modal - Only shown when connecting USB drivers --> | |
{#if showUSBCalibration} | |
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> | |
<div class="bg-slate-800 rounded-lg p-6 max-w-2xl w-full m-4 space-y-4"> | |
<div class="flex justify-between items-center"> | |
<h2 class="text-lg font-semibold text-white"> | |
USB Calibration Required | |
{#if pendingUSBConnection} | |
<span class="text-sm text-slate-400 ml-2"> | |
(for {pendingUSBConnection === 'consumer' ? 'Consumer' : 'Producer'}) | |
</span> | |
{/if} | |
</h2> | |
<button | |
onclick={onCalibrationCancel} | |
class="text-gray-400 hover:text-white" | |
> | |
✕ | |
</button> | |
</div> | |
<div class="text-sm text-slate-300 mb-4"> | |
Before connecting USB drivers, the robot needs to be calibrated to map its physical range to software values. | |
</div> | |
<USBCalibrationPanel | |
calibrationManager={robot.calibrationManager} | |
connectionType={pendingUSBConnection || 'consumer'} | |
{onCalibrationComplete} | |
onCancel={onCalibrationCancel} | |
/> | |
</div> | |
</div> | |
{/if} | |
</div> |