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; } }