Skip to content

feat(firmware): record each ride's effort limits, v6 ride footer - #2199

Merged
timohueser merged 9 commits into
developfrom
claude/ride-zone-limits
Sep 25, 2026
Merged

timohueser merged 9 commits into
developfrom
claude/ride-zone-limits

Conversation

@timohueser

Copy link
Copy Markdown
Owner

Summary

Each recorded ride now carries the rider's effort limits that were in force when the ride started: max heart rate and FTP. An old ride therefore keeps its own zones after the rider changes the limits. The app's ride timeline, a separate PR, draws zones from these limits.

  • Device: App::ride_origin() copies max_hr and ftp_w from the settings when a ride starts. A resumed ride keeps its first copy through the resume image. The limits type obc_formats::ride::EffortLimits is shared by the device and the formats code; 0 means "not set".
  • Format: the ride object is now version 6. The footer is 154 bytes: byte 150 is max HR, byte 151 is reserved, and bytes 152–153 are FTP. The resume image is now version 4. The RRAM settings blob does not change.
  • The device's own ride-detail sensor page colours zones with the ride's limits, not the current settings. A ride without limits shows its graphs without zones.
  • iOS: RideZoneLimits sits on RideSummary and is readable as Ride.zoneLimits. heartRateZone(bpm:) and powerZone(watts:) use the device's edge rules. A parity test checks them against the firmware cases. Older app manifests read as not set.
  • Contracts: obc-ble-interface-spec.md §7.2 has the footer rows and the zone-edge table. FLAT_Store_Format.md §7.2 has the buffer sizes. ride-v6.bin replaces ride-v5.bin.
  • Resources: the App allocation is re-pinned from 58,184 to 58,440 B. The limits add 8 B to each of the 32 catalog entries, because the 64-bit object id pads each entry. The numbers are in the commit body. flat_ride_delta is re-pinned from 566 to 570 B.

Public docs

Not changed.

Checks

  • obc test -p for obc-formats, obc-route, obc-vectors, obc-app, obc-host-core, obc-link and obc-web-convert, with clippy -D warnings. New tests cover the snapshot at start, at resume and when not set, the resume codec, the vector, and the sensor page zones.
  • swift test in Packages/OBCKit, with the vector round-trip, zone parity and old-manifest tests.
  • obc shot --check: all 377 frames match. obc flash build, the board resource checks and obc suites check pass.

Requirements: none

🤖 Generated with Claude Code

timohueser and others added 4 commits September 25, 2026 10:35
Each ride now keeps the rider's max HR and FTP from the moment it
starts, so an old ride keeps its own effort zones when the settings
change later.

- Ride object v6: the footer grows to 154 bytes with the max HR limit,
  a reserved byte and the FTP limit (0 = not set). The spec states the
  fixed zone edges a reader applies to them.
- The recorder snapshots the limits into the ride origin at a fresh
  start. The resume image (v4) carries them, so a continued ride keeps
  its original snapshot.
- effort::Limits becomes the shared obc_formats::ride::EffortLimits.
- ride-v6.bin replaces ride-v5.bin, regenerated with the obc-vectors
  generator; the web decoder, the conversion bridge, the UI-frame
  staging and the flat-engine tests read the new vector.
- The seeded demo sensor rides carry max HR 185 and FTP 250.
- flat_ride_delta re-pinned at 570 B (+4 B footer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RideObjectCodec reads and writes the v6 footer limits. RideSummary
carries them as `zoneLimits: RideZoneLimits` (nil = not set on the
device at ride time), and `Ride.zoneLimits` reads them. Edits keep them,
and the archive manifest stores them; a manifest without the key reads
as not set.

RideZoneLimits.heartRateZone(bpm:) and .powerZone(watts:) map a value to
zone 0...4 with the device's edge rules, and the tests mirror the
effort.rs cases. The website and default mock rides carry max HR 185 and
FTP 250; Sunday Coffee Spin has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ride-detail sensor page now takes its HR and power zones from the
limits the ride recorded, not from today's settings. A ride recorded
without limits shows its graphs without zones.

- RideInfo and RideSummary carry the footer's EffortLimits into the
  resident ride catalog.
- The UI-frame staging gives the heart-rate trip day the 185 bpm and
  250 W limits the ride-detail-hr frame shows; no frame changed.

Resource re-pins, from one head build on the pinned host (rustc 1.96.0):
- RideEntry 112 B -> 120 B: 4 B of limits, and the u64 object id rounds
  the entry up by 4 B more.
- App allocation 58,184 B -> 58,440 B (+256 B = 32 catalog entries x 8 B).
- Board guards pass: 315,736 B linked resident inside the band, residual
  main stack 43,688 B, poll frame 9,864 B, init_idle frame 1,856 B.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: f1a74fa9-1253-423c-8812-708455e68d74


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

timohueser and others added 5 commits September 25, 2026 11:20
RideObject gains effortLimits (null = not set), so the decoder and the
encoder round-trip ride-v6.bin byte for byte. The dev harness rides move
to version 6, and the version-mismatch test writes version 7 at the v6
footer offset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The website fixture is generated, so the Grimsel ride's max HR 185 and
FTP 250 move into generate-website-fixture.py, and website.json is
regenerated from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the demo-ride seeder and its README against the Kandel ride
fixture: the seeder keeps both the single-file path and the demo rides'
185 bpm and 250 W limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- gpx_to_ride writes the v6 footer with max HR 185 bpm and FTP 250 W by
  default; two optional arguments set other limits.
- kandel-ride package revision 183efc8f (53,219 bytes): the rebuilt
  v6 kandel.obcr and the updated README, published to the fixture store.
- The file seeder replaces a v5 ride of the same name in place (same
  object, next revision). Both seeders rewrite every other v5 ride on
  the card as v6 without limits, samples unchanged, because the v6
  board rejects v5 rides and one unreadable ride blocks the whole Rides
  menu. A different current payload with the same name is still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v5 check and the upgrade opened a store handle per ride and never
closed it. The store has five open-object slots, so a card with more
rides refused the seed as Busy part-way through the upgrade. The
seeded-ride check closes its handle too. The host test now upgrades
more v5 rides than there are slots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timohueser
timohueser merged commit ffcedc9 into develop Sep 25, 2026
31 checks passed
@timohueser
timohueser deleted the claude/ride-zone-limits branch September 25, 2026 10:14
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