using Hawkeye.VisionBuilder.Workflow.Configuration; using Hawkeye.VisionBuilder.Workflow.Links; using OpenCvSharp; using Serilog; using System.ComponentModel.Design.Serialization; using System.Diagnostics; using System.Drawing; using System.Reflection.PortableExecutable; using System.Text.Json; using System.Xml; using VisionBuilder.UI.Common.Processing; using Size = OpenCvSharp.Size; namespace Hawkeye.VisionBuilder.Workflow; public class WorkflowList { public IImageSource ImageSource { get; set; } public Mat? RecipeImage { get; set; } public WorkflowConfiguration Configuration { get; set; } = new WorkflowConfiguration(); public List Operations { get; set; } = new List(); public Context Context { get; set; } = new Context(); private int _executionCounter = 0; public void Execute() { Context.GraphicsElements.Clear(); UpdateConfigVariables(); long total = 0; foreach (BaseOperation operation in Operations) { if (!operation.Enabled) { operation.Result=true; continue; } if (Context.CancellationToken.IsCancellationRequested) break; operation.Interpret(Context); } _executionCounter++; if (_executionCounter % 10 == 0) GC.Collect(); } public BaseOperation GetById(Guid originId) { var operation = Operations.FirstOrDefault(x => x.Id == originId); if(operation==null)return NoOrigin.Instance; return operation as BaseOperation; } public IHaveOrigin GetOriginById(Guid originId) { return GetById(originId) as IHaveOrigin ?? NoOrigin.Instance; } public void ExecuteTill(BaseOperation op) { Context.GraphicsElements.Clear(); UpdateConfigVariables(); foreach (BaseOperation operation in Operations) { if(!operation.Enabled)continue; if(operation == op) break; operation.Interpret(Context); } } public void ExecuteOne(BaseOperation operation) { Context.GraphicsElements.Clear(); UpdateConfigVariables(); try { operation.Interpret(Context); } catch (Exception e) { operation.SetError(e.Message); } } void UpdateConfigVariables() { BaseOperation.SetScriptValue("config", Configuration); } public void Save(BinaryWriter bw) { Configuration.Save(bw); bw.Write(Operations.Count); foreach (BaseOperation operation in Operations) { bw.Write(operation.GetType().AssemblyQualifiedName); operation.Save(bw); } if (RecipeImage == null) { var mat = new Mat(128, 128, MatType.CV_8UC3, Scalar.Gray); var image = mat.ToBytes(); bw.Write(image.Length); bw.Write(image); } else { var mat = RecipeImage; var image = mat.Resize(new Size(128, 128)).ToBytes(); bw.Write(image.Length); bw.Write(image); } } public event Action PythonMissing=delegate { }; public void Load(BinaryReader br, OperationDiscoveryService operationDiscoveryService) { Configuration.Load(br); if (!File.Exists(Configuration.PythonPath)) PythonMissing(); int count = br.ReadInt32(); Operations.Clear(); for (int i = 0; i < count; i++) { string operationType = br.ReadString(); var type=Type.GetType(operationType); BaseOperation operation = operationDiscoveryService.CreateInstance(type); operation.Load(br); Operations.Add(operation); } int length = br.ReadInt32(); var image = br.ReadBytes(length); RecipeImage = Mat.FromImageData(image); } public static WorkflowList LoadFromFile(string fileName) { using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) using (var br = new BinaryReader(fs)) { var workflowList = new WorkflowList(); workflowList.Load(br, new OperationDiscoveryService(workflowList)); return workflowList; } } public void SaveXML(XmlWriter writer) { writer.WriteStartElement("Workflow"); // Save Configuration writer.WriteStartElement("Configuration"); writer.WriteAttributeString("RuntimeCameraType", Configuration.RuntimeCameraType.ToString()); writer.WriteAttributeString("DevelopmentCameraType", Configuration.DevelopmentCameraType.ToString()); writer.WriteAttributeString("Outputs", Configuration.Outputs.ToString()); writer.WriteAttributeString("EmulationPath", Configuration.EmulationPath); writer.WriteAttributeString("PythonPath", Configuration.PythonPath); writer.WriteAttributeString("ResultPin", Configuration.ResultPin.ToString()); writer.WriteAttributeString("SerialPort", Configuration.SerialPort); writer.WriteAttributeString("Delay", Configuration.Delay.ToString()); writer.WriteEndElement(); // Configuration // Save Operations writer.WriteStartElement("Operations"); writer.WriteAttributeString("Count", Operations.Count.ToString()); foreach (BaseOperation operation in Operations) { writer.WriteStartElement("Operation"); writer.WriteAttributeString("Type", operation.GetType().AssemblyQualifiedName); operation.SaveXML(writer); writer.WriteEndElement(); // Operation } writer.WriteEndElement(); // Operations // Save Recipe Image writer.WriteStartElement("RecipeImage"); if (RecipeImage == null) { var mat = new Mat(128, 128, MatType.CV_8UC3, Scalar.Gray); var image = mat.ToBytes(); writer.WriteAttributeString("Length", image.Length.ToString()); writer.WriteBase64(image, 0, image.Length); } else { var mat = RecipeImage; var image = mat.Resize(new Size(128, 128)).ToBytes(); writer.WriteAttributeString("Length", image.Length.ToString()); writer.WriteBase64(image, 0, image.Length); } writer.WriteEndElement(); // RecipeImage writer.WriteEndElement(); // Workflow } public void LoadXML(XmlReader reader, OperationDiscoveryService operationDiscoveryService) { while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element) { switch (reader.Name) { case "Configuration": LoadConfigurationXML(reader); break; case "Operations": LoadOperationsXML(reader, operationDiscoveryService); break; case "RecipeImage": LoadRecipeImageXML(reader); break; } } } } private void LoadConfigurationXML(XmlReader reader) { if (reader.GetAttribute("RuntimeCameraType") != null) Configuration.RuntimeCameraType = Enum.Parse(reader.GetAttribute("RuntimeCameraType")); if (reader.GetAttribute("DevelopmentCameraType") != null) Configuration.DevelopmentCameraType = Enum.Parse(reader.GetAttribute("DevelopmentCameraType")); if (reader.GetAttribute("Outputs") != null) Configuration.Outputs = Enum.Parse(reader.GetAttribute("Outputs")); if (reader.GetAttribute("EmulationPath") != null) Configuration.EmulationPath = reader.GetAttribute("EmulationPath"); if (reader.GetAttribute("PythonPath") != null) Configuration.PythonPath = reader.GetAttribute("PythonPath"); if (reader.GetAttribute("ResultPin") != null) Configuration.ResultPin = int.Parse(reader.GetAttribute("ResultPin")); if (reader.GetAttribute("SerialPort") != null) Configuration.SerialPort = reader.GetAttribute("SerialPort"); if (reader.GetAttribute("Delay") != null) Configuration.Delay = int.Parse(reader.GetAttribute("Delay")); } private void LoadOperationsXML(XmlReader reader, OperationDiscoveryService operationDiscoveryService) { Operations.Clear(); while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element && reader.Name == "Operation") { string operationType = reader.GetAttribute("Type"); var type = Type.GetType(operationType); if (type != null) { BaseOperation operation = operationDiscoveryService.CreateInstance(type); // go to child elements of Operation operation.LoadXML(reader); Operations.Add(operation); } } else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "Operations") { break; } } } private void LoadRecipeImageXML(XmlReader reader) { if (reader.GetAttribute("Length") != null) { int length = int.Parse(reader.GetAttribute("Length")); byte[] imageData = new byte[length]; int bytesRead = reader.ReadElementContentAsBase64(imageData, 0, length); if (bytesRead > 0) { RecipeImage = Mat.FromImageData(imageData); } } } private Dictionary ConvertJsonElementsToNatives(Dictionary dict) { var result = new Dictionary(); foreach (var kvp in dict) { result[kvp.Key] = ConvertJsonElementToNative(kvp.Value); } return result; } private object ConvertJsonElementToNative(object value) { if (value is JsonElement element) { return element.ValueKind switch { JsonValueKind.String => element.GetString(), JsonValueKind.Number => element.TryGetInt32(out var intVal) ? intVal : element.TryGetDouble(out var doubleVal) ? doubleVal : element.GetDecimal(), JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.Null => null, JsonValueKind.Object => ConvertJsonElementsToNatives( JsonSerializer.Deserialize>(element.GetRawText())), JsonValueKind.Array => element.EnumerateArray() .Select(x => ConvertJsonElementToNative(x)).ToArray(), _ => value }; } else if (value is Dictionary nestedDict) { return ConvertJsonElementsToNatives(nestedDict); } return value; } public void SaveJSON(string fileName) { var workflowData = new Dictionary(); // Save Configuration var configData = new Dictionary { ["RuntimeCameraType"] = Configuration.RuntimeCameraType.ToString(), ["DevelopmentCameraType"] = Configuration.DevelopmentCameraType.ToString(), ["Outputs"] = Configuration.Outputs.ToString(), ["EmulationPath"] = Configuration.EmulationPath, ["PythonPath"] = Configuration.PythonPath, ["ResultPin"] = Configuration.ResultPin, ["SerialPort"] = Configuration.SerialPort, ["Delay"] = Configuration.Delay }; workflowData["Configuration"] = configData; // Save Operations var operationsData = new List>(); foreach (BaseOperation operation in Operations) { var operationData = new Dictionary { ["Type"] = operation.GetType().AssemblyQualifiedName }; var operationDict = new Dictionary(); operation.Save(operationDict); operationData["Data"] = operationDict; operationsData.Add(operationData); } workflowData["Operations"] = operationsData; // Save Recipe Image if (RecipeImage == null) { var mat = new Mat(128, 128, MatType.CV_8UC3, Scalar.Gray); var image = mat.ToBytes(); workflowData["RecipeImage"] = Convert.ToBase64String(image); } else { var mat = RecipeImage; var image = mat.Resize(new Size(128, 128)).ToBytes(); workflowData["RecipeImage"] = Convert.ToBase64String(image); } var options = new JsonSerializerOptions { WriteIndented = true }; string jsonString = JsonSerializer.Serialize(workflowData, options); File.WriteAllText(fileName, jsonString); } public void LoadJSON(string fileName, OperationDiscoveryService operationDiscoveryService) { string jsonString = File.ReadAllText(fileName); var workflowData = JsonSerializer.Deserialize>(jsonString); if (workflowData == null) return; // Load Configuration if (workflowData.ContainsKey("Configuration")) { var configElement = (JsonElement)workflowData["Configuration"]; var configData = JsonSerializer.Deserialize>(configElement.GetRawText()); if (configData != null) { var convertedConfigData = ConvertJsonElementsToNatives(configData); if (convertedConfigData.ContainsKey("RuntimeCameraType")) Configuration.RuntimeCameraType = Enum.Parse(convertedConfigData["RuntimeCameraType"].ToString()); if (convertedConfigData.ContainsKey("DevelopmentCameraType")) Configuration.DevelopmentCameraType = Enum.Parse(convertedConfigData["DevelopmentCameraType"].ToString()); if (convertedConfigData.ContainsKey("Outputs")) Configuration.Outputs = Enum.Parse(convertedConfigData["Outputs"].ToString()); if (convertedConfigData.ContainsKey("EmulationPath")) Configuration.EmulationPath = convertedConfigData["EmulationPath"].ToString(); if (convertedConfigData.ContainsKey("PythonPath")) Configuration.PythonPath = convertedConfigData["PythonPath"].ToString(); if (convertedConfigData.ContainsKey("ResultPin")) Configuration.ResultPin = (int)convertedConfigData["ResultPin"]; if (convertedConfigData.ContainsKey("SerialPort")) Configuration.SerialPort = convertedConfigData["SerialPort"].ToString(); if (convertedConfigData.ContainsKey("Delay")) Configuration.Delay = (int)convertedConfigData["Delay"]; } } if (!File.Exists(Configuration.PythonPath)) PythonMissing(); // Load Operations Operations.Clear(); if (workflowData.ContainsKey("Operations")) { var operationsElement = (JsonElement)workflowData["Operations"]; var operationsArray = JsonSerializer.Deserialize>>(operationsElement.GetRawText()); if (operationsArray != null) { foreach (var operationData in operationsArray) { if (operationData.ContainsKey("Type") && operationData.ContainsKey("Data")) { string operationType = operationData["Type"].ToString(); var type = Type.GetType(operationType); if (type != null) { BaseOperation operation = operationDiscoveryService.CreateInstance(type); var dataElement = (JsonElement)operationData["Data"]; var operationDict = JsonSerializer.Deserialize>(dataElement.GetRawText()); if (operationDict != null) { var convertedDict = ConvertJsonElementsToNatives(operationDict); operation.Load(convertedDict); } Operations.Add(operation); } } } } } // Load Recipe Image if (workflowData.ContainsKey("RecipeImage")) { string base64Image = workflowData["RecipeImage"].ToString(); if (!string.IsNullOrEmpty(base64Image)) { byte[] imageData = Convert.FromBase64String(base64Image); RecipeImage = Mat.FromImageData(imageData); } } } public static WorkflowList LoadJSONFromFile(string fileName) { var workflowList = new WorkflowList(); workflowList.LoadJSON(fileName, new OperationDiscoveryService(workflowList)); return workflowList; } public void HotReloadParameters(string fileName) { string jsonString = File.ReadAllText(fileName); var workflowData = JsonSerializer.Deserialize>(jsonString); if (workflowData == null) return; // Update Configuration if (workflowData.ContainsKey("Configuration")) { var configElement = (JsonElement)workflowData["Configuration"]; var configData = JsonSerializer.Deserialize>(configElement.GetRawText()); if (configData != null) { var convertedConfigData = ConvertJsonElementsToNatives(configData); if (convertedConfigData.ContainsKey("RuntimeCameraType")) Configuration.RuntimeCameraType = Enum.Parse(convertedConfigData["RuntimeCameraType"].ToString()); if (convertedConfigData.ContainsKey("DevelopmentCameraType")) Configuration.DevelopmentCameraType = Enum.Parse(convertedConfigData["DevelopmentCameraType"].ToString()); if (convertedConfigData.ContainsKey("Outputs")) Configuration.Outputs = Enum.Parse(convertedConfigData["Outputs"].ToString()); if (convertedConfigData.ContainsKey("EmulationPath")) Configuration.EmulationPath = convertedConfigData["EmulationPath"].ToString(); if (convertedConfigData.ContainsKey("PythonPath")) Configuration.PythonPath = convertedConfigData["PythonPath"].ToString(); if (convertedConfigData.ContainsKey("ResultPin")) Configuration.ResultPin = (int)convertedConfigData["ResultPin"]; if (convertedConfigData.ContainsKey("SerialPort")) Configuration.SerialPort = convertedConfigData["SerialPort"].ToString(); if (convertedConfigData.ContainsKey("Delay")) Configuration.Delay = (int)convertedConfigData["Delay"]; } } // Update operation parameters by matching Id — do NOT recreate the Operations list if (workflowData.ContainsKey("Operations")) { var operationsElement = (JsonElement)workflowData["Operations"]; var operationsArray = JsonSerializer.Deserialize>>(operationsElement.GetRawText()); if (operationsArray != null) { foreach (var operationData in operationsArray) { if (!operationData.ContainsKey("Data")) continue; var dataElement = (JsonElement)operationData["Data"]; var operationDict = JsonSerializer.Deserialize>(dataElement.GetRawText()); if (operationDict == null) continue; var convertedDict = ConvertJsonElementsToNatives(operationDict); if (!convertedDict.ContainsKey("Id")) continue; var id = new Guid(convertedDict["Id"].ToString()); var existingOp = Operations.FirstOrDefault(x => x.Id == id); if (existingOp != null) { existingOp.Load(convertedDict); } else { Log.Warning("HotReload: no matching operation for Id {Id}", id); } } } } } }