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

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

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

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

View File

@@ -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' }));
});
}
}
}