using Microsoft.Web.WebView2.Core; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace HMIWeb { public partial class MainForm : Form { private Microsoft.Web.WebView2.WinForms.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 MainForm() { 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; } 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 = $"HMI Host - {wwwroot}"; webView = new Microsoft.Web.WebView2.WinForms.WebView2(); webView.Dock = DockStyle.Fill; this.Controls.Add(webView); await webView.EnsureCoreWebView2Async(); 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); } 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; } // --- Logic Loop --- private void PlcTimer_Tick(object sender, EventArgs e) { // 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); // 2. 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 }; string json = JsonConvert.SerializeObject(payload); // 3. Send to React via PostMessage (WebView2) if (webView != null && webView.CoreWebView2 != null) { webView.CoreWebView2.PostWebMessageAsJson(json); } // 4. Broadcast to WebSocket (Dev/HMR) _wsServer?.Broadcast(json); } private List GetChangedIOs() { // Simply return list of all active IOs or just send all for simplicity var list = new List(); for (int i = 0; i < 32; i++) { list.Add(new { id = i, type = "input", state = inputs[i] }); list.Add(new { id = i, type = "output", state = outputs[i] }); } return list; } private void MainForm_Load(object sender, EventArgs e) { // Setup Simulation Timer (50ms) plcTimer = new Timer(); plcTimer.Interval = 50; plcTimer.Tick += PlcTimer_Tick; plcTimer.Start(); } // --- Helper Methods called by Bridge --- public void SetTargetPosition(string axis, double val) { if (axis == "X") targetX = val; if (axis == "Y") targetY = val; if (axis == "Z") targetZ = val; } 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($"[MainForm] 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; } }