107 lines
6.1 KiB
C#
107 lines
6.1 KiB
C#
using Evergreen.Health.Application.Health.Sleep;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Evergreen.Health.Application.Health.Integrations.Ib43;
|
|
|
|
public sealed class Ib43MqttService(IOptions<Ib43Options> options, IIb43MqttTransportFactory factory,
|
|
Ib43StateStore store, TimeProvider clock, ILogger<Ib43MqttService> logger, IIb43ReportSink sink = null,
|
|
IBedPresenceSink presenceSink = null) : BackgroundService
|
|
{
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
var configuration = options.Value;
|
|
logger.LogInformation("IB43 MQTT startup: Enabled={Enabled}, Host={Host}, Port={Port}, PasswordConfigured={PasswordConfigured}, Devices={Count}",
|
|
configuration.Enabled, configuration.Host, configuration.Port,
|
|
!string.IsNullOrWhiteSpace(configuration.Password), configuration.Devices?.Count ?? 0);
|
|
if (!configuration.Enabled)
|
|
{
|
|
logger.LogInformation("IB43 MQTT disabled; set Ib43__Enabled=true to enable");
|
|
return;
|
|
}
|
|
// 即使直接构造(测试/其他宿主)也不带错误配置进入重试。
|
|
if (!configuration.IsValid()) throw new InvalidOperationException("Invalid Ib43 configuration");
|
|
var failures = 0;
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
var connectedAt = clock.GetUtcNow();
|
|
var connectionId = Guid.NewGuid();
|
|
using var connection = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
|
var connectionToken = connection.Token;
|
|
try
|
|
{
|
|
using var transport = factory.Create();
|
|
var seenKinds = new HashSet<string>(StringComparer.Ordinal);
|
|
int? lastMatchedBeds = null;
|
|
transport.Message += async (topic, payload, retained) =>
|
|
{
|
|
var report = store.Receive(topic, payload, retained);
|
|
if (sink != null && report.Accepted && report.Readings.Count > 0)
|
|
{
|
|
var binding = configuration.Devices.Single(d => d.PostTopic == topic);
|
|
var reportId = Guid.NewGuid().ToString("N");
|
|
Ib43SavedReport saved;
|
|
// 等待持久化再结束回调(MQTT 自动 ACK 在此之后)。失败时阻塞重试而非静默丢弃。
|
|
while (true)
|
|
{
|
|
stoppingToken.ThrowIfCancellationRequested();
|
|
try
|
|
{
|
|
saved = await sink.SaveAsync(reportId, binding, report, stoppingToken);
|
|
break;
|
|
}
|
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { throw; }
|
|
catch (Exception error)
|
|
{
|
|
logger.LogWarning("IB43 persistence failed ({Type}); retrying in 5 seconds", error.GetType().Name);
|
|
await Task.Delay(TimeSpan.FromSeconds(5), clock, stoppingToken);
|
|
}
|
|
}
|
|
if (saved.MatchedCount != lastMatchedBeds)
|
|
logger.LogInformation("IB43 persistence: matched beds={Count}; zero means no user observation saved", saved.MatchedCount);
|
|
lastMatchedBeds = saved.MatchedCount;
|
|
// 发布失败与写库重试分开,联动问题不能导致重写已保存的报文。
|
|
try { Ib43PresenceAdapter.Publish(presenceSink, binding, report, saved.ActiveBeds, connectionId, connectionToken); }
|
|
catch (Exception error)
|
|
{
|
|
logger.LogWarning("IB43 presence publication failed ({Type})", error.GetType().Name);
|
|
}
|
|
}
|
|
if (!report.Accepted || report.Issues.Count > 0)
|
|
logger.LogDebug("IB43 report has {Count} parsing issues", report.Issues.Count);
|
|
if (report.Accepted && (report.Readings.Count > 0 || report.Fields.Count > 0))
|
|
foreach (var kind in report.Kinds)
|
|
if (seenKinds.Add(kind))
|
|
logger.LogInformation("IB43 first {Kind} report received in this connection", kind);
|
|
};
|
|
transport.OversizedMessage += store.RejectOversized;
|
|
store.SetTransport("connecting");
|
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
|
timeout.CancelAfter(TimeSpan.FromSeconds(15));
|
|
await transport.ConnectAndSubscribeAsync(configuration, timeout.Token);
|
|
store.SetTransport("connected");
|
|
connectedAt = clock.GetUtcNow();
|
|
logger.LogInformation("IB43 MQTT connected and subscribed to {Count} configured devices", configuration.Devices.Count);
|
|
while (transport.IsConnected)
|
|
await Task.Delay(TimeSpan.FromSeconds(1), clock, stoppingToken);
|
|
}
|
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; }
|
|
catch (Exception exception)
|
|
{
|
|
// 不写密码、连接选项、完整异常文本或原始健康报文。
|
|
logger.LogWarning("IB43 MQTT connection/subscription failed ({Type}); retrying", exception.GetType().Name);
|
|
}
|
|
finally
|
|
{
|
|
connection.Cancel();
|
|
store.SetTransport("disconnected");
|
|
}
|
|
if (clock.GetUtcNow() - connectedAt > TimeSpan.FromMinutes(1)) failures = 0;
|
|
var seconds = Math.Min(30, Math.Pow(2, Math.Min(failures++, 5)));
|
|
try { await Task.Delay(TimeSpan.FromSeconds(seconds), clock, stoppingToken); }
|
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; }
|
|
}
|
|
}
|
|
}
|