namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; /// /// Parsed TwinCAT symbolic tag path. Handles global-variable-list (GVL.Counter), /// program-variable (MAIN.bStart), structured member access /// (Motor1.Status.Running), array subscripts (Data[5]), multi-dim arrays /// (Matrix[1,2]), and bit-access (Flags.0). /// /// /// 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 /// Program: scope prefix. The leading segment is the namespace (POU name / /// GVL name) and subsequent segments walk into struct/array members. /// public sealed record TwinCATSymbolPath( IReadOnlyList Segments, int? BitIndex) { 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(); } public static TwinCATSymbolPath? TryParse(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; var src = value.Trim(); var parts = new List(); 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(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(); 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; 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 Subscripts);