Skip to content
7 changes: 7 additions & 0 deletions src/app/tablet/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function TabletLayout({
children,
}: {
children: React.ReactNode;
}) {
return <div>{children}</div>;
}
5 changes: 5 additions & 0 deletions src/app/tablet/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TabletDashboard } from '@/components/tablet/TabletDashboard';

export default function TabletPage() {
return <TabletDashboard />;
}
139 changes: 138 additions & 1 deletion src/components/dashboard/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useState } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Toaster, toast } from 'sonner';
import { Header } from '@/components/dashboard/header';
import { CameraFeed } from '@/components/dashboard/camera-feed';
Expand All @@ -19,10 +19,123 @@ const vitalsData = [
{ time: '13:00', heartRate: 75, battery: 60 },
];

const TELEOP_SEND_INTERVAL_MS = 50;

export function Dashboard() {
const [isAutonomous, setIsAutonomous] = useState(true);
const [speed, setSpeed] = useState([50]);
const [activeTask, setActiveTask] = useState<string | null>(null);
const [linearX, setLinearX] = useState(0);
const [linearY, setLinearY] = useState(0);
const [angular, setAngular] = useState(0);

const wsRef = useRef<WebSocket | null>(null);
const currentCmdRef = useRef({ linearX: 0, linearY: 0, angular: 0 });
const sendIntervalRef = useRef<NodeJS.Timeout | null>(null);
const speedRef = useRef(speed);
const isAutonomousRef = useRef(isAutonomous);

// Garde les refs synchronisées avec le state pour que l'intervalle lise les valeurs à jour
useEffect(() => {
speedRef.current = speed;
}, [speed]);

useEffect(() => {
isAutonomousRef.current = isAutonomous;
}, [isAutonomous]);

// Connexion WebSocket
useEffect(() => {
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.hostname}:8765`;

wsRef.current = new WebSocket(wsUrl);

wsRef.current.onopen = () => {
console.log('[WS] Connecté au backend');
toast.success('Robot connecté');
};

wsRef.current.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
console.log('[WS] Message reçu:', message.type);
} catch (error) {
console.error('[WS] Erreur parsing:', error);
}
};

wsRef.current.onerror = () => {
toast.error('Erreur de connexion au robot');
};

wsRef.current.onclose = () => {
console.log('[WS] Déconnecté');
toast.error('Robot déconnecté');
};

return () => {
if (wsRef.current) {
wsRef.current.close();
}
};
}, []);

// Envoie la commande courante au robot
const sendCurrentCmd = () => {
if (isAutonomousRef.current) return;
if (wsRef.current?.readyState !== WebSocket.OPEN) return;

const speedFactor = (speedRef.current[0] ?? 50) / 100;
const { linearX: x, linearY: y, angular: a } = currentCmdRef.current;

const message = {
type: 'teleop.move',
data: {
linear_x: x * speedFactor,
linear_y: y * speedFactor,
angular_z: a * speedFactor,
},
};
wsRef.current.send(JSON.stringify(message));
};

// Boucle d'envoi à 20 Hz quand en mode manuel
useEffect(() => {
if (isAutonomous) {
// Stop la boucle et reset la commande
if (sendIntervalRef.current) {
clearInterval(sendIntervalRef.current);
sendIntervalRef.current = null;
}
currentCmdRef.current = { linearX: 0, linearY: 0, angular: 0 };
return;
}

sendIntervalRef.current = setInterval(
sendCurrentCmd,
TELEOP_SEND_INTERVAL_MS
);

return () => {
if (sendIntervalRef.current) {
clearInterval(sendIntervalRef.current);
sendIntervalRef.current = null;
}
};
}, [isAutonomous]);

// Sécurité : stop si la fenêtre perd le focus
useEffect(() => {
const onBlur = () => {
currentCmdRef.current = { linearX: 0, linearY: 0, angular: 0 };
setLinearX(0);
setLinearY(0);
setAngular(0);
};
window.addEventListener('blur', onBlur);
return () => window.removeEventListener('blur', onBlur);
}, []);
const { connectionState, health } = useRobotHealth();

const handleStartTask = (taskName: string) => {
Expand All @@ -38,11 +151,28 @@ export function Dashboard() {
const handleEmergencyStop = () => {
setIsAutonomous(false);
setActiveTask(null);
currentCmdRef.current = { linearX: 0, linearY: 0, angular: 0 };
setLinearX(0);
setLinearY(0);
setAngular(0);
toast.error("ARRÊT D'URGENCE ACTIVÉ ! Passage en mode manuel.", {
style: { backgroundColor: '#ef4444', color: 'white', border: 'none' },
});
};

// Met à jour la commande courante — l'intervalle s'occupe de l'envoi
const handleTeleopMove = (
newLinearX: number,
newLinearY: number,
newAngular: number
) => {
currentCmdRef.current = {
linearX: newLinearX,
linearY: newLinearY,
angular: newAngular,
};
};

return (
<div className="min-h-screen bg-background font-sans text-foreground">
<Toaster position="top-right" />
Expand All @@ -62,6 +192,13 @@ export function Dashboard() {
speed={speed}
setSpeed={setSpeed}
onEmergencyStop={handleEmergencyStop}
linearX={linearX}
setLinearX={setLinearX}
linearY={linearY}
setLinearY={setLinearY}
angular={angular}
setAngular={setAngular}
onTeleopMove={handleTeleopMove}
/>
</div>

Expand Down
76 changes: 75 additions & 1 deletion src/components/dashboard/manual-control.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';

import React from 'react';
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Power } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Expand All @@ -20,6 +19,13 @@ interface ManualControlProps {
speed: number[];
setSpeed: (value: number[]) => void;
onEmergencyStop: () => void;
linearX?: number;
setLinearX?: (value: number) => void;
linearY?: number;
setLinearY?: (value: number) => void;
angular?: number;
setAngular?: (value: number) => void;
onTeleopMove?: (linearX: number, linearY: number, angular: number) => void;
}

export function ManualControl({
Expand All @@ -28,7 +34,52 @@ export function ManualControl({
speed,
setSpeed,
onEmergencyStop,
setLinearX,
setLinearY,
setAngular,
onTeleopMove,
}: Readonly<ManualControlProps>) {
const applyDirection = (
direction: 'up' | 'down' | 'left' | 'right' | 'stop'
) => {
if (isAutonomous) return;

let newLinearX = 0;
const newLinearY = 0;
let newAngular = 0;

switch (direction) {
case 'up':
newLinearX = 1;
break;
case 'down':
newLinearX = -1;
break;
case 'left':
newAngular = 1;
break;
case 'right':
newAngular = -1;
break;
case 'stop':
// tout à zéro
break;
}

setLinearX?.(newLinearX);
setLinearY?.(newLinearY);
setAngular?.(newAngular);
onTeleopMove?.(newLinearX, newLinearY, newAngular);
};

const release = () => {
if (isAutonomous) return;
setLinearX?.(0);
setLinearY?.(0);
setAngular?.(0);
onTeleopMove?.(0, 0, 0);
};

return (
<Card>
<CardHeader>
Expand Down Expand Up @@ -73,36 +124,59 @@ export function ManualControl({
variant="secondary"
className="h-12 w-full"
disabled={isAutonomous}
onMouseDown={() => applyDirection('up')}
onMouseUp={release}
onMouseLeave={release}
onTouchStart={() => applyDirection('up')}
onTouchEnd={release}
>
<ArrowUp className="h-6 w-6" />
</Button>
<div />

<Button
variant="secondary"
className="h-12 w-full"
disabled={isAutonomous}
onMouseDown={() => applyDirection('left')}
onMouseUp={release}
onMouseLeave={release}
onTouchStart={() => applyDirection('left')}
onTouchEnd={release}
>
<ArrowLeft className="h-6 w-6" />
</Button>
<Button
variant="secondary"
className="h-12 w-full"
disabled={isAutonomous}
onClick={() => applyDirection('stop')}
>
<Power className="h-6 w-6" />
</Button>
<Button
variant="secondary"
className="h-12 w-full"
disabled={isAutonomous}
onMouseDown={() => applyDirection('right')}
onMouseUp={release}
onMouseLeave={release}
onTouchStart={() => applyDirection('right')}
onTouchEnd={release}
>
<ArrowRight className="h-6 w-6" />
</Button>

<div />
<Button
variant="secondary"
className="h-12 w-full"
disabled={isAutonomous}
onMouseDown={() => applyDirection('down')}
onMouseUp={release}
onMouseLeave={release}
onTouchStart={() => applyDirection('down')}
onTouchEnd={release}
>
<ArrowDown className="h-6 w-6" />
</Button>
Expand Down
46 changes: 46 additions & 0 deletions src/components/tablet/AlertBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Info, AlertTriangle, AlertCircle } from 'lucide-react';

interface Alert {
timestamp: string;
message: string;
severity: 'info' | 'warning' | 'error';
}

interface AlertBannerProps {
lastAlert: Alert | null;
}

export function AlertBanner({ lastAlert }: AlertBannerProps) {
if (!lastAlert) {
return (
<Alert variant="default" className="bg-muted/50">
<Info className="h-4 w-4" />
<AlertTitle>Information</AlertTitle>
<AlertDescription>Aucune alerte récente</AlertDescription>
</Alert>
);
}

const severityIcon = {
info: <Info className="h-4 w-4" />,
warning: <AlertTriangle className="h-4 w-4" />,
error: <AlertCircle className="h-4 w-4" />,
};

const severityVariant = {
info: 'default',
warning: 'default',
error: 'destructive',
} as const;

return (
<Alert variant={severityVariant[lastAlert.severity]}>
{severityIcon[lastAlert.severity]}
<AlertTitle>{lastAlert.timestamp}</AlertTitle>
<AlertDescription>{lastAlert.message}</AlertDescription>
</Alert>
);
}
Loading
Loading