Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverOptions.cs
2026-04-26 02:31:50 -04:00

431 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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), #143144 (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>
/// L5K (Studio 5000 controller export) imports merged into <see cref="Tags"/> at
/// <c>InitializeAsync</c>. Each entry points at one L5K file + the device whose tags it
/// describes; the parser extracts <c>TAG</c> + <c>DATATYPE</c> blocks and produces
/// <see cref="AbCipTagDefinition"/> records (alias tags + ExternalAccess=None tags
/// skipped — see <see cref="Import.L5kIngest"/>). Pre-declared <see cref="Tags"/> entries
/// win on <c>Name</c> conflicts so operators can override import results without
/// editing the L5K source.
/// </summary>
public IReadOnlyList<AbCipL5kImportOptions> L5kImports { get; init; } = [];
/// <summary>
/// L5X (Studio 5000 XML controller export) imports merged into <see cref="Tags"/> at
/// <c>InitializeAsync</c>. Same shape and merge semantics as <see cref="L5kImports"/> —
/// the entries differ only in source format. Pre-declared <see cref="Tags"/> entries win
/// on <c>Name</c> conflicts; entries already produced by <see cref="L5kImports"/> also win
/// so an L5X re-export of the same controller doesn't double-emit. See
/// <see cref="Import.L5xParser"/> for the format-specific mechanics.
/// </summary>
public IReadOnlyList<AbCipL5xImportOptions> L5xImports { get; init; } = [];
/// <summary>
/// Kepware-format CSV imports merged into <see cref="Tags"/> at <c>InitializeAsync</c>.
/// Same merge semantics as <see cref="L5kImports"/> / <see cref="L5xImports"/> —
/// pre-declared <see cref="Tags"/> entries win on <c>Name</c> conflicts, and tags
/// produced by earlier import collections (L5K → L5X → CSV in call order) also win
/// so an Excel-edited copy of the same controller does not double-emit. See
/// <see cref="Import.CsvTagImporter"/> for the column layout + parse rules.
/// </summary>
public IReadOnlyList<AbCipCsvImportOptions> CsvImports { 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 <see cref="Core.Abstractions.IAlarmSource"/>; 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 &amp; 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>
/// PR abcip-3.1 — optional sink for non-fatal driver warnings (legacy-firmware
/// <c>ConnectionSize</c> mis-match, etc.). Production hosting wires this to Serilog;
/// unit tests pin a list-collecting lambda to assert which warnings fired. <c>null</c>
/// swallows warnings — convenient for back-compat deployments that don't care.
/// </summary>
public Action<string>? OnWarning { 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 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>
/// <param name="ConnectionSize">PR abcip-3.1 — optional override for the family-default
/// <see cref="PlcFamilies.AbCipPlcFamilyProfile.DefaultConnectionSize"/>. Threads through to
/// libplctag's <c>connection_size</c> attribute on the underlying tag handle so operators can
/// dial the CIP Forward Open buffer down for legacy firmware (v19-and-earlier ControlLogix
/// caps at 504) or up for high-throughput shops on FW20+. Validated against the Kepware
/// supported range [500..4002] at <c>InitializeAsync</c>; out-of-range values fault the
/// driver. <c>null</c> uses the family default — back-compat with deployments that haven't
/// touched the knob.</param>
/// <param name="AddressingMode">PR abcip-3.2 — controls whether the driver addresses tags by
/// ASCII symbolic path (the default), by CIP logical-segment instance ID, or asks the driver
/// to pick. Logical addressing skips per-poll ASCII parsing on every read and unlocks
/// symbol-table-cached scans for 500+-tag projects, but requires a one-time symbol-table
/// walk at first read + is unsupported on Micro800 / SLC500 / PLC5 (their CIP firmware does
/// not honour Symbol Object instance IDs). When the user picks <see cref="AbCip.AddressingMode.Logical"/>
/// against an unsupported family the driver logs a warning + falls back to symbolic so
/// misconfiguration does not fault the driver. <see cref="AbCip.AddressingMode.Auto"/> currently
/// resolves to symbolic — a future PR will plumb a real auto-detection heuristic; the docs
/// in <c>docs/drivers/AbCip-Performance.md</c> §"Addressing mode" call this out.</param>
/// <param name="ReadStrategy">PR abcip-3.3 — picks how a multi-member UDT batch is read on this
/// device. <see cref="AbCip.ReadStrategy.WholeUdt"/> issues one read per parent UDT and decodes
/// each subscribed member from the buffer in-memory (the historical behaviour that ships in
/// task #194 — best when a large fraction of a UDT's members are subscribed).
/// <see cref="AbCip.ReadStrategy.MultiPacket"/> bundles per-member reads into one CIP
/// Multi-Service Packet — best for sparse UDT subscriptions where reading the whole UDT
/// buffer just to extract one or two fields wastes wire bandwidth. <see cref="AbCip.ReadStrategy.Auto"/>
/// (the default) lets the planner pick per-batch using
/// <paramref name="MultiPacketSparsityThreshold"/>: if the subscribed-member fraction is below
/// the threshold MultiPacket wins, otherwise WholeUdt wins. Family compatibility — Micro800 /
/// SLC500 / PLC5 lack Multi-Service-Packet support per
/// <see cref="PlcFamilies.AbCipPlcFamilyProfile.SupportsRequestPacking"/>; user-forced
/// <see cref="AbCip.ReadStrategy.MultiPacket"/> against those families logs a warning + falls
/// back to <see cref="AbCip.ReadStrategy.WholeUdt"/> at device-init time. The libplctag .NET
/// wrapper (1.5.x) does not expose a public knob for explicit Multi-Service-Packet bundling,
/// so today's MultiPacket runtime issues one libplctag read per member; the planner's grouping
/// is still load-bearing because it gives the runtime the right plan to execute when an
/// upstream wrapper release exposes wire-level bundling.</param>
/// <param name="MultiPacketSparsityThreshold">PR abcip-3.3 — sparsity-threshold knob the planner
/// uses when <paramref name="ReadStrategy"/> is <see cref="AbCip.ReadStrategy.Auto"/>. The
/// planner divides <c>subscribedMembers / totalMembers</c> for each parent UDT in a batch;
/// a fraction strictly less than the threshold picks
/// <see cref="AbCip.ReadStrategy.MultiPacket"/>, else <see cref="AbCip.ReadStrategy.WholeUdt"/>.
/// Default <c>0.25</c> — picked because reading 1/4 of a UDT's members is the rough break-even
/// where the wire-cost of one whole-UDT read still beats N member reads on ControlLogix's
/// 4002-byte connection size; see <c>docs/drivers/AbCip-Performance.md</c> §"Read strategy".
/// Clamped to <c>[0..1]</c> at planner time; values outside the range silently saturate.</param>
public sealed record AbCipDeviceOptions(
string HostAddress,
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
string? DeviceName = null,
int? ConnectionSize = null,
AddressingMode AddressingMode = AddressingMode.Auto,
ReadStrategy ReadStrategy = ReadStrategy.Auto,
double MultiPacketSparsityThreshold = 0.25);
/// <summary>
/// PR abcip-3.3 — per-device strategy for reading multi-member UDT batches. <see cref="WholeUdt"/>
/// mirrors the task #194 behaviour: one libplctag read on the parent tag, each subscribed member
/// decoded from the buffer at its computed offset. <see cref="MultiPacket"/> bundles per-member
/// reads into one CIP Multi-Service Packet so sparse UDT subscriptions don't pay for the whole
/// UDT buffer. <see cref="Auto"/> lets the planner pick per-batch using
/// <see cref="AbCipDeviceOptions.MultiPacketSparsityThreshold"/>.
/// </summary>
/// <remarks>
/// <para>Strategy resolution lives at two layers:</para>
/// <list type="bullet">
/// <item><b>Device init</b> — user-forced <see cref="MultiPacket"/> against a family whose
/// profile sets <see cref="PlcFamilies.AbCipPlcFamilyProfile.SupportsRequestPacking"/>
/// = <c>false</c> (Micro800, SLC500, PLC5) falls back to <see cref="WholeUdt"/> with a
/// warning. <see cref="Auto"/> stays as-is (the planner re-evaluates per batch).</item>
/// <item><b>Per-batch (Auto only)</b> — for each parent UDT in the request set, the planner
/// computes <c>subscribedMembers / totalMembers</c> and routes the group through
/// <see cref="MultiPacket"/> when the fraction is below the threshold, else
/// <see cref="WholeUdt"/>.</item>
/// </list>
/// <para>libplctag .NET wrapper (1.5.x) does not expose explicit Multi-Service-Packet bundling,
/// so today's runtime issues one libplctag read per member when the planner picks MultiPacket —
/// the same wrapper limitation called out in PR abcip-3.1 (ConnectionSize) and PR abcip-3.2
/// (instance-ID addressing). The planner's grouping is still observable from tests + future-proofs
/// the driver for when an upstream wrapper release exposes wire-level bundling.</para>
/// </remarks>
public enum ReadStrategy
{
/// <summary>Driver picks per-batch based on
/// <see cref="AbCipDeviceOptions.MultiPacketSparsityThreshold"/>. Default.</summary>
Auto = 0,
/// <summary>One read per parent UDT; members decoded from the buffer in-memory. Best when a
/// large fraction of the UDT's members are subscribed (dense reads).</summary>
WholeUdt = 1,
/// <summary>Bundle per-member reads into one CIP Multi-Service Packet. Best when only a few
/// members of a large UDT are subscribed (sparse reads). Unsupported on Micro800 / SLC500 /
/// PLC5; the driver warns + falls back to <see cref="WholeUdt"/> at device init.</summary>
MultiPacket = 2,
}
/// <summary>
/// PR abcip-3.2 — how the AB CIP driver addresses tags on a given device. <see cref="Symbolic"/>
/// is the historical default + matches every previous driver build: each read carries the tag
/// name as ASCII bytes + the controller parses the path on every request. <see cref="Logical"/>
/// uses CIP logical-segment instance IDs (Symbol Object class 0x6B) — the controller looks the
/// tag up in its own symbol table once + the driver caches the resolved instance ID for
/// subsequent reads, eliminating the per-poll ASCII parse step. <see cref="Auto"/> lets the
/// driver pick (today: always Symbolic; a future PR fingerprints the controller and switches
/// to Logical when supported).
/// </summary>
/// <remarks>
/// Logical addressing requires a one-time symbol-table walk at the first read on the device
/// (the driver issues an <c>@tags</c> read via <see cref="LibplctagTagEnumerator"/> and stores
/// the name → instance-id map on the per-device <c>DeviceState</c>). It is unsupported on
/// Micro800 / SLC500 / PLC5 — see <see cref="PlcFamilies.AbCipPlcFamilyProfile.SupportsLogicalAddressing"/>.
/// The libplctag .NET wrapper (1.5.x) does not expose a public knob for instance-ID
/// addressing, so the driver translates Logical → libplctag attribute via reflection on
/// <c>NativeTagWrapper.SetAttributeString</c> — same best-effort fallback pattern as
/// PR abcip-3.1's ConnectionSize plumbing.
/// </remarks>
public enum AddressingMode
{
/// <summary>Driver picks. Currently resolves to <see cref="Symbolic"/>; future PR may
/// auto-detect based on family + firmware + symbol-table size.</summary>
Auto = 0,
/// <summary>ASCII symbolic-path addressing — the libplctag default. Per-poll ASCII parse on
/// the controller; works on every CIP family.</summary>
Symbolic = 1,
/// <summary>CIP logical-segment / instance-ID addressing. Requires a one-time
/// symbol-table walk at first read; subsequent reads skip ASCII parsing on the
/// controller. Unsupported on Micro800 / SLC500 / PLC5.</summary>
Logical = 2,
}
/// <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>
/// <param name="StringLength">Capacity of the DATA character array on a Logix STRING / STRINGnn
/// UDT — 82 for the stock <c>STRING</c>, 20/40/80/etc for user-defined <c>STRING_20</c>,
/// <c>STRING_40</c>, <c>STRING_80</c> variants. Threads through libplctag's
/// <c>str_max_capacity</c> attribute so the wrapper allocates the correct backing buffer
/// and <c>GetString</c> / <c>SetString</c> truncate at the right boundary. <c>null</c>
/// keeps libplctag's default 82-byte STRING behaviour for back-compat. Ignored for
/// non-<see cref="AbCipDataType.String"/> types.</param>
/// <param name="Description">Tag description carried from the L5K/L5X export (or set explicitly
/// in pre-declared config). Surfaces as the OPC UA <c>Description</c> attribute on the
/// produced Variable node so SCADA / engineering clients see the comment from the source
/// project. <c>null</c> leaves Description unset, matching pre-2.3 behaviour.</param>
/// <param name="ScanRateMs">PR abcip-4.1 — optional per-tag publish rate (in milliseconds) that
/// overrides the subscription's default <c>publishingInterval</c> for this tag. Mirrors
/// Kepware's "scan classes" + Siemens / Mitsubishi per-tag scan groups; the driver buckets
/// tags by resolved interval at <see cref="AbCipDriver.SubscribeAsync"/> time + runs one
/// <see cref="Core.Abstractions.PollGroupEngine"/> loop per distinct interval so a fast HMI
/// tag is not delayed behind a slow batch tag's 10 s tick. <c>null</c> = use the subscription
/// default (legacy behaviour). The 100 ms floor enforced by the engine still applies — a
/// <c>ScanRateMs &lt; 100</c> is clamped up. UDT member tags inherit the parent tag's
/// <c>ScanRateMs</c> at member-fan-out time. See
/// <c>docs/drivers/AbCip-Operability.md</c> §"Per-tag scan rate".</param>
/// <param name="WriteDeadband">PR abcip-4.2 — optional numeric write deadband. When set and both
/// the previous successfully-written value and the new write are numeric, the driver suppresses
/// the next write if <c>|new - last| &lt; WriteDeadband</c>. Suppressed writes still return
/// <c>Good</c> so the OPC UA write semantics observed by clients are unchanged — the driver
/// simply skips the wire round-trip. Mirrors Kepware's "Deadband (write)" knob and is the
/// write-side companion to the read-side deadband already shipped at the OPC UA monitored-item
/// layer. NaN / Infinity values bypass suppression (let the wire decide). See
/// <c>docs/drivers/AbCip-Operability.md</c> §"Write deadband / write-on-change".</param>
/// <param name="WriteOnChange">PR abcip-4.2 — optional write-on-change gate. When <c>true</c> and
/// the new write equals the previous successfully-written value, the driver suppresses the
/// write (returns <c>Good</c> without hitting the wire). Combines with <see cref="WriteDeadband"/>
/// for numeric tags — the deadband test takes priority for numerics, equality is the fallback
/// for non-numeric types (BOOL setpoints, STRING constants, etc.). Default <c>false</c> —
/// legacy behaviour where every write goes to the wire.</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,
int? StringLength = null,
string? Description = null,
int? ScanRateMs = null,
double? WriteDeadband = null,
bool WriteOnChange = 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>
/// <remarks>
/// <para><see cref="Description"/> carries the per-member comment from L5K/L5X UDT definitions so
/// the OPC UA Variable nodes produced for individual members surface their descriptions too,
/// not just the top-level tag.</para>
/// <para>PR abcip-2.6 — <see cref="AoiQualifier"/> tags AOI parameters as Input / Output /
/// InOut / Local. Plain UDT members default to <see cref="AoiQualifier.Local"/>. Discovery
/// groups Input / Output / InOut members under sub-folders so an AOI-typed tag fans out as
/// <c>Tag/Inputs/...</c>, <c>Tag/Outputs/...</c>, <c>Tag/InOut/...</c> while Local stays at the
/// UDT root — matching how AOIs visually present in Studio 5000.</para>
/// </remarks>
public sealed record AbCipStructureMember(
string Name,
AbCipDataType DataType,
bool Writable = true,
bool WriteIdempotent = false,
int? StringLength = null,
string? Description = null,
AoiQualifier AoiQualifier = AoiQualifier.Local);
/// <summary>
/// PR abcip-2.6 — directional qualifier for AOI parameters. Surfaces the Studio 5000
/// <c>Usage</c> attribute (<c>Input</c> / <c>Output</c> / <c>InOut</c>) so discovery can group
/// AOI members into sub-folders and downstream consumers can reason about parameter direction.
/// Plain UDT members (non-AOI types) default to <see cref="Local"/>, which keeps them at the
/// UDT root + indicates they are internal storage rather than a directional parameter.
/// </summary>
public enum AoiQualifier
{
/// <summary>UDT member or AOI local tag — non-directional, browsed at the parent's root.</summary>
Local,
/// <summary>AOI input parameter — written by the caller, read by the AOI body.</summary>
Input,
/// <summary>AOI output parameter — written by the AOI body, read by the caller.</summary>
Output,
/// <summary>AOI bidirectional parameter — passed by reference, both sides may read/write.</summary>
InOut,
}
/// <summary>
/// One L5K-import entry. Either <see cref="FilePath"/> or <see cref="InlineText"/> must be
/// set (FilePath wins when both supplied — useful for tests that pre-load fixtures into
/// options without touching disk).
/// </summary>
/// <param name="DeviceHostAddress">Target device <c>HostAddress</c> tags from this file are bound to.</param>
/// <param name="FilePath">On-disk path to a <c>*.L5K</c> export. Loaded eagerly at InitializeAsync.</param>
/// <param name="InlineText">Pre-loaded L5K body — used by tests + Admin UI uploads.</param>
/// <param name="NamePrefix">Optional prefix prepended to imported tag names to avoid collisions
/// when ingesting multiple files into one driver instance.</param>
public sealed record AbCipL5kImportOptions(
string DeviceHostAddress,
string? FilePath = null,
string? InlineText = null,
string NamePrefix = "");
/// <summary>
/// One L5X-import entry. Mirrors <see cref="AbCipL5kImportOptions"/> field-for-field — the
/// two are kept as distinct types so configuration JSON makes the source format explicit
/// (an L5X file under an <c>L5kImports</c> entry would parse-fail confusingly otherwise).
/// </summary>
/// <param name="DeviceHostAddress">Target device <c>HostAddress</c> tags from this file are bound to.</param>
/// <param name="FilePath">On-disk path to a <c>*.L5X</c> XML export. Loaded eagerly at InitializeAsync.</param>
/// <param name="InlineText">Pre-loaded L5X body — used by tests + Admin UI uploads.</param>
/// <param name="NamePrefix">Optional prefix prepended to imported tag names to avoid collisions
/// when ingesting multiple files into one driver instance.</param>
public sealed record AbCipL5xImportOptions(
string DeviceHostAddress,
string? FilePath = null,
string? InlineText = null,
string NamePrefix = "");
/// <summary>
/// One Kepware-format CSV import entry. Field shape mirrors <see cref="AbCipL5kImportOptions"/>
/// so configuration JSON stays consistent across the three import sources.
/// </summary>
/// <param name="DeviceHostAddress">Target device <c>HostAddress</c> tags from this file are bound to.</param>
/// <param name="FilePath">On-disk path to a Kepware-format <c>*.csv</c>. Loaded eagerly at InitializeAsync.</param>
/// <param name="InlineText">Pre-loaded CSV body — used by tests + Admin UI uploads.</param>
/// <param name="NamePrefix">Optional prefix prepended to imported tag names to avoid collisions.</param>
public sealed record AbCipCsvImportOptions(
string DeviceHostAddress,
string? FilePath = null,
string? InlineText = null,
string NamePrefix = "");
/// <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; }
}