Auto: abcip-2.4 — CSV tag import/export

CsvTagImporter / CsvTagExporter parse and emit Kepware-format AB CIP tag
CSVs (Tag Name, Address, Data Type, Respect Data Type, Client Access,
Scan Rate, Description, Scaling). Import maps Tag Name → AbCipTagDefinition.Name,
Address → TagPath, Data Type → DataType, Description → Description,
Client Access → Writable. Skips blank rows + ;/# section markers; honours
column reordering via header lookup; RFC-4180-ish quoting.

CsvImports collection on AbCipDriverOptions mirrors L5kImports/L5xImports
and is consumed by InitializeAsync (declared > L5K > L5X > CSV precedence).

CLI tag-export command dumps the merged tag table from a driver-options JSON
to a Kepware CSV — runs the same import-merge precedence the driver uses but
without contacting any PLC.

Tests cover R/W mapping, blank-row skip, quoted comma, escaped quote, name
prefix, unknown-type fall-through, header reordering, and a load → export →
reparse round-trip.

Closes #232
This commit is contained in:
Joseph Doherty
2026-04-25 18:33:55 -04:00
parent 7ee0cbc3f4
commit 08d8a104bb
6 changed files with 689 additions and 0 deletions

View File

@@ -152,6 +152,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
declaredNames: declaredNames,
allTags: allTags);
}
foreach (var import in _options.CsvImports)
{
MergeCsvImport(import, declaredNames, allTags);
}
foreach (var tag in allTags)
{
@@ -234,6 +238,43 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
}
}
/// <summary>
/// CSV-import variant of <see cref="MergeImport"/>. The CSV path produces
/// <see cref="AbCipTagDefinition"/> records directly (no intermediate document) so we
/// can't share the L5K/L5X parser-delegate signature. Merge semantics are identical:
/// a name already covered by a declaration or an earlier import is left untouched so
/// the precedence chain (declared > L5K > L5X > CSV) holds.
/// </summary>
private static void MergeCsvImport(
AbCipCsvImportOptions import,
HashSet<string> declaredNames,
List<AbCipTagDefinition> allTags)
{
if (string.IsNullOrWhiteSpace(import.DeviceHostAddress))
throw new InvalidOperationException(
"AbCip CSV import is missing DeviceHostAddress — every imported tag needs a target device.");
string? csvText = null;
if (!string.IsNullOrEmpty(import.FilePath))
csvText = System.IO.File.ReadAllText(import.FilePath);
else if (!string.IsNullOrEmpty(import.InlineText))
csvText = import.InlineText;
if (csvText is null) return;
var importer = new CsvTagImporter
{
DefaultDeviceHostAddress = import.DeviceHostAddress,
NamePrefix = import.NamePrefix,
};
var result = importer.Import(csvText);
foreach (var tag in result.Tags)
{
if (declaredNames.Contains(tag.Name)) continue;
allTags.Add(tag);
declaredNames.Add(tag.Name);
}
}
public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
await ShutdownAsync(cancellationToken).ConfigureAwait(false);