Auto: abcip-2.2 — L5X (XML) parser + ingest

Adds Import/L5xParser.cs that consumes Studio 5000 L5X (XML) controller
exports via System.Xml.XPath and produces the same L5kDocument bundle as
L5kParser, so L5kIngest handles both formats interchangeably.

- Controller-scope and program-scope <Tag> elements with Name, DataType,
  TagType, ExternalAccess, AliasFor, and <Description> child.
- <DataType>/<Members>/<Member> with Hidden BOOL-host (ZZZZZZZZZZ*) skip.
- AddOnInstructionDefinitions surfaced as L5kDataType entries so AOI-typed
  tags pick up a member layout the same way UDT-typed tags do; hidden
  EnableIn/EnableOut parameters skipped. Full directional Input/Output/InOut
  modelling stays deferred to PR 2.6.

AbCipDriverOptions gains parallel L5xImports collection (mirrors
L5kImports field-for-field). InitializeAsync funnels both through one
shared MergeImport helper that differs only in the parser delegate.

Tests: 8 L5X fixtures cover controller- and program-scope tags, alias skip,
UDT layout fan-out, AOI-typed tag, ZZZZZZZZZZ host skip, hidden AOI param
skip, missing-ExternalAccess default, and an empty-controller no-throw.

Closes #230
This commit is contained in:
Joseph Doherty
2026-04-25 18:10:53 -04:00
parent 2731318c81
commit cfcaf5c1d3
4 changed files with 529 additions and 22 deletions

View File

@@ -130,28 +130,27 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
var allTags = new List<AbCipTagDefinition>(_options.Tags);
foreach (var import in _options.L5kImports)
{
if (string.IsNullOrWhiteSpace(import.DeviceHostAddress))
throw new InvalidOperationException(
"AbCip L5K import is missing DeviceHostAddress — every imported tag needs a target device.");
IL5kSource? src = null;
if (!string.IsNullOrEmpty(import.FilePath))
src = new FileL5kSource(import.FilePath);
else if (!string.IsNullOrEmpty(import.InlineText))
src = new StringL5kSource(import.InlineText);
if (src is null) continue;
var doc = L5kParser.Parse(src);
var ingest = new L5kIngest
{
DefaultDeviceHostAddress = import.DeviceHostAddress,
NamePrefix = import.NamePrefix,
};
var result = ingest.Ingest(doc);
foreach (var importedTag in result.Tags)
{
if (declaredNames.Contains(importedTag.Name)) continue;
allTags.Add(importedTag);
declaredNames.Add(importedTag.Name);
}
MergeImport(
deviceHost: import.DeviceHostAddress,
filePath: import.FilePath,
inlineText: import.InlineText,
namePrefix: import.NamePrefix,
parse: L5kParser.Parse,
formatLabel: "L5K",
declaredNames: declaredNames,
allTags: allTags);
}
foreach (var import in _options.L5xImports)
{
MergeImport(
deviceHost: import.DeviceHostAddress,
filePath: import.FilePath,
inlineText: import.InlineText,
namePrefix: import.NamePrefix,
parse: L5xParser.Parse,
formatLabel: "L5X",
declaredNames: declaredNames,
allTags: allTags);
}
foreach (var tag in allTags)
@@ -194,6 +193,47 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return Task.CompletedTask;
}
/// <summary>
/// Shared L5K / L5X import path — keeps source-format selection (parser delegate) the
/// only behavioural axis between the two formats. Adds the parser's tags to
/// <paramref name="allTags"/> while skipping any name already covered by an earlier
/// declaration or import (declared > L5K > L5X precedence falls out from call order).
/// </summary>
private static void MergeImport(
string deviceHost,
string? filePath,
string? inlineText,
string namePrefix,
Func<IL5kSource, L5kDocument> parse,
string formatLabel,
HashSet<string> declaredNames,
List<AbCipTagDefinition> allTags)
{
if (string.IsNullOrWhiteSpace(deviceHost))
throw new InvalidOperationException(
$"AbCip {formatLabel} import is missing DeviceHostAddress — every imported tag needs a target device.");
IL5kSource? src = null;
if (!string.IsNullOrEmpty(filePath))
src = new FileL5kSource(filePath);
else if (!string.IsNullOrEmpty(inlineText))
src = new StringL5kSource(inlineText);
if (src is null) return;
var doc = parse(src);
var ingest = new L5kIngest
{
DefaultDeviceHostAddress = deviceHost,
NamePrefix = namePrefix,
};
var result = ingest.Ingest(doc);
foreach (var importedTag in result.Tags)
{
if (declaredNames.Contains(importedTag.Name)) continue;
allTags.Add(importedTag);
declaredNames.Add(importedTag.Name);
}
}
public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
await ShutdownAsync(cancellationToken).ConfigureAwait(false);