Phase 1 Stream A — Core.Abstractions project + 11 capability interfaces + DriverTypeRegistry + interface-independence tests

New project src/ZB.MOM.WW.OtOpcUa.Core.Abstractions (.NET 10, BCL-only dependencies, GenerateDocumentationFile=true, TreatWarningsAsErrors=true) defining the contract surface every driver implements. Per docs/v2/plan.md decisions #4 (composable capability interfaces), #52 (streaming IAddressSpaceBuilder), #53 (capability discovery via `is` checks no flag enum), #54 (optional IRediscoverable sub-interface), #59 (Core.Abstractions internal-only for now design as if public).

Eleven capability interfaces:
- IDriver — required lifecycle / health / config-apply / memory-footprint accounting (per driver-stability.md Tier A/B allocation tracking)
- ITagDiscovery — discovers tags streaming to IAddressSpaceBuilder
- IReadable — on-demand reads idempotent for Polly retry
- IWritable — writes NOT auto-retried by default per decisions #44 + #45
- ISubscribable — data-change subscriptions covering both native (Galaxy MXAccess advisory, OPC UA monitored items, TwinCAT ADS) and driver-internal polled (Modbus, AB CIP, S7, FOCAS) mechanisms; OnDataChange callback regardless of source
- IAlarmSource — alarm events + acknowledge + AlarmSeverity enum mirroring acl-design.md NodePermissions alarm-severity values
- IHistoryProvider — HistoryReadRaw + HistoryReadProcessed with continuation points
- IRediscoverable — opt-in change-detection signal; static drivers don't implement
- IHostConnectivityProbe — generalized from Galaxy's GalaxyRuntimeProbeManager per plan §5a
- IDriverConfigEditor — Admin UI plug-point for per-driver custom config editors deferred to each driver's phase per decision #27
- IAddressSpaceBuilder — streaming builder API for driver-driven address-space construction

Plus DTOs: DriverDataType, SecurityClassification (mirroring v1 Galaxy model), DriverAttributeInfo (replaces Galaxy-specific GalaxyAttributeInfo per plan §5a), DriverHealth + DriverState, DataValueSnapshot (universal OPC UA quality + timestamp carrier per decision #13), HostConnectivityStatus + HostState + HostStatusChangedEventArgs, RediscoveryEventArgs, DataChangeEventArgs, AlarmEventArgs + AlarmAcknowledgeRequest + AlarmSeverity, WriteRequest + WriteResult, HistoryReadResult + HistoryAggregateType, ISubscriptionHandle + IAlarmSubscriptionHandle + IVariableHandle.

DriverTypeRegistry singleton with Register / Get / TryGet / All; thread-safe via Interlocked.Exchange snapshot replacement on registration; case-insensitive lookups; rejects duplicate registrations; rejects empty type names. DriverTypeMetadata record carries TypeName + AllowedNamespaceKinds (NamespaceKindCompatibility flags enum per decision #111) + per-config-tier JSON Schemas the validator checks at draft-publish time (decision #91).

Tests project tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests (xUnit v3 1.1.0 matching existing test projects). 24 tests covering: 1) interface independence reflection check (no references outside BCL/System; all public types in root namespace; every capability interface is public); 2) DriverTypeRegistry round-trip, case-insensitive lookups, KeyNotFoundException on unknown, null on TryGet of unknown, InvalidOperationException on duplicate registration (case-insensitive too), All() enumeration, NamespaceKindCompatibility bitmask combinations, ArgumentException on empty type names.

Build: 0 errors, 4 warnings (only pre-existing transitive package vulnerability + analyzer hints). Full test suite: 845 passing / 1 failing — strict improvement over Phase 0 baseline (821/1) by the 24 new Core.Abstractions tests; no regressions in any other test project.

Phase 1 entry-gate record (docs/v2/implementation/entry-gate-phase-1.md) documents the deviation: only Stream A executed in this continuation since Streams B-E need SQL Server / GLAuth / Galaxy infrastructure standup per dev-environment.md Step 1, which is currently TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-17 14:15:55 -04:00
parent 45ffa3e7d4
commit 980ea5190c
23 changed files with 941 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver-agnostic value snapshot returned by <see cref="IReadable"/> and pushed
/// by <see cref="ISubscribable.OnDataChange"/>. Mirrors the OPC UA <c>DataValue</c>
/// shape so the node-manager can pass through quality, source timestamp, and
/// server timestamp without translation.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/plan.md</c> decision #13 — every driver maps to the same
/// OPC UA StatusCode space; this DTO is the universal carrier.
/// </remarks>
/// <param name="Value">The raw value; null when <see cref="StatusCode"/> indicates Bad.</param>
/// <param name="StatusCode">OPC UA status code (numeric value matches the OPC UA spec).</param>
/// <param name="SourceTimestampUtc">Driver-side timestamp when the value was sampled at the source. Null if unavailable.</param>
/// <param name="ServerTimestampUtc">Driver-side timestamp when the driver received / processed the value.</param>
public sealed record DataValueSnapshot(
object? Value,
uint StatusCode,
DateTime? SourceTimestampUtc,
DateTime ServerTimestampUtc);

View File

@@ -0,0 +1,28 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver-agnostic per-attribute (tag) descriptor used by the generic node-manager
/// to build OPC UA address-space variables. Every driver maps its native attribute
/// metadata into this DTO during discovery.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/plan.md</c> §5a (LmxNodeManager reusability) — <c>DriverAttributeInfo</c>
/// replaces the v1 Galaxy-specific <c>GalaxyAttributeInfo</c> in the generic node-manager
/// so the same node-manager class works against every driver.
/// </remarks>
/// <param name="FullName">
/// Driver-side full reference for read/write addressing
/// (e.g. for Galaxy: <c>"DelmiaReceiver_001.DownloadPath"</c>).
/// </param>
/// <param name="DriverDataType">Driver-agnostic data type; maps to OPC UA built-in type at build time.</param>
/// <param name="IsArray">True when this attribute is a 1-D array.</param>
/// <param name="ArrayDim">Declared array length when <see cref="IsArray"/> is true; null otherwise.</param>
/// <param name="SecurityClass">Write-authorization tier for this attribute.</param>
/// <param name="IsHistorized">True when this attribute is expected to feed historian / HistoryRead.</param>
public sealed record DriverAttributeInfo(
string FullName,
DriverDataType DriverDataType,
bool IsArray,
uint? ArrayDim,
SecurityClassification SecurityClass,
bool IsHistorized);

View File

@@ -0,0 +1,28 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver-agnostic data type for an attribute or signal.
/// Maps to OPC UA built-in types at the address-space build layer.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/driver-specs.md</c> driver DataType columns, every driver maps its
/// native types into this enumeration. Mirrors the OPC UA built-in type set commonly
/// seen across Modbus / S7 / AB CIP / TwinCAT / FANUC / Galaxy.
/// </remarks>
public enum DriverDataType
{
Boolean,
Int16,
Int32,
Int64,
UInt16,
UInt32,
UInt64,
Float32,
Float64,
String,
DateTime,
/// <summary>Galaxy-style attribute reference encoded as an OPC UA String.</summary>
Reference,
}

View File

@@ -0,0 +1,38 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Health snapshot a driver returns to the Core. Drives the status dashboard,
/// ServiceLevel computation, and Bad-quality fan-out decisions.
/// </summary>
/// <param name="State">Current driver-instance state.</param>
/// <param name="LastSuccessfulRead">Timestamp of the most recent successful equipment read; null if never.</param>
/// <param name="LastError">Most recent error message; null when state is Healthy.</param>
public sealed record DriverHealth(
DriverState State,
DateTime? LastSuccessfulRead,
string? LastError);
/// <summary>Driver-instance lifecycle state.</summary>
public enum DriverState
{
/// <summary>Driver has not been initialized yet.</summary>
Unknown,
/// <summary>Driver is in the middle of <see cref="IDriver.InitializeAsync"/> or <see cref="IDriver.ReinitializeAsync"/>.</summary>
Initializing,
/// <summary>Driver is connected and serving data.</summary>
Healthy,
/// <summary>Driver is connected but reporting degraded data (e.g. some equipment unreachable, some tags Bad).</summary>
Degraded,
/// <summary>Driver lost connection to its data source; reconnecting in the background.</summary>
Reconnecting,
/// <summary>
/// Driver hit an unrecoverable error and stopped trying.
/// Operator must reinitialize via Admin UI; nodes report Bad quality.
/// </summary>
Faulted,
}

View File

@@ -0,0 +1,94 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Process-singleton registry of driver types known to this OtOpcUa instance.
/// Per-driver assemblies register their type metadata at startup; the Core uses
/// the registry to validate <c>DriverInstance.DriverType</c> values from the central config DB.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/plan.md</c> decisions #91 (JSON content validation in Admin app, not SQL CLR)
/// and #111 (driver type → namespace kind mapping enforced by sp_ValidateDraft).
/// The registry is the source of truth for both checks.
///
/// Thread-safety: registration happens at startup (single thread); lookups happen on every
/// config-apply (multi-threaded). The internal dictionary is replaced atomically via
/// <see cref="System.Threading.Interlocked"/> on register; readers see a stable snapshot.
/// </remarks>
public sealed class DriverTypeRegistry
{
private IReadOnlyDictionary<string, DriverTypeMetadata> _types =
new Dictionary<string, DriverTypeMetadata>(StringComparer.OrdinalIgnoreCase);
/// <summary>Register a driver type. Throws if the type name is already registered.</summary>
public void Register(DriverTypeMetadata metadata)
{
ArgumentNullException.ThrowIfNull(metadata);
var snapshot = _types;
if (snapshot.ContainsKey(metadata.TypeName))
{
throw new InvalidOperationException(
$"Driver type '{metadata.TypeName}' is already registered. " +
$"Each driver type may be registered only once per process.");
}
var next = new Dictionary<string, DriverTypeMetadata>(snapshot, StringComparer.OrdinalIgnoreCase)
{
[metadata.TypeName] = metadata,
};
Interlocked.Exchange(ref _types, next);
}
/// <summary>Look up a driver type by name. Throws if unknown.</summary>
public DriverTypeMetadata Get(string driverType)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
if (_types.TryGetValue(driverType, out var metadata))
return metadata;
throw new KeyNotFoundException(
$"Driver type '{driverType}' is not registered. " +
$"Known types: {string.Join(", ", _types.Keys)}.");
}
/// <summary>Try to look up a driver type by name. Returns null if unknown (no exception).</summary>
public DriverTypeMetadata? TryGet(string driverType)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
return _types.GetValueOrDefault(driverType);
}
/// <summary>Snapshot of all registered driver types.</summary>
public IReadOnlyCollection<DriverTypeMetadata> All() => _types.Values.ToList();
}
/// <summary>Per-driver-type metadata used by the Core, validator, and Admin UI.</summary>
/// <param name="TypeName">Driver type name (matches <c>DriverInstance.DriverType</c> column values).</param>
/// <param name="AllowedNamespaceKinds">Which namespace kinds this driver type may be bound to.</param>
/// <param name="DriverConfigJsonSchema">JSON Schema (Draft 2020-12) the driver's <c>DriverConfig</c> column must validate against.</param>
/// <param name="DeviceConfigJsonSchema">JSON Schema for <c>DeviceConfig</c> (multi-device drivers); null if the driver has no device layer.</param>
/// <param name="TagConfigJsonSchema">JSON Schema for <c>TagConfig</c>; required for every driver since every driver has tags.</param>
public sealed record DriverTypeMetadata(
string TypeName,
NamespaceKindCompatibility AllowedNamespaceKinds,
string DriverConfigJsonSchema,
string? DeviceConfigJsonSchema,
string TagConfigJsonSchema);
/// <summary>Bitmask of namespace kinds a driver type may populate. Per decision #111.</summary>
[Flags]
public enum NamespaceKindCompatibility
{
/// <summary>Driver does not populate any namespace (invalid; should never appear in registry).</summary>
None = 0,
/// <summary>Driver may populate Equipment-kind namespaces (UNS path, Equipment rows).</summary>
Equipment = 1,
/// <summary>Driver may populate SystemPlatform-kind namespaces (Galaxy hierarchy, FolderPath).</summary>
SystemPlatform = 2,
/// <summary>Driver may populate the future Simulated namespace (replay driver — not in v2.0).</summary>
Simulated = 4,
}

View File

@@ -0,0 +1,45 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Streaming builder API a driver uses to register OPC UA nodes during discovery.
/// Core owns the tree; driver streams <c>AddFolder</c> / <c>AddVariable</c> calls
/// as it discovers nodes — no buffering of the whole tree.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/plan.md</c> decision #52 — drivers register nodes via this builder
/// rather than returning a tree object. Supports incremental / large address spaces
/// without forcing the driver to buffer the whole tree.
/// </remarks>
public interface IAddressSpaceBuilder
{
/// <summary>
/// Add a folder node. Returns a child builder scoped to inside this folder, so subsequent
/// calls on the child place nodes under it.
/// </summary>
/// <param name="browseName">OPC UA browse name (the segment of the path under the parent).</param>
/// <param name="displayName">Human-readable display name. May equal <paramref name="browseName"/>.</param>
IAddressSpaceBuilder Folder(string browseName, string displayName);
/// <summary>
/// Add a variable node corresponding to a tag. Driver-side full reference + data-type
/// metadata come from the <see cref="DriverAttributeInfo"/> DTO.
/// </summary>
/// <param name="browseName">OPC UA browse name (the segment of the path under the parent folder).</param>
/// <param name="displayName">Human-readable display name. May equal <paramref name="browseName"/>.</param>
/// <param name="attributeInfo">Driver-side metadata for the variable.</param>
IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo);
/// <summary>
/// Add a property to the current node (folder or variable). Properties are static metadata
/// read once at build time (e.g. OPC 40010 Identification fields per the schemas-repo
/// <c>_base</c> equipment-class template).
/// </summary>
void AddProperty(string browseName, DriverDataType dataType, object? value);
}
/// <summary>Opaque handle for a registered variable. Used by Core for subscription routing.</summary>
public interface IVariableHandle
{
/// <summary>Driver-side full reference for read/write addressing.</summary>
string FullReference { get; }
}

View File

@@ -0,0 +1,54 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver capability for alarm events. Optional — only drivers whose backends expose
/// alarm conditions implement this. Currently: Galaxy (MxAccess alarms), FOCAS
/// (CNC alarms), OPC UA Client (A&amp;C events from upstream server).
/// </summary>
public interface IAlarmSource
{
/// <summary>
/// Subscribe to alarm events for a node-set (typically: a folder or equipment subtree).
/// The driver fires <see cref="OnAlarmEvent"/> for every alarm transition.
/// </summary>
Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
IReadOnlyList<string> sourceNodeIds,
CancellationToken cancellationToken);
/// <summary>Cancel an alarm subscription returned by <see cref="SubscribeAlarmsAsync"/>.</summary>
Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken);
/// <summary>Acknowledge one or more active alarms by source node ID + condition ID.</summary>
Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
CancellationToken cancellationToken);
/// <summary>Server-pushed alarm transition (raise / clear / change).</summary>
event EventHandler<AlarmEventArgs>? OnAlarmEvent;
}
/// <summary>Opaque alarm-subscription identity returned by <see cref="IAlarmSource.SubscribeAlarmsAsync"/>.</summary>
public interface IAlarmSubscriptionHandle
{
/// <summary>Driver-internal subscription identifier (for diagnostics + post-mortem).</summary>
string DiagnosticId { get; }
}
/// <summary>One alarm acknowledgement in a batch.</summary>
public sealed record AlarmAcknowledgeRequest(
string SourceNodeId,
string ConditionId,
string? Comment);
/// <summary>Event payload for <see cref="IAlarmSource.OnAlarmEvent"/>.</summary>
public sealed record AlarmEventArgs(
IAlarmSubscriptionHandle SubscriptionHandle,
string SourceNodeId,
string ConditionId,
string AlarmType,
string Message,
AlarmSeverity Severity,
DateTime SourceTimestampUtc);
/// <summary>Mirrors the <c>NodePermissions</c> alarm-severity enum in <c>docs/v2/acl-design.md</c>.</summary>
public enum AlarmSeverity { Low, Medium, High, Critical }

View File

@@ -0,0 +1,60 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Required capability for every driver instance. Owns lifecycle, metadata, health.
/// Other capabilities (<see cref="ITagDiscovery"/>, <see cref="IReadable"/>,
/// <see cref="IWritable"/>, <see cref="ISubscribable"/>, <see cref="IAlarmSource"/>,
/// <see cref="IHistoryProvider"/>, <see cref="IRediscoverable"/>,
/// <see cref="IHostConnectivityProbe"/>) are composable — a driver implements only what its
/// backend actually supports.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/plan.md</c> decisions #4 (composable capability interfaces) and #53
/// (capability discovery via <c>is</c> checks — no redundant flag enum).
/// </remarks>
public interface IDriver
{
/// <summary>Stable logical ID of this driver instance, sourced from the central config DB.</summary>
string DriverInstanceId { get; }
/// <summary>Driver type name (e.g. "Galaxy", "ModbusTcp", "AbCip"). Matches <c>DriverInstance.DriverType</c>.</summary>
string DriverType { get; }
/// <summary>Initialize the driver from its <c>DriverConfig</c> JSON; open connections; prepare for first use.</summary>
Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken);
/// <summary>
/// Apply a config change in place without tearing down the driver process.
/// Used by <c>IGenerationApplier</c> when only this driver's config changed in the new generation.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/driver-stability.md</c> §"In-process only (Tier A/B)" — Reinitialize is the
/// only Core-initiated recovery path for in-process drivers; if it fails, the driver instance
/// is marked Faulted and its nodes go Bad quality, but the server process keeps running.
/// </remarks>
Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken);
/// <summary>Stop the driver, close connections, release resources. Called on shutdown or driver removal.</summary>
Task ShutdownAsync(CancellationToken cancellationToken);
/// <summary>Current health snapshot, polled by Core for the status dashboard and ServiceLevel.</summary>
DriverHealth GetHealth();
/// <summary>
/// Approximate driver-attributable footprint in bytes (caches, queues, symbol tables).
/// Polled every 30s by Core; on cache-budget breach, Core asks the driver to flush via
/// <see cref="FlushOptionalCachesAsync"/>.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/driver-stability.md</c> §"In-process only (Tier A/B) — driver-instance
/// allocation tracking". Tier C drivers (process-isolated) report through the same
/// interface but the cache-flush is internal to their host.
/// </remarks>
long GetMemoryFootprint();
/// <summary>
/// Drop optional caches (symbol cache, browse cache, etc.) to bring footprint back below budget.
/// Required-for-correctness state must NOT be flushed.
/// </summary>
Task FlushOptionalCachesAsync(CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,30 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Optional plug-point a driver implements to provide a custom Admin UI editor for its
/// <c>DriverConfig</c> JSON. Drivers that don't implement this fall back to the generic
/// JSON editor with schema-driven validation against the registered JSON schema.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/plan.md</c> decision #27 — driver-specific config editors are deferred
/// to each driver's implementation phase; v2.0 ships with the generic JSON editor as the
/// default. This interface is the future plug-point so phase-specific editors can land
/// incrementally.
///
/// The actual UI rendering happens in the Admin Blazor Server app (see
/// <c>docs/v2/admin-ui.md</c>). This interface in <c>Core.Abstractions</c> is the
/// contract between the driver and the Admin app — the Admin app discovers
/// implementations and slots them into the Driver Detail screen.
/// </remarks>
public interface IDriverConfigEditor
{
/// <summary>Driver type name this editor handles (e.g. "Galaxy", "ModbusTcp").</summary>
string DriverType { get; }
/// <summary>
/// Type of the Razor component (must derive from <c>ComponentBase</c> in the Admin app's
/// `Components/Shared/` folder) that renders the editor. Returned as <c>Type</c> so the
/// <c>Core.Abstractions</c> project doesn't need a Blazor reference.
/// </summary>
Type EditorComponentType { get; }
}

View File

@@ -0,0 +1,50 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver capability for historical-data reads (OPC UA HistoryRead). Optional —
/// only drivers whose backends carry historian data implement this. Currently:
/// Galaxy (Wonderware Historian via the optional plugin), OPC UA Client (forward
/// to upstream server).
/// </summary>
public interface IHistoryProvider
{
/// <summary>
/// Read raw historical samples for a single attribute over a time range.
/// The Core wraps this with continuation-point handling.
/// </summary>
Task<HistoryReadResult> ReadRawAsync(
string fullReference,
DateTime startUtc,
DateTime endUtc,
uint maxValuesPerNode,
CancellationToken cancellationToken);
/// <summary>
/// Read processed (aggregated) samples — interval-bucketed average / min / max / etc.
/// Optional — drivers that only support raw history can throw <see cref="NotSupportedException"/>.
/// </summary>
Task<HistoryReadResult> ReadProcessedAsync(
string fullReference,
DateTime startUtc,
DateTime endUtc,
TimeSpan interval,
HistoryAggregateType aggregate,
CancellationToken cancellationToken);
}
/// <summary>Result of a HistoryRead call.</summary>
/// <param name="Samples">Returned samples in chronological order.</param>
/// <param name="ContinuationPoint">Opaque token for the next call when more samples are available; null when complete.</param>
public sealed record HistoryReadResult(
IReadOnlyList<DataValueSnapshot> Samples,
byte[]? ContinuationPoint);
/// <summary>Aggregate function for processed history reads. Mirrors OPC UA Part 13 standard aggregates.</summary>
public enum HistoryAggregateType
{
Average,
Minimum,
Maximum,
Total,
Count,
}

View File

@@ -0,0 +1,41 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Optional driver capability for per-host connectivity reporting. Currently used by
/// the Galaxy driver (Platform / AppEngine ScanState) but generalized so future drivers
/// with multi-host topology (e.g. an OPC UA Client gateway proxying multiple upstream
/// servers) can opt in.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/plan.md</c> §5a — the Galaxy driver's <c>GalaxyRuntimeProbeManager</c>
/// becomes <c>IHostConnectivityProbe</c> after the v2 refactor.
/// </remarks>
public interface IHostConnectivityProbe
{
/// <summary>
/// Snapshot of host-level connectivity. The Core uses this to drive Bad-quality
/// fan-out scoped to the affected host's subtree (not the whole driver namespace).
/// </summary>
IReadOnlyList<HostConnectivityStatus> GetHostStatuses();
/// <summary>Fired when a host transitions Running ↔ Stopped (or similar lifecycle change).</summary>
event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;
}
/// <summary>Per-host connectivity snapshot.</summary>
/// <param name="HostName">Driver-side host identifier (e.g. for Galaxy: Platform or AppEngine name).</param>
/// <param name="State">Current state.</param>
/// <param name="LastChangedUtc">Timestamp of the last state transition.</param>
public sealed record HostConnectivityStatus(
string HostName,
HostState State,
DateTime LastChangedUtc);
/// <summary>Event payload for <see cref="IHostConnectivityProbe.OnHostStatusChanged"/>.</summary>
public sealed record HostStatusChangedEventArgs(
string HostName,
HostState OldState,
HostState NewState);
/// <summary>Host lifecycle state. Generalization of Galaxy's Platform/Engine ScanState.</summary>
public enum HostState { Unknown, Running, Stopped, Faulted }

View File

@@ -0,0 +1,25 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver capability for on-demand reads. Required for any driver whose nodes are
/// readable from OPC UA clients (essentially all of them — every committed v2 driver
/// implements this).
/// </summary>
/// <remarks>
/// Reads are idempotent — Polly retry pipelines can safely retry on transient failures
/// (per <c>docs/v2/plan.md</c> decisions #34 and #44).
/// </remarks>
public interface IReadable
{
/// <summary>
/// Read a batch of attributes by their full driver-side reference.
/// Returns one snapshot per requested reference, in the same order.
/// </summary>
/// <remarks>
/// Per-reference failures should be reported via the snapshot's <see cref="DataValueSnapshot.StatusCode"/>
/// (Bad-coded), not as exceptions. The whole call should throw only if the driver itself is unreachable.
/// </remarks>
Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
IReadOnlyList<string> fullReferences,
CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,29 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Optional driver capability — drivers whose backend has a native change signal
/// (Galaxy <c>time_of_last_deploy</c>, OPC UA server change notifications, TwinCAT
/// symbol-version-changed) implement this to tell Core when to re-run discovery.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/plan.md</c> decision #54 — static drivers (Modbus, S7, etc. whose tags
/// only change via a published config generation) don't implement <c>IRediscoverable</c>.
/// The Core just sees absence of the interface and skips change-detection wiring for that driver.
/// </remarks>
public interface IRediscoverable
{
/// <summary>
/// Fired when the driver's backend signals that the address space may have changed.
/// The Core's response is to re-run <see cref="ITagDiscovery.DiscoverAsync"/> and
/// diff the result against the current address space.
/// </summary>
event EventHandler<RediscoveryEventArgs>? OnRediscoveryNeeded;
}
/// <summary>Event payload for <see cref="IRediscoverable.OnRediscoveryNeeded"/>.</summary>
/// <param name="Reason">Driver-supplied reason string for the diagnostic log (e.g. "Galaxy time_of_last_deploy advanced", "TwinCAT symbol-version-changed 0x0702").</param>
/// <param name="ScopeHint">
/// Optional hint about which subtree changed. Null means "the whole address space may have changed".
/// A non-null value (e.g. a folder path) lets the Core scope the rebuild surgically.
/// </param>
public sealed record RediscoveryEventArgs(string Reason, string? ScopeHint);

View File

@@ -0,0 +1,47 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver capability for data-change subscriptions — covers both native subscriptions
/// (Galaxy MXAccess advisory, OPC UA monitored items, TwinCAT ADS notifications) and
/// driver-internal polled subscriptions (Modbus, AB CIP, S7, FOCAS). The driver owns
/// its polling loop where applicable; the Core just sees <see cref="OnDataChange"/>
/// callbacks regardless of mechanism.
/// </summary>
public interface ISubscribable
{
/// <summary>
/// Subscribe to data changes for a batch of attributes.
/// The driver MAY fire <see cref="OnDataChange"/> immediately with the current value
/// (initial-data callback per OPC UA convention) and again on every change.
/// </summary>
/// <returns>An opaque subscription handle the caller passes to <see cref="UnsubscribeAsync"/>.</returns>
Task<ISubscriptionHandle> SubscribeAsync(
IReadOnlyList<string> fullReferences,
TimeSpan publishingInterval,
CancellationToken cancellationToken);
/// <summary>Cancel a subscription returned by <see cref="SubscribeAsync"/>.</summary>
Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken);
/// <summary>
/// Server-pushed data-change notification. Fires whenever a subscribed attribute changes,
/// and (per OPC UA convention) on subscription establishment for current values.
/// </summary>
event EventHandler<DataChangeEventArgs>? OnDataChange;
}
/// <summary>Opaque subscription identity returned by <see cref="ISubscribable.SubscribeAsync"/>.</summary>
public interface ISubscriptionHandle
{
/// <summary>Driver-internal subscription identifier (for diagnostics + post-mortem).</summary>
string DiagnosticId { get; }
}
/// <summary>Event payload for <see cref="ISubscribable.OnDataChange"/>.</summary>
/// <param name="SubscriptionHandle">The handle returned by the original <see cref="ISubscribable.SubscribeAsync"/> call.</param>
/// <param name="FullReference">Driver-side full reference of the changed attribute.</param>
/// <param name="Snapshot">New value + quality + timestamps.</param>
public sealed record DataChangeEventArgs(
ISubscriptionHandle SubscriptionHandle,
string FullReference,
DataValueSnapshot Snapshot);

View File

@@ -0,0 +1,15 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver capability for discovering tags and hierarchy from the backend.
/// Streams discovered nodes into <see cref="IAddressSpaceBuilder"/> rather than
/// buffering the entire tree (decision #52 — supports incremental / large address spaces).
/// </summary>
public interface ITagDiscovery
{
/// <summary>
/// Discover the driver's tag set and stream nodes to the builder.
/// The driver decides ordering (root → leaf typically) and may yield as many calls as needed.
/// </summary>
Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,34 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver capability for on-demand writes. Optional — read-only drivers (a hypothetical
/// historian-only adapter, for example) can omit this.
/// </summary>
/// <remarks>
/// Per <c>docs/v2/plan.md</c> decisions #44 + #45 — <b>writes are NOT auto-retried by default</b>.
/// A timeout may fire after the device already accepted the command; replaying non-idempotent
/// field actions (pulses, alarm acks, recipe steps, counter increments) can cause duplicate
/// operations. Per-tag opt-in via <c>Tag.WriteIdempotent = true</c> in the central config DB
/// enables retry; otherwise the OPC UA client decides whether to re-issue.
/// </remarks>
public interface IWritable
{
/// <summary>
/// Write a batch of values to the driver. Returns one status per requested write,
/// in the same order.
/// </summary>
/// <param name="writes">Pairs of full reference + value to write.</param>
/// <param name="cancellationToken">Cancellation token; the driver should abort the batch if cancelled.</param>
Task<IReadOnlyList<WriteResult>> WriteAsync(
IReadOnlyList<WriteRequest> writes,
CancellationToken cancellationToken);
}
/// <summary>One write request in a batch.</summary>
/// <param name="FullReference">Driver-side full reference (matches <see cref="DriverAttributeInfo.FullName"/>).</param>
/// <param name="Value">Value to write; type must be compatible with the attribute's <see cref="DriverDataType"/>.</param>
public sealed record WriteRequest(string FullReference, object? Value);
/// <summary>Result of one write in a batch.</summary>
/// <param name="StatusCode">OPC UA status code (numeric value matches the OPC UA spec).</param>
public sealed record WriteResult(uint StatusCode);

View File

@@ -0,0 +1,23 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Security classification for write authorization on a tag, mirroring
/// the v1 Galaxy SecurityClassification model documented in <c>docs/DataTypeMapping.md</c>.
/// Generalized so non-Galaxy drivers can declare per-tag write protection levels.
/// </summary>
/// <remarks>
/// Maps to <c>NodePermissions</c> write tiers in <c>docs/v2/acl-design.md</c>:
/// FreeAccess + Operate require <c>WriteOperate</c>; Tune requires <c>WriteTune</c>;
/// Configure requires <c>WriteConfigure</c>; SecuredWrite + VerifiedWrite + ViewOnly
/// are read-only from OPC UA (v1 behavior preserved).
/// </remarks>
public enum SecurityClassification
{
FreeAccess = 0,
Operate = 1,
SecuredWrite = 2,
VerifiedWrite = 3,
Tune = 4,
Configure = 5,
ViewOnly = 6,
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
</Project>