fix(abcip): accept module-defined I/O tag paths (Local:1:I.Data, ABB_OmniCore:I.X)
Live read-only check: a production CompactLogix lists the module tag ABB_OmniCore:I in @tags and libplctag reads ABB_OmniCore:I.ConnectionFaulted without complaint, but the driver refused the path as malformed (identifier rule had no colon) and reported the tag BadCommunicationError. Colon-separated identifier/slot parts are now legal in the controller-scope base segment only (never past it, never under Program: scope).
This commit is contained in:
@@ -100,7 +100,10 @@ public sealed record AbCipTagPath(
|
||||
var bracketIdx = part.IndexOf('[');
|
||||
if (bracketIdx < 0)
|
||||
{
|
||||
if (!IsValidIdent(part)) return null;
|
||||
// The controller-scope base name of a module-defined I/O tag carries colons
|
||||
// (Local:1:I, ABB_OmniCore:I) — legal in the first segment only.
|
||||
var isModuleBase = segments.Count == 0 && programScope is null && IsValidModuleBase(part);
|
||||
if (!isModuleBase && !IsValidIdent(part)) return null;
|
||||
segments.Add(new AbCipTagPathSegment(part, []));
|
||||
continue;
|
||||
}
|
||||
@@ -122,6 +125,25 @@ public sealed record AbCipTagPath(
|
||||
return new AbCipTagPath(programScope, segments, bitIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>true</c> for a module-defined I/O tag base name: an identifier followed by one or more
|
||||
/// <c>:</c>-separated identifier or slot-number parts (<c>Local:1:I</c>, <c>ABB_OmniCore:I</c>).
|
||||
/// </summary>
|
||||
/// <param name="s">The candidate base name.</param>
|
||||
/// <returns><c>true</c> when <paramref name="s"/> is a module tag base name.</returns>
|
||||
private static bool IsValidModuleBase(string s)
|
||||
{
|
||||
var parts = s.Split(':');
|
||||
if (parts.Length < 2 || !IsValidIdent(parts[0])) return false;
|
||||
for (var i = 1; i < parts.Length; i++)
|
||||
{
|
||||
var p = parts[i];
|
||||
if (p.Length == 0) return false;
|
||||
if (!IsValidIdent(p) && !p.All(char.IsAsciiDigit)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsValidIdent(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return false;
|
||||
|
||||
@@ -128,6 +128,41 @@ public sealed class AbCipTagPathTests
|
||||
AbCipTagPath.TryParse(input).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Module-defined I/O tags carry colons in their base name (<c>Local:1:I</c>,
|
||||
/// <c>ABB_OmniCore:I</c>). Seen live: a CompactLogix lists <c>ABB_OmniCore:I</c> in
|
||||
/// <c>@tags</c>, libplctag reads <c>ABB_OmniCore:I.ConnectionFaulted</c> fine, and the driver
|
||||
/// refused it as a malformed path.
|
||||
/// </summary>
|
||||
/// <param name="input">A module tag path.</param>
|
||||
/// <param name="baseName">The expected base (first segment) name.</param>
|
||||
[Theory]
|
||||
[InlineData("ABB_OmniCore:I.ConnectionFaulted", "ABB_OmniCore:I")]
|
||||
[InlineData("Local:1:I.Data", "Local:1:I")]
|
||||
[InlineData("Local:2:O.Data.3", "Local:2:O")]
|
||||
[InlineData("Rack1:3:C.Ch0Config.RangeType", "Rack1:3:C")]
|
||||
public void Module_IO_tag_base_names_with_colons_parse_and_round_trip(string input, string baseName)
|
||||
{
|
||||
var parsed = AbCipTagPath.TryParse(input);
|
||||
parsed.ShouldNotBeNull();
|
||||
parsed.ProgramScope.ShouldBeNull();
|
||||
parsed.Segments[0].Name.ShouldBe(baseName);
|
||||
parsed.ToLibplctagName().ShouldBe(input);
|
||||
}
|
||||
|
||||
/// <summary>A colon is only legal in the base name, and never as a dangling or doubled separator.</summary>
|
||||
/// <param name="input">A malformed path.</param>
|
||||
[Theory]
|
||||
[InlineData("Motor.Sub:I")] // colon past the base segment
|
||||
[InlineData("Local::I.Data")] // empty colon part
|
||||
[InlineData("Local:1:.Data")] // trailing colon
|
||||
[InlineData(":Local.Data")] // leading colon
|
||||
[InlineData("Program:P.Local:1:I")] // module tags are controller-scope only
|
||||
public void Colons_outside_a_module_base_name_are_rejected(string input)
|
||||
{
|
||||
AbCipTagPath.TryParse(input).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that identifiers with underscores are accepted.</summary>
|
||||
[Fact]
|
||||
public void Ident_with_underscore_accepted()
|
||||
|
||||
Reference in New Issue
Block a user