Files
WebUITest-RealProjecT/Handler/Project/Dialog/fWebView.cs
arDTDev 86fe466b55 feat: Add VisionData panel, HW error display, and reel handler 3D model
- Add hardware error banner with priority system (motion > i/o > emergency)
- Add DIO status to HW status display with backend integration
- Remove status text from HW status, keep only LED indicators
- Add VisionDataPanel showing real-time recognized data for L/C/R ports
- Add GetVisionData API in MachineBridge with batch field support
- Add BroadcastVisionData function (250ms interval)
- Replace 3D model with detailed reel handler equipment
- Use OrthographicCamera with front view for distortion-free display
- Fix ProcessedDataPanel layout to avoid right sidebar overlap
- Show log viewer filename in error message when file not found

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 23:22:56 +09:00

508 lines
17 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();
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<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}");
}
}
}
}