83a314e734
Port of the ignitionoee TwinCAT string + type-reconciliation fixes onto the Beckhoff-library client: - STRING/WSTRING subscriptions never worked: AddDeviceNotificationExAsync was called with typeof(string) and no length, and the Beckhoff any-type marshaller throws ArgumentNullException for a string without one, so one string tag tore down its whole subscribe call. A string now registers the PLC's own byte width as raw bytes (0.19.2: STRING(n) of any n). - Strings are read as raw bytes and decoded by TwinCATStringCodec. The Beckhoff marshaller already cut a read at the first NUL (0.19.3 / TwinCAT-007 were not defects on the read path); the codec keeps that rule for both STRING and WSTRING and extends it to the new notification path. A STRING now decodes as Windows-1252 (TwinCAT-010) - or UTF-8 when the target reports UTF-8 strings - instead of the host culture's ANSI code page the Beckhoff marshaller falls back to (1250 on a Polish host, 1251 Cyrillic, ...). - TwinCATTypeGuard (TwinCAT-002/-008): a read refuses a numeric tag over a PLC string (and vice versa / STRING vs WSTRING / string over a container) with BadTypeMismatch instead of handing back a string object at Good; a notification - raw bytes decoded as the DECLARED type - additionally refuses width and float/integer mismatches (DINT over REAL reinterpreted the bits). - A refused notification is isolated: that tag publishes BadTypeMismatch with one WARN, keeps a dead intent for the probe-tick retry, and the rest of the subscription registers.
83 lines
3.8 KiB
C#
83 lines
3.8 KiB
C#
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");
|
|
}
|
|
}
|