Add OPC UA array element write integration test
This commit is contained in:
@@ -363,7 +363,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var value = nodesToWrite[i].Value.WrappedValue.Value;
|
var writeValue = nodesToWrite[i];
|
||||||
|
var value = writeValue.Value.WrappedValue.Value;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(writeValue.IndexRange))
|
||||||
|
{
|
||||||
|
if (!TryApplyArrayElementWrite(tagRef, value, writeValue.IndexRange, out var updatedArray))
|
||||||
|
{
|
||||||
|
errors[i] = new ServiceResult(StatusCodes.BadIndexRangeInvalid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = updatedArray;
|
||||||
|
}
|
||||||
|
|
||||||
var success = _mxAccessClient.WriteAsync(tagRef, value).GetAwaiter().GetResult();
|
var success = _mxAccessClient.WriteAsync(tagRef, value).GetAwaiter().GetResult();
|
||||||
errors[i] = success ? ServiceResult.Good : new ServiceResult(StatusCodes.BadInternalError);
|
errors[i] = success ? ServiceResult.Good : new ServiceResult(StatusCodes.BadInternalError);
|
||||||
}
|
}
|
||||||
@@ -376,6 +389,53 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryApplyArrayElementWrite(string tagRef, object? writeValue, string indexRange, out object updatedArray)
|
||||||
|
{
|
||||||
|
updatedArray = null!;
|
||||||
|
|
||||||
|
if (!int.TryParse(indexRange, out var index) || index < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var currentValue = _mxAccessClient.ReadAsync(tagRef).GetAwaiter().GetResult().Value;
|
||||||
|
if (currentValue is not Array currentArray || currentArray.Rank != 1 || index >= currentArray.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var nextArray = (Array)currentArray.Clone();
|
||||||
|
var elementType = currentArray.GetType().GetElementType();
|
||||||
|
if (elementType == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var normalizedValue = NormalizeIndexedWriteValue(writeValue);
|
||||||
|
nextArray.SetValue(ConvertArrayElementValue(normalizedValue, elementType), index);
|
||||||
|
updatedArray = nextArray;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? NormalizeIndexedWriteValue(object? value)
|
||||||
|
{
|
||||||
|
if (value is Array array && array.Length == 1)
|
||||||
|
return array.GetValue(0);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? ConvertArrayElementValue(object? value, Type elementType)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (elementType.IsValueType)
|
||||||
|
return Activator.CreateInstance(elementType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementType.IsInstanceOfType(value))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
if (elementType == typeof(string))
|
||||||
|
return value.ToString();
|
||||||
|
|
||||||
|
return Convert.ChangeType(value, elementType);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Subscription Delivery
|
#region Subscription Delivery
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
|||||||
public Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default)
|
public Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
WrittenValues.Add((fullTagReference, value));
|
WrittenValues.Add((fullTagReference, value));
|
||||||
|
if (WriteResult)
|
||||||
|
TagValues[fullTagReference] = Vtq.Good(value);
|
||||||
return Task.FromResult(WriteResult);
|
return Task.FromResult(WriteResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,27 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
|||||||
return Session.ReadValue(nodeId);
|
return Session.ReadValue(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write a node's value, optionally using an OPC UA index range for array element writes.
|
||||||
|
/// Returns the server status code for the write.
|
||||||
|
/// </summary>
|
||||||
|
public StatusCode Write(NodeId nodeId, object value, string? indexRange = null)
|
||||||
|
{
|
||||||
|
var nodesToWrite = new WriteValueCollection
|
||||||
|
{
|
||||||
|
new WriteValue
|
||||||
|
{
|
||||||
|
NodeId = nodeId,
|
||||||
|
AttributeId = Attributes.Value,
|
||||||
|
IndexRange = indexRange,
|
||||||
|
Value = new DataValue(new Variant(value))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Session.Write(null, nodesToWrite, out var results, out _);
|
||||||
|
return results[0];
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a subscription with a monitored item on the given node.
|
/// Create a subscription with a monitored item on the given node.
|
||||||
/// Returns the subscription and monitored item for inspection.
|
/// Returns the subscription and monitored item for inspection.
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Opc.Ua;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||||
|
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
|
||||||
|
{
|
||||||
|
public class ArrayWriteTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Write_SingleArrayElement_UpdatesWholeArrayValue()
|
||||||
|
{
|
||||||
|
var fixture = OpcUaServerFixture.WithFakeMxAccessClient();
|
||||||
|
fixture.MxAccessClient!.TagValues["MESReceiver_001.MoveInPartNumbers[]"] = Vtq.Good(
|
||||||
|
Enumerable.Range(0, 50).Select(i => $"PART-{i:00}").ToArray());
|
||||||
|
|
||||||
|
await fixture.InitializeAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new OpcUaTestClient();
|
||||||
|
await client.ConnectAsync(fixture.EndpointUrl);
|
||||||
|
|
||||||
|
// Browse path: TestMachine_001 -> MESReceiver -> MoveInPartNumbers
|
||||||
|
var nodeId = client.MakeNodeId("MESReceiver_001.MoveInPartNumbers[]");
|
||||||
|
|
||||||
|
var before = client.Read(nodeId).Value as string[];
|
||||||
|
before.ShouldNotBeNull();
|
||||||
|
before.Length.ShouldBe(50);
|
||||||
|
before[1].ShouldBe("PART-01");
|
||||||
|
|
||||||
|
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, indexRange: "1");
|
||||||
|
StatusCode.IsGood(status).ShouldBe(true);
|
||||||
|
|
||||||
|
var after = client.Read(nodeId).Value as string[];
|
||||||
|
after.ShouldNotBeNull();
|
||||||
|
after.Length.ShouldBe(50);
|
||||||
|
after[0].ShouldBe("PART-00");
|
||||||
|
after[1].ShouldBe("UPDATED-PART");
|
||||||
|
after[2].ShouldBe("PART-02");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await fixture.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user