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(); BroadcastVisionData(); } } // 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}"); } } // Vision Data 브로드캐스트 (listView21과 동일한 데이터) private void BroadcastVisionData() { try { var bridge = new WebUI.MachineBridge(this); string visionDataJson = bridge.GetVisionData(); var payload = new { type = "VISION_DATA_UPDATE", data = JsonConvert.DeserializeObject(visionDataJson) }; 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] Vision data broadcast error: {ex.Message}"); } } private List GetChangedIOs() { var list = new List(); // 실제 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}"); } } } }