Files
WebUITest-RealProjecT/FrontEnd/communication.ts
arDTDev 8bbd76e670 feat: Add vision controls, function menu, and custom alert dialogs
- Add Vision menu with Camera (QRCode) and Barcode (Keyence) controls
- Add Function menu with Manage, Log Viewer, and folder navigation
- Add quick action buttons (Manual, Light, Print, Cancel) to header
- Replace browser alert() with custom AlertDialog component
- Add MachineBridge methods for vision, lighting, folders, and manual operations
- Add WebSocketServer handlers for all new commands
- Add communication layer methods for frontend-backend integration

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 23:39:38 +09:00

690 lines
27 KiB
TypeScript

// Check if running in WebView2
const isWebView = typeof window !== 'undefined' && !!window.chrome?.webview;
// 비동기 프록시 캐싱 (한 번만 초기화) - 매번 접근 시 오버헤드 제거
const machine = isWebView ? window.chrome!.webview!.hostObjects.machine : null;
type MessageCallback = (data: any) => void;
class CommunicationLayer {
private listeners: MessageCallback[] = [];
private ws: WebSocket | null = null;
private isConnected = false;
constructor() {
if (isWebView) {
console.log("[COMM] Running in WebView2 Mode");
this.isConnected = true; // WebView2 is always connected
window.chrome!.webview!.addEventListener('message', (event: any) => {
this.notifyListeners(event.data);
});
} else {
console.log("[COMM] Running in Browser Mode (WebSocket)");
this.connectWebSocket();
}
}
private connectWebSocket() {
this.ws = new WebSocket('ws://localhost:8081');
this.ws.onopen = () => {
console.log("[COMM] WebSocket Connected");
this.isConnected = true;
this.notifyListeners({ type: 'CONNECTION_STATE', connected: true });
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.notifyListeners(data);
} catch (e) {
console.error("[COMM] JSON Parse Error", e);
}
};
this.ws.onclose = () => {
console.log("[COMM] WebSocket Closed. Reconnecting...");
this.isConnected = false;
this.notifyListeners({ type: 'CONNECTION_STATE', connected: false });
setTimeout(() => this.connectWebSocket(), 2000);
};
this.ws.onerror = (err) => {
console.error("[COMM] WebSocket Error", err);
};
}
private notifyListeners(data: any) {
this.listeners.forEach(cb => cb(data));
}
public subscribe(callback: MessageCallback) {
this.listeners.push(callback);
return () => {
this.listeners = this.listeners.filter(cb => cb !== callback);
};
}
public getConnectionState(): boolean {
return this.isConnected;
}
// --- API Methods ---
public async getConfig(): Promise<string> {
if (isWebView && machine) {
return await machine.GetConfig();
} else {
// WebSocket Request/Response Pattern
return new Promise((resolve, reject) => {
// 1. Wait for Connection (max 2s)
if (!this.isConnected) {
// Simple wait logic (could be improved)
setTimeout(() => {
if (!this.isConnected) reject("WebSocket connection timeout");
}, 2000);
}
// 2. Send Request with Timeout (max 10s)
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject("Config fetch timeout");
}, 10000);
const handler = (data: any) => {
if (data.type === 'CONFIG_DATA') {
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_CONFIG' }));
});
}
}
public async getIOList(): Promise<string> {
if (isWebView && machine) {
return await machine.GetIOList();
} else {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
setTimeout(() => {
if (!this.isConnected) reject("WebSocket connection timeout");
}, 2000);
}
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject("IO fetch timeout");
}, 10000);
const handler = (data: any) => {
if (data.type === 'IO_LIST_DATA') {
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_IO_LIST' }));
});
}
}
public async getRecipeList(): Promise<string> {
if (isWebView && machine) {
return await machine.GetRecipeList();
} else {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
setTimeout(() => {
if (!this.isConnected) reject("WebSocket connection timeout");
}, 2000);
}
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject("Recipe fetch timeout");
}, 10000);
const handler = (data: any) => {
if (data.type === 'RECIPE_LIST_DATA') {
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_RECIPE_LIST' }));
});
}
}
public async saveConfig(configJson: string): Promise<void> {
if (isWebView && machine) {
await machine.SaveConfig(configJson);
} else {
this.ws?.send(JSON.stringify({ type: 'SAVE_CONFIG', data: JSON.parse(configJson) }));
}
}
public async sendControl(command: string) {
if (isWebView && machine) {
await machine.SystemControl(command);
} else {
this.ws?.send(JSON.stringify({ type: 'CONTROL', command }));
}
}
public async moveAxis(axis: string, value: number) {
if (isWebView && machine) {
await machine.MoveAxis(axis, value);
} else {
this.ws?.send(JSON.stringify({ type: 'MOVE', axis, value }));
}
}
public async setIO(id: number, state: boolean) {
if (isWebView && machine) {
await machine.SetIO(id, false, state);
} else {
this.ws?.send(JSON.stringify({ type: 'SET_IO', id, state }));
}
}
public async selectRecipe(recipeId: string): Promise<{ success: boolean; message: string; recipeId?: string }> {
if (isWebView && machine) {
const resultJson = await machine.SelectRecipe(recipeId);
return JSON.parse(resultJson);
} 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: "Recipe selection timeout" });
}, 10000);
const handler = (data: any) => {
if (data.type === 'RECIPE_SELECTED') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'SELECT_RECIPE', recipeId }));
});
}
}
public async copyRecipe(recipeId: string, newName: string): Promise<{ success: boolean; message: string; newRecipe?: any }> {
if (isWebView && machine) {
const resultJson = await machine.CopyRecipe(recipeId, newName);
return JSON.parse(resultJson);
} 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: "Recipe copy timeout" });
}, 10000);
const handler = (data: any) => {
if (data.type === 'RECIPE_COPIED') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'COPY_RECIPE', recipeId, newName }));
});
}
}
public async deleteRecipe(recipeId: string): Promise<{ success: boolean; message: string; recipeId?: string }> {
if (isWebView && machine) {
const resultJson = await machine.DeleteRecipe(recipeId);
return JSON.parse(resultJson);
} 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: "Recipe delete timeout" });
}, 10000);
const handler = (data: any) => {
if (data.type === 'RECIPE_DELETED') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'DELETE_RECIPE', recipeId }));
});
}
}
public async getRecipe(recipeTitle: string): Promise<string> {
if (isWebView && machine) {
return await machine.GetRecipe(recipeTitle);
} else {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
setTimeout(() => {
if (!this.isConnected) reject("WebSocket connection timeout");
}, 2000);
}
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject("Recipe fetch timeout");
}, 10000);
const handler = (data: any) => {
if (data.type === 'RECIPE_DATA') {
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_RECIPE', recipeTitle }));
});
}
}
public async saveRecipe(recipeTitle: string, recipeData: string): Promise<{ success: boolean; message: string; recipeId?: string }> {
if (isWebView && machine) {
const resultJson = await machine.SaveRecipe(recipeTitle, recipeData);
return JSON.parse(resultJson);
} 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: "Recipe save timeout" });
}, 10000);
const handler = (data: any) => {
if (data.type === 'RECIPE_SAVED') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'SAVE_RECIPE', recipeTitle, recipeData }));
});
}
}
public async initializeDevice(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
const resultJson = await machine.InitializeDevice();
return JSON.parse(resultJson);
} 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: "Initialize device timeout" });
}, 10000);
const handler = (data: any) => {
if (data.type === 'DEVICE_INITIALIZED') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'INITIALIZE_DEVICE' }));
});
}
}
public async getInitializeStatus(): Promise<string> {
if (isWebView && machine) {
return await machine.GetInitializeStatus();
} else {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
setTimeout(() => {
if (!this.isConnected) reject("WebSocket connection timeout");
}, 2000);
}
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject("Initialize status fetch timeout");
}, 10000);
const handler = (data: any) => {
if (data.type === 'INITIALIZE_STATUS_DATA') {
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_INITIALIZE_STATUS' }));
});
}
}
public async getProcessedData(): Promise<string> {
if (isWebView && machine) {
return await machine.GetProcessedData();
} else {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
setTimeout(() => {
if (!this.isConnected) reject("WebSocket connection timeout");
}, 2000);
}
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject("Processed data fetch timeout");
}, 10000);
const handler = (data: any) => {
if (data.type === 'PROCESSED_DATA') {
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_PROCESSED_DATA' }));
});
}
}
// ===== VISION CONTROL METHODS =====
private async sendVisionCommand(command: string, responseType: 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' };
} 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: "Vision 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 cameraConnect(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_CONNECT', 'CAMERA_RESULT');
}
public async cameraDisconnect(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_DISCONNECT', 'CAMERA_RESULT');
}
public async cameraGetImage(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_GET_IMAGE', 'CAMERA_RESULT');
}
public async cameraLiveView(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_LIVE_VIEW', 'CAMERA_RESULT');
}
public async cameraReadTest(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('CAMERA_READ_TEST', 'CAMERA_RESULT');
}
public async keyenceTriggerOn(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('KEYENCE_TRIGGER_ON', 'KEYENCE_RESULT');
}
public async keyenceTriggerOff(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('KEYENCE_TRIGGER_OFF', 'KEYENCE_RESULT');
}
public async keyenceGetImage(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('KEYENCE_GET_IMAGE', 'KEYENCE_RESULT');
}
public async keyenceSaveImage(): Promise<{ success: boolean; message: string }> {
return this.sendVisionCommand('KEYENCE_SAVE_IMAGE', 'KEYENCE_RESULT');
}
// 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' };
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject(new Error('Light toggle timeout'));
}, 5000);
const handler = (data: any) => {
if (data.type === 'LIGHT_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_LIGHT' }));
});
}
}
public async openManualPrint(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
return { success: false, message: 'Manual print not yet implemented in WebView2 mode' };
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject(new Error('Manual print timeout'));
}, 5000);
const handler = (data: any) => {
if (data.type === 'MANUAL_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: 'OPEN_MANUAL_PRINT' }));
});
}
}
public async cancelJob(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
return { success: false, message: 'Cancel job not yet implemented in WebView2 mode' };
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject(new Error('Cancel job timeout'));
}, 5000);
const handler = (data: any) => {
if (data.type === 'CANCEL_JOB_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_JOB' }));
});
}
}
public async openManage(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
return { success: false, message: 'Manage not yet implemented in WebView2 mode' };
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject(new Error('Open manage timeout'));
}, 5000);
const handler = (data: any) => {
if (data.type === '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: 'OPEN_MANAGE' }));
});
}
}
public async openManual(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
return { success: false, message: 'Manual not yet implemented in WebView2 mode' };
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject(new Error('Open manual timeout'));
}, 5000);
const handler = (data: any) => {
if (data.type === 'MANUAL_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'OPEN_MANUAL' }));
});
}
}
public async openLogViewer(): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
return { success: false, message: 'Log viewer not yet implemented in WebView2 mode' };
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject(new Error('Open log viewer timeout'));
}, 5000);
const handler = (data: any) => {
if (data.type === 'LOG_VIEWER_RESULT') {
clearTimeout(timeoutId);
this.listeners = this.listeners.filter(cb => cb !== handler);
resolve(data.data);
}
};
this.listeners.push(handler);
this.ws?.send(JSON.stringify({ type: 'OPEN_LOG_VIEWER' }));
});
}
}
private async openFolder(command: string): Promise<{ success: boolean; message: string }> {
if (isWebView && machine) {
return { success: false, message: 'Folder open not yet implemented in WebView2 mode' };
} else {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.listeners = this.listeners.filter(cb => cb !== handler);
reject(new Error('Open folder timeout'));
}, 5000);
const handler = (data: any) => {
if (data.type === 'FOLDER_RESULT') {
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 openProgramFolder(): Promise<{ success: boolean; message: string }> {
return this.openFolder('OPEN_PROGRAM_FOLDER');
}
public async openLogFolder(): Promise<{ success: boolean; message: string }> {
return this.openFolder('OPEN_LOG_FOLDER');
}
public async openScreenshotFolder(): Promise<{ success: boolean; message: string }> {
return this.openFolder('OPEN_SCREENSHOT_FOLDER');
}
public async openSavedDataFolder(): Promise<{ success: boolean; message: string }> {
return this.openFolder('OPEN_SAVED_DATA_FOLDER');
}
}
export const comms = new CommunicationLayer();