Skip to content

Commit 1731da5

Browse files
eriedclaude
andcommitted
BRANCH.md: describe p6-fixes scope (temps, connect-auth, lock mutex)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a9cce42 commit 1731da5

1 file changed

Lines changed: 97 additions & 88 deletions

File tree

BRANCH.md

Lines changed: 97 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,100 @@
1-
# wear-os-watch-ultra
2-
3-
## What this branch adds
4-
5-
Companion app for Wear OS 5+ watches (built and tuned on the Galaxy Watch
6-
Ultra). The phone holds the BLE link to the wheel and pushes a compact
7-
telemetry snapshot to the watch over the Wearable Data Layer; the watch is a
8-
thin client and never talks to the wheel directly.
9-
10-
This branch is built on top of `main` (V14 + V12 + P6 multi-wheel support
11-
verified through 0.3.1), so the watch dashboard inherits the corrected P6
12-
telemetry offsets (PWM, torque, MOS+motor temps, signed reverse speed).
13-
14-
Concretely shipped here:
15-
16-
- **Full-bleed speed dial** that wraps the entire watch face. Same arc
17-
geometry as the phone dashboard (260° sweep, accent-tinted safe band,
18-
orange/red danger wedges, ticks). Speed number, units, batteries and
19-
buttons live inside the dial.
20-
- **Three batteries at a glance** above the speed number: wheel, phone, and
21-
watch, each colour-graded by the same red/amber/green thresholds used on
22-
the phone dashboard.
23-
- **Accent colour follows the phone.** The accent the user picked in app
24-
settings travels through the wire format and tints the dial safe band,
25-
the wheel-name header on page 2, and the horn / light buttons.
26-
- **Imperial units follow the phone.** When `imperialUnits` is on, the
27-
watch shows mph, miles, and °F; flipping the setting takes effect within
28-
one publish cycle (≤200 ms).
29-
- **Page 2 — at-a-glance details.** Wheel name in accent, live speed, then
30-
a tabular column of voltage / current / power (V × A) / PWM / temp /
31-
torque / trip. Values align vertically across rows so you can scan down a
32-
column.
33-
- **Buttons follow the phone iconography.** Horn = `Icons.Filled.Campaign`,
34-
Light = `Icons.Filled.FlashOn` — same glyphs as the phone dashboard.
35-
- **Disconnected state** shows a phone glyph and a two-line "Open EUC
36-
Planet on your phone" message. No more red dot that read as an error.
37-
- **Resolution-clean.** All sizes derive from `BoxWithConstraints.maxWidth`
38-
so the layout looks right on small round watches (~390 dp) and on Watch
39-
Ultra (~454 dp) without separate code paths.
40-
- **Auto-start ping** on phone-app open and a manual "Play" button next to
41-
the Auto-start setting so users can verify pairing without backgrounding
42-
and relaunching the phone app.
43-
44-
## Architecture
45-
46-
- `WearBridge` (phone, `app/`) subscribes to `WheelRepository` flows and
47-
`SettingsRepository.settings`, samples to 5 Hz, packs a `DataMap` and
48-
publishes at `/euc/state`. Reads phone battery via `BatteryManager`.
49-
- `WatchBridgeService` (watch, `wear/`) decodes the DataMap into a
50-
`WatchState` and updates a singleton `WatchStateRepository`.
51-
- `WatchApp` (Compose) collects from the repo and renders.
52-
- Control flow (horn / light) is the existing reverse channel: watch sends
53-
short Messages on `/euc/control`, phone routes them through
54-
`WheelRepository`.
55-
56-
## Who should test this
57-
58-
- **Watch Ultra owners** with a paired phone running the matching debug or
59-
pre-release APK from the same branch: confirm the dial reads correctly,
60-
battery percentages match Settings/Battery on each device, accent and
61-
imperial follow the phone, and horn/light controls work.
62-
- **Other Wear OS 5+ watches** (round and rectangular): the layout should
63-
scale; please report clipping or overlap. Square watches use the same
64-
dial with the corners falling outside the arc — intentional.
65-
- **Anyone curious about the UI without hardware**: debug builds expose an
66-
ADB demo broadcast. With the watch app open:
67-
```
68-
adb shell am broadcast -p com.eried.eucplanet \
69-
-a com.eried.eucplanet.wear.DEMO \
70-
--ef speed 32 --ei battery 78 --ei phone 64 \
71-
--es accent teal --ef maxSpeed 70 \
72-
--ez imperial false --es name "InMotion V14"
73-
```
74-
Speed/battery/accent/imperial extras are all optional.
75-
76-
## Known limits
77-
78-
- **Pairing must be done via the Wear OS by Google companion app** the
79-
first time. Without pairing, the watch shows the disconnected
80-
placeholder forever; this branch does not change that.
81-
- **Tile and complication** (carousel and watch-face quick-glance) are not
82-
here yet. The companion launches as an app you open from the launcher.
83-
- **No standalone (watch-only) BLE.** The watch never connects to the
84-
wheel directly; if the phone's app process is killed, telemetry stops.
85-
- **No on-watch settings.** Imperial / accent / max-speed cap are read
86-
from the phone — change them there.
1+
# p6-fixes
2+
3+
## What this branch fixes
4+
5+
Three concrete P6 bugs found by analysing a labelled real-hardware capture
6+
(`FINALP6/NEW CAPTURE/btsnoop_hci.log` + matching screen recording, with
7+
the InMotion app's "Detailed Data" page providing ground-truth values).
8+
9+
This branch is built directly on top of `main` 0.4.0-preview1, so V14 /
10+
V12 / Wear OS / multi-wheel preview adapters are all preserved unchanged.
11+
The only path it touches is the InMotion P6.
12+
13+
### 1. Temperatures were wrong
14+
15+
The previous parser read `data[28]/4` for MOS and `data[30]/4` for motor
16+
in the 0x87 realtime frame. Those bytes are not temperatures — they are
17+
the **speed-alarm field** (`uint16 LE` in 0.01 km/h, fixed at 13679 =
18+
85 mph for our wheel). Across 2,300+ frames in the long ride capture,
19+
`data[28]` was constant at 111. The /4 reading happened to land near a
20+
plausible Celsius value in the original short capture by coincidence.
21+
22+
The real **MOS sensor** is at `data[70]` as a raw Fahrenheit byte:
23+
24+
```kotlin
25+
val mosF = data[70].toInt() and 0xFF
26+
val mosC = (mosF - 32) * 5f / 9f
27+
```
28+
29+
Verified against:
30+
- Parked wheel labelled MOS = 72 °F → `data[70]` reads 0x48 = 72 across
31+
the entire NEW CAPTURE (181/181 frames).
32+
- 25-min ride OLD capture: `data[70]` drifts 67-80 °F (19-27 °C),
33+
warming under load — physically correct thermistor signal.
34+
35+
**Motor and driver-board temps do not appear in the realtime 0x87 stream
36+
on this firmware.** Every candidate offset is either a static config
37+
byte or a wrap-around counter. The InMotion app shows those as 79 °F
38+
on a parked wheel which is most likely a cached default rather than a
39+
live sensor read. They are therefore no longer reported on P6 until a
40+
different request unlocks them.
41+
42+
### 2. Lights / horn / max-speed were silently ignored until connect-auth
43+
44+
Our `setP6Light` byte output is **byte-for-byte identical** to what the
45+
InMotion app sends — `aa aa 16 06 02 21 60 50 [v v 03]` — but the wheel
46+
silently drops control writes at the L2CAP layer until a password
47+
handshake has run once after connect. The InMotion app does this on
48+
every connect; we previously only did it on demand when locking.
49+
50+
Added `requiresConnectAuth(): Boolean` to `WheelAdapter` (default
51+
false), set true in the P6 path of `InMotionV2Adapter`. The polling
52+
loop in `WheelRepository.runPollingLoop` now calls
53+
`runConnectAuthHandshake()` once between the init sequence and the
54+
first realtime poll for wheels that need it. V14 family wheels are
55+
unaffected.
56+
57+
The handshake is a fixed echo (the wheel returns a 16-byte "encrypted"
58+
blob and accepts the same blob back), so adding it doesn't change the
59+
security posture — it just primes the wheel's control endpoint.
60+
61+
### 3. Lock taps could clobber each other's pending auth
62+
63+
`pendingAuthKeyDeferred` and `pendingAuthConfirmDeferred` were nullable
64+
singletons assigned by `authenticateAndLock`. Two near-simultaneous
65+
`toggleLock()` calls (rapid taps, or a connect-time auth racing a
66+
manual tap) both wrote the singletons; the first call's deferred got
67+
overwritten and timed out without ever reaching the `setLock` write —
68+
matching the symptom "lock looks like it took, then nothing happened".
69+
70+
Wrapped `authenticateAndLock` body in `authMutex.withLock { … }` so
71+
concurrent taps queue up cleanly. Each handshake completes its full
72+
request → key → verify → confirm → lock cycle before the next runs.
73+
74+
## Other deliverables
75+
76+
- Added `setP6AutoHeadlight(on)` builder (`60 2f [v]`) for the
77+
General Settings → Lighting → Auto Headlight switch. Verified on the
78+
wire (5 toggles in the capture, frames `2f 01 7e` for ON and
79+
`2f 00 7f` for OFF). UI wiring to follow.
80+
- New analysis tools under `tools/` (`p6_new_writes.py`,
81+
`p6_new_realtime.py`, `p6_temp_search.py`) for replaying captures
82+
during future protocol work.
83+
84+
## What still needs verification
85+
86+
- The auth-on-connect change should be tested on a real V14 to confirm
87+
no regression. The default-false flag means V14 takes the same code
88+
path as before, but worth a sanity test.
89+
- Whether the wheel re-locks the control endpoint after some idle
90+
window. If so we'll need to repeat the handshake periodically; the
91+
capture only spans 3 minutes which isn't enough to tell.
92+
- Motor and driver-board temperatures may live in a different request
93+
family (e.g. an info-bundle or settings sub-cmd we don't poll). A
94+
longer hot-wheel capture comparing post-ride values to telemetry
95+
bytes could reveal a different field.
8796

8897
## Feedback
8998

90-
File issues at https://github.com/eried/eucplanet/issues. Tag with the
91-
watch model and Wear OS version if you can.
99+
File issues at https://github.com/eried/eucplanet/issues. Please tag
100+
P6-specific issues with `wheel:p6` if you can.

0 commit comments

Comments
 (0)