using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
///
/// AB CIP / EtherNet-IP driver configuration, bound from the driver's DriverConfig
/// JSON at DriverHost.RegisterAsync. One instance supports N devices (PLCs) behind
/// the same driver; per-device routing is keyed on the tag's device routing key (the RawPath's
/// device segment, delivered on ).
///
public sealed class AbCipDriverOptions
{
///
/// PLCs this driver instance talks to. Each device contributes its own
/// string as the hostName key used by resilience pipelines and the Admin UI.
///
///
/// Wave C moves the device's live connection host into the Device row's DeviceConfig;
/// for Wave B the list stays bound from DriverConfig as today, and a tag routes to its device
/// by matching against the device's name ()
/// or its .
///
public IReadOnlyList Devices { get; init; } = [];
///
/// Authored raw tags this driver serves. The deploy artifact hands each authored raw
/// Tag as a (RawPath identity + driver TagConfig blob +
/// WriteIdempotent flag + DeviceName routing key); the driver maps each through
/// into its RawPath → definition table.
/// Pre-declared tags always emit during discovery; opt in to controller-side discovery via
/// .
///
public IReadOnlyList RawTags { get; init; } = [];
/// Per-device probe settings. Falls back to defaults when omitted.
public AbCipProbeOptions Probe { get; init; } = new();
///
/// 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.
///
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
///
/// When true, DiscoverAsync walks each device's Logix symbol table via
/// the @tags pseudo-tag + surfaces controller-resident globals under a
/// Discovered/ sub-folder. Pre-declared tags always emit regardless. Default
/// false to keep the strict-config path for deployments where only declared tags
/// should appear in the address space.
///
public bool EnableControllerBrowse { get; init; }
///
/// When true, declared ALMD tags are surfaced as alarm conditions
/// via IAlarmSource; the driver polls each subscribed
/// alarm's InFaulted + Severity members + fires OnAlarmEvent on
/// state transitions. Default false — 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.
///
public bool EnableAlarmProjection { get; init; }
///
/// 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.
///
public TimeSpan AlarmPollInterval { get; init; } = TimeSpan.FromSeconds(1);
///
/// Opt-in for the declaration-only whole-UDT read fast path. When false (the
/// default) a batch of UDT members is always read per-member, because the byte offsets
/// computed by 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 true 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.
///
public bool EnableDeclarationOnlyUdtGrouping { get; init; }
///
/// Timeout for the AdminUI Test Connect probe, in seconds. The AdminUI clamps to a
/// 60s server-side maximum; this default is what the form pre-fills for new instances.
///
/// Valid range: 1–60 seconds; the AdminUI clamps to 60s server-side.
public int ProbeTimeoutSeconds { get; init; } = 5;
}
///
/// One PLC endpoint. must parse via
/// ; misconfigured devices fail driver
/// initialization rather than silently connecting to nothing.
///
/// Canonical ab://gateway[:port]/cip-path string.
/// Which per-family profile to apply. Determines the family
/// AllowPacking default, ConnectionSize default, unconnected-only hint, and
/// other quirks; per-device overrides via and
/// take precedence when set.
/// Optional display label for Admin UI. Falls back to .
/// Per-device override for CIP request-packing
/// (firmware 20+). null (the default) inherits the family profile's
/// SupportsRequestPacking; set explicitly to opt a single device in or out without
/// touching every other device on the same family.
/// Per-device override for the Forward Open
/// ConnectionSize (Large Forward Open packet size in bytes). null inherits the family
/// profile's DefaultConnectionSize. Honoured by the driver layer; the underlying
/// libplctag 1.5.2 wrapper has no direct ConnectionSize property, so the value is
/// plumbed through 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.
public sealed record AbCipDeviceOptions(
string HostAddress,
AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
string? DeviceName = null,
bool? AllowPacking = null,
int? ConnectionSize = null);
///
/// One AB-backed OPC UA variable. Mirrors the ModbusTagDefinition shape.
///
/// Tag name; becomes the OPC UA browse name and full reference (the RawPath in v3).
/// The tag's device routing key — the RawPath's device segment,
/// threaded in from by the driver at table-build time (the mapper
/// leaves it empty; device placement is no longer in the TagConfig blob). Resolved against the driver's
/// devices by matching or .
/// TODO(v3 WaveC): once the connection host moves to the Device row's DeviceConfig this is a pure logical
/// device name; the name still reads DeviceHostAddress to avoid churn across the driver + CLI.
/// Logix symbolic path (controller or program scope).
/// Logix atomic type, or for UDT-typed tags.
/// When true and the tag's ExternalAccess permits writes, IWritable routes writes here.
/// Safe to replay on write timeout. Default false.
/// For -typed tags, the declared UDT
/// member layout. When supplied, discovery fans out the UDT into a folder + one Variable per
/// member (member TagPath = {tag.TagPath}.{member.Name}). When null on a Structure
/// tag, the driver treats it as a black-box and relies on downstream configuration to address
/// members individually via dotted syntax. Ignored for atomic types.
/// GuardLogix safety-partition tag hint. When true, the driver
/// forces SecurityClassification.ViewOnly on discovery regardless of
/// — 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.
/// Phase 4c — number of array elements for a 1-D array tag. Reads via
/// libplctag's elem_count into an element-typed CLR array when
/// is set; 1 for a scalar. Ignored for .
/// Review I-1 — the EXPLICIT array signal. true ⟺ the source TagConfig
/// had isArray:true (with arrayLength >= 1); the tag discovers as an OPC UA
/// array node (IsArray + ArrayDim) and reads as a typed CLR array — even when
/// is 1 (a valid 1-element array). ElementCount alone
/// cannot carry this because a scalar and a 1-element array both have a count of 1.
/// Ignored for .
public sealed record AbCipTagDefinition(
string Name,
string DeviceHostAddress,
string TagPath,
AbCipDataType DataType,
bool Writable = true,
bool WriteIdempotent = false,
IReadOnlyList? Members = null,
bool SafetyTag = false,
int ElementCount = 1,
bool IsArray = false);
///
/// One declared member of a UDT tag. Name is the member identifier on the PLC (e.g. Speed,
/// Status), DataType is the atomic Logix type, Writable/WriteIdempotent mirror
/// . Declaration-driven — the real CIP Template Object reader
/// (class 0x6C) that would auto-discover member layouts lands as a follow-up PR.
///
/// The member identifier on the PLC.
/// The atomic Logix type of the member.
/// Whether the member is writable.
/// Whether writes to the member are idempotent.
/// Number of array elements for a 1-D array member; 1 for scalar.
/// Review I-1 — the EXPLICIT array signal for a member: true ⟺ the
/// member is a 1-D array (even of length 1). Discovers as an OPC UA array node when set.
public sealed record AbCipStructureMember(
string Name,
AbCipDataType DataType,
bool Writable = true,
bool WriteIdempotent = false,
int ElementCount = 1,
bool IsArray = false);
/// Which AB PLC family the device is — selects the profile applied to connection params.
public enum AbCipPlcFamily
{
ControlLogix,
CompactLogix,
Micro800,
GuardLogix,
}
///
/// Background connectivity-probe settings. Enabled by default; the probe reads a cheap tag
/// on the PLC at the configured interval to drive IHostConnectivityProbe
/// state transitions + Admin UI health status.
///
public sealed class AbCipProbeOptions
{
/// Gets a value indicating whether the probe is enabled.
public bool Enabled { get; init; } = true;
/// Gets the interval at which the probe reads the probe tag.
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
/// Gets the timeout for each probe read operation.
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
///
/// Tag path used for the probe. When is true but this is
/// null/blank, the driver logs a warning and runs no probe loops;
/// GetHostStatuses() will then report every device as Unknown. A family-default
/// system-tag fallback (e.g. @raw_cpu_type on ControlLogix) is a deferred follow-up;
/// today an operator opting into the probe must supply a tag path explicitly.
///
public string? ProbeTagPath { get; init; }
}