diff --git a/CHANGELOG.md b/CHANGELOG.md index ced6385..e89695c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.3] - 2024-12-10 +This update adds a **NODEGRAPH** functionality to the instance view (Simit-alike). This is a very first beta version for the early adopters. Linking outputs to inputs and datablock values is possible. +![](docs/img/nodegraph.png) + +### Features +- One big feature: nodegraph! + +### Fix +- Version in code now automatically updates when building the app + ## [0.1.2] - 2024-10-10 Minor update, mainly annoying bugfix and some small performance improvements diff --git a/PLCsimAdvanced_Manager/Components/Nodegraph.razor b/PLCsimAdvanced_Manager/Components/Nodegraph.razor new file mode 100644 index 0000000..b8f586d --- /dev/null +++ b/PLCsimAdvanced_Manager/Components/Nodegraph.razor @@ -0,0 +1,83 @@ +@using Blazor.Diagrams +@using Blazor.Diagrams.Core +@using Blazor.Diagrams.Core.PathGenerators +@using Blazor.Diagrams.Core.Routers +@using Blazor.Diagrams.Options +@using Blazor.Diagrams.Components +@using Blazor.Diagrams.Components.Widgets +@using Blazor.Diagrams.Core.Geometry +@using Blazor.Diagrams.Core.Models +@using PLCsimAdvanced_Manager.Services.Nodegraph +@using PLCsimAdvanced_Manager.Services.Nodegraph.InputNode +@using PLCsimAdvanced_Manager.Services.Nodegraph.OutputNode +@inject NodegraphServiceFactory NodegraphServiceFactory + +
+ + + + + + + + + + Start simulation + Stop simulation + + @if (simulationRunning) + { + Stop simulation to change nodegraph + } + else + { + + + + + + + + + + + + + + } +
+ + +@code { + + [Parameter] public string InstanceName { get; set; } + + private NodegraphService _nodegraphService; + private BlazorDiagram Diagram; + + private bool simulationRunning; + + + protected override void OnInitialized() + { + _nodegraphService = NodegraphServiceFactory.GetOrCreateService(InstanceName); + Diagram = _nodegraphService.Diagram; + _nodegraphService.OnSimulationStarted += OnSimulationStarted; + _nodegraphService.OnSimulationStopped += OnSimulationStopped; + simulationRunning = _nodegraphService.IsSimulationRunning; + } + + private void OnSimulationStarted(object sender, EventArgs e) + { + simulationRunning = true; + StateHasChanged(); + } + + private void OnSimulationStopped(object sender, EventArgs e) + { + simulationRunning = false; + StateHasChanged(); + } + + +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Components/Nodegraph.razor.css b/PLCsimAdvanced_Manager/Components/Nodegraph.razor.css new file mode 100644 index 0000000..4f6ea78 --- /dev/null +++ b/PLCsimAdvanced_Manager/Components/Nodegraph.razor.css @@ -0,0 +1,9 @@ +.diagram-container{ + width: 100%; + height: 400px; + border: 1px solid lightgray; +} + +.diagram-container.border-yellow { + border: 4px solid #ff8400; + } diff --git a/PLCsimAdvanced_Manager/Components/VariablesTable.razor b/PLCsimAdvanced_Manager/Components/VariablesTable.razor new file mode 100644 index 0000000..aae0ec1 --- /dev/null +++ b/PLCsimAdvanced_Manager/Components/VariablesTable.razor @@ -0,0 +1,465 @@ +@using Blazor.Diagrams +@using Blazor.Diagrams.Core.Controls.Default +@using Blazor.Diagrams.Core.Geometry +@using PLCsimAdvanced_Manager.Services +@using PLCsimAdvanced_Manager.Services.Nodegraph.InputNode +@using PLCsimAdvanced_Manager.Services.Nodegraph.OutputNode +@using Size = MudBlazor.Size +@using Color = MudBlazor.Color +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@implements IDisposable +@inject ManagerFacade managerFacade + + +@if (SelectedInstance.OperatingState != EOperatingState.Off) +{ + + + Values + + + + + + Name + Type + + + @($"{context.Key}") + + + + @context.Name + + + @context.DataValue.Type + + + @switch (Area) + { + case (EArea.Input): + + break; + case (EArea.Output): + + break; + case (EArea.DataBlock): + + Read from + Write to + + break; + } + + + + No matching records found + + + Not found + + + + + +} +else +{ + Instance not registered +} + +@code { + + [Parameter] public string InstanceName { get; set; } + [Parameter] public EArea Area { get; set; } + [Parameter] public BlazorDiagram Diagram { get; set; } + private IInstance SelectedInstance; + + + private bool InstanceRegistered = false; + + + private SDataValueByName[] _DataValueByNames; + + + protected override async Task OnInitializedAsync() + { + OnSelectInstance(InstanceName); + } + + private void OnSelectInstance(string name) + { + SelectedInstance = SimulationRuntimeManager.CreateInterface(name); + SelectedInstance.UpdateTagList(); + + + if (SelectedInstance.OperatingState != EOperatingState.Off) + { + InstanceRegistered = true; + setDataValueByNames(); + } + } + + + // ------------------------------- + private IEnumerable pagedData; + private MudTable table; + + private int totalItems; + private string searchString = null; + + + private Task> ServerReload(TableState state) + { + var _DataValueByNamesToGet = _DataValueByNames?.Where(element => + { + if (string.IsNullOrWhiteSpace(searchString)) + return true; + if (element.Name.Contains(searchString, StringComparison.OrdinalIgnoreCase)) + return true; + if (element.DataValue.Type.ToString().Contains(searchString, StringComparison.OrdinalIgnoreCase)) + return true; + if ($"{element.Name} {element.DataValue.Type} {element.DataValue.ToString()}".Contains(searchString)) + return true; + return false; + }) + .Where(element => Diagram.Nodes.All(node => node.Title != element.Name)) + .ToArray(); + if (_DataValueByNamesToGet != null) + { + totalItems = _DataValueByNamesToGet.Count(); + + + if (!InstanceRegistered) + { + OnSelectInstance(SelectedInstance.Name); + return Task.FromResult(new TableData()); + } + + + pagedData = _DataValueByNamesToGet.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray(); + } + + return Task.FromResult(new TableData() { TotalItems = totalItems, Items = pagedData }); + } + + private void OnSearch(string text) + { + searchString = text; + // table.ReloadServerData(); + } + + private void setDataValueByNames() + { + if (SelectedInstance == null) + { + Snackbar.Add("Issue with reading data for the given instance", Severity.Error); + return; + } + + SelectedInstance.UpdateTagList(); + + InvokeAsync(StateHasChanged); + + _DataValueByNames = SelectedInstance.TagInfos.Where(e => e.Area == Area && e.PrimitiveDataType != EPrimitiveDataType.Struct) + .Select(e => new SDataValueByName { Name = e.Name, DataValue = new SDataValue { Type = e.PrimitiveDataType } }) + .ToArray(); + } + + public object parseData(SDataValue dataValue) + { + switch (dataValue.Type) + { + case EPrimitiveDataType.Unspecific: + return "Unspecific type, value not avaliable"; + case EPrimitiveDataType.Struct: + return "Struct type, not implemented"; // not yet implemented for now + case EPrimitiveDataType.Bool: + return dataValue.Bool; + case EPrimitiveDataType.Int8: + return dataValue.Int8; + case EPrimitiveDataType.Int16: + return dataValue.Int16; + case EPrimitiveDataType.Int32: + return dataValue.Int32; + case EPrimitiveDataType.Int64: + return dataValue.Int64; + case EPrimitiveDataType.UInt8: + return dataValue.UInt8; + case EPrimitiveDataType.UInt16: + return dataValue.UInt16; + case EPrimitiveDataType.UInt32: + return dataValue.UInt32; + case EPrimitiveDataType.UInt64: + return dataValue.UInt64; + case EPrimitiveDataType.Float: + return dataValue.Float; + case EPrimitiveDataType.Double: + return dataValue.Double; + case EPrimitiveDataType.Char: + return dataValue.Char; + case EPrimitiveDataType.WChar: + return dataValue.WChar; + default: + throw new ArgumentOutOfRangeException(); + } + } + + + public void AddVariableToDiagram(SDataValueByName dataValueByName, ENodeType nodeType) + { + Point position; + if (Diagram.Nodes.Count == 0) + { + position = new Point(100, 200); + } + else + { + var oldPos = Diagram.Nodes.Last().Position; + position = new Point(oldPos.X + 20, oldPos.Y + 20); + } + + + if (nodeType == ENodeType.Input) + { + AddInputNodeToDiagram(dataValueByName, position); + } + else if (nodeType == ENodeType.Output) + { + AddOutputNodeToDiagram(dataValueByName, position); + } + + + InvokeAsync(StateHasChanged); + table.ReloadServerData(); + } + + public void Dispose() + { + SelectedInstance?.Dispose(); + Snackbar?.Dispose(); + } + + private void AddInputNodeToDiagram(SDataValueByName dataValueByName, Point position) + { + InputNodeModel node; + switch (dataValueByName.DataValue.Type) + { + case EPrimitiveDataType.Unspecific: + break; + case EPrimitiveDataType.Struct: + break; + case EPrimitiveDataType.Bool: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Int8: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Int16: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Int32: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Int64: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.UInt8: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.UInt16: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.UInt32: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.UInt64: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Float: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Double: + node = Diagram.Nodes.Add(new InputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Char: + break; + case EPrimitiveDataType.WChar: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void AddOutputNodeToDiagram(SDataValueByName dataValueByName, Point position) + { + OutputNodeModel node; + switch (dataValueByName.DataValue.Type) + { + case EPrimitiveDataType.Unspecific: + break; + case EPrimitiveDataType.Struct: + break; + case EPrimitiveDataType.Bool: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Int8: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Int16: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Int32: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Int64: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.UInt8: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.UInt16: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.UInt32: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.UInt64: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Float: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Double: + node = Diagram.Nodes.Add(new OutputNodeModel(position: position) + { + Title = dataValueByName.Name + }); + Diagram.Controls.AddFor(node).Add(new RemoveControl(0.5, 0)); + + break; + case EPrimitiveDataType.Char: + break; + case EPrimitiveDataType.WChar: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + + public enum ENodeType + { + Input, + Output + } + +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/MainLayout.razor b/PLCsimAdvanced_Manager/MainLayout.razor index 4298048..457ebcb 100644 --- a/PLCsimAdvanced_Manager/MainLayout.razor +++ b/PLCsimAdvanced_Manager/MainLayout.razor @@ -58,6 +58,12 @@ { _open = !_open; } + + protected override void OnInitialized() + { + base.OnInitialized(); + version = ThisAssembly.Git.BaseTag; + } string version = "0.1.1"; bool newVersionAvailable = false; diff --git a/PLCsimAdvanced_Manager/PLCsimAdvanced_Manager.csproj b/PLCsimAdvanced_Manager/PLCsimAdvanced_Manager.csproj index 593cd8a..bf4dd98 100644 --- a/PLCsimAdvanced_Manager/PLCsimAdvanced_Manager.csproj +++ b/PLCsimAdvanced_Manager/PLCsimAdvanced_Manager.csproj @@ -8,6 +8,7 @@ win-x64 + 12 @@ -16,6 +17,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -25,10 +30,11 @@ + - + .dockerignore @@ -42,6 +48,10 @@ + + <_ContentIncludedByDefault Remove="Services\Nodegraph\InOutNode\InOutNodeWidget.razor" /> + + diff --git a/PLCsimAdvanced_Manager/Pages/Instance.razor b/PLCsimAdvanced_Manager/Pages/Instance.razor index 025f9b4..f72d34f 100644 --- a/PLCsimAdvanced_Manager/Pages/Instance.razor +++ b/PLCsimAdvanced_Manager/Pages/Instance.razor @@ -322,6 +322,9 @@ + + + diff --git a/PLCsimAdvanced_Manager/Pages/_Host.cshtml b/PLCsimAdvanced_Manager/Pages/_Host.cshtml index 74871c6..819e064 100644 --- a/PLCsimAdvanced_Manager/Pages/_Host.cshtml +++ b/PLCsimAdvanced_Manager/Pages/_Host.cshtml @@ -16,6 +16,10 @@ + + + + @@ -37,5 +41,8 @@ + + + \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Program.cs b/PLCsimAdvanced_Manager/Program.cs index 95ae787..279d0fc 100644 --- a/PLCsimAdvanced_Manager/Program.cs +++ b/PLCsimAdvanced_Manager/Program.cs @@ -7,6 +7,7 @@ using MudBlazor.Services; using PLCsimAdvanced_Manager.Services; using PLCsimAdvanced_Manager.Services.Logger; +using PLCsimAdvanced_Manager.Services.Nodegraph; using PLCsimAdvanced_Manager.Services.Persistence; using PLCsimAdvanced_Manager.Shared; @@ -40,6 +41,8 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + //hosted services builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); // TODO -> split hosted services and singleton part diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/BaseNodeModel.cs b/PLCsimAdvanced_Manager/Services/Nodegraph/BaseNodeModel.cs new file mode 100644 index 0000000..d71ef6e --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/BaseNodeModel.cs @@ -0,0 +1,19 @@ +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; + +namespace PLCsimAdvanced_Manager.Services.Nodegraph; + +public abstract class BaseNodeModel(Point position) : NodeModel(position) +{ + public abstract void Calculate(); +} + +public class BaseNodeModel(Point position) : BaseNodeModel(position) +{ + public T? Value { get; set; } + + public override void Calculate() + { + /// + } +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/InputNode/InputNodeModel.cs b/PLCsimAdvanced_Manager/Services/Nodegraph/InputNode/InputNodeModel.cs new file mode 100644 index 0000000..675540c --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/InputNode/InputNodeModel.cs @@ -0,0 +1,23 @@ +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using PLCsimAdvanced_Manager.Services.Nodegraph.PortModel; + +namespace PLCsimAdvanced_Manager.Services.Nodegraph.InputNode; + +public abstract class InputNodeModel(Point position) : BaseNodeModel(position) +{ +} +public class InputNodeModel : InputNodeModel +{ + public T? Value { get; set; } + + public InputNodeModel(Point position) : base(position) + { + AddPort(new OutputPortModel(this)); + } + + public override void Calculate() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/InputNode/InputNodeWidget.razor b/PLCsimAdvanced_Manager/Services/Nodegraph/InputNode/InputNodeWidget.razor new file mode 100644 index 0000000..bb44755 --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/InputNode/InputNodeWidget.razor @@ -0,0 +1,52 @@ +@typeparam T +@using Blazor.Diagrams.Core.Models +@using Blazor.Diagrams.Components.Renderers + +
+
+
@Node.Title
+
+
+ @Node.Value?.GetType() +
+ @foreach (var port in Node.Ports) + { + + } +
+ + +@code { + [Parameter] public InputNodeModel Node { get; set; } + + private string portColor; + + + protected override void OnInitialized() + { + portColor = GetPortColor(); + } + + private string GetPortColor() + { + return typeof(T) switch + { + Type t when t == typeof(bool) => "background-color:#6C757D ", + Type t when t == typeof(byte) => "background-color:#A4C3B2 ", + Type t when t == typeof(sbyte) => "background-color:#C5A880 ", + Type t when t == typeof(short) => "background-color:#B7B8A3 ", + Type t when t == typeof(ushort) => "background-color:#899DA4 ", + Type t when t == typeof(int) => "background-color:#D4A5A5 ", + Type t when t == typeof(uint) => "background-color:#B5CDA3 ", + Type t when t == typeof(long) => "background-color:#A3C9D9 ", + Type t when t == typeof(ulong) => "background-color:#D9C3A3 ", + Type t when t == typeof(float) => "background-color:#A3B9C9 ", + Type t when t == typeof(double) => "background-color:#C9A3B9 ", + Type t when t == typeof(decimal) => "background-color:#A3D9C9 ", + Type t when t == typeof(char) => "background-color:#D9A3A3 ", + Type t when t == typeof(string) => "background-color:#B3B3A3 ", + _ => "background-color:#cccccc" + }; + } + +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/InputNode/InputNodeWidget.razor.css b/PLCsimAdvanced_Manager/Services/Nodegraph/InputNode/InputNodeWidget.razor.css new file mode 100644 index 0000000..aad34fd --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/InputNode/InputNodeWidget.razor.css @@ -0,0 +1,25 @@ +.card{ + width: 80px; + border: 1px solid #ccc; + border-radius: 10px; + padding: 1px; + background-color:#f9f9f9; +} + +.card.selected{ + border-color: #4949bd; + /*background-color: #e0e0ff;*/ +} + +::deep .diagram-port{ + width: 15px; + height: 15px; + background-color: #0a53be; + border-top-left-radius: 50%; + border-top-right-radius: 50%; + border-bottom-left-radius: 50%; + border-bottom-right-radius: 50%; + position: absolute; + right: -7px; + top: 10px; +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/NodegraphService.cs b/PLCsimAdvanced_Manager/Services/Nodegraph/NodegraphService.cs new file mode 100644 index 0000000..2012716 --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/NodegraphService.cs @@ -0,0 +1,307 @@ +using Blazor.Diagrams; +using Blazor.Diagrams.Core; +using Blazor.Diagrams.Core.Anchors; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.PathGenerators; +using Blazor.Diagrams.Core.Routers; +using Blazor.Diagrams.Options; +using MatBlazor; +using PLCsimAdvanced_Manager.Services.Nodegraph; +using PLCsimAdvanced_Manager.Services.Nodegraph.InputNode; +using PLCsimAdvanced_Manager.Services.Nodegraph.OutputNode; +using PLCsimAdvanced_Manager.Services.Nodegraph.PortModel; +using Siemens.Simatic.Simulation.Runtime; + +namespace PLCsimAdvanced_Manager.Services.Nodegraph; + +public class NodegraphService +{ + public BlazorDiagram Diagram { get; set; } = new BlazorDiagram(new BlazorDiagramOptions() + { + AllowMultiSelection = true, + Zoom = + { + Enabled = false, + }, + Links = + { + DefaultRouter = new NormalRouter(), + DefaultPathGenerator = new SmoothPathGenerator() + }, + }); + + private IInstance SelectedInstance; + private List sorted = new List(); + private HashSet visited = new HashSet(); + private SDataValueByName[] inputnodes; + private SDataValueByName[] outputnodes; + private Timer _timer; + public event EventHandler OnSimulationStarted; + public event EventHandler OnSimulationStopped; + public bool IsSimulationRunning; + + + public NodegraphService(string plcInstanceName) + { + Diagram.RegisterComponent, InputNodeWidget>(); + Diagram.RegisterComponent, InputNodeWidget>(); + Diagram.RegisterComponent, InputNodeWidget>(); + Diagram.RegisterComponent, InputNodeWidget>(); + Diagram.RegisterComponent, InputNodeWidget>(); + Diagram.RegisterComponent, InputNodeWidget>(); + Diagram.RegisterComponent, InputNodeWidget>(); + Diagram.RegisterComponent, InputNodeWidget>(); + Diagram.RegisterComponent, InputNodeWidget>(); + Diagram.RegisterComponent, InputNodeWidget>(); + + Diagram.RegisterComponent, OutputNodeWidget>(); + Diagram.RegisterComponent, OutputNodeWidget>(); + Diagram.RegisterComponent, OutputNodeWidget>(); + Diagram.RegisterComponent, OutputNodeWidget>(); + Diagram.RegisterComponent, OutputNodeWidget>(); + Diagram.RegisterComponent, OutputNodeWidget>(); + Diagram.RegisterComponent, OutputNodeWidget>(); + Diagram.RegisterComponent, OutputNodeWidget>(); + Diagram.RegisterComponent, OutputNodeWidget>(); + Diagram.RegisterComponent, OutputNodeWidget>(); + + SelectedInstance = SimulationRuntimeManager.CreateInterface(plcInstanceName); + + _timer = new Timer(_ => GraphExecution(), null, Timeout.Infinite, 10); + + } + + private void graphCompilation() + { + // remove nodes that have no links + Diagram.Nodes.Where(e => !e.PortLinks.Any()).ToList().ForEach(e => Diagram.Nodes.Remove(e)); + + // Get PLC output~nodegraph input nodes + var nodegraphInputNodes = Diagram.Nodes + .Where(node => node is InputNodeModel) + .ToList(); + var outputDataValueByName = SelectedInstance.TagInfos + .Where(e => (e.Area == EArea.Output || e.Area == EArea.DataBlock) && e.PrimitiveDataType != EPrimitiveDataType.Struct) + .Select(e => new SDataValueByName + { Name = e.Name, DataValue = new SDataValue { Type = e.PrimitiveDataType } }) + .ToArray(); + + inputnodes = nodegraphInputNodes + .Select(node => outputDataValueByName.FirstOrDefault(data => data.Name == node.Title)) + .ToArray(); + + // PLC input~nodegraph output nodes + var nodegraphOutputNodes = Diagram.Nodes + .Where(node => node is OutputNodeModel) + .ToList(); + var inputDataValueByName = SelectedInstance.TagInfos + .Where(e => (e.Area == EArea.Input || e.Area == EArea.DataBlock) && e.PrimitiveDataType != EPrimitiveDataType.Struct) + .Select(e => new SDataValueByName + { Name = e.Name, DataValue = new SDataValue { Type = e.PrimitiveDataType } }) + .ToArray(); + outputnodes = nodegraphOutputNodes + .Select(node => inputDataValueByName.FirstOrDefault(data => data.Name == node.Title)) + .ToArray(); + + foreach (var node in nodegraphInputNodes) + { + visited.Add(node.Id); + } + + foreach (var node in Diagram.Nodes) + { + Visit(node as BaseNodeModel); + } + } + + private void Visit(BaseNodeModel node) + { + if (visited.Contains(node.Id)) + return; + + visited.Add(node.Id); + var inputPorts = node.Ports.OfType().ToList(); + foreach (var dependency in inputPorts) + { + var sourceNode = Diagram.Links[0].Source as SinglePortAnchor; + Visit(sourceNode.Port.Parent as BaseNodeModel); + } + + sorted.Add(node); + } + + private void GraphExecution() + { + // 1. read input nodes + // 2. execute graph + // 3. write output nodes + + SelectedInstance.ReadSignals(ref inputnodes); + foreach (var inputnode in inputnodes) + { + var node = Diagram.Nodes.FirstOrDefault(e => e.Title == inputnode.Name); + if (node is BaseNodeModel baseNode) + { + if (baseNode is InputNodeModel boolNode) + { + boolNode.Value = inputnode.DataValue.Bool; + } + else if (baseNode is InputNodeModel byteNode) + { + byteNode.Value = inputnode.DataValue.UInt8; + } + else if (baseNode is InputNodeModel shortNode) + { + shortNode.Value = inputnode.DataValue.Int16; + } + else if (baseNode is InputNodeModel intNode) + { + intNode.Value = inputnode.DataValue.Int32; + } + else if (baseNode is InputNodeModel longNode) + { + longNode.Value = inputnode.DataValue.Int64; + } + else if (baseNode is InputNodeModel ushortNode) + { + ushortNode.Value = inputnode.DataValue.UInt16; + } + else if (baseNode is InputNodeModel uintNode) + { + uintNode.Value = inputnode.DataValue.UInt32; + } + else if (baseNode is InputNodeModel ulongNode) + { + ulongNode.Value = inputnode.DataValue.UInt64; + } + else if (baseNode is InputNodeModel floatNode) + { + floatNode.Value = inputnode.DataValue.Float; + } + else if (baseNode is InputNodeModel doubleNode) + { + doubleNode.Value = inputnode.DataValue.Double; + } + else + { + throw new InvalidOperationException($"Unsupported type: {baseNode.GetType()}"); + } + } + } + + foreach (var node in sorted) + { + var baseNode = node as BaseNodeModel; + baseNode.Calculate(); + // node. calculate values + } + + for (int i = 0; i < outputnodes.Length; i++) + { + var node = Diagram.Nodes.FirstOrDefault(e => e.Title == outputnodes[i].Name); + if (node is BaseNodeModel baseNode) + { + if (baseNode is OutputNodeModel boolNode) + { + outputnodes[i].DataValue.Bool = boolNode.Value; + } + else if (baseNode is OutputNodeModel byteNode) + { + outputnodes[i].DataValue.UInt8 = byteNode.Value; + } + else if (baseNode is OutputNodeModel shortNode) + { + outputnodes[i].DataValue.Int16 = shortNode.Value; + } + else if (baseNode is OutputNodeModel intNode) + { + outputnodes[i].DataValue.Int32 = intNode.Value; + } + else if (baseNode is OutputNodeModel longNode) + { + outputnodes[i].DataValue.Int64 = longNode.Value; + } + else if (baseNode is OutputNodeModel ushortNode) + { + outputnodes[i].DataValue.UInt16 = ushortNode.Value; + } + else if (baseNode is OutputNodeModel uintNode) + { + outputnodes[i].DataValue.UInt32 = uintNode.Value; + } + else if (baseNode is OutputNodeModel ulongNode) + { + outputnodes[i].DataValue.UInt64 = ulongNode.Value; + } + else if (baseNode is OutputNodeModel floatNode) + { + outputnodes[i].DataValue.Float = floatNode.Value; + } + else if (baseNode is OutputNodeModel doubleNode) + { + outputnodes[i].DataValue.Double = doubleNode.Value; + } + else + { + throw new InvalidOperationException($"Unsupported type: {baseNode.GetType()}"); + } + } + } + + SelectedInstance.WriteSignals(ref outputnodes); + } + + private void LockDiagram() + { + Diagram.Nodes.ToList().ForEach(node => node.Locked = true); + Diagram.Links.ToList().ForEach(link => link.Locked = true); + } + + private void UnlockDiagram() + { + Diagram.Nodes.ToList().ForEach(node => node.Locked = false); + Diagram.Links.ToList().ForEach(link => link.Locked = false); + } + + public void ExecuteSimulation() + { + graphCompilation(); + if (Diagram.Nodes.Count==0) + { + return; + } + + + LockDiagram(); + PlcStartSetup(); + _timer.Change(0, 100); + IsSimulationRunning = true; + OnSimulationStarted?.Invoke(this, EventArgs.Empty); + } + + public void StopSimulation() + { + _timer.Change(Timeout.Infinite, Timeout.Infinite); + UnlockDiagram(); + IsSimulationRunning = false; + OnSimulationStopped?.Invoke(this, EventArgs.Empty); + } + + public void Dispose() + { + _timer?.Dispose(); + } + + private async void PlcStartSetup() + { + if (SelectedInstance.OperatingState == EOperatingState.Off) + { + SelectedInstance.PowerOn(); + SelectedInstance.Run(); + } + else if (SelectedInstance.OperatingState == EOperatingState.Stop) + { + SelectedInstance.Run(); + } + } +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/NodegraphServiceFactory.cs b/PLCsimAdvanced_Manager/Services/Nodegraph/NodegraphServiceFactory.cs new file mode 100644 index 0000000..964ce51 --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/NodegraphServiceFactory.cs @@ -0,0 +1,19 @@ +using System.Collections.Concurrent; +using Blazor.Diagrams.Core; + +namespace PLCsimAdvanced_Manager.Services.Nodegraph; + +public class NodegraphServiceFactory +{ + private readonly ConcurrentDictionary _services = new ConcurrentDictionary(); + + public NodegraphService GetOrCreateService(string plcInstanceName) + { + return _services.GetOrAdd(plcInstanceName, id => new NodegraphService(id)); + } + + public void RemoveService(string plcInstanceName) + { + _services.TryRemove(plcInstanceName, out _); + } +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/OutputNode/OutputNodeModel.cs b/PLCsimAdvanced_Manager/Services/Nodegraph/OutputNode/OutputNodeModel.cs new file mode 100644 index 0000000..6d97b9e --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/OutputNode/OutputNodeModel.cs @@ -0,0 +1,28 @@ +using Blazor.Diagrams.Core.Anchors; +using Blazor.Diagrams.Core.Geometry; +using PLCsimAdvanced_Manager.Services.Nodegraph.InputNode; +using PLCsimAdvanced_Manager.Services.Nodegraph.PortModel; + +namespace PLCsimAdvanced_Manager.Services.Nodegraph.OutputNode; + +public abstract class OutputNodeModel(Point position) : BaseNodeModel(position) +{ +} +public class OutputNodeModel : OutputNodeModel +{ + public T? Value { get; set; } + + public OutputNodeModel(Point position) : base(position) + { + AddPort(new InputPortModel(this)); + } + + public override void Calculate() + { + var source = PortLinks.First().Source as SinglePortAnchor; + if (source == null) + return; + Value = (source.Port.Parent as InputNodeModel).Value; + + } +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/OutputNode/OutputNodeWidget.razor b/PLCsimAdvanced_Manager/Services/Nodegraph/OutputNode/OutputNodeWidget.razor new file mode 100644 index 0000000..869ed3a --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/OutputNode/OutputNodeWidget.razor @@ -0,0 +1,50 @@ +@typeparam T +@using Blazor.Diagrams.Core.Models +@using Blazor.Diagrams.Components.Renderers + +
+
+
@Node.Title
+
+
+ @Node.Value?.GetType() +
+ @foreach (var port in Node.Ports) + { + + } +
+ + +@code { + [Parameter] public OutputNodeModel Node { get; set; } + private string portColor; + + protected override void OnInitialized() + { + portColor = GetPortColor(); + } + + private string GetPortColor() + { + return typeof(T) switch + { + Type t when t == typeof(bool) => "background-color:#6C757D ", + Type t when t == typeof(byte) => "background-color:#A4C3B2 ", + Type t when t == typeof(sbyte) => "background-color:#C5A880 ", + Type t when t == typeof(short) => "background-color:#B7B8A3 ", + Type t when t == typeof(ushort) => "background-color:#899DA4 ", + Type t when t == typeof(int) => "background-color:#D4A5A5 ", + Type t when t == typeof(uint) => "background-color:#B5CDA3 ", + Type t when t == typeof(long) => "background-color:#A3C9D9 ", + Type t when t == typeof(ulong) => "background-color:#D9C3A3 ", + Type t when t == typeof(float) => "background-color:#A3B9C9 ", + Type t when t == typeof(double) => "background-color:#C9A3B9 ", + Type t when t == typeof(decimal) => "background-color:#A3D9C9 ", + Type t when t == typeof(char) => "background-color:#D9A3A3 ", + Type t when t == typeof(string) => "background-color:#B3B3A3 ", + _ => "background-color:#cccccc" + }; + } + +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/OutputNode/OutputNodeWidget.razor.css b/PLCsimAdvanced_Manager/Services/Nodegraph/OutputNode/OutputNodeWidget.razor.css new file mode 100644 index 0000000..3adcf3a --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/OutputNode/OutputNodeWidget.razor.css @@ -0,0 +1,25 @@ +.card{ + width: 80px; + border: 1px solid #ccc; + border-radius: 10px; + padding: 1px; + background-color:#f9f9f9; +} + +.card.selected{ + border-color: #4949bd; + /*background-color: #e0e0ff;*/ +} + +::deep .diagram-port{ + width: 15px; + height: 15px; + background-color: #0a53be; + border-top-left-radius: 50%; + border-top-right-radius: 50%; + border-bottom-left-radius: 50%; + border-bottom-right-radius: 50%; + position: absolute; + left: -7px; + top: 10px; +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/PortModels/InputPortModel.cs b/PLCsimAdvanced_Manager/Services/Nodegraph/PortModels/InputPortModel.cs new file mode 100644 index 0000000..4131b57 --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/PortModels/InputPortModel.cs @@ -0,0 +1,24 @@ +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Models.Base; + +namespace PLCsimAdvanced_Manager.Services.Nodegraph.PortModel; + +public abstract class InputPortModel(NodeModel parent) + : Blazor.Diagrams.Core.Models.PortModel(parent, PortAlignment.Left, null, null); + +public class InputPortModel(NodeModel parent): Blazor.Diagrams.Core.Models.PortModel(parent, PortAlignment.Left, null, null) +{ + public T? Value { get; set; } + + + + public override bool CanAttachTo(ILinkable other) + { + if (!base.CanAttachTo(other)) + return false; + if (Links.Count>0) + return false; + + return other is OutputPortModel; + } +} \ No newline at end of file diff --git a/PLCsimAdvanced_Manager/Services/Nodegraph/PortModels/OutputPortModel.cs b/PLCsimAdvanced_Manager/Services/Nodegraph/PortModels/OutputPortModel.cs new file mode 100644 index 0000000..56db6ea --- /dev/null +++ b/PLCsimAdvanced_Manager/Services/Nodegraph/PortModels/OutputPortModel.cs @@ -0,0 +1,23 @@ +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Models.Base; + +namespace PLCsimAdvanced_Manager.Services.Nodegraph.PortModel; + +public abstract class OutputPortModel(NodeModel parent) + : Blazor.Diagrams.Core.Models.PortModel(parent, PortAlignment.Right, null, null); +public class OutputPortModel(NodeModel paretn): Blazor.Diagrams.Core.Models.PortModel(paretn, PortAlignment.Right, null, null) +{ + public T? Value { get; set; } + + public override bool CanAttachTo(ILinkable other) + { + if (!base.CanAttachTo(other)) + return false; + + if (other.Links.Count>0) + return false; + + return other is InputPortModel; + } + +} \ No newline at end of file diff --git a/README.md b/README.md index 15b38c2..11c8a48 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,10 @@ Features: - [x] **Advanced network setting**. Being able to set every interface of the instance to an interface of the host machine - [x] Create **snapshots** and restore them - [x] Option for **auto start** instances on startup of PLCsim Advanced Manager. Either just register or completely start the instance when starting the manager application. (see instance settings) +- [x] A **nodegraph** to link variables and do basic virtual commissioning + Future feature ideas: -- [ ] Easy **virtual commissioning** by e.g. setting buttons and lights in the UI to the PLC variables - [ ] **Traces** on the variables for analysis - [ ] **Desktop version** so no webbrowser is needed for a local setup - [ ] Connect to **remote PLCsim Advance APIs** @@ -35,6 +36,8 @@ Future feature ideas: ![](docs/img/instancePage.png) +![](docs/img/nodegraph.png) + # Quickstart diff --git a/docs/img/nodegraph.png b/docs/img/nodegraph.png new file mode 100644 index 0000000..377895b Binary files /dev/null and b/docs/img/nodegraph.png differ