fix(heidenhain): Tool/Number, Tool/Index, Tool/AxisCode publish their declared integer types (#867)
HeidenhainSession.Leaf's `case Lsv2Tool t` switch expression mixed long, int and double? arms, so C# unified the natural type to double? and Tool/Number (Int64), Tool/Index and Tool/AxisCode (Int32) were published as Double. Every arm in that switch (and in the alarm-list, axes and feature-acquire switches in the same file) is now boxed to object explicitly so no numeric arm can widen another. New HeidenhainLeafRuntimeTypeTests asserts value.GetType() for every catalog value leaf against its declared DataType (scalar + array), plus PLC/DATA binding coercion for Int16/Int32/Int64/Boolean. Verified red on the old code (exactly the three Tool leaves) and green with the fix. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
@@ -256,14 +256,15 @@ internal sealed class HeidenhainSession : IAsyncDisposable
|
||||
await PrepareTelemetryAsync(wire, ct).ConfigureAwait(false);
|
||||
value = feature switch
|
||||
{
|
||||
"Execution" => Lsv2Data.U16(await _client.ReadRuntimeAsync(23, ct).ConfigureAwait(false)),
|
||||
"ProgramState" => Lsv2Data.U16(await _client.ReadRuntimeAsync(26, ct).ConfigureAwait(false)),
|
||||
// Each arm boxed explicitly so no numeric arm can widen another (lmxopcua#867).
|
||||
"Execution" => (object)Lsv2Data.U16(await _client.ReadRuntimeAsync(23, ct).ConfigureAwait(false)),
|
||||
"ProgramState" => (object)Lsv2Data.U16(await _client.ReadRuntimeAsync(26, ct).ConfigureAwait(false)),
|
||||
"Program" => Lsv2Data.Program(await _client.ReadRuntimeAsync(24, ct).ConfigureAwait(false)),
|
||||
"Overrides" => Lsv2Data.Overrides(await _client.ReadRuntimeAsync(25, ct).ConfigureAwait(false)),
|
||||
"Axes" => Lsv2Data.Axes(await _client.ReadRuntimeAsync(22, ct).ConfigureAwait(false)),
|
||||
"Tool" => Lsv2Data.Tool(await _client.ReadRuntimeAsync(51, ct).ConfigureAwait(false)),
|
||||
HeidenhainCatalog.AlarmsFeature => await AlarmsAsync(ct).ConfigureAwait(false),
|
||||
_ => await TimerAsync(feature, ct).ConfigureAwait(false),
|
||||
_ => (object)await TimerAsync(feature, ct).ConfigureAwait(false),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -444,24 +445,27 @@ internal sealed class HeidenhainSession : IAsyncDisposable
|
||||
case double[] o when address.StartsWith("Overrides/", StringComparison.Ordinal):
|
||||
return o[name switch { "Feed" => 0, "Spindle" => 1, _ => 2 }];
|
||||
case Lsv2Axes a:
|
||||
return name == "Names" ? a.Names : a.Positions;
|
||||
return name == "Names" ? (object)a.Names : (object)a.Positions;
|
||||
case Lsv2Tool t:
|
||||
// lmxopcua#867: every arm is cast to object. Without it C# unifies long/int/double? to double?, so
|
||||
// Tool/Number (Int64) and Tool/Index + Tool/AxisCode (Int32) were published as Double. A null
|
||||
// geometry still boxes to null (BadNotSupported upstream).
|
||||
return name switch
|
||||
{
|
||||
"Number" => t.Number,
|
||||
"Index" => t.Index,
|
||||
"AxisCode" => t.Axis,
|
||||
"Length" => t.Length,
|
||||
_ => t.Radius,
|
||||
"Number" => (object)t.Number,
|
||||
"Index" => (object)t.Index,
|
||||
"AxisCode" => (object)t.Axis,
|
||||
"Length" => (object?)t.Length,
|
||||
_ => (object?)t.Radius,
|
||||
};
|
||||
case List<Lsv2Alarm> list:
|
||||
return name switch
|
||||
{
|
||||
"Count" => list.Count,
|
||||
"Messages" => list.Select(x => x.Message).ToArray(),
|
||||
"Classes" => list.Select(x => x.ErrorClass).ToArray(),
|
||||
"Groups" => list.Select(x => x.Group).ToArray(),
|
||||
_ => list.Select(x => x.Number).ToArray(),
|
||||
"Count" => (object)list.Count,
|
||||
"Messages" => (object)list.Select(x => x.Message).ToArray(),
|
||||
"Classes" => (object)list.Select(x => x.ErrorClass).ToArray(),
|
||||
"Groups" => (object)list.Select(x => x.Group).ToArray(),
|
||||
_ => (object)list.Select(x => x.Number).ToArray(),
|
||||
};
|
||||
case int i:
|
||||
return i;
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Heidenhain.Tests.Simulator;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Heidenhain.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// lmxopcua#867: the published value's RUNTIME type must be the CLR type of the leaf's declared data type.
|
||||
/// Shouldly equality cannot catch this (7.0 equals 7L), so every assertion here compares <c>GetType()</c>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class HeidenhainLeafRuntimeTypeTests
|
||||
{
|
||||
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
||||
|
||||
private static Type ClrType(DriverDataType type, bool isArray)
|
||||
{
|
||||
var scalar = type switch
|
||||
{
|
||||
DriverDataType.Boolean => typeof(bool),
|
||||
DriverDataType.Int16 => typeof(short),
|
||||
DriverDataType.Int32 => typeof(int),
|
||||
DriverDataType.Int64 => typeof(long),
|
||||
DriverDataType.Float64 => typeof(double),
|
||||
DriverDataType.String => typeof(string),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
|
||||
};
|
||||
return isArray ? scalar.MakeArrayType() : scalar;
|
||||
}
|
||||
|
||||
/// <summary>Every fixed value leaf (capability strings excluded), with its declared type.</summary>
|
||||
public static TheoryData<string> CatalogValueLeaves()
|
||||
{
|
||||
var data = new TheoryData<string>();
|
||||
foreach (var e in HeidenhainCatalog.Entries.Where(e => !e.IsCapability)) data.Add(e.Address);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static SimulatedControl FullyPopulatedControl()
|
||||
{
|
||||
var c = new SimulatedControl();
|
||||
c.Runtime[22] = () => SimReply.Ok("S_RI", [0, 1, .. Encoding.ASCII.GetBytes("1.5\0X\0")]);
|
||||
c.Runtime[23] = () => SimReply.Ok("S_RI", [0, 4]);
|
||||
c.Runtime[26] = () => SimReply.Ok("S_RI", [0, 7]);
|
||||
c.Runtime[24] = () => SimReply.Ok("S_RI", [0, 0, 0, 5, .. "A\0B\0"u8.ToArray()]);
|
||||
c.Runtime[25] = () => SimReply.Ok("S_RI", new byte[12]);
|
||||
var alarm = new byte[10];
|
||||
BinaryPrimitives.WriteInt16BigEndian(alarm, 3);
|
||||
BinaryPrimitives.WriteInt16BigEndian(alarm.AsSpan(2), 4);
|
||||
BinaryPrimitives.WriteInt32BigEndian(alarm.AsSpan(4), 12);
|
||||
alarm[8] = 65;
|
||||
c.Runtime[27] = () => SimReply.Ok("S_RI", alarm);
|
||||
c.Runtime[28] = () => SimReply.Refuse(57);
|
||||
// 24-byte tool reply: number 49, index 1, axis 2, length 100.5, radius 3.25 (float64le).
|
||||
var tool = new byte[24];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(tool, 49);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(tool.AsSpan(4), 1);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(tool.AsSpan(6), 2);
|
||||
BinaryPrimitives.WriteDoubleLittleEndian(tool.AsSpan(8), 100.5);
|
||||
BinaryPrimitives.WriteDoubleLittleEndian(tool.AsSpan(16), 3.25);
|
||||
c.Runtime[51] = () => SimReply.Ok("S_RI", tool);
|
||||
for (var s = 30; s <= 34; s++) c.Runtime[s] = () => SimReply.Ok("S_RI", [0, 0, 1, 0]);
|
||||
return c;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CatalogValueLeaves))]
|
||||
public async Task Catalog_leaf_publishes_the_clr_type_of_its_declared_data_type(string address)
|
||||
{
|
||||
HeidenhainCatalog.TryGet(address, out var entry).ShouldBeTrue();
|
||||
var control = FullyPopulatedControl();
|
||||
await using var sim = Lsv2Simulator.WithControl(control);
|
||||
var client = new Lsv2.Lsv2Client("127.0.0.1", sim.Port, TimeSpan.FromSeconds(1));
|
||||
var tag = TestTags.Def(address);
|
||||
var settings = new HeidenhainSessionSettings(true, true, false, 64, true, false, false);
|
||||
await using var session = new HeidenhainSession(client, settings, (_, _) => ValueTask.FromResult(""));
|
||||
await client.OpenAsync(Ct);
|
||||
await session.InitializeAsync(Ct);
|
||||
|
||||
var sample = (await session.ReadBatchAsync([tag], Ct))[0];
|
||||
|
||||
sample.Status.ShouldBe(HeidenhainStatus.Good, address);
|
||||
sample.Value.ShouldNotBeNull(address);
|
||||
sample.Value.GetType().ShouldBe(ClrType(entry!.DataType, entry.IsArray), address);
|
||||
control.Violations.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_integer_catalog_leaf_is_covered()
|
||||
{
|
||||
// Pins the theory's reach: the integer leaves #867 broke are in the data set.
|
||||
var leaves = CatalogValueLeaves().Select(r => r.Data).ToList();
|
||||
leaves.ShouldContain("Tool/Number");
|
||||
leaves.ShouldContain("Tool/Index");
|
||||
leaves.ShouldContain("Tool/AxisCode");
|
||||
leaves.ShouldContain("Alarms/Count");
|
||||
leaves.ShouldContain("Program/Line");
|
||||
leaves.ShouldContain("Status/ExecutionState");
|
||||
leaves.Count(l => l.StartsWith("Timers/", StringComparison.Ordinal)).ShouldBe(HeidenhainCatalog.TimerNames.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(DriverDataType.Int16, 42)]
|
||||
[InlineData(DriverDataType.Int32, 42)]
|
||||
[InlineData(DriverDataType.Int64, 42)]
|
||||
[InlineData(DriverDataType.Int16, 42L)]
|
||||
[InlineData(DriverDataType.Int32, 42L)]
|
||||
[InlineData(DriverDataType.Int64, 42L)]
|
||||
[InlineData(DriverDataType.Int16, 42.0)]
|
||||
[InlineData(DriverDataType.Int32, 42.0)]
|
||||
[InlineData(DriverDataType.Int64, 42.0)]
|
||||
[InlineData(DriverDataType.Boolean, true)]
|
||||
[InlineData(DriverDataType.Boolean, false)]
|
||||
public void Binding_coercion_publishes_the_clr_type_of_the_declared_data_type(DriverDataType type, object native)
|
||||
{
|
||||
HeidenhainBindingCodec.Coerce(native, type).GetType().ShouldBe(ClrType(type, false));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(HeidenhainPlcArea.Byte, new byte[] { 7 }, DriverDataType.Int16)]
|
||||
[InlineData(HeidenhainPlcArea.Byte, new byte[] { 7 }, DriverDataType.Int32)]
|
||||
[InlineData(HeidenhainPlcArea.Word, new byte[] { 7, 0 }, DriverDataType.Int64)]
|
||||
[InlineData(HeidenhainPlcArea.Dword, new byte[] { 7, 0, 0, 0 }, DriverDataType.Int32)]
|
||||
[InlineData(HeidenhainPlcArea.InputWord, new byte[] { 7, 0 }, DriverDataType.Int32)]
|
||||
[InlineData(HeidenhainPlcArea.Marker, new byte[] { 1 }, DriverDataType.Boolean)]
|
||||
public void Plc_binding_decode_publishes_the_clr_type_of_the_declared_data_type(HeidenhainPlcArea area, byte[] bytes, DriverDataType type)
|
||||
{
|
||||
HeidenhainBindingCodec.DecodePlc(area, bytes, type).GetType().ShouldBe(ClrType(type, false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user