diff --git a/App.tsx b/App.tsx index f365636..eb157a5 100644 --- a/App.tsx +++ b/App.tsx @@ -8,6 +8,7 @@ import AgvControls from './components/AgvControls'; import BmsPanel from './components/BmsPanel'; import AcsControls from './components/AcsControls'; import AgvStatusPanel from './components/AgvStatusPanel'; +import AgvAutoRunControls from './components/AgvAutoRunControls'; import SystemLogPanel from './components/SystemLogPanel'; import { SerialPortHandler } from './services/serialService'; @@ -40,1231 +41,1261 @@ const calculateAcsCrc16 = (data: number[] | Uint8Array): number => { }; const App: React.FC = () => { - // --- State --- - const [activeTool, setActiveTool] = useState(ToolType.SELECT); - - // Map Data - const [mapData, setMapData] = useState(INITIAL_MAP); + // --- State --- + const [activeTool, setActiveTool] = useState(ToolType.SELECT); - // AGV Logic State - const [agvState, setAgvState] = useState({ - x: 100, - y: 100, - rotation: 0, - targetRotation: null, - speed: MAX_SPEED, - liftHeight: 0, - motionState: AgvMotionState.IDLE, - runConfig: { - direction: 'FWD', - branch: 'STRAIGHT', - speedLevel: 'M' - }, - error: null, - sensorStatus: '1', // Default per C# DevAGV.cs - detectedRfid: null, - sensorLineFront: false, - sensorLineRear: false, - sensorMark: false, - - // Lift & Magnet - magnetOn: false, - liftStatus: 'IDLE', - lidarEnabled: true, // Default ON + // Map Data + const [mapData, setMapData] = useState(INITIAL_MAP); - // Protocol Flags Initial State - system0: 0, - system1: (1 << SystemFlag1.agv_stop), // Default STOP state - errorFlags: 0, - signalFlags: 0, // Will be updated by useEffect + // AGV Logic State + const [agvState, setAgvState] = useState({ + x: 100, + y: 100, + rotation: 0, + targetRotation: null, + speed: MAX_SPEED, + liftHeight: 0, + motionState: AgvMotionState.IDLE, + runConfig: { + direction: 'FWD', + branch: 'STRAIGHT', + speedLevel: 'M' + }, + error: null, + sensorStatus: '1', // Default per C# DevAGV.cs + detectedRfid: null, + sensorLineFront: false, + sensorLineRear: false, + sensorMark: false, - batteryLevel: 95, - maxCapacity: 100, // 100Ah - batteryTemp: 25, - cellVoltages: Array(8).fill(3.2), - }); + // Lift & Magnet + magnetOn: false, + liftStatus: 'IDLE', + lidarEnabled: true, // Default ON - // Ref to hold latest state for Serial Callbacks (prevents stale closures) - const agvStateRef = useRef(agvState); - useEffect(() => { - agvStateRef.current = agvState; - }, [agvState]); + // Protocol Flags Initial State + system0: 0, + system1: (1 << SystemFlag1.agv_stop), // Default STOP state + errorFlags: 0, + signalFlags: 0, // Will be updated by useEffect - // Logs - const [logs, setLogs] = useState([]); - - // Auto-clear logic: Keep last 500 lines - const addLog = useCallback((source: 'AGV' | 'BMS' | 'ACS' | 'SYSTEM', type: 'INFO' | 'RX' | 'TX' | 'ERROR', message: string) => { - setLogs(prev => [...prev.slice(-499), { - id: crypto.randomUUID(), - timestamp: new Date().toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }), - source, - type, - message - }]); - }, []); - - const clearLogs = useCallback((source: 'AGV' | 'BMS' | 'ACS' | 'SYSTEM') => { - setLogs(prev => prev.filter(l => l.source !== source)); - }, []); - - // Serial Handlers - const agvSerialRef = useRef(null); - const bmsSerialRef = useRef(null); - const acsSerialRef = useRef(null); - - const [agvConnected, setAgvConnected] = useState(false); - const [bmsConnected, setBmsConnected] = useState(false); - const [acsConnected, setAcsConnected] = useState(false); - - const [agvPortInfo, setAgvPortInfo] = useState(null); - const [bmsPortInfo, setBmsPortInfo] = useState(null); - const [acsPortInfo, setAcsPortInfo] = useState(null); - - // Serial Configuration - const [agvBaudRate, setAgvBaudRate] = useState(57600); - const [bmsBaudRate, setBmsBaudRate] = useState(9600); - const [acsBaudRate, setAcsBaudRate] = useState(9600); - - // --- Helpers to Set/Clear Bits --- - const setAgvBit = (field: 'system0' | 'system1' | 'errorFlags' | 'signalFlags', bit: number, value: boolean) => { - setAgvState(prev => { - let current = prev[field]; - if (value) current |= (1 << bit); - else current &= ~(1 << bit); - - if (current === prev[field]) return prev; - return { ...prev, [field]: current }; + batteryLevel: 95, + maxCapacity: 100, // 100Ah + batteryTemp: 25, + cellVoltages: Array(8).fill(3.2), }); - }; - // --- AGV Protocol Parser --- - const agvBufferRef = useRef([]); - - // Format: [STX]ACK[CMD]**[ETX] - const sendAck = useCallback((commandToAck: string) => { - if (agvSerialRef.current) { - const encoder = new TextEncoder(); - const cmdBytes = encoder.encode("ACK"); - const valBytes = encoder.encode(commandToAck); - - const payload = new Uint8Array(1 + cmdBytes.length + valBytes.length + 2 + 1); - payload[0] = 0x02; // STX - payload.set(cmdBytes, 1); - payload.set(valBytes, 1 + cmdBytes.length); - payload[payload.length - 3] = 0x2A; // '*' - payload[payload.length - 2] = 0x2A; // '*' - payload[payload.length - 1] = 0x03; // ETX + // Ref to hold latest state for Serial Callbacks (prevents stale closures) + const agvStateRef = useRef(agvState); + useEffect(() => { + agvStateRef.current = agvState; + }, [agvState]); - agvSerialRef.current.send(payload); - addLog('AGV', 'TX', `ACK ${commandToAck}`); - } - }, [addLog]); + // Logs + const [logs, setLogs] = useState([]); - // Tag Transmission Logic - const sendTag = useCallback((tagId: string) => { - if (agvSerialRef.current) { - const paddedId = tagId.padStart(6, '0').slice(-6); - const encoder = new TextEncoder(); - const cmdBytes = encoder.encode("TAG"); - const valBytes = encoder.encode(paddedId); - - const payload = new Uint8Array(1 + cmdBytes.length + valBytes.length + 2 + 1); - let offset = 0; - payload[offset++] = 0x02; // STX - payload.set(cmdBytes, offset); offset += cmdBytes.length; - payload.set(valBytes, offset); offset += valBytes.length; - payload[offset++] = 0x2A; // '*' - payload[offset++] = 0x2A; // '*' - payload[offset++] = 0x03; // ETX + // Auto-clear logic: Keep last 500 lines + const addLog = useCallback((source: 'AGV' | 'BMS' | 'ACS' | 'SYSTEM', type: 'INFO' | 'RX' | 'TX' | 'ERROR', message: string) => { + setLogs(prev => [...prev.slice(-499), { + id: crypto.randomUUID(), + timestamp: new Date().toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }), + source, + type, + message + }]); + }, []); - agvSerialRef.current.send(payload); - addLog('AGV', 'TX', `TAG ${paddedId}`); - } - }, [addLog]); + const clearLogs = useCallback((source: 'AGV' | 'BMS' | 'ACS' | 'SYSTEM') => { + setLogs(prev => prev.filter(l => l.source !== source)); + }, []); - const handleAgvData = useCallback((data: Uint8Array) => { - for(let i=0; i(null); + const bmsSerialRef = useRef(null); + const acsSerialRef = useRef(null); - const buf = agvBufferRef.current; - while(true) { - const stxIdx = buf.indexOf(0x02); - if (stxIdx === -1) { - agvBufferRef.current = []; - return; + const [agvConnected, setAgvConnected] = useState(false); + const [bmsConnected, setBmsConnected] = useState(false); + const [acsConnected, setAcsConnected] = useState(false); + + const [agvPortInfo, setAgvPortInfo] = useState(null); + const [bmsPortInfo, setBmsPortInfo] = useState(null); + const [acsPortInfo, setAcsPortInfo] = useState(null); + + // Serial Configuration + const [agvBaudRate, setAgvBaudRate] = useState(57600); + const [bmsBaudRate, setBmsBaudRate] = useState(9600); + const [acsBaudRate, setAcsBaudRate] = useState(9600); + + // --- Helpers to Set/Clear Bits --- + const setAgvBit = (field: 'system0' | 'system1' | 'errorFlags' | 'signalFlags', bit: number, value: boolean) => { + setAgvState(prev => { + let current = prev[field]; + if (value) current |= (1 << bit); + else current &= ~(1 << bit); + + if (current === prev[field]) return prev; + return { ...prev, [field]: current }; + }); + }; + + // --- AGV Protocol Parser --- + const agvBufferRef = useRef([]); + + // Format: [STX]ACK[CMD]**[ETX] + const sendAck = useCallback((commandToAck: string) => { + if (agvSerialRef.current) { + const encoder = new TextEncoder(); + const cmdBytes = encoder.encode("ACK"); + const valBytes = encoder.encode(commandToAck); + + const payload = new Uint8Array(1 + cmdBytes.length + valBytes.length + 2 + 1); + payload[0] = 0x02; // STX + payload.set(cmdBytes, 1); + payload.set(valBytes, 1 + cmdBytes.length); + payload[payload.length - 3] = 0x2A; // '*' + payload[payload.length - 2] = 0x2A; // '*' + payload[payload.length - 1] = 0x03; // ETX + + agvSerialRef.current.send(payload); + addLog('AGV', 'TX', `ACK ${commandToAck}`); } - - if (stxIdx > 0) { - buf.splice(0, stxIdx); + }, [addLog]); + + // Tag Transmission Logic + const sendTag = useCallback((tagId: string) => { + if (agvSerialRef.current) { + const paddedId = tagId.padStart(6, '0').slice(-6); + const encoder = new TextEncoder(); + const cmdBytes = encoder.encode("TAG"); + const valBytes = encoder.encode(paddedId); + + const payload = new Uint8Array(1 + cmdBytes.length + valBytes.length + 2 + 1); + let offset = 0; + payload[offset++] = 0x02; // STX + payload.set(cmdBytes, offset); offset += cmdBytes.length; + payload.set(valBytes, offset); offset += valBytes.length; + payload[offset++] = 0x2A; // '*' + payload[offset++] = 0x2A; // '*' + payload[offset++] = 0x03; // ETX + + agvSerialRef.current.send(payload); + addLog('AGV', 'TX', `TAG ${paddedId}`); + } + }, [addLog]); + + const handleAgvData = useCallback((data: Uint8Array) => { + for (let i = 0; i < data.length; i++) { + agvBufferRef.current.push(data[i]); } - const etxIdx = buf.indexOf(0x03); - if (etxIdx === -1) { - return; - } - - const frameBytes = buf.splice(0, etxIdx + 1); - processAgvFrame(frameBytes); - } - }, []); - - const processAgvFrame = (frame: number[]) => { - if (frame.length < 7) return; - - const textDecoder = new TextDecoder(); - const cmd = textDecoder.decode(new Uint8Array(frame.slice(1, 4))); - const dataStr = textDecoder.decode(new Uint8Array(frame.slice(4, frame.length - 3))).trim(); - - addLog('AGV', 'RX', `${cmd} ${dataStr}`); - - switch (cmd) { - case 'CRN': // Run Command (Auto Run) - if (dataStr.length >= 4) { - const dirChar = dataStr[0].toUpperCase(); - const bunki = dataStr[1]; - const speed = dataStr[2]; - const sensor = dataStr[3]; - - const isBack = dirChar === 'B' || dirChar === 'R'; - const direction = isBack ? 'BWD' : 'FWD'; - - setAgvState(s => { - const newBranch = (bunki === 'L' ? 'LEFT' : bunki === 'R' ? 'RIGHT' : bunki === 'S' ? 'STRAIGHT' : s.runConfig.branch); - const newSpeed = (['L','M','H'].includes(speed) ? speed : s.runConfig.speedLevel) as any; - - // Treat '0' as placeholder for sensor if we want to preserve CBR logic, - // BUT allow manual toggle via UI to override later. - // Here, we assume CRN sending '0' shouldn't turn off sensor if user set it via CBR. - const newSensor = (sensor === '0') ? s.sensorStatus : sensor; - const newLidar = newSensor !== '0'; - - return { - ...s, - sensorStatus: newSensor, - lidarEnabled: newLidar, - motionState: AgvMotionState.RUNNING, - runConfig: { - direction: direction, - branch: newBranch, - speedLevel: newSpeed - } - }; - }); - addLog('AGV', 'INFO', `Run: Dir=${direction} Branch=${bunki} Speed=${speed} Sensor=${sensor}`); - } else { - setAgvState(s => ({ ...s, motionState: AgvMotionState.RUNNING })); + const buf = agvBufferRef.current; + while (true) { + const stxIdx = buf.indexOf(0x02); + if (stxIdx === -1) { + agvBufferRef.current = []; + return; } - // Update Flags - setAgvBit('system1', SystemFlag1.agv_stop, false); - setAgvBit('system1', SystemFlag1.agv_run, true); - break; - case 'CST': // Stop Command - if (dataStr.length > 0 && dataStr.charAt(0).toUpperCase() === 'M') { - // Mark Stop Command (Drive to next mark) - setAgvState(s => ({ - ...s, - motionState: AgvMotionState.MARK_STOPPING, - runConfig: { - ...s.runConfig, - speedLevel: 'L' // Force low speed for precision - } - })); - // In Mark Stopping mode, AGV is still technically running until it hits the mark + if (stxIdx > 0) { + buf.splice(0, stxIdx); + } + + const etxIdx = buf.indexOf(0x03); + if (etxIdx === -1) { + return; + } + + const frameBytes = buf.splice(0, etxIdx + 1); + processAgvFrame(frameBytes); + } + }, []); + + const processAgvFrame = (frame: number[]) => { + if (frame.length < 7) return; + + const textDecoder = new TextDecoder(); + const cmd = textDecoder.decode(new Uint8Array(frame.slice(1, 4))); + const dataStr = textDecoder.decode(new Uint8Array(frame.slice(4, frame.length - 3))).trim(); + + addLog('AGV', 'RX', `${cmd} ${dataStr}`); + + switch (cmd) { + case 'CRN': // Run Command (Auto Run) + if (dataStr.length >= 4) { + const dirChar = dataStr[0].toUpperCase(); + const bunki = dataStr[1]; + const speed = dataStr[2]; + const sensor = dataStr[3]; + + const isBack = dirChar === 'B' || dirChar === 'R'; + const direction = isBack ? 'BWD' : 'FWD'; + + setAgvState(s => { + const newBranch = (bunki === 'L' ? 'LEFT' : bunki === 'R' ? 'RIGHT' : bunki === 'S' ? 'STRAIGHT' : s.runConfig.branch); + const newSpeed = (['L', 'M', 'H'].includes(speed) ? speed : s.runConfig.speedLevel) as any; + + // Treat '0' as placeholder for sensor if we want to preserve CBR logic, + // BUT allow manual toggle via UI to override later. + // Here, we assume CRN sending '0' shouldn't turn off sensor if user set it via CBR. + const newSensor = (sensor === '0') ? s.sensorStatus : sensor; + const newLidar = newSensor !== '0'; + + return { + ...s, + sensorStatus: newSensor, + lidarEnabled: newLidar, + motionState: AgvMotionState.RUNNING, + runConfig: { + direction: direction, + branch: newBranch, + speedLevel: newSpeed + } + }; + }); + addLog('AGV', 'INFO', `Run: Dir=${direction} Branch=${bunki} Speed=${speed} Sensor=${sensor}`); + } else { + setAgvState(s => ({ ...s, motionState: AgvMotionState.RUNNING })); + } + // Update Flags setAgvBit('system1', SystemFlag1.agv_stop, false); setAgvBit('system1', SystemFlag1.agv_run, true); - addLog('AGV', 'INFO', 'CMD: Mark Stop Mode Started'); - } else { - // Normal Stop + break; + + case 'CST': // Stop Command + if (dataStr.length > 0 && dataStr.charAt(0).toUpperCase() === 'M') { + // Mark Stop Command (Drive to next mark) + setAgvState(s => ({ + ...s, + motionState: AgvMotionState.MARK_STOPPING, + runConfig: { + ...s.runConfig, + speedLevel: 'L' // Force low speed for precision + } + })); + // In Mark Stopping mode, AGV is still technically running until it hits the mark + setAgvBit('system1', SystemFlag1.agv_stop, false); + setAgvBit('system1', SystemFlag1.agv_run, true); + addLog('AGV', 'INFO', 'CMD: Mark Stop Mode Started'); + } else { + // Normal Stop + setAgvBit('system1', SystemFlag1.agv_run, false); + setAgvBit('system1', SystemFlag1.agv_stop, true); + setAgvState(s => ({ ...s, motionState: AgvMotionState.IDLE })); + } + break; + + case 'CBR': // Branch + if (dataStr.length >= 3) { + const dirChar = dataStr[0].toUpperCase(); + const bunki = dataStr[1]; + const speed = dataStr[2]; + // Default to '1' (ON) if not provided, assuming CBR implies a valid path + const sensor = dataStr.length > 3 ? dataStr[3] : '1'; + + const isBack = dirChar === 'B' || dirChar === 'R'; + const direction = isBack ? 'BWD' : 'FWD'; + + setAgvState(s => { + // Logic: Update if specific char provided, else keep existing + const newBranch = (bunki === 'L' ? 'LEFT' : bunki === 'R' ? 'RIGHT' : bunki === 'S' ? 'STRAIGHT' : s.runConfig.branch); + const newSpeed = (['L', 'M', 'H'].includes(speed) ? speed : s.runConfig.speedLevel) as any; + + const newLidar = sensor !== '0'; + + return { + ...s, + sensorStatus: sensor, + lidarEnabled: newLidar, + runConfig: { + direction: direction, + branch: newBranch, + speedLevel: newSpeed + } + }; + }); + addLog('AGV', 'INFO', `Branch Set: Dir=${direction} Branch=${bunki} Speed=${speed} Sensor=${sensor}`); + } + break; + + case 'CRT': // Manual Control (Remote / Jog) + const op = dataStr.charAt(0).toUpperCase(); + let manualMotion = AgvMotionState.IDLE; + let targetDir: 'FWD' | 'BWD' = 'FWD'; + + if (op === 'F') { + manualMotion = AgvMotionState.FORWARD; + targetDir = 'FWD'; + } + else if (op === 'B') { + manualMotion = AgvMotionState.BACKWARD; + targetDir = 'BWD'; + } + else if (op === 'L') manualMotion = AgvMotionState.TURN_LEFT; + else if (op === 'R') manualMotion = AgvMotionState.TURN_RIGHT; + else if (op === 'S') manualMotion = AgvMotionState.IDLE; + + setAgvState(s => ({ + ...s, + motionState: manualMotion, + runConfig: { + ...s.runConfig, + direction: targetDir + } + })); + + const isMoving = manualMotion !== AgvMotionState.IDLE; + setAgvBit('system1', SystemFlag1.agv_stop, !isMoving); + setAgvBit('system1', SystemFlag1.agv_run, isMoving); + + sendAck(cmd); + break; + + case 'SFR': // Reset Errors + setAgvState(s => ({ ...s, error: null, errorFlags: 0 })); setAgvBit('system1', SystemFlag1.agv_run, false); setAgvBit('system1', SystemFlag1.agv_stop, true); - setAgvState(s => ({ ...s, motionState: AgvMotionState.IDLE })); - } - break; - - case 'CBR': // Branch - if (dataStr.length >= 3) { - const dirChar = dataStr[0].toUpperCase(); - const bunki = dataStr[1]; - const speed = dataStr[2]; - // Default to '1' (ON) if not provided, assuming CBR implies a valid path - const sensor = dataStr.length > 3 ? dataStr[3] : '1'; + break; - const isBack = dirChar === 'B' || dirChar === 'R'; - const direction = isBack ? 'BWD' : 'FWD'; - - setAgvState(s => { - // Logic: Update if specific char provided, else keep existing - const newBranch = (bunki === 'L' ? 'LEFT' : bunki === 'R' ? 'RIGHT' : bunki === 'S' ? 'STRAIGHT' : s.runConfig.branch); - const newSpeed = (['L','M','H'].includes(speed) ? speed : s.runConfig.speedLevel) as any; - - const newLidar = sensor !== '0'; - - return { - ...s, - sensorStatus: sensor, - lidarEnabled: newLidar, - runConfig: { - direction: direction, - branch: newBranch, - speedLevel: newSpeed - } - }; - }); - addLog('AGV', 'INFO', `Branch Set: Dir=${direction} Branch=${bunki} Speed=${speed} Sensor=${sensor}`); - } - break; - - case 'CRT': // Manual Control (Remote / Jog) - const op = dataStr.charAt(0).toUpperCase(); - let manualMotion = AgvMotionState.IDLE; - let targetDir: 'FWD' | 'BWD' = 'FWD'; - - if (op === 'F') { - manualMotion = AgvMotionState.FORWARD; - targetDir = 'FWD'; - } - else if (op === 'B') { - manualMotion = AgvMotionState.BACKWARD; - targetDir = 'BWD'; - } - else if (op === 'L') manualMotion = AgvMotionState.TURN_LEFT; - else if (op === 'R') manualMotion = AgvMotionState.TURN_RIGHT; - else if (op === 'S') manualMotion = AgvMotionState.IDLE; - - setAgvState(s => ({ - ...s, - motionState: manualMotion, - runConfig: { - ...s.runConfig, - direction: targetDir + case 'CBT': // Charging Command + if (dataStr.length > 4) { + const chargeCmd = dataStr[4]; + const isCharging = chargeCmd === 'I'; + setAgvBit('system1', SystemFlag1.Battery_charging, isCharging); } - })); - - const isMoving = manualMotion !== AgvMotionState.IDLE; - setAgvBit('system1', SystemFlag1.agv_stop, !isMoving); - setAgvBit('system1', SystemFlag1.agv_run, isMoving); - - sendAck(cmd); - break; + break; - case 'SFR': // Reset Errors - setAgvState(s => ({ ...s, error: null, errorFlags: 0 })); - setAgvBit('system1', SystemFlag1.agv_run, false); - setAgvBit('system1', SystemFlag1.agv_stop, true); - break; - - case 'CBT': // Charging Command - if (dataStr.length > 4) { - const chargeCmd = dataStr[4]; - const isCharging = chargeCmd === 'I'; - setAgvBit('system1', SystemFlag1.Battery_charging, isCharging); - } - break; - - case 'CLF': // Lift / Magnet Control - const subCmd = dataStr.trim().toUpperCase(); - if (subCmd === 'UP0000') { - setAgvState(s => ({ ...s, liftStatus: 'UP' })); - } else if (subCmd === 'DN0000') { - setAgvState(s => ({ ...s, liftStatus: 'DOWN' })); - } else if (subCmd === 'STP000') { - setAgvState(s => ({ ...s, liftStatus: 'IDLE' })); - } else if (subCmd === 'ON0000') { - setAgvState(s => ({ ...s, magnetOn: true })); - } else if (subCmd === 'OFF000') { - setAgvState(s => ({ ...s, magnetOn: false })); - } - sendAck(cmd); - break; + case 'CLF': // Lift / Magnet Control + const subCmd = dataStr.trim().toUpperCase(); + if (subCmd === 'UP0000') { + setAgvState(s => ({ ...s, liftStatus: 'UP' })); + } else if (subCmd === 'DN0000') { + setAgvState(s => ({ ...s, liftStatus: 'DOWN' })); + } else if (subCmd === 'STP000') { + setAgvState(s => ({ ...s, liftStatus: 'IDLE' })); + } else if (subCmd === 'ON0000') { + setAgvState(s => ({ ...s, magnetOn: true })); + } else if (subCmd === 'OFF000') { + setAgvState(s => ({ ...s, magnetOn: false })); + } + sendAck(cmd); + break; - case 'CTL': // Turn Left 180 - handleTurn180('LEFT'); - sendAck(cmd); - break; + case 'CTL': // Turn Left 180 + handleTurn180('LEFT'); + sendAck(cmd); + break; - case 'CTR': // Turn Right 180 - handleTurn180('RIGHT'); - sendAck(cmd); - break; + case 'CTR': // Turn Right 180 + handleTurn180('RIGHT'); + sendAck(cmd); + break; - case 'SSH': case 'SSM': case 'SSL': case 'SSS': case 'SHS': case 'SLS': case 'SPK': case 'SPM': - case 'SPL': case 'SPS': case 'SIK': case 'SIM': case 'SIH': case 'SIL': case 'SIS': case 'SDK': - case 'SDW': case 'SDL': case 'SDS': case 'SRS': case 'SCK': case 'SSK': case 'STT': case 'SSI': - case 'SMD': case 'SSC': case 'SPN': case 'SPH': case 'SCH': case 'SDH': case 'SDM': case 'SLB': - case 'SGS': - sendAck(cmd); - break; - - case 'ACK': - break; - } - }; + case 'SSH': case 'SSM': case 'SSL': case 'SSS': case 'SHS': case 'SLS': case 'SPK': case 'SPM': + case 'SPL': case 'SPS': case 'SIK': case 'SIM': case 'SIH': case 'SIL': case 'SIS': case 'SDK': + case 'SDW': case 'SDL': case 'SDS': case 'SRS': case 'SCK': case 'SSK': case 'STT': case 'SSI': + case 'SMD': case 'SSC': case 'SPN': case 'SPH': case 'SCH': case 'SDH': case 'SDM': case 'SLB': + case 'SGS': + sendAck(cmd); + break; - const handleTurn180 = (direction: 'LEFT' | 'RIGHT') => { - setAgvState(s => ({ - ...s, - motionState: direction === 'LEFT' ? AgvMotionState.TURN_LEFT_180 : AgvMotionState.TURN_RIGHT_180, - targetRotation: direction === 'LEFT' ? s.rotation - 180 : s.rotation + 180 - })); - }; - - const autoSendData = useCallback(() => { - if (!agvConnected || !agvSerialRef.current || !agvSerialRef.current.connected) return; - // Use ref to get latest state without recreating function - const s = agvStateRef.current; - const barr = new Uint8Array(34); - barr[0] = 0x02; barr[1] = 0x53; barr[2] = 0x54; barr[3] = 0x53; - - const encodeHex = (val: number, len: number) => { - const hex = val.toString(16).toUpperCase().padStart(len, '0'); - return new TextEncoder().encode(hex); - }; - - const voltBytes = new TextEncoder().encode("255"); - barr.set(voltBytes, 4); - barr.set(encodeHex(s.system0, 4), 7); - barr.set(encodeHex(s.system1, 4), 11); - barr.set(encodeHex(s.errorFlags, 4), 15); - - const spdChar = s.runConfig.speedLevel.charCodeAt(0); - barr[19] = spdChar; - - const branchChar = s.runConfig.branch === 'LEFT' ? 'L'.charCodeAt(0) : s.runConfig.branch === 'RIGHT' ? 'R'.charCodeAt(0) : 'S'.charCodeAt(0); - barr[20] = branchChar; - - const dirChar = s.runConfig.direction === 'FWD' ? 'F'.charCodeAt(0) : 'B'.charCodeAt(0); - barr[21] = dirChar; - - barr[22] = s.sensorStatus.charCodeAt(0); - barr.set(encodeHex(s.signalFlags, 2), 23); - - barr[31] = '*'.charCodeAt(0); barr[32] = '*'.charCodeAt(0); barr[33] = 0x03; - - agvSerialRef.current.send(barr); - - // Log Transmission (Throttled log could be nice, but simple for now) - addLog('AGV', 'TX', `STS (AutoSend)`); - }, [agvConnected, addLog]); - - useEffect(() => { - if (agvConnected) { - const timer = setInterval(() => { - autoSendData(); - }, 500); - return () => clearInterval(timer); - } - }, [autoSendData, agvConnected]); - - // --- Main Logic: Sensors, Signals & Lift Physics --- - useEffect(() => { - // 1. Calculate Signals - let sig = 0; - if (agvState.sensorMark) sig |= (1 << AgvSignal.mark_sensor_1); - - // Front Line Sensor & Gate Out - if (!agvState.sensorLineFront) { - // Gate Out: Active when line is NOT detected - sig |= (1 << AgvSignal.front_gate_out); - } - - // Rear Sensor Gate Out - if (!agvState.sensorLineRear) { - // Gate Out: Active when line is NOT detected - sig |= (1 << AgvSignal.rear_sensor_out); - } - - // Lift Sensors - if (agvState.liftHeight <= 0) sig |= (1 << AgvSignal.lift_down_sensor); - if (agvState.liftHeight >= 100) sig |= (1 << AgvSignal.lift_up_sensor); - - // Magnet Relay - if (agvState.magnetOn) sig |= (1 << AgvSignal.magnet_relay); - - // 2. Calculate Error Flags - let err = agvState.errorFlags; - if (agvState.error === 'LINE_OUT') err |= (1 << AgvError.line_out_error); - else err &= ~(1 << AgvError.line_out_error); - - // 3. Calculate System Flags - let sys1 = agvState.system1; - const isAutoRun = agvState.motionState === AgvMotionState.RUNNING || agvState.motionState === AgvMotionState.MARK_STOPPING; - const isManualRun = [AgvMotionState.FORWARD, AgvMotionState.BACKWARD, AgvMotionState.TURN_LEFT, AgvMotionState.TURN_RIGHT, AgvMotionState.TURN_LEFT_180, AgvMotionState.TURN_RIGHT_180].includes(agvState.motionState); - - if (isAutoRun) { - sys1 |= (1 << SystemFlag1.agv_run); - sys1 &= ~(1 << SystemFlag1.agv_stop); - } else if (isManualRun) { - // Manual move: Ensure stop bit is OFF. Run bit can be ON to indicate active state (matches CRT logic) - sys1 |= (1 << SystemFlag1.agv_run); - sys1 &= ~(1 << SystemFlag1.agv_stop); - } else if (agvState.motionState === AgvMotionState.IDLE) { - sys1 &= ~(1 << SystemFlag1.agv_run); - sys1 |= (1 << SystemFlag1.agv_stop); - } - - if (sig !== agvState.signalFlags || err !== agvState.errorFlags || sys1 !== agvState.system1) { - setAgvState(s => ({ ...s, signalFlags: sig, errorFlags: err, system1: sys1 })); - } - }, [ - agvState.sensorMark, agvState.sensorLineFront, agvState.sensorLineRear, agvState.error, - agvState.motionState, agvState.signalFlags, agvState.errorFlags, agvState.system1, - agvState.detectedRfid, agvState.liftHeight, agvState.magnetOn - ]); - - // --- Lift Animation Physics Loop --- - useEffect(() => { - let animId: number; - const animateLift = () => { - setAgvState(prev => { - if (prev.liftStatus === 'IDLE') return prev; - - let newHeight = prev.liftHeight; - const step = 0.6; // Speed of lift (~3s for 0-100) - - if (prev.liftStatus === 'UP') { - newHeight += step; - if (newHeight >= 100) { - newHeight = 100; - return { ...prev, liftHeight: 100, liftStatus: 'IDLE' }; - } - } else if (prev.liftStatus === 'DOWN') { - newHeight -= step; - if (newHeight <= 0) { - newHeight = 0; - return { ...prev, liftHeight: 0, liftStatus: 'IDLE' }; - } - } - - return { ...prev, liftHeight: newHeight }; - }); - animId = requestAnimationFrame(animateLift); - }; - - animId = requestAnimationFrame(animateLift); - return () => cancelAnimationFrame(animId); - }, []); // Logic relies on functional state updates, so empty dependency is safe-ish, but let's be cleaner. - // Actually, requestAnimationFrame runs continuously. We just need to ensure we don't leak. - - - // --- BMS Protocol Logic --- - const bmsBufferRef = useRef([]); - - const handleBmsData = useCallback((data: Uint8Array) => { - // Log raw data for debugging visibility - const hex = Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); - addLog('BMS', 'RX', `RAW: ${hex}`); - - for (let i = 0; i < data.length; i++) { - bmsBufferRef.current.push(data[i]); - } - - const buf = bmsBufferRef.current; - // Protocol: 7 bytes total. Start=0xDD, End=0x77. - while (buf.length >= 7) { - const startIdx = buf.indexOf(0xDD); - if (startIdx === -1) { - bmsBufferRef.current = []; - return; - } - if (startIdx > 0) { - buf.splice(0, startIdx); - } - - // Wait for end byte (not fully safe if 77 is inside payload, but standard for this packet size) - // Actually, fixed length is 34 for Status, 23 for Cells. - // But we receive potentially fragmented. - // Let's rely on fixed length per known command if we had one. - // Here we just look for 0x77. - const endIdx = buf.indexOf(0x77); - if (endIdx === -1) return; - - const packet = buf.splice(0, endIdx + 1); - processBmsPacket(packet); - } - }, [addLog]); - - const processBmsPacket = (packet: number[]) => { - const cmd = packet[2]; - // const hexStr = packet.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(' '); - - if (cmd === 0x03) { - sendBmsStatus(); - } else if (cmd === 0x04) { - sendBmsCellVoltage(); - } - }; - - const sendBmsStatus = () => { - if (!bmsSerialRef.current) return; - const s = agvStateRef.current; // Use Ref for latest state - const resp = new Uint8Array(34); - resp[0] = 0xDD; resp[1] = 0x03; resp[2] = 0x00; resp[3] = 0x1b; //arrary 3value 0->1b - - const totalV = Math.floor(s.cellVoltages.reduce((a,b)=>a+b, 0) * 100); - resp[4] = (totalV >> 8) & 0xFF; resp[5] = totalV & 0xFF; - - const remainAh = Math.floor(s.maxCapacity * (s.batteryLevel / 100)); - resp[8] = (remainAh >> 8) & 0xFF; resp[9] = remainAh & 0xFF; - const maxAh = s.maxCapacity; - resp[10] = (maxAh >> 8) & 0xFF; resp[11] = maxAh & 0xFF; - - resp[23] = Math.floor(s.batteryLevel); - - const t1 = Math.floor((s.batteryTemp * 10) + 2731); - const t2 = Math.floor((s.batteryTemp * 10) + 2731); - - resp[27] = (t1 >> 8) & 0xFF; resp[28] = t1 & 0xFF; - resp[29] = (t2 >> 8) & 0xFF; resp[30] = t2 & 0xFF; - - resp[31] = 0x00; //*magic numbee(skip) - resp[32] = 0x00; //*magic numbee(skip) - resp[33] = 0x77; - - bmsSerialRef.current.send(resp); - addLog('BMS', 'TX', 'DD ... 77 (Status)'); - }; - - const sendBmsCellVoltage = () => { - if (!bmsSerialRef.current) return; - const s = agvStateRef.current; // Use Ref for latest state - const resp = new Uint8Array(23); - resp[0] = 0xDD; resp[1] = 0x04; resp[2] = 0x00; resp[3] = 0x10; - let checksumSum = resp[3]; - - for (let i = 0; i < 8; i++) { - const v = Math.floor((s.cellVoltages[i] || 0) * 1000); - const idx = 4 + (i * 2); - const high = (v >> 8) & 0xFF; - const low = v & 0xFF; - resp[idx] = high; resp[idx + 1] = low; - checksumSum += high + low; - } - - const checksum = (0xFFFF - checksumSum + 1) & 0xFFFF; - resp[20] = (checksum >> 8) & 0xFF; resp[21] = checksum & 0xFF; - resp[22] = 0x77; - - bmsSerialRef.current.send(resp); - addLog('BMS', 'TX', 'DD ... 77 (Cells)'); - }; - - // --- ACS Protocol Logic (Implementing C# Logic) --- - const acsBufferRef = useRef([]); - const acsParseErrorsRef = useRef(0); - - const sendAcsPacket = useCallback((cmd: number, payload: number[] = []) => { - if (!acsSerialRef.current) { - addLog('ACS', 'ERROR', 'Not Connected'); - return; - } - - const id = 1; // Default AGV ID - const len = 1 + 1 + payload.length; // ID + CMD + DATA - const buffer = [0x02, len, id, cmd, ...payload]; - - // Calculate CRC over [LEN, ID, CMD, DATA] (indices 1 to end) - const crcData = buffer.slice(1); - const crc = calculateAcsCrc16(crcData); - - buffer.push(crc & 0xFF); // Low Byte - buffer.push((crc >> 8) & 0xFF); // High Byte - buffer.push(0x03); // ETX - - acsSerialRef.current.send(new Uint8Array(buffer)); - - const hex = buffer.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(' '); - addLog('ACS', 'TX', `CMD:${cmd.toString(16).toUpperCase()} [${hex}]`); - }, [addLog]); - - const handleAcsData = useCallback((data: Uint8Array) => { - for (let i = 0; i < data.length; i++) { - acsBufferRef.current.push(data[i]); - } - - const buf = acsBufferRef.current; - while (buf.length > 0) { - // 1. Find STX - const stxIndex = buf.indexOf(0x02); // STX - if (stxIndex === -1) { - buf.length = 0; // Clear junk - return; - } - // Remove garbage before STX - if (stxIndex > 0) { - buf.splice(0, stxIndex); - } - - // 2. Check minimal size (STX + Len + ID + Cmd + CRC + ETX) = 7 bytes if data is 0? - // Per C#: Length = 1(ID)+1(Cmd)+DataLen. - // Packet: STX(1) + Len(1) + Payload(Len) + CRC(2) + ETX(1) = 5 + Len. - if (buf.length < 2) return; // Need Length byte - - const lengthByte = buf[1]; - const totalPacketSize = lengthByte + 5; - - if (buf.length < totalPacketSize) return; // Wait for more data - - // 3. Extract and Verify - const packetData = buf.slice(0, totalPacketSize); - - // Verify ETX - if (packetData[totalPacketSize - 1] !== 0x03) { - addLog('ACS', 'ERROR', 'ETX Missing'); - buf.shift(); // Invalid packet, shift 1 and retry - continue; - } - - // Verify CRC - // CRC covers: [Length, ID, Command, Data...] (Skip STX, Exclude CRC bytes) - // Bytes to CRC: From index 1 to (1 + length) inclusive? - // C# Logic: CalculateCRC16(packetData.Skip(1).Take(length + 1)) - // packetData indices: 0=STX, 1=Len, 2=ID, 3=Cmd... - // We need indices 1 to (1 + length) inclusive. - // Total size is length + 5. - // CRC is at [total - 3, total - 2]. - - const dataForCrc = packetData.slice(1, lengthByte + 2); // slice end is exclusive - const calcCrc = calculateAcsCrc16(dataForCrc); - - const receivedCrc = (packetData[totalPacketSize - 2] << 8) | packetData[totalPacketSize - 3]; // Little Endian in C# BitConverter? - // C# BitConverter.ToUInt16(rawData, rawData.Length - 3). - // rawData ends with [CRC_L, CRC_H, ETX] usually? - // Default Little Endian. So index len-3 is Low, len-2 is High. - - if (receivedCrc !== 0xFFFF && calcCrc !== receivedCrc) { - const hex = packetData.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(' '); - addLog('ACS', 'ERROR', `CRC Fail: ${hex}`); - acsParseErrorsRef.current++; - if (acsParseErrorsRef.current > 3) buf.length = 0; // Reset on too many errors - else buf.shift(); - continue; - } - - acsParseErrorsRef.current = 0; - - // Parse Success - const id = packetData[2]; - const cmd = packetData[3]; - const payload = packetData.slice(4, totalPacketSize - 3); - - const hexStr = packetData.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(' '); - addLog('ACS', 'RX', `[ID:${id} CMD:${cmd}] ${hexStr}`); - - // Consume buffer - buf.splice(0, totalPacketSize); - } - }, [addLog]); - - // --- Connect/Disconnect Handlers --- - - const connectAgv = useCallback(async () => { - if (!agvSerialRef.current) { - agvSerialRef.current = new SerialPortHandler(handleAgvData); - } - try { - await agvSerialRef.current.connect(agvBaudRate); - setAgvConnected(true); - setAgvPortInfo(agvSerialRef.current.getPortInfo()); - addLog('AGV', 'INFO', `Connected at ${agvBaudRate} baud`); - } catch (err: any) { - if (err.message !== 'USER_CANCELLED') { - console.error(err); - addLog('AGV', 'ERROR', `Connection failed: ${err.message}`); + case 'ACK': + break; } - } - }, [agvBaudRate, handleAgvData, addLog]); - - const disconnectAgv = useCallback(async () => { - if (agvSerialRef.current) { - await agvSerialRef.current.disconnect(); - setAgvConnected(false); - setAgvPortInfo(null); - addLog('AGV', 'INFO', 'Disconnected'); - } - }, [addLog]); - - const connectBms = useCallback(async () => { - if (!bmsSerialRef.current) { - bmsSerialRef.current = new SerialPortHandler(handleBmsData); - } - try { - await bmsSerialRef.current.connect(bmsBaudRate); - setBmsConnected(true); - setBmsPortInfo(bmsSerialRef.current.getPortInfo()); - addLog('BMS', 'INFO', `Connected at ${bmsBaudRate} baud`); - } catch (err: any) { - if (err.message !== 'USER_CANCELLED') { - console.error(err); - addLog('BMS', 'ERROR', `Connection failed: ${err.message}`); - } - } - }, [bmsBaudRate, handleBmsData, addLog]); - - const disconnectBms = useCallback(async () => { - if (bmsSerialRef.current) { - await bmsSerialRef.current.disconnect(); - setBmsConnected(false); - setBmsPortInfo(null); - addLog('BMS', 'INFO', 'Disconnected'); - } - }, [addLog]); - - const connectAcs = useCallback(async () => { - if (!acsSerialRef.current) { - acsSerialRef.current = new SerialPortHandler(handleAcsData); - } - try { - await acsSerialRef.current.connect(acsBaudRate); - setAcsConnected(true); - setAcsPortInfo(acsSerialRef.current.getPortInfo()); - addLog('ACS', 'INFO', `Connected at ${acsBaudRate} baud`); - } catch (err: any) { - if (err.message !== 'USER_CANCELLED') { - console.error(err); - addLog('ACS', 'ERROR', `Connection failed: ${err.message}`); - } - } - }, [acsBaudRate, handleAcsData, addLog]); - - const disconnectAcs = useCallback(async () => { - if (acsSerialRef.current) { - await acsSerialRef.current.disconnect(); - setAcsConnected(false); - setAcsPortInfo(null); - addLog('ACS', 'INFO', 'Disconnected'); - } - }, [addLog]); - - // --- Map Loading Logic (Updated for New Structure) --- - const loadMapFromCSharp = useCallback((csharpData: CSharpMapData) => { - const newNodes: MapNode[] = []; - const newEdges: MapEdge[] = []; - const edgeSet = new Set(); - - // 1. Process Nodes - if (csharpData.Nodes && Array.isArray(csharpData.Nodes)) { - csharpData.Nodes.forEach((n) => { - const [xStr, yStr] = n.Position.split(',').map(s => s.trim()); - const x = parseFloat(xStr) || 0; - const y = parseFloat(yStr) || 0; - - // Map StationType to Internal NodeType - let internalType = n.StationType; - if (n.StationType === 2) internalType = NodeType.Charging; - else if (n.StationType === 3) internalType = NodeType.UnLoader; - - newNodes.push({ - id: n.Id, - x, y, - type: internalType, - name: n.Text || n.AliasName || "", - rfidId: n.RfidId, - connectedNodes: n.ConnectedNodes || [], - foreColor: n.NodeTextForeColor, - fontSize: n.NodeTextFontSize - }); - - if (n.ConnectedNodes && Array.isArray(n.ConnectedNodes)) { - n.ConnectedNodes.forEach((targetId) => { - const ids = [n.Id, targetId].sort(); - const key = ids.join('-'); - if (!edgeSet.has(key)) { - edgeSet.add(key); - newEdges.push({ - id: `edge_${key}`, - from: ids[0], - to: ids[1] - }); - } - }); - } - }); - } - - // 2. Process Labels (Convert to Nodes Type=Label) - if (csharpData.Labels && Array.isArray(csharpData.Labels)) { - csharpData.Labels.forEach(l => { - const [xStr, yStr] = l.Position.split(',').map(s => s.trim()); - newNodes.push({ - id: l.Id, - x: parseFloat(xStr)||0, y: parseFloat(yStr)||0, - type: NodeType.Label, - name: "", - rfidId: "", - connectedNodes: [], - labelText: l.Text, - foreColor: l.ForeColor, - backColor: l.BackColor, - fontSize: l.FontSize - }); - }); - } - - // 3. Process Images (Convert to Nodes Type=Image) - if (csharpData.Images && Array.isArray(csharpData.Images)) { - csharpData.Images.forEach(img => { - const [xStr, yStr] = img.Position.split(',').map(s => s.trim()); - newNodes.push({ - id: img.Id, - x: parseFloat(xStr)||0, y: parseFloat(yStr)||0, - type: NodeType.Image, - name: img.Name, - rfidId: "", - connectedNodes: [], - imageBase64: img.ImageBase64 - }); - }); - } - - // 4. Process Magnets - const newMagnets: MagnetLine[] = []; - if (csharpData.Magnets && Array.isArray(csharpData.Magnets)) { - csharpData.Magnets.forEach(m => { - const type = m.ControlPoint ? 'CURVE' : 'STRAIGHT'; - newMagnets.push({ - id: m.Id, - type: type, - p1: { x: m.P1.X, y: m.P1.Y }, - p2: { x: m.P2.X, y: m.P2.Y }, - controlPoint: m.ControlPoint ? { x: m.ControlPoint.X, y: m.ControlPoint.Y } : undefined - }); - }); - } - - // 5. Process Marks - const newMarks: FloorMark[] = []; - if (csharpData.Marks && Array.isArray(csharpData.Marks)) { - csharpData.Marks.forEach(mk => { - newMarks.push({ - id: mk.Id, - x: mk.X, - y: mk.Y, - rotation: mk.Rotation - }); - }); - } - - setMapData({ - nodes: newNodes, - edges: newEdges, - marks: newMarks, - magnets: newMagnets - }); - addLog('SYSTEM', 'INFO', `New Map loaded: ${newNodes.length} objects`); - - }, [addLog]); - - // --- Initialize Map from NewMap.json on Mount --- - useEffect(() => { - fetch('NewMap.json') - .then(response => { - if (!response.ok) throw new Error("Default map not found"); - return response.json(); - }) - .then((data: CSharpMapData) => { - loadMapFromCSharp(data); - }) - .catch(err => { - console.log("Could not load default map:", err); - }); - }, [loadMapFromCSharp]); - - // --- Map IO Logic --- - const processMapFile = (file: File) => { - const reader = new FileReader(); - reader.onload = (evt) => { - try { - const jsonStr = evt.target?.result as string; - const csharpData = JSON.parse(jsonStr) as CSharpMapData; - loadMapFromCSharp(csharpData); - } catch (err) { - console.error(err); - alert('Failed to parse map file.'); - } - }; - reader.readAsText(file); - }; - - const saveMap = () => { - const outNodes: any[] = []; - const outLabels: any[] = []; - const outImages: any[] = []; - - mapData.nodes.forEach(n => { - const pos = `${Math.round(n.x)}, ${Math.round(n.y)}`; - - if (n.type === NodeType.Label) { - outLabels.push({ - Id: n.id, - Type: 1, - Text: n.labelText || "Label", - Position: pos, - ForeColor: n.foreColor || "White", - BackColor: n.backColor || "Transparent", - FontFamily: "Arial", - FontSize: n.fontSize || 12, - FontStyle: 0, - Padding: 5 - }); - } else if (n.type === NodeType.Image) { - outImages.push({ - Id: n.id, - Type: 2, - Name: n.name || "Image", - Position: pos, - ImagePath: "", - ImageBase64: n.imageBase64 || "", - Scale: "1, 1", - Opacity: 1.0, - Rotation: 0.0 - }); - } else { - // Standard Node - // Map Internal NodeType back to StationType - let stType = n.type; - if (n.type === NodeType.Charging) stType = 2; // Cleaner - else if (n.type === NodeType.UnLoader) stType = 3; // Unloader - - // Re-calculate connected nodes - const connected = new Set(); - mapData.edges.forEach(e => { - if (e.from === n.id) connected.add(e.to); - if (e.to === n.id) connected.add(e.from); - }); - - outNodes.push({ - Id: n.id, - Text: n.name || "", - Position: pos, - Type: 0, // Always 0 for nodes - StationType: stType, - ConnectedNodes: Array.from(connected), - RfidId: n.rfidId || "", - NodeTextForeColor: n.foreColor, - NodeTextFontSize: n.fontSize || 7.0, - AliasName: "", - SpeedLimit: 0, - CanDocking: [1,2,3,4,5].includes(n.type), - DockDirection: n.type === NodeType.Charging || n.type === NodeType.ChargerStation ? 1 : 0, - CanTurnLeft: true, - CanTurnRight: true, - DisableCross: false, - IsActive: true - }); - } - }); - - const outMagnets = mapData.magnets.map(m => ({ - Id: m.id, - Type: 4, - P1: { X: m.p1.x, Y: m.p1.y }, - P2: { X: m.p2.x, Y: m.p2.y }, - ControlPoint: m.controlPoint ? { X: m.controlPoint.x, Y: m.controlPoint.y } : null - })); - - const outMarks = mapData.marks.map(mk => ({ - Id: mk.id, - Type: 3, - Position: `${Math.round(mk.x)}, ${Math.round(mk.y)}`, - X: mk.x, - Y: mk.y, - Rotation: mk.rotation - })); - - const exportData = { - Nodes: outNodes, - Labels: outLabels, - Images: outImages, - Magnets: outMagnets, - Marks: outMarks, - Settings: { - BackgroundColorArgb: -14671840, - ShowGrid: false - }, - CreatedDate: new Date().toISOString(), - Version: "1.3" - }; - - const json = JSON.stringify(exportData, null, 2); - const blob = new Blob([json], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'NewMap.json'; - a.click(); - }; - - const loadMap = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'application/json'; - input.onchange = (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - processMapFile(file); - } - }; - input.click(); - }; - - // --- Drag and Drop Handlers --- - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - const file = e.dataTransfer.files[0]; - if (file && file.name.endsWith('.json')) { - processMapFile(file); - } - }; - - // --- Real-time updates to Serial (AGV Events) --- - const prevSensors = useRef({ rfid: null as string|null, mark: false, line: false }); - useEffect(() => { - if (!agvConnected || !agvSerialRef.current) return; - - // Check RFID change - if (agvState.detectedRfid !== prevSensors.current.rfid) { - if (agvState.detectedRfid) { - sendTag(agvState.detectedRfid); - } - } - - prevSensors.current = { - rfid: agvState.detectedRfid, - mark: agvState.sensorMark, - line: agvState.sensorLineFront }; - }, [agvState, agvConnected, sendTag]); - return ( -
- {/* 1. Far Left: Toolbar */} -
- -
+ const handleTurn180 = (direction: 'LEFT' | 'RIGHT') => { + setAgvState(s => ({ + ...s, + motionState: direction === 'LEFT' ? AgvMotionState.TURN_LEFT_180 : AgvMotionState.TURN_RIGHT_180, + targetRotation: direction === 'LEFT' ? s.rotation - 180 : s.rotation + 180 + })); + }; - {/* 2. Inner Left: Protocol Flags (Separated Sidebar) - Removed BMS from here */} -
-
- -
+ const autoSendData = useCallback(() => { + if (!agvConnected || !agvSerialRef.current || !agvSerialRef.current.connected) return; + // Use ref to get latest state without recreating function + const s = agvStateRef.current; + const barr = new Uint8Array(34); + barr[0] = 0x02; barr[1] = 0x53; barr[2] = 0x54; barr[3] = 0x53; - - {/* Middle 2: ACS Panel (Fixed) */} -
- -
+ const encodeHex = (val: number, len: number) => { + const hex = val.toString(16).toUpperCase().padStart(len, '0'); + return new TextEncoder().encode(hex); + }; + + const voltBytes = new TextEncoder().encode("255"); + barr.set(voltBytes, 4); + barr.set(encodeHex(s.system0, 4), 7); + barr.set(encodeHex(s.system1, 4), 11); + barr.set(encodeHex(s.errorFlags, 4), 15); + + const spdChar = s.runConfig.speedLevel.charCodeAt(0); + barr[19] = spdChar; + + const branchChar = s.runConfig.branch === 'LEFT' ? 'L'.charCodeAt(0) : s.runConfig.branch === 'RIGHT' ? 'R'.charCodeAt(0) : 'S'.charCodeAt(0); + barr[20] = branchChar; + + const dirChar = s.runConfig.direction === 'FWD' ? 'F'.charCodeAt(0) : 'B'.charCodeAt(0); + barr[21] = dirChar; + + barr[22] = s.sensorStatus.charCodeAt(0); + barr.set(encodeHex(s.signalFlags, 2), 23); + + barr[31] = '*'.charCodeAt(0); barr[32] = '*'.charCodeAt(0); barr[33] = 0x03; + + agvSerialRef.current.send(barr); + + // Log Transmission (Throttled log could be nice, but simple for now) + addLog('AGV', 'TX', `STS (AutoSend)`); + }, [agvConnected, addLog]); + + useEffect(() => { + if (agvConnected) { + const timer = setInterval(() => { + autoSendData(); + }, 500); + return () => clearInterval(timer); + } + }, [autoSendData, agvConnected]); + + // --- Main Logic: Sensors, Signals & Lift Physics --- + useEffect(() => { + // 1. Calculate Signals + let sig = 0; + if (agvState.sensorMark) sig |= (1 << AgvSignal.mark_sensor_1); + + // Front Line Sensor & Gate Out + if (!agvState.sensorLineFront) { + // Gate Out: Active when line is NOT detected + sig |= (1 << AgvSignal.front_gate_out); + } + + // Rear Sensor Gate Out + if (!agvState.sensorLineRear) { + // Gate Out: Active when line is NOT detected + sig |= (1 << AgvSignal.rear_sensor_out); + } + + // Lift Sensors + if (agvState.liftHeight <= 0) sig |= (1 << AgvSignal.lift_down_sensor); + if (agvState.liftHeight >= 100) sig |= (1 << AgvSignal.lift_up_sensor); + + // Magnet Relay + if (agvState.magnetOn) sig |= (1 << AgvSignal.magnet_relay); + + // 2. Calculate Error Flags + let err = agvState.errorFlags; + if (agvState.error === 'LINE_OUT') err |= (1 << AgvError.line_out_error); + else err &= ~(1 << AgvError.line_out_error); + + // 3. Calculate System Flags + let sys1 = agvState.system1; + const isAutoRun = agvState.motionState === AgvMotionState.RUNNING || agvState.motionState === AgvMotionState.MARK_STOPPING; + const isManualRun = [AgvMotionState.FORWARD, AgvMotionState.BACKWARD, AgvMotionState.TURN_LEFT, AgvMotionState.TURN_RIGHT, AgvMotionState.TURN_LEFT_180, AgvMotionState.TURN_RIGHT_180].includes(agvState.motionState); + + if (isAutoRun) { + sys1 |= (1 << SystemFlag1.agv_run); + sys1 &= ~(1 << SystemFlag1.agv_stop); + } else if (isManualRun) { + // Manual move: Ensure stop bit is OFF. Run bit can be ON to indicate active state (matches CRT logic) + sys1 |= (1 << SystemFlag1.agv_run); + sys1 &= ~(1 << SystemFlag1.agv_stop); + } else if (agvState.motionState === AgvMotionState.IDLE) { + sys1 &= ~(1 << SystemFlag1.agv_run); + sys1 |= (1 << SystemFlag1.agv_stop); + } + + if (sig !== agvState.signalFlags || err !== agvState.errorFlags || sys1 !== agvState.system1) { + setAgvState(s => ({ ...s, signalFlags: sig, errorFlags: err, system1: sys1 })); + } + }, [ + agvState.sensorMark, agvState.sensorLineFront, agvState.sensorLineRear, agvState.error, + agvState.motionState, agvState.signalFlags, agvState.errorFlags, agvState.system1, + agvState.detectedRfid, agvState.liftHeight, agvState.magnetOn + ]); + + // --- Lift Animation Physics Loop --- + useEffect(() => { + let animId: number; + const animateLift = () => { + setAgvState(prev => { + if (prev.liftStatus === 'IDLE') return prev; + + let newHeight = prev.liftHeight; + const step = 0.6; // Speed of lift (~3s for 0-100) + + if (prev.liftStatus === 'UP') { + newHeight += step; + if (newHeight >= 100) { + newHeight = 100; + return { ...prev, liftHeight: 100, liftStatus: 'IDLE' }; + } + } else if (prev.liftStatus === 'DOWN') { + newHeight -= step; + if (newHeight <= 0) { + newHeight = 0; + return { ...prev, liftHeight: 0, liftStatus: 'IDLE' }; + } + } + + return { ...prev, liftHeight: newHeight }; + }); + animId = requestAnimationFrame(animateLift); + }; + + animId = requestAnimationFrame(animateLift); + return () => cancelAnimationFrame(animId); + }, []); // Logic relies on functional state updates, so empty dependency is safe-ish, but let's be cleaner. + // Actually, requestAnimationFrame runs continuously. We just need to ensure we don't leak. -
+ // --- BMS Protocol Logic --- + const bmsBufferRef = useRef([]); - {/* 3. Center: Canvas & Bottom Panels */} -
-
- addLog('SYSTEM', 'INFO', msg)} - /> -
- - {/* Fixed Communication Panels (Now inside center column) */} -
-
- l.source === 'AGV')} - isConnected={agvConnected} - onConnect={connectAgv} - onDisconnect={disconnectAgv} - onClear={() => clearLogs('AGV')} - baudRate={agvBaudRate} - setBaudRate={setAgvBaudRate} - portInfo={agvPortInfo} - colorClass="text-blue-400" - /> -
-
- l.source === 'BMS')} - isConnected={bmsConnected} - onConnect={connectBms} - onDisconnect={disconnectBms} - onClear={() => clearLogs('BMS')} - baudRate={bmsBaudRate} - setBaudRate={setBmsBaudRate} - portInfo={bmsPortInfo} - colorClass="text-yellow-400" - /> -
-
- l.source === 'ACS')} - isConnected={acsConnected} - onConnect={connectAcs} - onDisconnect={disconnectAcs} - onClear={() => clearLogs('ACS')} - baudRate={acsBaudRate} - setBaudRate={setAcsBaudRate} - portInfo={acsPortInfo} - colorClass="text-green-400" - /> -
-
-
+ const handleBmsData = useCallback((data: Uint8Array) => { + // Log raw data for debugging visibility + const hex = Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); + addLog('BMS', 'RX', `RAW: ${hex}`); - {/* 4. Right: Controls & BMS & ACS */} -
- - {/* Top: Controls (Scrollable if needed) */} -
- setAgvState(s => ({...s, motionState: m}))} - setLift={(h) => setAgvState(s => ({...s, liftHeight: h}))} - setRunConfig={(c) => setAgvState(s => ({...s, runConfig: c}))} - setError={(e) => setAgvState(s => ({...s, error: e}))} - onTurn180={handleTurn180} - setMagnet={(isOn) => setAgvState(s => ({...s, magnetOn: isOn}))} - setLiftStatus={(status) => setAgvState(s => ({...s, liftStatus: status}))} - setLidar={(isOn) => setAgvState(s => ({...s, lidarEnabled: isOn, sensorStatus: isOn ? '1' : '0'}))} - /> -
- - {/* Middle 1: BMS Panel (Fixed) */} -
- setAgvState(s => ({...s, batteryLevel: l}))} /> -
+ for (let i = 0; i < data.length; i++) { + bmsBufferRef.current.push(data[i]); + } - {/* Bottom: System Logs (Fixed Height) */} -
- l.source === 'SYSTEM')} onClear={() => clearLogs('SYSTEM')} /> -
-
-
- ); + const buf = bmsBufferRef.current; + // Protocol: 7 bytes total. Start=0xDD, End=0x77. + while (buf.length >= 7) { + const startIdx = buf.indexOf(0xDD); + if (startIdx === -1) { + bmsBufferRef.current = []; + return; + } + if (startIdx > 0) { + buf.splice(0, startIdx); + } + + // Wait for end byte (not fully safe if 77 is inside payload, but standard for this packet size) + // Actually, fixed length is 34 for Status, 23 for Cells. + // But we receive potentially fragmented. + // Let's rely on fixed length per known command if we had one. + // Here we just look for 0x77. + const endIdx = buf.indexOf(0x77); + if (endIdx === -1) return; + + const packet = buf.splice(0, endIdx + 1); + processBmsPacket(packet); + } + }, [addLog]); + + const processBmsPacket = (packet: number[]) => { + const cmd = packet[2]; + // const hexStr = packet.map(b => b.toString(16).padStart(2,'0').toUpperCase()).join(' '); + + if (cmd === 0x03) { + sendBmsStatus(); + } else if (cmd === 0x04) { + sendBmsCellVoltage(); + } + }; + + const sendBmsStatus = () => { + if (!bmsSerialRef.current) return; + const s = agvStateRef.current; // Use Ref for latest state + const resp = new Uint8Array(34); + resp[0] = 0xDD; resp[1] = 0x03; resp[2] = 0x00; resp[3] = 0x1b; //arrary 3value 0->1b + + const totalV = Math.floor(s.cellVoltages.reduce((a, b) => a + b, 0) * 100); + resp[4] = (totalV >> 8) & 0xFF; resp[5] = totalV & 0xFF; + + const remainAh = Math.floor(s.maxCapacity * (s.batteryLevel / 100)); + resp[8] = (remainAh >> 8) & 0xFF; resp[9] = remainAh & 0xFF; + const maxAh = s.maxCapacity; + resp[10] = (maxAh >> 8) & 0xFF; resp[11] = maxAh & 0xFF; + + resp[23] = Math.floor(s.batteryLevel); + + const t1 = Math.floor((s.batteryTemp * 10) + 2731); + const t2 = Math.floor((s.batteryTemp * 10) + 2731); + + resp[27] = (t1 >> 8) & 0xFF; resp[28] = t1 & 0xFF; + resp[29] = (t2 >> 8) & 0xFF; resp[30] = t2 & 0xFF; + + resp[31] = 0x00; //*magic numbee(skip) + resp[32] = 0x00; //*magic numbee(skip) + resp[33] = 0x77; + + bmsSerialRef.current.send(resp); + addLog('BMS', 'TX', 'DD ... 77 (Status)'); + }; + + const sendBmsCellVoltage = () => { + if (!bmsSerialRef.current) return; + const s = agvStateRef.current; // Use Ref for latest state + const resp = new Uint8Array(23); + resp[0] = 0xDD; resp[1] = 0x04; resp[2] = 0x00; resp[3] = 0x10; + let checksumSum = resp[3]; + + for (let i = 0; i < 8; i++) { + const v = Math.floor((s.cellVoltages[i] || 0) * 1000); + const idx = 4 + (i * 2); + const high = (v >> 8) & 0xFF; + const low = v & 0xFF; + resp[idx] = high; resp[idx + 1] = low; + checksumSum += high + low; + } + + const checksum = (0xFFFF - checksumSum + 1) & 0xFFFF; + resp[20] = (checksum >> 8) & 0xFF; resp[21] = checksum & 0xFF; + resp[22] = 0x77; + + bmsSerialRef.current.send(resp); + addLog('BMS', 'TX', 'DD ... 77 (Cells)'); + }; + + // --- ACS Protocol Logic (Implementing C# Logic) --- + const acsBufferRef = useRef([]); + const acsParseErrorsRef = useRef(0); + + const sendAcsPacket = useCallback((cmd: number, payload: number[] = []) => { + if (!acsSerialRef.current) { + addLog('ACS', 'ERROR', 'Not Connected'); + return; + } + + const id = 1; // Default AGV ID + const len = 1 + 1 + payload.length; // ID + CMD + DATA + const buffer = [0x02, len, id, cmd, ...payload]; + + // Calculate CRC over [LEN, ID, CMD, DATA] (indices 1 to end) + const crcData = buffer.slice(1); + const crc = calculateAcsCrc16(crcData); + + buffer.push(crc & 0xFF); // Low Byte + buffer.push((crc >> 8) & 0xFF); // High Byte + buffer.push(0x03); // ETX + + acsSerialRef.current.send(new Uint8Array(buffer)); + + const hex = buffer.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); + addLog('ACS', 'TX', `CMD:${cmd.toString(16).toUpperCase()} [${hex}]`); + }, [addLog]); + + const handleAcsData = useCallback((data: Uint8Array) => { + for (let i = 0; i < data.length; i++) { + acsBufferRef.current.push(data[i]); + } + + const buf = acsBufferRef.current; + while (buf.length > 0) { + // 1. Find STX + const stxIndex = buf.indexOf(0x02); // STX + if (stxIndex === -1) { + buf.length = 0; // Clear junk + return; + } + // Remove garbage before STX + if (stxIndex > 0) { + buf.splice(0, stxIndex); + } + + // 2. Check minimal size (STX + Len + ID + Cmd + CRC + ETX) = 7 bytes if data is 0? + // Per C#: Length = 1(ID)+1(Cmd)+DataLen. + // Packet: STX(1) + Len(1) + Payload(Len) + CRC(2) + ETX(1) = 5 + Len. + if (buf.length < 2) return; // Need Length byte + + const lengthByte = buf[1]; + const totalPacketSize = lengthByte + 5; + + if (buf.length < totalPacketSize) return; // Wait for more data + + // 3. Extract and Verify + const packetData = buf.slice(0, totalPacketSize); + + // Verify ETX + if (packetData[totalPacketSize - 1] !== 0x03) { + addLog('ACS', 'ERROR', 'ETX Missing'); + buf.shift(); // Invalid packet, shift 1 and retry + continue; + } + + // Verify CRC + // CRC covers: [Length, ID, Command, Data...] (Skip STX, Exclude CRC bytes) + // Bytes to CRC: From index 1 to (1 + length) inclusive? + // C# Logic: CalculateCRC16(packetData.Skip(1).Take(length + 1)) + // packetData indices: 0=STX, 1=Len, 2=ID, 3=Cmd... + // We need indices 1 to (1 + length) inclusive. + // Total size is length + 5. + // CRC is at [total - 3, total - 2]. + + const dataForCrc = packetData.slice(1, lengthByte + 2); // slice end is exclusive + const calcCrc = calculateAcsCrc16(dataForCrc); + + const receivedCrc = (packetData[totalPacketSize - 2] << 8) | packetData[totalPacketSize - 3]; // Little Endian in C# BitConverter? + // C# BitConverter.ToUInt16(rawData, rawData.Length - 3). + // rawData ends with [CRC_L, CRC_H, ETX] usually? + // Default Little Endian. So index len-3 is Low, len-2 is High. + + if (receivedCrc !== 0xFFFF && calcCrc !== receivedCrc) { + const hex = packetData.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); + addLog('ACS', 'ERROR', `CRC Fail: ${hex}`); + acsParseErrorsRef.current++; + if (acsParseErrorsRef.current > 3) buf.length = 0; // Reset on too many errors + else buf.shift(); + continue; + } + + acsParseErrorsRef.current = 0; + + // Parse Success + const id = packetData[2]; + const cmd = packetData[3]; + const payload = packetData.slice(4, totalPacketSize - 3); + + const hexStr = packetData.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); + addLog('ACS', 'RX', `[ID:${id} CMD:${cmd}] ${hexStr}`); + + // Consume buffer + buf.splice(0, totalPacketSize); + } + }, [addLog]); + + // --- Connect/Disconnect Handlers --- + + const connectAgv = useCallback(async () => { + if (!agvSerialRef.current) { + agvSerialRef.current = new SerialPortHandler(handleAgvData); + } + try { + await agvSerialRef.current.connect(agvBaudRate); + setAgvConnected(true); + setAgvPortInfo(agvSerialRef.current.getPortInfo()); + addLog('AGV', 'INFO', `Connected at ${agvBaudRate} baud`); + } catch (err: any) { + if (err.message !== 'USER_CANCELLED') { + console.error(err); + addLog('AGV', 'ERROR', `Connection failed: ${err.message}`); + } + } + }, [agvBaudRate, handleAgvData, addLog]); + + const disconnectAgv = useCallback(async () => { + if (agvSerialRef.current) { + await agvSerialRef.current.disconnect(); + setAgvConnected(false); + setAgvPortInfo(null); + addLog('AGV', 'INFO', 'Disconnected'); + } + }, [addLog]); + + const connectBms = useCallback(async () => { + if (!bmsSerialRef.current) { + bmsSerialRef.current = new SerialPortHandler(handleBmsData); + } + try { + await bmsSerialRef.current.connect(bmsBaudRate); + setBmsConnected(true); + setBmsPortInfo(bmsSerialRef.current.getPortInfo()); + addLog('BMS', 'INFO', `Connected at ${bmsBaudRate} baud`); + } catch (err: any) { + if (err.message !== 'USER_CANCELLED') { + console.error(err); + addLog('BMS', 'ERROR', `Connection failed: ${err.message}`); + } + } + }, [bmsBaudRate, handleBmsData, addLog]); + + const disconnectBms = useCallback(async () => { + if (bmsSerialRef.current) { + await bmsSerialRef.current.disconnect(); + setBmsConnected(false); + setBmsPortInfo(null); + addLog('BMS', 'INFO', 'Disconnected'); + } + }, [addLog]); + + const connectAcs = useCallback(async () => { + if (!acsSerialRef.current) { + acsSerialRef.current = new SerialPortHandler(handleAcsData); + } + try { + await acsSerialRef.current.connect(acsBaudRate); + setAcsConnected(true); + setAcsPortInfo(acsSerialRef.current.getPortInfo()); + addLog('ACS', 'INFO', `Connected at ${acsBaudRate} baud`); + } catch (err: any) { + if (err.message !== 'USER_CANCELLED') { + console.error(err); + addLog('ACS', 'ERROR', `Connection failed: ${err.message}`); + } + } + }, [acsBaudRate, handleAcsData, addLog]); + + const disconnectAcs = useCallback(async () => { + if (acsSerialRef.current) { + await acsSerialRef.current.disconnect(); + setAcsConnected(false); + setAcsPortInfo(null); + addLog('ACS', 'INFO', 'Disconnected'); + } + }, [addLog]); + + // --- Map Loading Logic (Updated for New Structure) --- + const loadMapFromCSharp = useCallback((csharpData: CSharpMapData) => { + const newNodes: MapNode[] = []; + const newEdges: MapEdge[] = []; + const edgeSet = new Set(); + + // 1. Process Nodes + if (csharpData.Nodes && Array.isArray(csharpData.Nodes)) { + csharpData.Nodes.forEach((n) => { + const [xStr, yStr] = n.Position.split(',').map(s => s.trim()); + const x = parseFloat(xStr) || 0; + const y = parseFloat(yStr) || 0; + + // Map StationType to Internal NodeType + let internalType = n.StationType; + if (n.StationType === 2) internalType = NodeType.Charging; + else if (n.StationType === 3) internalType = NodeType.UnLoader; + + newNodes.push({ + id: n.Id, + x, y, + type: internalType, + name: n.Text || n.AliasName || "", + rfidId: n.RfidId, + connectedNodes: n.ConnectedNodes || [], + foreColor: n.NodeTextForeColor, + fontSize: n.NodeTextFontSize + }); + + if (n.ConnectedNodes && Array.isArray(n.ConnectedNodes)) { + n.ConnectedNodes.forEach((targetId) => { + const ids = [n.Id, targetId].sort(); + const key = ids.join('-'); + if (!edgeSet.has(key)) { + edgeSet.add(key); + newEdges.push({ + id: `edge_${key}`, + from: ids[0], + to: ids[1] + }); + } + }); + } + }); + } + + // 2. Process Labels (Convert to Nodes Type=Label) + if (csharpData.Labels && Array.isArray(csharpData.Labels)) { + csharpData.Labels.forEach(l => { + const [xStr, yStr] = l.Position.split(',').map(s => s.trim()); + newNodes.push({ + id: l.Id, + x: parseFloat(xStr) || 0, y: parseFloat(yStr) || 0, + type: NodeType.Label, + name: "", + rfidId: "", + connectedNodes: [], + labelText: l.Text, + foreColor: l.ForeColor, + backColor: l.BackColor, + fontSize: l.FontSize + }); + }); + } + + // 3. Process Images (Convert to Nodes Type=Image) + if (csharpData.Images && Array.isArray(csharpData.Images)) { + csharpData.Images.forEach(img => { + const [xStr, yStr] = img.Position.split(',').map(s => s.trim()); + newNodes.push({ + id: img.Id, + x: parseFloat(xStr) || 0, y: parseFloat(yStr) || 0, + type: NodeType.Image, + name: img.Name, + rfidId: "", + connectedNodes: [], + imageBase64: img.ImageBase64 + }); + }); + } + + // 4. Process Magnets + const newMagnets: MagnetLine[] = []; + if (csharpData.Magnets && Array.isArray(csharpData.Magnets)) { + csharpData.Magnets.forEach(m => { + const type = m.ControlPoint ? 'CURVE' : 'STRAIGHT'; + newMagnets.push({ + id: m.Id, + type: type, + p1: { x: m.P1.X, y: m.P1.Y }, + p2: { x: m.P2.X, y: m.P2.Y }, + controlPoint: m.ControlPoint ? { x: m.ControlPoint.X, y: m.ControlPoint.Y } : undefined + }); + }); + } + + // 5. Process Marks + const newMarks: FloorMark[] = []; + if (csharpData.Marks && Array.isArray(csharpData.Marks)) { + csharpData.Marks.forEach(mk => { + newMarks.push({ + id: mk.Id, + x: mk.X, + y: mk.Y, + rotation: mk.Rotation + }); + }); + } + + setMapData({ + nodes: newNodes, + edges: newEdges, + marks: newMarks, + magnets: newMagnets + }); + addLog('SYSTEM', 'INFO', `New Map loaded: ${newNodes.length} objects`); + + }, [addLog]); + + // --- Initialize Map from NewMap.json on Mount --- + useEffect(() => { + fetch('NewMap.json') + .then(response => { + if (!response.ok) throw new Error("Default map not found"); + return response.json(); + }) + .then((data: CSharpMapData) => { + loadMapFromCSharp(data); + }) + .catch(err => { + console.log("Could not load default map:", err); + }); + }, [loadMapFromCSharp]); + + // --- Map IO Logic --- + const processMapFile = (file: File) => { + const reader = new FileReader(); + reader.onload = (evt) => { + try { + const jsonStr = evt.target?.result as string; + const csharpData = JSON.parse(jsonStr) as CSharpMapData; + loadMapFromCSharp(csharpData); + } catch (err) { + console.error(err); + alert('Failed to parse map file.'); + } + }; + reader.readAsText(file); + }; + + const saveMap = () => { + const outNodes: any[] = []; + const outLabels: any[] = []; + const outImages: any[] = []; + + mapData.nodes.forEach(n => { + const pos = `${Math.round(n.x)}, ${Math.round(n.y)}`; + + if (n.type === NodeType.Label) { + outLabels.push({ + Id: n.id, + Type: 1, + Text: n.labelText || "Label", + Position: pos, + ForeColor: n.foreColor || "White", + BackColor: n.backColor || "Transparent", + FontFamily: "Arial", + FontSize: n.fontSize || 12, + FontStyle: 0, + Padding: 5 + }); + } else if (n.type === NodeType.Image) { + outImages.push({ + Id: n.id, + Type: 2, + Name: n.name || "Image", + Position: pos, + ImagePath: "", + ImageBase64: n.imageBase64 || "", + Scale: "1, 1", + Opacity: 1.0, + Rotation: 0.0 + }); + } else { + // Standard Node + // Map Internal NodeType back to StationType + let stType = n.type; + if (n.type === NodeType.Charging) stType = 2; // Cleaner + else if (n.type === NodeType.UnLoader) stType = 3; // Unloader + + // Re-calculate connected nodes + const connected = new Set(); + mapData.edges.forEach(e => { + if (e.from === n.id) connected.add(e.to); + if (e.to === n.id) connected.add(e.from); + }); + + outNodes.push({ + Id: n.id, + Text: n.name || "", + Position: pos, + Type: 0, // Always 0 for nodes + StationType: stType, + ConnectedNodes: Array.from(connected), + RfidId: n.rfidId || "", + NodeTextForeColor: n.foreColor, + NodeTextFontSize: n.fontSize || 7.0, + AliasName: "", + SpeedLimit: 0, + CanDocking: [1, 2, 3, 4, 5].includes(n.type), + DockDirection: n.type === NodeType.Charging || n.type === NodeType.ChargerStation ? 1 : 0, + CanTurnLeft: true, + CanTurnRight: true, + DisableCross: false, + IsActive: true + }); + } + }); + + const outMagnets = mapData.magnets.map(m => ({ + Id: m.id, + Type: 4, + P1: { X: m.p1.x, Y: m.p1.y }, + P2: { X: m.p2.x, Y: m.p2.y }, + ControlPoint: m.controlPoint ? { X: m.controlPoint.x, Y: m.controlPoint.y } : null + })); + + const outMarks = mapData.marks.map(mk => ({ + Id: mk.id, + Type: 3, + Position: `${Math.round(mk.x)}, ${Math.round(mk.y)}`, + X: mk.x, + Y: mk.y, + Rotation: mk.rotation + })); + + const exportData = { + Nodes: outNodes, + Labels: outLabels, + Images: outImages, + Magnets: outMagnets, + Marks: outMarks, + Settings: { + BackgroundColorArgb: -14671840, + ShowGrid: false + }, + CreatedDate: new Date().toISOString(), + Version: "1.3" + }; + + const json = JSON.stringify(exportData, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'NewMap.json'; + a.click(); + }; + + const loadMap = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/json'; + input.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + processMapFile(file); + } + }; + input.click(); + }; + + // --- Drag and Drop Handlers --- + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + const file = e.dataTransfer.files[0]; + if (file && file.name.endsWith('.json')) { + processMapFile(file); + } + }; + + // --- Real-time updates to Serial (AGV Events) --- + const prevSensors = useRef({ rfid: null as string | null, mark: false, line: false }); + useEffect(() => { + if (!agvConnected || !agvSerialRef.current) return; + + // Check RFID change + if (agvState.detectedRfid !== prevSensors.current.rfid) { + if (agvState.detectedRfid) { + sendTag(agvState.detectedRfid); + } + } + + prevSensors.current = { + rfid: agvState.detectedRfid, + mark: agvState.sensorMark, + line: agvState.sensorLineFront + }; + }, [agvState, agvConnected, sendTag]); + + return ( +
+ {/* 1. Far Left: Toolbar */} +
+ +
+ + {/* 2. Inner Left: Protocol Flags (Separated Sidebar) - Removed BMS from here */} +
+
+ + +
+ + {/* Auto Run Controls (Left Sidebar) */} +
+
+ { + if (agvState.error) return; + setAgvState(s => ({ ...s, runConfig: { ...s.runConfig, [key]: value } })); + }} + toggleRun={() => { + if (agvState.error) return; + if (agvState.motionState === AgvMotionState.RUNNING || agvState.motionState === AgvMotionState.MARK_STOPPING) { + setAgvState(s => ({ ...s, motionState: AgvMotionState.IDLE })); + } else { + const isFwd = agvState.runConfig.direction === 'FWD'; + const hasLine = isFwd ? agvState.sensorLineFront : agvState.sensorLineRear; + if (!hasLine) { + setAgvState(s => ({ ...s, error: 'LINE_OUT' })); + return; + } + setAgvState(s => ({ ...s, motionState: AgvMotionState.RUNNING })); + } + }} + isRunning={agvState.motionState === AgvMotionState.RUNNING || agvState.motionState === AgvMotionState.MARK_STOPPING} + isError={agvState.error !== null} + setLidar={(isOn) => setAgvState(s => ({ ...s, lidarEnabled: isOn, sensorStatus: isOn ? '1' : '0' }))} + /> +
+
+ + {/* Middle 2: ACS Panel (Fixed) */} +
+ +
+ + +
+ + {/* 3. Center: Canvas & Bottom Panels */} +
+
+ addLog('SYSTEM', 'INFO', msg)} + /> +
+ + {/* Fixed Communication Panels (Now inside center column) */} +
+
+ l.source === 'AGV')} + isConnected={agvConnected} + onConnect={connectAgv} + onDisconnect={disconnectAgv} + onClear={() => clearLogs('AGV')} + baudRate={agvBaudRate} + setBaudRate={setAgvBaudRate} + portInfo={agvPortInfo} + colorClass="text-blue-400" + /> +
+
+ l.source === 'BMS')} + isConnected={bmsConnected} + onConnect={connectBms} + onDisconnect={disconnectBms} + onClear={() => clearLogs('BMS')} + baudRate={bmsBaudRate} + setBaudRate={setBmsBaudRate} + portInfo={bmsPortInfo} + colorClass="text-yellow-400" + /> +
+
+ l.source === 'ACS')} + isConnected={acsConnected} + onConnect={connectAcs} + onDisconnect={disconnectAcs} + onClear={() => clearLogs('ACS')} + baudRate={acsBaudRate} + setBaudRate={setAcsBaudRate} + portInfo={acsPortInfo} + colorClass="text-green-400" + /> +
+
+
+ + {/* 4. Right: Controls & BMS & ACS */} +
+ + {/* Top: Controls (Scrollable if needed) */} +
+ setAgvState(s => ({ ...s, motionState: m }))} + setLift={(h) => setAgvState(s => ({ ...s, liftHeight: h }))} + setRunConfig={(c) => setAgvState(s => ({ ...s, runConfig: c }))} + setError={(e) => setAgvState(s => ({ ...s, error: e }))} + onTurn180={handleTurn180} + setMagnet={(isOn) => setAgvState(s => ({ ...s, magnetOn: isOn }))} + setLiftStatus={(status) => setAgvState(s => ({ ...s, liftStatus: status }))} + setLidar={(isOn) => setAgvState(s => ({ ...s, lidarEnabled: isOn, sensorStatus: isOn ? '1' : '0' }))} + /> +
+ + {/* Middle 1: BMS Panel (Fixed) */} +
+ setAgvState(s => ({ ...s, batteryLevel: l }))} /> +
+ + {/* Bottom: System Logs (Fixed Height) */} +
+ l.source === 'SYSTEM')} onClear={() => clearLogs('SYSTEM')} /> +
+
+
+ ); }; export default App; \ No newline at end of file diff --git a/commit_msg_2.txt b/commit_msg_2.txt new file mode 100644 index 0000000..6f44bca --- /dev/null +++ b/commit_msg_2.txt @@ -0,0 +1 @@ +Update: Relocate AutoRun controls and cleanup diff --git a/components/AgvControls.tsx b/components/AgvControls.tsx index 2ef25e5..5c34527 100644 --- a/components/AgvControls.tsx +++ b/components/AgvControls.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { StopCircle, Play, Square, AlertTriangle, ChevronsUp, ChevronsDown, Magnet, Radar, ArrowLeft, ArrowRight } from 'lucide-react'; import { AgvState, AgvMotionState, AgvRunConfig } from '../types'; import AgvManualControls from './AgvManualControls'; -import AgvAutoRunControls from './AgvAutoRunControls'; + interface AgvControlsProps { agvState: AgvState; @@ -29,22 +29,7 @@ const AgvControls: React.FC = ({ agvState, setMotion, setLift, }); }; - const toggleRun = () => { - if (isError) return; - if (isRunning) { - setMotion(AgvMotionState.IDLE); - } else { - const isFwd = agvState.runConfig.direction === 'FWD'; - const hasLine = isFwd ? agvState.sensorLineFront : agvState.sensorLineRear; - - if (!hasLine) { - setError('LINE_OUT'); - return; - } - setMotion(AgvMotionState.RUNNING); - } - }; const handleMarkStop = () => { if (agvState.motionState === AgvMotionState.RUNNING) { @@ -106,8 +91,8 @@ const AgvControls: React.FC = ({ agvState, setMotion, setLift, onClick={() => setLiftStatus('DOWN')} disabled={agvState.liftHeight === 0} className={`p-1.5 rounded disabled:opacity-30 transition-colors ${agvState.liftStatus === 'DOWN' - ? 'bg-blue-600 text-white shadow-[0_0_10px_rgba(37,99,235,0.5)]' - : 'bg-gray-700 hover:bg-gray-600 text-gray-300' + ? 'bg-blue-600 text-white shadow-[0_0_10px_rgba(37,99,235,0.5)]' + : 'bg-gray-700 hover:bg-gray-600 text-gray-300' }`} title="Lower Lift" > @@ -127,8 +112,8 @@ const AgvControls: React.FC = ({ agvState, setMotion, setLift, onClick={() => setLiftStatus('UP')} disabled={agvState.liftHeight === 100} className={`p-1.5 rounded disabled:opacity-30 transition-colors ${agvState.liftStatus === 'UP' - ? 'bg-blue-600 text-white shadow-[0_0_10px_rgba(37,99,235,0.5)]' - : 'bg-gray-700 hover:bg-gray-600 text-gray-300' + ? 'bg-blue-600 text-white shadow-[0_0_10px_rgba(37,99,235,0.5)]' + : 'bg-gray-700 hover:bg-gray-600 text-gray-300' }`} title="Raise Lift" > @@ -139,8 +124,8 @@ const AgvControls: React.FC = ({ agvState, setMotion, setLift,