diff --git a/UaClient.UnitTests/IntegrationTests/IntegrationTests.cs b/UaClient.UnitTests/IntegrationTests/IntegrationTests.cs index fd911dd..b0b0852 100644 --- a/UaClient.UnitTests/IntegrationTests/IntegrationTests.cs +++ b/UaClient.UnitTests/IntegrationTests/IntegrationTests.cs @@ -582,6 +582,10 @@ void onPropertyChanged(object s, PropertyChangedEventArgs e) { } .Should().NotBeNull(); vm.CurrentTimeQueue .Should().NotBeEmpty(); + vm.DemoStaticArraysVector + .Should().NotBeEmpty(); + vm.VectorsAsObjects + .Should().NotBeEmpty(); } [Subscription(endpointUrl: "opc.tcp://localhost:48010", publishingInterval: 500, keepAliveCount: 20)] @@ -616,8 +620,33 @@ public DataValue CurrentTimeAsDataValue /// [MonitoredItem(nodeId: "i=2258")] public ObservableQueue CurrentTimeQueue { get; } = new ObservableQueue(capacity: 16, isFixedSize: true); + + /// + /// Gets or sets the value of DemoStaticArraysVector. + /// + [MonitoredItem(nodeId: "ns=2;s=Demo.Static.Arrays.Vector")] + public CustomTypeLibrary.CustomVector[] DemoStaticArraysVector + { + get { return this._DemoStaticArraysVector; } + set { this.SetProperty(ref this._DemoStaticArraysVector, value); } + } + + private CustomTypeLibrary.CustomVector[] _DemoStaticArraysVector; + + /// + /// Gets or sets the value of DemoStaticArraysVector. + /// + [MonitoredItem(nodeId: "ns=2;s=Demo.Static.Arrays.Vector")] + public object[] VectorsAsObjects + { + get { return this._VectorsAsObjects; } + set { this.SetProperty(ref this._VectorsAsObjects, value); } + } + + private object[] _VectorsAsObjects; } + [Fact] public async Task StackTest() { diff --git a/UaClient.UnitTests/UnitTests/DataValueExtensionTests.cs b/UaClient.UnitTests/UnitTests/DataValueExtensionTests.cs new file mode 100644 index 0000000..27d5b97 --- /dev/null +++ b/UaClient.UnitTests/UnitTests/DataValueExtensionTests.cs @@ -0,0 +1,67 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Workstation.ServiceModel.Ua; +using Xunit; + +namespace Workstation.UaClient.UnitTests +{ + public class DataValueExtensionTests + { + [Fact] + public void GetValueAsString() + { + var val = new DataValue("Hi"); + + val.GetValueOrDefault() + .Should().Be("Hi"); + val.GetValueOrDefault(-1) + .Should().Be(-1); + } + + [Fact] + public void GetValueAsCustomVector() + { + var obj = new CustomTypeLibrary.CustomVector { X = 1.0, Y = 2.0, Z = 3.0 }; + var val = new DataValue(obj); + + val.GetValueOrDefault() + .Should().BeEquivalentTo(obj); + val.GetValueOrDefault() + .Should().BeEquivalentTo(obj); + val.GetValueOrDefault(-1) + .Should().Be(-1); + } + + [Fact] + public void GetValueAsArrayInt32() + { + var array = new int[] { 1, 2, 3, 4, 5 }; + var val = new DataValue(array); + + val.GetValueOrDefault() + .Should().BeEquivalentTo(array); + val.GetValueOrDefault(-1) + .Should().Be(-1); + } + + [Fact] + public void GetValueAsArrayCustomVector() + { + var array = new CustomTypeLibrary.CustomVector[] + { + new CustomTypeLibrary.CustomVector { X = 1.0, Y = 2.0, Z = 3.0 } + }; + var val = new DataValue(array); + + val.GetValueOrDefault() + .Should().BeEquivalentTo(array); + val.GetValueOrDefault() + .Should().BeEquivalentTo(array); + val.GetValueOrDefault(-1) + .Should().Be(-1); + } + } +} diff --git a/UaClient/ServiceModel/Ua/DataValueExtensions.cs b/UaClient/ServiceModel/Ua/DataValueExtensions.cs index d33d873..ae24dab 100644 --- a/UaClient/ServiceModel/Ua/DataValueExtensions.cs +++ b/UaClient/ServiceModel/Ua/DataValueExtensions.cs @@ -42,20 +42,50 @@ public static class DataValueExtensions [return: MaybeNull] public static T GetValueOrDefault(this DataValue dataValue) { - var value = dataValue.GetValue(); - if (value != null) + var value = dataValue.Value; + switch (value) { - if (value is T) - { - return (T)value; - } - } + case ExtensionObject obj: + // handle object, custom type + var v2 = obj.BodyType == BodyType.Encodable ? obj.Body : obj; + if (v2 is T t1) + { + return t1; + } + return default!; - // While [MaybeNull] attribute signals to the caller - // that the return value can be null. It is ignored - // by the compiler inside of the method, hence we - // have to use the bang operator. - return default!; + case ExtensionObject[] objArray: + // handle object[], custom type[] + var v3 = objArray.Select(e => e.BodyType == BodyType.Encodable ? e.Body : e); + var elementType = typeof(T).GetElementType(); + if (elementType == null) + { + return default!; + } + try + { + var v4 = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(elementType).Invoke(null, new object[] { v3 }); + var v5 = typeof(Enumerable).GetMethod("ToArray").MakeGenericMethod(elementType).Invoke(null, new object[] { v4 }); + if (v5 is T t2) + { + return t2; + } + return default!; + } + catch (Exception) + { + return default!; + } + + default: + // handle built-in type + if (value is T t) + { + return t; + } + return default!; + + } } /// @@ -68,16 +98,50 @@ public static T GetValueOrDefault(this DataValue dataValue) [return: NotNullIfNotNull("defaultValue")] public static T GetValueOrDefault(this DataValue dataValue, T defaultValue) { - var value = dataValue.GetValue(); - if (value != null) + var value = dataValue.Value; + switch (value) { - if (value is T) - { - return (T)value; - } - } + case ExtensionObject obj: + // handle object, custom type + var v2 = obj.BodyType == BodyType.Encodable ? obj.Body : obj; + if (v2 is T t1) + { + return t1; + } + return defaultValue; - return defaultValue; + case ExtensionObject[] objArray: + // handle object[], custom type[] + var v3 = objArray.Select(e => e.BodyType == BodyType.Encodable ? e.Body : e); + var elementType = typeof(T).GetElementType(); + if (elementType == null) + { + return defaultValue; + } + try + { + var v4 = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(elementType).Invoke(null, new object[] { v3 }); + var v5 = typeof(Enumerable).GetMethod("ToArray").MakeGenericMethod(elementType).Invoke(null, new object[] { v4 }); + if (v5 is T t2) + { + return t2; + } + return defaultValue; + } + catch (Exception) + { + return defaultValue; + } + + default: + // handle built-in type + if (value is T t) + { + return t; + } + return defaultValue; + + } } } } diff --git a/UaClient/ServiceModel/Ua/MonitoredItemBase.cs b/UaClient/ServiceModel/Ua/MonitoredItemBase.cs index 4eee0c4..463c23c 100644 --- a/UaClient/ServiceModel/Ua/MonitoredItemBase.cs +++ b/UaClient/ServiceModel/Ua/MonitoredItemBase.cs @@ -385,6 +385,98 @@ private void SetDataErrorInfo(StatusCode statusCode) } } + /// + /// Subscribes to data changes of an attribute of a node. + /// Unwraps the published value and sets it in a property. + /// + public class ValueMonitoredItem : MonitoredItemBase + { + private StatusCode statusCode; + + public ValueMonitoredItem(object target, PropertyInfo property, ExpandedNodeId nodeId, uint attributeId = 13, string? indexRange = null, MonitoringMode monitoringMode = MonitoringMode.Reporting, int samplingInterval = -1, MonitoringFilter? filter = null, uint queueSize = 0, bool discardOldest = true) + : base(property.Name, nodeId, attributeId, indexRange, monitoringMode, samplingInterval, filter, queueSize, discardOldest) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + Target = target; + Property = property; + } + + /// + /// Gets the target object. + /// + public object Target { get; } + + /// + /// Gets the property of the target to store the published value. + /// + public PropertyInfo Property { get; } + + public override void Publish(DataValue dataValue) + { + var value = dataValue.GetValueOrDefault(); + Property.SetValue(Target, value); + SetDataErrorInfo(dataValue.StatusCode); + } + + public override void Publish(Variant[] eventFields) + { + } + + public override bool TryGetValue(out DataValue? value) + { + var pi = Property; + if (pi.CanRead) + { + value = new DataValue(Property.GetValue(Target)); + return true; + } + value = default(DataValue); + return false; + } + + public override void OnCreateResult(MonitoredItemCreateResult result) + { + ServerId = result.MonitoredItemId; + SetDataErrorInfo(result.StatusCode); + } + + public override void OnWriteResult(StatusCode statusCode) + { + SetDataErrorInfo(statusCode); + } + + private void SetDataErrorInfo(StatusCode statusCode) + { + if (this.statusCode == statusCode) + { + return; + } + + this.statusCode = statusCode; + var targetAsDataErrorInfo = Target as ISetDataErrorInfo; + if (targetAsDataErrorInfo != null) + { + if (!StatusCode.IsGood(statusCode)) + { + targetAsDataErrorInfo.SetErrors(Property.Name, new string[] { StatusCodes.GetDefaultMessage(statusCode) }); + } + else + { + targetAsDataErrorInfo.SetErrors(Property.Name, null); + } + } + } + } + /// /// Subscribes to events of an attribute of a node. /// Sets the published event in a property of type BaseEvent or subtype. diff --git a/UaClient/ServiceModel/Ua/SubscriptionBase.cs b/UaClient/ServiceModel/Ua/SubscriptionBase.cs index 1048b5d..984e025 100644 --- a/UaClient/ServiceModel/Ua/SubscriptionBase.cs +++ b/UaClient/ServiceModel/Ua/SubscriptionBase.cs @@ -169,16 +169,18 @@ public SubscriptionBase(UaApplication? application) } } - this.monitoredItems.Add(new ValueMonitoredItem( - target: this, - property: propertyInfo, - nodeId: ExpandedNodeId.Parse(mia.NodeId), - indexRange: mia.IndexRange, - attributeId: mia.AttributeId, - samplingInterval: mia.SamplingInterval, - filter: filter, - queueSize: mia.QueueSize, - discardOldest: mia.DiscardOldest)); + this.monitoredItems.Add((MonitoredItemBase)Activator.CreateInstance( + typeof(ValueMonitoredItem<>).MakeGenericType(propType), + this, + propertyInfo, + ExpandedNodeId.Parse(mia.NodeId), + mia.AttributeId, + mia.IndexRange, + MonitoringMode.Reporting, + mia.SamplingInterval, + filter, + mia.QueueSize, + mia.DiscardOldest)); } diff --git a/UaClient/Workstation.UaClient.csproj b/UaClient/Workstation.UaClient.csproj index 14eca5d..a1bee79 100644 --- a/UaClient/Workstation.UaClient.csproj +++ b/UaClient/Workstation.UaClient.csproj @@ -4,7 +4,7 @@ netstandard2.0;netcoreapp3.1 Workstation.UaClient Workstation - 3.0.1 + 3.1.0 Andrew Cullen Converter Systems LLC https://github.com/convertersystems/opc-ua-client @@ -13,10 +13,10 @@ opc, opc-ua, iiot https://github.com/convertersystems/opc-ua-client Copyright © 2021 Converter Systems LLC. - 3.0.1.0 + 3.1.0.0 Workstation.UaClient ($(TargetFramework)) true - 3.0.1.0 + 3.1.0.0 True Key.snk true @@ -35,7 +35,7 @@ - +