196 lines
8.5 KiB
C#
196 lines
8.5 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|
|
|
/// <summary>
|
|
/// Parsed Logix-symbolic tag path. Handles controller-scope (<c>Motor1_Speed</c>),
|
|
/// program-scope (<c>Program:MainProgram.StepIndex</c>), structured member access
|
|
/// (<c>Motor1.Speed.Setpoint</c>), array subscripts (<c>Array[0]</c>, <c>Matrix[1,2]</c>),
|
|
/// and bit-within-DINT access (<c>Flags.3</c>). Reassembles the canonical Logix syntax via
|
|
/// <see cref="ToLibplctagName"/>, which is the exact string libplctag's <c>name=...</c>
|
|
/// attribute consumes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Scope + members + subscripts are captured structurally so PR 6 (UDT support) can walk
|
|
/// the path against a cached template without re-parsing. <see cref="BitIndex"/> is
|
|
/// non-null only when the trailing segment is a decimal integer between 0 and 31 that
|
|
/// parses as a bit-selector — this is the <c>.N</c> syntax documented in the Logix 5000
|
|
/// General Instructions Reference §Tags, and it applies only to DINT-typed parents. The
|
|
/// parser does not validate the parent type (requires live template data) — it accepts the
|
|
/// shape and defers type-correctness to the runtime.
|
|
/// </remarks>
|
|
public sealed record AbCipTagPath(
|
|
string? ProgramScope,
|
|
IReadOnlyList<AbCipTagPathSegment> Segments,
|
|
int? BitIndex,
|
|
AbCipTagPathSlice? Slice = null)
|
|
{
|
|
/// <summary>Rebuild the canonical Logix tag string.</summary>
|
|
public string ToLibplctagName()
|
|
{
|
|
var buf = new System.Text.StringBuilder();
|
|
if (ProgramScope is not null)
|
|
buf.Append("Program:").Append(ProgramScope).Append('.');
|
|
|
|
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 (Slice is not null) buf.Append('[').Append(Slice.Start).Append("..").Append(Slice.End).Append(']');
|
|
if (BitIndex is not null) buf.Append('.').Append(BitIndex.Value);
|
|
return buf.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logix-symbol form for issuing a single libplctag tag-create that reads the slice as a
|
|
/// contiguous buffer — i.e. the bare array name (with the start subscript) without the
|
|
/// <c>..End</c> suffix. The driver pairs this with <see cref="AbCipTagCreateParams.ElementCount"/>
|
|
/// = <see cref="AbCipTagPathSlice.Count"/> to issue a single Rockwell array read.
|
|
/// </summary>
|
|
public string ToLibplctagSliceArrayName()
|
|
{
|
|
if (Slice is null) return ToLibplctagName();
|
|
var buf = new System.Text.StringBuilder();
|
|
if (ProgramScope is not null)
|
|
buf.Append("Program:").Append(ProgramScope).Append('.');
|
|
|
|
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(']');
|
|
}
|
|
// Anchor the read at the slice start; libplctag treats Name=Tag[0] + ElementCount=N as
|
|
// "read N consecutive elements starting at index 0", which is the exact Rockwell
|
|
// array-read semantic this PR is wiring up.
|
|
buf.Append('[').Append(Slice.Start).Append(']');
|
|
return buf.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a Logix-symbolic tag reference. Returns <c>null</c> on a shape the parser
|
|
/// doesn't support — the driver surfaces that as a config-validation error rather than
|
|
/// attempting a best-effort translation.
|
|
/// </summary>
|
|
public static AbCipTagPath? TryParse(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value)) return null;
|
|
var src = value.Trim();
|
|
|
|
string? programScope = null;
|
|
const string programPrefix = "Program:";
|
|
if (src.StartsWith(programPrefix, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var afterPrefix = src[programPrefix.Length..];
|
|
var dotIdx = afterPrefix.IndexOf('.');
|
|
if (dotIdx <= 0) return null;
|
|
programScope = afterPrefix[..dotIdx];
|
|
src = afterPrefix[(dotIdx + 1)..];
|
|
if (string.IsNullOrEmpty(src)) return null;
|
|
}
|
|
|
|
// Split on dots, but preserve any [i,j] subscript runs that contain only digits + commas.
|
|
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<AbCipTagPathSegment>(parts.Count);
|
|
AbCipTagPathSlice? slice = null;
|
|
for (var partIdx = 0; partIdx < parts.Count; partIdx++)
|
|
{
|
|
var part = parts[partIdx];
|
|
var bracketIdx = part.IndexOf('[');
|
|
if (bracketIdx < 0)
|
|
{
|
|
if (!IsValidIdent(part)) return null;
|
|
segments.Add(new AbCipTagPathSegment(part, []));
|
|
continue;
|
|
}
|
|
if (!part.EndsWith(']')) return null;
|
|
var name = part[..bracketIdx];
|
|
if (!IsValidIdent(name)) return null;
|
|
var inner = part[(bracketIdx + 1)..^1];
|
|
|
|
// Slice syntax `[N..M]` — only allowed on the LAST segment, must not coexist with
|
|
// multi-dim subscripts, must not be combined with bit-index, and requires M >= N.
|
|
// Any other shape is rejected so callers see a config-validation error rather than
|
|
// the driver attempting a best-effort scalar read.
|
|
if (inner.Contains(".."))
|
|
{
|
|
if (partIdx != parts.Count - 1) return null; // slice + sub-element
|
|
if (bitIndex is not null) return null; // slice + bit index
|
|
if (inner.Contains(',')) return null; // slice cannot be multi-dim
|
|
var parts2 = inner.Split("..", 2, StringSplitOptions.None);
|
|
if (parts2.Length != 2) return null;
|
|
if (!int.TryParse(parts2[0], out var sliceStart) || sliceStart < 0) return null;
|
|
if (!int.TryParse(parts2[1], out var sliceEnd) || sliceEnd < sliceStart) return null;
|
|
slice = new AbCipTagPathSlice(sliceStart, sliceEnd);
|
|
segments.Add(new AbCipTagPathSegment(name, []));
|
|
continue;
|
|
}
|
|
|
|
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 AbCipTagPathSegment(name, subs));
|
|
}
|
|
if (segments.Count == 0) return null;
|
|
|
|
return new AbCipTagPath(programScope, segments, bitIndex, slice);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>One path segment: a member name plus any numeric subscripts.</summary>
|
|
public sealed record AbCipTagPathSegment(string Name, IReadOnlyList<int> Subscripts);
|
|
|
|
/// <summary>
|
|
/// Inclusive-on-both-ends array slice carried on the trailing segment of an
|
|
/// <see cref="AbCipTagPath"/>. <c>Tag[0..15]</c> parses to <c>Start=0, End=15</c>; the
|
|
/// planner pairs this with libplctag's <c>ElementCount</c> attribute to issue a single
|
|
/// Rockwell array read covering <c>End - Start + 1</c> elements.
|
|
/// </summary>
|
|
public sealed record AbCipTagPathSlice(int Start, int End)
|
|
{
|
|
/// <summary>Total element count covered by the slice (inclusive both ends).</summary>
|
|
public int Count => End - Start + 1;
|
|
}
|