Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATStringCodecTests.cs
T
Joseph Doherty 83a314e734 fix(twincat): string subscriptions, Windows-1252 STRING decode, and declared-type kind checks (ignitionoee 0.19.2-0.19.4)
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.
2026-10-07 14:05:55 -04:00

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");
}
}