AB CIP PR 2 — scaffolding + Core (AbCipDriver skeleton + libplctag binding + host / tag-path / data-type / status-code parsers + per-family profiles + SafeHandle wrapper + test harness). Ships everything needed to stand up the driver project as a compiling assembly with no wire calls yet — PR 3 adds IReadable against ab_server which is the first PR that actually touches the native library. Project reference shape matches Modbus / OpcUaClient / S7 (only Core.Abstractions, no Core / Configuration / Polly) so the driver stays lean and doesn't drag EF Core into every deployment that wants AB support. libplctag 1.5.2 pinned (1.6.x only exists as alpha — stable 1.5 series covers ControlLogix / CompactLogix / Micro800 / SLC500 / PLC-5 / MicroLogix which matches plan decision #11 family coverage). libplctag.NativeImport arrives transitively. AbCipHostAddress parses ab://gateway[:port]/cip-path canonical strings end-to-end: handles hostname or IP gateway, optional explicit port (default 44818 EtherNet-IP reserved), CIP path including bridged routes (1,2,2,10.0.0.10,1,0), empty path for Micro800 / MicroLogix without backplane routing, case-insensitive scheme, default-port stripping in canonical form for round-trip stability. Opaque string survives straight into libplctag's gateway / path attributes so no translation layer at wire time. AbCipTagPath handles the full Logix symbolic tag surface — controller-scope (Motor1_Speed), program-scope (Program:MainProgram.StepIndex), structured member access (Motor1.Speed.Setpoint), multi-dim array subscripts (Matrix[1,2,3]), bit-within-DINT via .N syntax (Flags.3, Motor.Status.12) with valid range 0-31 per Logix 5000 General Instructions Reference. Structural capture so PR 6 UDT work can walk the path against a cached template without reparsing. Rejects malformed shapes (empty scopes, ident starting with digit, spaces, empty/negative/non-numeric subscripts, unbalanced brackets, leading / trailing dots). Round-trips via ToLibplctagName producing the exact string libplctag's name attribute expects. AbCipDataType mirrors ModbusDataType shape — atomic Bool / SInt / Int / DInt / LInt / USInt / UInt / UDInt / ULInt / Real / LReal / String / Dt plus a Structure marker for UDT-typed tags (resolved via CIP Template Object at discovery time in PR 5/6). ToDriverDataType adapter follows the Modbus widening convention for unsigned + 64-bit until DriverDataType picks those up. AbCipStatusMapper covers the CIP general-status values an AB PLC actually returns during normal operation (0x00/0x04/0x05/0x06/0x08/0x0A/0x0B/0x0E/0x10/0x13/0x16) + libplctag PLCTAG_STATUS_* codes (0, >0 pending, negative error families). Mirrors ModbusDriver.MapModbusExceptionToStatus so Admin UI status displays stay uniform across drivers. PlcTagHandle is a SafeHandle around the int32 native tag ID with plc_tag_destroy slot wired as a no-op for PR 2 (P/Invoke DllImport arrives with PR 3 when the wire calls land). Lifetime guaranteed by the SafeHandle finalizer — every leaked handle gets cleaned up even when the owner is GC'd without explicit Dispose. IsInvalid when native ID <= 0 so destroying a negative (error) handle never happens. Critical because driver-specs.md §3 flags libplctag native heap as invisible to GetMemoryFootprint — leaked handles directly feed the Tier-B recycle trigger. AbCipDriverOptions captures the multi-device shape — one driver instance can talk to N PLCs via Devices[] (each with HostAddress + PlcFamily + optional DeviceName); Tags[] references devices by HostAddress as the cross-key; AbCipProbeOptions + driver-wide Timeout. AbCipDriver implements IDriver only — InitializeAsync parses every device's HostAddress and selects its PlcFamilyProfile (fails fast on malformed strings via InvalidOperationException → Faulted health), per-device state cached in a DeviceState record with parsed address + profile + empty TagHandles dict for later PRs. ReinitializeAsync is the Tier-B escape hatch — shuts down every device, disposes every PlcTagHandle via SafeHandle lifetime, reinitializes from options. ShutdownAsync clears the device dict and flips health to Unknown. PlcFamilies/AbCipPlcFamilyProfile gives four baseline profiles — ControlLogix (4002 ConnectionSize, path 1,0, Large Forward Open + request packing + connected messaging, FW20+ baseline), CompactLogix (narrower 504 default for 5069-L3x safety), Micro800 (488 cap, empty path, unconnected-only, no request packing), GuardLogix (shares ControlLogix wire protocol — safety partition is tag-level, surfaced as ViewOnly in PR 12). Tests — 76 new cases across 4 test classes — AbCipHostAddressTests (10 valid shapes, 10 invalid shapes, ToString canonicalization, round-trip stability), AbCipTagPathTests (18 cases including multi-scope / multi-member / multi-subscript / bit-in-DINT / rejected shapes / underscore idents / round-trip), AbCipStatusMapperTests (12 CIP + 8 libplctag codes), AbCipDriverTests (IDriver lifecycle + multi-device init + malformed-address fault + per-family profile lookup + PlcTagHandle invalid/dispose idempotency + AbCipDataType mapping). Full solution builds 0 errors; 254 warnings are pre-existing xUnit1051 CancellationToken hints outside this PR. Solution file updated to include both new projects. Unblocks PR 3 (IReadable against ab_server) which is the first PR to exercise the native library end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverOptions.cs
Normal file
91
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverOptions.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// AB CIP / EtherNet-IP driver configuration, bound from the driver's <c>DriverConfig</c>
|
||||
/// JSON at <c>DriverHost.RegisterAsync</c>. One instance supports N devices (PLCs) behind
|
||||
/// the same driver; per-device routing is keyed on <see cref="AbCipDeviceOptions.HostAddress"/>
|
||||
/// via <c>IPerCallHostResolver</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Per v2 plan decisions #11 (libplctag), #41 (AbCip vs AbLegacy split), #143–144 (per-call
|
||||
/// host resolver + resilience keys), #144 (bulkhead keyed on <c>(DriverInstanceId, HostName)</c>).
|
||||
/// </remarks>
|
||||
public sealed class AbCipDriverOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// PLCs this driver instance talks to. Each device contributes its own <see cref="AbCipHostAddress"/>
|
||||
/// string as the <c>hostName</c> key used by resilience pipelines and the Admin UI.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AbCipDeviceOptions> Devices { get; init; } = [];
|
||||
|
||||
/// <summary>Pre-declared tag map across all devices — AB discovery lands in PR 5.</summary>
|
||||
public IReadOnlyList<AbCipTagDefinition> Tags { get; init; } = [];
|
||||
|
||||
/// <summary>Per-device probe settings. Falls back to defaults when omitted.</summary>
|
||||
public AbCipProbeOptions Probe { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Default libplctag call timeout applied to reads/writes/discovery when the caller does
|
||||
/// not pass a more specific value. Matches the Modbus driver's 2-second default.
|
||||
/// </summary>
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One PLC endpoint. <see cref="HostAddress"/> must parse via
|
||||
/// <see cref="AbCipHostAddress.TryParse"/>; misconfigured devices fail driver
|
||||
/// initialization rather than silently connecting to nothing.
|
||||
/// </summary>
|
||||
/// <param name="HostAddress">Canonical <c>ab://gateway[:port]/cip-path</c> string.</param>
|
||||
/// <param name="PlcFamily">Which per-family profile to apply. Determines ConnectionSize,
|
||||
/// request-packing support, unconnected-only hint, and other quirks.</param>
|
||||
/// <param name="DeviceName">Optional display label for Admin UI. Falls back to <see cref="HostAddress"/>.</param>
|
||||
public sealed record AbCipDeviceOptions(
|
||||
string HostAddress,
|
||||
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
|
||||
string? DeviceName = null);
|
||||
|
||||
/// <summary>
|
||||
/// One AB-backed OPC UA variable. Mirrors the <c>ModbusTagDefinition</c> shape.
|
||||
/// </summary>
|
||||
/// <param name="Name">Tag name; becomes the OPC UA browse name and full reference.</param>
|
||||
/// <param name="DeviceHostAddress">Which device (<see cref="AbCipDeviceOptions.HostAddress"/>) this tag lives on.</param>
|
||||
/// <param name="TagPath">Logix symbolic path (controller or program scope).</param>
|
||||
/// <param name="DataType">Logix atomic type, or <see cref="AbCipDataType.Structure"/> for UDT-typed tags.</param>
|
||||
/// <param name="Writable">When <c>true</c> and the tag's ExternalAccess permits writes, IWritable routes writes here.</param>
|
||||
/// <param name="WriteIdempotent">Per plan decisions #44–#45, #143 — safe to replay on write timeout. Default <c>false</c>.</param>
|
||||
public sealed record AbCipTagDefinition(
|
||||
string Name,
|
||||
string DeviceHostAddress,
|
||||
string TagPath,
|
||||
AbCipDataType DataType,
|
||||
bool Writable = true,
|
||||
bool WriteIdempotent = false);
|
||||
|
||||
/// <summary>Which AB PLC family the device is — selects the profile applied to connection params.</summary>
|
||||
public enum AbCipPlcFamily
|
||||
{
|
||||
ControlLogix,
|
||||
CompactLogix,
|
||||
Micro800,
|
||||
GuardLogix,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Background connectivity-probe settings. Enabled by default; the probe reads a cheap tag
|
||||
/// on the PLC at the configured interval to drive <see cref="Core.Abstractions.IHostConnectivityProbe"/>
|
||||
/// state transitions + Admin UI health status.
|
||||
/// </summary>
|
||||
public sealed class AbCipProbeOptions
|
||||
{
|
||||
public bool Enabled { get; init; } = true;
|
||||
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// Tag path used for the probe. If null, the driver attempts to read a default
|
||||
/// system tag (PR 8 wires this up — the choice is family-dependent, e.g.
|
||||
/// <c>@raw_cpu_type</c> on ControlLogix or a user-configured probe tag on Micro800).
|
||||
/// </summary>
|
||||
public string? ProbeTagPath { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user