using Microsoft.Extensions.Logging; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Hosting; namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect; /// /// Static factory registration helper for . The Host registers it /// once at startup; the bootstrapper then materialises MTConnect DriverInstance rows from /// the deployed configuration into live driver instances. Mirrors /// ModbusDriverFactoryExtensions / FocasDriverFactoryExtensions. /// /// /// /// There is exactly one parser for the config document. This factory does not carry a /// config DTO of its own — it delegates to , which /// the driver itself must own because the runtime delivers a config change to a live /// instance (DriverInstanceActor assigns its driver once and calls /// ReinitializeAsync(newJson) thereafter). A second DTO here would drift from that /// one silently, and the drift would first show up at deploy time as an operator's edit /// being applied differently by the two paths — or not at all. /// /// /// Construction opens nothing. The Wave-0 universal discovery browser builds a /// throwaway instance through this factory purely to read /// ITagDiscovery.SupportsOnlineDiscovery, and never initializes it — so a factory /// that dialled the Agent (or that pre-built an ) would /// open a connection per AdminUI browse render against a possibly-unreachable Agent. The /// agent-client factory is therefore left null: builds the /// production client inside InitializeAsync. /// /// /// The instance is returned as its concrete type. The runtime resolves every optional /// capability (, , /// , , /// ) by pattern-matching the object /// hands back, so nothing here may narrow it — a /// narrowed return is how a capability ends up implemented but never dispatched. There is /// deliberately no IWritable: the MTConnect Agent surface is read-only. /// /// public static class MTConnectDriverFactoryExtensions { /// /// The DriverInstance.DriverType value this factory answers to. /// /// /// Aliased to , not a literal. The registry key, /// the instance's self-reported , the probe's /// DriverType, and the constant every dispatch map keys off are therefore the same /// symbol — the repo-wide fix for the historical TwinCat/Focas drift, where a /// driver's own spelling silently diverged from the constant and dispatch maps missed it. /// Retained as a named const (rather than deleted in favour of the constant) so the existing /// factory callers and the sibling *DriverFactoryExtensions.DriverTypeName convention /// keep working; it is now an alias with no independent value. /// public const string DriverTypeName = DriverTypeNames.MTConnect; /// /// Register the MTConnect factory with the driver registry. The optional /// is captured at registration time and used to construct /// an per driver instance — without it the driver runs /// with the null logger (tests and standalone callers stay unchanged). /// /// The driver factory registry to register with. /// Optional logger factory for creating loggers per driver instance. public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null) { ArgumentNullException.ThrowIfNull(registry); registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory)); } /// Public for the Server-side bootstrapper + test consumers. /// The unique identifier for the driver instance. /// The driver's DriverConfig JSON. /// The constructed instance. public static MTConnectDriver CreateInstance(string driverInstanceId, string driverConfigJson) => CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null); /// Logger-aware overload — used by 's closure when wired through DI. /// The unique identifier for the driver instance. /// The driver's DriverConfig JSON. /// Optional logger factory for creating loggers per driver instance. /// The constructed instance. /// /// or is null or blank. /// /// /// The config document is unparseable, deserialises to null, omits the required /// agentUri, or carries an unauthorable enum/tag value. Throwing is correct at this /// seam: a deployment carrying such a row must fail rather than register a driver pointed at /// nothing, and the discovery browser's CanBrowse catches factory exceptions so a /// half-authored config merely disables the address picker. /// public static MTConnectDriver CreateInstance( string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson); // The single config authority — see the class remarks for why this is not a second DTO. // Note the asymmetry with the driver's own lifecycle path: an empty ("{}" / "[]") document // there means "no config supplied, keep the constructor options", but this factory HAS no // constructor options to keep, so an empty document is simply a config with no agentUri and // must fault. var options = MTConnectDriver.ParseOptions(driverInstanceId, driverConfigJson); // Named arguments deliberately: the ctor's trailing parameters are all optional, so a // future insertion could silently re-bind a positional argument to the wrong one. return new MTConnectDriver( options, driverInstanceId, agentClientFactory: null, logger: loggerFactory?.CreateLogger()); } }