8000 fix(session): bound the sensor sync wait and fail closed by shroominic · Pull Request #134 · shroominic/OpenGlucose · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
8000
Diff view
84 changes: 84 additions & 0 deletions openhealth/lib/src/app_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ class CgmAppController extends ChangeNotifier {
static const _scanTimeout = Duration(seconds: 6);
static const _historyPersistDebounce = Duration(milliseconds: 900);
static const _restoredConnectDelay = Duration(milliseconds: 700);
// A live session that reaches the syncing stage can stop making progress
// when the sensor's frames never decode into a reading. Bound that wait so
// setup fails closed with a next action instead of spinning forever.
static const _syncStageDeadline = Duration(seconds: 45);
static const _syncStalledStatusText = 'Sensor sent no readable reading';
static const _liveRefreshThreshold = Duration(minutes: 2);
static const _historyCatchUpThreshold = Duration(minutes: 5);
static const _resumeOffsetMetadataKey = 'resumeOffset';
Expand Down Expand Up @@ -85,6 +90,8 @@ class CgmAppController extends ChangeNotifier {
StreamSubscription<CgmLogEntry>? _logSubscription;
Timer? _historyPersistTimer;
Timer? _reconnectTimer;
Timer? _syncStageTimer;
bool _syncStageStalled = false;
CgmSessionSnapshot? _snapshot;
DiscoveredSensor? _selectedSensor;
List<CgmReading> _persistedHistory = const <CgmReading>[];
Expand Down Expand Up @@ -586,6 +593,8 @@ class CgmAppController extends ChangeNotifier {
);
_logs.clear();
_lastError = null;
_cancelSyncStageDeadline();
_syncStageStalled = false;
_startPlatformTask(
_pushLiveActivity(),
'Updating private lock-screen state',
Expand All @@ -601,6 +610,15 @@ class CgmAppController extends ChangeNotifier {
);
_session = session;
_snapshotSubscription = session.snapshots.listen((nextSnapshot) {
if (_syncStageStalled) {
if (nextSnapshot.stage == CgmSyncStage.syncing) {
// The bounded failure already replaced this stage. Keep the
// terminal state until the driver reports data, an error, or a
// disconnect.
return;
}
_syncStageStalled = false;
}
final isErrorSnapshot = nextSnapshot.stage == CgmSyncStage.error;
if (isErrorSnapshot) {
_debugAppSessionTrace('error-snapshot-received');
Expand Down Expand Up @@ -673,6 +691,7 @@ class CgmAppController extends ChangeNotifier {
} else {
_cancelReconnect();
}
_trackSyncStageDeadline();
_startPlatformTask(
_pushLiveActivity(),
'Updating private lock-screen state',
Expand Down Expand Up @@ -750,6 +769,7 @@ class CgmAppController extends ChangeNotifier {
initialSnapshot?.stage == CgmSyncStage.ready) {
await initialSelectionPromotion;
}
_trackSyncStageDeadline();
notifyListeners();
} catch (error) {
final safeError = _safeError('Connection', error);
Expand Down Expand Up @@ -1147,6 +1167,8 @@ class CgmAppController extends ChangeNotifier {
_clearInspectedBondTransfer();
await _invalidateScan();
_cancelReconnect();
_cancelSyncStageDeadline();
_syncStageStalled = false;
_historyPersistTimer?.cancel();
_historyPersistTimer = null;
final sensorToArchive = clearSelection ? _selectedSensor : null;
Expand Down Expand Up @@ -1322,6 +1344,7 @@ class CgmAppController extends ChangeNotifier {
_historyPersistTimer?.cancel();
_historyPersistTimer = null;
_cancelReconnect();
_cancelSyncStageDeadline();
unawaited(_snapshotSubscription?.cancel());
unawaited(_logSubscription?.cancel());
super.dispose();
Expand Down Expand Up @@ -2220,6 +2243,67 @@ class CgmAppController extends ChangeNotifier {
_reconnectTimer = null;
}

/// Arms the bounded wait for the first readable result of a live session.
///
/// The wait is skipped when the driver owns its own terminal outcome or the
/// stage is expected to run long, so a healthy session is never failed for
/// merely taking its time.
void _trackSyncStageDeadline() {
final snapshot = _snapshot;
if (_disposed ||
isMockDriver ||
snapshot == null ||
snapshot.stage != CgmSyncStage.syncing ||
_syncStageWaitsAreDriverOwned(snapshot)) {
_cancelSyncStageDeadline();
return;
}
_syncStageTimer ??= Timer(_syncStageDeadline, _failSyncStage);
}

bool _syncStageWaitsAreDriverOwned(CgmSessionSnapshot snapshot) {
// Libre 2 publishes its own phase and terminal failure, and an in-progress
// history sync is a bounded, driver-owned exchange with its own progress.
return snapshot.metadata['cgm.libre2.phase'] != null ||
snapshot.historySync.inProgress ||
snapshot.metadata.containsKey(cgmBondTransferStateMetadataKey);
}

/// Fails the stalled session closed so setup shows a next action instead of
/// an endless "Syncing sensor history" card.
void _failSyncStage() {
_syncStageTimer = null;
final snapshot = _snapshot;
if (_disposed ||
snapshot == null ||
snapshot.stage != CgmSyncStage.syncing) {
return;
}
_syncStageStalled = true;
_lastError = sensorSyncStalledMessage;
_cancelReconnect();
_snapshot = snapshot.copyWith(
stage: CgmSyncStage.error,
statusText: _syncStalledStatusText,
lastError: sensorSyncStalledMessage,
metadata: <String, String>{
...snapshot.metadata,
cgmAutomaticReconnectAllowedMetadataKey: 'false',
cgmSessionSyncStalledMetadataKey: 'true',
},
);
_startPlatformTask(
_pushLiveActivity(),
'Updating private lock-screen state',
);
notifyListeners();
}

void _cancelSyncStageDeadline() {
_syncStageTimer?.cancel();
_syncStageTimer = null;
}

bool _canAutomaticallyReconnect(CgmSessionSnapshot currentSnapshot) {
if (!snapshotAllowsAutomaticReconnect(currentSnapshot)) {
return false;
Expand Down
15 changes: 12 additions & 3 deletions openhealth/lib/src/sensor_connection_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,7 @@ class _SensorConnectionScreenState extends State<SensorConnectionScreen> {
return switch (_connectionStage) {
CgmSyncStage.ready => const SizedBox.shrink(),
CgmSyncStage.error || CgmSyncStage.disconnected => _ConnectionFailure(
title: libreConnectionWasLost(_controller.snapshot)
? 'Connection lost'
: 'Could not connect',
title: _connectionFailureTitle,
message: _connectionFailureMessage,
onRetry: _connectingSensor == null || _actionInProgress
? null
Expand Down Expand Up @@ -287,6 +285,17 @@ class _SensorConnectionScreenState extends State<SensorConnectionScreen> {
'OpenGlucose could not connect to this sensor.';
}

String get _connectionFailureTitle {
final snapshot = _controller.snapshot;
if (snapshot != null && snapshotHasSyncStalled(snapshot)) {
// The link came up, but the sensor never returned a readable reading.
return 'No reading from this sensor';
}
return libreConnectionWasLost(snapshot)
? 'Connection lost'
: 'Could not connect';
}

void _backFromInlineSetup() {
if (_libre2Expanded) {
_collapseLibre2Setup();
Expand Down
14 changes: 14 additions & 0 deletions openhealth/lib/src/session_presentation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,20 @@ bool snapshotAllowsAutomaticReconnect(CgmSessionSnapshot snapshot) {
true;
}

/// Closed session metadata set by the host when a connected session stopped
/// making progress inside its bounded sync deadline. The host writes `true`.
const cgmSessionSyncStalledMetadataKey = 'cgm.session.syncStalled';

/// Product copy for a sensor that connected but never produced a readable
/// reading inside that deadline. It names a next action without promising
/// compatibility with sensor models this build cannot decode.
const String sensorSyncStalledMessage =
"This sensor didn't return a readable reading. Try again, or choose "
'another sensor. Some sensor models are not supported yet.';

bool snapshotHasSyncStalled(CgmSessionSnapshot snapshot) =>
snapshot.metadata[cgmSessionSyncStalledMetadataKey] == 'true';

class GlucoseTrendSummary {
const GlucoseTrendSummary({this.symbol = '', this.deltaText = ''});

Expand Down
Loading
Loading
0