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:
107
FrontEnd/App.tsx
107
FrontEnd/App.tsx
@@ -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 { Layout } from './components/layout/Layout';
|
||||
import { HomePage } from './pages/HomePage';
|
||||
import { IOMonitorPage } from './pages/IOMonitorPage';
|
||||
import { RecipePage } from './pages/RecipePage';
|
||||
import { HistoryPage } from './pages/HistoryPage';
|
||||
import { SystemState, Recipe, IOPoint, LogEntry, RobotTarget, ConfigItem } from './types';
|
||||
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 ---
|
||||
|
||||
@@ -35,7 +54,8 @@ const INITIAL_IO: IOPoint[] = [
|
||||
|
||||
// --- MAIN APP ---
|
||||
|
||||
export default function App() {
|
||||
// 내부 App 컴포넌트 (AlertContext 내부에서 사용)
|
||||
function AppContent() {
|
||||
const [activeTab, setActiveTab] = useState<'recipe' | 'motion' | 'camera' | 'setting' | 'initialize' | null>(null);
|
||||
const [systemState, setSystemState] = useState<SystemState>(SystemState.IDLE);
|
||||
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 [isLoading, setIsLoading] = useState(true);
|
||||
const [isHostConnected, setIsHostConnected] = useState(false);
|
||||
const [isPickerMoveOpen, setIsPickerMoveOpen] = useState(false);
|
||||
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 --
|
||||
useEffect(() => {
|
||||
@@ -57,6 +119,16 @@ export default function App() {
|
||||
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.position) {
|
||||
setRobotTarget({ x: msg.position.x, y: msg.position.y, z: msg.position.z });
|
||||
@@ -85,7 +157,7 @@ export default function App() {
|
||||
clearInterval(timer);
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}, [isPickerMoveOpen]);
|
||||
|
||||
// -- INITIALIZATION --
|
||||
useEffect(() => {
|
||||
@@ -187,8 +259,14 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const pickerMoveContextValue: PickerMoveContextType = {
|
||||
isPickerMoveOpen,
|
||||
openPickerMove,
|
||||
closePickerMove
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertProvider>
|
||||
<PickerMoveContext.Provider value={pickerMoveContextValue}>
|
||||
<HashRouter>
|
||||
<Layout
|
||||
currentTime={currentTime}
|
||||
@@ -234,9 +312,28 @@ export default function App() {
|
||||
path="/recipe"
|
||||
element={<RecipePage />}
|
||||
/>
|
||||
<Route
|
||||
path="/history"
|
||||
element={<HistoryPage />}
|
||||
/>
|
||||
</Routes>
|
||||
</Layout>
|
||||
</HashRouter>
|
||||
|
||||
{/* PickerMoveDialog - 전역에서 관리 */}
|
||||
<PickerMoveDialog
|
||||
isOpen={isPickerMoveOpen}
|
||||
onClose={closePickerMove}
|
||||
/>
|
||||
</PickerMoveContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// 외부 App 컴포넌트 - AlertProvider로 감싸기
|
||||
export default function App() {
|
||||
return (
|
||||
<AlertProvider>
|
||||
<AppContent />
|
||||
</AlertProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -434,10 +434,10 @@ class CommunicationLayer {
|
||||
|
||||
// ===== 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) {
|
||||
// WebView2 mode - direct call to C# methods
|
||||
return { success: false, message: 'Vision commands not yet implemented in WebView2 mode' };
|
||||
const result = await (machine as any)[methodName]();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isConnected) {
|
||||
@@ -465,45 +465,46 @@ class CommunicationLayer {
|
||||
}
|
||||
|
||||
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 }> {
|
||||
return this.sendVisionCommand('CAMERA_DISCONNECT', 'CAMERA_RESULT');
|
||||
return this.sendVisionCommand('CAMERA_DISCONNECT', 'CAMERA_RESULT', 'CameraDisconnect');
|
||||
}
|
||||
|
||||
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 }> {
|
||||
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 }> {
|
||||
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 }> {
|
||||
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 }> {
|
||||
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 }> {
|
||||
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 }> {
|
||||
return this.sendVisionCommand('KEYENCE_SAVE_IMAGE', 'KEYENCE_RESULT');
|
||||
return this.sendVisionCommand('KEYENCE_SAVE_IMAGE', 'KEYENCE_RESULT', 'KeyenceSaveImage');
|
||||
}
|
||||
|
||||
// Light, Manual Print, Cancel Job commands
|
||||
public async toggleLight(): Promise<{ success: boolean; message: string }> {
|
||||
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 {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -537,7 +538,18 @@ class CommunicationLayer {
|
||||
count: number;
|
||||
}): Promise<{ success: boolean; message: string }> {
|
||||
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 {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -564,7 +576,8 @@ class CommunicationLayer {
|
||||
|
||||
public async cancelJob(): Promise<{ success: boolean; message: string }> {
|
||||
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 {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -588,7 +601,8 @@ class CommunicationLayer {
|
||||
|
||||
public async openManage(): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
return { success: false, message: 'Manage not yet implemented in WebView2 mode' };
|
||||
const result = await machine.OpenManage();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -612,7 +626,8 @@ class CommunicationLayer {
|
||||
|
||||
public async openManual(): Promise<{ success: boolean; message: string }> {
|
||||
if (isWebView && machine) {
|
||||
return { success: false, message: 'Manual not yet implemented in WebView2 mode' };
|
||||
const result = await machine.OpenManual();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -636,7 +651,8 @@ class CommunicationLayer {
|
||||
|
||||
public async openLogViewer(): Promise<{ success: boolean; message: string }> {
|
||||
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 {
|
||||
return new Promise((resolve, reject) => {
|
||||
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) {
|
||||
return { success: false, message: 'Folder open not yet implemented in WebView2 mode' };
|
||||
const result = await (machine as any)[methodName]();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -683,19 +700,323 @@ class CommunicationLayer {
|
||||
}
|
||||
|
||||
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 }> {
|
||||
return this.openFolder('OPEN_LOG_FOLDER');
|
||||
return this.openFolder('OPEN_LOG_FOLDER', 'OpenLogFolder');
|
||||
}
|
||||
|
||||
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 }> {
|
||||
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' }));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Package, X, ChevronRight } from 'lucide-react';
|
||||
import { comms } from '../communication';
|
||||
import { useAlert } from '../contexts/AlertContext';
|
||||
import { usePickerMove } from '../App';
|
||||
|
||||
interface FunctionMenuProps {
|
||||
isOpen: boolean;
|
||||
@@ -12,6 +13,7 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { showAlert } = useAlert();
|
||||
const { openPickerMove } = usePickerMove();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -29,8 +31,6 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleCommand = async (commandFn: () => Promise<{ success: boolean; message: string }>, actionName: string) => {
|
||||
try {
|
||||
const result = await commandFn();
|
||||
@@ -55,6 +55,8 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<div
|
||||
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]"
|
||||
@@ -77,7 +79,12 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
<div className="p-2">
|
||||
{/* Manage */}
|
||||
<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"
|
||||
>
|
||||
<span>Manage</span>
|
||||
@@ -136,5 +143,7 @@ export const FunctionMenu: React.FC<FunctionMenuProps> = ({ isOpen, onClose }) =
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const InitializeModal: React.FC<InitializeModalProps> = ({ isOpen, onClos
|
||||
]);
|
||||
const [isInitializing, setIsInitializing] = useState(false);
|
||||
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
|
||||
useEffect(() => {
|
||||
|
||||
446
FrontEnd/components/PickerMoveDialog.tsx
Normal file
446
FrontEnd/components/PickerMoveDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface PanelHeaderProps {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon }) => (
|
||||
<div className="flex items-center gap-3 mb-6 border-b border-white/10 pb-2">
|
||||
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 ${className || ''}`}>
|
||||
<div className="text-neon-blue animate-pulse">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
@@ -15,5 +17,6 @@ export const PanelHeader: React.FC<PanelHeaderProps> = ({ title, icon: Icon }) =
|
||||
{title}
|
||||
</h2>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-neon-blue/50 to-transparent"></div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,68 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 {
|
||||
isHostConnected: boolean;
|
||||
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 }) => {
|
||||
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 (
|
||||
<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">
|
||||
{['PLC', 'MOTION', 'VISION', 'LIGHT'].map(hw => (
|
||||
<div key={hw} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-neon-green rounded-full shadow-[0_0_5px_#0aff00]"></div>
|
||||
<span className="font-bold text-slate-300">{hw}</span>
|
||||
<div className="flex gap-4">
|
||||
{/* H/W 상태 표시 (윈폼 HWState와 동일) */}
|
||||
{hwStatus.map((hw) => {
|
||||
const colors = getStatusColor(hw.status);
|
||||
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 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>
|
||||
<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 className="flex gap-8 text-neon-blue">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
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 { FunctionMenu } from '../FunctionMenu';
|
||||
import { ManualPrintDialog, PrintData } from '../ManualPrintDialog';
|
||||
@@ -127,6 +127,20 @@ export const Header: React.FC<HeaderProps> = ({ currentTime, onTabChange, active
|
||||
<XCircle className="w-5 h-5" />
|
||||
<span className="leading-tight">CANCEL</span>
|
||||
</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>
|
||||
|
||||
{/* Main Navigation */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import React, { createContext, useContext, useState, ReactNode, useCallback, useRef } from 'react';
|
||||
|
||||
interface AlertConfig {
|
||||
type: 'success' | 'error' | 'warning' | 'info';
|
||||
@@ -6,8 +6,14 @@ interface AlertConfig {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ConfirmConfig {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface AlertContextType {
|
||||
showAlert: (config: AlertConfig) => void;
|
||||
showConfirm: (config: ConfirmConfig) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const AlertContext = createContext<AlertContextType | undefined>(undefined);
|
||||
@@ -26,6 +32,8 @@ interface AlertProviderProps {
|
||||
|
||||
export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => {
|
||||
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) => {
|
||||
setAlertConfig({ ...config, isOpen: true });
|
||||
@@ -35,9 +43,25 @@ export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => {
|
||||
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 (
|
||||
<AlertContext.Provider value={{ showAlert }}>
|
||||
<AlertContext.Provider value={{ showAlert, showConfirm }}>
|
||||
{children}
|
||||
{/* Alert Dialog */}
|
||||
{alertConfig && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
@@ -77,6 +101,53 @@ export const AlertProvider: React.FC<AlertProviderProps> = ({ children }) => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
320
FrontEnd/pages/HistoryPage.tsx
Normal file
320
FrontEnd/pages/HistoryPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -51,7 +51,8 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
|
||||
|
||||
// Subscribe to real-time IO updates
|
||||
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 => {
|
||||
const newIO = [...prev];
|
||||
msg.ioState.forEach((update: { id: number, type: string, state: boolean }) => {
|
||||
@@ -61,6 +62,39 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
|
||||
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 () => {
|
||||
@@ -72,6 +106,19 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
|
||||
? []
|
||||
: 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 (
|
||||
<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">
|
||||
@@ -130,11 +177,13 @@ export const IOMonitorPage: React.FC<IOMonitorPageProps> = ({ onToggle }) => {
|
||||
{axis.locks.map(lock => (
|
||||
<div
|
||||
key={lock.id}
|
||||
onClick={() => handleInterlockToggle(axis.axisIndex, lock.id)}
|
||||
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
|
||||
? '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>
|
||||
|
||||
@@ -99,6 +99,56 @@ declare global {
|
||||
InitializeDevice(): Promise<string>;
|
||||
GetInitializeStatus(): 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;
|
||||
|
||||
@@ -6,6 +6,8 @@ using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Web.WebView2.WinForms;
|
||||
using Newtonsoft.Json;
|
||||
using Project.WebUI;
|
||||
using AR;
|
||||
using arDev.DIO;
|
||||
|
||||
namespace Project.Dialog
|
||||
{
|
||||
@@ -15,14 +17,16 @@ namespace Project.Dialog
|
||||
private Timer plcTimer;
|
||||
private WebSocketServer _wsServer;
|
||||
|
||||
// Machine State (Simulated PLC Memory)
|
||||
// Machine State
|
||||
private double currX = 0, currY = 0, currZ = 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 currentRecipeId = "1"; // Default recipe
|
||||
|
||||
// IO 캐시 (변경된 값만 전송하기 위함)
|
||||
private bool[] _lastInputs;
|
||||
private bool[] _lastOutputs;
|
||||
|
||||
public fWebView()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// Set default inputs (Pressure OK, Estop OK)
|
||||
inputs[4] = true;
|
||||
inputs[6] = true;
|
||||
// IO 캐시 초기화
|
||||
int diCount = PUB.dio?.GetDICount ?? 32;
|
||||
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
|
||||
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()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
|
||||
// Form
|
||||
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||
this.Text = "STD Label Attach - Web UI";
|
||||
//
|
||||
// fWebView
|
||||
//
|
||||
this.ClientSize = new System.Drawing.Size(1784, 961);
|
||||
this.Name = "fWebView";
|
||||
this.StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "STD Label Attach - Web UI";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
private async void InitializeWebView()
|
||||
@@ -106,6 +234,9 @@ namespace Project.Dialog
|
||||
}
|
||||
}
|
||||
|
||||
// HW 상태 업데이트 카운터 (250ms 주기 = 50ms * 5)
|
||||
private int _hwUpdateCounter = 0;
|
||||
|
||||
// --- Logic Loop ---
|
||||
private void PlcTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
@@ -114,36 +245,105 @@ namespace Project.Dialog
|
||||
currY = Lerp(currY, targetY, 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
|
||||
{
|
||||
type = "STATUS_UPDATE",
|
||||
sysState = systemState,
|
||||
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);
|
||||
|
||||
// 3. Send to React via PostMessage (WebView2)
|
||||
// 4. Send to React via PostMessage (WebView2)
|
||||
if (webView != null && webView.CoreWebView2 != null)
|
||||
{
|
||||
webView.CoreWebView2.PostWebMessageAsJson(json);
|
||||
}
|
||||
|
||||
// 4. Broadcast to WebSocket (Dev/HMR)
|
||||
// 5. Broadcast to WebSocket (Dev/HMR)
|
||||
_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()
|
||||
{
|
||||
// Simply return list of all active IOs or just send all for simplicity
|
||||
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] });
|
||||
list.Add(new { id = i, type = "output", state = outputs[i] });
|
||||
int diCount = PUB.dio.GetDICount;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -164,9 +364,37 @@ namespace Project.Dialog
|
||||
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)
|
||||
@@ -214,5 +442,34 @@ namespace Project.Dialog
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
120
Handler/Project/Dialog/fWebView.resx
Normal file
120
Handler/Project/Dialog/fWebView.resx
Normal 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>
|
||||
@@ -32,8 +32,12 @@ namespace Project
|
||||
//Pub.sm.setNewStep(eSMStep.XMOVE); //홈을 위해서 바로 이동 모션으로 가게한다
|
||||
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(); }));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -730,6 +730,9 @@
|
||||
<EmbeddedResource Include="Dialog\fVAR.resx">
|
||||
<DependentUpon>fVAR.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialog\fWebView.resx">
|
||||
<DependentUpon>fWebView.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialog\fZPLEditor.resx">
|
||||
<DependentUpon>fZPLEditor.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using AR;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Project.WebUI
|
||||
{
|
||||
@@ -1145,7 +1146,14 @@ namespace Project.WebUI
|
||||
}
|
||||
|
||||
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);
|
||||
var response2 = new { success = true, message = $"Light turned {(!cur ? "ON" : "OFF")}" };
|
||||
return JsonConvert.SerializeObject(response2);
|
||||
@@ -1173,7 +1181,7 @@ namespace Project.WebUI
|
||||
var selectedPrinter = printer.ToLower() == "left" ? PUB.PrinterL : PUB.PrinterR;
|
||||
|
||||
// Create ZPL
|
||||
string zpl = selectedPrinter.makeZPL_210908(new AR.Class.Reel
|
||||
string zpl = selectedPrinter.makeZPL_210908(new Class.Reel
|
||||
{
|
||||
SID = sid,
|
||||
venderLot = venderLot,
|
||||
@@ -1290,10 +1298,11 @@ namespace Project.WebUI
|
||||
return JsonConvert.SerializeObject(response);
|
||||
}
|
||||
|
||||
// The manage dialog (fPickerMove) cannot be opened from web UI
|
||||
// This would require implementing a separate picker management page
|
||||
// Set flag to indicate picker move dialog is open
|
||||
PUB.flag.set(eVarBool.FG_MOVE_PICKER, true, "PICKERMOVE_WEB");
|
||||
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);
|
||||
}
|
||||
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()
|
||||
{
|
||||
try
|
||||
@@ -1433,5 +1465,837 @@ namespace Project.WebUI
|
||||
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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +334,13 @@ namespace Project.WebUI
|
||||
var response = new { type = "MANAGE_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
|
||||
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")
|
||||
{
|
||||
var bridge = new MachineBridge(_mainForm);
|
||||
@@ -376,6 +383,151 @@ namespace Project.WebUI
|
||||
var response = new { type = "FOLDER_RESULT", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -493,6 +493,18 @@ namespace Project
|
||||
|
||||
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)
|
||||
{
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user