Skip to content
Merged
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
Diff view
20 changes: 20 additions & 0 deletions lib/ble/ble_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2538,6 +2538,26 @@ class BleEngine {
_log('Live streams enabled (optical: wrist-gated).');
}

/// Clear the sticky standard-HR fallback and give the full live set another
/// chance. The fallback protects a struggling radio from the high-rate
/// flood, but it never resets and [enableLiveStreams] honours it silently —
/// so once tripped, every later step calibration / workout counted 0 steps
/// for the rest of the process lifetime (the IMU toggles were never sent).
/// Call this from EXPLICIT foreground user actions whose feature needs the
/// 100 Hz stream: there the flood is the point, and if the radio genuinely
/// can't sustain it the detectors re-trip (and re-downgrade) within seconds.
Future<void> retryFullLiveStreams() async {
if (state.standardHrFallback) {
_log('Radio fallback: cleared by explicit user action — retrying the '
'full live set.');
state.standardHrFallback = false;
_marginalRadio.reset();
_frameCorruption.reset();
onState(state);
}
await enableLiveStreams();
}

/// Background live downgrade: keep ONLY the compact realtime-HR stream (0x28)
/// armed and turn the high-rate R10/R11 + IMU + optical flood OFF. Used while
/// backgrounded with no live consumer, so the radio isn't saturated by a raw
Expand Down
26 changes: 23 additions & 3 deletions lib/state/app_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2813,11 +2813,17 @@ class AppState extends ChangeNotifier {
// open session still expects on. If the background downgrade left live in
// HR-only, upgrade to full (the walk needs the 100 Hz IMU stream) without
// taking ownership.
//
// retryFullLiveStreams (not enableLiveStreams): the walk NEEDS the 100 Hz
// IMU stream, and the sticky standard-HR fallback silently vetoes it —
// every calibration after a fallback trip counted 0 steps forever. An
// explicit user-initiated walk is exactly the moment to give the full
// flood another chance; the detectors re-trip if the radio can't cope.
if (!engine.liveEnabled) {
await engine.enableLiveStreams();
await engine.retryFullLiveStreams();
_stepCalEnabledStreams = true;
} else if (engine.liveHrOnly) {
await engine.enableLiveStreams();
} else if (engine.liveHrOnly || device.standardHrFallback) {
await engine.retryFullLiveStreams();
}
_resetLivePedometer(); // count this walk from 0
notifyListeners();
Expand Down Expand Up @@ -2917,6 +2923,20 @@ class AppState extends ChangeNotifier {
if (activeWorkout != null) return;
final start = DateTime.now();
final id = workoutId ?? 'w${start.millisecondsSinceEpoch}';
// The workout screen's live step count rides the 100 Hz IMU stream, which
// the sticky standard-HR fallback silently suppresses (same starvation as
// the calibration walk) — and which may simply be off (spot-check cleanup
// restores streams to OFF when they were off before) or still in the
// background HR-only downgrade. A deliberate workout start is an explicit
// user action — retry the full live set; detectors re-trip if it can't
// hold. No ownership flag: the background downgrade / session close
// manage the stream lifecycle exactly as for openSession's arming.
if (isConnected &&
(!engine.liveEnabled ||
engine.liveHrOnly ||
device.standardHrFallback)) {
unawaited(engine.retryFullLiveStreams());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
_workoutRawBase = _liveRaw;
activeWorkout = LiveWorkoutState(
startTime: start,
Expand Down
19 changes: 19 additions & 0 deletions lib/ui/today/step_calibration_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ class _StepCalibrationScreenState extends State<StepCalibrationScreen> {
@override
Widget build(BuildContext context) {
final steps = context.select<AppState, int>((a) => a.liveSteps);
// The standard-HR radio fallback suppresses the 100 Hz IMU stream this
// walk counts on. startStepCalibration clears it and retries; if it
// TRIPS AGAIN mid-walk the radio genuinely can't sustain the stream —
// say so instead of showing "Keep walking…" over a count of 0 forever.
final radioDegraded =
context.select<AppState, bool>((a) => a.device.standardHrFallback);
final done = _learnedCadence != null;
final t = (_target > 0 ? steps / _target : 0.0).clamp(0.0, 1.0).toDouble();
final ready = steps >= _target;
Expand Down Expand Up @@ -144,6 +150,19 @@ class _StepCalibrationScreenState extends State<StepCalibrationScreen> {
: Text(_started ? 'Keep walking…' : 'Starting…',
style: AppText.label.copyWith(color: AppColors.inkSoft)),
),
if (radioDegraded && _started && !ready) ...[
const SizedBox(height: Sp.x4),
StateCard(
icon: OsIcon.bluetooth,
title: "Bluetooth can't keep up",
message:
'The connection to your strap is struggling to carry the '
'high-rate motion stream, so steps aren\'t coming through. '
'Bring your phone closer to the strap and retry.',
actionLabel: 'Retry stream',
onAction: _start,
),
],
const SizedBox(height: Sp.x6),
SurfaceCard(
entranceIndex: 1,
Expand Down