Settings: units, wheel circumference, HR alerts, auto-pause, vibration (#44) #91

Merged
robert merged 2 commits from area/settings-screen into main 2026-09-04 14:37:21 +02:00
Owner

Closes #44.

What this does

The phone-side settings screen, scoped per the issue's two 2026-09 update comments:

  • Auto-pause keeps only the on/off setting (behaviour and threshold are #69's job, D35).
  • Voice turn announcements default off (D45), feeding a not-yet-built Phase 3 announcer.

Layout

  • companion/core/.../settings/RideSettings.kt + SettingsRepository.kt (pure JVM, :companion:core): the domain model (UnitSystem, WheelCircumference/WheelCircumferencePreset, HrZoneBoundaries, HrSettings, VibrationIntensity, RideSettings) and validated setters, over a SettingsStorage interface — the same RouteLibrary/RouteRecordStorage split #25 established. SettingsRepositoryTest covers it: 16 Kotest cases, all green.
  • New :companion:settings module (Android): SharedPreferencesSettingsStorage (adapter, same reasoning as SharedPreferencesActiveRoutePointerStorage from #25 — every setting is a scalar or small fixed tuple, no querying need, so no Room/KSP here), SettingsStore (wiring singleton, same shape as RouteStore), SettingsScreen/SettingsActivity (Compose, same shape as RouteLibraryScreen/RouteLibraryActivity).

Why a new module rather than folding into :companion:route or the app module: settings are not a route concern (bundling them into :companion:route would make that module's name misleading), and :companion is explicitly meant to stay thin composition-root wiring per its own build.gradle.kts KDoc — logic belongs in the module that owns it.

MainActivity gets a second button (Settings, alongside the existing Routes one) into the new activity, same-app explicit Intent, no package-visibility concern (D36 is a PebbleKit-only concern).

Wheel circumference presets

Three common road sizes on the 622 mm (ETRTO/"700C") bead-seat diameter: 700x25c (2105 mm), 700x28c (2136 mm), 700x32c (2155 mm). These are the standard cyclocomputer wheel-size-table figures, checked live against https://sport-calculator.com/calculators/cycling/bicycle-tire-size-chart and the ETRTO tyre-size chart (2026-09-04) rather than recalled from memory. WheelCircumference.Manual takes an exact mm value for riders who measure their own rollout.

What has a consumer today, and what does not

Nothing on the watch side has a consumer yet — that's the headline finding, not a per-setting exception:

  • unitSystem — UNITS (wire id 3) already has a defined shape in docs/PROTOCOL.md §2.1 (0 = metric, 1 = imperial), but no AppMessage send path exists anywhere in this codebase before Spike A (#4) — PebbleTransport in :companion:pebble is still an empty placeholder interface. fields.c on the watch is hardcoded to metric strings (km/h, km) regardless of this setting.
  • wheelCircumference, hr.zoneBoundaries/hr.alertsEnabled, autoPauseEnabled, vibrationIntensity, autoSwitchToNavView, voiceAnnouncementsEnabled — no wire key exists at all. docs/PROTOCOL.md §2.6 (Configuration) defines only CONFIG_PAGES/CONFIG_SEQ (page layout, #61). Per D48 this PR does not invent a wire format for these; that's a call for whichever future issue actually needs the push to happen.
  • No wheel-circumference-to-speed derivation exists (needs a wheel-rev sensor consumer that isn't built).
  • No HR alert delivery exists — no live HR data flows anywhere in the companion yet (HR is watch-local per D3, and only reaches the phone as buffered HR_SAMPLES, which nothing turns into an alert). SettingsRepository enforces "alerts cannot be enabled while zones are unconfigured" as its own invariant, independent of delivery existing.
  • ride_link.c's vibration calls (#11/PR #84) are hardcoded; nothing reads vibrationIntensity yet.
  • No nav-view auto-switch code exists; nothing reads autoSwitchToNavView yet.
  • voiceAnnouncementsEnabled feeds the not-yet-built Phase 3 announcer (FR-N19, D45).

So: "settings persisted and pushed to the watch on connect" from the issue's acceptance criteria is only half built here, honestly — persistence is real and tested, the push is scoped out because the transport to push over does not exist yet for any key, not just these.

Verification

  • ./gradlew :companion:core:test — green, 16/16 (--no-configuration-cache; this sandbox only has JDK 25, no JDK 17 toolchain for Gradle's configuration-cache serialization step, unrelated to the actual test run).
  • ./gradlew projects — confirms :companion:settings registers and the whole build's Gradle configuration resolves cleanly.
  • :companion:settings (Compose UI, manifest, MainActivity wiring) is hand-reviewed only, not compiled — no Android SDK in this sandbox (ANDROID_HOME/local.properties unset), the same constraint every companion PR has hit tonight. ./gradlew :companion:settings:compileDebugKotlin fails at the SDK-location check, not at any Kotlin/Gradle syntax problem.

No new Gradle dependency was needed — SharedPreferences, Compose and coroutines were already in the version catalog for :companion:route.

Closes #44. ## What this does The phone-side settings screen, scoped per the issue's two 2026-09 update comments: - Auto-pause keeps only the **on/off setting** (behaviour and threshold are #69's job, D35). - Voice turn announcements default **off** (D45), feeding a not-yet-built Phase 3 announcer. ## Layout - **`companion/core/.../settings/RideSettings.kt` + `SettingsRepository.kt`** (pure JVM, `:companion:core`): the domain model (`UnitSystem`, `WheelCircumference`/`WheelCircumferencePreset`, `HrZoneBoundaries`, `HrSettings`, `VibrationIntensity`, `RideSettings`) and validated setters, over a `SettingsStorage` interface — the same `RouteLibrary`/`RouteRecordStorage` split #25 established. `SettingsRepositoryTest` covers it: 16 Kotest cases, all green. - **New `:companion:settings` module** (Android): `SharedPreferencesSettingsStorage` (adapter, same reasoning as `SharedPreferencesActiveRoutePointerStorage` from #25 — every setting is a scalar or small fixed tuple, no querying need, so no Room/KSP here), `SettingsStore` (wiring singleton, same shape as `RouteStore`), `SettingsScreen`/`SettingsActivity` (Compose, same shape as `RouteLibraryScreen`/`RouteLibraryActivity`). **Why a new module** rather than folding into `:companion:route` or the app module: settings are not a route concern (bundling them into `:companion:route` would make that module's name misleading), and `:companion` is explicitly meant to stay thin composition-root wiring per its own `build.gradle.kts` KDoc — logic belongs in the module that owns it. `MainActivity` gets a second button (`Settings`, alongside the existing `Routes` one) into the new activity, same-app explicit `Intent`, no package-visibility concern (D36 is a PebbleKit-only concern). ## Wheel circumference presets Three common road sizes on the 622 mm (ETRTO/"700C") bead-seat diameter: 700x25c (2105 mm), 700x28c (2136 mm), 700x32c (2155 mm). These are the standard cyclocomputer wheel-size-table figures, checked live against https://sport-calculator.com/calculators/cycling/bicycle-tire-size-chart and the ETRTO tyre-size chart (2026-09-04) rather than recalled from memory. `WheelCircumference.Manual` takes an exact mm value for riders who measure their own rollout. ## What has a consumer today, and what does not **Nothing on the watch side has a consumer yet** — that's the headline finding, not a per-setting exception: - `unitSystem` — `UNITS` (wire id 3) **already has a defined shape** in `docs/PROTOCOL.md` §2.1 (0 = metric, 1 = imperial), but no AppMessage *send path* exists anywhere in this codebase before Spike A (#4) — `PebbleTransport` in `:companion:pebble` is still an empty placeholder interface. `fields.c` on the watch is hardcoded to metric strings (`km/h`, `km`) regardless of this setting. - `wheelCircumference`, `hr.zoneBoundaries`/`hr.alertsEnabled`, `autoPauseEnabled`, `vibrationIntensity`, `autoSwitchToNavView`, `voiceAnnouncementsEnabled` — **no wire key exists at all**. `docs/PROTOCOL.md` §2.6 (Configuration) defines only `CONFIG_PAGES`/`CONFIG_SEQ` (page layout, #61). Per D48 this PR does not invent a wire format for these; that's a call for whichever future issue actually needs the push to happen. - No wheel-circumference-to-speed derivation exists (needs a wheel-rev sensor consumer that isn't built). - No HR alert *delivery* exists — no live HR data flows anywhere in the companion yet (HR is watch-local per D3, and only reaches the phone as buffered `HR_SAMPLES`, which nothing turns into an alert). `SettingsRepository` enforces "alerts cannot be enabled while zones are unconfigured" as its own invariant, independent of delivery existing. - `ride_link.c`'s vibration calls (#11/PR #84) are hardcoded; nothing reads `vibrationIntensity` yet. - No nav-view auto-switch code exists; nothing reads `autoSwitchToNavView` yet. - `voiceAnnouncementsEnabled` feeds the not-yet-built Phase 3 announcer (FR-N19, D45). So: **"settings persisted and pushed to the watch on connect"** from the issue's acceptance criteria is only half built here, honestly — persistence is real and tested, the push is scoped out because the transport to push over does not exist yet for any key, not just these. ## Verification - `./gradlew :companion:core:test` — **green**, 16/16 (`--no-configuration-cache`; this sandbox only has JDK 25, no JDK 17 toolchain for Gradle's configuration-cache serialization step, unrelated to the actual test run). - `./gradlew projects` — confirms `:companion:settings` registers and the whole build's Gradle configuration resolves cleanly. - `:companion:settings` (Compose UI, manifest, MainActivity wiring) is **hand-reviewed only, not compiled** — no Android SDK in this sandbox (`ANDROID_HOME`/`local.properties` unset), the same constraint every companion PR has hit tonight. `./gradlew :companion:settings:compileDebugKotlin` fails at the SDK-location check, not at any Kotlin/Gradle syntax problem. No new Gradle dependency was needed — `SharedPreferences`, Compose and coroutines were already in the version catalog for `:companion:route`.
Settings: units, wheel circumference, HR alerts, auto-pause, vibration (#44)
Some checks failed
dev-artifact / build-pbw (push) Has been cancelled
dev-artifact / build-apk (push) Has been cancelled
dev-artifact / publish (push) Has been cancelled
fast-lane / jvm-tests (pull_request) Has been cancelled
fast-lane / pebble-build (pull_request) Has been cancelled
fast-lane / lint-and-secrets (pull_request) Has been cancelled
fast-lane / meta-declares-required-jobs (pull_request) Has been cancelled
fast-lane / host-c-tests (push) Has been cancelled
fast-lane / jvm-tests (push) Has been cancelled
fast-lane / pebble-build (push) Has been cancelled
fast-lane / lint-and-secrets (push) Has been cancelled
fast-lane / meta-declares-required-jobs (push) Has been cancelled
fast-lane / host-c-tests (pull_request) Has been cancelled
5c1037d944
Adds the phone-side settings screen the issue asks for, scoped per its two
2026-09 update comments: auto-pause keeps only the on/off setting (behaviour
is #69/D35), and voice turn announcements default off (D45, feeds a
not-yet-built Phase 3 announcer).

Business logic (RideSettings, SettingsRepository — defaults, wheel
circumference presets/manual override, HR zone boundary validation, the
"alerts need zones configured" invariant) is pure JVM in :companion:core,
following the RouteLibrary/#25 split, with 16 passing Kotest cases in
SettingsRepositoryTest (`./gradlew :companion:core:test`, confirmed green
with --no-configuration-cache — this sandbox has no JDK 17 toolchain for the
configuration-cache serialization step, only JDK 25).

The Android half lives in a new :companion:settings module (SharedPreferences
storage adapter, SettingsStore wiring object, SettingsScreen/SettingsActivity
in Compose) rather than folding into :companion:route or the app module —
settings are not a route concern, and :companion is meant to stay thin
composition-root wiring. No Room/KSP here: every setting is a scalar or small
fixed tuple, the same "flat key-value file" case
SharedPreferencesActiveRoutePointerStorage's KDoc already argues for.

What has a real consumer today: nothing does yet, on the watch side. UNITS
(id 3) already has a defined wire shape in docs/PROTOCOL.md §2.1, but no
AppMessage send path exists anywhere in this codebase before Spike A (#4) —
PebbleTransport is still an empty placeholder interface. The other settings
(wheel circumference, HR zones, auto-pause toggle, vibration, auto-switch-nav
-view, voice announcements) have no wire key at all in §2.6 Configuration,
which today only covers CONFIG_PAGES/CONFIG_SEQ. Per D48, no wire format is
invented here; everything is persisted locally and the PR body states
setting-by-setting what is wired vs. inert.

No Android SDK in this sandbox: :companion:settings and the MainActivity
wiring are hand-reviewed only, not compiled — same constraint every companion
PR has hit tonight. `./gradlew projects` confirms the module registers and
Gradle configuration resolves cleanly up to the SDK-location check.

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
robert force-pushed area/settings-screen from 5c1037d944
Some checks failed
dev-artifact / build-pbw (push) Has been cancelled
dev-artifact / build-apk (push) Has been cancelled
dev-artifact / publish (push) Has been cancelled
fast-lane / jvm-tests (pull_request) Has been cancelled
fast-lane / pebble-build (pull_request) Has been cancelled
fast-lane / lint-and-secrets (pull_request) Has been cancelled
fast-lane / meta-declares-required-jobs (pull_request) Has been cancelled
fast-lane / host-c-tests (push) Has been cancelled
fast-lane / jvm-tests (push) Has been cancelled
fast-lane / pebble-build (push) Has been cancelled
fast-lane / lint-and-secrets (push) Has been cancelled
fast-lane / meta-declares-required-jobs (push) Has been cancelled
fast-lane / host-c-tests (pull_request) Has been cancelled
to 5b0922790e
Some checks failed
dev-artifact / build-pbw (push) Failing after 0s
dev-artifact / build-apk (push) Failing after 0s
dev-artifact / publish (push) Has been skipped
fast-lane / host-c-tests (pull_request) Failing after 0s
fast-lane / jvm-tests (pull_request) Failing after 0s
fast-lane / pebble-build (pull_request) Failing after 0s
fast-lane / lint-and-secrets (pull_request) Failing after 0s
fast-lane / meta-declares-required-jobs (pull_request) Failing after 0s
fast-lane / host-c-tests (push) Failing after 0s
fast-lane / jvm-tests (push) Failing after 0s
fast-lane / pebble-build (push) Failing after 0s
fast-lane / lint-and-secrets (push) Failing after 0s
fast-lane / meta-declares-required-jobs (push) Failing after 0s
2026-09-04 14:37:13 +02:00
Compare
robert merged commit 494fc4cd9f into main 2026-09-04 14:37:21 +02:00
Sign in to join this conversation.
No description provided.