fix(twincat): accept TwinCAT 2 leading-dot globals in TwinCATSymbolPath (ignitionoee 0.19.1)

A TC2 global is named with ONE leading dot (.OPC_HEARTBEAT) and the PLC
requires it (SYM_HNDBYNAME answers 0x710 without it). TryParse rejected the
dot, so the driver fell back to the raw string and lost bit-index parsing:
.X.WORD.3 went to ADS verbatim instead of reading .X.WORD and extracting bit 3.
The dot now stays on the root segment and round-trips through ToAdsSymbolName;
.., ., .[1] and .9x stay invalid.
This commit is contained in:
Joseph Doherty
2026-10-07 13:58:40 -04:00
parent e3d7a8f071
commit b7b754fd98
3 changed files with 58 additions and 2 deletions
@@ -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");
}
}