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:
Joseph Doherty
2026-04-25 18:58:49 -04:00
parent 177d75784b
commit e3c0750f7d
9 changed files with 373 additions and 9 deletions

View File

@@ -153,6 +153,81 @@ public sealed class L5kIngestTests
Should.Throw<InvalidOperationException>(() => new L5kIngest().Ingest(doc));
}
[Fact]
public void AOI_member_Usage_maps_to_AoiQualifier_through_ingest()
{
// PR abcip-2.6 — L5K AOI parameters carry a Usage := Input / Output / InOut attribute.
// Ingest must map those values onto AbCipStructureMember.AoiQualifier so the discovery
// layer can group AOI members under sub-folders. Plain DATATYPE members get Local.
const string body = """
ADD_ON_INSTRUCTION_DEFINITION ValveAoi
PARAMETERS
PARAMETER Cmd : BOOL (Usage := Input) := 0;
PARAMETER Status : DINT (Usage := Output) := 0;
PARAMETER Buffer : DINT (Usage := InOut) := 0;
PARAMETER Local1 : DINT := 0;
END_PARAMETERS
END_ADD_ON_INSTRUCTION_DEFINITION
DATATYPE PlainUdt
MEMBER Speed : DINT := 0;
END_DATATYPE
TAG
Valve_001 : ValveAoi;
Tank1 : PlainUdt;
END_TAG
""";
var doc = L5kParser.Parse(new StringL5kSource(body));
var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }.Ingest(doc);
var aoiTag = result.Tags.Single(t => t.Name == "Valve_001");
aoiTag.Members.ShouldNotBeNull();
aoiTag.Members!.Single(m => m.Name == "Cmd").AoiQualifier.ShouldBe(AoiQualifier.Input);
aoiTag.Members.Single(m => m.Name == "Status").AoiQualifier.ShouldBe(AoiQualifier.Output);
aoiTag.Members.Single(m => m.Name == "Buffer").AoiQualifier.ShouldBe(AoiQualifier.InOut);
aoiTag.Members.Single(m => m.Name == "Local1").AoiQualifier.ShouldBe(AoiQualifier.Local);
// Plain UDT members default to Local — no Usage attribute to map.
var plainTag = result.Tags.Single(t => t.Name == "Tank1");
plainTag.Members.ShouldNotBeNull();
plainTag.Members!.Single().AoiQualifier.ShouldBe(AoiQualifier.Local);
}
[Fact]
public void L5x_AOI_member_Usage_maps_to_AoiQualifier_through_ingest()
{
// Same mapping as the L5K case above, exercised through the L5X parser to confirm both
// formats land at the same downstream representation.
const string body = """
<?xml version="1.0" encoding="UTF-8"?>
<RSLogix5000Content>
<Controller Name="C">
<AddOnInstructionDefinitions>
<AddOnInstructionDefinition Name="MyAoi">
<Parameters>
<Parameter Name="Cmd" DataType="BOOL" Usage="Input" />
<Parameter Name="Status" DataType="DINT" Usage="Output" />
<Parameter Name="Buffer" DataType="DINT" Usage="InOut" />
</Parameters>
</AddOnInstructionDefinition>
</AddOnInstructionDefinitions>
<Tags>
<Tag Name="Valve_001" TagType="Base" DataType="MyAoi" />
</Tags>
</Controller>
</RSLogix5000Content>
""";
var doc = L5xParser.Parse(new StringL5kSource(body));
var result = new L5kIngest { DefaultDeviceHostAddress = DeviceHost }.Ingest(doc);
var aoiTag = result.Tags.Single();
aoiTag.Members.ShouldNotBeNull();
aoiTag.Members!.Single(m => m.Name == "Cmd").AoiQualifier.ShouldBe(AoiQualifier.Input);
aoiTag.Members.Single(m => m.Name == "Status").AoiQualifier.ShouldBe(AoiQualifier.Output);
aoiTag.Members.Single(m => m.Name == "Buffer").AoiQualifier.ShouldBe(AoiQualifier.InOut);
}
[Fact]
public void NamePrefix_is_applied_to_imported_tags()
{