Backend changes (C#): - Add SelectRecipe method to MachineBridge for recipe selection - Add currentRecipeId tracking in MainForm - Implement SELECT_RECIPE handler in WebSocketServer Frontend changes (React/TypeScript): - Add selectRecipe method to communication layer - Update handleSelectRecipe to call backend and handle response - Recipe selection updates ModelInfoPanel automatically - Add error handling and logging for recipe operations Layout improvements: - Add Layout component with persistent Header and Footer - Create separate IOMonitorPage for full-screen I/O monitoring - Add dynamic IO tab switcher in Header (Inputs/Outputs) - Ensure consistent UI across all pages 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
172 lines
5.8 KiB
C#
172 lines
5.8 KiB
C#
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<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++)
|
|
{
|
|
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;
|
|
}
|
|
}
|