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 options, IIb43MqttTransportFactory factory, Ib43StateStore store, TimeProvider clock, ILogger 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(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; } } } }