a5c3de63e9
Codec-encoded scalars ("True", "42.5") reached RealOpcUaClient.WriteValuesAsync
as strings and were sent as String Variants, so a Boolean/Double node answered
BadTypeMismatch (0x80740000).
RealOpcUaClient now reads each written node's DataType attribute once per
session (batched, cached, cleared on connect/disconnect; derived types and
enumerations resolved via the supertype chain) and converts non-null scalar
values with the new OpcUaWriteCoercion table: Boolean, SByte..UInt64, Float,
Double, String, DateTime, Guid, all culture-invariant. Overflow, fractional
values to integer types and unparseable strings are refused client-side with
BadTypeMismatch and a message naming the value and target type - never a
rounded value. Arrays, null, unmapped types and nodes whose DataType read
fails pass through unchanged. WriteValueAsync now surfaces the outcome's own
status code instead of always BadNodeIdUnknown. MxGateway path untouched.
Tests: OpcUaWriteCoercionTests (coercion table incl. overflow, invalid and
culture-sensitive strings) and OpcUaWriteCoercionLiveTests (RequiresOpcUa:
String writes to Double/Boolean/UInt32 opc-plc nodes read back typed; reverting
the coercion reproduces 0x80740000). Component-DataConnectionLayer.md updated.
72 lines
3.3 KiB
C#
72 lines
3.3 KiB
C#
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
|
|
|
|
/// <summary>
|
|
/// Gitea #58 live round-trip: a codec-encoded String scalar written to a numeric / Boolean
|
|
/// node of the infra OPC PLC server (<c>opc.tcp://localhost:50000</c>, see
|
|
/// <c>infra/docker-compose.yml</c> + <c>infra/opcua/nodes.json</c>) is coerced to the node's
|
|
/// DataType and reads back as that type. Before the fix the server answered
|
|
/// <c>BadTypeMismatch</c> (0x80740000). Skips when the server is unreachable.
|
|
/// </summary>
|
|
[Trait("Category", "RequiresOpcUa")]
|
|
public class OpcUaWriteCoercionLiveTests
|
|
{
|
|
private const string EndpointUrl = "opc.tcp://localhost:50000";
|
|
|
|
// Writable, non-simulated nodes from infra/opcua/nodes.json (namespace http://microsoft.com/Opc/OpcPlc/).
|
|
private const string DoubleNode = "nsu=http://microsoft.com/Opc/OpcPlc/;s=Motor.Speed";
|
|
private const string BooleanNode = "nsu=http://microsoft.com/Opc/OpcPlc/;s=Motor.Running";
|
|
private const string UInt32Node = "nsu=http://microsoft.com/Opc/OpcPlc/;s=Motor.FaultCode";
|
|
|
|
[SkippableFact]
|
|
public async Task String_writes_to_typed_nodes_are_coerced_and_read_back_typed()
|
|
{
|
|
await using var client = new RealOpcUaClient();
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
|
try
|
|
{
|
|
await client.ConnectAsync(EndpointUrl, new OpcUaConnectionOptions(OperationTimeoutMs: 5000), deadline.Token);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Skip.If(true, $"OPC UA test server unavailable: {ex.Message}");
|
|
return;
|
|
}
|
|
|
|
// Two distinct values per node so the read-back cannot pass on a stale value.
|
|
foreach (var (speed, running, fault) in new[] { ("1234.5", "True", "17"), ("42.25", "False", "0") })
|
|
{
|
|
var outcomes = await client.WriteValuesAsync(
|
|
[(DoubleNode, speed), (BooleanNode, running), (UInt32Node, fault)], deadline.Token);
|
|
|
|
Assert.All(outcomes, o =>
|
|
{
|
|
Assert.Null(o.Error);
|
|
Assert.Equal(0u, o.StatusCode);
|
|
});
|
|
|
|
var reads = await client.ReadValuesAsync([DoubleNode, BooleanNode, UInt32Node], deadline.Token);
|
|
Assert.Equal(double.Parse(speed, System.Globalization.CultureInfo.InvariantCulture),
|
|
Assert.IsType<double>(reads[0].Value));
|
|
Assert.Equal(bool.Parse(running), Assert.IsType<bool>(reads[1].Value));
|
|
Assert.Equal(uint.Parse(fault, System.Globalization.CultureInfo.InvariantCulture),
|
|
Assert.IsType<uint>(reads[2].Value));
|
|
}
|
|
|
|
// A value that does not fit the node type is refused client-side with a clear error,
|
|
// and the node keeps its previous value.
|
|
var bad = await client.WriteValuesAsync([(UInt32Node, "-1"), (DoubleNode, "abc")], deadline.Token);
|
|
Assert.All(bad, o =>
|
|
{
|
|
Assert.Equal(Opc.Ua.StatusCodes.BadTypeMismatch, o.StatusCode);
|
|
Assert.Contains("Cannot convert", o.Error);
|
|
});
|
|
var after = await client.ReadValuesAsync([UInt32Node, DoubleNode], deadline.Token);
|
|
Assert.Equal(0u, after[0].Value);
|
|
Assert.Equal(42.25, after[1].Value);
|
|
|
|
await client.DisconnectAsync();
|
|
}
|
|
}
|