v3(b1-twincat): apply Modbus exemplar pattern to TwinCAT (multi-device)
- Rename TwinCATEquipmentTagParser -> TwinCATTagDefinitionFactory; TryParse ->
FromTagConfig(tagConfig, rawPath, out def). Drop leading-'{' heuristic + the
deviceHostAddress key; def.Name = rawPath. Keep strict-enum + Inspect; add a
Structure guard (sole tag path now) + inverse ToTagConfig.
- Options: Tags -> RawTags (RawTagEntry); keep Devices + protocol fields.
- Driver: _tagsByRawPath (Ordinal); byName-only resolver; build table from
_options.RawTags via FromTagConfig, threading WriteIdempotent + DeviceName.
Multi-device: ResolveDeviceHost matches entry.DeviceName against options.Devices
(by name, then host) -> def.DeviceHostAddress (TODO(v3 WaveC): live host from the
Device row's DeviceConfig). DiscoverAsync now emits from the table.
- Factory: retire pre-declared tags[] DTO path; bind rawTags; keep Devices.
- Cli: BuildOptions serialises typed defs -> RawTagEntry via ToTagConfig.
- Tests: TwinCATRawTags helper; migrate Tags= -> RawTags=; rename parser tests to
FromTagConfig; equipment/resolve-host/array tests delivered via RawTags.
Gate: Contracts + Driver build green; Driver.TwinCAT.Tests 192 pass, Cli.Tests 70 pass.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
@@ -57,9 +58,11 @@ public static class TwinCATDriverFactoryExtensions
|
||||
$"TwinCAT config for '{driverInstanceId}' has a device missing HostAddress"),
|
||||
DeviceName: d.DeviceName))]
|
||||
: [],
|
||||
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 TwinCAT
|
||||
// tags (RawPath + TagConfig blob + WriteIdempotent + owning DeviceName). The driver maps each
|
||||
// through TwinCATTagDefinitionFactory at Initialize; the legacy pre-declared DTO tag path
|
||||
// (tags[] → BuildTag) is retired.
|
||||
RawTags = dto.RawTags is { Count: > 0 } ? [.. dto.RawTags] : [],
|
||||
Probe = new TwinCATProbeOptions
|
||||
{
|
||||
Enabled = dto.Probe?.Enabled ?? true,
|
||||
@@ -85,43 +88,6 @@ public static class TwinCATDriverFactoryExtensions
|
||||
public static TwinCATDriverOptions ParseOptionsForTests(string driverConfigJson, string driverInstanceId)
|
||||
=> ParseOptions(driverConfigJson, driverInstanceId);
|
||||
|
||||
private static TwinCATTagDefinition BuildTag(TwinCATTagDto t, string driverInstanceId)
|
||||
{
|
||||
var dataType = ParseEnum<TwinCATDataType>(t.DataType, t.Name, driverInstanceId, "DataType");
|
||||
|
||||
if (dataType == TwinCATDataType.Structure)
|
||||
throw new InvalidOperationException(
|
||||
$"TwinCAT tag '{t.Name ?? "<unnamed>"}' in '{driverInstanceId}' specifies " +
|
||||
"DataType 'Structure'. The driver does not support UDT/FB-instance pre-declared tags. " +
|
||||
"Use EnableControllerBrowse to discover UDT members individually, or declare " +
|
||||
"individual atomic-typed tags for the fields you need.");
|
||||
|
||||
return new TwinCATTagDefinition(
|
||||
Name: t.Name ?? throw new InvalidOperationException(
|
||||
$"TwinCAT config for '{driverInstanceId}' has a tag missing Name"),
|
||||
DeviceHostAddress: t.DeviceHostAddress ?? throw new InvalidOperationException(
|
||||
$"TwinCAT tag '{t.Name}' in '{driverInstanceId}' missing DeviceHostAddress"),
|
||||
SymbolPath: t.SymbolPath ?? throw new InvalidOperationException(
|
||||
$"TwinCAT tag '{t.Name}' in '{driverInstanceId}' missing SymbolPath"),
|
||||
DataType: dataType,
|
||||
Writable: t.Writable ?? true,
|
||||
WriteIdempotent: t.WriteIdempotent ?? false,
|
||||
ArrayLength: t.ArrayLength is > 0 ? t.ArrayLength : null);
|
||||
}
|
||||
|
||||
private static T ParseEnum<T>(string? raw, string? tagName, string driverInstanceId, string field)
|
||||
where T : struct, Enum
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
throw new InvalidOperationException(
|
||||
$"TwinCAT tag '{tagName ?? "<unnamed>"}' in '{driverInstanceId}' missing {field}");
|
||||
return Enum.TryParse<T>(raw, ignoreCase: true, out var v)
|
||||
? v
|
||||
: throw new InvalidOperationException(
|
||||
$"TwinCAT tag '{tagName}' has unknown {field} '{raw}'. " +
|
||||
$"Expected one of {string.Join(", ", Enum.GetNames<T>())}");
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
@@ -146,8 +112,10 @@ public static class TwinCATDriverFactoryExtensions
|
||||
/// <summary>Gets or sets the list of configured devices.</summary>
|
||||
public List<TwinCATDeviceDto>? Devices { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the list of configured tags.</summary>
|
||||
public List<TwinCATTagDto>? Tags { get; init; }
|
||||
/// <summary>Gets or sets the authored raw tags (RawPath + TagConfig blob + WriteIdempotent +
|
||||
/// owning DeviceName) the deploy artifact delivers. The driver maps each through the
|
||||
/// tag-definition factory at Initialize; the legacy pre-declared <c>tags[]</c> path is retired.</summary>
|
||||
public List<RawTagEntry>? RawTags { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the probe configuration.</summary>
|
||||
public TwinCATProbeDto? Probe { get; init; }
|
||||
@@ -162,35 +130,6 @@ public static class TwinCATDriverFactoryExtensions
|
||||
public string? DeviceName { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class TwinCATTagDto
|
||||
{
|
||||
/// <summary>Gets or sets the tag name.</summary>
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the device host address.</summary>
|
||||
public string? DeviceHostAddress { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the symbol path.</summary>
|
||||
public string? SymbolPath { get; init; }
|
||||
|
||||
/// <summary>Gets or sets the data type.</summary>
|
||||
public string? DataType { get; init; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether the tag is writable.</summary>
|
||||
public bool? Writable { get; init; }
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether writes are idempotent.</summary>
|
||||
public bool? WriteIdempotent { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional 1-D array element count. When positive, the tag is a 1-D array of this
|
||||
/// many <see cref="DataType"/> elements — drives <c>IsArray</c>/<c>ArrayDim</c> at
|
||||
/// discovery and a native ADS array read at runtime (Phase 4c).
|
||||
/// <c>null</c> or non-positive = scalar (the default).
|
||||
/// </summary>
|
||||
public int? ArrayLength { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class TwinCATProbeDto
|
||||
{
|
||||
/// <summary>Gets or sets a value indicating whether probing is enabled.</summary>
|
||||
|
||||
Reference in New Issue
Block a user