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:
@@ -6,6 +6,8 @@ using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Web.WebView2.WinForms;
|
||||
using Newtonsoft.Json;
|
||||
using Project.WebUI;
|
||||
using AR;
|
||||
using arDev.DIO;
|
||||
|
||||
namespace Project.Dialog
|
||||
{
|
||||
@@ -15,14 +17,16 @@ namespace Project.Dialog
|
||||
private Timer plcTimer;
|
||||
private WebSocketServer _wsServer;
|
||||
|
||||
// Machine State (Simulated PLC Memory)
|
||||
// Machine State
|
||||
private double currX = 0, currY = 0, currZ = 0;
|
||||
private double targetX = 0, targetY = 0, targetZ = 0;
|
||||
private bool[] inputs = new bool[32];
|
||||
private bool[] outputs = new bool[32];
|
||||
private string systemState = "IDLE";
|
||||
private string currentRecipeId = "1"; // Default recipe
|
||||
|
||||
// IO 캐시 (변경된 값만 전송하기 위함)
|
||||
private bool[] _lastInputs;
|
||||
private bool[] _lastOutputs;
|
||||
|
||||
public fWebView()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -38,25 +42,149 @@ namespace Project.Dialog
|
||||
MessageBox.Show("Failed to start WebSocket Server (Port 8081). Run as Admin or allow port.\n" + ex.Message);
|
||||
}
|
||||
|
||||
// Set default inputs (Pressure OK, Estop OK)
|
||||
inputs[4] = true;
|
||||
inputs[6] = true;
|
||||
// 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();
|
||||
|
||||
// Form
|
||||
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||
this.Text = "STD Label Attach - Web UI";
|
||||
//
|
||||
// fWebView
|
||||
//
|
||||
this.ClientSize = new System.Drawing.Size(1784, 961);
|
||||
this.Name = "fWebView";
|
||||
this.StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "STD Label Attach - Web UI";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
private async void InitializeWebView()
|
||||
@@ -106,6 +234,9 @@ namespace Project.Dialog
|
||||
}
|
||||
}
|
||||
|
||||
// HW 상태 업데이트 카운터 (250ms 주기 = 50ms * 5)
|
||||
private int _hwUpdateCounter = 0;
|
||||
|
||||
// --- Logic Loop ---
|
||||
private void PlcTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
@@ -114,36 +245,105 @@ namespace Project.Dialog
|
||||
currY = Lerp(currY, targetY, 0.1);
|
||||
currZ = Lerp(currZ, targetZ, 0.1);
|
||||
|
||||
// 2. Prepare Data Packet
|
||||
// 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() // Function to return array of IO states
|
||||
ioState = GetChangedIOs() // 변경된 IO만 전송
|
||||
};
|
||||
|
||||
string json = JsonConvert.SerializeObject(payload);
|
||||
|
||||
// 3. Send to React via PostMessage (WebView2)
|
||||
// 4. Send to React via PostMessage (WebView2)
|
||||
if (webView != null && webView.CoreWebView2 != null)
|
||||
{
|
||||
webView.CoreWebView2.PostWebMessageAsJson(json);
|
||||
}
|
||||
|
||||
// 4. Broadcast to WebSocket (Dev/HMR)
|
||||
// 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()
|
||||
{
|
||||
// Simply return list of all active IOs or just send all for simplicity
|
||||
var list = new List<object>();
|
||||
for (int i = 0; i < 32; i++)
|
||||
|
||||
// 실제 DIO에서 값 읽기
|
||||
if (PUB.dio != null)
|
||||
{
|
||||
list.Add(new { id = i, type = "input", state = inputs[i] });
|
||||
list.Add(new { id = i, type = "output", state = outputs[i] });
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -164,9 +364,37 @@ namespace Project.Dialog
|
||||
if (axis == "Z") targetZ = val;
|
||||
}
|
||||
|
||||
public void SetOutput(int id, bool state)
|
||||
// DO 출력 제어 (DIOMonitor.cs의 tblDO_ItemClick과 동일한 로직)
|
||||
public bool SetOutput(int id, bool state)
|
||||
{
|
||||
if (id < 32) outputs[id] = 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)
|
||||
@@ -214,5 +442,34 @@ namespace Project.Dialog
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user