Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATStringCodec.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

70 lines
3.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text;
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
/// <summary>
/// Decodes an IEC <c>STRING(n)</c> / <c>WSTRING(n)</c> buffer read at the PLC's own declared width.
/// The driver reads string symbols as raw bytes and decodes them here instead of letting the
/// Beckhoff marshaller pick an encoding, because that marshaller falls back to the HOST's ANSI code
/// page (<c>CultureInfo.CurrentCulture.TextInfo.ANSICodePage</c>) for a non-UTF-8 target — so a
/// TwinCAT 2 string would decode differently on a Polish (1250) or Cyrillic (1251) server.
/// </summary>
/// <remarks>
/// Two rules, both ported from the ignitionoee TwinCAT driver and both found on a real PLC:
/// <list type="bullet">
/// <item>A value ENDS at its first NUL — <c>0x00</c> for a STRING, an aligned <c>0x00 0x00</c>
/// code unit for a WSTRING. Whatever follows is stale content a longer earlier value left in the
/// buffer (live on the Elma cleaner Z33856: work order <c>67504440</c> read back with a stale
/// <c>"\0" + "1"</c> tail).</item>
/// <item>A STRING is single-byte <b>Windows-1252</b> (umlauts, <c>°</c> in alarm texts), unless
/// the target reports UTF-8 string data (TwinCAT 3.1 4026+ "UTF8 Encoding for STRING").
/// ISO-8859-1 is the fallback on a runtime without code-page support — identical outside
/// 0x80–0x9F.</item>
/// </list>
/// </remarks>
internal static class TwinCATStringCodec
{
private static readonly Encoding Windows1252 = CreateWindows1252();
private static Encoding CreateWindows1252()
{
try
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return Encoding.GetEncoding(1252);
}
catch (Exception)
{
return Encoding.Latin1;
}
}
/// <summary>Decodes an IEC <c>STRING(n)</c> buffer: ends at the first NUL, Windows-1252 unless <paramref name="utf8"/>.</summary>
/// <param name="bytes">The raw symbol bytes (n+1 for a <c>STRING(n)</c>).</param>
/// <param name="utf8">True when the target reports UTF-8 string data.</param>
/// <returns>The decoded value.</returns>
public static string DecodeString(ReadOnlySpan<byte> bytes, bool utf8)
{
var end = bytes.IndexOf((byte)0);
if (end >= 0) bytes = bytes[..end];
return (utf8 ? Encoding.UTF8 : Windows1252).GetString(bytes);
}
/// <summary>Decodes an IEC <c>WSTRING(n)</c> buffer (UTF-16LE): ends at the first aligned <c>U+0000</c>.</summary>
/// <param name="bytes">The raw symbol bytes (2(n+1) for a <c>WSTRING(n)</c>).</param>
/// <returns>The decoded value.</returns>
public static string DecodeWString(ReadOnlySpan<byte> bytes)
{
var end = bytes.Length & ~1; // whole UTF-16 code units only — a trailing odd byte is ignored
for (var i = 0; i + 1 < end; i += 2)
{
if (bytes[i] == 0 && bytes[i + 1] == 0)
{
end = i;
break;
}
}
return Encoding.Unicode.GetString(bytes[..end]);
}
}