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

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