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:
@@ -1,5 +1,6 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Historian.Aveva/ZB.MOM.WW.OtOpcUa.Historian.Aveva.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.Shared/ZB.MOM.WW.OtOpcUa.Client.Shared.csproj"/>
|
||||
@@ -7,6 +8,7 @@
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.UI/ZB.MOM.WW.OtOpcUa.Client.UI.csproj"/>
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Tests/ZB.MOM.WW.OtOpcUa.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Historian.Aveva.Tests/ZB.MOM.WW.OtOpcUa.Historian.Aveva.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.IntegrationTests/ZB.MOM.WW.OtOpcUa.IntegrationTests.csproj"/>
|
||||
|
||||
56
docs/v2/implementation/entry-gate-phase-1.md
Normal file
56
docs/v2/implementation/entry-gate-phase-1.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Phase 1 — Entry Gate Record
|
||||
|
||||
**Phase**: 1 — Configuration project + Core.Abstractions + Admin scaffold
|
||||
**Branch**: `phase-1-configuration`
|
||||
**Date**: 2026-04-17
|
||||
**Implementation lead**: Claude (executing on behalf of dohertj2)
|
||||
|
||||
## Entry conditions
|
||||
|
||||
| Check | Required | Actual | Pass |
|
||||
|-------|----------|--------|------|
|
||||
| Phase 0 exit gate cleared | Rename complete, all v1 tests pass under OtOpcUa names | Phase 0 merged to `v2` at commit `45ffa3e` | ✅ |
|
||||
| `v2` branch is clean | Clean | Clean post-merge | ✅ |
|
||||
| Phase 0 PR merged | — | Merged via `--no-ff` to v2 | ✅ |
|
||||
| SQL Server 2019+ instance available | For development | NOT YET AVAILABLE — see deviation below | ⚠️ |
|
||||
| LDAP/GLAuth dev instance available | For Admin auth integration testing | Existing v1 GLAuth at `C:\publish\glauth\` | ✅ |
|
||||
| ScadaLink CentralUI source accessible | For parity reference | `C:\Users\dohertj2\Desktop\scadalink-design\` per memory | ✅ |
|
||||
| Phase 1-relevant design docs reviewed | All read by impl lead | ✅ Read in preceding sessions | ✅ |
|
||||
| Decisions read | #1–142 covered cumulatively | ✅ | ✅ |
|
||||
|
||||
## Deviation: SQL Server dev instance not yet stood up
|
||||
|
||||
The Phase 1 entry gate requires a SQL Server 2019+ dev instance for the `Configuration` project's EF Core migrations + tests. This is per `dev-environment.md` Step 1, which is currently TODO.
|
||||
|
||||
**Decision**: proceed with **Stream A only** (Core.Abstractions) in this continuation. Stream A has zero infrastructure dependencies — it's a `.NET 10` project with BCL-only references defining capability interfaces and DTOs. Streams B (Configuration), C (Core), D (Server), and E (Admin) all have infrastructure dependencies (SQL Server, GLAuth, Galaxy) and require the dev environment standup to be productive.
|
||||
|
||||
The SQL Server standup is a one-line `docker run` per `dev-environment.md` §"Bootstrap Order — Inner-loop Developer Machine" step 5. It can happen in parallel with subsequent Stream A work but is not a blocker for Stream A itself.
|
||||
|
||||
**This continuation will execute only Stream A.** Streams B–E require their own continuations after the dev environment is stood up.
|
||||
|
||||
## Phase 1 work scope (for reference)
|
||||
|
||||
Per `phase-1-configuration-and-admin-scaffold.md`:
|
||||
|
||||
| Stream | Scope | Status this continuation |
|
||||
|--------|-------|--------------------------|
|
||||
| **A. Core.Abstractions** | 11 capability interfaces + DTOs + DriverTypeRegistry | ▶ EXECUTING |
|
||||
| B. Configuration | EF Core schema, stored procs, LiteDB cache, generation-diff applier | DEFERRED — needs SQL Server |
|
||||
| C. Core | `LmxNodeManager → GenericDriverNodeManager` rename, `IAddressSpaceBuilder`, driver hosting | DEFERRED — depends on Stream A + needs Galaxy |
|
||||
| D. Server | `Microsoft.Extensions.Hosting` host, credential-bound bootstrap | DEFERRED — depends on Stream B |
|
||||
| E. Admin | Blazor Server scaffold mirroring ScadaLink | DEFERRED — depends on Stream B |
|
||||
|
||||
## Baseline metrics (carried from Phase 0 exit)
|
||||
|
||||
- **Total tests**: 822 (pass + fail)
|
||||
- **Pass count**: 821 (improved from baseline 820 — one flaky test happened to pass at Phase 0 exit)
|
||||
- **Fail count**: 1 (the second pre-existing failure may flap; either 1 or 2 failures is consistent with baseline)
|
||||
- **Build warnings**: 30 (lower than original baseline 167)
|
||||
- **Build errors**: 0
|
||||
|
||||
Phase 1 must not introduce new failures or new errors against this baseline.
|
||||
|
||||
## Signoff
|
||||
|
||||
Implementation lead: Claude (Opus 4.7) — 2026-04-17
|
||||
Reviewer: pending — Stream A PR will require a second reviewer per overview.md exit-gate rules
|
||||
21
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DataValueSnapshot.cs
Normal file
21
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DataValueSnapshot.cs
Normal 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);
|
||||
@@ -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);
|
||||
28
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs
Normal file
28
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverDataType.cs
Normal 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,
|
||||
}
|
||||
38
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverHealth.cs
Normal file
38
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverHealth.cs
Normal 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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
54
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs
Normal file
54
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs
Normal 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&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 }
|
||||
60
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs
Normal file
60
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs
Normal 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);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
50
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs
Normal file
50
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs
Normal 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,
|
||||
}
|
||||
@@ -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 }
|
||||
25
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs
Normal file
25
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs
Normal 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);
|
||||
}
|
||||
29
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs
Normal file
29
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs
Normal 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);
|
||||
47
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs
Normal file
47
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ISubscribable.cs
Normal 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);
|
||||
15
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs
Normal file
15
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs
Normal 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);
|
||||
}
|
||||
34
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IWritable.cs
Normal file
34
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IWritable.cs
Normal 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);
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,107 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
||||
|
||||
public sealed class DriverTypeRegistryTests
|
||||
{
|
||||
private static DriverTypeMetadata SampleMetadata(
|
||||
string typeName = "Modbus",
|
||||
NamespaceKindCompatibility allowed = NamespaceKindCompatibility.Equipment) =>
|
||||
new(typeName, allowed,
|
||||
DriverConfigJsonSchema: "{\"type\": \"object\"}",
|
||||
DeviceConfigJsonSchema: "{\"type\": \"object\"}",
|
||||
TagConfigJsonSchema: "{\"type\": \"object\"}");
|
||||
|
||||
[Fact]
|
||||
public void Register_ThenGet_RoundTrips()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
var metadata = SampleMetadata();
|
||||
|
||||
registry.Register(metadata);
|
||||
|
||||
registry.Get("Modbus").ShouldBe(metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_IsCaseInsensitive()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Galaxy"));
|
||||
|
||||
registry.Get("galaxy").ShouldNotBeNull();
|
||||
registry.Get("GALAXY").ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_UnknownType_Throws()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
|
||||
Should.Throw<KeyNotFoundException>(() => registry.Get("UnregisteredType"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_UnknownType_ReturnsNull()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
|
||||
registry.TryGet("UnregisteredType").ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_DuplicateType_Throws()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
|
||||
Should.Throw<InvalidOperationException>(() => registry.Register(SampleMetadata("Modbus")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_DuplicateTypeIsCaseInsensitive()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
|
||||
Should.Throw<InvalidOperationException>(() => registry.Register(SampleMetadata("modbus")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_ReturnsRegisteredTypes()
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
registry.Register(SampleMetadata("Modbus"));
|
||||
registry.Register(SampleMetadata("S7"));
|
||||
registry.Register(SampleMetadata("Galaxy", NamespaceKindCompatibility.SystemPlatform));
|
||||
|
||||
var all = registry.All();
|
||||
|
||||
all.Count.ShouldBe(3);
|
||||
all.Select(m => m.TypeName).ShouldBe(new[] { "Modbus", "S7", "Galaxy" }, ignoreOrder: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NamespaceKindCompatibility_FlagsAreBitmask()
|
||||
{
|
||||
// Per decision #111 — driver types like OpcUaClient may be valid for multiple namespace kinds.
|
||||
var both = NamespaceKindCompatibility.Equipment | NamespaceKindCompatibility.SystemPlatform;
|
||||
|
||||
both.HasFlag(NamespaceKindCompatibility.Equipment).ShouldBeTrue();
|
||||
both.HasFlag(NamespaceKindCompatibility.SystemPlatform).ShouldBeTrue();
|
||||
both.HasFlag(NamespaceKindCompatibility.Simulated).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Get_RejectsEmptyTypeName(string? typeName)
|
||||
{
|
||||
var registry = new DriverTypeRegistry();
|
||||
Should.Throw<ArgumentException>(() => registry.Get(typeName!));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that <c>Core.Abstractions</c> stays a true contract project — it must not depend on
|
||||
/// any implementation type, any other OtOpcUa project, or anything beyond BCL + System types.
|
||||
/// Per <c>docs/v2/plan.md</c> decision #59 (Core.Abstractions internal-only for now; design as
|
||||
/// if public to minimize churn later).
|
||||
/// </summary>
|
||||
public sealed class InterfaceIndependenceTests
|
||||
{
|
||||
private static readonly Assembly Assembly = typeof(IDriver).Assembly;
|
||||
|
||||
[Fact]
|
||||
public void Assembly_HasNoReferencesOutsideBcl()
|
||||
{
|
||||
// Allowed reference assembly name prefixes — BCL + the assembly itself.
|
||||
var allowed = new[]
|
||||
{
|
||||
"System",
|
||||
"Microsoft.Win32",
|
||||
"netstandard",
|
||||
"mscorlib",
|
||||
"ZB.MOM.WW.OtOpcUa.Core.Abstractions",
|
||||
};
|
||||
|
||||
var referenced = Assembly.GetReferencedAssemblies();
|
||||
var disallowed = referenced
|
||||
.Where(r => !allowed.Any(a => r.Name!.StartsWith(a, StringComparison.Ordinal)))
|
||||
.ToList();
|
||||
|
||||
disallowed.ShouldBeEmpty(
|
||||
$"Core.Abstractions must reference only BCL/System assemblies. " +
|
||||
$"Found disallowed references: {string.Join(", ", disallowed.Select(a => a.Name))}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllPublicTypes_LiveInRootNamespace()
|
||||
{
|
||||
// Per the decision-#59 "design as if public" rule — no nested sub-namespaces; one flat surface.
|
||||
var publicTypes = Assembly.GetExportedTypes();
|
||||
var nonRoot = publicTypes
|
||||
.Where(t => t.Namespace != "ZB.MOM.WW.OtOpcUa.Core.Abstractions")
|
||||
.ToList();
|
||||
|
||||
nonRoot.ShouldBeEmpty(
|
||||
$"Core.Abstractions should expose all public types in the root namespace. " +
|
||||
$"Found types in other namespaces: {string.Join(", ", nonRoot.Select(t => $"{t.FullName}"))}");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(IDriver))]
|
||||
[InlineData(typeof(ITagDiscovery))]
|
||||
[InlineData(typeof(IReadable))]
|
||||
[InlineData(typeof(IWritable))]
|
||||
[InlineData(typeof(ISubscribable))]
|
||||
[InlineData(typeof(IAlarmSource))]
|
||||
[InlineData(typeof(IHistoryProvider))]
|
||||
[InlineData(typeof(IRediscoverable))]
|
||||
[InlineData(typeof(IHostConnectivityProbe))]
|
||||
[InlineData(typeof(IDriverConfigEditor))]
|
||||
[InlineData(typeof(IAddressSpaceBuilder))]
|
||||
public void EveryCapabilityInterface_IsPublic(Type type)
|
||||
{
|
||||
type.IsPublic.ShouldBeTrue($"{type.Name} must be public — drivers in separate assemblies implement it.");
|
||||
type.IsInterface.ShouldBeTrue($"{type.Name} must be an interface, not a class.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="1.1.0"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user