From c625947b2eb36df1bf6ecf2f49aa8dc485c21fa7 Mon Sep 17 00:00:00 2001 From: arDTDev Date: Tue, 25 Nov 2025 20:40:46 +0900 Subject: [PATCH] feat: Add WebView2 backend integration for React UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Handler/Project/Dialog/fWebView.cs | 259 +++++------ Handler/Project/STDLabelAttach(ATV).csproj | 27 +- Handler/Project/WebUI/MachineBridge.cs | 474 +++++++++++++++++++++ Handler/Project/WebUI/WebSocketServer.cs | 263 ++++++++++++ Handler/Project/app.config | 2 +- Handler/Project/fMain.cs | 4 + Handler/Project/packages.config | 11 +- 7 files changed, 896 insertions(+), 144 deletions(-) create mode 100644 Handler/Project/WebUI/MachineBridge.cs create mode 100644 Handler/Project/WebUI/WebSocketServer.cs diff --git a/Handler/Project/Dialog/fWebView.cs b/Handler/Project/Dialog/fWebView.cs index 953a988..4ea96df 100644 --- a/Handler/Project/Dialog/fWebView.cs +++ b/Handler/Project/Dialog/fWebView.cs @@ -1,23 +1,49 @@ 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; namespace Project.Dialog { public partial class fWebView : Form { - private WebView2 webView2; - private TextBox txtUrl; - private Button btnGo; - private Button btnBack; - private Button btnForward; - private Button btnRefresh; + private WebView2 webView; + private Timer plcTimer; + private WebSocketServer _wsServer; + + // Machine State (Simulated PLC Memory) + 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() { 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); + } + + // Set default inputs (Pressure OK, Estop OK) + inputs[4] = true; + inputs[6] = true; + + // Load event handler + this.Load += FWebView_Load; } private void InitializeComponent() @@ -26,71 +52,52 @@ namespace Project.Dialog // Form this.ClientSize = new System.Drawing.Size(1200, 800); - this.Text = "WebView2 Browser"; + this.Text = "STD Label Attach - Web UI"; this.Name = "fWebView"; - - // 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.StartPosition = FormStartPosition.CenterScreen; 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 webView2.EnsureCoreWebView2Async(null); + await webView.EnsureCoreWebView2Async(); - // ๋„ค๋น„๊ฒŒ์ด์…˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ - webView2.CoreWebView2.NavigationCompleted += CoreWebView2_NavigationCompleted; - webView2.CoreWebView2.SourceChanged += CoreWebView2_SourceChanged; + 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); + } - // ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ๋กœ๋“œ - webView2.CoreWebView2.Navigate("http://localhost:3000"); - txtUrl.Text = "http://localhost:3000"; + 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) { @@ -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) - { - 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) + // 2. Prepare Data Packet + var payload = new { - NavigateToUrl(); - e.Handled = true; - e.SuppressKeyPress = true; - } - } + type = "STATUS_UPDATE", + sysState = systemState, + position = new { x = currX, y = currY, z = currZ }, + ioState = GetChangedIOs() // Function to return array of IO states + }; - private void NavigateToUrl() - { - string url = txtUrl.Text; - if (!url.StartsWith("http://") && !url.StartsWith("https://")) + string json = JsonConvert.SerializeObject(payload); + + // 3. Send to React via PostMessage (WebView2) + if (webView != null && webView.CoreWebView2 != null) { - url = "http://" + url; + webView.CoreWebView2.PostWebMessageAsJson(json); } - try - { - webView2.CoreWebView2.Navigate(url); - } - catch (Exception ex) - { - MessageBox.Show($"ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹คํŒจ: {ex.Message}", "Error", - MessageBoxButtons.OK, MessageBoxIcon.Error); - } + // 4. Broadcast to WebSocket (Dev/HMR) + _wsServer?.Broadcast(json); } - private void BtnBack_Click(object sender, EventArgs e) + private List GetChangedIOs() { - if (webView2.CanGoBack) + // Simply return list of all active IOs or just send all for simplicity + var list = new List(); + 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) - { - webView2.GoForward(); - } + // Setup Simulation Timer (50ms) + plcTimer = new Timer(); + 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) { try { - string result = await webView2.CoreWebView2.ExecuteScriptAsync(script); - MessageBox.Show($"์‹คํ–‰ ๊ฒฐ๊ณผ: {result}"); + string result = await webView.CoreWebView2.ExecuteScriptAsync(script); + Console.WriteLine($"Script result: {result}"); } catch (Exception ex) { - MessageBox.Show($"์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ์‹คํŒจ: {ex.Message}", "Error", - MessageBoxButtons.OK, MessageBoxIcon.Error); + Console.WriteLine($"Script execution failed: {ex.Message}"); } } @@ -185,23 +207,12 @@ namespace Project.Dialog { try { - webView2.CoreWebView2.PostWebMessageAsString(message); + webView.CoreWebView2.PostWebMessageAsString(message); } catch (Exception ex) { - MessageBox.Show($"๋ฉ”์‹œ์ง€ ์ „์†ก ์‹คํŒจ: {ex.Message}", "Error", - MessageBoxButtons.OK, MessageBoxIcon.Error); + Console.WriteLine($"PostMessage failed: {ex.Message}"); } } - - // JavaScript์—์„œ C#์œผ๋กœ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  - private void SetupWebMessageReceived() - { - webView2.CoreWebView2.WebMessageReceived += (sender, e) => - { - string message = e.TryGetWebMessageAsString(); - MessageBox.Show($"์›น์—์„œ ๋ฐ›์€ ๋ฉ”์‹œ์ง€: {message}"); - }; - } } } diff --git a/Handler/Project/STDLabelAttach(ATV).csproj b/Handler/Project/STDLabelAttach(ATV).csproj index 351bc4c..0478732 100644 --- a/Handler/Project/STDLabelAttach(ATV).csproj +++ b/Handler/Project/STDLabelAttach(ATV).csproj @@ -118,6 +118,15 @@ False ..\DLL\libxl\libxl.net.dll + + ..\packages\Microsoft.Web.WebView2.1.0.2903.40\lib\net462\Microsoft.Web.WebView2.Core.dll + + + ..\packages\Microsoft.Web.WebView2.1.0.2903.40\lib\net462\Microsoft.Web.WebView2.WinForms.dll + + + ..\packages\Microsoft.Web.WebView2.1.0.2903.40\lib\net462\Microsoft.Web.WebView2.Wpf.dll + ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll @@ -142,15 +151,6 @@ - - ..\packages\Microsoft.Web.WebView2.1.0.2849.39\lib\net45\Microsoft.Web.WebView2.Core.dll - - - ..\packages\Microsoft.Web.WebView2.1.0.2849.39\lib\net45\Microsoft.Web.WebView2.WinForms.dll - - - ..\packages\Microsoft.Web.WebView2.1.0.2849.39\lib\net45\Microsoft.Web.WebView2.Wpf.dll - @@ -318,6 +318,8 @@ Form + + Form @@ -992,4 +994,11 @@ + + + + ์ด ํ”„๋กœ์ ํŠธ๋Š” ์ด ์ปดํ“จํ„ฐ์— ์—†๋Š” NuGet ํŒจํ‚ค์ง€๋ฅผ ์ฐธ์กฐํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ํŒจํ‚ค์ง€๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๋ ค๋ฉด NuGet ํŒจํ‚ค์ง€ ๋ณต์›์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ http://go.microsoft.com/fwlink/?LinkID=322105๋ฅผ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค. ๋ˆ„๋ฝ๋œ ํŒŒ์ผ์€ {0}์ž…๋‹ˆ๋‹ค. + + + \ No newline at end of file diff --git a/Handler/Project/WebUI/MachineBridge.cs b/Handler/Project/WebUI/MachineBridge.cs new file mode 100644 index 0000000..6bf4fc9 --- /dev/null +++ b/Handler/Project/WebUI/MachineBridge.cs @@ -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(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); + } + } + + /// + /// ธถภฬฑืทนภฬผว ผบฐ๘ + /// + /// + 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(); + 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 "[]"; + } + } + + /// + /// ธถภฬฑืทนภฬผวผบฐ๘ + /// + /// + public void SaveConfig(string configJson) + { + try + { + Console.WriteLine($"[Backend] SAVE CONFIG REQUEST RECEIVED"); + + // Parse array format from React + var settingsArray = JsonConvert.DeserializeObject>>(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(); + + // 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(); + + 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(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(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); + } + } + } +} diff --git a/Handler/Project/WebUI/WebSocketServer.cs b/Handler/Project/WebUI/WebSocketServer.cs new file mode 100644 index 0000000..765013e --- /dev/null +++ b/Handler/Project/WebUI/WebSocketServer.cs @@ -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 _clients = new List(); + 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 _socketLocks = new System.Collections.Concurrent.ConcurrentDictionary(); + + 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(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(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(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } + } + catch { /* Ignore send errors */ } + finally + { + semaphore.Release(); + } + } + }); + } + } + } + } +} diff --git a/Handler/Project/app.config b/Handler/Project/app.config index 81c50fe..453537d 100644 --- a/Handler/Project/app.config +++ b/Handler/Project/app.config @@ -7,7 +7,7 @@
- +
diff --git a/Handler/Project/fMain.cs b/Handler/Project/fMain.cs index 3f56d9e..01d34b5 100644 --- a/Handler/Project/fMain.cs +++ b/Handler/Project/fMain.cs @@ -486,8 +486,12 @@ namespace Project thConnection.IsBackground = true; thConnection.Start(); + + fwebview = new fWebView(); + fwebview.Show(); } + Dialog.fWebView fwebview = null; private void Plc_ValueChanged(object sender, AR.MemoryMap.Core.monitorvalueargs e) { diff --git a/Handler/Project/packages.config b/Handler/Project/packages.config index 4a120cc..3422259 100644 --- a/Handler/Project/packages.config +++ b/Handler/Project/packages.config @@ -1,16 +1,7 @@ ๏ปฟ - - - - - - - - - - +