feat: Add real-time IO/interlock updates, HW status display, and history page

- Implement real-time IO value updates via IOValueChanged event
- Add interlock toggle and real-time interlock change events
- Fix ToggleLight to check return value of DIO.SetRoomLight
- Add HW status display in Footer matching WinForms HWState
- Implement GetHWStatus API and 250ms broadcast interval
- Create HistoryPage React component for work history viewing
- Add GetHistoryData API for database queries
- Add date range selection, search, filter, and CSV export
- Add History button in Header navigation
- Add PickerMoveDialog component for manage operations
- Fix DataSet column names (idx, PRNATTACH, PRNVALID, qtymax)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-27 00:14:47 +09:00
parent bb67d04d90
commit 3bd35ad852
19 changed files with 2917 additions and 81 deletions

View File

@@ -1,12 +1,31 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, createContext, useContext } from 'react';
import { HashRouter, Routes, Route } from 'react-router-dom'; import { HashRouter, Routes, Route } from 'react-router-dom';
import { Layout } from './components/layout/Layout'; import { Layout } from './components/layout/Layout';
import { HomePage } from './pages/HomePage'; import { HomePage } from './pages/HomePage';
import { IOMonitorPage } from './pages/IOMonitorPage'; import { IOMonitorPage } from './pages/IOMonitorPage';
import { RecipePage } from './pages/RecipePage'; import { RecipePage } from './pages/RecipePage';
import { HistoryPage } from './pages/HistoryPage';
import { SystemState, Recipe, IOPoint, LogEntry, RobotTarget, ConfigItem } from './types'; import { SystemState, Recipe, IOPoint, LogEntry, RobotTarget, ConfigItem } from './types';
import { comms } from './communication'; import { comms } from './communication';
import { AlertProvider } from './contexts/AlertContext'; import { AlertProvider, useAlert } from './contexts/AlertContext';
import { PickerMoveDialog } from './components/PickerMoveDialog';
// PickerMoveDialog 전역 상태 Context
interface PickerMoveContextType {
isPickerMoveOpen: boolean;
openPickerMove: () => Promise<boolean>;
closePickerMove: () => void;
}
const PickerMoveContext = createContext<PickerMoveContextType | null>(null);
export const usePickerMove = () => {
const context = useContext(PickerMoveContext);
if (!context) {
throw new Error('usePickerMove must be used within PickerMoveProvider');
}
return context;
};
// --- MOCK DATA --- // --- MOCK DATA ---
@@ -35,7 +54,8 @@ const INITIAL_IO: IOPoint[] = [
// --- MAIN APP --- // --- MAIN APP ---
export default function App() { // 내부 App 컴포넌트 (AlertContext 내부에서 사용)
function AppContent() {
const [activeTab, setActiveTab] = useState<'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null>(null); const [activeTab, setActiveTab] = useState<'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null>(null);
const [systemState, setSystemState] = useState<SystemState>(SystemState.IDLE); const [systemState, setSystemState] = useState<SystemState>(SystemState.IDLE);
const [currentRecipe, setCurrentRecipe] = useState<Recipe>({ id: '0', name: 'No Recipe', lastModified: '-' }); const [currentRecipe, setCurrentRecipe] = useState<Recipe>({ id: '0', name: 'No Recipe', lastModified: '-' });
@@ -45,7 +65,49 @@ export default function App() {
const [currentTime, setCurrentTime] = useState(new Date()); const [currentTime, setCurrentTime] = useState(new Date());
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isHostConnected, setIsHostConnected] = useState(false); const [isHostConnected, setIsHostConnected] = useState(false);
const [isPickerMoveOpen, setIsPickerMoveOpen] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const { showAlert } = useAlert();
// PickerMoveDialog 열기 함수 (백엔드에서 openManage 호출)
const openPickerMove = async (): Promise<boolean> => {
try {
const result = await comms.openManage();
if (!result.success) {
showAlert({
type: 'error',
title: 'Cannot Open',
message: result.message
});
return false;
}
setIsPickerMoveOpen(true);
return true;
} catch (error: any) {
showAlert({
type: 'error',
title: 'Error',
message: error.message || 'Failed to open manage dialog'
});
return false;
}
};
// PickerMoveDialog 닫기 함수
const closePickerMove = async () => {
setIsPickerMoveOpen(false);
try {
const result = await comms.closeManage();
if (result.shouldAutoInit) {
const initResult = await comms.initializeDevice();
if (!initResult.success) {
console.error('[App] Auto-init failed:', initResult.message);
}
}
} catch (error) {
console.error('[App] closeManage error:', error);
}
};
// -- COMMUNICATION LAYER -- // -- COMMUNICATION LAYER --
useEffect(() => { useEffect(() => {
@@ -57,6 +119,16 @@ export default function App() {
addLog(msg.connected ? "HOST CONNECTED" : "HOST DISCONNECTED", msg.connected ? "info" : "warning"); addLog(msg.connected ? "HOST CONNECTED" : "HOST DISCONNECTED", msg.connected ? "info" : "warning");
} }
// AUTO_OPEN_MANAGE 이벤트 처리 - 백엔드에서 IDLE 상태 진입 시 피커 이동 필요할 때 전송
if (msg.type === 'AUTO_OPEN_MANAGE') {
console.log('[App] AUTO_OPEN_MANAGE event received:', msg.data?.reason);
addLog(`AUTO MANAGE: ${msg.data?.reason || 'Picker move required'}`, 'warning');
// 이미 열려있지 않은 경우에만 열기
if (!isPickerMoveOpen) {
openPickerMove();
}
}
if (msg.type === 'STATUS_UPDATE') { if (msg.type === 'STATUS_UPDATE') {
if (msg.position) { if (msg.position) {
setRobotTarget({ x: msg.position.x, y: msg.position.y, z: msg.position.z }); setRobotTarget({ x: msg.position.x, y: msg.position.y, z: msg.position.z });
@@ -85,7 +157,7 @@ export default function App() {
clearInterval(timer); clearInterval(timer);
unsubscribe(); unsubscribe();
}; };
}, []); }, [isPickerMoveOpen]);
// -- INITIALIZATION -- // -- INITIALIZATION --
useEffect(() => { useEffect(() => {
@@ -187,8 +259,14 @@ export default function App() {
} }
}; };
const pickerMoveContextValue: PickerMoveContextType = {
isPickerMoveOpen,
openPickerMove,
closePickerMove
};
return ( return (
<AlertProvider> <PickerMoveContext.Provider value={pickerMoveContextValue}>
<HashRouter> <HashRouter>
<Layout <Layout
currentTime={currentTime} currentTime={currentTime}
@@ -234,9 +312,28 @@ export default function App() {
path="/recipe" path="/recipe"
element={<RecipePage />} element={<RecipePage />}
/> />
<Route
path="/history"
element={<HistoryPage />}
/>
</Routes> </Routes>
</Layout> </Layout>
</HashRouter> </HashRouter>
{/* PickerMoveDialog - 전역에서 관리 */}
<PickerMoveDialog
isOpen={isPickerMoveOpen}
onClose={closePickerMove}
/>
</PickerMoveContext.Provider>
);
}
// 외부 App 컴포넌트 - AlertProvider로 감싸기
export default function App() {
return (
<AlertProvider>
<AppContent />
</AlertProvider> </AlertProvider>
); );
} }

View File

@@ -434,10 +434,10 @@ class CommunicationLayer {
// ===== VISION CONTROL METHODS ===== // ===== VISION CONTROL METHODS =====
private async sendVisionCommand(command: string, responseType: string): Promise<{ success: boolean; message: string }> { private async sendVisionCommand(command: string, responseType: string, methodName: string): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) { if (isWebView && machine) {
// WebView2 mode - direct call to C# methods const result = await (machine as any)[methodName]();
return { success: false, message: 'Vision commands not yet implemented in WebView2 mode' }; return JSON.parse(result);
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.isConnected) { if (!this.isConnected) {
@@ -465,45 +465,46 @@ class CommunicationLayer {
} }
public async cameraConnect(): Promise<{ success: boolean; message: string }> { public async cameraConnect(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_CONNECT', 'CAMERA_RESULT'); return this.sendVisionCommand('CAMERA_CONNECT', 'CAMERA_RESULT', 'CameraConnect');
} }
public async cameraDisconnect(): Promise<{ success: boolean; message: string }> { public async cameraDisconnect(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_DISCONNECT', 'CAMERA_RESULT'); return this.sendVisionCommand('CAMERA_DISCONNECT', 'CAMERA_RESULT', 'CameraDisconnect');
} }
public async cameraGetImage(): Promise<{ success: boolean; message: string }> { public async cameraGetImage(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_GET_IMAGE', 'CAMERA_RESULT'); return this.sendVisionCommand('CAMERA_GET_IMAGE', 'CAMERA_RESULT', 'CameraGetImage');
} }
public async cameraLiveView(): Promise<{ success: boolean; message: string }> { public async cameraLiveView(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_LIVE_VIEW', 'CAMERA_RESULT'); return this.sendVisionCommand('CAMERA_LIVE_VIEW', 'CAMERA_RESULT', 'CameraLiveView');
} }
public async cameraReadTest(): Promise<{ success: boolean; message: string }> { public async cameraReadTest(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_READ_TEST', 'CAMERA_RESULT'); return this.sendVisionCommand('CAMERA_READ_TEST', 'CAMERA_RESULT', 'CameraReadTest');
} }
public async keyenceTriggerOn(): Promise<{ success: boolean; message: string }> { public async keyenceTriggerOn(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('KEYENCE_TRIGGER_ON', 'KEYENCE_RESULT'); return this.sendVisionCommand('KEYENCE_TRIGGER_ON', 'KEYENCE_RESULT', 'KeyenceTriggerOn');
} }
public async keyenceTriggerOff(): Promise<{ success: boolean; message: string }> { public async keyenceTriggerOff(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('KEYENCE_TRIGGER_OFF', 'KEYENCE_RESULT'); return this.sendVisionCommand('KEYENCE_TRIGGER_OFF', 'KEYENCE_RESULT', 'KeyenceTriggerOff');
} }
public async keyenceGetImage(): Promise<{ success: boolean; message: string }> { public async keyenceGetImage(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('KEYENCE_GET_IMAGE', 'KEYENCE_RESULT'); return this.sendVisionCommand('KEYENCE_GET_IMAGE', 'KEYENCE_RESULT', 'KeyenceGetImage');
} }
public async keyenceSaveImage(): Promise<{ success: boolean; message: string }> { public async keyenceSaveImage(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('KEYENCE_SAVE_IMAGE', 'KEYENCE_RESULT'); return this.sendVisionCommand('KEYENCE_SAVE_IMAGE', 'KEYENCE_RESULT', 'KeyenceSaveImage');
} }
// Light, Manual Print, Cancel Job commands // Light, Manual Print, Cancel Job commands
public async toggleLight(): Promise<{ success: boolean; message: string }> { public async toggleLight(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) { if (isWebView && machine) {
return { success: false, message: 'Light control not yet implemented in WebView2 mode' }; const result = await machine.ToggleLight();
return JSON.parse(result);
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
@@ -537,7 +538,18 @@ class CommunicationLayer {
count: number; count: number;
}): Promise<{ success: boolean; message: string }> { }): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) { if (isWebView && machine) {
return { success: false, message: 'Manual print not yet implemented in WebView2 mode' }; const result = await machine.ExecuteManualPrint(
printData.sid,
printData.venderLot,
printData.qty,
printData.mfg,
printData.rid,
printData.spy,
printData.partNo,
printData.printer,
printData.count
);
return JSON.parse(result);
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
@@ -564,7 +576,8 @@ class CommunicationLayer {
public async cancelJob(): Promise<{ success: boolean; message: string }> { public async cancelJob(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) { if (isWebView && machine) {
return { success: false, message: 'Cancel job not yet implemented in WebView2 mode' }; const result = await machine.CancelJob();
return JSON.parse(result);
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
@@ -588,7 +601,8 @@ class CommunicationLayer {
public async openManage(): Promise<{ success: boolean; message: string }> { public async openManage(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) { if (isWebView && machine) {
return { success: false, message: 'Manage not yet implemented in WebView2 mode' }; const result = await machine.OpenManage();
return JSON.parse(result);
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
@@ -612,7 +626,8 @@ class CommunicationLayer {
public async openManual(): Promise<{ success: boolean; message: string }> { public async openManual(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) { if (isWebView && machine) {
return { success: false, message: 'Manual not yet implemented in WebView2 mode' }; const result = await machine.OpenManual();
return JSON.parse(result);
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
@@ -636,7 +651,8 @@ class CommunicationLayer {
public async openLogViewer(): Promise<{ success: boolean; message: string }> { public async openLogViewer(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) { if (isWebView && machine) {
return { success: false, message: 'Log viewer not yet implemented in WebView2 mode' }; const result = await machine.OpenLogViewer();
return JSON.parse(result);
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
@@ -658,9 +674,10 @@ class CommunicationLayer {
} }
} }
private async openFolder(command: string): Promise<{ success: boolean; message: string }> { private async openFolder(command: string, methodName: string): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) { if (isWebView && machine) {
return { success: false, message: 'Folder open not yet implemented in WebView2 mode' }; const result = await (machine as any)[methodName]();
return JSON.parse(result);
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
@@ -683,19 +700,323 @@ class CommunicationLayer {
} }
public async openProgramFolder(): Promise<{ success: boolean; message: string }> { public async openProgramFolder(): Promise<{ success: boolean; message: string }> {
return this.openFolder('OPEN_PROGRAM_FOLDER'); return this.openFolder('OPEN_PROGRAM_FOLDER', 'OpenProgramFolder');
} }
public async openLogFolder(): Promise<{ success: boolean; message: string }> { public async openLogFolder(): Promise<{ success: boolean; message: string }> {
return this.openFolder('OPEN_LOG_FOLDER'); return this.openFolder('OPEN_LOG_FOLDER', 'OpenLogFolder');
} }
public async openScreenshotFolder(): Promise<{ success: boolean; message: string }> { public async openScreenshotFolder(): Promise<{ success: boolean; message: string }> {
return this.openFolder('OPEN_SCREENSHOT_FOLDER'); return this.openFolder('OPEN_SCREENSHOT_FOLDER', 'OpenScreenshotFolder');
} }
public async openSavedDataFolder(): Promise<{ success: boolean; message: string }> { public async openSavedDataFolder(): Promise<{ success: boolean; message: string }> {
return this.openFolder('OPEN_SAVED_DATA_FOLDER'); return this.openFolder('OPEN_SAVED_DATA_FOLDER', 'OpenSavedDataFolder');
}
// ===== PICKER MOVE METHODS =====
public async getPickerStatus(): Promise<void> {
if (isWebView && machine) {
const result = await machine.GetPickerStatus();
this.notifyListeners({ type: 'PICKER_STATUS', data: JSON.parse(result) });
} else {
this.ws?.send(JSON.stringify({ type: 'GET_PICKER_STATUS' }));
}
}
private async sendPickerCommand(command: string, responseType: string, methodName: string): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
const result = await (machine as any)[methodName]();
return JSON.parse(result);
} else {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
setTimeout(() => {
if (!this.isConnected) reject({ success: false, message: "WebSocket connection timeout" });
}, 2000);
}
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject({ success: false, message: "Picker command timeout" });
}, 10000);
const handler = (data: any) => {
if (data.type === responseType) {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: command }));
});
}
}
public async pickerMoveLeft(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_MOVE_LEFT', 'PICKER_RESULT', 'PickerMoveLeft');
}
public async pickerMoveLeftWait(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_MOVE_LEFT_WAIT', 'PICKER_RESULT', 'PickerMoveLeftWait');
}
public async pickerMoveCenter(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_MOVE_CENTER', 'PICKER_RESULT', 'PickerMoveCenter');
}
public async pickerMoveRightWait(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_MOVE_RIGHT_WAIT', 'PICKER_RESULT', 'PickerMoveRightWait');
}
public async pickerMoveRight(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_MOVE_RIGHT', 'PICKER_RESULT', 'PickerMoveRight');
}
public async pickerJogStart(direction: 'up' | 'down' | 'left' | 'right'): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
const result = await machine.PickerJogStart(direction);
return JSON.parse(result);
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject({ success: false, message: "Jog command timeout" });
}, 5000);
const handler = (data: any) => {
if (data.type === 'PICKER_JOG_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'PICKER_JOG_START', direction }));
});
}
}
public async pickerJogStop(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_JOG_STOP', 'PICKER_JOG_RESULT', 'PickerJogStop');
}
public async pickerStop(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_STOP', 'PICKER_RESULT', 'PickerStop');
}
public async cancelVisionValidation(side: 'left' | 'right'): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
const result = await machine.CancelVisionValidation(side);
return JSON.parse(result);
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject({ success: false, message: "Vision cancel timeout" });
}, 5000);
const handler = (data: any) => {
if (data.type === 'VISION_CANCEL_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'CANCEL_VISION_VALIDATION', side }));
});
}
}
public async pickerManagePosition(side: 'left' | 'right'): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
const result = await machine.PickerManagePosition(side);
return JSON.parse(result);
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject({ success: false, message: "Manage position timeout" });
}, 30000); // Longer timeout for motion
const handler = (data: any) => {
if (data.type === 'PICKER_MANAGE_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'PICKER_MANAGE_POSITION', side }));
});
}
}
public async pickerManageReturn(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_MANAGE_RETURN', 'PICKER_MANAGE_RESULT', 'PickerManageReturn');
}
public async pickerZHome(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_Z_HOME', 'PICKER_RESULT', 'PickerZHome');
}
public async pickerZZero(): Promise<{ success: boolean; message: string }> {
return this.sendPickerCommand('PICKER_Z_ZERO', 'PICKER_RESULT', 'PickerZZero');
}
public async pickerTestPrint(side: 'left' | 'right'): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
const result = await machine.PickerTestPrint(side);
return JSON.parse(result);
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject({ success: false, message: "Test print timeout" });
}, 10000);
const handler = (data: any) => {
if (data.type === 'PICKER_PRINT_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'PICKER_TEST_PRINT', side }));
});
}
}
public async canCloseManage(): Promise<{ canClose: boolean; message: string }> {
if (isWebView && machine) {
const result = await machine.CanCloseManage();
return JSON.parse(result);
} else {
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve({ canClose: true, message: '' });
}, 5000);
const handler = (data: any) => {
if (data.type === 'CAN_CLOSE_MANAGE_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'CAN_CLOSE_MANAGE' }));
});
}
}
// Close manage dialog - backend handles flag clear and auto-init check
public async closeManage(): Promise<{ shouldAutoInit: boolean }> {
if (isWebView && machine) {
const result = await machine.CloseManage();
return JSON.parse(result);
} else {
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve({ shouldAutoInit: false });
}, 5000);
const handler = (data: any) => {
if (data.type === 'CLOSE_MANAGE_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'CLOSE_MANAGE' }));
});
}
}
// ===== HISTORY DATA =====
// Get history data from database (fHistory.cs의 refreshList와 동일)
public async getHistoryData(startDate: string, endDate: string, search: string): Promise<{ success: boolean; data: any[]; mcName?: string; message?: string }> {
if (isWebView && machine) {
const result = await machine.GetHistoryData(startDate, endDate, search);
return JSON.parse(result);
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject({ success: false, message: 'History data fetch timeout', data: [] });
}, 30000); // 30 second timeout for potentially large data
const handler = (data: any) => {
if (data.type === 'HISTORY_DATA_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'GET_HISTORY_DATA', startDate, endDate, search }));
});
}
}
// ===== INTERLOCK METHODS =====
// Toggle interlock (DIOMonitor.cs의 gvILXF_ItemClick과 동일)
public async toggleInterlock(axisIndex: number, lockIndex: number): Promise<{ success: boolean; newState?: boolean; message?: string }> {
if (isWebView && machine) {
const result = await machine.ToggleInterlock(axisIndex, lockIndex);
return JSON.parse(result);
} else {
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve({ success: false, message: 'Timeout' });
}, 5000);
const handler = (data: any) => {
if (data.type === 'TOGGLE_INTERLOCK_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'TOGGLE_INTERLOCK', axisIndex, lockIndex }));
});
}
}
// Get interlock list
public async getInterlockList(): Promise<string> {
if (isWebView && machine) {
return await machine.GetInterlockList();
} else {
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve('[]');
}, 5000);
const handler = (data: any) => {
if (data.type === 'INTERLOCK_LIST_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(JSON.stringify(data.data));
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'GET_INTERLOCK_LIST' }));
});
}
} }
} }

View File

@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
import { Package, X, ChevronRight } from 'lucide-react'; import { Package, X, ChevronRight } from 'lucide-react';
import { comms } from '../communication'; import { comms } from '../communication';
import { useAlert } from '../contexts/AlertContext'; import { useAlert } from '../contexts/AlertContext';
import { usePickerMove } from '../App';
interface FunctionMenuProps { interface FunctionMenuProps {
isOpen: boolean; isOpen: boolean;
@@ -12,6 +13,7 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null); const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const { showAlert } = useAlert(); const { showAlert } = useAlert();
const { openPickerMove } = usePickerMove();
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
@@ -29,8 +31,6 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
}; };
}, [isOpen, onClose]); }, [isOpen, onClose]);
if (!isOpen) return null;
const handleCommand = async (commandFn: () => Promise<{ success: boolean; message: string }>, actionName: string) => { const handleCommand = async (commandFn: () => Promise<{ success: boolean; message: string }>, actionName: string) => {
try { try {
const result = await commandFn(); const result = await commandFn();
@@ -55,6 +55,8 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
}; };
return ( return (
<>
{isOpen && (
<div <div
ref={menuRef} ref={menuRef}
className="absolute top-20 left-1/2 -translate-x-1/2 z-50 bg-black/95 backdrop-blur-md border border-neon-blue/50 rounded-lg shadow-glow-blue min-w-[300px]" className="absolute top-20 left-1/2 -translate-x-1/2 z-50 bg-black/95 backdrop-blur-md border border-neon-blue/50 rounded-lg shadow-glow-blue min-w-[300px]"
@@ -77,7 +79,12 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
<div className="p-2"> <div className="p-2">
{/* Manage */} {/* Manage */}
<button <button
onClick={() => handleCommand(() => comms.openManage(), 'Manage')} onClick={async () => {
const success = await openPickerMove();
if (success) {
onClose();
}
}}
className="w-full flex items-center justify-between px-4 py-3 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors font-tech" className="w-full flex items-center justify-between px-4 py-3 text-left text-slate-300 hover:bg-neon-blue/10 hover:text-neon-blue rounded transition-colors font-tech"
> >
<span>Manage</span> <span>Manage</span>
@@ -136,5 +143,7 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
</div> </div>
</div> </div>
</div> </div>
)}
</>
); );
}; };

View File

@@ -28,7 +28,7 @@ export const InitializeModal: React.FC<InitializeModalProps> = ({ isOpen, onClos
]); ]);
const [isInitializing, setIsInitializing] = useState(false); const [isInitializing, setIsInitializing] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [statusInterval, setStatusInterval] = useState<NodeJS.Timeout | null>(null); const [statusInterval, setStatusInterval] = useState<ReturnType<typeof setInterval> | null>(null);
// Reset state when modal closes // Reset state when modal closes
useEffect(() => { useEffect(() => {

View File

@@ -0,0 +1,446 @@
import React, { useState, useEffect, useCallback } from 'react';
import { X, Move, Square, ChevronLeft, ChevronRight, ChevronUp, ChevronDown, Home, Printer, XCircle, Settings } from 'lucide-react';
import { comms } from '../communication';
import { useAlert } from '../contexts/AlertContext';
interface PickerMoveDialogProps {
isOpen: boolean;
onClose: () => void;
}
interface PickerStatus {
xEnabled: boolean;
zEnabled: boolean;
pickerSafe: boolean;
managementEnabled: boolean;
manPosL: boolean;
manPosR: boolean;
}
export const PickerMoveDialog: React.FC<PickerMoveDialogProps> = ({ isOpen, onClose }) => {
const { showAlert, showConfirm } = useAlert();
const [status, setStatus] = useState<PickerStatus>({
xEnabled: false,
zEnabled: false,
pickerSafe: false,
managementEnabled: false,
manPosL: false,
manPosR: false
});
const [isJogging, setIsJogging] = useState(false);
// Subscribe to picker status updates
useEffect(() => {
if (!isOpen) return;
const unsubscribe = comms.subscribe((data: any) => {
if (data.type === 'PICKER_STATUS') {
setStatus(data.data);
}
});
// Request initial status
comms.getPickerStatus();
// Set up polling interval
const intervalId = setInterval(() => {
comms.getPickerStatus();
}, 200);
return () => {
unsubscribe();
clearInterval(intervalId);
};
}, [isOpen]);
const handleClose = async () => {
try {
const result = await comms.canCloseManage();
if (!result.canClose) {
showAlert({
type: 'error',
title: 'Cannot Close',
message: result.message || 'Printer motion is in management position.\nReturn to position and try again.'
});
return;
}
onClose();
} catch (error) {
// If check fails, still allow close
onClose();
}
};
// === Position Move Buttons ===
const handleMoveLeft = async () => {
const result = await comms.pickerMoveLeft();
if (!result.success) {
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
}
};
const handleMoveLeftWait = async () => {
const result = await comms.pickerMoveLeftWait();
if (!result.success) {
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
}
};
const handleMoveCenter = async () => {
const result = await comms.pickerMoveCenter();
if (!result.success) {
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
}
};
const handleMoveRightWait = async () => {
const result = await comms.pickerMoveRightWait();
if (!result.success) {
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
}
};
const handleMoveRight = async () => {
const result = await comms.pickerMoveRight();
if (!result.success) {
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
}
};
// === Jog Control ===
const handleJogStart = useCallback(async (direction: 'up' | 'down' | 'left' | 'right') => {
setIsJogging(true);
await comms.pickerJogStart(direction);
}, []);
const handleJogStop = useCallback(async () => {
setIsJogging(false);
await comms.pickerJogStop();
}, []);
const handleStop = async () => {
await comms.pickerStop();
};
// === Vision Validation Cancel ===
const handleCancelVisionL = async () => {
const confirmed = await showConfirm({
title: 'Cancel Vision Validation',
message: 'Do you want to cancel LEFT-QR code verification?'
});
if (confirmed) {
const result = await comms.cancelVisionValidation('left');
if (result.success) {
showAlert({ type: 'success', title: 'Cancelled', message: 'LEFT-QR verification cancelled' });
} else {
showAlert({ type: 'error', title: 'Cancel Failed', message: result.message });
}
}
};
const handleCancelVisionR = async () => {
const confirmed = await showConfirm({
title: 'Cancel Vision Validation',
message: 'Do you want to cancel RIGHT-QR code verification?'
});
if (confirmed) {
const result = await comms.cancelVisionValidation('right');
if (result.success) {
showAlert({ type: 'success', title: 'Cancelled', message: 'RIGHT-QR verification cancelled' });
} else {
showAlert({ type: 'error', title: 'Cancel Failed', message: result.message });
}
}
};
// === Management Position ===
const handleManagePosL = async () => {
const result = await comms.pickerManagePosition('left');
if (!result.success) {
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
}
};
const handleManagePosR = async () => {
const result = await comms.pickerManagePosition('right');
if (!result.success) {
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
}
};
const handleManagePosReturn = async () => {
const result = await comms.pickerManageReturn();
if (!result.success) {
showAlert({ type: 'error', title: 'Return Failed', message: result.message });
}
};
// === Z-Axis Control ===
const handleZHome = async () => {
const confirmed = await showConfirm({
title: 'Z-Axis Home',
message: 'Do you want to proceed with picker Z-axis home search?'
});
if (confirmed) {
const result = await comms.pickerZHome();
if (!result.success) {
showAlert({ type: 'error', title: 'Home Failed', message: result.message });
}
}
};
const handleZZero = async () => {
const confirmed = await showConfirm({
title: 'Z-Axis Zero',
message: 'Do you want to move picker Z-axis to coordinate:0?'
});
if (confirmed) {
const result = await comms.pickerZZero();
if (!result.success) {
showAlert({ type: 'error', title: 'Move Failed', message: result.message });
}
}
};
// === Print Test ===
const handlePrintL = async () => {
const result = await comms.pickerTestPrint('left');
if (!result.success) {
showAlert({ type: 'error', title: 'Print Failed', message: result.message });
}
};
const handlePrintR = async () => {
const result = await comms.pickerTestPrint('right');
if (!result.success) {
showAlert({ type: 'error', title: 'Print Failed', message: result.message });
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
onClick={handleClose}
/>
{/* Dialog */}
<div className="relative bg-black/95 backdrop-blur-md border-2 border-neon-blue rounded-lg shadow-2xl w-full max-w-4xl mx-4 animate-fadeIn">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center gap-3">
<Move className="w-6 h-6 text-neon-blue" />
<h2 className="text-xl font-tech font-bold text-white uppercase tracking-wider">
Picker(X) Movement and Management
</h2>
</div>
<button
onClick={handleClose}
className="text-slate-400 hover:text-white transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Content */}
<div className="p-4">
{/* Row 1: Jog Controls */}
<div className="grid grid-cols-5 gap-2 mb-2">
{/* Z Up */}
<button
onMouseDown={() => handleJogStart('up')}
onMouseUp={handleJogStop}
onMouseLeave={handleJogStop}
disabled={!status.zEnabled}
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-purple-400 transition-colors"
>
<ChevronUp className="w-16 h-16" strokeWidth={3} />
</button>
{/* X Left Jog */}
<button
onMouseDown={() => handleJogStart('left')}
onMouseUp={handleJogStop}
onMouseLeave={handleJogStop}
disabled={!status.xEnabled}
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-blue-900 transition-colors"
>
<ChevronLeft className="w-16 h-16" strokeWidth={3} />
</button>
{/* Stop */}
<button
onClick={handleStop}
className={`h-24 flex items-center justify-center border border-white/10 rounded-lg transition-colors ${
status.pickerSafe
? 'bg-green-500/30 hover:bg-green-500/40 text-green-400'
: 'bg-red-500/30 hover:bg-red-500/40 text-red-400'
}`}
>
<Square className="w-16 h-16" fill="currentColor" />
</button>
{/* X Right Jog */}
<button
onMouseDown={() => handleJogStart('right')}
onMouseUp={handleJogStop}
onMouseLeave={handleJogStop}
disabled={!status.xEnabled}
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-blue-900 transition-colors"
>
<ChevronRight className="w-16 h-16" strokeWidth={3} />
</button>
{/* Z Down */}
<button
onMouseDown={() => handleJogStart('down')}
onMouseUp={handleJogStop}
onMouseLeave={handleJogStop}
disabled={!status.zEnabled}
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-purple-400 transition-colors"
>
<ChevronDown className="w-16 h-16" strokeWidth={3} />
</button>
</div>
{/* Row 2: Position Buttons */}
<div className="grid grid-cols-5 gap-2 mb-2">
<button
onClick={handleMoveLeft}
disabled={!status.managementEnabled}
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
>
Left
</button>
<button
onClick={handleMoveLeftWait}
disabled={!status.managementEnabled}
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
>
Wait
</button>
<button
onClick={handleMoveCenter}
disabled={!status.managementEnabled}
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-green-400 font-tech text-2xl font-bold transition-colors"
>
Center
</button>
<button
onClick={handleMoveRightWait}
disabled={!status.managementEnabled}
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
>
Wait
</button>
<button
onClick={handleMoveRight}
disabled={!status.managementEnabled}
className="h-24 flex items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed border border-white/10 rounded-lg text-white font-tech text-2xl font-bold transition-colors"
>
Right
</button>
</div>
{/* Row 3: Vision Cancel & Management Position */}
<div className="grid grid-cols-5 gap-2 mb-2">
<button
onClick={handleCancelVisionL}
className="h-24 flex flex-col items-center justify-center bg-amber-900/30 hover:bg-amber-900/50 border border-amber-500/50 rounded-lg text-amber-300 font-tech font-bold transition-colors"
>
<XCircle className="w-6 h-6 mb-1" />
<span className="text-sm">Vision Validation</span>
<span className="text-sm">Cancel(L)</span>
</button>
<button
onClick={handleManagePosL}
disabled={!status.managementEnabled}
className="h-24 flex flex-col items-center justify-center bg-purple-900/30 hover:bg-purple-900/50 disabled:opacity-50 disabled:cursor-not-allowed border border-purple-500/50 rounded-lg text-purple-300 font-tech font-bold transition-colors"
>
<Settings className="w-6 h-6 mb-1" />
<span className="text-sm">Print Management</span>
<span className="text-sm">Position(L)</span>
</button>
<button
onClick={handleManagePosReturn}
disabled={!status.managementEnabled}
className="h-24 flex flex-col items-center justify-center bg-purple-900/30 hover:bg-purple-900/50 disabled:opacity-50 disabled:cursor-not-allowed border border-purple-500/50 rounded-lg text-purple-300 font-tech font-bold transition-colors"
>
<Home className="w-6 h-6 mb-1" />
<span className="text-sm">Management Position</span>
<span className="text-sm">Return</span>
</button>
<button
onClick={handleManagePosR}
disabled={!status.managementEnabled}
className="h-24 flex flex-col items-center justify-center bg-purple-900/30 hover:bg-purple-900/50 disabled:opacity-50 disabled:cursor-not-allowed border border-purple-500/50 rounded-lg text-purple-300 font-tech font-bold transition-colors"
>
<Settings className="w-6 h-6 mb-1" />
<span className="text-sm">Print Management</span>
<span className="text-sm">Position(R)</span>
</button>
<button
onClick={handleCancelVisionR}
className="h-24 flex flex-col items-center justify-center bg-amber-900/30 hover:bg-amber-900/50 border border-amber-500/50 rounded-lg text-amber-300 font-tech font-bold transition-colors"
>
<XCircle className="w-6 h-6 mb-1" />
<span className="text-sm">Vision Validation</span>
<span className="text-sm">Cancel(R)</span>
</button>
</div>
{/* Row 4: Z-Home, Print, Z-Zero */}
<div className="grid grid-cols-5 gap-2">
<button
onClick={handleZHome}
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
>
<Home className="w-6 h-6 mb-1" />
<span>Z-HOME</span>
</button>
<button
onClick={handlePrintL}
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
>
<Printer className="w-6 h-6 mb-1" />
<span>PRINT(L)</span>
</button>
<button
disabled
className="h-24 flex items-center justify-center bg-slate-800/30 border border-white/10 rounded-lg text-slate-500 font-tech text-lg font-bold cursor-not-allowed"
>
--
</button>
<button
onClick={handlePrintR}
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
>
<Printer className="w-6 h-6 mb-1" />
<span>PRINT(R)</span>
</button>
<button
onClick={handleZZero}
className="h-24 flex flex-col items-center justify-center bg-slate-800/50 hover:bg-slate-700/50 border border-white/10 rounded-lg text-white font-tech text-lg font-bold transition-colors"
>
<span className="text-2xl mb-1">0</span>
<span>Z-ZERO</span>
</button>
</div>
</div>
{/* Footer */}
<div className="flex justify-end p-4 border-t border-white/10">
<button
onClick={handleClose}
className="px-6 py-2 bg-slate-700 hover:bg-slate-600 border border-white/20 text-white font-tech font-bold rounded transition-colors"
>
Close
</button>
</div>
</div>
</div>
);
};

View File

@@ -1,13 +1,15 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
interface PanelHeaderProps { interface PanelHeaderProps {
title: string; title: string;
icon: LucideIcon; icon: LucideIcon;
className?: string;
children?: ReactNode;
} }
export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon }) => ( export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon, className, children }) => (
<div className="flex items-center gap-3 mb-6 border-b border-white/10 pb-2"> <div className={`flex items-center gap-3 mb-6 border-b border-white/10 pb-2 ${className || ''}`}>
<div className="text-neon-blue animate-pulse"> <div className="text-neon-blue animate-pulse">
<Icon className="w-5 h-5" /> <Icon className="w-5 h-5" />
</div> </div>
@@ -15,5 +17,6 @@ export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon }) =
{title} {title}
</h2> </h2>
<div className="flex-1 h-px bg-gradient-to-r from-neon-blue/50 to-transparent"></div> <div className="flex-1 h-px bg-gradient-to-r from-neon-blue/50 to-transparent"></div>
{children}
</div> </div>
); );

View File

@@ -1,24 +1,68 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { RobotTarget } from '../../types'; import { RobotTarget } from '../../types';
import { comms } from '../../communication';
// HW 상태 타입 (윈폼 HWState와 동일)
// status: 0=SET(미설정/회색), 1=ON(연결/녹색), 2=TRIG(트리거/노란색), 3=OFF(연결안됨/빨간색)
interface HWItem {
name: string;
title: string;
status: number;
}
interface FooterProps { interface FooterProps {
isHostConnected: boolean; isHostConnected: boolean;
robotTarget: RobotTarget; robotTarget: RobotTarget;
} }
// 상태에 따른 LED 색상 반환
const getStatusColor = (status: number): { bg: string; shadow: string; text: string } => {
switch (status) {
case 1: // ON - 녹색
return { bg: 'bg-neon-green', shadow: 'shadow-[0_0_5px_#0aff00]', text: 'text-green-400' };
case 2: // TRIG - 노란색
return { bg: 'bg-yellow-400', shadow: 'shadow-[0_0_5px_#facc15]', text: 'text-yellow-400' };
case 3: // OFF - 빨간색
return { bg: 'bg-red-500', shadow: 'shadow-[0_0_5px_#ef4444]', text: 'text-red-400' };
default: // SET - 회색 (미설정)
return { bg: 'bg-gray-500', shadow: '', text: 'text-gray-400' };
}
};
export const Footer: React.FC<FooterProps> = ({ isHostConnected, robotTarget }) => { export const Footer: React.FC<FooterProps> = ({ isHostConnected, robotTarget }) => {
const [hwStatus, setHwStatus] = useState<HWItem[]>([]);
useEffect(() => {
// HW_STATUS_UPDATE 이벤트 구독
const unsubscribe = comms.subscribe((msg: any) => {
if (msg?.type === 'HW_STATUS_UPDATE' && msg.data) {
setHwStatus(msg.data);
}
});
return () => {
unsubscribe();
};
}, []);
return ( return (
<footer className="absolute bottom-0 left-0 right-0 h-10 bg-black/80 border-t border-neon-blue/30 flex items-center px-6 justify-between z-40 backdrop-blur text-xs font-mono text-slate-400"> <footer className="absolute bottom-0 left-0 right-0 h-10 bg-black/80 border-t border-neon-blue/30 flex items-center px-6 justify-between z-40 backdrop-blur text-xs font-mono text-slate-400">
<div className="flex gap-6"> <div className="flex gap-4">
{['PLC', 'MOTION', 'VISION', 'LIGHT'].map(hw => ( {/* H/W 상태 표시 (윈폼 HWState와 동일) */}
<div key={hw} className="flex items-center gap-2"> {hwStatus.map((hw) => {
<div className="w-2 h-2 bg-neon-green rounded-full shadow-[0_0_5px_#0aff00]"></div> const colors = getStatusColor(hw.status);
<span className="font-bold text-slate-300">{hw}</span> return (
<div key={hw.name} className="flex items-center gap-1.5" title={`${hw.name}: ${hw.title}`}>
<div className={`w-2 h-2 rounded-full transition-all ${colors.bg} ${colors.shadow}`}></div>
<span className={`font-bold ${colors.text}`}>{hw.name}</span>
<span className="text-[10px] text-slate-500">{hw.title}</span>
</div> </div>
))} );
<div className="flex items-center gap-2"> })}
{/* HOST 연결 상태 */}
<div className="flex items-center gap-1.5 ml-2 pl-2 border-l border-slate-700">
<div className={`w-2 h-2 rounded-full transition-all ${isHostConnected ? 'bg-neon-green shadow-[0_0_5px_#0aff00]' : 'bg-red-500 shadow-[0_0_5px_#ff0000] animate-pulse'}`}></div> <div className={`w-2 h-2 rounded-full transition-all ${isHostConnected ? 'bg-neon-green shadow-[0_0_5px_#0aff00]' : 'bg-red-500 shadow-[0_0_5px_#ff0000] animate-pulse'}`}></div>
<span className={`font-bold ${isHostConnected ? 'text-slate-300' : 'text-red-400'}`}>HOST</span> <span className={`font-bold ${isHostConnected ? 'text-green-400' : 'text-red-400'}`}>HOST</span>
</div> </div>
</div> </div>
<div className="flex gap-8 text-neon-blue"> <div className="flex gap-8 text-neon-blue">

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { Activity, Settings, Move, Camera, Layers, Cpu, Target, Lightbulb, Printer, XCircle, Package, BookOpen } from 'lucide-react'; import { Activity, Settings, Move, Camera, Layers, Cpu, Target, Lightbulb, Printer, XCircle, Package, BookOpen, History, Clock } from 'lucide-react';
import { VisionMenu } from '../VisionMenu'; import { VisionMenu } from '../VisionMenu';
import { FunctionMenu } from '../FunctionMenu'; import { FunctionMenu } from '../FunctionMenu';
import { ManualPrintDialog, PrintData } from '../ManualPrintDialog'; import { ManualPrintDialog, PrintData } from '../ManualPrintDialog';
@@ -127,6 +127,20 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
<XCircle className="w-5 h-5" /> <XCircle className="w-5 h-5" />
<span className="leading-tight">CANCEL</span> <span className="leading-tight">CANCEL</span>
</button> </button>
<button
onClick={() => {
navigate('/history');
onTabChange(null);
}}
className={`flex flex-col items-center justify-center gap-1 px-3 py-2 rounded-xl font-tech font-bold text-[10px] transition-all border border-transparent min-w-[70px] ${location.pathname === '/history'
? 'bg-neon-blue/10 text-neon-blue border-neon-blue shadow-glow-blue'
: 'text-slate-400 hover:text-purple-400 hover:bg-white/5'
}`}
title="View Work History"
>
<Clock className="w-5 h-5" />
<span className="leading-tight">HISTORY</span>
</button>
</div> </div>
{/* Main Navigation */} {/* Main Navigation */}

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, ReactNode } from 'react'; import React, { createContext, useContext, useState, ReactNode, useCallback, useRef } from 'react';
interface AlertConfig { interface AlertConfig {
type: 'success' | 'error' | 'warning' | 'info'; type: 'success' | 'error' | 'warning' | 'info';
@@ -6,8 +6,14 @@ interface AlertConfig {
message: string; message: string;
} }
interface ConfirmConfig {
title: string;
message: string;
}
interface AlertContextType { interface AlertContextType {
showAlert: (config: AlertConfig) => void; showAlert: (config: AlertConfig) => void;
showConfirm: (config: ConfirmConfig) => Promise<boolean>;
} }
const AlertContext = createContext<AlertContextType | undefined>(undefined); const AlertContext = createContext<AlertContextType | undefined>(undefined);
@@ -26,6 +32,8 @@ interface AlertProviderProps {
export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => { export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => {
const [alertConfig, setAlertConfig] = useState<(AlertConfig & { isOpen: boolean }) | null>(null); const [alertConfig, setAlertConfig] = useState<(AlertConfig & { isOpen: boolean }) | null>(null);
const [confirmConfig, setConfirmConfig] = useState<(ConfirmConfig & { isOpen: boolean }) | null>(null);
const confirmResolveRef = useRef<((value: boolean) => void) | null>(null);
const showAlert = (config: AlertConfig) => { const showAlert = (config: AlertConfig) => {
setAlertConfig({ ...config, isOpen: true }); setAlertConfig({ ...config, isOpen: true });
@@ -35,9 +43,25 @@ export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => {
setAlertConfig(null); setAlertConfig(null);
}; };
const showConfirm = useCallback((config: ConfirmConfig): Promise<boolean> => {
return new Promise((resolve) => {
confirmResolveRef.current = resolve;
setConfirmConfig({ ...config, isOpen: true });
});
}, []);
const handleConfirm = (result: boolean) => {
setConfirmConfig(null);
if (confirmResolveRef.current) {
confirmResolveRef.current(result);
confirmResolveRef.current = null;
}
};
return ( return (
<AlertContext.Provider value={{ showAlert }}> <AlertContext.Provider value={{ showAlert, showConfirm }}>
{children} {children}
{/* Alert Dialog */}
{alertConfig && ( {alertConfig && (
<div className="fixed inset-0 z-[100] flex items-center justify-center"> <div className="fixed inset-0 z-[100] flex items-center justify-center">
{/* Backdrop */} {/* Backdrop */}
@@ -77,6 +101,53 @@ export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => {
</div> </div>
</div> </div>
)} )}
{/* Confirm Dialog */}
{confirmConfig && (
<div className="fixed inset-0 z-[100] flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
onClick={() => handleConfirm(false)}
/>
{/* Dialog */}
<div className="relative bg-black/95 backdrop-blur-md border-2 border-amber-500 rounded-lg shadow-2xl max-w-md w-full mx-4 animate-fadeIn">
{/* Icon and Title */}
<div className="flex items-center gap-4 p-6 border-b border-white/10">
<svg className="w-12 h-12 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h2 className="text-xl font-tech font-bold text-white uppercase tracking-wider">
{confirmConfig.title}
</h2>
</div>
{/* Message */}
<div className="p-6 max-w-full overflow-hidden">
<p className="text-slate-300 font-mono text-sm whitespace-pre-wrap leading-relaxed break-words max-w-full">
{confirmConfig.message}
</p>
</div>
{/* Buttons */}
<div className="flex justify-end gap-3 p-6 border-t border-white/10">
<button
onClick={() => handleConfirm(false)}
className="px-6 py-2 bg-slate-700 hover:bg-slate-600 border border-white/20 text-white font-tech font-bold rounded transition-colors"
>
No
</button>
<button
onClick={() => handleConfirm(true)}
className="px-6 py-2 bg-amber-500/20 hover:bg-amber-500/30 border border-amber-500 text-amber-400 font-tech font-bold rounded transition-colors"
>
Yes
</button>
</div>
</div>
</div>
)}
</AlertContext.Provider> </AlertContext.Provider>
); );
}; };

View File

@@ -0,0 +1,320 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Search, Download, ChevronLeft, ChevronRight, Filter, X } from 'lucide-react';
import { comms } from '../communication';
interface HistoryRow {
idx: number;
jtype: string;
rid: string;
sid: string;
qty: number;
vname: string;
vlot: string;
loc: string;
qr: string;
stime: string;
etime: string;
prnattach: boolean;
prnvalid: boolean;
rid0: string;
sid0: string;
qtymax: number;
}
export const HistoryPage: React.FC = () => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<HistoryRow[]>([]);
const [filteredData, setFilteredData] = useState<HistoryRow[]>([]);
const [mcName, setMcName] = useState('');
// Date range state
const today = new Date();
const [startDate, setStartDate] = useState(today.toISOString().split('T')[0]);
const [endDate, setEndDate] = useState(today.toISOString().split('T')[0]);
// Search state
const [dbSearch, setDbSearch] = useState('');
const [localFilter, setLocalFilter] = useState('');
// Load data on mount
useEffect(() => {
fetchData();
}, []);
// Apply local filter when it changes
useEffect(() => {
if (!localFilter.trim()) {
setFilteredData(data);
} else {
const keyword = localFilter.toLowerCase();
setFilteredData(data.filter(row =>
row.rid0?.toLowerCase().includes(keyword) ||
row.sid0?.toLowerCase().includes(keyword) ||
row.qr?.toLowerCase().includes(keyword) ||
row.vlot?.toLowerCase().includes(keyword) ||
row.vname?.toLowerCase().includes(keyword) ||
row.rid?.toLowerCase().includes(keyword) ||
row.sid?.toLowerCase().includes(keyword)
));
}
}, [localFilter, data]);
const fetchData = async () => {
setIsLoading(true);
try {
const result = await comms.getHistoryData(startDate, endDate, dbSearch);
if (result.success) {
setData(result.data || []);
setFilteredData(result.data || []);
if (result.mcName) {
setMcName(result.mcName);
}
} else {
console.error('Failed to fetch history:', result.message);
setData([]);
setFilteredData([]);
}
} catch (error) {
console.error('Error fetching history:', error);
setData([]);
setFilteredData([]);
}
setIsLoading(false);
};
const handleSearch = () => {
// Validate dates
if (new Date(endDate) < new Date(startDate)) {
alert('End date is earlier than start date');
return;
}
fetchData();
};
const handlePrevDay = () => {
const sd = new Date(startDate);
sd.setDate(sd.getDate() - 1);
const newDate = sd.toISOString().split('T')[0];
setStartDate(newDate);
setEndDate(newDate);
};
const handleNextDay = () => {
const ed = new Date(endDate);
ed.setDate(ed.getDate() + 1);
const newDate = ed.toISOString().split('T')[0];
setStartDate(newDate);
setEndDate(newDate);
};
const handleExport = () => {
if (filteredData.length === 0) {
alert('No data to export');
return;
}
// Create CSV content
const headers = ['IDX', 'JTYPE', 'RID', 'SID', 'QTY', 'QTY_MAX', 'VENDOR', 'V.LOT', 'LOC', 'QR', 'START', 'END', 'ATTACH', 'VALID', 'RID0', 'SID0'];
const csvContent = [
headers.join(','),
...filteredData.map(row => [
row.idx,
`"${row.jtype || ''}"`,
`"${row.rid || ''}"`,
`"${row.sid || ''}"`,
row.qty || 0,
row.qtymax || 0,
`"${row.vname || ''}"`,
`"${row.vlot || ''}"`,
`"${row.loc || ''}"`,
`"${row.qr || ''}"`,
`"${row.stime || ''}"`,
`"${row.etime || ''}"`,
row.prnattach ? 'Y' : 'N',
row.prnvalid ? 'Y' : 'N',
`"${row.rid0 || ''}"`,
`"${row.sid0 || ''}"`,
].join(','))
].join('\n');
// Download file
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `export_${startDate.replace(/-/g, '')}~${endDate.replace(/-/g, '')}.csv`;
link.click();
};
return (
<div className="min-h-screen bg-gray-900 text-white p-4">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/')}
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<h1 className="text-2xl font-tech font-bold text-neon-blue">
WORK HISTORY {mcName && <span className="text-gray-400 text-lg">({mcName})</span>}
</h1>
</div>
<div className="text-sm text-gray-400">
Total: <span className="text-white font-bold">{filteredData.length}</span> records
{localFilter && <span className="text-yellow-400 ml-2">(filtered from {data.length})</span>}
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-4 mb-4 p-4 bg-gray-800/50 rounded-xl border border-gray-700">
{/* Date Range */}
<div className="flex items-center gap-2">
<button
onClick={handlePrevDay}
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 transition-colors"
title="Previous Day"
>
<ChevronLeft className="w-4 h-4" />
</button>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="px-3 py-2 rounded-lg bg-gray-700 border border-gray-600 text-white text-sm focus:outline-none focus:border-neon-blue"
/>
<span className="text-gray-500">~</span>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="px-3 py-2 rounded-lg bg-gray-700 border border-gray-600 text-white text-sm focus:outline-none focus:border-neon-blue"
/>
<button
onClick={handleNextDay}
className="p-2 rounded-lg bg-gray-700 hover:bg-gray-600 transition-colors"
title="Next Day"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
{/* DB Search */}
<div className="flex items-center gap-2">
<input
type="text"
value={dbSearch}
onChange={(e) => setDbSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="Search in DB..."
className="px-3 py-2 rounded-lg bg-gray-700 border border-gray-600 text-white text-sm focus:outline-none focus:border-neon-blue w-48"
/>
<button
onClick={handleSearch}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-neon-blue/20 text-neon-blue border border-neon-blue hover:bg-neon-blue/30 transition-colors disabled:opacity-50"
>
<Search className="w-4 h-4" />
<span>Search</span>
</button>
</div>
{/* Local Filter */}
<div className="flex items-center gap-2 ml-auto">
<Filter className="w-4 h-4 text-gray-500" />
<input
type="text"
value={localFilter}
onChange={(e) => setLocalFilter(e.target.value)}
placeholder="Filter results..."
className={`px-3 py-2 rounded-lg border text-white text-sm focus:outline-none focus:border-neon-blue w-48 ${localFilter ? 'bg-lime-900/30 border-lime-500' : 'bg-gray-700 border-gray-600'
}`}
/>
{localFilter && (
<button
onClick={() => setLocalFilter('')}
className="p-1 rounded hover:bg-gray-700"
>
<X className="w-4 h-4 text-gray-400" />
</button>
)}
</div>
{/* Export */}
<button
onClick={handleExport}
disabled={filteredData.length === 0}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-green-600/20 text-green-400 border border-green-600 hover:bg-green-600/30 transition-colors disabled:opacity-50"
>
<Download className="w-4 h-4" />
<span>Export CSV</span>
</button>
</div>
{/* Data Table */}
<div className="bg-gray-800/50 rounded-xl border border-gray-700 overflow-hidden">
<div className="overflow-x-auto max-h-[calc(100vh-280px)]">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-gray-800 border-b border-gray-700">
<tr>
<th className="px-3 py-2 text-left text-gray-400 font-medium">IDX</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">TYPE</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">RID</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">SID</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">QTY</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">VENDOR</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">V.LOT</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">LOC</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">START</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">END</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">ATTACH</th>
<th className="px-3 py-2 text-left text-gray-400 font-medium">VALID</th>
</tr>
</thead>
<tbody>
{isLoading ? (
<tr>
<td colSpan={12} className="px-3 py-8 text-center text-gray-500">
Loading...
</td>
</tr>
) : filteredData.length === 0 ? (
<tr>
<td colSpan={12} className="px-3 py-8 text-center text-gray-500">
No data found
</td>
</tr>
) : (
filteredData.map((row, index) => (
<tr
key={row.idx || index}
className="border-b border-gray-700/50 hover:bg-gray-700/30 transition-colors"
>
<td className="px-3 py-2 text-gray-500">{row.idx}</td>
<td className="px-3 py-2 text-xs">{row.jtype}</td>
<td className="px-3 py-2 font-mono text-xs">{row.rid}</td>
<td className="px-3 py-2 font-mono text-xs">{row.sid}</td>
<td className="px-3 py-2 text-right">{row.qty}/{row.qtymax}</td>
<td className="px-3 py-2">{row.vname}</td>
<td className="px-3 py-2 font-mono text-xs">{row.vlot}</td>
<td className="px-3 py-2 text-center">{row.loc}</td>
<td className="px-3 py-2 text-xs text-gray-400">{row.stime}</td>
<td className="px-3 py-2 text-xs text-gray-400">{row.etime}</td>
<td className={`px-3 py-2 text-center font-bold ${row.prnattach ? 'text-green-400' : 'text-red-400'}`}>
{row.prnattach ? 'Y' : 'N'}
</td>
<td className={`px-3 py-2 text-center font-bold ${row.prnvalid ? 'text-green-400' : 'text-red-400'}`}>
{row.prnvalid ? 'Y' : 'N'}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
};

View File

@@ -51,7 +51,8 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
// Subscribe to real-time IO updates // Subscribe to real-time IO updates
const unsubscribe = comms.subscribe((msg: any) => { const unsubscribe = comms.subscribe((msg: any) => {
if (msg?.type === 'STATUS_UPDATE' && msg.ioState) { // STATUS_UPDATE - 주기적인 상태 업데이트 (변경된 IO만 포함)
if (msg?.type === 'STATUS_UPDATE' && msg.ioState && msg.ioState.length > 0) {
setIoPoints(prev => { setIoPoints(prev => {
const newIO = [...prev]; const newIO = [...prev];
msg.ioState.forEach((update: { id: number, type: string, state: boolean }) => { msg.ioState.forEach((update: { id: number, type: string, state: boolean }) => {
@@ -61,6 +62,39 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
return newIO; 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 () => { return () => {
@@ -72,6 +106,19 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
? [] ? []
: ioPoints.filter(p => p.type === (activeIOTab === 'in' ? 'input' : 'output')); : 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 ( return (
<main className="relative w-full h-full px-6 pt-6 pb-20"> <main className="relative w-full h-full px-6 pt-6 pb-20">
<div className="glass-holo p-6 h-full flex flex-col gap-4"> <div className="glass-holo p-6 h-full flex flex-col gap-4">
@@ -130,11 +177,13 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
{axis.locks.map(lock => ( {axis.locks.map(lock => (
<div <div
key={lock.id} key={lock.id}
onClick={() => handleInterlockToggle(axis.axisIndex, lock.id)}
className={` className={`
flex items-center gap-2 px-3 py-2 transition-all border rounded 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 ${lock.state
? 'bg-red-500/20 border-red-500 text-red-400 shadow-[0_0_10px_rgba(239,68,68,0.3)]' ? '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'} : 'bg-slate-800/40 border-slate-700 text-slate-500 hover:border-slate-600'}
`} `}
> >
<div className={`w-2 h-2 rounded-full shrink-0 ${lock.state ? 'bg-red-500 shadow-[0_0_6px_#ef4444]' : 'bg-slate-700'}`}></div> <div className={`w-2 h-2 rounded-full shrink-0 ${lock.state ? 'bg-red-500 shadow-[0_0_6px_#ef4444]' : 'bg-slate-700'}`}></div>

View File

@@ -99,6 +99,56 @@ declare global {
InitializeDevice(): Promise<string>; InitializeDevice(): Promise<string>;
GetInitializeStatus(): Promise<string>; GetInitializeStatus(): Promise<string>;
GetProcessedData(): Promise<string>; GetProcessedData(): Promise<string>;
// Vision control methods
CameraConnect(): Promise<string>;
CameraDisconnect(): Promise<string>;
CameraGetImage(): Promise<string>;
CameraLiveView(): Promise<string>;
CameraReadTest(): Promise<string>;
KeyenceTriggerOn(): Promise<string>;
KeyenceTriggerOff(): Promise<string>;
KeyenceGetImage(): Promise<string>;
KeyenceSaveImage(): Promise<string>;
// Light, Manual Print, Cancel Job
ToggleLight(): Promise<string>;
ExecuteManualPrint(sid: string, venderLot: string, qty: string, mfg: string, rid: string, spy: string, partNo: string, printer: string, count: number): Promise<string>;
CancelJob(): Promise<string>;
// Manage, Manual, Log Viewer
OpenManage(): Promise<string>;
CloseManage(): Promise<string>;
OpenManual(): Promise<string>;
OpenLogViewer(): Promise<string>;
// Folder operations
OpenProgramFolder(): Promise<string>;
OpenLogFolder(): Promise<string>;
OpenScreenshotFolder(): Promise<string>;
OpenSavedDataFolder(): Promise<string>;
// Picker Move methods
GetPickerStatus(): Promise<string>;
PickerMoveLeft(): Promise<string>;
PickerMoveLeftWait(): Promise<string>;
PickerMoveCenter(): Promise<string>;
PickerMoveRightWait(): Promise<string>;
PickerMoveRight(): Promise<string>;
PickerJogStart(direction: string): Promise<string>;
PickerJogStop(): Promise<string>;
PickerStop(): Promise<string>;
CancelVisionValidation(side: string): Promise<string>;
PickerManagePosition(side: string): Promise<string>;
PickerManageReturn(): Promise<string>;
PickerZHome(): Promise<string>;
PickerZZero(): Promise<string>;
PickerTestPrint(side: string): Promise<string>;
CanCloseManage(): Promise<string>;
// Interlock methods
ToggleInterlock(axisIndex: number, lockIndex: number): Promise<string>;
GetInterlockList(): Promise<string>;
// HW Status methods
GetHWStatus(): Promise<string>;
// History dialog
OpenHistory(): Promise<string>;
// History data
GetHistoryData(startDate: string, endDate: string, search: string): Promise<string>;
} }
}; };
addEventListener(type: string, listener: (event: any) => void): void; addEventListener(type: string, listener: (event: any) => void): void;

View File

@@ -6,6 +6,8 @@ using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms; using Microsoft.Web.WebView2.WinForms;
using Newtonsoft.Json; using Newtonsoft.Json;
using Project.WebUI; using Project.WebUI;
using AR;
using arDev.DIO;
namespace Project.Dialog namespace Project.Dialog
{ {
@@ -15,14 +17,16 @@ namespace Project.Dialog
private Timer plcTimer; private Timer plcTimer;
private WebSocketServer _wsServer; private WebSocketServer _wsServer;
// Machine State (Simulated PLC Memory) // Machine State
private double currX = 0, currY = 0, currZ = 0; private double currX = 0, currY = 0, currZ = 0;
private double targetX = 0, targetY = 0, targetZ = 0; private double targetX = 0, targetY = 0, targetZ = 0;
private bool[] inputs = new bool[32];
private bool[] outputs = new bool[32];
private string systemState = "IDLE"; private string systemState = "IDLE";
private string currentRecipeId = "1"; // Default recipe private string currentRecipeId = "1"; // Default recipe
// IO 캐시 (변경된 값만 전송하기 위함)
private bool[] _lastInputs;
private bool[] _lastOutputs;
public fWebView() public fWebView()
{ {
InitializeComponent(); InitializeComponent();
@@ -38,25 +42,149 @@ namespace Project.Dialog
MessageBox.Show("Failed to start WebSocket Server (Port 8081). Run as Admin or allow port.\n" + ex.Message); MessageBox.Show("Failed to start WebSocket Server (Port 8081). Run as Admin or allow port.\n" + ex.Message);
} }
// Set default inputs (Pressure OK, Estop OK) // IO 캐시 초기화
inputs[4] = true; int diCount = PUB.dio?.GetDICount ?? 32;
inputs[6] = true; int doCount = PUB.dio?.GetDOCount ?? 32;
_lastInputs = new bool[diCount];
_lastOutputs = new bool[doCount];
// IO 값 변경 이벤트 구독 (DIOMonitor.cs와 동일)
if (PUB.dio != null)
{
PUB.dio.IOValueChanged += Dio_IOValueChanged;
}
// 인터락 값 변경 이벤트 구독 (DIOMonitor.cs와 동일)
if (PUB.iLock != null)
{
for (int i = 0; i < PUB.iLock.Length; i++)
{
PUB.iLock[i].ValueChanged += ILock_ValueChanged;
}
}
// Load event handler // Load event handler
this.Load += FWebView_Load; this.Load += FWebView_Load;
this.FormClosed += FWebView_FormClosed;
}
private void FWebView_FormClosed(object sender, FormClosedEventArgs e)
{
// IO 이벤트 구독 해제
if (PUB.dio != null)
{
PUB.dio.IOValueChanged -= Dio_IOValueChanged;
}
// 인터락 이벤트 구독 해제
if (PUB.iLock != null)
{
for (int i = 0; i < PUB.iLock.Length; i++)
{
PUB.iLock[i].ValueChanged -= ILock_ValueChanged;
}
}
}
// 인터락 값 변경 이벤트 핸들러 (DIOMonitor.cs의 LockXF_ValueChanged와 동일)
private void ILock_ValueChanged(object sender, AR.InterfaceValueEventArgs e)
{
try
{
var item = sender as CInterLock;
if (item == null) return;
var axisIndex = item.idx;
var ilockUpdate = new
{
type = "INTERLOCK_CHANGED",
data = new
{
axisIndex = axisIndex,
lockIndex = (int)e.ArrIDX,
state = e.NewValue,
hexValue = item.Value().HexString()
}
};
string json = JsonConvert.SerializeObject(ilockUpdate);
// WebView2로 전송
if (webView != null && webView.CoreWebView2 != null)
{
this.BeginInvoke(new Action(() =>
{
try
{
webView.CoreWebView2.PostWebMessageAsJson(json);
}
catch { }
}));
}
// WebSocket으로 전송
_wsServer?.Broadcast(json);
}
catch (Exception ex)
{
Console.WriteLine($"[fWebView] Interlock change broadcast error: {ex.Message}");
}
}
// IO 값 변경 이벤트 핸들러 (DIOMonitor.cs의 dio_IOValueChanged와 동일)
private void Dio_IOValueChanged(object sender, IOValueEventArgs e)
{
try
{
// 변경된 IO만 즉시 전송
var ioUpdate = new
{
type = "IO_CHANGED",
data = new
{
id = e.ArrIDX,
ioType = e.Direction == eIOPINDIR.INPUT ? "input" : "output",
state = e.NewValue
}
};
string json = JsonConvert.SerializeObject(ioUpdate);
// WebView2로 전송
if (webView != null && webView.CoreWebView2 != null)
{
this.BeginInvoke(new Action(() =>
{
try
{
webView.CoreWebView2.PostWebMessageAsJson(json);
}
catch { }
}));
}
// WebSocket으로 전송
_wsServer?.Broadcast(json);
}
catch (Exception ex)
{
Console.WriteLine($"[fWebView] IO change broadcast error: {ex.Message}");
}
} }
private void InitializeComponent() private void InitializeComponent()
{ {
this.SuspendLayout(); this.SuspendLayout();
//
// Form // fWebView
this.ClientSize = new System.Drawing.Size(1200, 800); //
this.Text = "STD Label Attach - Web UI"; this.ClientSize = new System.Drawing.Size(1784, 961);
this.Name = "fWebView"; this.Name = "fWebView";
this.StartPosition = FormStartPosition.CenterScreen; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "STD Label Attach - Web UI";
this.ResumeLayout(false); this.ResumeLayout(false);
} }
private async void InitializeWebView() private async void InitializeWebView()
@@ -106,6 +234,9 @@ namespace Project.Dialog
} }
} }
// HW 상태 업데이트 카운터 (250ms 주기 = 50ms * 5)
private int _hwUpdateCounter = 0;
// --- Logic Loop --- // --- Logic Loop ---
private void PlcTimer_Tick(object sender, EventArgs e) private void PlcTimer_Tick(object sender, EventArgs e)
{ {
@@ -114,36 +245,105 @@ namespace Project.Dialog
currY = Lerp(currY, targetY, 0.1); currY = Lerp(currY, targetY, 0.1);
currZ = Lerp(currZ, targetZ, 0.1); currZ = Lerp(currZ, targetZ, 0.1);
// 2. Prepare Data Packet // 2. 시스템 상태 업데이트
if (PUB.sm != null)
{
systemState = PUB.sm.Step.ToString();
}
// 3. Prepare Data Packet
var payload = new var payload = new
{ {
type = "STATUS_UPDATE", type = "STATUS_UPDATE",
sysState = systemState, sysState = systemState,
position = new { x = currX, y = currY, z = currZ }, position = new { x = currX, y = currY, z = currZ },
ioState = GetChangedIOs() // Function to return array of IO states ioState = GetChangedIOs() // 변경된 IO만 전송
}; };
string json = JsonConvert.SerializeObject(payload); string json = JsonConvert.SerializeObject(payload);
// 3. Send to React via PostMessage (WebView2) // 4. Send to React via PostMessage (WebView2)
if (webView != null && webView.CoreWebView2 != null) if (webView != null && webView.CoreWebView2 != null)
{ {
webView.CoreWebView2.PostWebMessageAsJson(json); webView.CoreWebView2.PostWebMessageAsJson(json);
} }
// 4. Broadcast to WebSocket (Dev/HMR) // 5. Broadcast to WebSocket (Dev/HMR)
_wsServer?.Broadcast(json); _wsServer?.Broadcast(json);
// 6. HW 상태 업데이트 (250ms 주기 - 윈폼의 _Display_Interval_250ms와 동일)
_hwUpdateCounter++;
if (_hwUpdateCounter >= 5) // 50ms * 5 = 250ms
{
_hwUpdateCounter = 0;
BroadcastHWStatus();
}
}
// H/W 상태 브로드캐스트 (윈폼의 HWState 업데이트와 동일)
private void BroadcastHWStatus()
{
try
{
var bridge = new WebUI.MachineBridge(this);
string hwStatusJson = bridge.GetHWStatus();
var payload = new
{
type = "HW_STATUS_UPDATE",
data = JsonConvert.DeserializeObject(hwStatusJson)
};
string json = JsonConvert.SerializeObject(payload);
// WebView2로 전송
if (webView != null && webView.CoreWebView2 != null)
{
webView.CoreWebView2.PostWebMessageAsJson(json);
}
// WebSocket으로 전송
_wsServer?.Broadcast(json);
}
catch (Exception ex)
{
Console.WriteLine($"[fWebView] HW status broadcast error: {ex.Message}");
}
} }
private List<object> GetChangedIOs() private List<object> GetChangedIOs()
{ {
// Simply return list of all active IOs or just send all for simplicity
var list = new List<object>(); var list = new List<object>();
for (int i = 0; i < 32; i++)
// 실제 DIO에서 값 읽기
if (PUB.dio != null)
{ {
list.Add(new { id = i, type = "input", state = inputs[i] }); int diCount = PUB.dio.GetDICount;
list.Add(new { id = i, type = "output", state = outputs[i] }); int doCount = PUB.dio.GetDOCount;
// DI (Digital Input) - 변경된 값만 추가
for (int i = 0; i < diCount && i < _lastInputs.Length; i++)
{
bool currentValue = PUB.dio.GetDIValue(i);
if (currentValue != _lastInputs[i])
{
list.Add(new { id = i, type = "input", state = currentValue });
_lastInputs[i] = currentValue;
} }
}
// DO (Digital Output) - 변경된 값만 추가
for (int i = 0; i < doCount && i < _lastOutputs.Length; i++)
{
bool currentValue = PUB.dio.GetDOValue(i);
if (currentValue != _lastOutputs[i])
{
list.Add(new { id = i, type = "output", state = currentValue });
_lastOutputs[i] = currentValue;
}
}
}
return list; return list;
} }
@@ -164,9 +364,37 @@ namespace Project.Dialog
if (axis == "Z") targetZ = val; if (axis == "Z") targetZ = val;
} }
public void SetOutput(int id, bool state) // DO 출력 제어 (DIOMonitor.cs의 tblDO_ItemClick과 동일한 로직)
public bool SetOutput(int id, bool state)
{ {
if (id < 32) outputs[id] = state; try
{
if (PUB.dio == null)
{
Console.WriteLine($"[fWebView] SetOutput failed: DIO not initialized");
return false;
}
if (PUB.dio.IsInit == false)
{
// DIO가 초기화되지 않은 경우 가상 신호 생성 (디버그 모드)
PUB.dio.RaiseEvent(eIOPINDIR.OUTPUT, id, state);
PUB.log.Add($"[Web] Fake DO: idx={id}, val={state}");
return true;
}
else
{
// 실제 출력 제어
PUB.dio.SetOutput(id, state);
PUB.log.Add($"[Web] Set output: idx={id}, val={state}");
return true;
}
}
catch (Exception ex)
{
Console.WriteLine($"[fWebView] SetOutput error: {ex.Message}");
return false;
}
} }
public void HandleCommand(string cmd) public void HandleCommand(string cmd)
@@ -214,5 +442,34 @@ namespace Project.Dialog
Console.WriteLine($"PostMessage failed: {ex.Message}"); Console.WriteLine($"PostMessage failed: {ex.Message}");
} }
} }
// 이벤트 브로드캐스트 (WebView2 + WebSocket)
public void BroadcastEvent(string eventType, object data = null)
{
try
{
var payload = new
{
type = eventType,
data = data
};
string json = JsonConvert.SerializeObject(payload);
// WebView2로 전송
if (webView != null && webView.CoreWebView2 != null)
{
webView.CoreWebView2.PostWebMessageAsJson(json);
}
// WebSocket으로 전송
_wsServer?.Broadcast(json);
Console.WriteLine($"[fWebView] BroadcastEvent: {eventType}");
}
catch (Exception ex)
{
Console.WriteLine($"BroadcastEvent failed: {ex.Message}");
}
}
} }
} }

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -32,8 +32,12 @@ namespace Project
//Pub.sm.setNewStep(eSMStep.XMOVE); //홈을 위해서 바로 이동 모션으로 가게한다 //Pub.sm.setNewStep(eSMStep.XMOVE); //홈을 위해서 바로 이동 모션으로 가게한다
if (PUB.mot.HasHomeSetOff == true && DIO.GetIOInput(eDIName.PICKER_SAFE) == false) if (PUB.mot.HasHomeSetOff == true && DIO.GetIOInput(eDIName.PICKER_SAFE) == false)
{ {
// 웹 UI에도 자동 관리창 열기 이벤트 전송
SendWebEvent("AUTO_OPEN_MANAGE", new { reason = "Picker needs to be moved to safe position" });
//피커의 이동이 필요한 상황 //피커의 이동이 필요한 상황
this.BeginInvoke(new Action(() => { btManage.PerformClick(); })); this.BeginInvoke(new Action(() => { btManage.PerformClick(); }));
} }
else else
{ {

View File

@@ -730,6 +730,9 @@
<EmbeddedResource Include="Dialog\fVAR.resx"> <EmbeddedResource Include="Dialog\fVAR.resx">
<DependentUpon>fVAR.cs</DependentUpon> <DependentUpon>fVAR.cs</DependentUpon>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="Dialog\fWebView.resx">
<DependentUpon>fWebView.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialog\fZPLEditor.resx"> <EmbeddedResource Include="Dialog\fZPLEditor.resx">
<DependentUpon>fZPLEditor.cs</DependentUpon> <DependentUpon>fZPLEditor.cs</DependentUpon>
</EmbeddedResource> </EmbeddedResource>

View File

@@ -6,6 +6,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using AR; using AR;
using System.Threading.Tasks;
namespace Project.WebUI namespace Project.WebUI
{ {
@@ -1145,7 +1146,14 @@ namespace Project.WebUI
} }
var cur = DIO.GetIOOutput(AR.eDOName.ROOMLIGHT); var cur = DIO.GetIOOutput(AR.eDOName.ROOMLIGHT);
DIO.SetRoomLight(!cur, true); var success = DIO.SetRoomLight(!cur, true);
if (success == false)
{
var response = new { success = false, message = "Failed to control room light" };
return JsonConvert.SerializeObject(response);
}
PUB.log.Add($"User Request: Room Light {(!cur ? "ON" : "OFF")}", false); PUB.log.Add($"User Request: Room Light {(!cur ? "ON" : "OFF")}", false);
var response2 = new { success = true, message = $"Light turned {(!cur ? "ON" : "OFF")}" }; var response2 = new { success = true, message = $"Light turned {(!cur ? "ON" : "OFF")}" };
return JsonConvert.SerializeObject(response2); return JsonConvert.SerializeObject(response2);
@@ -1173,7 +1181,7 @@ namespace Project.WebUI
var selectedPrinter = printer.ToLower() == "left" ? PUB.PrinterL : PUB.PrinterR; var selectedPrinter = printer.ToLower() == "left" ? PUB.PrinterL : PUB.PrinterR;
// Create ZPL // Create ZPL
string zpl = selectedPrinter.makeZPL_210908(new AR.Class.Reel string zpl = selectedPrinter.makeZPL_210908(new Class.Reel
{ {
SID = sid, SID = sid,
venderLot = venderLot, venderLot = venderLot,
@@ -1290,10 +1298,11 @@ namespace Project.WebUI
return JsonConvert.SerializeObject(response); return JsonConvert.SerializeObject(response);
} }
// The manage dialog (fPickerMove) cannot be opened from web UI // Set flag to indicate picker move dialog is open
// This would require implementing a separate picker management page PUB.flag.set(eVarBool.FG_MOVE_PICKER, true, "PICKERMOVE_WEB");
PUB.log.Add("User Request: Manage (Web UI)", false); PUB.log.Add("User Request: Manage (Web UI)", false);
var response2 = new { success = false, message = "Manage is not available in Web UI. Please use the main program." };
var response2 = new { success = true, message = "Manage dialog opened" };
return JsonConvert.SerializeObject(response2); return JsonConvert.SerializeObject(response2);
} }
catch (Exception ex) catch (Exception ex)
@@ -1304,6 +1313,29 @@ namespace Project.WebUI
} }
} }
public string CloseManage()
{
try
{
// Clear the flag
PUB.flag.set(eVarBool.FG_MOVE_PICKER, false, "PICKERMOVE_WEB");
// Check if auto-init is needed: home not set AND picker is in center position
bool shouldAutoInit = PUB.mot.HasHomeSetOff == true && DIO.GetIOInput(eDIName.PICKER_SAFE);
PUB.log.Add($"User Request: Close Manage (Web UI), shouldAutoInit={shouldAutoInit}", false);
var response = new { shouldAutoInit = shouldAutoInit };
return JsonConvert.SerializeObject(response);
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Failed to close manage: {ex.Message}");
var response = new { shouldAutoInit = false };
return JsonConvert.SerializeObject(response);
}
}
public string OpenManual() public string OpenManual()
{ {
try try
@@ -1433,5 +1465,837 @@ namespace Project.WebUI
return JsonConvert.SerializeObject(response); return JsonConvert.SerializeObject(response);
} }
} }
// ===== PICKER MOVE METHODS =====
private static bool _manPosL = false;
private static bool _manPosR = false;
public string GetPickerStatus()
{
try
{
// X축 피커 이동 가능 여부 체크
bool xEnabled = false;
if (PUB.mot.IsHomeSet((int)eAxis.PZ_PICK) == false)
{
if (PUB.mot.IsOrg((int)eAxis.PZ_PICK) || PUB.mot.IsLimitN((int)eAxis.PZ_PICK))
xEnabled = true;
}
else
{
var PosZ = MOT.GetPZPos(ePZLoc.READY);
var OffZ = MOT.getPositionOffset(PosZ);
xEnabled = OffZ < 1;
}
var doorsafef = DIO.isSaftyDoorF();
var managementEnabled = PUB.mot.HasHomeSetOff == false && doorsafef == true;
var status = new
{
xEnabled = xEnabled,
zEnabled = xEnabled, // Z jog uses same condition
pickerSafe = DIO.GetIOInput(eDIName.PICKER_SAFE),
managementEnabled = managementEnabled,
manPosL = _manPosL,
manPosR = _manPosR
};
return JsonConvert.SerializeObject(status);
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Failed to get picker status: {ex.Message}");
return JsonConvert.SerializeObject(new { xEnabled = false, zEnabled = false, pickerSafe = false, managementEnabled = false, manPosL = false, manPosR = false });
}
}
private bool CheckPickerSafety(out string errorMessage)
{
errorMessage = null;
if (DIO.isSaftyDoorF() == false)
{
errorMessage = "Front door is open";
return false;
}
if (PUB.mot.HasHomeSetOff)
{
errorMessage = "Motion home operation is not completed";
return false;
}
return true;
}
private bool CheckZAxisReady(out string errorMessage)
{
errorMessage = null;
var z = MOT.GetPZPos(ePZLoc.READY);
var zpos = MOT.getPositionOffset(z);
if (zpos >= 0.5)
{
errorMessage = "Raise the Z axis and try again";
return false;
}
return true;
}
public string PickerMoveLeft()
{
try
{
if (!CheckPickerSafety(out string safetyError))
return JsonConvert.SerializeObject(new { success = false, message = safetyError });
if (!CheckZAxisReady(out string zError))
return JsonConvert.SerializeObject(new { success = false, message = zError });
var m1 = MOT.GetLMPos(eLMLoc.READY);
if (MOT.getPositionMatch(m1) == false)
return JsonConvert.SerializeObject(new { success = false, message = "Printer attachment is not in ready position.\nCannot move as collision may occur" });
var p1 = MOT.GetPXPos(ePXLoc.PICKOFFL);
MOT.Move(eAxis.PX_PICK, p1.Position, 250, p1.Acc, false, false, false);
return JsonConvert.SerializeObject(new { success = true, message = "Moving to left position" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerMoveLeftWait()
{
try
{
if (!CheckPickerSafety(out string safetyError))
return JsonConvert.SerializeObject(new { success = false, message = safetyError });
if (!CheckZAxisReady(out string zError))
return JsonConvert.SerializeObject(new { success = false, message = zError });
var m1 = MOT.GetLMPos(eLMLoc.READY);
if (MOT.getPositionMatch(m1) == false)
return JsonConvert.SerializeObject(new { success = false, message = "Printer attachment is not in ready position.\nCannot move as collision may occur" });
var p1 = MOT.GetPXPos(ePXLoc.READYL);
MOT.Move(eAxis.PX_PICK, p1.Position, 250, p1.Acc, false, false, false);
return JsonConvert.SerializeObject(new { success = true, message = "Moving to left wait position" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerMoveCenter()
{
try
{
if (!CheckPickerSafety(out string safetyError))
return JsonConvert.SerializeObject(new { success = false, message = safetyError });
if (!CheckZAxisReady(out string zError))
return JsonConvert.SerializeObject(new { success = false, message = zError });
var p1 = MOT.GetPXPos(ePXLoc.PICKON);
MOT.Move(eAxis.PX_PICK, p1.Position, 250, p1.Acc, false, false, false);
return JsonConvert.SerializeObject(new { success = true, message = "Moving to center position" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerMoveRightWait()
{
try
{
if (!CheckPickerSafety(out string safetyError))
return JsonConvert.SerializeObject(new { success = false, message = safetyError });
if (!CheckZAxisReady(out string zError))
return JsonConvert.SerializeObject(new { success = false, message = zError });
var m1 = MOT.GetRMPos(eRMLoc.READY);
if (MOT.getPositionMatch(m1) == false)
return JsonConvert.SerializeObject(new { success = false, message = "Printer attachment is not in ready position.\nCannot move as collision may occur" });
var p1 = MOT.GetPXPos(ePXLoc.READYR);
MOT.Move(eAxis.PX_PICK, p1.Position, 250, p1.Acc, false, false, false);
return JsonConvert.SerializeObject(new { success = true, message = "Moving to right wait position" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerMoveRight()
{
try
{
if (!CheckPickerSafety(out string safetyError))
return JsonConvert.SerializeObject(new { success = false, message = safetyError });
if (!CheckZAxisReady(out string zError))
return JsonConvert.SerializeObject(new { success = false, message = zError });
var m1 = MOT.GetRMPos(eRMLoc.READY);
if (MOT.getPositionMatch(m1) == false)
return JsonConvert.SerializeObject(new { success = false, message = "Printer attachment is not in ready position.\nCannot move as collision may occur" });
var p1 = MOT.GetPXPos(ePXLoc.PICKOFFR);
MOT.Move(eAxis.PX_PICK, p1.Position, 250, p1.Acc, false, false, false);
return JsonConvert.SerializeObject(new { success = true, message = "Moving to right position" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerJogStart(string direction)
{
try
{
switch (direction)
{
case "up":
PUB.mot.JOG((int)eAxis.PZ_PICK, arDev.MOT.MOTION_DIRECTION.Negative, AR.SETTING.Data.JOG_Speed, AR.SETTING.Data.JOG_Acc);
break;
case "down":
PUB.mot.JOG((int)eAxis.PZ_PICK, arDev.MOT.MOTION_DIRECTION.Positive, AR.SETTING.Data.JOG_Speed, AR.SETTING.Data.JOG_Acc);
break;
case "left":
PUB.mot.JOG((int)eAxis.PX_PICK, arDev.MOT.MOTION_DIRECTION.Negative, AR.SETTING.Data.JOG_Speed, AR.SETTING.Data.JOG_Acc);
break;
case "right":
PUB.mot.JOG((int)eAxis.PX_PICK, arDev.MOT.MOTION_DIRECTION.Positive, AR.SETTING.Data.JOG_Speed, AR.SETTING.Data.JOG_Acc);
break;
}
return JsonConvert.SerializeObject(new { success = true, message = $"Jog {direction} started" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerJogStop()
{
try
{
PUB.mot.MoveStop("pmove_web", (short)eAxis.PX_PICK);
PUB.mot.MoveStop("pmove_web", (short)eAxis.PZ_PICK);
return JsonConvert.SerializeObject(new { success = true, message = "Jog stopped" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerStop()
{
try
{
PUB.mot.MoveStop("pmove_web", (int)eAxis.PX_PICK);
return JsonConvert.SerializeObject(new { success = true, message = "Picker stopped" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string CancelVisionValidation(string side)
{
try
{
if (side == "left")
{
PUB.flag.set(eVarBool.FG_PRC_VISIONL, false, "CANCEL_WEB");
PUB.flag.set(eVarBool.FG_PORTL_ITEMON, false, "CANCEL_WEB");
PUB.log.Add($"LEFT-QR verification cancelled JGUID={PUB.Result.ItemDataL.guid}");
}
else
{
PUB.flag.set(eVarBool.FG_PRC_VISIONR, false, "CANCEL_WEB");
PUB.flag.set(eVarBool.FG_PORTR_ITEMON, false, "CANCEL_WEB");
PUB.log.Add($"RIGHT-QR verification cancelled JGUID={PUB.Result.ItemDataR.guid}");
}
return JsonConvert.SerializeObject(new { success = true, message = $"{side.ToUpper()}-QR verification cancelled" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerManagePosition(string side)
{
try
{
if (PUB.sm.Step != eSMStep.IDLE)
return JsonConvert.SerializeObject(new { success = false, message = "Available only in standby state" });
var Xpos = DIO.GetIOInput(eDIName.PICKER_SAFE);
if (Xpos == false)
return JsonConvert.SerializeObject(new { success = false, message = "Available only when picker is in center position" });
int vidx = side == "left" ? 0 : 2;
Task.Run(() =>
{
DateTime dt;
if (vidx == 0)
{
while (DIO.GetIOInput(eDIName.L_CYLUP) == false)
{
var dorlt = DIO.checkDigitalO(eDOName.L_CYLDN, new TimeSpan(1), false);
if (dorlt == eNormalResult.False)
System.Threading.Thread.Sleep(100);
else if (dorlt == eNormalResult.Error)
{
PUB.log.AddE("l_cylup check error");
return;
}
}
var zPos = MOT.GetLZPos(eLZLoc.READY).Clone();
zPos.Speed = 100;
MOT.Move(zPos);
dt = DateTime.Now;
while (MOT.getPositionMatch(zPos) == false)
{
if ((DateTime.Now - dt).TotalSeconds > 30) break;
System.Threading.Thread.Sleep(10);
}
var mPos = MOT.GetLMPos(eLMLoc.PRINTL07).Clone();
mPos.Speed = 100;
MOT.Move(mPos);
dt = DateTime.Now;
while (MOT.getPositionMatch(mPos) == false)
{
if ((DateTime.Now - dt).TotalSeconds > 30) break;
System.Threading.Thread.Sleep(10);
}
var zPos2 = MOT.GetLZPos(eLZLoc.PICKOFF);
var tPos = (zPos2.Position / 2f);
MOT.Move(eAxis.PL_UPDN, tPos, 100, zPos.Acc);
_manPosL = true;
}
else
{
while (DIO.GetIOInput(eDIName.R_CYLUP) == false)
{
var dorlt = DIO.checkDigitalO(eDOName.R_CYLDN, new TimeSpan(1), false);
if (dorlt == eNormalResult.False)
System.Threading.Thread.Sleep(100);
else if (dorlt == eNormalResult.Error)
{
PUB.log.AddE("r_cylup check error");
return;
}
}
var zPos = MOT.GetRZPos(eRZLoc.READY).Clone();
zPos.Speed = 100;
MOT.Move(zPos);
dt = DateTime.Now;
while (MOT.getPositionMatch(zPos) == false)
{
if ((DateTime.Now - dt).TotalSeconds > 30) break;
System.Threading.Thread.Sleep(10);
}
var mPos = MOT.GetRMPos(eRMLoc.PRINTL07).Clone();
mPos.Speed = 100;
MOT.Move(mPos);
dt = DateTime.Now;
while (MOT.getPositionMatch(mPos) == false)
{
if ((DateTime.Now - dt).TotalSeconds > 30) break;
System.Threading.Thread.Sleep(10);
}
var zPos2 = MOT.GetRZPos(eRZLoc.PICKOFF);
var tPos = (zPos2.Position / 2f);
MOT.Move(eAxis.PR_UPDN, tPos, 100, zPos.Acc);
_manPosR = true;
}
});
return JsonConvert.SerializeObject(new { success = true, message = $"Moving to {side} management position" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerManageReturn()
{
try
{
var Xpos = DIO.GetIOInput(eDIName.PICKER_SAFE);
if (Xpos == false)
return JsonConvert.SerializeObject(new { success = false, message = "Available only when picker is in center position" });
// Recover both positions
Task.Run(() => PosRecover(0));
Task.Run(() => PosRecover(2));
return JsonConvert.SerializeObject(new { success = true, message = "Returning to ready position" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
private void PosRecover(int vidx)
{
DateTime dt;
if (vidx == 0)
{
while (DIO.GetIOInput(eDIName.L_CYLUP) == false)
{
var dorlt = DIO.checkDigitalO(eDOName.L_CYLDN, new TimeSpan(1), false);
if (dorlt == eNormalResult.False)
System.Threading.Thread.Sleep(100);
else if (dorlt == eNormalResult.Error)
{
PUB.log.AddE("l_cylup check error");
return;
}
}
var zPos = MOT.GetLZPos(eLZLoc.READY).Clone();
zPos.Speed = 100;
MOT.Move(zPos);
dt = DateTime.Now;
while (MOT.getPositionMatch(zPos) == false)
{
if ((DateTime.Now - dt).TotalSeconds > 30) break;
System.Threading.Thread.Sleep(10);
}
var mPos = MOT.GetLMPos(eLMLoc.READY).Clone();
mPos.Speed = 100;
MOT.Move(mPos);
dt = DateTime.Now;
while (MOT.getPositionMatch(mPos) == false)
{
if ((DateTime.Now - dt).TotalSeconds > 30) break;
System.Threading.Thread.Sleep(10);
}
_manPosL = false;
}
else
{
while (DIO.GetIOInput(eDIName.R_CYLUP) == false)
{
var dorlt = DIO.checkDigitalO(eDOName.R_CYLDN, new TimeSpan(1), false);
if (dorlt == eNormalResult.False)
System.Threading.Thread.Sleep(100);
else if (dorlt == eNormalResult.Error)
{
PUB.log.AddE("R_cylup check error");
return;
}
}
var zPos = MOT.GetRZPos(eRZLoc.READY).Clone();
zPos.Speed = 100;
MOT.Move(zPos);
dt = DateTime.Now;
while (MOT.getPositionMatch(zPos) == false)
{
if ((DateTime.Now - dt).TotalSeconds > 30) break;
System.Threading.Thread.Sleep(10);
}
var mPos = MOT.GetRMPos(eRMLoc.READY).Clone();
mPos.Speed = 100;
MOT.Move(mPos);
dt = DateTime.Now;
while (MOT.getPositionMatch(mPos) == false)
{
if ((DateTime.Now - dt).TotalSeconds > 30) break;
System.Threading.Thread.Sleep(10);
}
_manPosR = false;
}
}
public string PickerZHome()
{
try
{
MOT.Home("Management_Web", eAxis.PZ_PICK, false);
return JsonConvert.SerializeObject(new { success = true, message = "Z-axis home search started" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerZZero()
{
try
{
if (PUB.mot.IsHomeSet((int)eAxis.PZ_PICK) == false)
return JsonConvert.SerializeObject(new { success = false, message = "Z home operation is not completed. Please perform HOME first" });
MOT.Move(eAxis.PZ_PICK, 0, 500, 1000, false, false, false);
return JsonConvert.SerializeObject(new { success = true, message = "Moving Z-axis to zero" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string PickerTestPrint(string side)
{
try
{
if (side == "left")
{
PUB.PrinterL.TestPrint(AR.SETTING.Data.DrawOutbox, "", "");
PUB.log.Add("Temporary print L:" + PUB.PrinterL.LastPrintZPL);
}
else
{
PUB.PrinterR.TestPrint(AR.SETTING.Data.DrawOutbox, "", "");
PUB.log.Add("Temporary print R:" + PUB.PrinterR.LastPrintZPL);
}
return JsonConvert.SerializeObject(new { success = true, message = $"{side.ToUpper()} test print completed" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
public string CanCloseManage()
{
try
{
if (_manPosL || _manPosR)
{
return JsonConvert.SerializeObject(new { canClose = false, message = "Printer motion is in management position.\nReturn to position and try again" });
}
return JsonConvert.SerializeObject(new { canClose = true, message = "" });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { canClose = false, message = ex.Message });
}
}
// ===== INTERLOCK METHODS =====
/// <summary>
/// 인터락 토글 (DIOMonitor.cs의 gvILXF_ItemClick과 동일)
/// </summary>
public string ToggleInterlock(int axisIndex, int lockIndex)
{
try
{
if (axisIndex < 0 || axisIndex >= PUB.iLock.Length)
{
return JsonConvert.SerializeObject(new { success = false, message = "Invalid axis index" });
}
var curValue = PUB.iLock[axisIndex].get(lockIndex);
PUB.iLock[axisIndex].set(lockIndex, !curValue, "IOMONITOR_WEB");
PUB.log.Add($"[Web] Interlock toggle: axis={axisIndex}, lock={lockIndex}, newVal={!curValue}");
return JsonConvert.SerializeObject(new { success = true, newState = !curValue });
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Failed to toggle interlock: {ex.Message}");
return JsonConvert.SerializeObject(new { success = false, message = ex.Message });
}
}
// ===== HISTORY DATA =====
/// <summary>
/// 작업 이력 데이터 조회 (fHistory.cs의 refreshList와 동일)
/// </summary>
public string GetHistoryData(string startDate, string endDate, string search)
{
try
{
var results = new List<object>();
using (var ta = new DataSet1TableAdapters.K4EE_Component_Reel_ResultTableAdapter())
{
var ds = new DataSet1();
// 검색어 처리
var searchPattern = string.IsNullOrEmpty(search) ? "%" : "%" + search + "%";
// DB에서 조회
ta.FillBySearch(ds.K4EE_Component_Reel_Result, AR.SETTING.Data.McName, startDate, endDate, searchPattern);
foreach (DataSet1.K4EE_Component_Reel_ResultRow row in ds.K4EE_Component_Reel_Result.Rows)
{
results.Add(new
{
idx = row.idx,
jtype = row.IsJTYPENull() ? "" : row.JTYPE,
rid = row.IsRIDNull() ? "" : row.RID,
sid = row.IsSIDNull() ? "" : row.SID,
qty = row.IsQTYNull() ? 0 : row.QTY,
vname = row.IsVNAMENull() ? "" : row.VNAME,
vlot = row.IsVLOTNull() ? "" : row.VLOT,
loc = row.IsLOCNull() ? "" : row.LOC,
qr = row.IsQRNull() ? "" : row.QR,
stime = row.STIME.ToString("yyyy-MM-dd HH:mm:ss"),
etime = row.IsETIMENull() ? "" : row.ETIME.ToString("yyyy-MM-dd HH:mm:ss"),
prnattach = !row.IsPRNATTACHNull() && row.PRNATTACH,
prnvalid = !row.IsPRNVALIDNull() && row.PRNVALID,
rid0 = row.IsRID0Null() ? "" : row.RID0,
sid0 = row.IsSID0Null() ? "" : row.SID0,
qtymax = row.IsqtymaxNull() ? 0 : row.qtymax
});
}
}
return JsonConvert.SerializeObject(new {
success = true,
data = results,
mcName = AR.SETTING.Data.McName
});
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Failed to get history data: {ex.Message}");
return JsonConvert.SerializeObject(new { success = false, message = ex.Message, data = new List<object>() });
}
}
// ===== HARDWARE STATUS METHODS =====
/// <summary>
/// H/W 상태 조회 (_Interval_250ms.cs의 HWState 업데이트 로직과 동일)
/// </summary>
public string GetHWStatus()
{
try
{
var hwList = new List<object>();
// 1. KeyenceF (BCD Front) - 바코드 리더
if (PUB.keyenceF != null)
{
hwList.Add(new
{
name = "BCD-F",
title = PUB.keyenceF.IsConnect ? (PUB.keyenceF.IsTriggerOn ? "TRIG" : "ON") : "OFF",
status = PUB.keyenceF.IsConnect ? (PUB.keyenceF.IsTriggerOn ? 2 : 1) : 3
});
}
else
{
hwList.Add(new { name = "BCD-F", title = "SET", status = 0 });
}
// 2. KeyenceR (BCD Rear) - 바코드 리더
if (PUB.keyenceR != null)
{
hwList.Add(new
{
name = "BCD-R",
title = PUB.keyenceR.IsConnect ? (PUB.keyenceR.IsTriggerOn ? "TRIG" : "ON") : "OFF",
status = PUB.keyenceR.IsConnect ? (PUB.keyenceR.IsTriggerOn ? 2 : 1) : 3
});
}
else
{
hwList.Add(new { name = "BCD-R", title = "SET", status = 0 });
}
// 3. Vision WebSocket Left
if (PUB.wsL != null)
{
hwList.Add(new
{
name = "VIS-L",
title = PUB.wsL.Connected ? "ON" : "OFF",
status = PUB.wsL.Connected ? 1 : 3
});
}
else
{
hwList.Add(new { name = "VIS-L", title = "SET", status = 0 });
}
// 4. Vision WebSocket Right
if (PUB.wsR != null)
{
hwList.Add(new
{
name = "VIS-R",
title = PUB.wsR.Connected ? "ON" : "OFF",
status = PUB.wsR.Connected ? 1 : 3
});
}
else
{
hwList.Add(new { name = "VIS-R", title = "SET", status = 0 });
}
// 5. BarcodeFix (Fixed Barcode Reader)
hwList.Add(new
{
name = "FIX",
title = PUB.BarcodeFix.IsOpen() ? AR.SETTING.Data.Barcode_Port : "OFF",
status = PUB.BarcodeFix.IsOpen() ? 1 : 3
});
// 6. PrinterL
if (PUB.PrinterL != null)
{
hwList.Add(new
{
name = "PRT-L",
title = PUB.PrinterL.IsOpen ? AR.SETTING.Data.PrintL_Port : "OFF",
status = PUB.PrinterL.IsOpen ? 1 : 3
});
}
else
{
hwList.Add(new { name = "PRT-L", title = "SET", status = 0 });
}
// 7. PrinterR
if (PUB.PrinterR != null)
{
hwList.Add(new
{
name = "PRT-R",
title = PUB.PrinterR.IsOpen ? AR.SETTING.Data.PrintR_Port : "OFF",
status = PUB.PrinterR.IsOpen ? 1 : 3
});
}
else
{
hwList.Add(new { name = "PRT-R", title = "SET", status = 0 });
}
// 8. PLC
if (PUB.plc != null)
{
hwList.Add(new
{
name = "PLC",
title = PUB.plc.Init ? AR.SETTING.Data.swplc_name : "OFF",
status = PUB.plc.Init ? 1 : 3
});
}
else
{
hwList.Add(new { name = "PLC", title = "SET", status = 0 });
}
// 9. Motion
hwList.Add(new
{
name = "MOT",
title = PUB.mot?.IsInit == true ? "ON" : "OFF",
status = PUB.mot?.IsInit == true ? 1 : 3
});
return JsonConvert.SerializeObject(hwList);
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Failed to get HW status: {ex.Message}");
return JsonConvert.SerializeObject(new List<object>());
}
}
/// <summary>
/// 인터락 목록 조회 (실시간 값 포함)
/// </summary>
public string GetInterlockList()
{
try
{
var interlocks = new List<object>();
for (int i = 0; i < PUB.iLock.Length; i++)
{
var axisName = ((AR.eAxis)i).ToString();
var nonAxis = false;
if (i >= 7)
{
axisName = PUB.iLock[i].Tag.ToString();
nonAxis = true;
}
string[] ilockNames;
if (i == 7) ilockNames = Enum.GetNames(typeof(eILockPRL));
else if (i == 8) ilockNames = Enum.GetNames(typeof(eILockPRR));
else if (i == 9) ilockNames = Enum.GetNames(typeof(eILockVS0));
else if (i == 10) ilockNames = Enum.GetNames(typeof(eILockVS1));
else if (i == 11) ilockNames = Enum.GetNames(typeof(eILockVS2));
else if (i == 12 || i == 13) ilockNames = Enum.GetNames(typeof(eILockCV));
else ilockNames = Enum.GetNames(typeof(eILock));
var lockValues = new List<object>();
for (int j = 0; j < ilockNames.Length && j < 64; j++)
{
bool state = PUB.iLock[i].get(j);
lockValues.Add(new
{
id = j,
name = ilockNames[j],
state = state
});
}
interlocks.Add(new
{
axisIndex = i,
axisName = axisName,
nonAxis = nonAxis,
locks = lockValues,
hexValue = PUB.iLock[i].Value().HexString()
});
}
return JsonConvert.SerializeObject(interlocks);
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] Failed to get interlock list: {ex.Message}");
return JsonConvert.SerializeObject(new List<object>());
}
}
} }
} }

View File

@@ -334,6 +334,13 @@ namespace Project.WebUI
var response = new { type = "MANAGE_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) }; var response = new { type = "MANAGE_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response)); await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
} }
else if (type == "CLOSE_MANAGE")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.CloseManage();
var response = new { type = "CLOSE_MANAGE_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "OPEN_MANUAL") else if (type == "OPEN_MANUAL")
{ {
var bridge = new MachineBridge(_mainForm); var bridge = new MachineBridge(_mainForm);
@@ -376,6 +383,151 @@ namespace Project.WebUI
var response = new { type = "FOLDER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) }; var response = new { type = "FOLDER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response)); await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
} }
// ===== PICKER MOVE HANDLERS =====
else if (type == "GET_PICKER_STATUS")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.GetPickerStatus();
var response = new { type = "PICKER_STATUS", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_MOVE_LEFT")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerMoveLeft();
var response = new { type = "PICKER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_MOVE_LEFT_WAIT")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerMoveLeftWait();
var response = new { type = "PICKER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_MOVE_CENTER")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerMoveCenter();
var response = new { type = "PICKER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_MOVE_RIGHT_WAIT")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerMoveRightWait();
var response = new { type = "PICKER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_MOVE_RIGHT")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerMoveRight();
var response = new { type = "PICKER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_JOG_START")
{
var bridge = new MachineBridge(_mainForm);
string direction = json["direction"]?.ToString() ?? "";
string resultJson = bridge.PickerJogStart(direction);
var response = new { type = "PICKER_JOG_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_JOG_STOP")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerJogStop();
var response = new { type = "PICKER_JOG_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_STOP")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerStop();
var response = new { type = "PICKER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "CANCEL_VISION_VALIDATION")
{
var bridge = new MachineBridge(_mainForm);
string side = json["side"]?.ToString() ?? "left";
string resultJson = bridge.CancelVisionValidation(side);
var response = new { type = "VISION_CANCEL_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_MANAGE_POSITION")
{
var bridge = new MachineBridge(_mainForm);
string side = json["side"]?.ToString() ?? "left";
string resultJson = bridge.PickerManagePosition(side);
var response = new { type = "PICKER_MANAGE_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_MANAGE_RETURN")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerManageReturn();
var response = new { type = "PICKER_MANAGE_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_Z_HOME")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerZHome();
var response = new { type = "PICKER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_Z_ZERO")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.PickerZZero();
var response = new { type = "PICKER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "PICKER_TEST_PRINT")
{
var bridge = new MachineBridge(_mainForm);
string side = json["side"]?.ToString() ?? "left";
string resultJson = bridge.PickerTestPrint(side);
var response = new { type = "PICKER_PRINT_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "CAN_CLOSE_MANAGE")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.CanCloseManage();
var response = new { type = "CAN_CLOSE_MANAGE_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
// ===== HISTORY DATA =====
else if (type == "GET_HISTORY_DATA")
{
var bridge = new MachineBridge(_mainForm);
string startDate = json["startDate"]?.ToString() ?? DateTime.Now.ToShortDateString();
string endDate = json["endDate"]?.ToString() ?? DateTime.Now.ToShortDateString();
string search = json["search"]?.ToString() ?? "";
string resultJson = bridge.GetHistoryData(startDate, endDate, search);
var response = new { type = "HISTORY_DATA_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
// ===== INTERLOCK HANDLERS =====
else if (type == "TOGGLE_INTERLOCK")
{
var bridge = new MachineBridge(_mainForm);
int axisIndex = json["axisIndex"]?.ToObject<int>() ?? 0;
int lockIndex = json["lockIndex"]?.ToObject<int>() ?? 0;
string resultJson = bridge.ToggleInterlock(axisIndex, lockIndex);
var response = new { type = "TOGGLE_INTERLOCK_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
else if (type == "GET_INTERLOCK_LIST")
{
var bridge = new MachineBridge(_mainForm);
string resultJson = bridge.GetInterlockList();
var response = new { type = "INTERLOCK_LIST_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -493,6 +493,18 @@ namespace Project
Dialog.fWebView fwebview = null; Dialog.fWebView fwebview = null;
// 웹 UI에 이벤트 전송
public void SendWebEvent(string eventType, object data = null)
{
if (fwebview != null)
{
this.BeginInvoke(new Action(() =>
{
fwebview.BroadcastEvent(eventType, data);
}));
}
}
private void Plc_ValueChanged(object sender, AR.MemoryMap.Core.monitorvalueargs e) private void Plc_ValueChanged(object sender, AR.MemoryMap.Core.monitorvalueargs e)
{ {
// //