feat(runtime,host): close F7 — driver subscribe + write paths + Host DI
v2-ci / build (push) Failing after 42s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (push) Has been skipped

Three pieces landed in one batch, closing F7-residual + Host DI #106:

Runtime/DriverInstanceActor:
  - Subscribe / Unsubscribe message contracts; the Connected state
    handles them via IDriver.ISubscribable. On every OnDataChange
    event the actor publishes AttributeValuePublished to its parent
    (DriverHostActor → OpcUaPublishActor). OPC UA StatusCode is
    mapped to the 3-state OpcUaQuality enum via severity bits
    (00=Good, 01=Uncertain, 10/11=Bad).
  - DetachSubscription tears the handler off the driver on
    DisconnectObserved, Unsubscribe, and PostStop so a stale handler
    never pushes to a dead actor.
  - WriteAttribute now dispatches IWritable.WriteAsync (batch of one)
    with a 5s CancellationTokenSource; status-code propagated to
    WriteAttributeResult on non-Good results.

Host:
  - New ProjectReferences to Core + every cross-platform driver
    assembly (AbCip/AbLegacy/FOCAS/Galaxy/Modbus/S7/TwinCAT).
    Galaxy is net10 (gRPC client to mxaccessgw); the COM-bound net48
    Wonderware Historian driver stays out of the Host's reference
    closure — its .Client gRPC wrapper is what binds for historian
    needs.
  - New DriverFactoryBootstrap.AddOtOpcUaDriverFactories() registers
    a singleton DriverFactoryRegistry, invokes each driver's
    Register(registry, loggerFactory), and binds IDriverFactory to
    DriverFactoryRegistryAdapter. Replaces the F7 NullDriverFactory
    default so deploys actually materialise real IDriver instances
    on driver-role nodes. ShouldStub() still gates per-platform
    behaviour at spawn time.
  - Program.cs wires AddOtOpcUaDriverFactories() before AddAkka so
    the runtime extension can resolve IDriverFactory from DI.

Tests: Runtime 46 -> 52 (+6):
- Write returns success when StatusCode = Good
- Write propagates non-Good status code in failure Reason
- Subscribe forwards OnDataChange to parent as AttributeValuePublished
- Quality translation: Uncertain (0x40...) and Bad (0x80...)
- Subscribe against non-ISubscribable returns failure
- DisconnectObserved detaches handler so late events are dropped

All 6 v2 test suites green: 152 tests passing.

Closes F7. F7-residual sub-tasks #110 (subscribe) and #111 (write)
both shipped. Host DI binding #106 shipped.
This commit is contained in:
Joseph Doherty
2026-05-26 09:28:34 -04:00
parent c02f016f1d
commit 3e3f7588bd
6 changed files with 387 additions and 11 deletions
@@ -0,0 +1,55 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
namespace ZB.MOM.WW.OtOpcUa.Host.Drivers;
/// <summary>
/// Wires every cross-platform driver assembly's <c>Register(registry, loggerFactory)</c>
/// extension into a single <see cref="DriverFactoryRegistry"/> singleton and binds the
/// v2 <see cref="IDriverFactory"/> abstraction to a <see cref="DriverFactoryRegistryAdapter"/>
/// over it. Replaces the F7 seam's <c>NullDriverFactory</c> default so deploys actually
/// materialise real <see cref="IDriver"/> instances on driver-role nodes.
///
/// Skipped entirely on admin-only nodes — they never run drivers, so the registry doesn't
/// need to exist (Program.cs guards via the <c>hasDriver</c> flag).
/// </summary>
public static class DriverFactoryBootstrap
{
/// <summary>
/// Register the cross-platform driver factories + bind <see cref="IDriverFactory"/>.
/// Must be called BEFORE <c>services.AddAkka</c> so the runtime extension can resolve
/// <see cref="IDriverFactory"/> from DI when spawning <c>DriverHostActor</c>.
/// </summary>
public static IServiceCollection AddOtOpcUaDriverFactories(this IServiceCollection services)
{
services.AddSingleton<DriverFactoryRegistry>(sp =>
{
var registry = new DriverFactoryRegistry();
var loggerFactory = sp.GetService<ILoggerFactory>();
Register(registry, loggerFactory);
return registry;
});
services.AddSingleton<IDriverFactory>(sp =>
new DriverFactoryRegistryAdapter(sp.GetRequiredService<DriverFactoryRegistry>()));
return services;
}
/// <summary>
/// Invoke every cross-platform driver's <c>Register</c> extension. New driver assemblies
/// get added here — one line per type. ShouldStub() in <c>DriverInstanceActor</c> still
/// handles platform/role-dependent stubbing (e.g. Galaxy on macOS), so registering a
/// factory here doesn't mean it always runs in production.
/// </summary>
private static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory)
{
Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry);
Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry);
Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.S7.S7DriverFactoryExtensions.Register(registry);
Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry);
}
}