using System.ComponentModel.DataAnnotations;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
///
/// FOCAS driver configuration. One instance supports N CNC devices. Each device gets its own
/// (DriverInstanceId, HostAddress) per-host pipeline key at the Phase 6.1 resilience layer.
///
public sealed class FocasDriverOptions
{
/// Gets the list of configured CNC devices.
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 + ); the driver maps each through
/// into its RawPath → definition table,
/// routing each tag to its device by . FOCAS has no online
/// tag-discovery for user tags — the driver serves exactly these (plus the fixed-node tree).
///
public IReadOnlyList RawTags { get; init; } = [];
/// Gets the probe options.
public FocasProbeOptions Probe { get; init; } = new();
/// Gets the timeout duration for operations.
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
/// Gets the alarm projection options.
public FocasAlarmProjectionOptions AlarmProjection { get; init; } = new();
/// Gets the handle recycle options.
public FocasHandleRecycleOptions HandleRecycle { get; init; } = new();
/// Gets the fixed tree options.
public FocasFixedTreeOptions FixedTree { get; init; } = new();
/// Gets the PMC range-coalescing options.
public FocasPmcCoalesceOptions PmcCoalesce { get; init; } = new();
///
/// 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.
///
[Display(Name = "Probe timeout (seconds)", Description = "Connection test timeout. Default 10s.", GroupName = "Diagnostics")]
[Range(1, 60)]
public int ProbeTimeoutSeconds { get; init; } = 10;
}
///
/// Merges adjacent / near PMC reads in one poll batch into fewer pmc_rdpmcrng calls.
/// On by default: a batch with a single PMC tag per area is unaffected, and a coalesced read the CNC
/// rejects falls back to per-tag reads, so enabling it cannot make a readable tag unreadable.
///
public sealed class FocasPmcCoalesceOptions
{
/// Gets a value indicating whether PMC coalescing is on.
public bool Enabled { get; init; } = true;
///
/// Largest range, in bytes, one coalesced read may cover. Bounds the response size and the
/// blast radius of a rejected range. Default 128.
///
[Range(1, 1024)]
public int MaxSpanBytes { get; init; } = 128;
///
/// Largest run of bytes nobody asked for that may sit between two merged tags. Reading a few
/// spare bytes is cheaper than a second round-trip, but every spare byte must be a valid PMC
/// address on the CNC. Default 8; 0 merges only strictly adjacent tags.
///
[Range(0, 1024)]
public int MaxGapBytes { get; init; } = 8;
}
///
/// Fixed-node tree exposed by FOCAS per docs/v2/driver-specs.md §7 —
/// Identity/, Axes/{name}/, etc. populated from
/// cnc_sysinfo / cnc_rdaxisname / cnc_rddynamic2. Disabled by
/// default so existing configs that only use user-authored tags don't grow new
/// nodes on upgrade.
///
public sealed class FocasFixedTreeOptions
{
/// Gets or sets a value indicating whether the fixed-node tree is enabled for every configured device.
public bool Enabled { get; init; } = false;
///
/// Poll cadence for cnc_rddynamic2. Each tick calls the API once per
/// configured axis + publishes OnDataChange for the axis subtree. Real CNCs
/// serve ~100ms loops comfortably; the default is conservative.
///
public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(250);
///
/// Poll cadence for program + operation-mode info. Slower than the axis
/// poll because program / mode transitions happen on operator timescales.
/// Zero / negative disables the program poll entirely.
///
public TimeSpan ProgramPollInterval { get; init; } = TimeSpan.FromSeconds(1);
///
/// Poll cadence for timers (power-on / operating / cutting / cycle).
/// These change at human timescales — default is 30s. Zero / negative
/// disables the timer poll entirely.
///
public TimeSpan TimerPollInterval { get; init; } = TimeSpan.FromSeconds(30);
}
///
/// Proactive session-recycle cadence. Fanuc CNCs have a finite FWLIB handle pool
/// (~5–10 concurrent connections) and certain series have documented handle-leak bugs
/// that manifest after long uptime. When is true the
/// driver closes + reopens each device's session on the cadence,
/// forcing FWLIB to release its handle slot back to the pool. Reads / writes during
/// recycle wait for the reconnect rather than failing — worst case an operator sees a
/// brief read latency spike once per cadence.
///
///
/// Disabled by default because a healthy CNC + driver doesn't need it. Enable when
/// field experience shows handle exhaustion against a specific series / firmware.
/// Typical tuning: 30 min for sites running multiple OtOpcUa instances against the
/// same CNC (they share the pool); 6 h for a single-client deployment.
///
public sealed class FocasHandleRecycleOptions
{
/// Gets or sets a value indicating whether handle recycling is enabled.
public bool Enabled { get; init; } = false;
/// Gets or sets the interval for handle recycle operations.
public TimeSpan Interval { get; init; } = TimeSpan.FromHours(1);
}
///
/// Controls the CNC active-alarm polling projection that surfaces FOCAS alarms via
/// IAlarmSource as Part 9 conditions at ALARM: raw tags.
///
///
/// Authoring an ALARM: tag IS the opt-in. The projection polls exactly the devices that own a
/// subscribed alarm tag, so a driver with none issues no cnc_rdalmmsg2 traffic whatever
/// says. Previously gated the poll and
/// SubscribeAlarmsAsync THREW while it was false — so an operator who authored an alarm tag and left
/// the default got a feed that stayed gated with a Warning on every reconnect. It is retained so existing
/// configs still bind, and no longer has any effect.
///
public sealed class FocasAlarmProjectionOptions
{
/// Retained for config compatibility; has no effect (see the class remarks).
public bool Enabled { get; init; } = false;
/// Poll cadence. One cnc_rdalmmsg2 call per device per tick.
public TimeSpan PollInterval { get; init; } = TimeSpan.FromSeconds(2);
}
///
/// One CNC the driver talks to. enables per-series
/// address validation at FocasDriver.InitializeAsync; leave as
/// to skip validation (legacy behaviour).
///
///
/// FOCAS TCP endpoint in focas://{ip}[:{port}] form, e.g.
/// focas://10.20.30.40:8193. The port defaults to 8193 when omitted.
/// Parsed by FocasHostAddress.TryParse at FocasDriver.InitializeAsync.
///
///
/// Optional human-readable label shown in the OPC UA address-space folder for this
/// device. Defaults to when null.
///
///
/// CNC controller series. Used by to gate which
/// FOCAS addresses are valid. Leave as (the
/// default) to skip range validation and preserve legacy behaviour.
///
///
/// Fallback only. A real CNC's cnc_rddynamic2 position block carries its own
/// scale (raw / base^exponent), which the driver always applies and which wins over
/// this setting. Only a block that carries no scale (a 4-byte payload, e.g. an older
/// simulator) is divided by 10^PositionDecimalPlaces. Default 0 (no scaling).
/// Negative values are clamped to 0.
///
public sealed record FocasDeviceOptions(
string HostAddress,
string? DeviceName = null,
FocasCncSeries Series = FocasCncSeries.Unknown,
int PositionDecimalPlaces = 0)
{
///
/// Axis-position decimal places, clamped to a non-negative value so the
/// 10^PositionDecimalPlaces divide at the publish seam can never misbehave.
///
public int PositionDecimalPlaces { get; init; } =
PositionDecimalPlaces < 0 ? 0 : PositionDecimalPlaces;
}
///
/// One FOCAS-backed OPC UA variable. is the canonical FOCAS
/// address string that parses via FocasAddress.TryParse —
/// X0.0 / R100 / PARAM:1815/0 / MACRO:500.
///
///
/// Whether the tag is declared writable in config. Reflected at the
/// DiscoverAsync address-space advertisement seam and gates the driver's
/// write path — FOCAS supports PMC/data writes (see FocasPmcBitRmwTests /
/// FocasReadWriteTests). Defaults to true.
///
///
/// Whether repeated writes of the same value are safe. Threaded through to
/// DriverAttributeInfo.WriteIdempotent by DiscoverAsync so OPC UA
/// clients can optimise write coalescing for idempotent tags. Defaults to false.
///
///
/// The tag's cross-driver alarm object, when it carries one — which makes the host
/// materialise it as a Part 9 condition at its RawPath. Only an ALARM: address can drive one.
///
public sealed record FocasTagDefinition(
string Name,
string DeviceHostAddress,
string Address,
FocasDataType DataType,
bool Writable = true,
bool WriteIdempotent = false,
FocasTagAlarm? Alarm = null);
///
/// What the FOCAS driver reads from a tag's cross-driver alarm object. The host owns the
/// rest of it (alarmType, historizeToAveva) through TagConfigIntent.
///
///
/// The authored alarm.severity (an integer), or when the object has none. An
/// authored severity is sent as the condition's exact Part 9 severity; without one the severity comes from
/// the displayed CNC alarm's type (FocasAlarmConditionMapper).
///
public sealed record FocasTagAlarm(int? Severity);
///
/// Controls periodic connectivity probing. One cnc_rdcncstat call per
/// configured device per tick; transitions fire OnHostStatusChanged.
/// Enabled by default so the driver surfaces a fast host-state transition when
/// a CNC goes offline between data reads.
///
public sealed class FocasProbeOptions
{
/// Gets or sets a value indicating whether probing is enabled.
public bool Enabled { get; init; } = true;
/// Gets or sets the probe interval.
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
/// Gets or sets the probe timeout.
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
}