namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
///
/// FOCAS driver configuration. One instance supports N CNC devices. Per plan decision #144
/// each device gets its own (DriverInstanceId, HostAddress) bulkhead key at the
/// Phase 6.1 resilience layer.
///
public sealed class FocasDriverOptions
{
public IReadOnlyList Devices { get; init; } = [];
public IReadOnlyList Tags { get; init; } = [];
public FocasProbeOptions Probe { get; init; } = new();
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
///
/// Fixed-tree behaviour knobs (issue #262, plan PR F1-f). Carries the
/// ApplyFigureScaling toggle that gates the cnc_getfigure
/// decimal-place division applied to position values before publishing.
///
public FocasFixedTreeOptions FixedTree { get; init; } = new();
///
/// Alarm projection knobs (issue #267, plan PR F3-a). Default mode is
/// — the projection only surfaces
/// currently-active alarms. Operators who want the on-CNC ring-buffer history
/// replayed as historic OPC UA events (so dashboards see the real CNC timestamp,
/// not the moment the projection polled) flip this to
/// .
///
public FocasAlarmProjectionOptions AlarmProjection { get; init; } = new();
///
/// Driver-level write opt-in (issue #268, plan PR F4-a). Defaults to
/// Enabled = false — the driver short-circuits every IWritable.WriteAsync
/// call to until the deployment explicitly
/// flips this on. Combined with the per-tag
/// gate (also default-off), every CNC write requires two opt-ins.
///
public FocasWritesOptions Writes { get; init; } = new();
}
///
/// Driver-level write controls (issue #268, plan PR F4-a). Per the F4-a decision record
/// writes ship behind a flag with a safe default: an operator who pulls the FOCAS driver
/// into production without touching Writes.Enabled gets read-only behaviour, and
/// even with the flag flipped on each individual tag must still set
/// = true.
///
public sealed record FocasWritesOptions
{
///
/// Driver-level master switch. Default false — every write returns
/// with the status text
/// "writes disabled at driver level".
///
public bool Enabled { get; init; } = false;
///
/// Issue #269, plan PR F4-b — granular kill-switch for cnc_wrparam
/// parameter writes (defense in depth on top of and the
/// per-tag ). Default false: an
/// operator who flips on without explicitly opting into
/// parameter writes still gets
/// for every PARAM: tag. A misdirected parameter write can put the CNC
/// in a bad state, so the third opt-in keeps the blast radius bounded.
/// Server-layer ACL: PARAM: tags additionally surface a
/// classification
/// so the OPC UA gate requires WriteConfigure group membership; this
/// flag is the driver-level kill switch the operator team can flip without a
/// redeploy.
///
public bool AllowParameter { get; init; } = false;
///
/// Issue #269, plan PR F4-b — granular kill-switch for cnc_wrmacro macro
/// variable writes (defense in depth on top of and the
/// per-tag ). Default false:
/// macro writes are gated separately from parameter writes because they're a
/// normal HMI-driven recipe / setpoint surface where parameter writes are
/// mostly emergency commissioning territory.
/// Server-layer ACL: MACRO: tags surface
/// so the OPC UA
/// gate requires WriteOperate group membership.
///
public bool AllowMacro { get; init; } = false;
///
/// Issue #270, plan PR F4-c — granular kill-switch for pmc_wrpmcrng PMC
/// range writes (and the bit-level read-modify-write that wraps it). Default
/// false: PMC is ladder working memory — a mistargeted bit can move
/// motion, latch a feedhold, or flip a safety interlock. Even with
/// on and a tag's
/// flag flipped on, PMC writes stay locked until this third opt-in fires.
/// Server-layer ACL: PMC tags surface
/// so the OPC UA
/// gate requires WriteOperate group membership; this flag is the driver-
/// level kill switch the operator team can flip without a redeploy.
///
public bool AllowPmc { get; init; } = false;
}
///
/// Mode for the FOCAS alarm projection (issue #267, plan PR F3-a). Default
/// matches today's behaviour — only currently-active
/// alarms surface as OPC UA events. additionally
/// polls cnc_rdalmhistry on connect + on a configurable cadence and emits the
/// ring-buffer entries as historic events, deduped by (OccurrenceTime, AlarmNumber,
/// AlarmType) so a polled entry never re-fires.
///
public enum FocasAlarmProjectionMode
{
/// Surface only currently-active CNC alarms. No history poll. Default.
ActiveOnly = 0,
///
/// Surface active alarms plus the on-CNC ring-buffer history. The projection
/// polls cnc_rdalmhistry on connect and on
/// ticks afterward.
/// Each new entry (keyed by (OccurrenceTime, AlarmNumber, AlarmType))
/// fires an with
/// SourceTimestampUtc set from the CNC's reported timestamp, not Now.
///
ActivePlusHistory = 1,
}
///
/// FOCAS alarm-projection knobs (issue #267, plan PR F3-a). Carries the mode switch +
/// the cadence / depth tuning for the cnc_rdalmhistry poll loop. Defaults match
/// "operator dashboard with five-minute refresh" — the single most common deployment
/// shape per the F3-a deployment doc.
///
public sealed record FocasAlarmProjectionOptions
{
/// Default poll interval — 5 minutes. Matches dashboard-class cadences.
public static readonly TimeSpan DefaultHistoryPollInterval = TimeSpan.FromMinutes(5);
///
/// Default ring-buffer depth requested per poll — 100. Most FANUC controllers
/// keep ~100 entries by default; pulling the full depth on every poll keeps the
/// dedup set authoritative across reconnects without burning extra wire bandwidth on
/// entries the dedup key would discard anyway.
///
public const int DefaultHistoryDepth = 100;
///
/// Hard ceiling on . The projection clamps user-requested
/// depths above this value down — typical CNC ring buffers cap well below this and
/// letting an operator type 10000 by accident shouldn't take down the wire
/// session with a giant cnc_rdalmhistry request.
///
public const int MaxHistoryDepth = 250;
/// Active-only (default) vs Active-plus-history. See .
public FocasAlarmProjectionMode Mode { get; init; } = FocasAlarmProjectionMode.ActiveOnly;
///
/// Cadence at which the projection re-polls cnc_rdalmhistry when
/// is .
/// Default = 5 minutes. Only applies after
/// the on-connect poll fires.
///
public TimeSpan HistoryPollInterval { get; init; } = DefaultHistoryPollInterval;
///
/// Number of most-recent ring-buffer entries to request per poll. Clamped to
/// [1..] at projection startup so misconfigured
/// values can't hammer the CNC. Default = 100.
///
public int HistoryDepth { get; init; } = DefaultHistoryDepth;
}
///
/// Per-driver fixed-tree options. New installs default
/// to true so position values surface in user units (mm / inch). Existing
/// deployments that already published raw scaled integers can flip this to false
/// for migration parity — the operator-facing concern is that switching the flag
/// mid-deployment changes the values clients see, so the migration path is
/// documentation-only (issue #262).
///
public sealed record FocasFixedTreeOptions
{
///
/// When true (default), position values from cnc_absolute /
/// cnc_machine / cnc_relative / cnc_distance /
/// cnc_actf are divided by 10^decimalPlaces per axis using the
/// cnc_getfigure snapshot cached at probe time. When false, the
/// raw integer values are published unchanged — used for migrations from
/// older drivers that didn't apply the scaling.
///
public bool ApplyFigureScaling { get; init; } = true;
}
///
/// One CNC the driver talks to. enables per-series
/// address validation at ; leave as
/// to skip validation (legacy behaviour).
/// declares the four MTB-specific override
/// cnc_rdparam numbers surfaced under Override/; pass null to
/// suppress the entire Override/ subfolder for that device (issue #259).
/// (issue #271, plan PR F4-d) is the CNC connection-level
/// password emitted via cnc_wrunlockparam on connect when the controller
/// gates parameter writes / certain reads behind a password switch (16i + some
/// 30i firmwares with parameter-protect on).
///
///
/// No-log invariant: is a secret. The driver MUST NOT
/// log it. FocasDeviceOptions.ToString() would include the field by default
/// because it's a positional record member, so the record's auto-generated
/// ToString is overridden via below to redact
/// the password. Any new logging surface that touches
/// must continue to redact. See docs/v2/focas-deployment.md § "FOCAS password
/// handling" for the no-log invariant and rotation runbook.
///
public sealed record FocasDeviceOptions(
string HostAddress,
string? DeviceName = null,
FocasCncSeries Series = FocasCncSeries.Unknown,
FocasOverrideParameters? OverrideParameters = null,
string? Password = null)
{
///
/// Issue #271 (plan PR F4-d) — record auto-generated ToString would print
/// verbatim. Override the printer so the secret is replaced
/// with "***" when the field is non-null. The no-log invariant relies on
/// this — every Serilog destructure that flows a
/// value through {Device} gets redaction for free.
///
private bool PrintMembers(System.Text.StringBuilder builder)
{
builder.Append("HostAddress = ").Append(HostAddress);
builder.Append(", DeviceName = ").Append(DeviceName);
builder.Append(", Series = ").Append(Series);
builder.Append(", OverrideParameters = ").Append(OverrideParameters);
builder.Append(", Password = ").Append(Password is null ? "" : "***");
return true;
}
}
///
/// One FOCAS-backed OPC UA variable. is the canonical FOCAS
/// address string that parses via —
/// X0.0 / R100 / PARAM:1815/0 / MACRO:500 /
/// DIAG:1031 / DIAG:280/2.
///
///
/// defaults to false per issue #268 / plan PR F4-a — a
/// newly-onboarded tag is read-only until the deployment explicitly opts it in, matching
/// the driver-level safer-by-default posture.
/// is plumbed through the
/// retry path at the
/// server layer (see ); a
/// true value lets the Polly pipeline retry on transient failures while
/// false (the default) disables retry per decisions #44/#45.
///
public sealed record FocasTagDefinition(
string Name,
string DeviceHostAddress,
string Address,
FocasDataType DataType,
bool Writable = false,
bool WriteIdempotent = false);
public sealed class FocasProbeOptions
{
public bool Enabled { get; init; } = true;
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
}