Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSymbolPath.cs
T
Joseph Doherty b7b754fd98 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.
2026-10-07 13:58:40 -04:00

121 lines
4.9 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
/// <summary>
/// Parsed TwinCAT symbolic tag path. Handles global-variable-list (<c>GVL.Counter</c>),
/// program-variable (<c>MAIN.bStart</c>), structured member access
/// (<c>Motor1.Status.Running</c>), array subscripts (<c>Data[5]</c>), multi-dim arrays
/// (<c>Matrix[1,2]</c>), and bit-access (<c>Flags.0</c>).
/// </summary>
/// <remarks>
/// <para>TwinCAT's symbolic syntax mirrors IEC 61131-3 structured-text identifiers — so the
/// 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,
int? BitIndex)
{
/// <summary>Converts this path to an ADS symbol name.</summary>
/// <returns>The ADS symbol name string.</returns>
public string ToAdsSymbolName()
{
var buf = new System.Text.StringBuilder();
for (var i = 0; i < Segments.Count; i++)
{
if (i > 0) buf.Append('.');
var seg = Segments[i];
buf.Append(seg.Name);
if (seg.Subscripts.Count > 0)
buf.Append('[').Append(string.Join(",", seg.Subscripts)).Append(']');
}
if (BitIndex is not null) buf.Append('.').Append(BitIndex.Value);
return buf.ToString();
}
/// <summary>Attempts to parse a TwinCAT symbol path from the given string.</summary>
/// <param name="value">The path string to parse.</param>
/// <returns>A TwinCATSymbolPath if parsing succeeds; otherwise null.</returns>
public static TwinCATSymbolPath? TryParse(string? value)
{
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;
for (var i = 0; i < src.Length; i++)
{
var c = src[i];
if (c == '[') depth++;
else if (c == ']') depth--;
else if (c == '.' && depth == 0)
{
parts.Add(src[start..i]);
start = i + 1;
}
}
parts.Add(src[start..]);
if (depth != 0 || parts.Any(string.IsNullOrEmpty)) return null;
int? bitIndex = null;
if (parts.Count >= 2 && int.TryParse(parts[^1], out var maybeBit)
&& maybeBit is >= 0 and <= 31
&& !parts[^1].Contains('['))
{
bitIndex = maybeBit;
parts.RemoveAt(parts.Count - 1);
}
var segments = new List<TwinCATSymbolSegment>(parts.Count);
foreach (var part in parts)
{
var bracketIdx = part.IndexOf('[');
if (bracketIdx < 0)
{
if (!IsValidIdent(part)) return null;
segments.Add(new TwinCATSymbolSegment(part, []));
continue;
}
if (!part.EndsWith(']')) return null;
var name = part[..bracketIdx];
if (!IsValidIdent(name)) return null;
var inner = part[(bracketIdx + 1)..^1];
var subs = new List<int>();
foreach (var tok in inner.Split(','))
{
if (!int.TryParse(tok, out var n) || n < 0) return null;
subs.Add(n);
}
if (subs.Count == 0) return null;
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);
}
private static bool IsValidIdent(string s)
{
if (string.IsNullOrEmpty(s)) return false;
if (!char.IsLetter(s[0]) && s[0] != '_') return false;
for (var i = 1; i < s.Length; i++)
if (!char.IsLetterOrDigit(s[i]) && s[i] != '_') return false;
return true;
}
}
public sealed record TwinCATSymbolSegment(string Name, IReadOnlyList<int> Subscripts);