Files
Evergreen-Health/Evergreen.Health.Application/Health/Integrations/Ib43/Ib43StateStore.cs
T
2026-11-11 16:11:41 +08:00

89 lines
4.4 KiB
C#

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<string, Ib43Reading> Readings, IReadOnlyDictionary<string, Ib43Field> Fields);
public sealed record Ib43Diagnostic(string Topic, DateTimeOffset ReceivedAt, string Raw, string[] Issues);
/// <summary>进程内有界缓存。没有设备遗嘱,不能将数据超时等同于已确认设备离线。</summary>
public sealed class Ib43StateStore(IOptions<Ib43Options> options, Ib43Parser parser, TimeProvider clock)
{
public const int MaxPayloadBytes = 65536;
private readonly object _gate = new();
private readonly Dictionary<string, DeviceState> _states = new(StringComparer.Ordinal);
private readonly Queue<Ib43Diagnostic> _diagnostics = new();
private string _transport = options.Value.Enabled ? "disconnected" : "disabled";
private sealed class DeviceState
{
public DateTimeOffset? LastTelemetryAt;
public Dictionary<string, Ib43Reading> Readings { get; } = new(StringComparer.Ordinal);
public Dictionary<string, Ib43Field> 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<string, Ib43Reading>() : new(state.Readings),
state == null ? new Dictionary<string, Ib43Field>() : new(state.Fields));
}).ToArray();
}
}
// 不通过公共 H5 API 暴露原始健康数据;后续持久化需绑定用户权限与保留期限。
public Ib43Diagnostic[] Diagnostics() { lock (_gate) return _diagnostics.ToArray(); }
}