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:
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user