Skip to content

Strap features, Charge and readiness rework, GPS and workout effect - #9

Merged
sayed0am merged 31 commits into
mainfrom
workouts
Oct 23, 2026
Merged

sayed0am merged 31 commits into
mainfrom
workouts

Conversation

@sayed0am

@sayed0am sayed0am commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Follows #8. 31 commits, grouped below. Each commit passes the unit tests on its own. A final independent review's findings are fixed in three commits, and the last one adds a wear poll found needed on hardware.

Workouts and GPS

  • GPS workouts start on fused location (network assisted), falling back to plain GPS after 20 s like Zepp; satellite assist data and time are fetched at start; mock locations are ignored. On a phone, the first fix came in about 5.5 s at a window.
  • Routes are smoothed (Kalman + RTS) instead of thinned. GPX export of the smoothed track is a noted TODO.
  • Phone workouts get their own load (EPOC), aerobic training effect, recovery time and, for outdoor runs with GPS, VO2max. These are open methods on Zepp's scales; no Zepp model is used.
  • Strap workouts keep their subtype; ones the strap auto-detected get a label and a "Walk detected, 10:02–10:40" notification that opens the workout.

Strap

  • Find strap and Restart on the strap page (Restart only when the strap reports key 45).
  • Off-body reminder: turns on the strap's wear reminder and follows its wear status (key 16). On firmware 3.25.0.3 the strap never pushed a change, so the app re-reads it every minute while connected. The events are stored (database 16) and Charge uses recorded off-body time as not worn.
  • Reads the runtime feature bitmap (0x0028), firmware check sends Zepp's version parameters, settings pushes, real resource versions, cloud token refresh.
  • Deferred, noted in code: cloud first bind without the Zepp app; strap-side phone workouts over 0x0019 + 0x0031.

Scores

  • PAI drop-off line comes from the strap's own projection.
  • Readiness: Zepp's bands for the strap's score, soft-minimum fusion, alert cap 54 (below Fair).
  • Sleep: the headline names what pulled the night down; regularity shows the SRI.
  • Daily load counts movement (35 % of heart-rate reserve, or 20 % while walking 60+ steps a minute) and splits workouts from the rest of the day.
  • Stress is ignored during workouts and for 20 minutes after.
  • Charge: no drain at rest, still heart rate isn't effort, illness slows recharge; handles the strap being off (Zepp's no-wear drift, a gap past 384 min), unrecorded cardio and calm hours.

Data

  • Database 15 → 17 (wear events, workout subtype) with AutoMigrations and migration tests.
  • CSV backup carries the new columns and tables.

Testing

  • ./gradlew :protocol:test :app:testDebugUnitTest :app:assembleDebug passes (app 319, protocol 132).
  • On hardware, 2026-10-23, firmware 3.25.0.3:
    • Restart: passes. The strap reports key 45, dropped the link 8 s after the command and was reconnected and syncing 6 s later.
    • Off-body reminder: passes with the wear poll. No push came when the strap was taken off or put back on, though a fresh read had it right. With the poll, each change showed within a minute and the notification came up and cleared.
    • Find strap: passes for buzz, Stop and the 60 s timeout.
  • Not yet on hardware: Find strap while a call rings, the auto-detect notification tap, Charge with real wear events, and GPS smoothing with the workout effect on an outdoor run.
  • Lint: 11 errors, all in code already on main before this branch.

🤖 Generated with Claude Code

sayed0am and others added 30 commits October 22, 2026 18:05
When a GPS workout starts, the phone's GPS is asked to download satellite
orbit predictions (XTRA) and the network time, as Zepp does, so the first
good fix comes sooner. Both come from the chip vendor's servers in the
phone's GNSS config, not Google services. A phone that ignores the
commands just settles at its usual speed.

Needs ACCESS_LOCATION_EXTRA_COMMANDS, granted at install with no prompt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The note under the PAI panel used the oldest day's stored daily value.
On the strap's saturating PAI curve with its 75-a-day cap, that isn't
what dropping the day removes: a capped day showing 75 may only take
56 with it. The app now rebuilds the seven daily slots from the stored
totals and re-sums the newest six, as the strap does, picking the curve
by the profile's sex (female when unset, like Zepp).

Exact for uncapped days; a day not synced at its end reads a little low.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes from a mock-location app are dropped before they reach the route,
as Zepp does, and the number dropped is logged when the workout stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hasNewVersion now carries firmwareFlag, the font, resource, base and
extended resource version/flag pairs and productionSource, in Zepp's
order. OpenStrap doesn't read the strap's resource versions yet, so each
holds Zepp's own default for "not reported". gpsVersion is left out
while unknown, as Zepp leaves it unset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After auth the app asks endpoint 0x0028 for the feature bitmap, keeps it
for the session, follows the strap's change pushes, and logs the raw
reply once so a hardware run can check the parser.

The bitmap sets the activity record size when the START size field
can't, and stands in for the history type list when the strap sends
none. An advertised type is still fetched when its bit reads clear: the
reply has never been captured, and a misread bitmap mustn't stop sleep
or HRV sync. Those disagreements are logged instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A GPS workout now asks Android's fused provider for high-accuracy
updates first. Fused seeds the GPS chip with a Wi-Fi and cell position,
so the first fix comes sooner; that matters on Samsung phones, which
ignore the assist-data commands from apps they haven't signed. If fused
gives nothing within 20 s, or the phone has no fused provider, the
workout switches to the plain GPS provider, as Zepp does.

On most phones fused is Google Play services. Coarse network fixes still
never reach the route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each fix now keeps its speed, bearing and altitude (and their
accuracies), stored in five new nullable route columns (database 13,
auto-migrated; CSV export carries them too).

A route is cleaned as before (settle, 30 m accuracy cut, 15 m/s jumps),
then run through a constant-velocity Kalman filter with a backward RTS
pass in local metres. Each fix's accuracy sets its weight, and Doppler
speed and bearing count as a velocity measurement when they're good.
Standing still is found from speed over a 20 s window, and a still
stretch collapses to one point. Skipping points that were within their
error no longer cuts corners: the 22 October test walk measures 185.6 m
(was 176.0; Zepp's live filter gives 180.0).

Live distance runs the same smoother over the route so far. GPX export
still writes the cleaned fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A workout recorded on the phone now gets the figures strap workouts
already had:
- Load: peak EPOC, built per 5 s epoch from heart-rate reserve, after
  the structure of Firstbeat's published EPOC model. Its two
  coefficients are a first fit, not yet calibrated.
- Aerobic training effect 0-5 from EPOC, using Zepp's breakpoints by
  activity class (class from the last 28 days' workout EPOC).
- Recovery time from training effect, longer when fatigue exceeds
  fitness, carrying over what earlier workouts still owe, capped at 72 h.
  Zepp itself always reports 0.
- VO2max for outdoor GPS runs only, from steady level stretches with the
  ACSM running equation, gated like Zepp (10 min qualifying, 2 min
  warm-up); none when the data is noisy.
Anaerobic training effect stays empty: heart rate lags short efforts
too much for an honest model.

Figures are worked out when a workout ends, after a gap fill, and for
older workouts when they're opened. Four nullable columns (database 14,
auto-migrated). Method in docs/algorithms/calculations.md §13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The strap's PAI record carries its projected total for each coming day
with no new activity. The next day's value is now stored (database 15,
auto-migrated, exported as tomorrow_total), so "drops off tomorrow" is
exactly total minus that. Older records without it fall back to
re-summing the ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Settings (0x000A): the strap's unsolicited 04 push now updates the
  keys it carries, the alarms screen included, and query replies must
  match the group asked for.
- After auth the app reads settings group 0 (device information) once
  and logs it. The firmware check sends the strap's real firmware,
  font, resource and extended-resource version/flag pairs; base
  resource stays -1/-1 as Zepp sends it.
- Cloud sign-in keeps login_token and its TTL when the cookie has them,
  and refreshes the app token with them (A3) when the server rejects it
  or half the TTL has passed. appid and request headers are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The strap's sleep score is rated in Zepp's bands (90 Optimal, 80 Good,
60 Fair, below Attention) instead of OpenStrap's 85/70/60. The headline
now says why: "Short night" under 6 h asleep, "Broken sleep" for 30+
min awake or 6+ wake-ups, "Light sleep" (weakest, below 80 only) when
deep + REM is under 25 %, otherwise Great/Good/Okay night by score. A
calm short night no longer reads "Restless night". Total sleep also
gets Zepp's oversleep bands (Fair over 9 h 30, Attention over 10 h).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Alert cap is now one below the Fair band (54, from FAIR_FROM =
  55), so an alert day always rates in the lowest band; the bands and
  the cap share one constant.
- The strap's own readiness score and its parts are labelled with
  Zepp's three bands (85 Optimal, 70 Good, else Pay attention), e.g.
  "HRV 89 (Optimal)". OpenStrap's own score keeps its four bands.
- Zepp's soft-minimum fusion, weighted by OpenStrap's importance
  weights, is computed alongside the current score but not shown
  (SOFT_MIN_FUSION is off). The backtest writes both next to the
  strap's score, for the review in readiness-calibration.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Daily load (TRIMP) now counts:
- every active minute inside a stored workout, phone or strap, at any
  intensity and once where they overlap; paused time doesn't count;
- outside workouts, a minute with a step at HRR 0.35 or more, or with
  60+ steps from HRR 0.20, so real walking still counts (it sits at
  HRR 0.24-0.34 at about 100 steps a minute). Raised heart rate while
  sitting no longer counts as training.

The Load panel shows the day as workouts plus everyday. TRIMP is the
only daily load; a workout's EPOC is now labelled EPOC, not Load.

On the six days in the test copy daily load drops, mostly where it came
from sitting: 15.4 -> 6.2 on a walking day, 13.5 -> 0.4 on a desk day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The strap's stress comes from heart-rate variability at rest, which
means nothing while exercising. Each workout's active time (phone,
minus pauses, or strap) plus 20 minutes after it, the half-life of
Charge's post-exercise decay, is now left out of:
- Charge: those minutes are never Stress or Relax; inside a workout
  they're Activity, after it ExerciseRecovery, or Rest when heart rate
  is under 10 % of reserve. Hard minutes are still Exercise.
- the calm readings behind the personal stress threshold;
- the day's stress tile, chart, average, lowest and highest.
Raw readings are still stored and exported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Regularity" on the Sleep screen is now the Sleep Regularity Index
(Phillips et al. 2017): how likely you are to be in the same state,
asleep or awake, 24 h apart, on its standard scale where 100 is
identical days and 0 is random. It replaces the score from the spread
of bed and wake times, which isn't an established measure. Bands
follow the UK Biobank quintiles in Windred et al. 2024 (below 72,
72-79, 79-87, 87 and up) and are descriptive only. Under 120 h of valid
day pairs it says "Not enough nights yet".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s it

Three ideas from Zepp's BioCharge:
- Calm, still minutes only recharge. Plain rest drains in proportion
  to charge (0.0005 x charge per minute: 0.04 at 80 as before, 0.025 at
  50), so a desk day still falls but more slowly as it goes.
- Outside workouts and the 20 minutes after, raised heart rate counts
  as activity or exercise only with steps that minute; sitting with a
  high heart rate is stress or rest. Workout minutes are unchanged.
- On a night body signals reach Alert, recharge is halved and every
  drain is 1.5x until the next sleep. Watch has no effect.

On the test copy Charge sits higher through the day: lows move from
26-42 to 42-50, and a no-sleep day's low from 6 to 28.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Not worn: a minute without heart rate outside sleep is not worn when
  the strap reports off-wrist or it sits in a 10+ minute gap. Short
  stretches drift at Zepp's default no-wear rate; past 384 minutes, or
  a stretch covering the usual sleep time, Charge shows a gap and
  restarts from the median of the last 7 wake values (80 with none).
  The day says "Strap off overnight, Charge estimated." A TODO notes
  learning the user's own drift rate, as Zepp does.
- 10+ minutes at HRR 0.30 or more count as exercise even without steps,
  so cycling or rowing that wasn't recorded drains again.
- Calm awake minutes recharge at Zepp's pace: 85 to 86 over 8 hours.

The strap-off night on the test copy (10-20) now restarts at 91 and
bottoms at 51, flagged, instead of a no-sleep low of 26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Find strap button, shown when the strap lists 0x001A, sends 03 and
turns into Stop buzzing. It stops on Stop, after Zepp's 60 s, on the
strap's own 07, or when the link drops. Uses the same 03/06 as a
ringing call (05 01 doesn't vibrate on fw 3.25).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Restart strap", with a confirm dialog, writes Settings GENERAL key 45
= true with the group version just read. Like Zepp, it's offered only
when the strap reports key 45 (read on connect). The link drops and
the usual reconnect brings the strap back. Not tried on a strap yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- With the new Off-body reminder setting on (Phone alerts, on by
  default), OpenStrap turns on the strap's wear reminder (GENERAL key
  44) and follows DEVICE_INFORMATION key 16 as read on connect and as
  pushed. Off body posts a "Wear reminder" notification; back on
  clears it. The first push is logged raw, as nobody has captured one.
- Each change goes in a new wear_event table (database 16); a lost link
  is stored as unknown.
- Charge treats recorded off-body time as not worn and recorded
  on-body time as worn; the heart-rate gap rule still covers time with
  nothing recorded.

Not checked on a strap yet: whether it reports key 44 or pushes key 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The summary's Type.subtype was parsed but dropped. It is now stored
(database 17), logged on sync, and subtype 1, a workout the strap
recognised on its own, gets an "Auto-detected" label in the workout
list and detail. A newly synced one that ended within the last day
posts Zepp's "<Sport> detected, 10:02–10:40" notification (codemap 05
§8); older backlog stays quiet. Subtype 6 will show whether a
phone-started 0x0019 session got mirrored on the strap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
workout.csv gains a subtype column (blank in older exports), and the
strap's on/off-body changes get their own wear_event.csv, with worn
blank where the link dropped. The restore test now carries both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tapping it switches to Activity with the workout's detail open, as the
live-workout notification does for its own screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Charge: only a recorded off-body is decisive. A recorded on-body no
  longer overrides the strap's off-wrist byte or the 10-minute gap
  rule, since the strap's wear push hasn't been seen on hardware and
  the connect-time read can go stale.
- Load: workout heart rate is reduced to minutes as it's read, and
  workout ranges are looked up by binary search, with identical results.
- Usual sleep measures each end from its own start, so a day sleep
  after a night shift (08:00-16:00) is a range rather than nothing.
- Stress runs reset across skipped minutes; CALM_STEPS replaces a bare
  10; KDocs state current behaviour and say where load differs from
  Zepp on purpose.
- Activity note mentions brisk walking from 20 % of reserve; docs give
  the Alert cap as 54.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Any stop ends Find strap, so a ringing call ending mid-find no
  longer leaves "Stop buzzing" up until the 60 s timeout.
- Wear changes are written one at a time in arrival order; a later
  change in the same second wins and stamps never step back.
- The off-body notification is reworded in OpenStrap's own words.
- Restart's request comes from SettingsCodec.restartFor, null when the
  strap doesn't report key 45, and is tested.
- ZeppSignIn's deferred TODO is its own comment, out of the KDoc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On firmware 3.25.0.3 the strap never pushed DEVICE_INFORMATION key 16
when taken off or put back on, with its wear reminder (GENERAL key 44)
on, though a fresh read had it right. The off-body reminder only
updated on reconnect. Re-read key 16 every minute; tested on the strap,
off and back on each showed within one poll and the notification
followed. The poll's replies are kept out of the first-push log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sayed0am
sayed0am merged commit 958d757 into main Oct 23, 2026
1 check passed
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