Repository navigation
Releases: alien2003/PulseStages
Release list
PulseStages 0.5.0
"Ask Sleep as Android to sync" now also waits until you are up: heart rate that OHealth uploads while you still sleep no
longer lets a night cut short reach Sleep as Android, and a notification tells you when a night that Sleep as Android
was asked about later gets another start or end, and how to repair it there. Install it over 0.4.1 like any update (same
signing key): the app keeps its data and settings, no night becomes stale, and no new permission is needed. With
OHealth, open it 75 minutes or more after waking up, once you have walked a little after the first hour, and pull down
on its "Home" tab (below).
Added
- The notification "Check a night in Sleep as Android". Sleep as Android never updates a night it imported, so a
night that PulseStages changes after asking Sleep as Android to sync may be out of date there. PulseStages now
remembers the nights each request covered and, when a later run writes one of them with another start or end (in
place, or merged into a new night with a part the watch app added after a short wake), shows "Check a night in Sleep
as Android" with the night's date, its times now and its times when Sleep as Android was asked (all of them when
nights of the request were merged), and how to repair it: delete there every record with the cloud icon between the
two times it names, which also covers a part of the night that Sleep as Android imported apart, without undoing the
deletion, then "Recompute" the night in PulseStages. Tapping the notification opens the night's detail; its button
"Open Sleep as Android" opens Sleep as Android. There is one notification per new version of a night: a newer version
of the same night replaces it without a second sound, a merged night's notification replaces the earlier one and
alerts again unless its times are the same, and a night back at the times Sleep as Android was asked about has its
notification removed; a recompute that keeps the times brings none. It has its own category, "Nights in Sleep as
Android", which can be turned off in Android's notification settings once the first notification has come; without
notifications, the Log still has the line. On the lock screen Android shows the full text unless it is set to hide
sensitive content. It covers only the nights of PulseStages' last request, not a "Sync now"; under "Never overwrite"
only a session that the watch app deleted and wrote again with other times can bring one, and since "Recompute"
rewrites nothing under that option, its repair then says to tap "Sync now" in Sleep as Android (Settings → Services)
instead. Tested in the build's tests, not yet on the development phone (user
guide, health-connect.md
§8.3, PLAN.md §2). - Merge-gap experiment (pre-registered: docs/prereg/prereg-merge-gap.md with its
digest and command log; ALGORITHM.md
§13.14):train.py mergegap-base,
mergegap-gates,mergegapandmergegap-report, withpulsetrain/mergegap.py(split nights constructed in
labelled nights, decoded merged and split, the cells, positive controls and decision rule) and the grouping fixture
tests/data/mergegap-grouping.jsonshared by the Python emulation andMergeGapGroupingFixtureTest; the Sleep as
Android replay of each arm,MergeGapGateReplayTestin:work(JUnit tagmergegap-replay, excluded from the
default test tasks;-Ppulsestages.mergegapReplay=true,-Ppulsestages.mergegapArms=<arms>,
-Ppulsestages.mergegapReport=<json>), with the gate's scenario timelines moved to
SleepAsAndroidScenarioTimelines.kt(tools/train/README.md). Outcome: "Merge gap" stays 30
min (case 4 of the rule: merging a separate nap that starts 35–59 min after the night made the night's staging
worse); nothing was installed and no estimate changes. The trial setting of the experiment, "Merge gap after a long
session" (sources.continuationGapMin), was removed before any release; a settings file or export that lists it
loads with the key ignored.
Changed
- The Sleep as Android request also waits for a walk after the night. OHealth's code can upload heart rate alone
while the user still sleeps (when the watch reports new heart-rate data, or when a pull's sleep fetch fails or is cut
off), and heart rate past a night cut at 07:29 would then release that night. For a watch app that writes steps minute
by minute, as OHealth does, the request now also waits for a minute with 5 or more steps from about an hour after the
end of the night on (Settings hint of "Ask Sleep as Android to sync", en and ru); a heart-rate-only upload brings no
steps. Steps before the night's end do not count (rarely, a walk during a long sleep-in after a night cut short still
lets it through). A watch app without steps, or with only coarse step totals, is held by heart rate alone as before.
The cost: with OHealth, open it 75 minutes or more after waking up once you have walked a little after the first hour,
and on a day in bed the night reaches Sleep as Android only about 12 hours after it ended. The check of an owed
request makes up to three more Health Connect reads once the heart rate is there. Tested with replays of 18 and 19
October and a constructed sleep-in scenario, not yet on the development phone (user
guide, health-connect.md
§8.3, PLAN.md §2). - Home's notifications row now reads "Notifications (progress, Sleep as Android)", and without the permission "Not
allowed: long runs show no progress, and a night to check in Sleep as Android is noted only in the Log." (en and ru). - The privacy screen now says that with "Ask Sleep as Android to sync" steps are also read after the night (a minute
of walking shows that you are up), and that a notification on the phone says how to update a night that changed
afterwards in Sleep as Android (en and ru; privacy.md
§1 and
§2). - Opening the app after the notification. The notification opens the app's screen with the same identity as the
launcher icon, so that when it starts the app, the launcher icon later brings the app back as it was instead of
opening a second copy of its screen, whatever screen of the app is on top. The app's activity is alsosingleTop:
another notification that opens it while it is on top, such as the progress notification, reaches the existing screen
(neither checked on a phone yet) (architecture.md §2). - When Sleep as Android gets a night with OHealth. The hint of "Ask Sleep as Android to sync", the user guide, the
README and the privacy notes no longer say that the request usually comes 1–2 hours after you wake up. It comes once
the watch app's heart rate in Health Connect reaches about an hour past the wake, and OHealth uploads heart rate only
when it pulls it from the watch: when the watch reconnects, when the watch reports the wake-up or (not observed) new
heart-rate data, and when OHealth is opened. It has no regular schedule in the background, and no OHealth setting
changes that (OHealth 4.62.7 on the development phone, 19–20 October 2026: after the night's last upload at 08:10
nothing reached Health Connect until the user opened OHealth at 13:55). The user guide now advises opening OHealth 75
minutes or more after waking up (once you have walked a little after the first hour, above) and pulling down on its
"Home" tab (the screen headed "Health"); with checks every 15 minutes Sleep as Android then usually gets the night
15–60 minutes later, with the default hourly checks up to an hour more (user
guide, health-connect.md
§8.3, PLAN.md §2).
Downloads
| File | Use |
|---|---|
PulseStages-0.5.0.apk |
Signed release build for everyday use. Later releases install over it as updates. |
PulseStages-0.5.0-debug.apk |
Debug build for testing, with the demo mode. |
SHA256SUMS.txt |
SHA-256 checksums of both APKs: sha256sum -c --ignore-missing SHA256SUMS.txt |
The two APKs are signed with different keys, so neither can update the other. To switch, uninstall first.
The sessions PulseStages wrote stay in Health Connect; grant its permissions again after the reinstall.
To get every later release installed as an update, add PulseStages to Obtainium
(user guide).
Release signing certificate SHA-256: ce5546e83c03eac9837b93fc2c04d2e37abd1b778c3ef9e01b67cd1602e35be5
Debug signing certificate SHA-256: b557d5d55ea3c6d98e72493b1e82f228465201e7271d93f7ace577ff1c5b35cc (a...
PulseStages 0.4.1
"Ask Sleep as Android to sync" now waits until a night is complete before it asks Sleep as Android to import it, so
Sleep as Android no longer keeps a night that OHealth published cut short while you were still asleep. Install it
over 0.4.0 like any update (same signing key): the app keeps its data and settings, and no night becomes stale.
Nights Sleep as Android already holds cut short must be deleted there once (below).
Fixed
- Sleep as Android kept nights that were cut short. OHealth publishes a night in steps: on the development phone it
wrote each of the nights of 17–19 October 2026 first with an end at about 07:29, while the user was still asleep, and
extended the same session to the real wake later, up to about five hours later. Sleep as Android imports a night
once and never updates it, and 0.4.0 asked it to sync right after PulseStages' first write of a night, so it kept
the 07:29 versions (confirmed on the development phone). "Ask Sleep as Android to sync" now sends its request, and
the second one 10 minutes later, only once the recent nights are complete: PulseStages has processed every recent
session of the enabled sources as it is now, the heart rate of each session's own app reaches "Merge gap" + 30
minutes past its end (60 minutes by default) and was uploaded after the session last changed, and this holds for 15
minutes and through one more look for late heart rate, which the next check then makes even within the hour. The
request therefore usually comes 1–2 hours after you wake up instead of minutes after the first write, and a
recompute waits at least 15 minutes; PulseStages still writes every version to Health Connect at once. While a
request waits, the message of a run you started says "Sleep as Android will be asked to sync once the nights are
complete.", and the log says what it waits for ("not asked yet: …"); the setting's hint and the privacy screen say
that the request comes once a night is complete. A second request still pending from 0.4.0 waits for complete
nights as well. The worksleep-as-android-follow-upnow reads Health Connect (one read of the sleep sessions of
the last 30 days and one heart-rate read per source, at most about four times an hour while a request is owed):
without background access the request is sent, without a new check, 15 minutes after a run in the foreground such
as "Estimate new" found the nights complete, and otherwise waits for such a run. Nights that Sleep as Android
already holds cut short stay as they are: delete them there (cloud icon; do not undo the deletion), then tap
"Recompute" on them in PulseStages, or "Sync now" twice in Sleep as Android in the afternoon (confirmed on the
development phone for three nights); do not tap "Sync now" there in the morning. On the development phone,
with test builds of this version, OHealth published the night of 20 October cut at 07:29 and extended it twice;
PulseStages wrote all three versions and asked Sleep as Android nothing while OHealth's heart rate had not passed
the end, and a request after a recompute went out after the wait, followed by the second one 10 minutes later.
Sleep as Android's import of a night after the wait is not yet confirmed
(user guide,
health-connect.md §8.3, PLAN.md §2).
Downloads
| File | Use |
|---|---|
PulseStages-0.4.1.apk |
Signed release build for everyday use. Later releases install over it as updates. |
PulseStages-0.4.1-debug.apk |
Debug build for testing, with the demo mode. |
SHA256SUMS.txt |
SHA-256 checksums of both APKs: sha256sum -c --ignore-missing SHA256SUMS.txt |
The two APKs are signed with different keys, so neither can update the other. To switch, uninstall first.
The sessions PulseStages wrote stay in Health Connect; grant its permissions again after the reinstall.
To get every later release installed as an update, add PulseStages to Obtainium
(user guide).
Release signing certificate SHA-256: ce5546e83c03eac9837b93fc2c04d2e37abd1b778c3ef9e01b67cd1602e35be5
Debug signing certificate SHA-256: b557d5d55ea3c6d98e72493b1e82f228465201e7271d93f7ace577ff1c5b35cc (a later debug APK installs over an earlier one only if this value is the same)
PulseStages 0.4.0
PulseStages can now ask Sleep as Android to import a night it wrote and, 10 minutes later, to add the night's heart
rate, without tapping "Sync now" ("Ask Sleep as Android to sync", on by default; without Sleep as Android nothing
happens, and the request carries no data). New nights can be looked for every 15 or 30 minutes, while the look for
late heart rate stays at most about hourly. Install it over 0.3.0 like any update (same signing key): the app keeps its
data and the check interval you chose, and no night becomes stale
(user guide).
Added
- Ask Sleep as Android to sync (Settings → "Written sessions" → "Ask Sleep as Android to sync",
write.requestSleepAsAndroidSync, on by default; turn it off there). Sleep as Android imports from Health Connect
only when it syncs, and it attaches heart rate and SpO2 only to the nights it had before a sync. With the setting on,
every run that wrote or rewrote a session (the automatic checks, "Estimate new", a backfill, a recompute; also one
that stopped after writing, on a Health Connect error or because WorkManager stopped it) sends Sleep as Android its
public sync request, "Request Backup Sync" (com.urbandroid.sleep.REQUEST_SYNC, restricted to its package, without
extras or data), if Sleep as Android is installed; after a new or recomputed night it sends one more 10 minutes later
(the one-time worksleep-as-android-follow-up, its due time kept in the sync state; a work that runs early waits for
that time), so that the night it imported gets its heart rate and SpO2. A run that only updated nights after late
heart rate gets no second request, and Sleep as Android keeps the stages it imported first: to get an automatic update
there, delete the night in Sleep as Android and recompute it. Confirmed on the development phone: the request runs
Sleep as Android's backup and its other enabled syncs and, 10 seconds later, a Health Connect sync of the last 30 days
(not described in its documentation, so it may change), the second request 10 minutes later does the same, its own
sessions in Health Connect do not grow, and a second sync attaches heart rate to the nights it imported. Observed with
Sleep as Android 20261006 but not yet confirmed on the development phone: its heart-rate awake detection can then add
awake stretches over the stages; repeated syncs probably add further 10-minute heart-rate values (its log shows them
offered again); and it may write the heart rate and SpO2 of its own nights to Health Connect. It
is on by default because without Sleep as Android it does nothing and the request carries no data; with Sleep as
Android installed, every write therefore also brings its backup, its other syncs and these heart-rate effects, so turn
the setting off if you do not want them. Not part of the params hash. The manifest now queriescom.urbandroid.sleep,
only to check that it is installed; the message of a run you started adds "Sleep as Android was asked to sync.", and
the privacy screen has a point about the request. English and Russian texts
(user guide,
health-connect.md §8.3,
privacy.md §2, PLAN.md §2).
Changed
- Checks every 15 or 30 minutes. Settings → "Schedule" → "Check for new nights every" is now a list: "15 min",
"30 min" or 1 to 24 hours (1 h by default); 15 minutes is WorkManager's minimum period. Health Connect cannot notify
an app of new data, so a shorter interval brings new nights sooner, and a check that finds nothing costs one
change-log read (health-connect.md §7.2). The setting's key is now
schedule.periodicMinutesinstead ofschedule.periodicHours: stored settings and exports of 0.3.0 and older keep
loading, their hours converted to minutes (also a quoted number;schedule.periodicMinuteswins when a file has
both), and exports contain onlyschedule.periodicMinutes, so 0.3.0 reads the interval of an export of this version
as 1 h. The periodic work is enqueued again once after the update (its schedule tag is now
schedule:periodic:<minutes>min:…), and its log line says "periodic check every … min". Home's "New nights" no
longer says "every hour" (settings reference). - Recent nights are re-checked at most about once an hour. The retry of recent nights without an estimate and the
refresh of written nights after late heart rate form one late-data pass, which runs at most every 55 minutes
(NewNightsSync.MIN_RECHECK_GAP_MS), measured from the start of the run whose pass last completed (kept in the sync
state,late_data_pass_at). So it stays about hourly with a 15- or 30-minute check, also for a night whose check
records nothing, and a backlog of more than 8 due nights is worked off one pass at a time; late arrivals keep their
schedule of about 1, 2, 4, 8, 16 and 32 hours. A pass cut short by a quota error or a refused access is repeated by
the next run. "Estimate new" runs the pass at once, and the next automatic pass waits 55 minutes from it. With the
default of 1 h little changes: only a run less than 55 minutes after the last pass, such as the daily run shortly
after a periodic one, now skips it (user guide,
health-connect.md §5). - The retry skips nights of disabled sources. A night without an estimate whose app was disabled on the Sources
screen is never found by the read of its time window, so it was retried, and cost a sleep-session read, on every
run; it is no longer retried (architecture.md §6.7).
Fixed
- Sleep as Android's read window in the documentation. The syncs it starts itself, after saving a record and when
its tracking ends, read Health Connect for the last 30 days like "Sync now", not 14 days as the 0.3.0 notes said
(observed with Sleep as Android 20261006, not yet confirmed on the development phone): they send the same sync request that "Ask Sleep as
Android to sync" sends (health-connect.md §8.3,
user guide).
Downloads
| File | Use |
|---|---|
PulseStages-0.4.0.apk |
Signed release build for everyday use. Later releases install over it as updates. |
PulseStages-0.4.0-debug.apk |
Debug build for testing, with the demo mode. |
SHA256SUMS.txt |
SHA-256 checksums of both APKs: sha256sum -c --ignore-missing SHA256SUMS.txt |
The two APKs are signed with different keys, so neither can update the other. To switch, uninstall first.
The sessions PulseStages wrote stay in Health Connect; grant its permissions again after the reinstall.
To get every later release installed as an update, add PulseStages to Obtainium
(user guide).
Release signing certificate SHA-256: ce5546e83c03eac9837b93fc2c04d2e37abd1b778c3ef9e01b67cd1602e35be5
Debug signing certificate SHA-256: b557d5d55ea3c6d98e72493b1e82f228465201e7271d93f7ace577ff1c5b35cc (a later debug APK installs over an earlier one only if this value is the same)
PulseStages 0.3.0
Sleep as Android can now show PulseStages' stages: the opt-in setting "Sleep as Android compatibility" starts every
written session one second before the watch app's, so Sleep as Android imports PulseStages' session instead of the
watch app's session without stages. Install it over 0.2.0 like any update (same signing key): the app keeps its data,
and nothing becomes stale until the setting is turned on
(user guide).
Added
- Sleep as Android compatibility (Settings → "Written sessions" → "Sleep as Android compatibility",
write.startBeforeSource, off by default). Sleep as Android imports only the first of overlapping Health Connect
sessions by start time. PulseStages' session started at exactly the source session's start and always lost that tie to
the older source session, so Sleep as Android showed OHealth's nights without stages. With the setting on, every
written session, naps included, starts 1 second before its source session and its first stage is extended back by that
second; the end, zone offsets, record key, title and the notes' keys are unchanged (the params hash in the notes
changes), and the app's screens keep the night's own times. Sleep as Android then imports PulseStages' sessions with
their stages: confirmed on the development phone with Sleep as Android 20261006, where the nights (deleted there
before the sync) showed a full hypnogram with the same phase shares as in PulseStages. It shows whole minutes, so
when the watch app's session starts on a whole minute, the night shows there one minute earlier (3:03 instead of
3:04 for a session starting at 3:03:59). The setting changes the written data and is part of the params hash only
while it is on: the update changes no hash, and turning it on (or off again) makes every written night stale;
"Recompute" rewrites them in place (same record key, next version), never under "Never overwrite". Nights Sleep as
Android already imported stay as they are until they are deleted there and synced again (Sleep as Android →
Settings → Services → "Sync now"; nights of the last 29 days only). English and Russian texts
(user guide,
health-connect.md §8.3, PLAN.md §2). - On-device suite: the shadow pipeline's
policy-start-before-sourcescenario checks that every captured session
starts exactly one second before its night, and the real-write sequence's second version moves the record's start by
that second, so an in-place upsert with another start is checked on the phone
(tools/device-test/README.md).
Changed
- Sleep as Android in the documentation: its import is no longer "not verified". Observed with Sleep as Android
20261006 and confirmed on the development phone, it syncs only when you tap "Sync now" in its Settings → Services
(Health Connect for the last 30 days) or the sync button of its Health Connect screen (up to 5 years back), when it
saves a record and when its own tracking ends (both the last 14 days); it keeps the first of overlapping sessions by
start time, whatever Health Connect's priority list says; it never imports a session that overlaps a record it
already has and never picks up later changes; it drops Health Connect's "unknown" and "sleeping" stages, so a nap
written as asleep / awake, once imported, shows there as a time span without phases (only its awake parts, if any);
and the thin stripes on nights without stages are its own heart-rate awake detection. The user guide's setup
replaces "delete the stage-less session" and "Write awake as light sleep" with the compatibility setting, a
recompute, deleting the imported nights of the last 29 days (cloud icon) in Sleep as Android and syncing again with
"Sync now", and warns against tapping its "Health Connect" row in Settings → Services (a switch: it turns the
integration off and Sleep as Android revokes its Health Connect permissions) and against its Backup → "Download from
Health Connect" (user guide,
health-connect.md §8.3).
Downloads
| File | Use |
|---|---|
PulseStages-0.3.0.apk |
Signed release build for everyday use. Later releases install over it as updates. |
PulseStages-0.3.0-debug.apk |
Debug build for testing, with the demo mode. |
SHA256SUMS.txt |
SHA-256 checksums of both APKs: sha256sum -c --ignore-missing SHA256SUMS.txt |
The two APKs are signed with different keys, so neither can update the other. To switch, uninstall first.
The sessions PulseStages wrote stay in Health Connect; grant its permissions again after the reinstall.
To get every later release installed as an update, add PulseStages to Obtainium
(user guide).
Release signing certificate SHA-256: ce5546e83c03eac9837b93fc2c04d2e37abd1b778c3ef9e01b67cd1602e35be5
Debug signing certificate SHA-256: b557d5d55ea3c6d98e72493b1e82f228465201e7271d93f7ace577ff1c5b35cc (a later debug APK installs over an earlier one only if this value is the same)
PulseStages 0.2.0
Nights and naps are updated automatically when their heart rate reaches Health Connect after they were written, so a
night estimated from part of its heart rate no longer stays so until it is recomputed by hand. Install it over 0.1.x
like any update (same signing key): the app keeps its data, its ledger is migrated on the first start, and the
sessions already in Health Connect stay; sessions written by 0.1.x are compared by their heart-rate sample count on
their first check (user guide).
Added
- Install and update with Obtainium: a "Get it on Obtainium" button in the README and the user guide adds
PulseStages to Obtainium with one tap and a confirmation; Obtainium then
installs it and every later release as an update. The import takes final releases only, selects
PulseStages-<version>.apkwith the APK filter^PulseStages-[0-9.]+\.apk$(never the-debugAPK) and expects
the release signing certificate. The app itself still has no update check and no internet access
(user guide).
The release notes link to that section. - Recent nights and naps are updated automatically when more heart rate arrives. Heart rate often reaches Health
Connect hours after the sleep session, and a night written from part of it (for example with the rules estimator at
40–60 % coverage) stayed so until it was recomputed by hand. Every sync now also reads the heart rate, steps and
optional channels of recent written nights and naps again and compares them with a fingerprint stored at the write:
on the schedule of the retry of nights without an estimate (every run for 36 hours after the session ended, growing
gaps for late arrivals), at most 8 per run. When the input changed, the night is estimated again, and its session is
rewritten only when it would change (stages, title or notes), under the same record key with the next version. Never
under "Never overwrite"; stale nights are left to "Recompute stale nights"; a session deleted outside the app is not
written again; fewer heart-rate samples than at the write never make a session worse. The night detail shows
"Updated automatically" with the reason (for example "more heart rate arrived (46 → 57 samples)"), "Estimate new"
adds "… nights updated with late data.", and the log has a line per update
(user guide,
architecture.md §3.6, PLAN.md §2). - Ledger schema 3:
estimated_nights.inputFingerprintandinputCheckedAt, with the migration 2 → 3 and the
exported schema3.json. The fingerprint holds per channel the number of samples and a SHA-256 digest of them, never
the samples; rows written before have none and are compared by their heart-rate sample count and coverage until
their first check (privacy.md,
architecture.md §4.2). - On-device suite: the shadow pipeline's
refresh-recentscenario runs the refresh over the real recent nights
with nothing written to Health Connect (tools/device-test/README.md).
Downloads
| File | Use |
|---|---|
PulseStages-0.2.0.apk |
Signed release build for everyday use. Later releases install over it as updates. |
PulseStages-0.2.0-debug.apk |
Debug build for testing, with the demo mode. |
SHA256SUMS.txt |
SHA-256 checksums of both APKs: sha256sum -c --ignore-missing SHA256SUMS.txt |
The two APKs are signed with different keys, so neither can update the other. To switch, uninstall first.
The sessions PulseStages wrote stay in Health Connect; grant its permissions again after the reinstall.
To get every later release installed as an update, add PulseStages to Obtainium
(user guide).
Release signing certificate SHA-256: ce5546e83c03eac9837b93fc2c04d2e37abd1b778c3ef9e01b67cd1602e35be5
Debug signing certificate SHA-256: b557d5d55ea3c6d98e72493b1e82f228465201e7271d93f7ace577ff1c5b35cc (a later debug APK installs over an earlier one only if this value is the same)
PulseStages 0.1.1
Naps are written to Health Connect as asleep / awake by default. A pre-registered experiment tried to make the
stage breakdown of naps realistic and found no nap stage model that passed its checks (per-epoch discrimination,
following the heart rate across sleep regimes, realism against published compositions of afternoon naps), so the
nap model stays and its stages, still shown in the app, are not validated. Nights are unchanged. Details in
ALGORITHM.md §13.13
and the user guide.
Added
- On-device end-to-end suite (
app/src/androidTest,tools/device-test/run-device-tests.sh). Runs against the
debug build installed on a phone and its real Health Connect data without reinstalling the app: snapshots of
everything the suite could touch before and after the run (the app's own records must stay byte-identical, other
apps' records unchanged); the backfill, sync and recompute pipeline over the real history with every write captured
in memory, with invariant, idempotency, recompute and policy checks and a per-session report; one test record in the
ps-e2e:namespace that is upserted, versioned and deleted within a minute; a read-only tour of every screen with
screenshots, whose clicks an allow-list restricts to what writes nothing; the app's job schedule before and after. A
safety guard refuses every write or delete of the suite outsideps-e2e:, an in-memory WorkManager keeps the app's
own background work out of the test processes, the read budget keeps the suite within Health Connect's background
quota across runs, and a cleanup step that always runs removes every test record (first by the ids recorded before
the write, without a read) and puts back the demo-mode flag
(tools/device-test/README.md). - Context margin of naps (Settings → "Naps: algorithm" → "Heart rate around naps" → "Context margin of naps",
algo.napMode.contextMarginMin): how many minutes of heart rate and steps (and of the optional channels) before and
after a nap its estimate uses. Empty, the default, is the night's "Context margin" (30 min), as before; 0 is the nap
itself only; at most the night's value, which sets how much is read from Health Connect. Nap-only: nights never read
it. Checked by the core-algo and settings validators and by the import; English and Russian texts
(settings reference). - Nap stage bias per session length in the model bundle: an optional
nap.stageLogBiasByLengthtable (lengths
and one log-bias row per length, linear interpolation in the session length, the first or last row outside;
exclusive with the nap section'sstageLogBias). It is left out of the canonical JSON when absent, so existing
bundles keep their fingerprints and personal models keyed to them stay valid. The shipped bundle does not use it
(docs/INTERCHANGE.md). - Realistic-nap experiment (pre-registered:
docs/prereg/prereg-naps-realistic.md
with its Amendment 1, digest and command log;
ALGORITHM.md §13.13):
train.py naps-realistic-gates,naps-realistic-timing,naps-realistic,naps-realistic-report,
naps-realistic-final,naps-realistic-external,naps-realistic-installandnaps-realistic-verify, with
pulsetrain/napsrealistic.py,pools.py(training tiles, evaluation pools, featureless sessions),litprior.py(the
published nap compositions: arm table, inclusion rule, targets, bands and guards),realism.py(per-epoch AUC, regime
contrasts, realism metrics, the decision rules) andperturb.py; sample weights inmodels.fit_classifier, a
per-length bias fit (bias.fit_stage_bias_table);algo-cli evaluateshares a preparation only between grid points
with the same mode and effective preprocessing, so a grid over the nap context margin prepares only the naps again
(tools/train/README.md). - Test-only pipeline for OpenNeuro ds006576 (real afternoon naps with ECG and EEG, CC0):
tools/train/external.py
(snapshot pin against the committed key listexternal-ds006576-v1.0.5.keys.tsv, a streamed fetch that keeps only
reduced signals, ECG → heart rate, test windows, night files, the digests that freeze the test set) and
tools/train/external_stage.py(YASA 0.7.0 sleep staging, which only places the test windows, in its own environment
tools/train/.venv-externalpinned with hashes byrequirements-external.lock, OpenMP runtime included). The data
are never used for training, for any parameter or for choosing between models, and are never committed
(tools/train/DATASETS.md). - Tests: the nap input restriction and preparation key, the length-bias table and its validation, the shipped bundle's
fingerprints pinned, and a personalized-nights test showing that personal bias deltas still apply after a bundle
update that keeps the night model; the training tests run on synthetic data only.
Changed
-
Naps are written to Health Connect as asleep / awake by default. "Written for a nap" (
algo.napMode.outputPolicy)
now defaults to "Asleep / awake" (SLEEP_WAKE) instead of "Stages", because no nap stage model passed the
pre-registered realistic-nap experiment
(ALGORITHM.md §13.13).
Rule (i) adopted no candidate in either tier, so the shipped nap model (N1) and the bundle stay unchanged. Rule (ii)
gives N1 the asleep / awake output: it fails the evidence check F and the realism check R, and on the
literature-reweighted pool its accuracy is 0.055 below calling every epoch light sleep (at least +0.03 was required
for stages; 4-class κ 0.246), while its sleep / wake κ is 0.269 (at least 0.20 required). Failing criteria (Walch,
held-out subjects, worse sampling variant):- N1: F, REM contrast r = 0.012 (the predicted late-night minus sleep-onset difference of the REM share of sleep, as a
fraction of the true difference; ≥ 0.25 required), the lower bound of its 95 % bootstrap interval −3.1 pp and its
BIDSleep difference −1.8 pp (> 0 required); R, featureless sessions of 90–135 min with 3.5–5.2 % REM of sleep, below
the lower bound of the published range (5.1 % at 90–105 min, 5.7 % at 120–135 min), and a late-night deep-sleep tail
of 0.226 (the share of 60-min windows from late in the night decoded as at least
half deep sleep, minus the true share; ≤ 0.10). - C0 (N1's classifier with a literature-anchored level per length): F, REM r = 0.122, lower bound −3.4 pp, BIDSleep
−1.6 pp; R, featureless REM above the range at 30–75 min and deep sleep below it at 105 and 135–170 min (mean2min),
late-night deep-sleep tail 0.104. - C1 (a heart-rate-only evidence model with a night-level bias): D, sleep-onset deep-sleep AUC 0.066 below N1's (at
most 0.05 allowed); R, featureless REM above and deep sleep below the range, guard excess 0.170 (implausible
single-nap compositions beyond those of the true compositions; ≤ 0.05), mid-anchor excess 2.46 pp (distance from the
published composition between the anchor lengths minus at them; ≤ 2.0), distance from the published composition
14.31 pp, above N1's 12.66. - C2 (the evidence model with a literature-anchored level per length): D, sleep-onset deep-sleep AUC 0.057 below
N1's; F, REM r = 0.070, lower bound −6.3 pp; R, featureless deep sleep below the range at 75–170 min, late-night
deep-sleep tail 0.132, guard excess 0.104, mid-anchor excess 2.10 pp.
The test on real afternoon naps (OpenNeuro ds006576) changed nothing: N1 stays in effect with the asleep / awake
output. The app still estimates the stages of a nap and shows them as its own estimate; they are not validated, and
"Stages" writes them anyway. Settings store only changed values, so the new default applies wherever the setting was
left at "Stages" (an explicit "Stages" of 0.1.0 included, since it was the default). Nap sessions already in Health
Connect keep their stages until they are recomputed: the new default and the new setting change the params hash, so
every estimate is offered for recomputation, and nights decode exactly as before (568 of 568 validation nights, per
epoch) (user guide). - N1: F, REM contrast r = 0.012 (the predicted late-night minus sleep-onset difference of the REM share of sleep, as a
-
Nap caution in the night detail: an estimated nap now says "Nap stages are not validated: the nap model failed
pre-registered checks, and on test sessions with the stage mix of published naps it was less accurate than calling
everything light sleep." (other estimators: "Nap stages are not validated; take them as the app's estimate only.")
instead of "Nap estimates are rough: … about 6 in 10 …", and the hint of "Written for a nap" says that nap stages are
not validated instead of "rough"; English and Russian. -
Documentation of naps: the README, the user guide and PLAN.md describe the asleep / awake default and say that the
nap stages shown in the app are not validated; the earlier figure of about 6 in 10 nap epochs right
([ALGORITHM.md §13.12](https://github.com/alien2003/PulseStages/blob/v0.1.1/ALGORITHM.md#1312-a-dedi...
PulseStages 0.1.0
First release. PulseStages estimates sleep stages (awake, light, deep, REM) from the heart rate that OHealth (OPPO
Watch) writes to Health Connect, and writes them back to Health Connect as a separate sleep session marked as an
estimate. The stages are estimated, not measured: on the open validation data, agreement with the sleep lab is fair
for nights (Cohen's κ about 0.32–0.34) and rough for naps (about 6 in 10 epochs right); see
ALGORITHM.md §13 and the
limitations. Start with the
user guide.
Added
App
- Screens. Compose Material 3 UI with four tabs: Home, Nights, Compare and Settings; Sources and Log open from
Settings. Dynamic colour on Android 12+, light and dark theme, edge-to-edge. On wide screens (the unfolded
foldable), Nights shows the list and the detail side by side and Settings uses two columns. The UI is in English and
Russian; choices whose labels do not fit side by side are shown as radio buttons. - Home: Health Connect status (with install / update on Android 9–13); required and optional permissions, with
separate indicators for history access, background access and notifications; enabled sources, last and next run;
counters of found, estimated, skipped, stale and failed sessions, and of nights and naps written. Buttons "Estimate
new", "Backfill history" (progress, cancel, resume), "Recompute all", "Recompute stale nights" and "Delete my
sessions from Health Connect". - Nights: the list shows duration, stage shares, confidence and coverage, with "Stale", "Elevated heart rate",
"Low confidence" and "Nap" markers, and a filter All / Nights / Naps. The night detail shows a hypnogram, the
heart-rate curve, per-epoch probabilities and features, the warnings, and whether the session was processed as a
night or a nap. Actions: "Recompute", "Compare with RULES", "Export CSV" and "Delete my session". - Compare scores the estimates against sessions with real stages from other apps: kappa, accuracy, macro F1,
per-stage metrics, a confusion matrix, per-night scores, and nights and naps apart. "Calibrate" fits a personal model
on these nights (kappa before and after on held-out nights); "Reset to global model" removes it. - Settings has every parameter in groups, each with a hint, JSON export and import with validation, reset to
defaults, reset of the HMM emissions to the physiology table, and an offer to recompute the sessions a change made
stale. The last card, "Versions and licences", shows the app, algorithm and model versions, the licence and the
ODC-By attribution notice of the training datasets. - Sources lists the apps that wrote sleep sessions to Health Connect (icon, name, session count, last session),
with an enable switch and a priority order for each; packages can be added by name, and a switch takes heart rate
and steps from any app. - Log with a filter by level and sharing as a text file.
- Privacy: a permission rationale and privacy screen, reachable from Home and from Health Connect
(ACTION_SHOW_PERMISSIONS_RATIONALE, and on Android 14+ theVIEW_PERMISSION_USAGEactivity-alias). The app has no
INTERNETpermission. - Icon: adaptive vector launcher icon with a monochrome layer, and
docs/icon.svg. - Demo mode (debug builds only): Settings → "Debug build" → "Demo mode" switches the Nights list, the night detail
and Compare to synthetic nights and naps, for screenshots. The release build contains none of its code or text.
Nights and naps
- Every session is processed as a night or a nap. Settings → Naps → "Nap threshold" (180 min; 0 = no
naps) decides which; sessions shorter than the source setting "Minimum session length" (90 min) are ignored. - "Written for a nap": Stages (the default), Asleep / awake (only AWAKE and SLEEPING are written to Health
Connect) or Nothing (not estimated and not written; "Recompute stale nights" writes them after switching back).
"Estimator for naps" defaults to "Same as nights". The nap values of the algorithm are in the collapsed group
"Naps: algorithm" (empty = the value of nights). - Naps never enter the elevated-heart-rate baseline and are never compared with it. The stage-share sanity check and
the proportion calibration skip naps unless "Check the stage shares of naps" is on. Calibration on reference
nights and the population EM use nights only. - The bundled model has a dedicated nap model, chosen by a pre-registered leave-one-subject-out test on nap-length
pieces of the validation nights
(ALGORITHM.md §13.12).
The night detail of a written nap says that its stages are rough.
Health Connect and storage
- Repository over connect-client 1.1.0: paginated reads; upsert by
clientRecordId, verified by reading the
record back, with delete-and-insert as the fallback; deletes of the app's own records only; Changes API; scan of
sleep origins; error mapping with backoff on rate limits and transient errors. - Session pipeline:
- source allowlist (default OHealth); the app's own sessions and sessions that already have stages are skipped;
- fragments closer than "Merge gap" (30 min) are merged, with the gaps written as AWAKE;
- priority between sources, also against a higher-priority source that recorded the night with its own stages; a
session that lies mostly inside a longer session of another app and covers at most half of it is a piece of that
night and does not replace it; - deterministic
clientRecordId(ps:+ SHA-256 of the sorted source uuids) andclientRecordVersion=
ALGO_VERSION · 1000 + recompute count; - replaced outputs are superseded, deleted sources are handled, records left from before a reinstall are
de-duplicated, and a failure on one session does not stop the others; - overwrite policies "Skip up-to-date nights", "Recompute everything" and "Never overwrite";
- the notes of every written session are JSON: estimator, mode (
NIGHT/NAP), algorithm version, params hash,
coverage, confidence, source session uuids, heart-rate sample count and whether the stage proportions were
calibrated; the CSV export has a last columnmode.
- Sync and backfill: Changes-API sync that falls back to re-reading the last 7 days; history backfill in 7-day
windows that resumes after a restart; a guard that refuses background reads without
READ_HEALTH_DATA_IN_BACKGROUND; deletion of all PulseStages sessions or of one session. Deleting cancels a running
backfill and recompute, and neither writes a deleted session again. - Room ledger
estimated_nights,night_sources,source_sessions,night_baselines,log_entries,
backfill_state,personal_models(schema version 2, with exported schemas). - DataStore settings (JSON export, import and reset; the file stores only changed values, so trained defaults
follow app updates) and the changes token. - Internal log, mirrored to logcat with the tag
PulseStages. - Data of development builds made before 0.1.0: the ledger migrates from schema version 1 and keeps every row, and a settings file
exported by such a build loads with the retired keysfeatures.baselineMinSessionMin,hybrid.shortSessionMaxMin
andpost.proportionsMinSessionMinignored (replaced bynapMode.maxSessionMin).
Background work (:work)
EstimateNewNightsWorker: an hourly Changes-API check, a daily run at a configurable time (default 09:00), and
"Estimate new" as expedited work.BackfillWorkerwith progress, cancel and resume, andRecomputeWorker.- The hourly and daily works wait for a battery that is not low (configurable); all works use exponential backoff,
and the runs the user starts show adataSyncforeground notification.
Algorithm (:core-algo, :algo-cli)
- Estimators behind
StageEstimator,ALGO_VERSION1: RULES (thresholds); HMM (Viterbi and forward-backward,
time-of-night priors, REM lockout, ultradian-cycle prior, per-night EM); HYBRID, the default (MLP emissions on
multi-scale features, decoded by an explicit-duration HSMM or a geometric HMM). - Bundled model
pulsestages-walch+bidsleep-mlp-nap-model-finalinapp/src/main/assets/models/hybrid_model.json:
the night model with a per-stage log-bias that matches the stage shares to the sleep lab (pre-registered,
ALGORITHM.md §13.10),
and the nap model above. - Processing: preprocessing (range filter, Hampel filter, bounded interpolation, smoothing, coverage), shared
post-processing, confidence and an elevated-heart-rate baseline; personalization by calibration on reference nights
and population EM. - Tooling: metrics (kappa, accuracy, F1, confusion matrix, minute errors), a synthetic night generator, the params
hash, CSV export, andalgo-cliwithfeatures,evaluate(metrics by mode, nap proxies, fold bundles with nap
sections) andestimate.
Training and validation (tools/train)
- Licence-checked download of PhysioNet sleep-accel 1.0.0 and BIDSleep 1.0.1 (both ODC-By 1.0). DREAMT is not used,
because its access is restricted. - OHealth emulation: one heart-rate value per 2 minutes (instantaneous and 2-minute mean), steps from the
accelerometer, 4-class labels. - Leave-one-subject-out validation with nested selection, computed through
algo-cli; the generated
TrainedDefaults.kt; the pre-registrations of the stage-proportion, nap and nap-model expe...