Auto: s7-d1 — TIA Portal CSV + STEP 7 Classic AWL symbol import

Closes #299
This commit is contained in:
Joseph Doherty
2026-04-26 06:32:18 -04:00
parent ac3fd45cc6
commit a908dff7b5
20 changed files with 2526 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.S7.SymbolImport;
using S7NetCpuType = global::S7.Net.CpuType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
@@ -102,6 +106,128 @@ public static class S7DriverFactoryExtensions
DeadbandAbsolute: t.DeadbandAbsolute,
DeadbandPercent: t.DeadbandPercent);
/// <summary>
/// PR-S7-D1 / #299 — append TIA Portal "Show all tags" CSV rows to
/// <paramref name="options"/> as <see cref="S7TagDefinition"/> entries. Returns a new
/// <see cref="S7DriverOptions"/> 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-symbols</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="S7ImportResult"/> counts surface on
/// <paramref name="result"/> for callers that want to assert "we got the row count
/// we expected".
/// </para>
/// <para>
/// UDT-typed rows materialise as placeholder tags (data type forced to
/// <see cref="S7DataType.Byte"/>); PR-S7-D2 will replace the placeholders with
/// proper UDT layout. See <c>docs/drivers/S7-TIA-Import.md</c>.
/// </para>
/// </remarks>
public static S7DriverOptions AddTiaCsvImport(
this S7DriverOptions options,
string path,
out S7ImportResult result,
S7ImportOptions? importOptions = null,
ILogger<TiaCsvImporter>? logger = null)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentException.ThrowIfNullOrWhiteSpace(path);
using var stream = File.OpenRead(path);
var importer = new TiaCsvImporter(logger ?? NullLogger<TiaCsvImporter>.Instance);
result = importer.Parse(stream, importOptions);
return MergeImportedTags(options, result.Tags);
}
/// <summary>
/// CLI-friendly overload that returns the <see cref="S7ImportResult"/> alongside the
/// modified options as a tuple. Mirrors <see cref="AddTiaCsvImport"/> but avoids the
/// <c>out</c> parameter for call sites that prefer pattern-matched destructuring.
/// </summary>
public static (S7DriverOptions Options, S7ImportResult Result) AddTiaCsvImportWithResult(
this S7DriverOptions options,
string path,
S7ImportOptions? importOptions = null,
ILogger<TiaCsvImporter>? logger = null)
{
var updated = options.AddTiaCsvImport(path, out var result, importOptions, logger);
return (updated, result);
}
/// <summary>
/// PR-S7-D1 / #299 — append STEP 7 Classic AWL <c>VAR_GLOBAL</c> + <c>DATA_BLOCK</c>
/// declarations to <paramref name="options"/> as <see cref="S7TagDefinition"/> entries.
/// Best-effort heuristic — see <see cref="AwlImporter"/> for the position-based
/// addressing rules.
/// </summary>
public static S7DriverOptions AddAwlImport(
this S7DriverOptions options,
string path,
out S7ImportResult result,
S7ImportOptions? importOptions = null,
ILogger<AwlImporter>? logger = null)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentException.ThrowIfNullOrWhiteSpace(path);
using var stream = File.OpenRead(path);
var importer = new AwlImporter(logger ?? NullLogger<AwlImporter>.Instance);
result = importer.Parse(stream, importOptions);
return MergeImportedTags(options, result.Tags);
}
/// <summary>
/// CLI-friendly overload that returns the <see cref="S7ImportResult"/> alongside the
/// modified options as a tuple. Mirrors <see cref="AddAwlImport"/>.
/// </summary>
public static (S7DriverOptions Options, S7ImportResult Result) AddAwlImportWithResult(
this S7DriverOptions options,
string path,
S7ImportOptions? importOptions = null,
ILogger<AwlImporter>? logger = null)
{
var updated = options.AddAwlImport(path, out var result, importOptions, logger);
return (updated, result);
}
/// <summary>
/// Concatenate <paramref name="imported"/> onto <paramref name="options"/>.<see cref="S7DriverOptions.Tags"/>
/// and return a new options object with every other field untouched. The importers
/// are additive so hand-edited Tags rows (e.g., system-status fields not surfaced by
/// the TIA / AWL export) keep sitting alongside the bulk-imported symbol rows.
/// </summary>
private static S7DriverOptions MergeImportedTags(
S7DriverOptions options, IReadOnlyList<S7TagDefinition> imported)
{
var merged = new List<S7TagDefinition>(options.Tags.Count + imported.Count);
merged.AddRange(options.Tags);
merged.AddRange(imported);
return new S7DriverOptions
{
Host = options.Host,
Port = options.Port,
CpuType = options.CpuType,
Rack = options.Rack,
Slot = options.Slot,
Timeout = options.Timeout,
Tags = merged,
Probe = options.Probe,
BlockCoalescingGapBytes = options.BlockCoalescingGapBytes,
TsapMode = options.TsapMode,
LocalTsap = options.LocalTsap,
RemoteTsap = options.RemoteTsap,
ScanGroupIntervals = options.ScanGroupIntervals,
};
}
private static T ParseEnum<T>(string? raw, string driverInstanceId, string field,
string? tagName = null, T? fallback = null) where T : struct, Enum
{