Auto: ablegacy-11 — RSLogix 500/PLC-5 CSV symbol import

Closes #254
This commit is contained in:
Joseph Doherty
2026-04-26 04:13:13 -04:00
parent 4fdeef7a6c
commit 4e8df38bb2
19 changed files with 1644 additions and 0 deletions

View File

@@ -1,6 +1,10 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Import;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
@@ -75,6 +79,80 @@ public static class AbLegacyDriverFactoryExtensions
return new AbLegacyDriver(options, driverInstanceId);
}
/// <summary>
/// ablegacy-11 / #254 — append RSLogix CSV symbol-export rows to
/// <paramref name="options"/> as <see cref="AbLegacyTagDefinition"/> entries bound to
/// <paramref name="deviceHostAddress"/>. Returns a new <see cref="AbLegacyDriverOptions"/>
/// with the imported tags concatenated onto the existing <c>Tags</c> list — useful both
/// at startup-time (server-side bootstrap that wants to seed a device's address space
/// from a customer-supplied CSV) and from the CLI (<c>import-rslogix</c> emits the
/// resulting JSON fragment for hand-merging into an appsettings file).
/// </summary>
/// <remarks>
/// <para>
/// The importer is permissive by default — malformed rows are logged and skipped;
/// the resulting <see cref="RsLogixImportResult"/> counts surface on
/// <paramref name="result"/> for callers that want to assert "we got the row count
/// we expected".
/// </para>
/// <para>
/// RSLogix 500's <c>.RSS</c> + RSLogix 5's <c>.RSP</c> binary project files are
/// out of scope for v1 — the binary format is proprietary and undocumented; no
/// libplctag or community parser exists. Customers must export to text/CSV via
/// RSLogix's "Tools → Database → Save" or "Database Export" before pointing the
/// importer at the file. See <c>docs/drivers/AbLegacy-RSLogix-Import.md</c>.
/// </para>
/// </remarks>
public static AbLegacyDriverOptions AddRsLogixImport(
this AbLegacyDriverOptions options,
string path,
string deviceHostAddress,
out RsLogixImportResult result,
ImportOptions? importOptions = null,
ILogger<RsLogixSymbolImport>? logger = null)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentException.ThrowIfNullOrWhiteSpace(path);
ArgumentException.ThrowIfNullOrWhiteSpace(deviceHostAddress);
using var stream = File.OpenRead(path);
var importer = new RsLogixSymbolImport(logger ?? NullLogger<RsLogixSymbolImport>.Instance);
result = importer.Parse(stream, deviceHostAddress, importOptions);
// Concat onto whatever's already on the options — the importer is additive so
// hand-edited Tags rows (e.g., system-status fields not surfaced by RSLogix) keep
// sitting alongside the bulk-imported symbol rows. Use init-syntax with-expression
// so the returned options keeps every other field (Devices, Probe, Timeout, …)
// untouched.
var merged = new List<AbLegacyTagDefinition>(options.Tags.Count + result.Tags.Count);
merged.AddRange(options.Tags);
merged.AddRange(result.Tags);
return new AbLegacyDriverOptions
{
Devices = options.Devices,
Tags = merged,
Probe = options.Probe,
Timeout = options.Timeout,
Retries = options.Retries,
};
}
/// <summary>
/// CLI-friendly overload that returns the <see cref="RsLogixImportResult"/> alongside
/// the modified options as a tuple. Mirrors <see cref="AddRsLogixImport"/> but avoids
/// the <c>out</c> parameter for call sites that prefer pattern-matched destructuring.
/// </summary>
public static (AbLegacyDriverOptions Options, RsLogixImportResult Result) AddRsLogixImportWithResult(
this AbLegacyDriverOptions options,
string path,
string deviceHostAddress,
ImportOptions? importOptions = null,
ILogger<RsLogixSymbolImport>? logger = null)
{
var updated = options.AddRsLogixImport(path, deviceHostAddress, out var result, importOptions, logger);
return (updated, result);
}
private static T ParseEnum<T>(string? raw, string driverInstanceId, string field,
string? tagName = null, T? fallback = null) where T : struct, Enum
{