Auto: abcip-2.6 — AOI input/output handling
AOI-aware browse paths: AOI instances now fan out under directional sub-folders (Inputs/, Outputs/, InOut/) instead of a flat layout. The sub-folders only appear when at least one member carries a non-Local AoiQualifier, so plain UDT tags keep the pre-2.6 flat structure. - Add AoiQualifier enum (Local / Input / Output / InOut) + new property on AbCipStructureMember (defaults to Local). - L5K parser learns ADD_ON_INSTRUCTION_DEFINITION blocks; PARAMETER entries' Usage attribute flows through L5kMember.Usage. - L5X parser captures the Usage attribute on <Parameter> elements. - L5kIngest maps Usage strings (Input/Output/InOut) to AoiQualifier; null + unknown values map to Local. - AbCipDriver.DiscoverAsync groups directional members under Inputs / Outputs / InOut sub-folders when any member is non-Local. - Tests for L5K AOI block parsing, L5X Usage capture, ingest mapping (both formats), and AOI-vs-plain UDT discovery fan-out. Closes #234
This commit is contained in:
@@ -59,7 +59,8 @@ public sealed class L5kIngest
|
||||
Name: m.Name,
|
||||
DataType: memberType,
|
||||
Writable: writable,
|
||||
Description: m.Description));
|
||||
Description: m.Description,
|
||||
AoiQualifier: MapAoiUsage(m.Usage)));
|
||||
}
|
||||
udtIndex[dt.Name] = members;
|
||||
}
|
||||
@@ -119,6 +120,19 @@ public sealed class L5kIngest
|
||||
private static bool IsAccessNone(string? externalAccess) =>
|
||||
externalAccess is not null && externalAccess.Trim().Equals("None", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-2.6 — map the AOI <c>Usage</c> attribute string to <see cref="AoiQualifier"/>.
|
||||
/// Plain UDT members (Usage = null) + unrecognised values map to <see cref="AoiQualifier.Local"/>.
|
||||
/// </summary>
|
||||
private static AoiQualifier MapAoiUsage(string? usage) =>
|
||||
usage?.Trim().ToUpperInvariant() switch
|
||||
{
|
||||
"INPUT" => AoiQualifier.Input,
|
||||
"OUTPUT" => AoiQualifier.Output,
|
||||
"INOUT" => AoiQualifier.InOut,
|
||||
_ => AoiQualifier.Local,
|
||||
};
|
||||
|
||||
/// <summary>Map a Logix atomic type name. Returns <c>null</c> for UDT/structure references.</summary>
|
||||
private static AbCipDataType? TryMapAtomic(string logixType) =>
|
||||
logixType?.Trim().ToUpperInvariant() switch
|
||||
|
||||
@@ -72,6 +72,16 @@ public static class L5kParser
|
||||
continue;
|
||||
}
|
||||
|
||||
// PR abcip-2.6 — ADD_ON_INSTRUCTION_DEFINITION block. AOI parameters carry a Usage
|
||||
// attribute (Input / Output / InOut); each PARAMETER becomes a member of the AOI's
|
||||
// L5kDataType entry so AOI-typed tags pick up a layout the same way UDT-typed tags do.
|
||||
if (StartsWithKeyword(line, "ADD_ON_INSTRUCTION_DEFINITION"))
|
||||
{
|
||||
var consumed = ParseAoiDefinitionBlock(lines, i, datatypes);
|
||||
i += consumed;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -312,7 +322,74 @@ public static class L5kParser
|
||||
if (typePart.Length == 0) return null;
|
||||
var externalAccess = attributes.TryGetValue("ExternalAccess", out var ea) ? ea.Trim() : null;
|
||||
var description = attributes.TryGetValue("Description", out var d) ? Unquote(d) : null;
|
||||
return new L5kMember(name, typePart, arrayDim, externalAccess, description);
|
||||
// PR abcip-2.6 — Usage attribute on AOI parameters (Input / Output / InOut). Plain UDT
|
||||
// members don't carry it; null on a regular DATATYPE MEMBER is the default + maps to Local
|
||||
// in the ingest layer.
|
||||
var usage = attributes.TryGetValue("Usage", out var u) ? u.Trim() : null;
|
||||
return new L5kMember(name, typePart, arrayDim, externalAccess, description, usage);
|
||||
}
|
||||
|
||||
// ---- AOI block ---------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// PR abcip-2.6 — parse <c>ADD_ON_INSTRUCTION_DEFINITION ... END_ADD_ON_INSTRUCTION_DEFINITION</c>
|
||||
/// blocks. Body is structured around PARAMETER entries (each carrying a <c>Usage</c>
|
||||
/// attribute) and optional LOCAL_TAGS / ROUTINE blocks. We extract the parameters as
|
||||
/// <see cref="L5kMember"/> rows + leave routines alone — only the surface API matters for
|
||||
/// tag-discovery fan-out. The L5K format encloses parameters either inside a
|
||||
/// <c>PARAMETERS ... END_PARAMETERS</c> block or as bare <c>PARAMETER ... ;</c> lines at
|
||||
/// the AOI top level depending on Studio 5000 export options; this parser accepts both.
|
||||
/// </summary>
|
||||
private static int ParseAoiDefinitionBlock(string[] lines, int start, List<L5kDataType> into)
|
||||
{
|
||||
var first = lines[start].Trim();
|
||||
var head = first.Substring("ADD_ON_INSTRUCTION_DEFINITION".Length).Trim();
|
||||
var name = ExtractFirstQuotedOrToken(head);
|
||||
var members = new List<L5kMember>();
|
||||
var i = start + 1;
|
||||
var inLocalsBlock = false;
|
||||
var inRoutineBlock = false;
|
||||
while (i < lines.Length)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (StartsWithKeyword(line, "END_ADD_ON_INSTRUCTION_DEFINITION"))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(name)) into.Add(new L5kDataType(name, members));
|
||||
return i - start + 1;
|
||||
}
|
||||
if (line.Length == 0) { i++; continue; }
|
||||
|
||||
// Skip routine bodies — they hold ladder / ST / FBD code we don't care about for
|
||||
// tag-discovery, and their own END_ROUTINE / END_LOCAL_TAGS tokens close them out.
|
||||
if (StartsWithKeyword(line, "ROUTINE")) { inRoutineBlock = true; i++; continue; }
|
||||
if (StartsWithKeyword(line, "END_ROUTINE")) { inRoutineBlock = false; i++; continue; }
|
||||
if (StartsWithKeyword(line, "LOCAL_TAGS")) { inLocalsBlock = true; i++; continue; }
|
||||
if (StartsWithKeyword(line, "END_LOCAL_TAGS")) { inLocalsBlock = false; i++; continue; }
|
||||
if (inRoutineBlock || inLocalsBlock) { i++; continue; }
|
||||
|
||||
// PARAMETERS / END_PARAMETERS wrappers are skipped — bare PARAMETER lines drive parsing.
|
||||
if (StartsWithKeyword(line, "PARAMETERS")) { i++; continue; }
|
||||
if (StartsWithKeyword(line, "END_PARAMETERS")) { i++; continue; }
|
||||
|
||||
if (StartsWithKeyword(line, "PARAMETER"))
|
||||
{
|
||||
var sb = new System.Text.StringBuilder(line);
|
||||
while (!sb.ToString().TrimEnd().EndsWith(';') && i + 1 < lines.Length)
|
||||
{
|
||||
var peek = lines[i + 1].Trim();
|
||||
if (StartsWithKeyword(peek, "END_ADD_ON_INSTRUCTION_DEFINITION")) break;
|
||||
i++;
|
||||
sb.Append(' ').Append(peek);
|
||||
}
|
||||
var entry = sb.ToString().TrimEnd(';').Trim();
|
||||
entry = entry.Substring("PARAMETER".Length).Trim();
|
||||
var member = ParseMemberEntry(entry);
|
||||
if (member is not null) members.Add(member);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(name)) into.Add(new L5kDataType(name, members));
|
||||
return i - start;
|
||||
}
|
||||
|
||||
// ---- helpers -----------------------------------------------------------
|
||||
@@ -377,10 +454,16 @@ public sealed record L5kTag(
|
||||
/// <summary>One UDT definition extracted from a <c>DATATYPE ... END_DATATYPE</c> block.</summary>
|
||||
public sealed record L5kDataType(string Name, IReadOnlyList<L5kMember> Members);
|
||||
|
||||
/// <summary>One member line inside a UDT definition.</summary>
|
||||
/// <summary>One member line inside a UDT definition or AOI parameter list.</summary>
|
||||
/// <remarks>
|
||||
/// PR abcip-2.6 — <see cref="Usage"/> carries the AOI <c>Usage</c> attribute (<c>Input</c> /
|
||||
/// <c>Output</c> / <c>InOut</c>) raw text. Plain UDT members + L5K AOI <c>LOCAL_TAGS</c> leave
|
||||
/// it null; the ingest layer maps null → <see cref="AoiQualifier.Local"/>.
|
||||
/// </remarks>
|
||||
public sealed record L5kMember(
|
||||
string Name,
|
||||
string DataType,
|
||||
int? ArrayDim,
|
||||
string? ExternalAccess,
|
||||
string? Description = null);
|
||||
string? Description = null,
|
||||
string? Usage = null);
|
||||
|
||||
@@ -218,12 +218,19 @@ public static class L5xParser
|
||||
if (!string.IsNullOrEmpty(raw)) paramDescription = raw.Trim();
|
||||
}
|
||||
|
||||
// PR abcip-2.6 — capture the AOI Usage attribute (Input / Output / InOut). RSLogix
|
||||
// also serialises Local AOI tags inside <LocalTags>, but those don't go through this
|
||||
// path — only <Parameters>/<Parameter> entries do — so any Usage value on a parameter
|
||||
// is one of the directional buckets.
|
||||
var usage = paramNode.GetAttribute("Usage", string.Empty);
|
||||
|
||||
members.Add(new L5kMember(
|
||||
Name: paramName,
|
||||
DataType: dataType,
|
||||
ArrayDim: arrayDim,
|
||||
ExternalAccess: string.IsNullOrEmpty(externalAccess) ? null : externalAccess,
|
||||
Description: paramDescription));
|
||||
Description: paramDescription,
|
||||
Usage: string.IsNullOrEmpty(usage) ? null : usage));
|
||||
}
|
||||
return new L5kDataType(name, members);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user