89 lines
4.4 KiB
C#
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(); }
|
|
}
|