v3(s7): apply Modbus RawTags exemplar to the S7 driver
Rename S7EquipmentTagParser -> S7TagDefinitionFactory; TryParse(reference)
-> FromTagConfig(tagConfig, rawPath, out def) (drops the leading-{ heuristic,
keys the def by RawPath, adds inverse ToTagConfig). Replace S7DriverOptions.Tags
(pre-declared S7TagDefinition list) with RawTags (IReadOnlyList<RawTagEntry>);
the driver builds its RawPath->def table from RawTags at Initialize via the
factory (skip+log on miss), resolver is byName-only, DiscoverAsync + init guards
+ address pre-parse now run off that table. Factory retires the pre-declared DTO
tag path (RawTags binding only). Cli BuildOptions serialises typed defs to
RawTagEntry. Tests migrated to a S7RawTags helper + FromTagConfig; parser test
files renamed. Coordinator's EquipmentTagConfigInspector S7 rename left to Wave-B
coordinator; Driver.S7.IntegrationTests (Docker-gated) left red per scope.
Driver.S7 + Cli build green; Driver.S7.Tests 264/264, S7.Cli.Tests 49/49.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Cli;
|
||||
@@ -48,8 +49,11 @@ public abstract class S7CommandBase : DriverCommandBase
|
||||
|
||||
/// <summary>
|
||||
/// Build an <see cref="S7DriverOptions"/> with the endpoint fields this base
|
||||
/// collected + whatever <paramref name="tags"/> the subclass declares. Probe
|
||||
/// disabled — CLI runs are one-shot.
|
||||
/// collected + whatever <paramref name="tags"/> the subclass declares. Each typed
|
||||
/// definition is serialised to its <c>TagConfig</c> blob via
|
||||
/// <see cref="S7TagDefinitionFactory.ToTagConfig"/> and packaged as a
|
||||
/// <see cref="RawTagEntry"/> (RawPath = the def's <c>Name</c>) — the v3 delivery shape the
|
||||
/// driver rebuilds definitions from. Probe disabled — CLI runs are one-shot.
|
||||
/// </summary>
|
||||
/// <param name="tags">The tag definitions to include in the options.</param>
|
||||
/// <returns>The constructed <see cref="S7DriverOptions"/>.</returns>
|
||||
@@ -61,7 +65,7 @@ public abstract class S7CommandBase : DriverCommandBase
|
||||
Rack = Rack,
|
||||
Slot = Slot,
|
||||
Timeout = Timeout,
|
||||
Tags = tags,
|
||||
RawTags = [.. tags.Select(t => new RawTagEntry(t.Name, S7TagDefinitionFactory.ToTagConfig(t), t.WriteIdempotent))],
|
||||
Probe = new S7ProbeOptions { Enabled = false },
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
||||
|
||||
@@ -53,8 +54,15 @@ public sealed class S7DriverOptions
|
||||
/// <summary>Connect + per-operation timeout.</summary>
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>Pre-declared tag map. S7 has a symbol-table protocol but S7.Net does not expose it, so the driver operates off a static tag list configured per-site. Address grammar documented in S7AddressParser (PR 63).</summary>
|
||||
public IReadOnlyList<S7TagDefinition> Tags { get; init; } = [];
|
||||
/// <summary>
|
||||
/// Authored raw tags this driver serves. The deploy artifact hands each authored raw
|
||||
/// <c>Tag</c> as a <see cref="RawTagEntry"/> (RawPath identity + driver <c>TagConfig</c> blob +
|
||||
/// WriteIdempotent flag); the driver maps each through
|
||||
/// <see cref="S7TagDefinitionFactory.FromTagConfig"/> into its RawPath → definition table.
|
||||
/// S7 has a symbol-table protocol but S7.Net does not expose it, so the driver serves exactly
|
||||
/// these authored tags. Address grammar documented in S7AddressParser (PR 63).
|
||||
/// </summary>
|
||||
public IReadOnlyList<RawTagEntry> RawTags { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Background connectivity-probe settings. When enabled, the driver runs a tick loop
|
||||
@@ -104,7 +112,7 @@ public sealed class S7ProbeOptions
|
||||
/// Element count when the tag is a 1-D array; <c>null</c> for a scalar. Any value <c>>= 1</c>
|
||||
/// (including 1) surfaces as a 1-D OPC UA array node.
|
||||
/// For an equipment tag this is threaded from the <c>TagConfig</c> JSON's <c>arrayLength</c>
|
||||
/// (honoured only when <c>isArray</c> is true) by <see cref="S7EquipmentTagParser"/>. When
|
||||
/// (honoured only when <c>isArray</c> is true) by <see cref="S7TagDefinitionFactory"/>. When
|
||||
/// set, the driver issues a single contiguous block read of
|
||||
/// <c>ArrayCount × element-bytes</c> from the tag's start address and decodes each element
|
||||
/// into an element-typed CLR array (<c>short[]</c> / <c>int[]</c> / <c>float[]</c> / etc.).
|
||||
|
||||
+64
-16
@@ -1,29 +1,45 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
||||
|
||||
/// <summary>Parses an equipment tag's <c>TagConfig</c> JSON (the shape authored by the AdminUI
|
||||
/// <c>S7TagConfigModel</c> — <c>address</c> / <c>dataType</c> / <c>stringLength</c>) into a transient
|
||||
/// <see cref="S7TagDefinition"/> whose <see cref="S7TagDefinition.Name"/> equals the reference string
|
||||
/// itself, so a value the driver publishes back keys the runtime's forward router correctly.</summary>
|
||||
public static class S7EquipmentTagParser
|
||||
/// <summary>
|
||||
/// v3 pure mapper: turns an authored raw tag's <c>TagConfig</c> JSON (the shape produced by the
|
||||
/// AdminUI <c>S7TagConfigModel</c> — <c>address</c> / <c>dataType</c> / <c>stringLength</c>) into a
|
||||
/// <see cref="S7TagDefinition"/>. Under v3 a tag's identity is its <b>RawPath</b> (a cluster-scoped
|
||||
/// slash path), not the address blob — so the produced definition's <see cref="S7TagDefinition.Name"/>
|
||||
/// is the RawPath the driver was handed, which is exactly the wire reference the driver's
|
||||
/// <c>RawPath → def</c> resolver keys on. The driver builds that table by mapping each
|
||||
/// <see cref="RawTagEntry"/> the deploy artifact delivers through <see cref="FromTagConfig"/>;
|
||||
/// <see cref="ToTagConfig"/> is the inverse, used by authoring paths (the Client CLI) that hold a typed
|
||||
/// definition and need to synthesise the <c>TagConfig</c> blob for a <see cref="RawTagEntry"/>.
|
||||
/// </summary>
|
||||
public static class S7TagDefinitionFactory
|
||||
{
|
||||
/// <summary>S7 string max length (DBSTRING header reserves 2 bytes; 254 chars max).</summary>
|
||||
private const int MaxStringLength = 254;
|
||||
|
||||
/// <summary>Attempts to parse an equipment-tag reference into a transient definition.</summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON (also used as the def identity).</param>
|
||||
/// <param name="def">The transient definition when parsing succeeds.</param>
|
||||
/// <returns><see langword="true"/> when <paramref name="reference"/> is an S7 address object.</returns>
|
||||
public static bool TryParse(string reference, out S7TagDefinition def)
|
||||
/// <summary>
|
||||
/// Maps an authored <c>TagConfig</c> object to a typed definition keyed by <paramref name="rawPath"/>.
|
||||
/// The input is always an authored TagConfig JSON object (there is no name-vs-blob heuristic in v3).
|
||||
/// The <c>dataType</c> enum is read STRICTLY — a present-but-invalid (typo'd) value rejects the whole
|
||||
/// tag (returns <see langword="false"/> ⇒ the driver surfaces <c>BadNodeIdUnknown</c>) rather than
|
||||
/// silently defaulting to a wrong-width Good. <see cref="S7TagDefinition.WriteIdempotent"/> is NOT read
|
||||
/// from the blob — it is a platform flag the caller threads in from the tag's
|
||||
/// <see cref="RawTagEntry.WriteIdempotent"/>.
|
||||
/// </summary>
|
||||
/// <param name="tagConfig">The authored equipment-tag TagConfig JSON.</param>
|
||||
/// <param name="rawPath">The tag's RawPath — becomes the definition's identity (<c>Name</c>).</param>
|
||||
/// <param name="def">The mapped definition when this returns <see langword="true"/>.</param>
|
||||
/// <returns><see langword="true"/> when <paramref name="tagConfig"/> is a valid S7 address object.</returns>
|
||||
public static bool FromTagConfig(string tagConfig, string rawPath, out S7TagDefinition def)
|
||||
{
|
||||
def = null!;
|
||||
// Authored tag names never start with '{' (AdminUI name validation), so a leading brace marks an equipment-tag TagConfig blob.
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return false;
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return false;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
using var doc = JsonDocument.Parse(tagConfig);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object
|
||||
|| !root.TryGetProperty("address", out var addrEl)
|
||||
@@ -42,18 +58,19 @@ public static class S7EquipmentTagParser
|
||||
if (dataType == S7DataType.String && (stringLength < 0 || stringLength > MaxStringLength)) return false;
|
||||
// Array intent: the canonical sink-side parse (DeploymentArtifact.ExtractTagArray)
|
||||
// honours arrayLength ONLY when isArray is true AND the prop is a JSON number — mirror
|
||||
// that here so the driver's transient def agrees byte-for-byte with the materialised
|
||||
// OPC UA node's ValueRank/ArrayDimensions. Absent / isArray=false ⇒ null (scalar).
|
||||
// that here so the driver's def agrees byte-for-byte with the materialised OPC UA node's
|
||||
// ValueRank/ArrayDimensions. Absent / isArray=false ⇒ null (scalar).
|
||||
var arrayCount = ReadArrayCount(root);
|
||||
// "writable" defaults to true when absent (today's value); node-level authz still governs
|
||||
// writes. Honouring the key makes read-only equipment tags authorable (UNDER-6).
|
||||
var writable = TagConfigJson.ReadWritable(root);
|
||||
def = new S7TagDefinition(
|
||||
Name: reference,
|
||||
Name: rawPath,
|
||||
Address: address,
|
||||
DataType: dataType,
|
||||
Writable: writable,
|
||||
StringLength: stringLength == 0 ? MaxStringLength : stringLength,
|
||||
WriteIdempotent: false,
|
||||
ArrayCount: arrayCount);
|
||||
return true;
|
||||
}
|
||||
@@ -62,6 +79,37 @@ public static class S7EquipmentTagParser
|
||||
catch (InvalidOperationException) { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inverse of <see cref="FromTagConfig"/>: serialises a typed definition back to the camelCase
|
||||
/// <c>TagConfig</c> JSON the mapper reads. Used by authoring paths (the Client CLI) that construct an
|
||||
/// <see cref="S7TagDefinition"/> from operator flags and need the <c>TagConfig</c> string to hand the
|
||||
/// driver as a <see cref="RawTagEntry"/>. The <c>dataType</c> enum is written as its name string;
|
||||
/// <c>stringLength</c> is emitted only when it diverges from the 254 default so the blob stays minimal.
|
||||
/// <c>Name</c> is NOT emitted — it becomes the <see cref="RawTagEntry.RawPath"/>. <c>WriteIdempotent</c>
|
||||
/// is NOT emitted — it travels on the <see cref="RawTagEntry"/>, not inside the blob.
|
||||
/// </summary>
|
||||
/// <param name="def">The definition to serialise.</param>
|
||||
/// <returns>The camelCase TagConfig JSON string.</returns>
|
||||
public static string ToTagConfig(S7TagDefinition def)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(def);
|
||||
var o = new JsonObject
|
||||
{
|
||||
["address"] = def.Address,
|
||||
["dataType"] = def.DataType.ToString(),
|
||||
["writable"] = def.Writable,
|
||||
};
|
||||
// FromTagConfig maps an absent / zero stringLength to MaxStringLength (254), so a def carrying
|
||||
// the 254 default round-trips whether or not we emit it — emit only when it diverges.
|
||||
if (def.StringLength != MaxStringLength) o["stringLength"] = def.StringLength;
|
||||
if (def.ArrayCount is { } count)
|
||||
{
|
||||
o["isArray"] = true;
|
||||
o["arrayLength"] = count;
|
||||
}
|
||||
return o.ToJsonString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deploy-time inspection (05/CONV-2): warns on a present-but-invalid <c>dataType</c> (silently
|
||||
/// defaulted by the lenient runtime) and on a structurally unparseable TagConfig (a silent
|
||||
@@ -51,8 +51,7 @@ public sealed class S7Driver
|
||||
_plcFactory = plcFactory ?? new S7PlcFactory();
|
||||
_logger = logger ?? NullLogger<S7Driver>.Instance;
|
||||
_resolver = new EquipmentTagRefResolver<S7TagDefinition>(
|
||||
r => _tagsByName.TryGetValue(r, out var t) ? t : null,
|
||||
r => S7EquipmentTagParser.TryParse(r, out var d) ? d : null);
|
||||
r => _tagsByRawPath.TryGetValue(r, out var t) ? t : null);
|
||||
// CONV-1: the polling overlay runs on the shared PollGroupEngine (structural array diff +
|
||||
// drain-before-dispose + capped-exponential failure backoff + caller-token-filtered OCE),
|
||||
// retiring the bespoke S7 poll fork. minInterval 100 ms matches the S7 scan-mailbox floor;
|
||||
@@ -104,12 +103,15 @@ public sealed class S7Driver
|
||||
/// <summary>OPC UA StatusCode used for a genuine device fault (CPU error, hardware fault).</summary>
|
||||
private const uint StatusBadDeviceFailure = 0x808B0000u;
|
||||
|
||||
private readonly Dictionary<string, S7TagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, S7ParsedAddress> _parsedByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
// v3: the RawPath → definition table. Keyed by RawPath (the v3 tag identity, == def.Name),
|
||||
// case-sensitive ordinal. Built in InitializeAsync from _options.RawTags via the pure factory.
|
||||
private readonly Dictionary<string, S7TagDefinition> _tagsByRawPath = new(StringComparer.Ordinal);
|
||||
// Pre-parsed S7 address per tag, keyed by RawPath (== def.Name). Populated alongside
|
||||
// _tagsByRawPath at init so a config typo fails fast here rather than as a per-read fault.
|
||||
private readonly Dictionary<string, S7ParsedAddress> _parsedByRawPath = new(StringComparer.Ordinal);
|
||||
|
||||
// Resolves a read/write/subscribe fullReference to a tag definition, bridging the two
|
||||
// authoring models: an authored tag-table entry (by name) OR an equipment tag whose
|
||||
// reference is its raw TagConfig JSON (parsed once via S7EquipmentTagParser, cached).
|
||||
// Resolves a read/write/subscribe fullReference (a RawPath in v3) to a tag definition by a single
|
||||
// authored-table hit; a miss is a miss (the driver surfaces BadNodeIdUnknown, never a parse attempt).
|
||||
private readonly EquipmentTagRefResolver<S7TagDefinition> _resolver;
|
||||
|
||||
/// <summary>
|
||||
@@ -175,6 +177,24 @@ public sealed class S7Driver
|
||||
if (HasConfigBody(driverConfigJson))
|
||||
_options = S7DriverFactoryExtensions.ParseOptions(_driverInstanceId, driverConfigJson);
|
||||
|
||||
// v3: build the RawPath → definition table from the authored raw tags BEFORE the config
|
||||
// guards run (they now iterate the mapped table). Each entry's TagConfig blob is mapped by
|
||||
// the pure factory; the entry's WriteIdempotent flag is threaded onto the def (it lives on
|
||||
// the RawTagEntry, not inside the blob). A mapper miss is skipped (logged), never thrown —
|
||||
// one bad tag must not fail the whole driver init.
|
||||
_tagsByRawPath.Clear();
|
||||
_parsedByRawPath.Clear();
|
||||
_resolver.Clear(); // no-op in v3 (the resolver reads the live table by closure); kept for symmetry
|
||||
foreach (var entry in _options.RawTags)
|
||||
{
|
||||
if (S7TagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var def))
|
||||
_tagsByRawPath[entry.RawPath] = def with { WriteIdempotent = entry.WriteIdempotent };
|
||||
else
|
||||
_logger.LogWarning(
|
||||
"S7 tag config did not map to a definition; skipping. Driver={DriverInstanceId} RawPath={RawPath}",
|
||||
_driverInstanceId, entry.RawPath);
|
||||
}
|
||||
|
||||
// Phase 4d combined config guard. Timer/Counter addresses and the wide/structured
|
||||
// types (Int64/UInt64/Float64/String/DateTime) are now SUPPORTED, but only in
|
||||
// specific shapes — this rejects the genuinely-unsupported configurations
|
||||
@@ -220,21 +240,17 @@ public sealed class S7Driver
|
||||
|
||||
// Parse every tag's address once at init so config typos fail fast here instead
|
||||
// of surfacing as BadInternalError on every Read against the bad tag. The parser
|
||||
// also rejects bit-offset > 7, DB 0, unknown area letters, etc.
|
||||
_tagsByName.Clear();
|
||||
_parsedByName.Clear();
|
||||
_resolver.Clear(); // drop transient equipment-tag parses so a config change re-parses
|
||||
foreach (var t in _options.Tags)
|
||||
// also rejects bit-offset > 7, DB 0, unknown area letters, etc. Keyed by RawPath
|
||||
// (== def.Name) so Read/Write can pick up the pre-parsed address.
|
||||
foreach (var t in _tagsByRawPath.Values)
|
||||
{
|
||||
var parsed = S7AddressParser.Parse(t.Address); // throws FormatException
|
||||
_tagsByName[t.Name] = t;
|
||||
_parsedByName[t.Name] = parsed;
|
||||
_parsedByRawPath[t.Name] = S7AddressParser.Parse(t.Address); // throws FormatException
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
_health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
|
||||
_logger.LogInformation("S7Driver connected. Driver={DriverInstanceId} Host={Host} CPU={CpuType} Tags={TagCount}",
|
||||
_driverInstanceId, _options.Host, _options.CpuType, _options.Tags.Count);
|
||||
_driverInstanceId, _options.Host, _options.CpuType, _tagsByRawPath.Count);
|
||||
|
||||
// Kick off the probe loop once the connection is up. Initial HostState stays
|
||||
// Unknown until the first probe tick succeeds — avoids broadcasting a premature
|
||||
@@ -360,7 +376,7 @@ public sealed class S7Driver
|
||||
/// </summary>
|
||||
private void RejectUnsupportedTagConfigs()
|
||||
{
|
||||
foreach (var t in _options.Tags)
|
||||
foreach (var t in _tagsByRawPath.Values)
|
||||
{
|
||||
var isWide = ByteAnchoredDataTypes.Contains(t.DataType);
|
||||
|
||||
@@ -442,7 +458,7 @@ public sealed class S7Driver
|
||||
/// </summary>
|
||||
private void RejectUnsupportedTagDataTypes()
|
||||
{
|
||||
foreach (var t in _options.Tags)
|
||||
foreach (var t in _tagsByRawPath.Values)
|
||||
{
|
||||
if (UnimplementedDataTypes.Contains(t.DataType))
|
||||
{
|
||||
@@ -573,12 +589,10 @@ public sealed class S7Driver
|
||||
|
||||
private async Task<object> ReadOneAsync(IS7Plc plc, S7TagDefinition tag, CancellationToken ct)
|
||||
{
|
||||
// Authored tags pre-parse their address at init (_parsedByName); an equipment-tag ref
|
||||
// (resolved transiently by _resolver) has no _parsedByName entry, so parse its address
|
||||
// on demand. S7AddressParser.Parse throws FormatException on a bad address, which the
|
||||
// caller's catch maps to BadCommunicationError — the same surface a bad authored tag
|
||||
// would have hit at init (transient defs aren't init-validated).
|
||||
var addr = _parsedByName.TryGetValue(tag.Name, out var parsed)
|
||||
// Every tag pre-parses its address at init (_parsedByRawPath); the on-demand
|
||||
// S7AddressParser.Parse is a defensive fallback for a tag not in that table. Parse throws
|
||||
// FormatException on a bad address, which the caller's catch maps to BadCommunicationError.
|
||||
var addr = _parsedByRawPath.TryGetValue(tag.Name, out var parsed)
|
||||
? parsed
|
||||
: S7AddressParser.Parse(tag.Address);
|
||||
|
||||
@@ -1045,16 +1059,16 @@ public sealed class S7Driver
|
||||
results[i] = new WriteResult(StatusBadNotWritable);
|
||||
continue;
|
||||
}
|
||||
// Timer/Counter transient-write guard: a transient equipment-tag ref is always
|
||||
// resolved with Writable=true (S7EquipmentTagParser hardcodes it, and node-level
|
||||
// auth governs writes), so Timer/Counter addresses bypass the !Writable gate above
|
||||
// and would otherwise reach EncodeScalarBlock, which throws NotSupportedException →
|
||||
// BadNotSupported. The docs say Timer/Counter writes return BadNotWritable. Detect
|
||||
// the area here before the call so BOTH authored (caught by guard-d at init) and
|
||||
// transient Timer/Counter writes consistently return BadNotWritable.
|
||||
// Timer/Counter write guard: an equipment tag authored writable=true (S7 mapper
|
||||
// honours the "writable" key, and node-level auth governs writes) whose address is a
|
||||
// Timer/Counter would bypass the !Writable gate above and reach EncodeScalarBlock,
|
||||
// which throws NotSupportedException → BadNotSupported. The docs say Timer/Counter
|
||||
// writes return BadNotWritable. Detect the area here before the call so it
|
||||
// consistently returns BadNotWritable (init guard-d also rejects a writable
|
||||
// Timer/Counter, but the mapper doesn't run that guard).
|
||||
// TryParse (not Parse) so a malformed address falls through to the existing
|
||||
// error path rather than throwing here.
|
||||
if (_parsedByName.TryGetValue(tag.Name, out var tcParsed)
|
||||
if (_parsedByRawPath.TryGetValue(tag.Name, out var tcParsed)
|
||||
? tcParsed.Area is S7Area.Timer or S7Area.Counter
|
||||
: (S7AddressParser.TryParse(tag.Address, out tcParsed) && tcParsed.Area is S7Area.Timer or S7Area.Counter))
|
||||
{
|
||||
@@ -1126,11 +1140,11 @@ public sealed class S7Driver
|
||||
$"S7 array writes are not yet supported (tag '{tag.Name}'). " +
|
||||
"Use individual scalar tags or set Writable=false for read-only arrays.");
|
||||
|
||||
// Parse the address the same way ReadOneAsync does: authored tags pre-parse at init
|
||||
// (_parsedByName); an equipment-tag ref (resolved transiently) parses on demand. Needed
|
||||
// here so the wide-type write can byte-address the block (the narrow path below addresses
|
||||
// by the raw address string instead).
|
||||
var addr = _parsedByName.TryGetValue(tag.Name, out var parsed)
|
||||
// Parse the address the same way ReadOneAsync does: every tag pre-parses at init
|
||||
// (_parsedByRawPath), with an on-demand parse as a defensive fallback. Needed here so the
|
||||
// wide-type write can byte-address the block (the narrow path below addresses by the raw
|
||||
// address string instead).
|
||||
var addr = _parsedByRawPath.TryGetValue(tag.Name, out var parsed)
|
||||
? parsed
|
||||
: S7AddressParser.Parse(tag.Address);
|
||||
|
||||
@@ -1370,7 +1384,7 @@ public sealed class S7Driver
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
var folder = builder.Folder("S7", "S7");
|
||||
foreach (var t in _options.Tags)
|
||||
foreach (var t in _tagsByRawPath.Values)
|
||||
{
|
||||
// A tag carrying a non-null array count (>= 1) surfaces as a 1-D OPC UA array node.
|
||||
// A null count stays scalar. A count of 1 IS a valid 1-element array: the foundation
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
||||
@@ -64,9 +65,10 @@ public static class S7DriverFactoryExtensions
|
||||
Rack = dto.Rack ?? 0,
|
||||
Slot = dto.Slot ?? 0,
|
||||
Timeout = TimeSpan.FromMilliseconds(dto.TimeoutMs ?? 5_000),
|
||||
Tags = dto.Tags is { Count: > 0 }
|
||||
? [.. dto.Tags.Select(t => BuildTag(t, driverInstanceId))]
|
||||
: [],
|
||||
// v3: the deploy artifact (Wave C) populates rawTags with the cluster's authored raw S7
|
||||
// tags. The driver maps each RawTagEntry's TagConfig blob through S7TagDefinitionFactory
|
||||
// at Initialize; the legacy pre-declared DTO tag path (Name/Address → BuildTag) is retired.
|
||||
RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [],
|
||||
Probe = new S7ProbeOptions
|
||||
{
|
||||
Enabled = dto.Probe?.Enabled ?? true,
|
||||
@@ -77,18 +79,6 @@ public static class S7DriverFactoryExtensions
|
||||
};
|
||||
}
|
||||
|
||||
private static S7TagDefinition BuildTag(S7TagDto t, string driverInstanceId) =>
|
||||
new(
|
||||
Name: t.Name ?? throw new InvalidOperationException(
|
||||
$"S7 config for '{driverInstanceId}' has a tag missing Name"),
|
||||
Address: t.Address ?? throw new InvalidOperationException(
|
||||
$"S7 tag '{t.Name}' in '{driverInstanceId}' missing Address"),
|
||||
DataType: ParseEnum<S7DataType>(t.DataType, driverInstanceId, "DataType",
|
||||
tagName: t.Name),
|
||||
Writable: t.Writable ?? true,
|
||||
StringLength: t.StringLength ?? 254,
|
||||
WriteIdempotent: t.WriteIdempotent ?? false);
|
||||
|
||||
private static T ParseEnum<T>(string? raw, string driverInstanceId, string field,
|
||||
string? tagName = null, T? fallback = null) where T : struct, Enum
|
||||
{
|
||||
@@ -127,29 +117,13 @@ public static class S7DriverFactoryExtensions
|
||||
public short? Slot { get; init; }
|
||||
/// <summary>Gets the connection timeout in milliseconds.</summary>
|
||||
public int? TimeoutMs { get; init; }
|
||||
/// <summary>Gets the list of tag definitions.</summary>
|
||||
public List<S7TagDto>? Tags { get; init; }
|
||||
/// <summary>Gets the authored raw tags (RawPath + TagConfig blob + WriteIdempotent) the deploy
|
||||
/// artifact delivers. The driver maps each through <see cref="S7TagDefinitionFactory"/> at Initialize.</summary>
|
||||
public List<RawTagEntry>? RawTags { get; init; }
|
||||
/// <summary>Gets the probe configuration.</summary>
|
||||
public S7ProbeDto? Probe { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Data transfer object for S7 tag definition.</summary>
|
||||
internal sealed class S7TagDto
|
||||
{
|
||||
/// <summary>Gets the tag name.</summary>
|
||||
public string? Name { get; init; }
|
||||
/// <summary>Gets the S7 address (e.g., DB1.DBD0).</summary>
|
||||
public string? Address { get; init; }
|
||||
/// <summary>Gets the data type name.</summary>
|
||||
public string? DataType { get; init; }
|
||||
/// <summary>Gets a value indicating whether the tag is writable.</summary>
|
||||
public bool? Writable { get; init; }
|
||||
/// <summary>Gets the string length for string types.</summary>
|
||||
public int? StringLength { get; init; }
|
||||
/// <summary>Gets a value indicating whether write is idempotent.</summary>
|
||||
public bool? WriteIdempotent { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Data transfer object for S7 probe configuration.</summary>
|
||||
internal sealed class S7ProbeDto
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user