diff --git a/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs b/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs index d216eed..99e0761 100644 --- a/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs +++ b/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs @@ -913,17 +913,18 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi // A cache hit bypasses the BCD pipeline, so the debug-view capture // would otherwise never see cache-served reads. Replay the - // observations captured when this entry was stored — re-stamped now, - // since the client receives this value right now — so the detail - // page reflects what the client actually gets. Entries stored while - // no viewer was armed carry no observations; those tags self-heal on - // the next cache miss. + // observations captured when this entry was stored, preserving each + // observation's ORIGINAL timestamp — the value the client receives is + // cache-aged (up to CacheTtlMs old), so the debug view must show its + // true age, not a misleading "just now". Entries stored while no + // viewer was armed carry no observations; those tags self-heal on the + // next cache miss. if (_ctx.Capture is { IsArmed: true } capture && cached.CapturedTags is { Count: > 0 } cachedTags) { foreach (var obs in cachedTags) capture.Record(obs.Address, obs.RawLow, obs.RawHigh, - obs.DecodedValue, CaptureDirection.Read); + obs.DecodedValue, CaptureDirection.Read, obs.UpdatedAtUtc); } byte[] hitFrame = BuildCacheHitFrame(originalTxId, unitId, cached.PduBytes); diff --git a/mbproxy/src/Mbproxy/Proxy/TagValueCapture.cs b/mbproxy/src/Mbproxy/Proxy/TagValueCapture.cs index c1b97a0..77c125d 100644 --- a/mbproxy/src/Mbproxy/Proxy/TagValueCapture.cs +++ b/mbproxy/src/Mbproxy/Proxy/TagValueCapture.cs @@ -126,7 +126,15 @@ internal sealed class TagValueCapture /// BCD-encoded low word as it sits on the PLC wire. /// BCD-encoded high word (0 for a 16-bit tag). /// Decoded binary integer the client reads/wrote. - public void Record(ushort address, ushort rawLow, ushort rawHigh, int decoded, CaptureDirection direction) + /// + /// When the value was actually observed from the PLC. Defaults to "now" for a live + /// pipeline observation. The response-cache hit path passes the cached observation's + /// original timestamp so a cache-served read shows its true age in the debug view + /// rather than appearing freshly read. + /// + public void Record( + ushort address, ushort rawLow, ushort rawHigh, int decoded, CaptureDirection direction, + DateTimeOffset? observedAtUtc = null) { if (!_armed) return; @@ -137,7 +145,7 @@ internal sealed class TagValueCapture ref _slots[idx], new TagValueObservation( _addresses[idx], _widths[idx], _names[idx], rawLow, rawHigh, decoded, direction, - DateTimeOffset.UtcNow)); + observedAtUtc ?? DateTimeOffset.UtcNow)); // A concurrent Disarm() may have flipped _armed (and cleared the slots) between // the _armed check above and the write just made — which would strand a stale