From 510cb6156eb8faa4f90e3220ffe83823aedac6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=B6pke?= Date: Tue, 25 Jun 2024 23:23:59 +0200 Subject: [PATCH 1/6] Avoid GlobalEncodeable factory --- src/PlcServer.cs | 20 +++++++++++++------- tests/BoilerTests.cs | 29 ++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/PlcServer.cs b/src/PlcServer.cs index 41023818..a3ce4f1e 100644 --- a/src/PlcServer.cs +++ b/src/PlcServer.cs @@ -18,7 +18,6 @@ namespace OpcPlc; using System.IO; using System.Reflection; using System.Threading; - using Meters = OpcPlc.MetricsConfig; public partial class PlcServer : StandardServer @@ -260,11 +259,16 @@ public override ResponseHeader Write(RequestHeader requestHeader, WriteValueColl /// protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration) { - var nodeManagers = new List(); + var nodeManagers = new List(); + + var x = typeof(StandardServer).GetField("m_serverInternal", BindingFlags.Instance | BindingFlags.NonPublic); + var y = x.FieldType.GetField("m_factory", BindingFlags.Instance | BindingFlags.NonPublic); + y.SetValue(server, new EncodeableFactory(false)); + + // Add encodable complex types. - // Add encodable complex types. server.Factory.AddEncodeableTypes(Assembly.GetExecutingAssembly()); - EncodeableFactory.GlobalFactory.AddEncodeableTypes(Assembly.GetExecutingAssembly()); + // EncodeableFactory.GlobalFactory.AddEncodeableTypes(Assembly.GetExecutingAssembly()); // Add DI node manager first so that it gets the namespace index 2. var diNodeManager = new DiNodeManager(server, configuration); @@ -423,9 +427,10 @@ protected override void OnServerStopping() IList currentSessions = ServerInternal.SessionManager.GetSessions(); if (currentSessions.Count > 0) - { - // provide some time for the connected clients to detect the shutdown state. - ServerInternal.Status.Value.ShutdownReason = new LocalizedText(string.Empty, "Application closed."); // Invariant. + { + // provide some time for the connected clients to detect the shutdown state. +#pragma warning disable CS0618 // Type or member is obsolete + ServerInternal.Status.Value.ShutdownReason = new LocalizedText(string.Empty, "Application closed."); // Invariant. ServerInternal.Status.Variable.ShutdownReason.Value = new LocalizedText(string.Empty, "Application closed."); // Invariant. ServerInternal.Status.Value.State = ServerState.Shutdown; ServerInternal.Status.Variable.State.Value = ServerState.Shutdown; @@ -439,6 +444,7 @@ protected override void OnServerStopping() Thread.Sleep(TimeSpan.FromSeconds(1)); } +#pragma warning restore CS0618 // Type or member is obsolete } } catch diff --git a/tests/BoilerTests.cs b/tests/BoilerTests.cs index dbe6be1b..4be34e5f 100644 --- a/tests/BoilerTests.cs +++ b/tests/BoilerTests.cs @@ -1,9 +1,12 @@ -namespace OpcPlc.Tests; - +namespace OpcPlc.Tests; + using BoilerModel1; using FluentAssertions; using NUnit.Framework; using Opc.Ua; +using Opc.Ua.Client.ComplexTypes; +using System.Dynamic; +using System.Text.Json; using static System.TimeSpan; /// @@ -11,10 +14,19 @@ namespace OpcPlc.Tests; /// [TestFixture] public class BoilerTests : SimulatorTestsBase -{ +{ + private ComplexTypeSystem _complexTypeSystem; public BoilerTests() : base(new[] { "--ctb" }) { } + + [OneTimeSetUp] + public void OneTimeSetUp() + { + _complexTypeSystem = new ComplexTypeSystem(Session); + var loaded = _complexTypeSystem.LoadNamespace(OpcPlc.Namespaces.OpcPlcBoiler).ConfigureAwait(false).GetAwaiter().GetResult(); + loaded.Should().BeTrue("BoilerDataType should be loaded"); + } [TearDown] public new virtual void TearDown() @@ -120,7 +132,14 @@ private void TurnHeaterOff() private BoilerDataType GetBoilerModel() { var nodeId = NodeId.Create(BoilerModel1.Variables.Boiler1_BoilerStatus, OpcPlc.Namespaces.OpcPlcBoiler, Session.NamespaceUris); - var value = Session.ReadValue(nodeId).Value; - return value.Should().BeOfType().Which.Body.Should().BeOfType().Subject; + var value = Session.ReadValue(nodeId).Value; + + + var x = (value as ExtensionObject).Body; + var json = JsonSerializer.Serialize(x); + + var bt = JsonSerializer.Deserialize(json); + return bt; + //return value.Should().BeOfType().Which.Body.Should().BeOfType().Subject; } } From 26fa9399cd62427f2b5c880451b6e10b92e7e82a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=B6pke?= Date: Wed, 26 Jun 2024 00:25:27 +0200 Subject: [PATCH 2/6] Initialize Boiler with expected defaults --- src/PluginNodes/ComplexTypeBoilerPluginNode.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PluginNodes/ComplexTypeBoilerPluginNode.cs b/src/PluginNodes/ComplexTypeBoilerPluginNode.cs index ace47259..7a9a795d 100644 --- a/src/PluginNodes/ComplexTypeBoilerPluginNode.cs +++ b/src/PluginNodes/ComplexTypeBoilerPluginNode.cs @@ -58,7 +58,13 @@ private void AddNodes(FolderState methodsFolder) // Convert to node that can be manipulated within the server. _node = new Boiler1State(null); - _node.Create(_plcNodeManager.SystemContext, passiveBoiler1Node); + _node.Create(_plcNodeManager.SystemContext, passiveBoiler1Node); + _node.BoilerStatus.Value = new BoilerDataType { + Pressure = 99_000, + Temperature = new BoilerTemperatureType { Bottom = 100, Top = 95 }, + HeaterState = BoilerHeaterStateType.On, + }; + _node.BoilerStatus.ClearChangeMasks(_plcNodeManager.SystemContext, includeChildren: true); // Put Boiler #2 into Boilers folder. // TODO: Find a better solution to avoid this dependency between boilers. From de421cf9b9f6514c078740e2dc2c3e20ff935f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=B6pke?= Date: Wed, 26 Jun 2024 00:29:49 +0200 Subject: [PATCH 3/6] small code improvement --- tests/BoilerTests.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/BoilerTests.cs b/tests/BoilerTests.cs index 4be34e5f..6780c5ce 100644 --- a/tests/BoilerTests.cs +++ b/tests/BoilerTests.cs @@ -134,12 +134,11 @@ private BoilerDataType GetBoilerModel() var nodeId = NodeId.Create(BoilerModel1.Variables.Boiler1_BoilerStatus, OpcPlc.Namespaces.OpcPlcBoiler, Session.NamespaceUris); var value = Session.ReadValue(nodeId).Value; - - var x = (value as ExtensionObject).Body; - var json = JsonSerializer.Serialize(x); + // change dynamic in-memory created Boiler type to expected BoilerDataType by serializing and deserializing it. + var inmemoryBoilerDataType = (value as ExtensionObject).Body; + var json = JsonSerializer.Serialize(inmemoryBoilerDataType); - var bt = JsonSerializer.Deserialize(json); - return bt; - //return value.Should().BeOfType().Which.Body.Should().BeOfType().Subject; + var boilerDataTypeFromGeneratedSourceCode = JsonSerializer.Deserialize(json); + return boilerDataTypeFromGeneratedSourceCode; } } From a835a60ee12c10ef4270afee8cf2e82e8503d761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=B6pke?= Date: Wed, 26 Jun 2024 11:31:59 +0200 Subject: [PATCH 4/6] Subscribe to the VendingMachines node instead of the server node, avoiding that the test fails because of unrelated alarms (like Audit Alarms, Connectivity Status Alarms etc.) --- tests/DeterministicAlarmsTests.cs | 6 +++--- tests/DeterministicAlarmsTests2.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/DeterministicAlarmsTests.cs b/tests/DeterministicAlarmsTests.cs index d8658ae5..2c508cc4 100644 --- a/tests/DeterministicAlarmsTests.cs +++ b/tests/DeterministicAlarmsTests.cs @@ -4,7 +4,7 @@ namespace OpcPlc.Tests; using NUnit.Framework; using Opc.Ua; using System.Collections.Generic; -using System.Linq; +using System.Linq; using static System.TimeSpan; [TestFixture] @@ -26,14 +26,14 @@ public DeterministicAlarmsTests() : base(new[] [SetUp] public void CreateMonitoredItem() { - SetUpMonitoredItem(Objects.Server, NodeClass.Object, Attributes.EventNotifier); + SetUpMonitoredItem(AlarmNodeId("VendingMachines"), NodeClass.Object, Attributes.EventNotifier); AddMonitoredItem(); } [Test] public void FiresEventSequence() - { + { var machine1 = AlarmNodeId("VendingMachine1"); var machine2 = AlarmNodeId("VendingMachine2"); diff --git a/tests/DeterministicAlarmsTests2.cs b/tests/DeterministicAlarmsTests2.cs index adedee91..0f0fef4d 100644 --- a/tests/DeterministicAlarmsTests2.cs +++ b/tests/DeterministicAlarmsTests2.cs @@ -26,7 +26,7 @@ public DeterministicAlarmsTests2() : base(new[] [SetUp] public void CreateMonitoredItem() { - SetUpMonitoredItem(Objects.Server, NodeClass.Object, Attributes.EventNotifier); + SetUpMonitoredItem(AlarmNodeId("VendingMachines"), NodeClass.Object, Attributes.EventNotifier); AddMonitoredItem(); } From 8e32b43983f532db8e407913690bff7815872480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=B6pke?= Date: Wed, 26 Jun 2024 11:42:35 +0200 Subject: [PATCH 5/6] Code clean up --- src/PlcServer.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/PlcServer.cs b/src/PlcServer.cs index a3ce4f1e..53ef8ec3 100644 --- a/src/PlcServer.cs +++ b/src/PlcServer.cs @@ -261,14 +261,16 @@ protected override MasterNodeManager CreateMasterNodeManager(IServerInternal ser { var nodeManagers = new List(); - var x = typeof(StandardServer).GetField("m_serverInternal", BindingFlags.Instance | BindingFlags.NonPublic); - var y = x.FieldType.GetField("m_factory", BindingFlags.Instance | BindingFlags.NonPublic); - y.SetValue(server, new EncodeableFactory(false)); - - // Add encodable complex types. - + // When used via NuGet package in-memory, the server needs to use it's own encodeable factory. + // Otherwise the client will not load the type definitions for decoding correctly. There is currently no public + // API to set the encodeable factory and it is not possible to provide own implementation, because other classes + // require the StandardServer or ServerInternalData as objects, so we need to use reflection to set it. + var serverInternalDataField = typeof(StandardServer).GetField("m_serverInternal", BindingFlags.Instance | BindingFlags.NonPublic); + var encodeableFactoryField = serverInternalDataField.FieldType.GetField("m_factory", BindingFlags.Instance | BindingFlags.NonPublic); + encodeableFactoryField.SetValue(server, new EncodeableFactory(false)); + + // Add encodable complex types. server.Factory.AddEncodeableTypes(Assembly.GetExecutingAssembly()); - // EncodeableFactory.GlobalFactory.AddEncodeableTypes(Assembly.GetExecutingAssembly()); // Add DI node manager first so that it gets the namespace index 2. var diNodeManager = new DiNodeManager(server, configuration); From 9283602eedd11879fc7773d3d1491be51ed37f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20K=C3=B6pke?= Date: Wed, 26 Jun 2024 11:56:22 +0200 Subject: [PATCH 6/6] Removed leftover --- src/PlcServer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/PlcServer.cs b/src/PlcServer.cs index 53ef8ec3..2ba4e291 100644 --- a/src/PlcServer.cs +++ b/src/PlcServer.cs @@ -431,7 +431,6 @@ protected override void OnServerStopping() if (currentSessions.Count > 0) { // provide some time for the connected clients to detect the shutdown state. -#pragma warning disable CS0618 // Type or member is obsolete ServerInternal.Status.Value.ShutdownReason = new LocalizedText(string.Empty, "Application closed."); // Invariant. ServerInternal.Status.Variable.ShutdownReason.Value = new LocalizedText(string.Empty, "Application closed."); // Invariant. ServerInternal.Status.Value.State = ServerState.Shutdown; @@ -446,7 +445,6 @@ protected override void OnServerStopping() Thread.Sleep(TimeSpan.FromSeconds(1)); } -#pragma warning restore CS0618 // Type or member is obsolete } } catch