using System.Text; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests; /// /// Pins — how an IEC STRING(n) / WSTRING(n) 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. /// [Trait("Category", "Unit")] public sealed class TwinCATStringCodecTests { /// /// 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 67504440 as "67504440\0" + "1" and part /// 110032285 as "110032285\0" + "1\0" + "30" — a wrong value at Good quality. /// [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(""); } /// A buffer with no NUL at all (a full-length value) decodes whole. [Fact] public void String_without_a_nul_decodes_the_whole_buffer() => TwinCATStringCodec.DecodeString("ABC"u8.ToArray(), utf8: false).ShouldBe("ABC"); /// /// An IEC STRING on TwinCAT is single-byte Windows-1252, not ASCII and not the host's culture /// code page: ° (0xB0, an Elma alarm text), ä (0xE4) and € (0x80, the one /// byte where 1252 and ISO-8859-1 differ) must decode to themselves on any host. /// [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 ä€"); } /// /// 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). /// [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 ä€"); } /// /// A WSTRING ends at its first ALIGNED U+0000 code unit (TwinCAT-007): "NEW\0LD" /// must publish "NEW". A zero HIGH byte of a non-NUL unit (every ASCII character in /// UTF-16LE) is not a terminator. /// [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"); } /// A trailing odd byte is not half a character — it is ignored. [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"); } }