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
@@ -14,7 +14,8 @@ public sealed class ReadCommand : TwinCATTagCommandBase
/// <summary>Gets or sets the TwinCAT symbol path to read.</summary>
[CommandOption("symbol", 's', Description =
"Symbol path. Program scope: 'MAIN.bStart'. Global: 'GVL.Counter'. " +
"Nested UDT member: 'Motor1.Status.Running'. Array element: 'Recipe[3]'.",
"Nested UDT member: 'Motor1.Status.Running'. Array element: 'Recipe[3]'. " +
"TwinCAT 2 global (keep its leading dot): '.OPC_HEARTBEAT'.",
IsRequired = true)]
public string SymbolPath { get; init; } = default!;
@@ -11,6 +11,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
/// grammar maps cleanly onto the AbCip Logix path parser, but without Logix's
/// <c>Program:</c> scope prefix. The leading segment is the namespace (POU name /
/// GVL name) and subsequent segments walk into struct/array members.</para>
/// <para><b>TwinCAT 2 globals</b> are named with ONE leading dot (<c>.OPC_HEARTBEAT</c>,
/// <c>.OPC_MACHINEDATA.PROCESSSTATUS</c>) and the PLC requires it — without the dot
/// <c>SYM_HNDBYNAME</c> answers 0x710 (symbol not found), observed live on a TC 2.11 CX. The dot
/// is kept on the ROOT segment's <see cref="TwinCATSymbolSegment.Name"/> so
/// <see cref="ToAdsSymbolName"/> round-trips it unchanged; it is not a separator and never yields
/// an empty segment. Exactly one dot: <c>..x</c>, <c>.</c> and <c>.[1]</c> stay invalid.</para>
/// </remarks>
public sealed record TwinCATSymbolPath(
IReadOnlyList<TwinCATSymbolSegment> Segments,
@@ -41,6 +47,11 @@ public sealed record TwinCATSymbolPath(
if (string.IsNullOrWhiteSpace(value)) return null;
var src = value.Trim();
// A TwinCAT 2 global's single leading dot belongs to the root segment, not before it.
// Strip it for the split and put it back on segment 0 below.
var tc2Global = src.StartsWith('.');
if (tc2Global) src = src[1..];
var parts = new List<string>();
var depth = 0;
var start = 0;
@@ -91,6 +102,7 @@ public sealed record TwinCATSymbolPath(
segments.Add(new TwinCATSymbolSegment(name, subs));
}
if (segments.Count == 0) return null;
if (tc2Global) segments[0] = segments[0] with { Name = "." + segments[0].Name };
return new TwinCATSymbolPath(segments, bitIndex);
}