using Microsoft.Extensions.Options; using System.Text.Json; namespace Evergreen.Health.Application.Health.Integrations.Ib43; public sealed record Ib43Field(JsonElement Value, DateTimeOffset ReceivedAt); public sealed record Ib43Snapshot(string ProductKey, string SerialNumber, bool Simulation, string Transport, string Freshness, DateTimeOffset? LastTelemetryAt, IReadOnlyDictionary Readings, IReadOnlyDictionary Fields); public sealed record Ib43Diagnostic(string Topic, DateTimeOffset ReceivedAt, string Raw, string[] Issues); /// 进程内有界缓存。没有设备遗嘱,不能将数据超时等同于已确认设备离线。 public sealed class Ib43StateStore(IOptions options, Ib43Parser parser, TimeProvider clock) { public const int MaxPayloadBytes = 65536; private readonly object _gate = new(); private readonly Dictionary _states = new(StringComparer.Ordinal); private readonly Queue _diagnostics = new(); private string _transport = options.Value.Enabled ? "disconnected" : "disabled"; private sealed class DeviceState { public DateTimeOffset? LastTelemetryAt; public Dictionary Readings { get; } = new(StringComparer.Ordinal); public Dictionary Fields { get; } = new(StringComparer.Ordinal); } public void SetTransport(string state) { lock (_gate) _transport = state; } public void RejectOversized(string topic) { lock (_gate) { _diagnostics.Enqueue(new(topic, clock.GetUtcNow(), "", ["payload-too-large:body-not-retained"])); while (_diagnostics.Count > 32) _diagnostics.Dequeue(); } } public Ib43Report Receive(string topic, string payload, bool retained = false) { lock (_gate) { var now = clock.GetUtcNow(); var binding = options.Value.Devices.FirstOrDefault(d => d.PostTopic == topic); Ib43Report report; if (binding == null || retained || System.Text.Encoding.UTF8.GetByteCount(payload) > MaxPayloadBytes) { report = new() { Raw = payload.Length > 4096 ? payload[..4096] : payload, ReceivedAt = now }; report.Issues.Add(binding == null ? "unbound-topic" : retained ? "retained-not-live" : "payload-too-large"); } else report = parser.Parse(topic, payload, binding, now); _diagnostics.Enqueue(new(topic, now, report.Raw, report.Issues.ToArray())); while (_diagnostics.Count > 32) _diagnostics.Dequeue(); if (!report.Accepted) return report; if (!_states.TryGetValue(topic, out var state)) _states[topic] = state = new(); foreach (var (key, value) in report.Readings) state.Readings[key] = value; foreach (var (key, value) in report.Fields) state.Fields[key] = new(value, now); // 只有有效实时字段刷新活跃性;能力包、未知包和坏包不能掩盖实时数据停止。 if (report.Readings.Count > 0 || report.Fields.Keys.Any(k => k is "airbagsPerson" or "airbagsPressure" or "motorHeight" or "motorStatus" or "snoreStatus")) state.LastTelemetryAt = now; return report; } } public Ib43Snapshot[] Snapshot() { lock (_gate) { var now = clock.GetUtcNow(); return options.Value.Devices.Select(binding => { _states.TryGetValue(binding.PostTopic, out var state); var last = state?.LastTelemetryAt; var freshness = last == null ? "unknown" : now - last.Value > TimeSpan.FromSeconds(options.Value.StaleAfterSeconds) ? "stale" : "recent"; return new Ib43Snapshot(binding.ProductKey, binding.SerialNumber, binding.Simulation, _transport, freshness, last, state == null ? new Dictionary() : new(state.Readings), state == null ? new Dictionary() : new(state.Fields)); }).ToArray(); } } // 不通过公共 H5 API 暴露原始健康数据;后续持久化需绑定用户权限与保留期限。 public Ib43Diagnostic[] Diagnostics() { lock (_gate) return _diagnostics.ToArray(); } }