using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers; /// /// Snapshot of a single driver instance's health, published to the /// driver-health DistributedPubSub topic whenever a driver actor /// transitions state or logs an error. Consumed by /// DriverStatusSignalRBridge in the AdminUI for live status push. /// /// Cluster this driver belongs to. /// Globally-unique driver instance ID. /// DriverState enum as string (Healthy, Faulted, Reconnecting, etc.). /// Most recent successful equipment read; null if never. /// Latest error message; null when none. /// Number of state-transitions into Faulted in the last 5 minutes. /// Timestamp this snapshot was published. /// /// When the driver last raised IRediscoverable.OnRediscoveryNeeded — i.e. it observed that the /// remote's tag set may have changed (a Galaxy redeploy, a TwinCAT symbol-version bump, an MTConnect /// agent restart, a Sparkplug rebirth). Null when the driver has never raised it, or does not /// implement IRediscoverable. /// Advisory only. The served address space is NOT rebuilt in response — raw tags are /// authored explicitly through the /raw browse-commit flow. This is a prompt for an operator /// to re-browse the device and commit whatever changed. /// /// /// The driver-supplied reason string from the same event (e.g. "deploy-time-changed"), shown /// to the operator alongside the prompt. Null when is null. /// /// /// Per-host connectivity as reported by IHostConnectivityProbe.GetHostStatuses(), or null when /// the driver does not implement that capability. Empty (not null) when it does but knows no hosts yet. /// Why this rides the health snapshot rather than a table. The DriverHostStatus /// entity used to own this, with a doc-comment describing a publisher hosted service that upserted rows /// from each driver node. That publisher was never built, and since per-cluster mesh Phase 4 it is /// unbuildable as described: Program.cs gates AddOtOpcUaConfigDb on the admin role, /// so a driver-only node has no ConfigDb connection to write rows to. This channel already reaches /// /hosts, already survives the mesh split via the Phase 5 gRPC telemetry stream, and already /// replays a last-value snapshot on every (re)subscribe — so per-host state re-primes after a /// reconnect without a durable store. The table was dropped. /// Value-equality caveat. A record's generated Equals compares this list by /// REFERENCE, so two DriverHealthChanged carrying equal-but-distinct lists are not equal. /// Nothing dedups on record equality — DriverInstanceActor keeps its own flattened fingerprint /// precisely because of this — but do not introduce such a comparison without fixing it here first. /// /// /// What this driver skipped when it last built its tag table, from /// ISkippedTagReporter.GetSkippedTags(); null when the driver does not implement that /// capability. All twelve production drivers do. /// Why this is on the health snapshot. A skip happens at init, is not an /// error, and does not stop the driver's OTHER tags from reading — so State stays Healthy, /// ErrorCount5Min stays 0, LastError stays null and LastSuccessfulReadUtc keeps /// advancing while the driver ignores part or all of its configuration. On the Makino /// that combination held while 20 of 22 tags were ignored. This field is what contradicts it. /// Additive, never a state change. See ISkippedTagReporter for why a skipped tag /// must not latch the driver non-Healthy — it would satisfy both halves of the comms-loss /// conjunction and reconnect-loop a healthy device. /// Value-equality caveat — the same one carries: the /// record's generated Equals compares the sample list by REFERENCE. /// /// /// Operator-facing driver diagnostics from IDriverDiagnostics.GetDiagnostics() (e.g. S7's /// negotiated PDU size), bounded and in ordinal key order; null when the driver does not implement the /// capability. Additive, never a state change. Same by-reference value-equality caveat as /// . /// public sealed record DriverHealthChanged( string ClusterId, string DriverInstanceId, string State, DateTime? LastSuccessfulReadUtc, string? LastError, int ErrorCount5Min, DateTime PublishedUtc, DateTime? RediscoveryNeededUtc = null, string? RediscoveryReason = null, IReadOnlyList? HostStatuses = null, SkippedTagReport? SkippedTags = null, IReadOnlyDictionary? Diagnostics = null) { /// /// DPS topic name. Both the runtime AkkaDriverHealthPublisher and the AdminUI /// DriverStatusSignalRBridge reference this single constant so renames can't /// silently desynchronise publisher and subscriber. /// public const string TopicName = "driver-health"; }