using System.Collections.Concurrent; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt; /// /// Thread-safe last-observed-value store backing for MQTT/Sparkplug B. /// MQTT is a subscribe-first protocol — the driver holds one live broker connection and values /// arrive pushed, not polled. But the OPC UA server still issues polled IReadable.ReadAsync /// batches, so this cache is the bridge: the subscription path (message handler) calls /// with the newest observed value per reference, and the read path serves /// the most recent one via . /// /// /// Keyed by RawPath — the v3 driver-reference identity (see /// EquipmentTagRefResolver<TDef> and , /// which is the RawPath) — never a topic/JSON-path-derived key. The parameter is /// therefore named rawPath throughout rather than "topic" or "key" to keep that identity /// fact visible at every call site. /// public sealed class LastValueCache { // OPC UA BadWaitingForInitialData (0x80320000) — the repo-wide convention for "no value has // been observed yet" (mirrored by CalculationDriver before its first evaluation, // VirtualTagEngine for a freshly-materialised node, and OtOpcUaNodeManager / // AddressSpaceApplier for a just-deployed variable). MQTT is subscribe-first, so an unseen // RawPath is in exactly that state until its first publish arrives. Deliberately NOT // GoodNoData — that code is reserved (see NullHistorianDataSource, OtOpcUaNodeManager // HistoryRead paths) for "the historian window held no samples", a different question from // "has this live reference ever been observed". private const uint BadWaitingForInitialData = 0x80320000u; private readonly ConcurrentDictionary _values = new(); /// /// Record the newest observed value for . Called from the MQTT /// subscription/message-handling path. A null or empty /// is a no-op — never throws. /// /// The RawPath identifying the tag. /// The newest observed value/quality/timestamps for that tag. public void Update(string rawPath, DataValueSnapshot snapshot) { if (string.IsNullOrEmpty(rawPath)) return; _values[rawPath] = snapshot; } /// /// Read the last observed value for . Never throws: a /// null/empty or a RawPath never observed both return a /// snapshot rather than an exception, so a batch read /// covering many references degrades per-reference instead of failing the whole call. /// /// The RawPath identifying the tag. /// The last observed snapshot, or a BadWaitingForInitialData snapshot if none has been observed. public DataValueSnapshot Read(string rawPath) { if (!string.IsNullOrEmpty(rawPath) && _values.TryGetValue(rawPath, out var snapshot)) return snapshot; return new DataValueSnapshot(null, BadWaitingForInitialData, null, DateTime.UtcNow); } }