import React, { useState, useEffect } from 'react'; import { RotateCw } from 'lucide-react'; import { IOPoint } from '../types'; import { comms } from '../communication'; interface IOMonitorPageProps { onToggle: (id: number, type: 'input' | 'output') => void; } interface InterlockData { axisIndex: number; axisName: string; nonAxis: boolean; locks: { id: number; name: string; state: boolean }[]; hexValue: string; } export const IOMonitorPage: React.FC = ({ onToggle }) => { const [ioPoints, setIoPoints] = useState([]); const [interlocks, setInterlocks] = useState([]); const [isLoading, setIsLoading] = useState(true); const [activeIOTab, setActiveIOTab] = useState<'in' | 'out' | 'interlock'>('in'); // Fetch initial IO list when page mounts useEffect(() => { const fetchIOList = async () => { setIsLoading(true); try { const ioStr = await comms.getIOList(); const ioData = JSON.parse(ioStr); // Handle new structured format: { inputs: [...], outputs: [...], interlocks: [...] } if (ioData.inputs && ioData.outputs) { // New format const flatIoList: IOPoint[] = [ ...ioData.outputs.map((io: any) => ({ id: io.id, name: io.name, type: 'output' as const, state: io.state })), ...ioData.inputs.map((io: any) => ({ id: io.id, name: io.name, type: 'input' as const, state: io.state })) ]; setIoPoints(flatIoList); setInterlocks(ioData.interlocks || []); } else if (Array.isArray(ioData)) { // Old format - already flat array setIoPoints(ioData); } } catch (e) { console.error('Failed to fetch IO list:', e); } setIsLoading(false); }; fetchIOList(); // Subscribe to real-time IO updates const unsubscribe = comms.subscribe((msg: any) => { // STATUS_UPDATE - 주기적인 상태 업데이트 (변경된 IO만 포함) if (msg?.type === 'STATUS_UPDATE' && msg.ioState && msg.ioState.length > 0) { setIoPoints(prev => { const newIO = [...prev]; msg.ioState.forEach((update: { id: number, type: string, state: boolean }) => { const idx = newIO.findIndex(p => p.id === update.id && p.type === update.type); if (idx >= 0) newIO[idx] = { ...newIO[idx], state: update.state }; }); return newIO; }); } // IO_CHANGED - 개별 IO 값 변경 이벤트 (실시간) if (msg?.type === 'IO_CHANGED' && msg.data) { const { id, ioType, state } = msg.data; setIoPoints(prev => { const newIO = [...prev]; const idx = newIO.findIndex(p => p.id === id && p.type === ioType); if (idx >= 0) { newIO[idx] = { ...newIO[idx], state: state }; } return newIO; }); } // INTERLOCK_CHANGED - 인터락 값 변경 이벤트 (실시간) if (msg?.type === 'INTERLOCK_CHANGED' && msg.data) { const { axisIndex, lockIndex, state, hexValue } = msg.data; setInterlocks(prev => { const newInterlocks = [...prev]; const axisIdx = newInterlocks.findIndex(a => a.axisIndex === axisIndex); if (axisIdx >= 0) { const lockIdx = newInterlocks[axisIdx].locks.findIndex(l => l.id === lockIndex); if (lockIdx >= 0) { newInterlocks[axisIdx].locks[lockIdx] = { ...newInterlocks[axisIdx].locks[lockIdx], state: state }; newInterlocks[axisIdx].hexValue = hexValue; } } return newInterlocks; }); } }); return () => { unsubscribe(); }; }, []); const points = activeIOTab === 'interlock' ? [] : ioPoints.filter(p => p.type === (activeIOTab === 'in' ? 'input' : 'output')); // 인터락 토글 핸들러 (DIOMonitor.cs의 gvILXF_ItemClick과 동일) const handleInterlockToggle = async (axisIndex: number, lockIndex: number) => { try { const result = await comms.toggleInterlock(axisIndex, lockIndex); if (!result.success) { console.error('[IOMonitor] Interlock toggle failed:', result.message); } // 성공 시 INTERLOCK_CHANGED 이벤트로 UI가 자동 업데이트됨 } catch (error) { console.error('[IOMonitor] Interlock toggle error:', error); } }; return (
{/* Local Header / Controls */}

SYSTEM I/O MONITOR

{activeIOTab === 'interlock' ? `TOTAL AXES: ${interlocks.length}` : `TOTAL POINTS: ${ioPoints.length}`}
{isLoading ? (
LOADING IO POINTS...
) : activeIOTab === 'interlock' ? (
{interlocks.map(axis => (
{axis.axisName} ({axis.hexValue})
{axis.locks.map(lock => (
handleInterlockToggle(axis.axisIndex, lock.id)} className={` flex items-center gap-2 px-3 py-2 transition-all border rounded cursor-pointer hover:translate-x-0.5 hover:brightness-110 ${lock.state ? 'bg-red-500/20 border-red-500 text-red-400 shadow-[0_0_10px_rgba(239,68,68,0.3)]' : 'bg-slate-800/40 border-slate-700 text-slate-500 hover:border-slate-600'} `} >
{lock.name}
))}
))}
) : (
{points.map(p => (
onToggle(p.id, p.type)} className={` flex items-center gap-4 px-4 py-3 cursor-pointer transition-all border clip-tech-sm group hover:translate-x-1 ${p.state ? (p.type === 'output' ? 'bg-neon-green/10 border-neon-green text-neon-green shadow-[0_0_15px_rgba(10,255,0,0.2)]' : 'bg-neon-yellow/10 border-neon-yellow text-neon-yellow shadow-[0_0_15px_rgba(255,230,0,0.2)]') : 'bg-slate-900/40 border-slate-800 text-slate-500 hover:border-slate-600 hover:bg-slate-800/40'} `} > {/* LED Indicator */}
{/* ID Badge */}
{p.type === 'input' ? 'I' : 'Q'}{p.id.toString().padStart(2, '0')}
{/* Name */}
{p.name}
))}
)}
); };