- 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>
476 lines
16 KiB
C#
476 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Windows.Forms;
|
|
using Microsoft.Web.WebView2.Core;
|
|
using Microsoft.Web.WebView2.WinForms;
|
|
using Newtonsoft.Json;
|
|
using Project.WebUI;
|
|
using AR;
|
|
using arDev.DIO;
|
|
|
|
namespace Project.Dialog
|
|
{
|
|
public partial class fWebView : Form
|
|
{
|
|
private WebView2 webView;
|
|
private Timer plcTimer;
|
|
private WebSocketServer _wsServer;
|
|
|
|
// Machine State
|
|
private double currX = 0, currY = 0, currZ = 0;
|
|
private double targetX = 0, targetY = 0, targetZ = 0;
|
|
private string systemState = "IDLE";
|
|
private string currentRecipeId = "1"; // Default recipe
|
|
|
|
// IO 캐시 (변경된 값만 전송하기 위함)
|
|
private bool[] _lastInputs;
|
|
private bool[] _lastOutputs;
|
|
|
|
public fWebView()
|
|
{
|
|
InitializeComponent();
|
|
InitializeWebView();
|
|
|
|
// Start WebSocket Server for HMR/Dev
|
|
try
|
|
{
|
|
_wsServer = new WebSocketServer("http://localhost:8081/", this);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("Failed to start WebSocket Server (Port 8081). Run as Admin or allow port.\n" + ex.Message);
|
|
}
|
|
|
|
// 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();
|
|
//
|
|
// fWebView
|
|
//
|
|
this.ClientSize = new System.Drawing.Size(1784, 961);
|
|
this.Name = "fWebView";
|
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
|
this.Text = "STD Label Attach - Web UI";
|
|
this.ResumeLayout(false);
|
|
|
|
}
|
|
|
|
private async void InitializeWebView()
|
|
{
|
|
// 1. Setup Virtual Host (http://hmi.local) pointing to FrontEnd/dist folder
|
|
// Navigate up from bin/debug/ to project root, then to FrontEnd/dist
|
|
string projectRoot = Path.GetFullPath(Path.Combine(Application.StartupPath, @"..\..\..\.."));
|
|
string wwwroot = Path.Combine(projectRoot, @"FrontEnd\dist");
|
|
|
|
this.Text = $"STD Label Attach - {wwwroot}";
|
|
|
|
webView = new WebView2();
|
|
webView.Dock = DockStyle.Fill;
|
|
this.Controls.Add(webView);
|
|
|
|
try
|
|
{
|
|
await webView.EnsureCoreWebView2Async();
|
|
|
|
if (!Directory.Exists(wwwroot))
|
|
{
|
|
MessageBox.Show($"Could not find frontend build at:\n{wwwroot}\n\nPlease run 'npm run build' in the FrontEnd folder.",
|
|
"Frontend Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
// Fallback to local wwwroot if needed, or just allow it to fail gracefully
|
|
Directory.CreateDirectory(wwwroot);
|
|
}
|
|
|
|
webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
|
|
"hmi.local",
|
|
wwwroot,
|
|
CoreWebView2HostResourceAccessKind.Allow
|
|
);
|
|
|
|
// 2. Inject Native Object
|
|
webView.CoreWebView2.AddHostObjectToScript("machine", new MachineBridge(this));
|
|
|
|
// 3. Load UI
|
|
webView.Source = new Uri("http://hmi.local/index.html");
|
|
|
|
// Disable default browser keys (F5, F12 etc) if needed for kiosk mode
|
|
webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"WebView2 초기화 실패: {ex.Message}", "Error",
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
// HW 상태 업데이트 카운터 (250ms 주기 = 50ms * 5)
|
|
private int _hwUpdateCounter = 0;
|
|
|
|
// --- Logic Loop ---
|
|
private void PlcTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
// 1. Simulate Motion (Move Current towards Target)
|
|
currX = Lerp(currX, targetX, 0.1);
|
|
currY = Lerp(currY, targetY, 0.1);
|
|
currZ = Lerp(currZ, targetZ, 0.1);
|
|
|
|
// 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() // 변경된 IO만 전송
|
|
};
|
|
|
|
string json = JsonConvert.SerializeObject(payload);
|
|
|
|
// 4. Send to React via PostMessage (WebView2)
|
|
if (webView != null && webView.CoreWebView2 != null)
|
|
{
|
|
webView.CoreWebView2.PostWebMessageAsJson(json);
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
var list = new List<object>();
|
|
|
|
// 실제 DIO에서 값 읽기
|
|
if (PUB.dio != null)
|
|
{
|
|
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;
|
|
}
|
|
|
|
private void FWebView_Load(object sender, EventArgs e)
|
|
{
|
|
// Setup Simulation Timer (50ms)
|
|
plcTimer = new Timer();
|
|
plcTimer.Interval = 50;
|
|
plcTimer.Tick += PlcTimer_Tick;
|
|
plcTimer.Start();
|
|
}
|
|
|
|
// --- Helper Methods called by Bridge ---
|
|
public void SetTargetPosition(string axis, double val)
|
|
{
|
|
if (axis == "X") targetX = val;
|
|
if (axis == "Y") targetY = val;
|
|
if (axis == "Z") targetZ = val;
|
|
}
|
|
|
|
// DO 출력 제어 (DIOMonitor.cs의 tblDO_ItemClick과 동일한 로직)
|
|
public bool SetOutput(int id, bool 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)
|
|
{
|
|
systemState = (cmd == "START") ? "RUNNING" : (cmd == "STOP") ? "PAUSED" : "IDLE";
|
|
}
|
|
|
|
public void SetCurrentRecipe(string recipeId)
|
|
{
|
|
currentRecipeId = recipeId;
|
|
Console.WriteLine($"[fWebView] Current recipe set to: {recipeId}");
|
|
// In real app, load recipe parameters here
|
|
}
|
|
|
|
public string GetCurrentRecipe()
|
|
{
|
|
return currentRecipeId;
|
|
}
|
|
|
|
private double Lerp(double a, double b, double t) => a + (b - a) * t;
|
|
|
|
// JavaScript 실행 메서드
|
|
public async void ExecuteScriptAsync(string script)
|
|
{
|
|
try
|
|
{
|
|
string result = await webView.CoreWebView2.ExecuteScriptAsync(script);
|
|
Console.WriteLine($"Script result: {result}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Script execution failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// C#에서 JavaScript로 메시지 전송
|
|
public void PostMessageToWeb(string message)
|
|
{
|
|
try
|
|
{
|
|
webView.CoreWebView2.PostWebMessageAsString(message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
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}");
|
|
}
|
|
}
|
|
}
|
|
}
|