Skip to content

Scheduled spot SpO2, longer intervals, 120m HR, and reliability fixes (#103) - #134

Open
foureight84 wants to merge 7 commits into
mainfrom
feat/103-longer-spo2-intervals
Open

foureight84 wants to merge 7 commits into
mainfrom
feat/103-longer-spo2-intervals

Conversation

@foureight84

@foureight84 foureight84 commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Consolidated PR folding #132 and #134 together, retargeted to main with all fixes from RC3/RC4 hardware testing on #103.

What is included

  1. App-started spot SpO2 (Hourly SpO2 on this YCBT ring looks computed from heart rate #103):

    • Opt-in background worker (ScheduledSpo2Worker) that runs real spot SpO2 measurements between the ring's background readings.
    • Dedicated intervals: 60, 120, and 180 min (ScheduledSpo2Prefs).
    • Clean 3-stop slider (steps = 1) for 1h, 2h, and 3h under Measurement Frequency.
    • Decoupled from the ring's SpO2 toggle so users can measure from the app even when the ring's built-in SpO2 monitor is disabled.
  2. Longer Heart-Rate Intervals (YCBT):

    • 30–120 min heart rate interval slider for YCBT and Colmi SmartHealth rings (01 0c 01 78 verified on hardware).
    • Slot grid and isClear collision checks anchor to the active HR interval, preventing phantom 30m skips when runs are delayed by Doze.
  3. BLE GATT Contention Resilience:

    • ScheduledSpo2Worker now retries and waits up to 45s for background sync to release the GATT lock when Android Doze wakes both workers simultaneously.
  4. Real vs. Estimated SpO2 UI:

    • "Hide estimated SpO₂" toggle in settings (default on) to hide hourly estimates derived from HR and show only real spot readings.
    • Individual reading rows in the Vital Detail view display an Estimated tag when derived from HR.
  5. Health Connect Filtering:

    • Hourly SpO2 estimates derived from HR are blocked from exporting to Health Connect.
    • Respiratory rate is excluded from export for YCBT rings that derive it as ~0.2 × HR, and greyed out in settings.
  6. Cold Process Sync Fix:

    • EventPersistenceSubscriber is started in PulseLoopApplication.onCreate so background sync data isn't dropped when Android launches the process without UI.

Release Candidate Testing

…rted

RingSyncWorker publishes what it pulls from the ring onto PulseEventBus, which keeps
no backlog (replay = 0). The only collector that writes it to Room was built and
started by the UI's startup (AppServices / PulseLoopApp), so in a process Android
launched just for the worker, every event was published to no one and dropped.

The subscriber is now one per process and started from PulseLoopApplication.onCreate;
AppServices uses the same instance. Groundwork for #103's scheduled spot SpO2, which
runs from the background too.
On YCBT rings the hourly SpO2 is computed from heart rate. The ring's manual
measurement is real, so the app can now run it on the SpO2 interval:
Measurement Frequency -> "Measure SpO2 from the app", off by default, offered for
YCBT rings with manual SpO2.

- Slots sit halfway between the ring's own readings on a grid from local midnight
  (60 min -> :30, 30 -> :15/:45). A run WorkManager starts within 5 min of a ring
  reading, or that would still be measuring at the next one, is skipped.
- ScheduledSpo2Worker is one-time work that appends the next slot as it finishes;
  app start and RingSyncWorker revive the chain if a run was cancelled.
- The ring stops the measurement when the link drops, so the run holds the
  connection until 04 0e: through the app's link when it has one, otherwise a
  private client with its own RingSyncCoordinator, which runs the normal startup
  sync first and stores the reading exactly as the Vitals button does.
…#103)

The measurement-frequency sliders stopped at 60 min because that is the
vendor app's own menu. Two things go past it now, each on its own evidence:

- Measure SpO2 from the app gets an interval of its own (60/120/180 min),
  independent of the ring's SpO2 setting — the app starts these runs, so the
  ring's limits don't apply, and each run is ~50 s of red LED that 2-3 h
  spacing spares at night. The slots keep their half-interval offset from the
  ring's grid; isClear still skips a run that lands near a ring reading.
- The heart-rate slider goes to 120 min for the YCBT family: the ring ACKed
  01 0c 01 78 with the app off and read at 10:00 and 12:00 (issue #103,
  2026-10-22). That is the only value tested, so nothing longer and no other
  family. The other monitor commands keep the 60-min ceiling their bytes
  always carried — 120 is not evidenced on their keys, and the blood-oxygen
  key's grid is what the scheduled spot has to stay between.

Slots at 120/180 min: unit tests against a 60- and a 30-min ring grid;
1591 tests, 0 failures.
Albabit's ring (issue #103, 2026-10-23) is catalogued COLMI_SMART_HEALTH
(model colmi-smarthealth) — same YCBT driver, but its own RingDeviceType,
so the == YCBT checks hid the scheduled spot SpO2 toggle and capped the
HR slider at 5-60. Gate the scheduled-SpO2 support check and the raised
measurement bounds (min 30, HR max 120) on both YCBT-protocol families;
same for the stress-toggle exclusion, since YCBTEncoder.monitorCommands
carries no stress key for either family. Manual-SpO2 capability still
required, and the worker path shares the one gate.
… estimated filtering (#103)

- Fix app SpO2 slider stops with steps=1 (60, 120, 180 min).
- Retry GATT connection in ScheduledSpo2Worker to wait out background sync.
- Base slot scheduling and isClear on HR interval for YCBT rings.
- Allow scheduling app spot SpO2 even if ring SpO2 toggle is disabled.
- Add toggle to hide estimated SpO2 from vitals card and detail chart.
- Display Estimated tag on individual reading rows in detail screen.
- Exclude estimated SpO2 and derived respiratory rate from Health Connect export.
… and prefs (#103)

- Verify computeRingIntervalMinutes uses HR interval on YCBT even when SpO2 is off.
- Verify 180 min app SpO2 on 120 min ring grid stays clear over repeated cycles.
- Verify VitalsExporter excludes estimated SpO2 and YCBT respiratory rate.
- Verify ScheduledSpo2Prefs hideEstimated and interval settings round-trip.
@foureight84
foureight84 force-pushed the feat/103-longer-spo2-intervals branch from 7458bff to 4f7c380 Compare October 24, 2026 06:51
@foureight84
foureight84 changed the base branch from feat/103-scheduled-spot-spo2 to main October 24, 2026 07:48
@foureight84 foureight84 changed the title Longer intervals for app-started SpO2 and heart rate (#103) Scheduled spot SpO2, longer intervals, 120m HR, and reliability fixes (#103) Oct 24, 2026
…chart (#103)

- Feed displayedSpo2 into spo2Series so Today/Vitals cards and widgets filter estimated readings.
- Coordinate GATT access: pause ScheduledSpo2Worker timeout while RingSyncWorker syncs; yield sync when measuring.
- Scale day-view chart maxGapMs with sample interval (max(90m, interval * 1.5)) so 120m HR/SpO2 readings connect.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant