refactor(driver-abcip): extract AbCipDriverOptions to .Contracts
Move AbCipDriverOptions (and AbCipDataType enum) to a new Driver.AbCip.Contracts sibling project. AbCipDataTypeExtensions (which uses DriverDataType from Core.Abstractions) stays in the runtime driver as AbCipDataTypeExtensions.cs. Replace two doc-comment <see cref="Core.Abstractions.IAlarmSource"/> and <see cref="Core.Abstractions.IHostConnectivityProbe"/> with <c>X</c> per the approved decision — no compilable using was present. The runtime Driver.AbCip project gains a ProjectReference to .Contracts; the .slnx is updated accordingly.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// Logix atomic + string data types, plus a <c>Structure</c> marker used when a tag
|
||||
/// references a UDT / predefined structure (Timer, Counter, Control). The concrete UDT
|
||||
/// shape is resolved via the CIP Template Object at discovery time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Mirrors the shape of <c>ModbusDataType</c>. Atomic Logix names (BOOL / SINT / INT / DINT /
|
||||
/// LINT / REAL / LREAL / STRING / DT) map one-to-one; BIT + BOOL-in-DINT collapse into
|
||||
/// <c>Bool</c> with the <c>.N</c> bit-index carried on the <c>AbCipTagPath</c>
|
||||
/// rather than the data type itself.
|
||||
/// </remarks>
|
||||
public enum AbCipDataType
|
||||
{
|
||||
Bool,
|
||||
SInt, // signed 8-bit
|
||||
Int, // signed 16-bit
|
||||
DInt, // signed 32-bit
|
||||
LInt, // signed 64-bit
|
||||
USInt, // unsigned 8-bit (Logix 5000 post-V21)
|
||||
UInt, // unsigned 16-bit
|
||||
UDInt, // unsigned 32-bit
|
||||
ULInt, // unsigned 64-bit
|
||||
Real, // 32-bit IEEE-754
|
||||
LReal, // 64-bit IEEE-754
|
||||
String, // Logix STRING (DINT Length + SINT[82] DATA — flattened to .NET string by libplctag)
|
||||
Dt, // Date/Time — Logix DT == DINT representing seconds-since-epoch per Rockwell conventions
|
||||
/// <summary>
|
||||
/// UDT / Predefined Structure (Timer / Counter / Control / Message / Axis). Shape is
|
||||
/// resolved at discovery time; reads + writes fan out to member Variables unless the
|
||||
/// caller has explicitly opted into whole-UDT decode.
|
||||
/// </summary>
|
||||
Structure,
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
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. Pre-declared tags always emit during
|
||||
/// discovery; opt in to controller-side discovery via
|
||||
/// <see cref="EnableControllerBrowse"/>.
|
||||
/// </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>
|
||||
/// When <c>true</c>, <c>DiscoverAsync</c> walks each device's Logix symbol table via
|
||||
/// the <c>@tags</c> pseudo-tag + surfaces controller-resident globals under a
|
||||
/// <c>Discovered/</c> sub-folder. Pre-declared tags always emit regardless. Default
|
||||
/// <c>false</c> to keep the strict-config path for deployments where only declared tags
|
||||
/// should appear in the address space.
|
||||
/// </summary>
|
||||
public bool EnableControllerBrowse { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Task #177 — when <c>true</c>, declared ALMD tags are surfaced as alarm conditions
|
||||
/// via <c>IAlarmSource</c>; the driver polls each subscribed
|
||||
/// alarm's <c>InFaulted</c> + <c>Severity</c> members + fires <c>OnAlarmEvent</c> on
|
||||
/// state transitions. Default <c>false</c> — operators explicitly opt in because
|
||||
/// projection semantics don't exactly mirror Rockwell FT Alarm & Events; shops
|
||||
/// running FT Live should keep this off + take alarms through the native route.
|
||||
/// </summary>
|
||||
public bool EnableAlarmProjection { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Poll interval for the ALMD projection loop. Shorter intervals catch faster edges
|
||||
/// at the cost of PLC round-trips; edges shorter than this interval are invisible to
|
||||
/// the projection (a 0→1→0 transition within one tick collapses to no event). Default
|
||||
/// 1 second — matches typical SCADA alarm-refresh conventions.
|
||||
/// </summary>
|
||||
public TimeSpan AlarmPollInterval { get; init; } = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in for the declaration-only whole-UDT read fast path. When <c>false</c> (the
|
||||
/// default) a batch of UDT members is always read per-member, because the byte offsets
|
||||
/// computed by <see cref="AbCipUdtMemberLayout"/> assume the controller lays members
|
||||
/// out in declaration order — and the Studio 5000 compiler does NOT guarantee that
|
||||
/// (it reorders for largest-first packing, BOOL host bytes, nested-struct padding).
|
||||
/// Decoding at declaration-order offsets against a reordered controller layout yields
|
||||
/// silently-plausible wrong numbers. Set <c>true</c> only when the operator has
|
||||
/// hand-verified that every configured UDT's member declaration order matches the
|
||||
/// controller's compiled layout; in that case whole-UDT grouping collapses N member
|
||||
/// reads into one. The richer CIP Template Object path remains the long-term fix.
|
||||
/// </summary>
|
||||
public bool EnableDeclarationOnlyUdtGrouping { get; init; }
|
||||
}
|
||||
|
||||
/// <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 the family
|
||||
/// <c>AllowPacking</c> default, <c>ConnectionSize</c> default, unconnected-only hint, and
|
||||
/// other quirks; per-device overrides via <see cref="AllowPacking"/> and
|
||||
/// <see cref="ConnectionSize"/> take precedence when set.</param>
|
||||
/// <param name="DeviceName">Optional display label for Admin UI. Falls back to <see cref="HostAddress"/>.</param>
|
||||
/// <param name="AllowPacking">Driver.AbCip-013 — per-device override for CIP request-packing
|
||||
/// (firmware 20+). <c>null</c> (the default) inherits the family profile's
|
||||
/// <c>SupportsRequestPacking</c>; set explicitly to opt a single device in or out without
|
||||
/// touching every other device on the same family.</param>
|
||||
/// <param name="ConnectionSize">Driver.AbCip-013 — per-device override for the Forward Open
|
||||
/// ConnectionSize (Large Forward Open packet size in bytes). <c>null</c> inherits the family
|
||||
/// profile's <c>DefaultConnectionSize</c>. Honoured by the driver layer; the underlying
|
||||
/// libplctag 1.5.2 wrapper has no direct <c>ConnectionSize</c> property, so the value is
|
||||
/// plumbed through <see cref="AbCipTagCreateParams"/> for forward-compat with future wrapper
|
||||
/// versions or a custom tag-attribute path; current builds use the family profile default at
|
||||
/// the wire layer regardless.</param>
|
||||
public sealed record AbCipDeviceOptions(
|
||||
string HostAddress,
|
||||
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
|
||||
string? DeviceName = null,
|
||||
bool? AllowPacking = null,
|
||||
int? ConnectionSize = 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>
|
||||
/// <param name="Members">For <see cref="AbCipDataType.Structure"/>-typed tags, the declared UDT
|
||||
/// member layout. When supplied, discovery fans out the UDT into a folder + one Variable per
|
||||
/// member (member TagPath = <c>{tag.TagPath}.{member.Name}</c>). When <c>null</c> on a Structure
|
||||
/// tag, the driver treats it as a black-box and relies on downstream configuration to address
|
||||
/// members individually via dotted <see cref="AbCipTagPath"/> syntax. Ignored for atomic types.</param>
|
||||
/// <param name="SafetyTag">GuardLogix safety-partition tag hint. When <c>true</c>, the driver
|
||||
/// forces <c>SecurityClassification.ViewOnly</c> on discovery regardless of
|
||||
/// <paramref name="Writable"/> — safety tags can only be written from the safety task of a
|
||||
/// GuardLogix controller; non-safety writes violate the safety-partition isolation and are
|
||||
/// rejected by the PLC anyway. Surfaces the intent explicitly instead of relying on the
|
||||
/// write attempt failing at runtime.</param>
|
||||
public sealed record AbCipTagDefinition(
|
||||
string Name,
|
||||
string DeviceHostAddress,
|
||||
string TagPath,
|
||||
AbCipDataType DataType,
|
||||
bool Writable = true,
|
||||
bool WriteIdempotent = false,
|
||||
IReadOnlyList<AbCipStructureMember>? Members = null,
|
||||
bool SafetyTag = false);
|
||||
|
||||
/// <summary>
|
||||
/// One declared member of a UDT tag. Name is the member identifier on the PLC (e.g. <c>Speed</c>,
|
||||
/// <c>Status</c>), DataType is the atomic Logix type, Writable/WriteIdempotent mirror
|
||||
/// <see cref="AbCipTagDefinition"/>. Declaration-driven — the real CIP Template Object reader
|
||||
/// (class 0x6C) that would auto-discover member layouts lands as a follow-up PR.
|
||||
/// </summary>
|
||||
public sealed record AbCipStructureMember(
|
||||
string Name,
|
||||
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 <c>IHostConnectivityProbe</c>
|
||||
/// state transitions + Admin UI health status.
|
||||
/// </summary>
|
||||
public sealed class AbCipProbeOptions
|
||||
{
|
||||
/// <summary>Gets a value indicating whether the probe is enabled.</summary>
|
||||
public bool Enabled { get; init; } = true;
|
||||
/// <summary>Gets the interval at which the probe reads the probe tag.</summary>
|
||||
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
|
||||
/// <summary>Gets the timeout for each probe read operation.</summary>
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// Tag path used for the probe. When <see cref="Enabled"/> is <c>true</c> but this is
|
||||
/// <c>null</c>/blank, the driver logs a warning and runs no probe loops (Driver.AbCip-011);
|
||||
/// <c>GetHostStatuses()</c> will then report every device as <c>Unknown</c>. A family-default
|
||||
/// system-tag fallback (e.g. <c>@raw_cpu_type</c> on ControlLogix) is a deferred follow-up;
|
||||
/// today an operator opting into the probe must supply a tag path explicitly.
|
||||
/// </summary>
|
||||
public string? ProbeTagPath { get; init; }
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<!-- NO PackageReference. -->
|
||||
</Project>
|
||||
Reference in New Issue
Block a user