|
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. |
87 | 96 |
|
88 | 97 | ## Feedback |
89 | 98 |
|
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