feat: Add WebView2 backend integration for React UI
- Add WebView2 support to STDLabelAttach project - Create MachineBridge.cs for JavaScript-C# communication - GetConfig/SaveConfig using SETTING.Data reflection - Recipe management (CRUD operations) - IO control interface - Motion control simulation - Add WebSocketServer.cs for HMR development support - Update fWebView.cs with PLC simulation and status updates - Integrate with existing project settings (Data/Setting.json) - Add virtual host mapping (http://hmi.local → FrontEnd/dist) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,49 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using Microsoft.Web.WebView2.Core;
|
using Microsoft.Web.WebView2.Core;
|
||||||
using Microsoft.Web.WebView2.WinForms;
|
using Microsoft.Web.WebView2.WinForms;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Project.WebUI;
|
||||||
|
|
||||||
namespace Project.Dialog
|
namespace Project.Dialog
|
||||||
{
|
{
|
||||||
public partial class fWebView : Form
|
public partial class fWebView : Form
|
||||||
{
|
{
|
||||||
private WebView2 webView2;
|
private WebView2 webView;
|
||||||
private TextBox txtUrl;
|
private Timer plcTimer;
|
||||||
private Button btnGo;
|
private WebSocketServer _wsServer;
|
||||||
private Button btnBack;
|
|
||||||
private Button btnForward;
|
// Machine State (Simulated PLC Memory)
|
||||||
private Button btnRefresh;
|
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
|
||||||
|
|
||||||
public fWebView()
|
public fWebView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
InitializeWebView();
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default inputs (Pressure OK, Estop OK)
|
||||||
|
inputs[4] = true;
|
||||||
|
inputs[6] = true;
|
||||||
|
|
||||||
|
// Load event handler
|
||||||
|
this.Load += FWebView_Load;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
@@ -26,71 +52,52 @@ namespace Project.Dialog
|
|||||||
|
|
||||||
// Form
|
// Form
|
||||||
this.ClientSize = new System.Drawing.Size(1200, 800);
|
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||||
this.Text = "WebView2 Browser";
|
this.Text = "STD Label Attach - Web UI";
|
||||||
this.Name = "fWebView";
|
this.Name = "fWebView";
|
||||||
|
this.StartPosition = FormStartPosition.CenterScreen;
|
||||||
// URL TextBox
|
|
||||||
this.txtUrl = new TextBox();
|
|
||||||
this.txtUrl.Location = new System.Drawing.Point(100, 10);
|
|
||||||
this.txtUrl.Size = new System.Drawing.Size(900, 25);
|
|
||||||
this.txtUrl.KeyDown += TxtUrl_KeyDown;
|
|
||||||
|
|
||||||
// Go Button
|
|
||||||
this.btnGo = new Button();
|
|
||||||
this.btnGo.Location = new System.Drawing.Point(1010, 10);
|
|
||||||
this.btnGo.Size = new System.Drawing.Size(70, 25);
|
|
||||||
this.btnGo.Text = "Go";
|
|
||||||
this.btnGo.Click += BtnGo_Click;
|
|
||||||
|
|
||||||
// Back Button
|
|
||||||
this.btnBack = new Button();
|
|
||||||
this.btnBack.Location = new System.Drawing.Point(10, 10);
|
|
||||||
this.btnBack.Size = new System.Drawing.Size(25, 25);
|
|
||||||
this.btnBack.Text = "◀";
|
|
||||||
this.btnBack.Click += BtnBack_Click;
|
|
||||||
|
|
||||||
// Forward Button
|
|
||||||
this.btnForward = new Button();
|
|
||||||
this.btnForward.Location = new System.Drawing.Point(40, 10);
|
|
||||||
this.btnForward.Size = new System.Drawing.Size(25, 25);
|
|
||||||
this.btnForward.Text = "▶";
|
|
||||||
this.btnForward.Click += BtnForward_Click;
|
|
||||||
|
|
||||||
// Refresh Button
|
|
||||||
this.btnRefresh = new Button();
|
|
||||||
this.btnRefresh.Location = new System.Drawing.Point(70, 10);
|
|
||||||
this.btnRefresh.Size = new System.Drawing.Size(25, 25);
|
|
||||||
this.btnRefresh.Text = "⟳";
|
|
||||||
this.btnRefresh.Click += BtnRefresh_Click;
|
|
||||||
|
|
||||||
// WebView2
|
|
||||||
this.webView2 = new WebView2();
|
|
||||||
this.webView2.Location = new System.Drawing.Point(10, 45);
|
|
||||||
this.webView2.Size = new System.Drawing.Size(1170, 740);
|
|
||||||
|
|
||||||
this.Controls.Add(this.txtUrl);
|
|
||||||
this.Controls.Add(this.btnGo);
|
|
||||||
this.Controls.Add(this.btnBack);
|
|
||||||
this.Controls.Add(this.btnForward);
|
|
||||||
this.Controls.Add(this.btnRefresh);
|
|
||||||
this.Controls.Add(this.webView2);
|
|
||||||
|
|
||||||
this.ResumeLayout(false);
|
this.ResumeLayout(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void InitializeWebView()
|
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
|
try
|
||||||
{
|
{
|
||||||
await webView2.EnsureCoreWebView2Async(null);
|
await webView.EnsureCoreWebView2Async();
|
||||||
|
|
||||||
// 네비게이션 이벤트 핸들러
|
if (!Directory.Exists(wwwroot))
|
||||||
webView2.CoreWebView2.NavigationCompleted += CoreWebView2_NavigationCompleted;
|
{
|
||||||
webView2.CoreWebView2.SourceChanged += CoreWebView2_SourceChanged;
|
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(
|
||||||
webView2.CoreWebView2.Navigate("http://localhost:3000");
|
"hmi.local",
|
||||||
txtUrl.Text = "http://localhost:3000";
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -99,84 +106,99 @@ namespace Project.Dialog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CoreWebView2_SourceChanged(object sender, CoreWebView2SourceChangedEventArgs e)
|
// --- Logic Loop ---
|
||||||
|
private void PlcTimer_Tick(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
txtUrl.Text = webView2.Source.ToString();
|
// 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);
|
||||||
|
|
||||||
private void CoreWebView2_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
|
// 2. Prepare Data Packet
|
||||||
{
|
var payload = new
|
||||||
btnBack.Enabled = webView2.CanGoBack;
|
|
||||||
btnForward.Enabled = webView2.CanGoForward;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BtnGo_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
NavigateToUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TxtUrl_KeyDown(object sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.KeyCode == Keys.Enter)
|
|
||||||
{
|
{
|
||||||
NavigateToUrl();
|
type = "STATUS_UPDATE",
|
||||||
e.Handled = true;
|
sysState = systemState,
|
||||||
e.SuppressKeyPress = true;
|
position = new { x = currX, y = currY, z = currZ },
|
||||||
}
|
ioState = GetChangedIOs() // Function to return array of IO states
|
||||||
}
|
};
|
||||||
|
|
||||||
private void NavigateToUrl()
|
string json = JsonConvert.SerializeObject(payload);
|
||||||
{
|
|
||||||
string url = txtUrl.Text;
|
// 3. Send to React via PostMessage (WebView2)
|
||||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
if (webView != null && webView.CoreWebView2 != null)
|
||||||
{
|
{
|
||||||
url = "http://" + url;
|
webView.CoreWebView2.PostWebMessageAsJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
// 4. Broadcast to WebSocket (Dev/HMR)
|
||||||
{
|
_wsServer?.Broadcast(json);
|
||||||
webView2.CoreWebView2.Navigate(url);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show($"페이지 로드 실패: {ex.Message}", "Error",
|
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BtnBack_Click(object sender, EventArgs e)
|
private List<object> GetChangedIOs()
|
||||||
{
|
{
|
||||||
if (webView2.CanGoBack)
|
// 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++)
|
||||||
{
|
{
|
||||||
webView2.GoBack();
|
list.Add(new { id = i, type = "input", state = inputs[i] });
|
||||||
|
list.Add(new { id = i, type = "output", state = outputs[i] });
|
||||||
}
|
}
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BtnForward_Click(object sender, EventArgs e)
|
private void FWebView_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (webView2.CanGoForward)
|
// Setup Simulation Timer (50ms)
|
||||||
{
|
plcTimer = new Timer();
|
||||||
webView2.GoForward();
|
plcTimer.Interval = 50;
|
||||||
}
|
plcTimer.Tick += PlcTimer_Tick;
|
||||||
|
plcTimer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BtnRefresh_Click(object sender, EventArgs e)
|
// --- Helper Methods called by Bridge ---
|
||||||
|
public void SetTargetPosition(string axis, double val)
|
||||||
{
|
{
|
||||||
webView2.Reload();
|
if (axis == "X") targetX = val;
|
||||||
|
if (axis == "Y") targetY = val;
|
||||||
|
if (axis == "Z") targetZ = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
// JavaScript 실행 예제 메서드
|
public void SetOutput(int id, bool state)
|
||||||
|
{
|
||||||
|
if (id < 32) outputs[id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
public async void ExecuteScriptAsync(string script)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string result = await webView2.CoreWebView2.ExecuteScriptAsync(script);
|
string result = await webView.CoreWebView2.ExecuteScriptAsync(script);
|
||||||
MessageBox.Show($"실행 결과: {result}");
|
Console.WriteLine($"Script result: {result}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"스크립트 실행 실패: {ex.Message}", "Error",
|
Console.WriteLine($"Script execution failed: {ex.Message}");
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,23 +207,12 @@ namespace Project.Dialog
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
webView2.CoreWebView2.PostWebMessageAsString(message);
|
webView.CoreWebView2.PostWebMessageAsString(message);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"메시지 전송 실패: {ex.Message}", "Error",
|
Console.WriteLine($"PostMessage failed: {ex.Message}");
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JavaScript에서 C#으로 메시지 수신
|
|
||||||
private void SetupWebMessageReceived()
|
|
||||||
{
|
|
||||||
webView2.CoreWebView2.WebMessageReceived += (sender, e) =>
|
|
||||||
{
|
|
||||||
string message = e.TryGetWebMessageAsString();
|
|
||||||
MessageBox.Show($"웹에서 받은 메시지: {message}");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,15 @@
|
|||||||
<SpecificVersion>False</SpecificVersion>
|
<SpecificVersion>False</SpecificVersion>
|
||||||
<HintPath>..\DLL\libxl\libxl.net.dll</HintPath>
|
<HintPath>..\DLL\libxl\libxl.net.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="Microsoft.Web.WebView2.Core, Version=1.0.2903.40, Culture=neutral, PublicKeyToken=2a8ab48044d2601e, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\Microsoft.Web.WebView2.1.0.2903.40\lib\net462\Microsoft.Web.WebView2.Core.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Microsoft.Web.WebView2.WinForms, Version=1.0.2903.40, Culture=neutral, PublicKeyToken=2a8ab48044d2601e, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\Microsoft.Web.WebView2.1.0.2903.40\lib\net462\Microsoft.Web.WebView2.WinForms.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Microsoft.Web.WebView2.Wpf, Version=1.0.2903.40, Culture=neutral, PublicKeyToken=2a8ab48044d2601e, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\Microsoft.Web.WebView2.1.0.2903.40\lib\net462\Microsoft.Web.WebView2.Wpf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
@@ -142,15 +151,6 @@
|
|||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
<Reference Include="Microsoft.Web.WebView2.Core, Version=1.0.2849.39, Culture=neutral, PublicKeyToken=2a8ab48044d2601e, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Microsoft.Web.WebView2.1.0.2849.39\lib\net45\Microsoft.Web.WebView2.Core.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Microsoft.Web.WebView2.WinForms, Version=1.0.2849.39, Culture=neutral, PublicKeyToken=2a8ab48044d2601e, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Microsoft.Web.WebView2.1.0.2849.39\lib\net45\Microsoft.Web.WebView2.WinForms.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Microsoft.Web.WebView2.Wpf, Version=1.0.2849.39, Culture=neutral, PublicKeyToken=2a8ab48044d2601e, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\packages\Microsoft.Web.WebView2.1.0.2849.39\lib\net45\Microsoft.Web.WebView2.Wpf.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
<Reference Include="System.Deployment" />
|
<Reference Include="System.Deployment" />
|
||||||
<Reference Include="System.Drawing" />
|
<Reference Include="System.Drawing" />
|
||||||
@@ -318,6 +318,8 @@
|
|||||||
<Compile Include="Dialog\fWebView.cs">
|
<Compile Include="Dialog\fWebView.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="WebUI\MachineBridge.cs" />
|
||||||
|
<Compile Include="WebUI\WebSocketServer.cs" />
|
||||||
<Compile Include="Dialog\fZPLEditor.cs">
|
<Compile Include="Dialog\fZPLEditor.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
@@ -992,4 +994,11 @@
|
|||||||
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
|
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
<Import Project="..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\Microsoft.Web.WebView2.targets" Condition="Exists('..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\Microsoft.Web.WebView2.targets')" />
|
||||||
|
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ErrorText>이 프로젝트는 이 컴퓨터에 없는 NuGet 패키지를 참조합니다. 해당 패키지를 다운로드하려면 NuGet 패키지 복원을 사용하십시오. 자세한 내용은 http://go.microsoft.com/fwlink/?LinkID=322105를 참조하십시오. 누락된 파일은 {0}입니다.</ErrorText>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Error Condition="!Exists('..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\Microsoft.Web.WebView2.targets'))" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
474
Handler/Project/WebUI/MachineBridge.cs
Normal file
474
Handler/Project/WebUI/MachineBridge.cs
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using AR;
|
||||||
|
|
||||||
|
namespace Project.WebUI
|
||||||
|
{
|
||||||
|
// Important: Allows JavaScript to see this class
|
||||||
|
[ClassInterface(ClassInterfaceType.AutoDual)]
|
||||||
|
[ComVisible(true)]
|
||||||
|
public class MachineBridge
|
||||||
|
{
|
||||||
|
// Reference to the main form to update logic
|
||||||
|
private Project.Dialog.fWebView _host;
|
||||||
|
|
||||||
|
// Data folder paths
|
||||||
|
private readonly string _dataFolder;
|
||||||
|
private readonly string _settingsPath;
|
||||||
|
private readonly string _recipeFolder;
|
||||||
|
|
||||||
|
public MachineBridge(Project.Dialog.fWebView host)
|
||||||
|
{
|
||||||
|
_host = host;
|
||||||
|
|
||||||
|
// Initialize data folder paths - use existing Data folder
|
||||||
|
_dataFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data");
|
||||||
|
_settingsPath = Path.Combine(_dataFolder, "Setting.json");
|
||||||
|
_recipeFolder = Path.Combine(_dataFolder, "recipe");
|
||||||
|
|
||||||
|
// Ensure folders exist
|
||||||
|
EnsureDataFoldersExist();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureDataFoldersExist()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(_dataFolder))
|
||||||
|
Directory.CreateDirectory(_dataFolder);
|
||||||
|
|
||||||
|
if (!Directory.Exists(_recipeFolder))
|
||||||
|
Directory.CreateDirectory(_recipeFolder);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to create data folders: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Methods called from React ---
|
||||||
|
|
||||||
|
public void MoveAxis(string axis, double value)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[C#] Moving {axis} to {value}");
|
||||||
|
_host.SetTargetPosition(axis, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetIO(int id, bool isInput, bool state)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[C#] Set IO {id} to {state}");
|
||||||
|
_host.SetOutput(id, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SystemControl(string command)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[C#] CMD: {command}");
|
||||||
|
_host.HandleCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SelectRecipe(string recipeId)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[C#] Selecting Recipe: {recipeId}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string recipePath = Path.Combine(_recipeFolder, $"{recipeId}.json");
|
||||||
|
|
||||||
|
if (!File.Exists(recipePath))
|
||||||
|
{
|
||||||
|
var response = new { success = false, message = "Recipe not found" };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
string json = File.ReadAllText(recipePath);
|
||||||
|
var recipeData = JsonConvert.DeserializeObject<dynamic>(json);
|
||||||
|
|
||||||
|
_host.SetCurrentRecipe(recipeId);
|
||||||
|
|
||||||
|
Console.WriteLine($"[INFO] Recipe {recipeId} selected successfully");
|
||||||
|
|
||||||
|
var response2 = new { success = true, message = "Recipe selected successfully", recipeId = recipeId, recipe = recipeData };
|
||||||
|
return JsonConvert.SerializeObject(response2);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to select recipe: {ex.Message}");
|
||||||
|
var response = new { success = false, message = ex.Message };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20><><EFBFBD>̱<CCB1><D7B7>̼<EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string GetConfig()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Use SETTING.Data (CommonSetting) from the project
|
||||||
|
if (AR.SETTING.Data == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[WARN] SETTING.Data is not initialized");
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingsArray = new List<object>();
|
||||||
|
var properties = AR.SETTING.Data.GetType().GetProperties();
|
||||||
|
|
||||||
|
foreach (var prop in properties)
|
||||||
|
{
|
||||||
|
// Skip non-browsable properties
|
||||||
|
var browsable = prop.GetCustomAttributes(typeof(System.ComponentModel.BrowsableAttribute), false)
|
||||||
|
.FirstOrDefault() as System.ComponentModel.BrowsableAttribute;
|
||||||
|
if (browsable != null && !browsable.Browsable)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip filename property
|
||||||
|
if (prop.Name == "filename")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Get property info
|
||||||
|
string key = prop.Name;
|
||||||
|
object value = prop.GetValue(AR.SETTING.Data, null);
|
||||||
|
string type = "String";
|
||||||
|
string group = "General";
|
||||||
|
string description = key.Replace("_", " ");
|
||||||
|
|
||||||
|
// Get Category attribute
|
||||||
|
var categoryAttr = prop.GetCustomAttributes(typeof(System.ComponentModel.CategoryAttribute), false)
|
||||||
|
.FirstOrDefault() as System.ComponentModel.CategoryAttribute;
|
||||||
|
if (categoryAttr != null)
|
||||||
|
group = categoryAttr.Category;
|
||||||
|
|
||||||
|
// Get DisplayName attribute
|
||||||
|
var displayNameAttr = prop.GetCustomAttributes(typeof(System.ComponentModel.DisplayNameAttribute), false)
|
||||||
|
.FirstOrDefault() as System.ComponentModel.DisplayNameAttribute;
|
||||||
|
if (displayNameAttr != null)
|
||||||
|
description = displayNameAttr.DisplayName;
|
||||||
|
|
||||||
|
// Get Description attribute
|
||||||
|
var descAttr = prop.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false)
|
||||||
|
.FirstOrDefault() as System.ComponentModel.DescriptionAttribute;
|
||||||
|
if (descAttr != null)
|
||||||
|
description = descAttr.Description;
|
||||||
|
|
||||||
|
// Determine type based on property type
|
||||||
|
if (prop.PropertyType == typeof(bool))
|
||||||
|
{
|
||||||
|
type = "Boolean";
|
||||||
|
value = value?.ToString().ToLower() ?? "false";
|
||||||
|
}
|
||||||
|
else if (prop.PropertyType == typeof(int) || prop.PropertyType == typeof(long) ||
|
||||||
|
prop.PropertyType == typeof(double) || prop.PropertyType == typeof(float))
|
||||||
|
{
|
||||||
|
type = "Number";
|
||||||
|
value = value?.ToString() ?? "0";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = value?.ToString() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsArray.Add(new
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
Value = value,
|
||||||
|
Group = group,
|
||||||
|
Type = type,
|
||||||
|
Description = description
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[INFO] Loaded {settingsArray.Count} settings from SETTING.Data");
|
||||||
|
return JsonConvert.SerializeObject(settingsArray);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to load settings: {ex.Message}");
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <20><><EFBFBD>̱<CCB1><D7B7>̼Ǽ<CCBC><C7BC><EFBFBD>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configJson"></param>
|
||||||
|
public void SaveConfig(string configJson)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Backend] SAVE CONFIG REQUEST RECEIVED");
|
||||||
|
|
||||||
|
// Parse array format from React
|
||||||
|
var settingsArray = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(configJson);
|
||||||
|
|
||||||
|
// Update SETTING.Data properties
|
||||||
|
var properties = AR.SETTING.Data.GetType().GetProperties();
|
||||||
|
|
||||||
|
foreach (var item in settingsArray)
|
||||||
|
{
|
||||||
|
string key = item["Key"].ToString();
|
||||||
|
string value = item["Value"].ToString();
|
||||||
|
string type = item["Type"].ToString();
|
||||||
|
|
||||||
|
var prop = properties.FirstOrDefault(p => p.Name == key);
|
||||||
|
if (prop == null || !prop.CanWrite)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Convert value based on type
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (type == "Boolean")
|
||||||
|
{
|
||||||
|
prop.SetValue(AR.SETTING.Data, bool.Parse(value), null);
|
||||||
|
}
|
||||||
|
else if (type == "Number")
|
||||||
|
{
|
||||||
|
if (prop.PropertyType == typeof(int))
|
||||||
|
prop.SetValue(AR.SETTING.Data, int.Parse(value), null);
|
||||||
|
else if (prop.PropertyType == typeof(long))
|
||||||
|
prop.SetValue(AR.SETTING.Data, long.Parse(value), null);
|
||||||
|
else if (prop.PropertyType == typeof(double))
|
||||||
|
prop.SetValue(AR.SETTING.Data, double.Parse(value), null);
|
||||||
|
else if (prop.PropertyType == typeof(float))
|
||||||
|
prop.SetValue(AR.SETTING.Data, float.Parse(value), null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prop.SetValue(AR.SETTING.Data, value, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[WARN] Failed to set property {key}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
AR.SETTING.Data.Save();
|
||||||
|
|
||||||
|
Console.WriteLine($"[INFO] Settings saved successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string GetIOList()
|
||||||
|
{
|
||||||
|
var ioList = new List<object>();
|
||||||
|
|
||||||
|
// Outputs (0-31)
|
||||||
|
for (int i = 0; i < 32; i++)
|
||||||
|
{
|
||||||
|
string name = $"DOUT_{i:D2}";
|
||||||
|
if (i == 0) name = "Tower Lamp Red";
|
||||||
|
if (i == 1) name = "Tower Lamp Yel";
|
||||||
|
if (i == 2) name = "Tower Lamp Grn";
|
||||||
|
|
||||||
|
ioList.Add(new { id = i, name = name, type = "output", state = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inputs (0-31)
|
||||||
|
for (int i = 0; i < 32; i++)
|
||||||
|
{
|
||||||
|
string name = $"DIN_{i:D2}";
|
||||||
|
bool initialState = false;
|
||||||
|
if (i == 0) name = "Front Door Sensor";
|
||||||
|
if (i == 1) name = "Right Door Sensor";
|
||||||
|
if (i == 2) name = "Left Door Sensor";
|
||||||
|
if (i == 3) name = "Back Door Sensor";
|
||||||
|
if (i == 4) { name = "Main Air Pressure"; initialState = true; }
|
||||||
|
if (i == 5) { name = "Vacuum Generator"; initialState = true; }
|
||||||
|
if (i == 6) { name = "Emergency Stop Loop"; initialState = true; }
|
||||||
|
|
||||||
|
ioList.Add(new { id = i, name = name, type = "input", state = initialState });
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonConvert.SerializeObject(ioList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetRecipeList()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var recipes = new List<object>();
|
||||||
|
|
||||||
|
if (!Directory.Exists(_recipeFolder))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_recipeFolder);
|
||||||
|
return JsonConvert.SerializeObject(recipes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonFiles = Directory.GetFiles(_recipeFolder, "*.json");
|
||||||
|
|
||||||
|
foreach (var filePath in jsonFiles)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
string json = File.ReadAllText(filePath);
|
||||||
|
var recipeData = JsonConvert.DeserializeObject<dynamic>(json);
|
||||||
|
|
||||||
|
var lastModified = File.GetLastWriteTime(filePath).ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
|
recipes.Add(new
|
||||||
|
{
|
||||||
|
id = fileName,
|
||||||
|
name = recipeData?.name ?? fileName,
|
||||||
|
lastModified = lastModified
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to read recipe {filePath}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[INFO] Loaded {recipes.Count} recipes from {_recipeFolder}");
|
||||||
|
return JsonConvert.SerializeObject(recipes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to get recipe list: {ex.Message}");
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetRecipe(string recipeId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string recipePath = Path.Combine(_recipeFolder, $"{recipeId}.json");
|
||||||
|
|
||||||
|
if (!File.Exists(recipePath))
|
||||||
|
{
|
||||||
|
var response = new { success = false, message = "Recipe not found" };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
string json = File.ReadAllText(recipePath);
|
||||||
|
Console.WriteLine($"[INFO] Loaded recipe {recipeId}");
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to get recipe {recipeId}: {ex.Message}");
|
||||||
|
var response = new { success = false, message = ex.Message };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SaveRecipe(string recipeId, string recipeData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string recipePath = Path.Combine(_recipeFolder, $"{recipeId}.json");
|
||||||
|
|
||||||
|
var recipe = JsonConvert.DeserializeObject(recipeData);
|
||||||
|
File.WriteAllText(recipePath, JsonConvert.SerializeObject(recipe, Formatting.Indented));
|
||||||
|
|
||||||
|
Console.WriteLine($"[INFO] Recipe {recipeId} saved successfully to {recipePath}");
|
||||||
|
|
||||||
|
var response = new { success = true, message = "Recipe saved successfully", recipeId = recipeId };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to save recipe {recipeId}: {ex.Message}");
|
||||||
|
var response = new { success = false, message = ex.Message };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CopyRecipe(string recipeId, string newName)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[C#] Copying Recipe: {recipeId} as {newName}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string sourcePath = Path.Combine(_recipeFolder, $"{recipeId}.json");
|
||||||
|
|
||||||
|
if (!File.Exists(sourcePath))
|
||||||
|
{
|
||||||
|
var response = new { success = false, message = "Source recipe not found" };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
string newId = Guid.NewGuid().ToString().Substring(0, 8);
|
||||||
|
string destPath = Path.Combine(_recipeFolder, $"{newId}.json");
|
||||||
|
|
||||||
|
string json = File.ReadAllText(sourcePath);
|
||||||
|
var recipeData = JsonConvert.DeserializeObject<dynamic>(json);
|
||||||
|
|
||||||
|
recipeData.name = newName;
|
||||||
|
|
||||||
|
File.WriteAllText(destPath, JsonConvert.SerializeObject(recipeData, Formatting.Indented));
|
||||||
|
|
||||||
|
string timestamp = DateTime.Now.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
|
Console.WriteLine($"[INFO] Recipe copied from {recipeId} to {newId}");
|
||||||
|
|
||||||
|
var response2 = new {
|
||||||
|
success = true,
|
||||||
|
message = "Recipe copied successfully",
|
||||||
|
newRecipe = new {
|
||||||
|
id = newId,
|
||||||
|
name = newName,
|
||||||
|
lastModified = timestamp
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return JsonConvert.SerializeObject(response2);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to copy recipe: {ex.Message}");
|
||||||
|
var response = new { success = false, message = ex.Message };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DeleteRecipe(string recipeId)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[C#] Deleting Recipe: {recipeId}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (recipeId == _host.GetCurrentRecipe())
|
||||||
|
{
|
||||||
|
var response1 = new { success = false, message = "Cannot delete currently selected recipe" };
|
||||||
|
return JsonConvert.SerializeObject(response1);
|
||||||
|
}
|
||||||
|
|
||||||
|
string recipePath = Path.Combine(_recipeFolder, $"{recipeId}.json");
|
||||||
|
|
||||||
|
if (!File.Exists(recipePath))
|
||||||
|
{
|
||||||
|
var response = new { success = false, message = "Recipe not found" };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Delete(recipePath);
|
||||||
|
|
||||||
|
Console.WriteLine($"[INFO] Recipe {recipeId} deleted successfully");
|
||||||
|
|
||||||
|
var response2 = new { success = true, message = "Recipe deleted successfully", recipeId = recipeId };
|
||||||
|
return JsonConvert.SerializeObject(response2);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ERROR] Failed to delete recipe: {ex.Message}");
|
||||||
|
var response = new { success = false, message = ex.Message };
|
||||||
|
return JsonConvert.SerializeObject(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
263
Handler/Project/WebUI/WebSocketServer.cs
Normal file
263
Handler/Project/WebUI/WebSocketServer.cs
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Project.Dialog;
|
||||||
|
|
||||||
|
namespace Project.WebUI
|
||||||
|
{
|
||||||
|
public class WebSocketServer
|
||||||
|
{
|
||||||
|
private HttpListener _httpListener;
|
||||||
|
private List<WebSocket> _clients = new List<WebSocket>();
|
||||||
|
private fWebView _mainForm;
|
||||||
|
|
||||||
|
public WebSocketServer(string url, fWebView form)
|
||||||
|
{
|
||||||
|
_mainForm = form;
|
||||||
|
_httpListener = new HttpListener();
|
||||||
|
_httpListener.Prefixes.Add(url);
|
||||||
|
_httpListener.Start();
|
||||||
|
Console.WriteLine($"[WS] Listening on {url}");
|
||||||
|
Task.Run(AcceptConnections);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AcceptConnections()
|
||||||
|
{
|
||||||
|
while (_httpListener.IsListening)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = await _httpListener.GetContextAsync();
|
||||||
|
if (context.Request.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
ProcessRequest(context);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 400;
|
||||||
|
context.Response.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[WS] Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private System.Collections.Concurrent.ConcurrentDictionary<WebSocket, SemaphoreSlim> _socketLocks = new System.Collections.Concurrent.ConcurrentDictionary<WebSocket, SemaphoreSlim>();
|
||||||
|
|
||||||
|
private async void ProcessRequest(HttpListenerContext context)
|
||||||
|
{
|
||||||
|
WebSocketContext wsContext = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
wsContext = await context.AcceptWebSocketAsync(subProtocol: null);
|
||||||
|
WebSocket socket = wsContext.WebSocket;
|
||||||
|
_socketLocks.TryAdd(socket, new SemaphoreSlim(1, 1));
|
||||||
|
|
||||||
|
lock (_clients) { _clients.Add(socket); }
|
||||||
|
Console.WriteLine("[WS] Client Connected");
|
||||||
|
|
||||||
|
await ReceiveLoop(socket);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[WS] Accept Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (wsContext != null)
|
||||||
|
{
|
||||||
|
WebSocket socket = wsContext.WebSocket;
|
||||||
|
lock (_clients) { _clients.Remove(socket); }
|
||||||
|
|
||||||
|
if (_socketLocks.TryRemove(socket, out var semaphore))
|
||||||
|
{
|
||||||
|
semaphore.Dispose();
|
||||||
|
}
|
||||||
|
socket.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReceiveLoop(WebSocket socket)
|
||||||
|
{
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
while (socket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
|
||||||
|
}
|
||||||
|
else if (result.MessageType == WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
string msg = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||||
|
HandleMessage(msg, socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleMessage(string msg, WebSocket socket)
|
||||||
|
{
|
||||||
|
// Simple JSON parsing (manual or Newtonsoft)
|
||||||
|
// Expected format: { "type": "...", "data": ... }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dynamic json = Newtonsoft.Json.JsonConvert.DeserializeObject(msg);
|
||||||
|
string type = json.type;
|
||||||
|
|
||||||
|
Console.WriteLine($"HandleMessage:{type}");
|
||||||
|
if (type == "GET_CONFIG")
|
||||||
|
{
|
||||||
|
// Simulate Delay for Loading Screen Test
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
// Send Config back
|
||||||
|
var bridge = new MachineBridge(_mainForm); // Re-use logic
|
||||||
|
string configJson = bridge.GetConfig();
|
||||||
|
var response = new { type = "CONFIG_DATA", data = Newtonsoft.Json.JsonConvert.DeserializeObject(configJson) };
|
||||||
|
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
else if (type == "GET_IO_LIST")
|
||||||
|
{
|
||||||
|
var bridge = new MachineBridge(_mainForm);
|
||||||
|
string ioJson = bridge.GetIOList();
|
||||||
|
var response = new { type = "IO_LIST_DATA", data = Newtonsoft.Json.JsonConvert.DeserializeObject(ioJson) };
|
||||||
|
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
else if (type == "GET_RECIPE_LIST")
|
||||||
|
{
|
||||||
|
var bridge = new MachineBridge(_mainForm);
|
||||||
|
string recipeJson = bridge.GetRecipeList();
|
||||||
|
var response = new { type = "RECIPE_LIST_DATA", data = Newtonsoft.Json.JsonConvert.DeserializeObject(recipeJson) };
|
||||||
|
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
else if (type == "SAVE_CONFIG")
|
||||||
|
{
|
||||||
|
string configJson = Newtonsoft.Json.JsonConvert.SerializeObject(json.data);
|
||||||
|
var bridge = new MachineBridge(_mainForm);
|
||||||
|
bridge.SaveConfig(configJson);
|
||||||
|
}
|
||||||
|
else if (type == "CONTROL")
|
||||||
|
{
|
||||||
|
string cmd = json.command;
|
||||||
|
_mainForm.Invoke(new Action(() => _mainForm.HandleCommand(cmd)));
|
||||||
|
}
|
||||||
|
else if (type == "MOVE")
|
||||||
|
{
|
||||||
|
string axis = json.axis;
|
||||||
|
double val = json.value;
|
||||||
|
_mainForm.Invoke(new Action(() => _mainForm.SetTargetPosition(axis, val)));
|
||||||
|
}
|
||||||
|
else if (type == "SET_IO")
|
||||||
|
{
|
||||||
|
int id = json.id;
|
||||||
|
bool state = json.state;
|
||||||
|
_mainForm.Invoke(new Action(() => _mainForm.SetOutput(id, state)));
|
||||||
|
}
|
||||||
|
else if (type == "SELECT_RECIPE")
|
||||||
|
{
|
||||||
|
string recipeId = json.recipeId;
|
||||||
|
var bridge = new MachineBridge(_mainForm);
|
||||||
|
string resultJson = bridge.SelectRecipe(recipeId);
|
||||||
|
var response = new { type = "RECIPE_SELECTED", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
|
||||||
|
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
else if (type == "COPY_RECIPE")
|
||||||
|
{
|
||||||
|
string recipeId = json.recipeId;
|
||||||
|
string newName = json.newName;
|
||||||
|
var bridge = new MachineBridge(_mainForm);
|
||||||
|
string resultJson = bridge.CopyRecipe(recipeId, newName);
|
||||||
|
var response = new { type = "RECIPE_COPIED", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
|
||||||
|
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
else if (type == "DELETE_RECIPE")
|
||||||
|
{
|
||||||
|
string recipeId = json.recipeId;
|
||||||
|
var bridge = new MachineBridge(_mainForm);
|
||||||
|
string resultJson = bridge.DeleteRecipe(recipeId);
|
||||||
|
var response = new { type = "RECIPE_DELETED", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) };
|
||||||
|
await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[WS] Msg Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Send(WebSocket socket, string message)
|
||||||
|
{
|
||||||
|
if (_socketLocks.TryGetValue(socket, out var semaphore))
|
||||||
|
{
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (socket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
byte[] buffer = Encoding.UTF8.GetBytes(message);
|
||||||
|
await socket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Broadcast(string message)
|
||||||
|
{
|
||||||
|
byte[] buffer = Encoding.UTF8.GetBytes(message);
|
||||||
|
WebSocket[] clientsCopy;
|
||||||
|
|
||||||
|
lock (_clients)
|
||||||
|
{
|
||||||
|
clientsCopy = _clients.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var client in clientsCopy)
|
||||||
|
{
|
||||||
|
if (client.State == WebSocketState.Open && _socketLocks.TryGetValue(client, out var semaphore))
|
||||||
|
{
|
||||||
|
// Fire and forget, but safely
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
// Try to get lock immediately. If busy (sending previous frame), skip this frame to prevent lag.
|
||||||
|
if (await semaphore.WaitAsync(0))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (client.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
await client.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* Ignore send errors */ }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||||
<section name="Project.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
<section name="Project.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||||
</sectionGroup>
|
</sectionGroup>
|
||||||
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
|
|
||||||
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
|
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
|
||||||
</configSections>
|
</configSections>
|
||||||
<connectionStrings>
|
<connectionStrings>
|
||||||
|
|||||||
@@ -486,8 +486,12 @@ namespace Project
|
|||||||
thConnection.IsBackground = true;
|
thConnection.IsBackground = true;
|
||||||
thConnection.Start();
|
thConnection.Start();
|
||||||
|
|
||||||
|
|
||||||
|
fwebview = new fWebView();
|
||||||
|
fwebview.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dialog.fWebView fwebview = null;
|
||||||
|
|
||||||
private void Plc_ValueChanged(object sender, AR.MemoryMap.Core.monitorvalueargs e)
|
private void Plc_ValueChanged(object sender, AR.MemoryMap.Core.monitorvalueargs e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
<package id="chilkat-x64" version="9.5.0.77" targetFramework="net47" requireReinstallation="true" />
|
<package id="chilkat-x64" version="9.5.0.77" targetFramework="net47" requireReinstallation="true" />
|
||||||
<package id="Emgu.CV" version="4.5.1.4349" targetFramework="net48" />
|
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="net48" />
|
||||||
<package id="EntityFramework" version="6.2.0" targetFramework="net47" />
|
|
||||||
<package id="EntityFramework.ko" version="6.2.0" targetFramework="net47" />
|
|
||||||
<package id="HidSharp" version="2.1.0" targetFramework="net47" />
|
|
||||||
<package id="Microsoft.AspNet.Cors" version="5.2.9" targetFramework="net48" />
|
|
||||||
<package id="Microsoft.AspNet.WebApi" version="5.2.9" targetFramework="net48" />
|
|
||||||
<package id="Microsoft.AspNet.WebApi.Client" version="5.2.9" targetFramework="net48" />
|
|
||||||
<package id="Microsoft.AspNet.WebApi.Core" version="5.2.9" targetFramework="net48" />
|
|
||||||
<package id="Microsoft.AspNet.WebApi.WebHost" version="5.2.9" targetFramework="net48" />
|
|
||||||
<package id="Microsoft.Web.WebView2" version="1.0.2849.39" targetFramework="net48" />
|
|
||||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||||
<package id="System.Drawing.Common" version="7.0.0" targetFramework="net48" />
|
<package id="System.Drawing.Common" version="7.0.0" targetFramework="net48" />
|
||||||
<package id="System.Drawing.Primitives" version="4.3.0" targetFramework="net47" />
|
<package id="System.Drawing.Primitives" version="4.3.0" targetFramework="net47" />
|
||||||
|
|||||||
Reference in New Issue
Block a user