Merge TwinCAT fixes ported from ignitionoee + live-validated on the Elma PLCs
This commit is contained in:
@@ -116,6 +116,9 @@ internal class FakeTwinCATClient : ITwinCATClient
|
||||
public List<FakeNotification> Notifications { get; } = new();
|
||||
/// <summary>Gets or sets a value indicating whether AddNotificationAsync should throw.</summary>
|
||||
public bool ThrowOnAddNotification { get; set; }
|
||||
/// <summary>Per-symbol exceptions <see cref="AddNotificationAsync"/> throws (e.g. a
|
||||
/// <see cref="TwinCATTypeMismatchException"/> for one refused tag).</summary>
|
||||
public Dictionary<string, Exception> AddNotificationErrors { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
/// <summary>Records the most recently-supplied <c>maxDelayMs</c> for Driver.TwinCAT-014 tests.</summary>
|
||||
public int LastMaxDelayMs { get; private set; }
|
||||
/// <summary>When set, <see cref="AddNotificationAsync"/> awaits this gate before creating the handle —
|
||||
@@ -137,6 +140,8 @@ internal class FakeTwinCATClient : ITwinCATClient
|
||||
{
|
||||
if (ThrowOnAddNotification)
|
||||
throw Exception ?? new InvalidOperationException("fake AddNotification failure");
|
||||
if (AddNotificationErrors.TryGetValue(symbolPath, out var perSymbol))
|
||||
throw perSymbol;
|
||||
|
||||
if (AddNotificationGate is { } gate)
|
||||
await gate.Task.ConfigureAwait(false);
|
||||
|
||||
+45
@@ -128,6 +128,51 @@ public sealed class TwinCATNativeNotificationTests
|
||||
fake.Notifications.Count.ShouldBe(0); // partial handle cleaned up
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A tag whose declared type cannot decode its PLC symbol (ignitionoee TwinCAT-002/-008) is refused
|
||||
/// by the client with <see cref="TwinCATTypeMismatchException"/>. The refusal is ISOLATED: that
|
||||
/// one tag goes Bad with <c>BadTypeMismatch</c> and every other tag in the same subscribe call
|
||||
/// still registers — before, the exception tore the whole subscription down.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Native_subscribe_isolates_a_type_mismatched_tag_to_BadTypeMismatch()
|
||||
{
|
||||
var fake = new FakeTwinCATClient();
|
||||
fake.AddNotificationErrors["MAIN.Order"] =
|
||||
new TwinCATTypeMismatchException("MAIN.Order", "declared DInt but the PLC symbol is the string STRING(3)");
|
||||
var factory = new FakeTwinCATClientFactory { Customise = () => fake };
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
|
||||
RawTags = TwinCATRawTags.Entries(
|
||||
[
|
||||
new TwinCATTagDefinition("Count", "ads://5.23.91.23.1.1:851", "MAIN.Count", TwinCATDataType.DInt),
|
||||
new TwinCATTagDefinition("Order", "ads://5.23.91.23.1.1:851", "MAIN.Order", TwinCATDataType.DInt),
|
||||
new TwinCATTagDefinition("Speed", "ads://5.23.91.23.1.1:851", "MAIN.Speed", TwinCATDataType.Real),
|
||||
]),
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = true,
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var events = new ConcurrentQueue<DataChangeEventArgs>();
|
||||
drv.OnDataChange += (_, e) => events.Enqueue(e);
|
||||
|
||||
var handle = await drv.SubscribeAsync(["Count", "Order", "Speed"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
|
||||
|
||||
fake.Notifications.Select(n => n.SymbolPath).ShouldBe(["MAIN.Count", "MAIN.Speed"], ignoreOrder: true);
|
||||
var refused = events.ShouldHaveSingleItem();
|
||||
refused.FullReference.ShouldBe("Order");
|
||||
refused.Snapshot.StatusCode.ShouldBe(TwinCATStatusMapper.BadTypeMismatch);
|
||||
refused.Snapshot.Value.ShouldBeNull();
|
||||
|
||||
// The good tags still deliver, and unsubscribe tears down cleanly.
|
||||
fake.FireNotification("MAIN.Count", 7);
|
||||
events.Last().Snapshot.Value.ShouldBe(7);
|
||||
await drv.UnsubscribeAsync(handle, CancellationToken.None);
|
||||
fake.Notifications.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
private sealed class AbTagParamsIrrelevant { }
|
||||
|
||||
private sealed class FailAfterNAddsFake : FakeTwinCATClient
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins <see cref="TwinCATStringCodec"/> — how an IEC <c>STRING(n)</c> / <c>WSTRING(n)</c> buffer,
|
||||
/// read at the PLC's own declared width, becomes a value. Ported from the ignitionoee TwinCAT
|
||||
/// driver (0.19.3 first-NUL STRING, 0.19.4 TwinCAT-007 first-NUL WSTRING and TwinCAT-010
|
||||
/// Windows-1252), whose byte fixtures these reuse.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TwinCATStringCodecTests
|
||||
{
|
||||
/// <summary>
|
||||
/// An IEC STRING ends at its FIRST NUL; the buffer beyond keeps whatever a longer earlier value
|
||||
/// left there. Live on the Elma cleaner Z33856 (2026-09-24) a decoder that only trimmed trailing
|
||||
/// NULs published work order <c>67504440</c> as <c>"67504440\0" + "1"</c> and part
|
||||
/// <c>110032285</c> as <c>"110032285\0" + "1\0" + "30"</c> — a wrong value at Good quality.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void String_stops_at_the_first_nul_and_ignores_stale_bytes_after_it()
|
||||
{
|
||||
byte[] workOrder = [(byte)'6', (byte)'7', (byte)'5', (byte)'0', (byte)'4', (byte)'4', (byte)'4', (byte)'0',
|
||||
0x00, (byte)'1', 0x00, (byte)'3', (byte)'0', 0x00];
|
||||
TwinCATStringCodec.DecodeString(workOrder, utf8: false).ShouldBe("67504440");
|
||||
|
||||
byte[] emptied = [0x00, (byte)'O', (byte)'L', (byte)'D'];
|
||||
TwinCATStringCodec.DecodeString(emptied, utf8: false).ShouldBe("");
|
||||
}
|
||||
|
||||
/// <summary>A buffer with no NUL at all (a full-length value) decodes whole.</summary>
|
||||
[Fact]
|
||||
public void String_without_a_nul_decodes_the_whole_buffer()
|
||||
=> TwinCATStringCodec.DecodeString("ABC"u8.ToArray(), utf8: false).ShouldBe("ABC");
|
||||
|
||||
/// <summary>
|
||||
/// An IEC STRING on TwinCAT is single-byte Windows-1252, not ASCII and not the host's culture
|
||||
/// code page: <c>°</c> (0xB0, an Elma alarm text), <c>ä</c> (0xE4) and <c>€</c> (0x80, the one
|
||||
/// byte where 1252 and ISO-8859-1 differ) must decode to themselves on any host.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void String_decodes_windows_1252_high_bytes()
|
||||
{
|
||||
byte[] text = [(byte)'8', (byte)'0', 0xB0, (byte)'C', (byte)' ', 0xE4, 0x80, 0x00, 0xFF];
|
||||
TwinCATStringCodec.DecodeString(text, utf8: false).ShouldBe("80°C ä€");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A TwinCAT 3.1 build 4026+ project with "UTF8 Encoding for STRING" on reports UTF-8 string
|
||||
/// data; the codec honours it (still ending at the first NUL).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void String_decodes_utf8_when_the_target_reports_utf8_strings()
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes("80°C ä€").Concat(new byte[] { 0x00, (byte)'X' }).ToArray();
|
||||
TwinCATStringCodec.DecodeString(bytes, utf8: true).ShouldBe("80°C ä€");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A WSTRING ends at its first ALIGNED <c>U+0000</c> code unit (TwinCAT-007): <c>"NEW\0LD"</c>
|
||||
/// must publish <c>"NEW"</c>. A zero HIGH byte of a non-NUL unit (every ASCII character in
|
||||
/// UTF-16LE) is not a terminator.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WString_stops_at_the_first_aligned_nul_code_unit()
|
||||
{
|
||||
var bytes = Encoding.Unicode.GetBytes("NEW\0LD\0");
|
||||
TwinCATStringCodec.DecodeWString(bytes).ShouldBe("NEW");
|
||||
TwinCATStringCodec.DecodeWString(Encoding.Unicode.GetBytes("Grüße")).ShouldBe("Grüße");
|
||||
}
|
||||
|
||||
/// <summary>A trailing odd byte is not half a character — it is ignored.</summary>
|
||||
[Fact]
|
||||
public void WString_ignores_a_trailing_odd_byte()
|
||||
{
|
||||
var bytes = Encoding.Unicode.GetBytes("AB").Concat(new byte[] { 0x43 }).ToArray();
|
||||
TwinCATStringCodec.DecodeWString(bytes).ShouldBe("AB");
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,12 @@ public sealed class TwinCATSymbolPathTests
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(".Motor")] // leading dot
|
||||
[InlineData(".")] // a bare dot names nothing
|
||||
[InlineData("..Motor")] // TwinCAT 2 globals carry ONE leading dot, never two
|
||||
[InlineData(".[1]")] // leading dot then no identifier
|
||||
[InlineData(".9x")] // leading dot then an ident starting with a digit
|
||||
[InlineData(".x.")] // leading dot, trailing dot
|
||||
[InlineData(" . x")] // whitespace between the dot and the identifier
|
||||
[InlineData("Motor.")] // trailing dot
|
||||
[InlineData("Motor.[0]")] // empty segment
|
||||
[InlineData("1bad")] // ident starts with digit
|
||||
@@ -148,4 +153,42 @@ public sealed class TwinCATSymbolPathTests
|
||||
parsed.ToAdsSymbolName().ShouldBe(c);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TwinCAT 2 names a GLOBAL variable with ONE leading dot (<c>.OPC_HEARTBEAT</c>) and the PLC
|
||||
/// requires it: on a real TC 2.11 CX (Elma cleaner Z33856, 2026-09-24) <c>SYM_HNDBYNAME</c>
|
||||
/// answered a handle for <c>.OPC_HEARTBEAT</c> and 0x710 (symbol not found) for
|
||||
/// <c>OPC_HEARTBEAT</c>. The parser rejected the dot, so a TC2 global fell back to the raw
|
||||
/// string — losing bit-index parsing (<c>.X.WORD.3</c> was sent to ADS verbatim).
|
||||
/// Ported from ignitionoee driver 0.19.1.
|
||||
/// </summary>
|
||||
/// <param name="input">A TwinCAT 2 global path as the PLC names it.</param>
|
||||
[Theory]
|
||||
[InlineData(".x")]
|
||||
[InlineData(".OPC_HEARTBEAT")]
|
||||
[InlineData(".OPC_MACHINEDATA.PROCESSSTATUS")]
|
||||
[InlineData(".ARRCARRIERDATA_V[1].SWORKORDER")]
|
||||
[InlineData(".OPC_LOGDATA.STATIONDATA[5].TEMPERATUREMAXIMUM")]
|
||||
public void TwinCAT2_leading_dot_global_parses_and_keeps_its_dot(string input)
|
||||
{
|
||||
var p = TwinCATSymbolPath.TryParse(input);
|
||||
p.ShouldNotBeNull(input);
|
||||
p.Segments[0].Name.ShouldStartWith(".");
|
||||
p.ToAdsSymbolName().ShouldBe(input);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The leading dot belongs to the ROOT segment only, so bit access on a TC2 global still parses:
|
||||
/// the parent word is read as <c>.OPC_STATUS.WORD</c> and bit 3 is extracted locally.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TwinCAT2_global_bit_access_parses_with_the_dot_on_the_root_segment()
|
||||
{
|
||||
var p = TwinCATSymbolPath.TryParse(".OPC_STATUS.WORD.3");
|
||||
p.ShouldNotBeNull();
|
||||
p.Segments.Select(s => s.Name).ShouldBe([".OPC_STATUS", "WORD"]);
|
||||
p.BitIndex.ShouldBe(3);
|
||||
(p with { BitIndex = null }).ToAdsSymbolName().ShouldBe(".OPC_STATUS.WORD");
|
||||
p.ToAdsSymbolName().ShouldBe(".OPC_STATUS.WORD.3");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins <c>AdsTwinCATClient.MapSymbolTypeName</c> — the symbol-browse type mapper — against the type
|
||||
/// names a real TwinCAT 2 PLC reports. A name the mapper does not know drops the symbol from
|
||||
/// discovery silently, so every TC2 spelling below was a symbol the browse never offered.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TwinCATSymbolTypeNameTests
|
||||
{
|
||||
/// <summary>
|
||||
/// TwinCAT 2 reports top-level integer globals by their ADS base-type name, not the IEC name:
|
||||
/// the Elma cleaner Z33856 (TC 2.11, symbol upload 2026-09-24) lists 233 symbols as
|
||||
/// <c>INT16</c> and 121 as <c>UINT16</c> (e.g. <c>.ACTUALOPERATIONMODE</c>). The Beckhoff
|
||||
/// type system carries these as built-in primitives (<c>AdsSymbolProvider.CreateBuildInTypes</c>:
|
||||
/// INT16, UINT16, INT32, UINT32, UINT8, FLOAT, DOUBLE), so <c>IDataType.Name</c> arrives spelled
|
||||
/// that way and must map like its IEC twin.
|
||||
/// </summary>
|
||||
/// <param name="typeName">The type name as the PLC / Beckhoff type system reports it.</param>
|
||||
/// <param name="expected">The driver atomic type it must map to.</param>
|
||||
[Theory]
|
||||
[InlineData("INT16", TwinCATDataType.Int)]
|
||||
[InlineData("UINT16", TwinCATDataType.UInt)]
|
||||
[InlineData("INT32", TwinCATDataType.DInt)]
|
||||
[InlineData("UINT32", TwinCATDataType.UDInt)]
|
||||
[InlineData("UINT8", TwinCATDataType.USInt)]
|
||||
[InlineData("FLOAT", TwinCATDataType.Real)]
|
||||
[InlineData("DOUBLE", TwinCATDataType.LReal)]
|
||||
public void TwinCAT2_ads_base_type_names_map_like_their_iec_twins(string typeName, TwinCATDataType expected)
|
||||
=> AdsTwinCATClient.MapSymbolTypeName(typeName).ShouldBe(expected);
|
||||
|
||||
/// <summary>
|
||||
/// The same TC2 data-type upload spells struct members with a trailing blank — <c>"REAL "</c>
|
||||
/// (54 members on Z33856) and <c>"DINT "</c> (24) — so the name is trimmed before matching.
|
||||
/// </summary>
|
||||
/// <param name="typeName">A padded type name.</param>
|
||||
/// <param name="expected">The driver atomic type it must map to.</param>
|
||||
[Theory]
|
||||
[InlineData("REAL ", TwinCATDataType.Real)]
|
||||
[InlineData("DINT ", TwinCATDataType.DInt)]
|
||||
[InlineData(" STRING(80) ", TwinCATDataType.String)]
|
||||
public void Padded_type_names_are_trimmed(string typeName, TwinCATDataType expected)
|
||||
=> AdsTwinCATClient.MapSymbolTypeName(typeName).ShouldBe(expected);
|
||||
|
||||
/// <summary>The IEC spellings, sized strings and unsupported names are unchanged.</summary>
|
||||
[Fact]
|
||||
public void Existing_iec_mappings_are_unchanged()
|
||||
{
|
||||
AdsTwinCATClient.MapSymbolTypeName("INT").ShouldBe(TwinCATDataType.Int);
|
||||
AdsTwinCATClient.MapSymbolTypeName("STRING(8)").ShouldBe(TwinCATDataType.String);
|
||||
AdsTwinCATClient.MapSymbolTypeName("WSTRING(20)").ShouldBe(TwinCATDataType.WString);
|
||||
AdsTwinCATClient.MapSymbolTypeName("ST_LOGDATA").ShouldBeNull();
|
||||
AdsTwinCATClient.MapSymbolTypeName("INT (VAR_IN_OUT)").ShouldBeNull();
|
||||
AdsTwinCATClient.MapSymbolTypeName(null).ShouldBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using Shouldly;
|
||||
using TwinCAT.TypeSystem;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins <see cref="TwinCATTypeGuard"/> — whether a tag's DECLARED <see cref="TwinCATDataType"/> may
|
||||
/// decode the PLC symbol it names. Ported from the ignitionoee TwinCAT driver's extra-type
|
||||
/// reconciliation (TwinCAT-002 width / float kind, TwinCAT-008 string kind): a refused pair leaves
|
||||
/// the node Bad (<c>BadTypeMismatch</c>) instead of publishing the PLC's bytes decoded as the wrong
|
||||
/// type at Good quality.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TwinCATTypeGuardTests
|
||||
{
|
||||
private static TwinCATPlcSymbolShape Plc(string typeName, int byteSize, DataTypeCategory? category = null)
|
||||
=> TwinCATPlcSymbolShape.Classify(typeName, category, byteSize);
|
||||
|
||||
// ---- classification ----
|
||||
|
||||
/// <summary>The PLC's type name / category classifies into the four kinds the guard reasons about.</summary>
|
||||
[Fact]
|
||||
public void Classify_recognises_string_kinds_scalars_and_containers()
|
||||
{
|
||||
Plc("STRING(80)", 81).Kind.ShouldBe(TwinCATPlcKind.String);
|
||||
Plc("STRING", 81).Kind.ShouldBe(TwinCATPlcKind.String);
|
||||
Plc("WSTRING(20)", 42).Kind.ShouldBe(TwinCATPlcKind.WString);
|
||||
Plc("T_MaxString", 256, DataTypeCategory.String).Kind.ShouldBe(TwinCATPlcKind.String);
|
||||
Plc("STRING_DATA", 40).Kind.ShouldBe(TwinCATPlcKind.Scalar); // a UDT named STRING_… is not a string
|
||||
Plc("ST_LOGDATA", 986, DataTypeCategory.Struct).Kind.ShouldBe(TwinCATPlcKind.NonScalar);
|
||||
Plc("ARRAY [0..12] OF DINT", 52).Kind.ShouldBe(TwinCATPlcKind.NonScalar);
|
||||
Plc("INT (VAR_IN_OUT)", 4).Kind.ShouldBe(TwinCATPlcKind.NonScalar);
|
||||
Plc("POINTER TO BYTE", 4, DataTypeCategory.Pointer).Kind.ShouldBe(TwinCATPlcKind.NonScalar);
|
||||
Plc("REAL", 4).IsFloat.ShouldBeTrue();
|
||||
Plc("INT16", 2).IsFloat.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Live on the TwinCAT 2.11 Elma cleaner Z33856 (2026-10-07): for a dotted MEMBER path the
|
||||
/// Beckhoff library reports the symbol's <c>ByteSize</c> as if the PLC's byte count were a bit
|
||||
/// count — <c>.OPC_MACHINEDATA.PROCESSSTATUS</c> (STRING(80)) came back as 11 bytes, an
|
||||
/// <c>INT</c> member as 1, a <c>UDINT</c> as 1 — while its resolved <c>DataType.ByteSize</c> was
|
||||
/// the true 81 / 2 / 4 (raw <c>SYM_INFOBYNAMEEX</c> answers 81). Sizing a string read by the
|
||||
/// symbol truncated <c>"ready for operation"</c> to <c>"ready for o"</c> at Good, and the
|
||||
/// notification width check refused every numeric member. The resolved type's size wins.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Classify_prefers_the_resolved_data_type_size_over_the_symbol_size()
|
||||
{
|
||||
TwinCATPlcSymbolShape.Classify("STRING(80)", DataTypeCategory.String, byteSize: 11, dataTypeByteSize: 81)
|
||||
.ByteSize.ShouldBe(81);
|
||||
var member = TwinCATPlcSymbolShape.Classify("INT", DataTypeCategory.Primitive, byteSize: 1, dataTypeByteSize: 2);
|
||||
member.ByteSize.ShouldBe(2);
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.Int, member).ShouldBeNull();
|
||||
TwinCATPlcSymbolShape.Classify("UDINT", DataTypeCategory.Primitive, byteSize: 1, dataTypeByteSize: 4).ByteSize.ShouldBe(4);
|
||||
|
||||
// No resolved type (or a zero size) falls back to the symbol's own size.
|
||||
TwinCATPlcSymbolShape.Classify("STRING(30)", null, byteSize: 31, dataTypeByteSize: null).ByteSize.ShouldBe(31);
|
||||
TwinCATPlcSymbolShape.Classify("STRING(30)", null, byteSize: 31, dataTypeByteSize: 0).ByteSize.ShouldBe(31);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Live on Z33856 (2026-10-07): <c>P_INITINPUT_ACTION.SLOTID</c> is a TwinCAT 2
|
||||
/// <c>STRING(20) (VAR_IN_OUT)</c> — a 4-byte reference — but the Beckhoff library resolves its
|
||||
/// <c>DataType</c> to the plain <c>STRING(20)</c> (category String). Classifying by the resolved
|
||||
/// type alone let a String tag read 21 bytes starting at the pointer and publish them as text at
|
||||
/// Good (<c>"n±\u008dâa»‰â…"</c>). The symbol's own type name marks it a reference, and that wins.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Classify_treats_a_var_in_out_symbol_as_non_scalar_even_when_its_resolved_type_is_a_string()
|
||||
{
|
||||
var shape = TwinCATPlcSymbolShape.Classify("STRING(20)", DataTypeCategory.String, byteSize: 4,
|
||||
dataTypeByteSize: 21, symbolTypeName: "STRING(20) (VAR_IN_OUT)");
|
||||
shape.Kind.ShouldBe(TwinCATPlcKind.NonScalar);
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.String, shape).ShouldNotBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.String, shape).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// ---- read path (symbol-aware: only the string kind can go wrong) ----
|
||||
|
||||
/// <summary>
|
||||
/// TwinCAT-008: before the fix only width separated a string from a number, so a numeric declared
|
||||
/// over a PLC string decoded the string's bytes as a number at Good. The Beckhoff read path hands
|
||||
/// a declared-numeric tag over a PLC <c>STRING(3)</c> back a <c>string</c> object at Good — refused.
|
||||
/// </summary>
|
||||
/// <param name="declared">The tag's declared type.</param>
|
||||
[Theory]
|
||||
[InlineData(TwinCATDataType.DInt)]
|
||||
[InlineData(TwinCATDataType.UDInt)]
|
||||
[InlineData(TwinCATDataType.Time)]
|
||||
[InlineData(TwinCATDataType.Int)]
|
||||
[InlineData(TwinCATDataType.LInt)]
|
||||
public void Read_refuses_a_numeric_declared_over_a_plc_string(TwinCATDataType declared)
|
||||
=> TwinCATTypeGuard.CheckRead(declared, Plc("STRING(3)", 4)).ShouldNotBeNull();
|
||||
|
||||
/// <summary>A string declared over a number, the other string kind, or a container is refused.</summary>
|
||||
[Fact]
|
||||
public void Read_refuses_a_string_over_anything_but_the_same_string_kind()
|
||||
{
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.String, Plc("DINT", 4)).ShouldNotBeNull();
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.WString, Plc("STRING(161)", 162)).ShouldNotBeNull();
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.String, Plc("WSTRING(80)", 162)).ShouldNotBeNull();
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.String, Plc("ST_LOGDATA", 81, DataTypeCategory.Struct)).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 0.19.2: a STRING / WSTRING tag matches a PLC <c>STRING(n)</c> / <c>WSTRING(n)</c> of ANY length.
|
||||
/// A numeric-over-numeric pair is left to the symbol-aware read, which converts the value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Read_accepts_same_kind_strings_of_any_length_and_numeric_pairs()
|
||||
{
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.String, Plc("STRING(8)", 9)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.String, Plc("STRING(255)", 256)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.WString, Plc("WSTRING(20)", 42)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.DInt, Plc("REAL", 4)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckRead(TwinCATDataType.DInt, Plc("E_STATE", 2, DataTypeCategory.Enum)).ShouldBeNull();
|
||||
}
|
||||
|
||||
// ---- notification path (raw bytes decoded as the declared type) ----
|
||||
|
||||
/// <summary>
|
||||
/// TwinCAT-002: an ADS notification carries raw bytes decoded as the DECLARED type, so a same-width
|
||||
/// float/integer clash reinterprets the bit pattern (DINT over REAL) and a width mismatch truncates
|
||||
/// (DINT over LINT) — both at Good before the guard.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Notification_refuses_width_and_float_kind_mismatches()
|
||||
{
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.DInt, Plc("REAL", 4)).ShouldNotBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.Real, Plc("DINT", 4)).ShouldNotBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.DInt, Plc("LINT", 8)).ShouldNotBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.LReal, Plc("REAL", 4)).ShouldNotBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.DInt, Plc("ARRAY [0..12] OF DINT", 52)).ShouldNotBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.DInt, Plc("STRING(3)", 4)).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A same-width, same-kind divergence (DINT vs UDINT, TIME vs a 4-byte integer, a TC2 INT16) is
|
||||
/// tolerated so a valid config is never refused; strings of the same kind match at any length.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Notification_tolerates_same_width_same_kind_and_same_kind_strings()
|
||||
{
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.UDInt, Plc("DINT", 4)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.Time, Plc("UDINT", 4)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.Int, Plc("INT16", 2)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.Bool, Plc("BOOL", 1)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.Real, Plc("REAL", 4)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.String, Plc("STRING(14)", 15)).ShouldBeNull();
|
||||
TwinCATTypeGuard.CheckNotification(TwinCATDataType.WString, Plc("WSTRING(20)", 42)).ShouldBeNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user