From 0577edecb6332674e9cdbc5d42168714f328a8c9 Mon Sep 17 00:00:00 2001 From: ADAV Date: Tue, 19 May 2026 16:53:50 +0200 Subject: [PATCH 1/8] feat(tablet): add initial tablet interface --- src/app/tablet/layout.tsx | 7 +++++++ src/app/tablet/page.tsx | 5 +++++ src/components/tablet/TabletDashbord.tsx | 10 ++++++++++ 3 files changed, 22 insertions(+) create mode 100644 src/app/tablet/layout.tsx create mode 100644 src/app/tablet/page.tsx create mode 100644 src/components/tablet/TabletDashbord.tsx diff --git a/src/app/tablet/layout.tsx b/src/app/tablet/layout.tsx new file mode 100644 index 0000000..0337442 --- /dev/null +++ b/src/app/tablet/layout.tsx @@ -0,0 +1,7 @@ +export default function TabletLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/src/app/tablet/page.tsx b/src/app/tablet/page.tsx new file mode 100644 index 0000000..0ca0681 --- /dev/null +++ b/src/app/tablet/page.tsx @@ -0,0 +1,5 @@ +import { TabletDashboard } from '@/components/tablet/TabletDashbord'; + +export default function TabletPage() { + return ; +} diff --git a/src/components/tablet/TabletDashbord.tsx b/src/components/tablet/TabletDashbord.tsx new file mode 100644 index 0000000..d66410b --- /dev/null +++ b/src/components/tablet/TabletDashbord.tsx @@ -0,0 +1,10 @@ +'use client'; + +export function TabletDashboard() { + return ( +
+

Tablette ROSMaster M3 Pro

+

Interface tablette démarrée avec succès

+
+ ); +} From ccb84647b3fa2f6aa578f2b4e591221986f5bfe2 Mon Sep 17 00:00:00 2001 From: ADAV Date: Wed, 20 May 2026 11:38:44 +0200 Subject: [PATCH 2/8] feat(tablet): add tablet dashboard with robot status, mission, arm and alert components --- src/app/tablet/page.tsx | 2 +- src/components/tablet/AlertBanner.tsx | 46 +++++++ src/components/tablet/ArmStatus.tsx | 116 ++++++++++++++++ src/components/tablet/MissionCard.tsx | 62 +++++++++ src/components/tablet/RobotStatus.tsx | 81 +++++++++++ src/components/tablet/TabletDashboard.tsx | 156 ++++++++++++++++++++++ src/components/tablet/TabletDashbord.tsx | 10 -- 7 files changed, 462 insertions(+), 11 deletions(-) create mode 100644 src/components/tablet/AlertBanner.tsx create mode 100644 src/components/tablet/ArmStatus.tsx create mode 100644 src/components/tablet/MissionCard.tsx create mode 100644 src/components/tablet/RobotStatus.tsx create mode 100644 src/components/tablet/TabletDashboard.tsx delete mode 100644 src/components/tablet/TabletDashbord.tsx diff --git a/src/app/tablet/page.tsx b/src/app/tablet/page.tsx index 0ca0681..61987ba 100644 --- a/src/app/tablet/page.tsx +++ b/src/app/tablet/page.tsx @@ -1,4 +1,4 @@ -import { TabletDashboard } from '@/components/tablet/TabletDashbord'; +import { TabletDashboard } from '@/components/tablet/TabletDashboard'; export default function TabletPage() { return ; diff --git a/src/components/tablet/AlertBanner.tsx b/src/components/tablet/AlertBanner.tsx new file mode 100644 index 0000000..985a932 --- /dev/null +++ b/src/components/tablet/AlertBanner.tsx @@ -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 ( + + + Information + Aucune alerte récente + + ); + } + + const severityIcon = { + info: , + warning: , + error: , + }; + + const severityVariant = { + info: 'default', + warning: 'default', + error: 'destructive', + } as const; + + return ( + + {severityIcon[lastAlert.severity]} + {lastAlert.timestamp} + {lastAlert.message} + + ); +} diff --git a/src/components/tablet/ArmStatus.tsx b/src/components/tablet/ArmStatus.tsx new file mode 100644 index 0000000..e11c560 --- /dev/null +++ b/src/components/tablet/ArmStatus.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { Move, Hand, AlertCircle, Wrench } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +interface ArmJoints { + shoulder: number; + elbow: number; + wrist: number; + gripper: number; +} + +interface ArmStatusProps { + connected: boolean; + joints: ArmJoints; + gripperState: 'open' | 'closed' | 'moving'; + currentAction?: string; + errorMessage?: string; +} + +export function ArmStatus({ + connected, + joints, + gripperState, + currentAction, + errorMessage, +}: ArmStatusProps) { + if (!connected) { + return ( + + + + + Bras robotique + + + +
+ + Bras non connecté ou hors service +
+
+
+ ); + } + + const gripperColor = { + open: 'text-green-500', + closed: 'text-blue-500', + moving: 'text-yellow-500', + }; + + const gripperLabel = { + open: 'Ouvert', + closed: 'Fermé', + moving: 'En mouvement', + }; + + return ( + + + + + + Bras robotique + + + Actif + + + + + {currentAction && ( +
+ + + Action : {currentAction} + +
+ )} + +
+
+ Épaule : + {joints.shoulder}° +
+
+ Coude : + {joints.elbow}° +
+
+ Poignet : + {joints.wrist}° +
+
+ Préhenseur : + + + {gripperLabel[gripperState]} ({joints.gripper}%) + +
+
+ + {errorMessage && ( +
+ + {errorMessage} +
+ )} +
+
+ ); +} diff --git a/src/components/tablet/MissionCard.tsx b/src/components/tablet/MissionCard.tsx new file mode 100644 index 0000000..a315239 --- /dev/null +++ b/src/components/tablet/MissionCard.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { Target } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; +import { Badge } from '@/components/ui/badge'; + +interface MissionCardProps { + missionName: string | null; + progress: number; + step: number; + totalSteps: number; + errorMessage?: string; +} + +export function MissionCard({ + missionName, + progress, + step, + totalSteps, + errorMessage, +}: MissionCardProps) { + if (!missionName) { + return ( + + + Aucune mission en cours + + + ); + } + + return ( + + + + + + {missionName} + + + {errorMessage ? 'Erreur' : 'En cours'} + + + + +
+ Progression + + {progress}% (étape {step}/{totalSteps}) + +
+ + {errorMessage && ( +
+ {errorMessage} +
+ )} +
+
+ ); +} diff --git a/src/components/tablet/RobotStatus.tsx b/src/components/tablet/RobotStatus.tsx new file mode 100644 index 0000000..e16534b --- /dev/null +++ b/src/components/tablet/RobotStatus.tsx @@ -0,0 +1,81 @@ +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Battery, Thermometer, Wifi, AlertTriangle } from 'lucide-react'; + +interface RobotStatusProps { + battery: number; + mode: 'manual' | 'auto' | 'maintenance' | 'stop'; + watchdog: boolean; + temperature?: number; + emergencyStop: boolean; +} + +export function RobotStatus({ + battery, + mode, + watchdog, + temperature, + emergencyStop, +}: RobotStatusProps) { + const getModeVariant = () => { + switch (mode) { + case 'auto': + return 'success'; + case 'manual': + return 'secondary'; + case 'maintenance': + return 'warning'; + case 'stop': + return 'destructive'; + default: + return 'default'; + } + }; + + const batteryColor = + battery < 20 + ? 'text-red-500' + : battery < 50 + ? 'text-yellow-500' + : 'text-green-500'; + + return ( + + + État du robot + + +
+
+ + {battery}% +
+
+ Mode: + {mode.toUpperCase()} +
+
+ + + {watchdog ? 'Watchdog OK' : 'Watchdog FAULT'} + +
+ {temperature !== undefined && ( +
+ + {temperature}°C +
+ )} +
+ {emergencyStop && ( +
+ + Arrêt d'urgence actif +
+ )} +
+
+ ); +} diff --git a/src/components/tablet/TabletDashboard.tsx b/src/components/tablet/TabletDashboard.tsx new file mode 100644 index 0000000..f6aa6cf --- /dev/null +++ b/src/components/tablet/TabletDashboard.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { TabletSmartphone } from 'lucide-react'; +import { RobotStatus } from './RobotStatus'; +import { MissionCard } from './MissionCard'; +import { ArmStatus } from './ArmStatus'; +import { AlertBanner } from './AlertBanner'; + +type Mode = 'manual' | 'auto' | 'maintenance' | 'stop'; + +const INITIAL_DATA = { + battery: 85, + mode: 'auto' as Mode, + watchdog: true, + temperature: 50, + emergencyStop: false, + mission: { + active: false, + name: '', + progress: 0, + step: 1, + totalSteps: 1, + error: undefined as string | undefined, + }, + arm: { + connected: true, + joints: { shoulder: 0, elbow: 45, wrist: 0, gripper: 0 }, + gripperState: 'closed' as const, + currentAction: 'Repos', + errorMessage: undefined as string | undefined, + }, + lastAlert: null as null | { + timestamp: string; + message: string; + severity: 'info' | 'warning' | 'error'; + }, +}; + +const generateRandomData = (): typeof INITIAL_DATA => ({ + battery: Math.floor(Math.random() * 100), + mode: (['manual', 'auto', 'maintenance', 'stop'] as Mode[])[ + Math.floor(Math.random() * 4) + ], + watchdog: Math.random() > 0.1, + temperature: 45 + Math.floor(Math.random() * 20), + emergencyStop: Math.random() < 0.05, + mission: { + active: Math.random() > 0.3, + name: 'Exploration bâtiment A', + progress: Math.floor(Math.random() * 100), + step: Math.floor(Math.random() * 5) + 1, + totalSteps: 5, + error: + Math.random() < 0.1 ? 'Obstacle détecté, arrêt temporaire' : undefined, + }, + arm: { + connected: Math.random() > 0.1, + joints: { + shoulder: Math.floor(Math.random() * 180) - 90, + elbow: Math.floor(Math.random() * 150), + wrist: Math.floor(Math.random() * 180) - 90, + gripper: Math.floor(Math.random() * 100), + }, + gripperState: (['open', 'closed', 'moving'] as const)[ + Math.floor(Math.random() * 3) + ], + currentAction: ['Repos', 'Préhension', 'Rangement', 'Pousse'][ + Math.floor(Math.random() * 4) + ], + errorMessage: Math.random() < 0.1 ? 'Préhenseur bloqué' : undefined, + }, + lastAlert: + Math.random() > 0.7 + ? { + timestamp: new Date().toLocaleTimeString(), + message: 'Batterie faible (15%)', + severity: 'warning' as const, + } + : null, +}); + +export function TabletDashboard() { + const [data, setData] = useState(INITIAL_DATA); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true); + setData(generateRandomData()); + const interval = setInterval(() => { + setData(generateRandomData()); + }, 3000); + return () => clearInterval(interval); + }, []); + + if (!mounted) { + return ( +
+

+ + Interface Robot - ROSMaster M3 Pro +

+
+
+
+
+
+
+
+
+
+ ); + } + + const armData = data.arm || { + connected: false, + joints: { shoulder: 0, elbow: 0, wrist: 0, gripper: 0 }, + gripperState: 'closed' as const, + currentAction: 'Inconnu', + errorMessage: undefined, + }; + + return ( +
+

+ + Interface Robot - ROSMaster M3 Pro +

+
+ + + + +
+
+ ); +} diff --git a/src/components/tablet/TabletDashbord.tsx b/src/components/tablet/TabletDashbord.tsx deleted file mode 100644 index d66410b..0000000 --- a/src/components/tablet/TabletDashbord.tsx +++ /dev/null @@ -1,10 +0,0 @@ -'use client'; - -export function TabletDashboard() { - return ( -
-

Tablette ROSMaster M3 Pro

-

Interface tablette démarrée avec succès

-
- ); -} From be1a58604f47cce60b28dfe5ae3a89d984f27e0f Mon Sep 17 00:00:00 2001 From: justeowen Date: Thu, 21 May 2026 16:46:12 +0200 Subject: [PATCH 3/8] feat(tablet): implement tablet interface with header, camera feed, manual control, task manager, and metrics --- .../dashboard/{header.tsx => Header.tsx} | 0 src/components/dashboard/dashboard.tsx | 72 +++++++++++++- src/components/dashboard/manual-control.tsx | 99 +++++++++++++++++++ src/components/tablet/tablet.tsx | 67 +++++++++++++ 4 files changed, 236 insertions(+), 2 deletions(-) rename src/components/dashboard/{header.tsx => Header.tsx} (100%) create mode 100644 src/components/tablet/tablet.tsx diff --git a/src/components/dashboard/header.tsx b/src/components/dashboard/Header.tsx similarity index 100% rename from src/components/dashboard/header.tsx rename to src/components/dashboard/Header.tsx diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index 68bb9cb..a26b89a 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -1,8 +1,8 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Toaster, toast } from 'sonner'; -import { Header } from '@/components/dashboard/header'; +import { Header } from '@/components/dashboard/Header'; import { CameraFeed } from '@/components/dashboard/camera-feed'; import { ManualControl } from '@/components/dashboard/manual-control'; import { TaskManager } from '@/components/dashboard/task-manager'; @@ -22,6 +22,47 @@ export function Dashboard() { const [isAutonomous, setIsAutonomous] = useState(true); const [speed, setSpeed] = useState([50]); const [activeTask, setActiveTask] = useState(null); + const [linearX, setLinearX] = useState(0); + const [linearY, setLinearY] = useState(0); + const [angular, setAngular] = useState(0); + const wsRef = useRef(null); + + // Initialiser la 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(); + } + }; + }, []); const handleStartTask = (taskName: string) => { setActiveTask(taskName); @@ -41,6 +82,26 @@ export function Dashboard() { }); }; + const handleTeleopMove = ( + newLinearX: number, + newLinearY: number, + newAngular: number + ) => { + if (!isAutonomous && wsRef.current?.readyState === WebSocket.OPEN) { + const speedFactor = (speed[0] ?? 50) / 100; + const message = { + type: 'teleop.move', + data: { + linear_x: newLinearX * speedFactor, + linear_y: newLinearY * speedFactor, + angular_z: newAngular * speedFactor, + }, + }; + wsRef.current.send(JSON.stringify(message)); + console.log('[WS] teleop.move envoyé:', message.data); + } + }; + return (
@@ -56,6 +117,13 @@ export function Dashboard() { speed={speed} setSpeed={setSpeed} onEmergencyStop={handleEmergencyStop} + linearX={linearX} + setLinearX={setLinearX} + linearY={linearY} + setLinearY={setLinearY} + angular={angular} + setAngular={setAngular} + onTeleopMove={handleTeleopMove} />
diff --git a/src/components/dashboard/manual-control.tsx b/src/components/dashboard/manual-control.tsx index 3b4c13d..a84be8c 100644 --- a/src/components/dashboard/manual-control.tsx +++ b/src/components/dashboard/manual-control.tsx @@ -20,6 +20,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({ @@ -28,7 +35,67 @@ export function ManualControl({ speed, setSpeed, onEmergencyStop, + linearX = 0, + setLinearX, + linearY = 0, + setLinearY, + angular = 0, + setAngular, + onTeleopMove, }: Readonly) { + const handleDirectionButton = ( + direction: 'up' | 'down' | 'left' | 'right' | 'stop' + ) => { + if (isAutonomous) return; + + let newLinearX = 0; + let newLinearY = 0; + let newAngular = 0; + + switch (direction) { + case 'up': + newLinearX = 1; + newLinearY = 0; + newAngular = 0; + break; + case 'down': + newLinearX = -1; + newLinearY = 0; + newAngular = 0; + break; + case 'left': + newLinearX = 0; + newLinearY = 1; + newAngular = 1; + break; + case 'right': + newLinearX = 0; + newLinearY = -1; + newAngular = -1; + break; + case 'stop': + newLinearX = 0; + newLinearY = 0; + newAngular = 0; + break; + } + + // Mettre à jour les états pour l'affichage + setLinearX?.(newLinearX); + setLinearY?.(newLinearY); + setAngular?.(newAngular); + + // Envoyer directement les valeurs au handler + onTeleopMove?.(newLinearX, newLinearY, newAngular); + }; + + const handleMouseUp = () => { + // Arrêter le robot quand on relâche + setLinearX?.(0); + setLinearY?.(0); + setAngular?.(0); + onTeleopMove?.(0, 0, 0); + }; return ( @@ -67,12 +134,32 @@ export function ManualControl({
+ {!isAutonomous && ( +
+
+ Linear X:{' '} + {linearX.toFixed(2)} +
+
+ Linear Y:{' '} + {linearY.toFixed(2)} +
+
+ Angular:{' '} + {angular.toFixed(2)} +
+
+ )} +
@@ -81,6 +168,9 @@ export function ManualControl({ variant="secondary" className="h-12 w-full" disabled={isAutonomous} + onMouseDown={() => handleDirectionButton('left')} + onMouseUp={handleMouseUp} + onMouseLeave={handleMouseUp} > @@ -88,6 +178,9 @@ export function ManualControl({ variant="secondary" className="h-12 w-full" disabled={isAutonomous} + onMouseDown={() => handleDirectionButton('stop')} + onMouseUp={handleMouseUp} + onMouseLeave={handleMouseUp} > @@ -95,6 +188,9 @@ export function ManualControl({ variant="secondary" className="h-12 w-full" disabled={isAutonomous} + onMouseDown={() => handleDirectionButton('right')} + onMouseUp={handleMouseUp} + onMouseLeave={handleMouseUp} > @@ -103,6 +199,9 @@ export function ManualControl({ variant="secondary" className="h-12 w-full" disabled={isAutonomous} + onMouseDown={() => handleDirectionButton('down')} + onMouseUp={handleMouseUp} + onMouseLeave={handleMouseUp} > diff --git a/src/components/tablet/tablet.tsx b/src/components/tablet/tablet.tsx new file mode 100644 index 0000000..0afbb4d --- /dev/null +++ b/src/components/tablet/tablet.tsx @@ -0,0 +1,67 @@ +'use client'; + +import React, { useState } from 'react'; +import { Toaster, toast } from 'sonner'; +import { Header } from '@/components/dashboard/Header'; +import { CameraFeed } from '@/components/dashboard/camera-feed'; +import { ManualControl } from '@/components/dashboard/manual-control'; +import { TaskManager } from '@/components/dashboard/task-manager'; +import { MetricsAndAlerts } from '@/components/dashboard/metrics-and-alerts'; + +export function Tablet() { + const [isAutonomous, setIsAutonomous] = useState(true); + const [speed, setSpeed] = useState([50]); + const [activeTask, setActiveTask] = useState(null); + + const handleStartTask = (taskName: string) => { + setActiveTask(taskName); + toast.success(`Tâche démarrée : ${taskName}`); + }; + + const handleStopTask = () => { + setActiveTask(null); + toast.error('Tâche interrompue.'); + }; + + const handleEmergencyStop = () => { + setIsAutonomous(false); + setActiveTask(null); + toast.error("ARRÊT D'URGENCE ACTIVÉ ! Passage en mode manuel.", { + style: { backgroundColor: '#ef4444', color: 'white', border: 'none' }, + }); + }; + + return ( +
+ + +
+ +
+
+ + +
+ +
+ +
+ +
+ +
+
+
+ ); +} From a5903ce957143ebc016117e240adcea9fb2e8b27 Mon Sep 17 00:00:00 2001 From: KelianHalleray Date: Fri, 22 May 2026 09:46:32 +0200 Subject: [PATCH 4/8] fix(teleop): add looping requests for teleop --- src/components/dashboard/dashboard.tsx | 97 ++++++++++++++++++--- src/components/dashboard/manual-control.tsx | 79 ++++++----------- 2 files changed, 112 insertions(+), 64 deletions(-) diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index a26b89a..86db334 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -18,6 +18,8 @@ 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]); @@ -25,9 +27,23 @@ export function Dashboard() { const [linearX, setLinearX] = useState(0); const [linearY, setLinearY] = useState(0); const [angular, setAngular] = useState(0); + const wsRef = useRef(null); + const currentCmdRef = useRef({ linearX: 0, linearY: 0, angular: 0 }); + const sendIntervalRef = useRef(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]); - // Initialiser la connexion WebSocket + // Connexion WebSocket useEffect(() => { const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.hostname}:8765`; @@ -64,6 +80,62 @@ export function Dashboard() { }; }, []); + // 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 handleStartTask = (taskName: string) => { setActiveTask(taskName); toast.success(`Tâche démarrée : ${taskName}`); @@ -77,29 +149,26 @@ 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 ) => { - if (!isAutonomous && wsRef.current?.readyState === WebSocket.OPEN) { - const speedFactor = (speed[0] ?? 50) / 100; - const message = { - type: 'teleop.move', - data: { - linear_x: newLinearX * speedFactor, - linear_y: newLinearY * speedFactor, - angular_z: newAngular * speedFactor, - }, - }; - wsRef.current.send(JSON.stringify(message)); - console.log('[WS] teleop.move envoyé:', message.data); - } + currentCmdRef.current = { + linearX: newLinearX, + linearY: newLinearY, + angular: newAngular, + }; }; return ( diff --git a/src/components/dashboard/manual-control.tsx b/src/components/dashboard/manual-control.tsx index a84be8c..694b25b 100644 --- a/src/components/dashboard/manual-control.tsx +++ b/src/components/dashboard/manual-control.tsx @@ -43,59 +43,47 @@ export function ManualControl({ setAngular, onTeleopMove, }: Readonly) { - const handleDirectionButton = ( + const applyDirection = ( direction: 'up' | 'down' | 'left' | 'right' | 'stop' ) => { if (isAutonomous) return; let newLinearX = 0; - let newLinearY = 0; + const newLinearY = 0; let newAngular = 0; switch (direction) { case 'up': newLinearX = 1; - newLinearY = 0; - newAngular = 0; break; case 'down': newLinearX = -1; - newLinearY = 0; - newAngular = 0; break; case 'left': - newLinearX = 0; - newLinearY = 1; newAngular = 1; break; case 'right': - newLinearX = 0; - newLinearY = -1; newAngular = -1; break; case 'stop': - newLinearX = 0; - newLinearY = 0; - newAngular = 0; + // tout à zéro break; } - // Mettre à jour les états pour l'affichage setLinearX?.(newLinearX); setLinearY?.(newLinearY); setAngular?.(newAngular); - - // Envoyer directement les valeurs au handler onTeleopMove?.(newLinearX, newLinearY, newAngular); }; - const handleMouseUp = () => { - // Arrêter le robot quand on relâche + const release = () => { + if (isAutonomous) return; setLinearX?.(0); setLinearY?.(0); setAngular?.(0); onTeleopMove?.(0, 0, 0); }; + return ( @@ -134,43 +122,31 @@ export function ManualControl({
- {!isAutonomous && ( -
-
- Linear X:{' '} - {linearX.toFixed(2)} -
-
- Linear Y:{' '} - {linearY.toFixed(2)} -
-
- Angular:{' '} - {angular.toFixed(2)} -
-
- )} -
+ @@ -178,9 +154,7 @@ export function ManualControl({ variant="secondary" className="h-12 w-full" disabled={isAutonomous} - onMouseDown={() => handleDirectionButton('stop')} - onMouseUp={handleMouseUp} - onMouseLeave={handleMouseUp} + onClick={() => applyDirection('stop')} > @@ -188,20 +162,25 @@ export function ManualControl({ variant="secondary" className="h-12 w-full" disabled={isAutonomous} - onMouseDown={() => handleDirectionButton('right')} - onMouseUp={handleMouseUp} - onMouseLeave={handleMouseUp} + onMouseDown={() => applyDirection('right')} + onMouseUp={release} + onMouseLeave={release} + onTouchStart={() => applyDirection('right')} + onTouchEnd={release} > +
From 4063aaf840990c5a32b2dcd9cc8e7ede23d90580 Mon Sep 17 00:00:00 2001 From: KelianHalleray Date: Fri, 22 May 2026 09:51:29 +0200 Subject: [PATCH 5/8] fix(teleop): resolve conflicts --- src/components/dashboard/dashboard.tsx | 2 +- src/components/tablet/tablet.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index 86db334..d8e7048 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Toaster, toast } from 'sonner'; -import { Header } from '@/components/dashboard/Header'; +import { Header } from '@/components/dashboard/header'; import { CameraFeed } from '@/components/dashboard/camera-feed'; import { ManualControl } from '@/components/dashboard/manual-control'; import { TaskManager } from '@/components/dashboard/task-manager'; diff --git a/src/components/tablet/tablet.tsx b/src/components/tablet/tablet.tsx index 0afbb4d..992ac6e 100644 --- a/src/components/tablet/tablet.tsx +++ b/src/components/tablet/tablet.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Toaster, toast } from 'sonner'; -import { Header } from '@/components/dashboard/Header'; +import { Header } from '@/components/dashboard/header'; import { CameraFeed } from '@/components/dashboard/camera-feed'; import { ManualControl } from '@/components/dashboard/manual-control'; import { TaskManager } from '@/components/dashboard/task-manager'; From bfe7b549de0e523cd34cc417ff426a3c7dfff28d Mon Sep 17 00:00:00 2001 From: KelianHalleray Date: Fri, 22 May 2026 10:01:45 +0200 Subject: [PATCH 6/8] fix: resolve lint --- src/components/dashboard/manual-control.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/dashboard/manual-control.tsx b/src/components/dashboard/manual-control.tsx index 694b25b..f313daf 100644 --- a/src/components/dashboard/manual-control.tsx +++ b/src/components/dashboard/manual-control.tsx @@ -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 { @@ -35,11 +34,8 @@ export function ManualControl({ speed, setSpeed, onEmergencyStop, - linearX = 0, setLinearX, - linearY = 0, setLinearY, - angular = 0, setAngular, onTeleopMove, }: Readonly) { From bbb9d2a5efdf2df8cea1766d5082f3591e5799b7 Mon Sep 17 00:00:00 2001 From: KelianHalleray Date: Fri, 22 May 2026 10:12:04 +0200 Subject: [PATCH 7/8] fix: resolve error --- src/components/dashboard/dashboard.tsx | 2 +- .../dashboard/{Header.tsx => header.tsx} | 0 src/components/tablet/ArmStatus.tsx | 116 ------------------ src/components/tablet/TabletDashboard.tsx | 12 -- src/components/tablet/tablet.tsx | 12 +- 5 files changed, 12 insertions(+), 130 deletions(-) rename src/components/dashboard/{Header.tsx => header.tsx} (100%) delete mode 100644 src/components/tablet/ArmStatus.tsx diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index a62057b..d1f16cf 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react'; import { Toaster, toast } from 'sonner'; -import { Header } from '@/components/dashboard/Header'; +import { Header } from '@/components/dashboard/header'; import { CameraFeed } from '@/components/dashboard/camera-feed'; import { ManualControl } from '@/components/dashboard/manual-control'; import { TaskManager } from '@/components/dashboard/task-manager'; diff --git a/src/components/dashboard/Header.tsx b/src/components/dashboard/header.tsx similarity index 100% rename from src/components/dashboard/Header.tsx rename to src/components/dashboard/header.tsx diff --git a/src/components/tablet/ArmStatus.tsx b/src/components/tablet/ArmStatus.tsx deleted file mode 100644 index e11c560..0000000 --- a/src/components/tablet/ArmStatus.tsx +++ /dev/null @@ -1,116 +0,0 @@ -'use client'; - -import { Move, Hand, AlertCircle, Wrench } from 'lucide-react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; - -interface ArmJoints { - shoulder: number; - elbow: number; - wrist: number; - gripper: number; -} - -interface ArmStatusProps { - connected: boolean; - joints: ArmJoints; - gripperState: 'open' | 'closed' | 'moving'; - currentAction?: string; - errorMessage?: string; -} - -export function ArmStatus({ - connected, - joints, - gripperState, - currentAction, - errorMessage, -}: ArmStatusProps) { - if (!connected) { - return ( - - - - - Bras robotique - - - -
- - Bras non connecté ou hors service -
-
-
- ); - } - - const gripperColor = { - open: 'text-green-500', - closed: 'text-blue-500', - moving: 'text-yellow-500', - }; - - const gripperLabel = { - open: 'Ouvert', - closed: 'Fermé', - moving: 'En mouvement', - }; - - return ( - - - - - - Bras robotique - - - Actif - - - - - {currentAction && ( -
- - - Action : {currentAction} - -
- )} - -
-
- Épaule : - {joints.shoulder}° -
-
- Coude : - {joints.elbow}° -
-
- Poignet : - {joints.wrist}° -
-
- Préhenseur : - - - {gripperLabel[gripperState]} ({joints.gripper}%) - -
-
- - {errorMessage && ( -
- - {errorMessage} -
- )} -
-
- ); -} diff --git a/src/components/tablet/TabletDashboard.tsx b/src/components/tablet/TabletDashboard.tsx index f6aa6cf..db4605c 100644 --- a/src/components/tablet/TabletDashboard.tsx +++ b/src/components/tablet/TabletDashboard.tsx @@ -4,7 +4,6 @@ import { useState, useEffect } from 'react'; import { TabletSmartphone } from 'lucide-react'; import { RobotStatus } from './RobotStatus'; import { MissionCard } from './MissionCard'; -import { ArmStatus } from './ArmStatus'; import { AlertBanner } from './AlertBanner'; type Mode = 'manual' | 'auto' | 'maintenance' | 'stop'; @@ -26,7 +25,6 @@ const INITIAL_DATA = { arm: { connected: true, joints: { shoulder: 0, elbow: 45, wrist: 0, gripper: 0 }, - gripperState: 'closed' as const, currentAction: 'Repos', errorMessage: undefined as string | undefined, }, @@ -62,9 +60,6 @@ const generateRandomData = (): typeof INITIAL_DATA => ({ wrist: Math.floor(Math.random() * 180) - 90, gripper: Math.floor(Math.random() * 100), }, - gripperState: (['open', 'closed', 'moving'] as const)[ - Math.floor(Math.random() * 3) - ], currentAction: ['Repos', 'Préhension', 'Rangement', 'Pousse'][ Math.floor(Math.random() * 4) ], @@ -142,13 +137,6 @@ export function TabletDashboard() { totalSteps={data.mission.totalSteps} errorMessage={data.mission.error} /> -
diff --git a/src/components/tablet/tablet.tsx b/src/components/tablet/tablet.tsx index 992ac6e..5c55cb6 100644 --- a/src/components/tablet/tablet.tsx +++ b/src/components/tablet/tablet.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { Toaster, toast } from 'sonner'; import { Header } from '@/components/dashboard/header'; import { CameraFeed } from '@/components/dashboard/camera-feed'; @@ -8,6 +8,16 @@ import { ManualControl } from '@/components/dashboard/manual-control'; import { TaskManager } from '@/components/dashboard/task-manager'; import { MetricsAndAlerts } from '@/components/dashboard/metrics-and-alerts'; +const vitalsData = [ + { time: '10:00', heartRate: 72, battery: 95 }, + { time: '10:30', heartRate: 75, battery: 90 }, + { time: '11:00', heartRate: 80, battery: 85 }, + { time: '11:30', heartRate: 76, battery: 78 }, + { time: '12:00', heartRate: 74, battery: 72 }, + { time: '12:30', heartRate: 79, battery: 65 }, + { time: '13:00', heartRate: 75, battery: 60 }, +]; + export function Tablet() { const [isAutonomous, setIsAutonomous] = useState(true); const [speed, setSpeed] = useState([50]); From 1029bdcc519b9f9a095af430db4591dbcb2c1602 Mon Sep 17 00:00:00 2001 From: KelianHalleray Date: Fri, 22 May 2026 10:15:07 +0200 Subject: [PATCH 8/8] fix: remove unused var --- src/components/tablet/TabletDashboard.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/tablet/TabletDashboard.tsx b/src/components/tablet/TabletDashboard.tsx index db4605c..a77a694 100644 --- a/src/components/tablet/TabletDashboard.tsx +++ b/src/components/tablet/TabletDashboard.tsx @@ -108,14 +108,6 @@ export function TabletDashboard() { ); } - const armData = data.arm || { - connected: false, - joints: { shoulder: 0, elbow: 0, wrist: 0, gripper: 0 }, - gripperState: 'closed' as const, - currentAction: 'Inconnu', - errorMessage: undefined, - }; - return (