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

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>