Temporary PKJS watchPosition() speed feed (issue #13) #110

Merged
robert merged 1 commit from area/pkjs-speed-feed into main 2026-09-05 12:21:32 +02:00
Owner

Closes #13.

What this does

Adds a temporary PKJS speed feed so the watchapp has real speed today, before any Android companion
work exists. Runs inside PebbleKit JS (the Pebble mobile app's own JS environment), not on the watch.

  • navigator.geolocation.watchPosition() with enableHighAccuracy: true, maximumAge: 0, and a
    15s timeout; a real error callback on every geolocation call (W3C PositionError codes 1-3,
    all logged and treated the same way: tell the watch SPEED_MMS is stale via the sentinel).
  • Speed/avg-speed/distance forwarded to the watch as SPEED_MMS / AVG_SPEED_MMS / DISTANCE_M.
    No raw lat/lon key exists anywhere in docs/PROTOCOL.md section 2 (phone -> watch) — MAP_POS
    is unrelated per-slice map-tile geometry, not a live GPS feed — so "position forwarded to the
    watch" is what position becomes on this wire today: distance and the two speed keys, not
    coordinates.
  • Distance and a moving-time average accumulated on the JS side, deliberately simpler than
    companion/core's SpeedPipeline/StopDetector (#17/#69): a single stopped-speed clamp (0.8 m/s
    — the same number as StopDetector.kt's STOP_THRESHOLD_MPS, reused for consistency, not the
    hysteresis/dwell machinery around it), no GPS-quality gating, no wraparound-safe accumulators.
  • Written in plain ES5 (var, no arrow/let/const/template literals). pypkjs (this SDK's
    emulator, pypkjs==2.0.7) runs PKJS on real V8 via STPyV8, so it would accept modern syntax, but
    I could not find current, official documentation of what JS engine the production Core Devices
    Pebble mobile app embeds on a real phone, so I did not assume ES2020+ is safe there.
  • Kept behind a fallback flag, per the issue's own Notes: localStorage['pkjsSpeedFeedEnabled'],
    defaulting to enabled (Phase 1 has no other speed source). Issue #21 ("retire the PKJS feed behind
    a fallback flag") is expected to flip it off, or change the default, once the real companion ships
    its own telemetry — see index.js's "FALLBACK FLAG" comment for exactly what that touches.
  • Known limitations documented in index.js's top comment and in docs/DEV.md: background
    reliability (PKJS has no hook tied to the watchapp being foregrounded, so Android's background
    execution limits can throttle or kill it silently), no BLE sensors (phone-GPS-only), no GPS-quality
    gating (single previous fix, no outlier rejection).

Message keys — where the numbers came from

SPEED_MMS (10) / AVG_SPEED_MMS (11) / DISTANCE_M (13) come from watchapp/package.json's
messageKeys object, which the Core Devices SDK's own process_message_keys.py build step turns
into build/js/message_keys.json (and a matching MESSAGE_KEY_* C header) — index.js reads them
via require('message_keys'), not a literal. I confirmed this by inspecting the actual bundled
build/pebble-js-app.js output: module.exports = {"AVG_SPEED_MMS":11,"DISTANCE_M":13,"SPEED_MMS":10},
matching docs/PROTOCOL.md section 2.2 exactly.

This is still a second, independent place those three integers are written down —
tools/gen_message_keys.py (#7) only generates proto.h and Proto.kt, it has no JS target, so
nothing checks this package.json fragment against shared/message_keys.json the way proto.h/
Proto.kt are checked against each other. I hand-copied the three numbers with a comment in
index.js citing docs/PROTOCOL.md section 2.2 and shared/message_keys.json directly. Extending
tools/gen_message_keys.py to also emit/check this package.json fragment (or a standalone JS
constants module) would close that gap properly — flagging it as a follow-up rather than doing it in
this PR, since it touches a generator with its own tests and a companion Kotlin output I did not want
to risk regressing here.

Verified

  • pebble build clean for emery, gabbro and basalt (pebble clean && pebble build, all three
    platforms, webpack bundling src/pkjs/index.js with no errors).
  • A real pypkjs-driven emery emulator run, not just a compile check:
    pebble install --emulator emery + pebble logs shows watchPosition() getting a genuine fix
    through pypkjs's own IP-geolocation path (pypkjs/javascript/navigator/geolocation.py: a real
    https://api.ipify.org lookup plus a bundled GeoLiteCity.dat, not a mock — this sandbox's network
    egress works, confirmed with curl), the resulting SPEED_MMS/AVG_SPEED_MMS/DISTANCE_M
    AppMessage arriving at the watch's C-side inbox handler (main.c: AppMessage inbox: message received), and the watch's ack coming back to PKJS
    (pkjs speed feed: sendAppMessage acked by watch (speed=0 avgSpeed=0 distance=0)). The 0s are
    correct — it's the first fix, no prior position to compute a delta from yet. Full log excerpt in
    docs/DEV.md.
  • Not verified live: multi-fix accumulation (distance/average building up over several fixes).
    pypkjs's own watchPosition() implementation (2.0.7) fires the success callback exactly once per
    call rather than actually repeating — a limitation of the emulator's geolocation stub, not of this
    code — so a second, later fix was never exercised in this sandbox. Verified by reading the
    accumulation logic instead; a real device (or a future pypkjs with a real recurring
    watchPosition()) would exercise it.
  • The error/timeout callback path is implemented per the W3C Geolocation API shape but was not
    exercised live either: pypkjs's stub only calls its failure callback on an HTTP exception, and this
    sandbox's network egress works, so no failure ever fired.

Files

  • watchapp/src/pkjs/index.js — new
  • watchapp/package.json — messageKeys changed from the unused scaffold placeholder (["dummy"],
    list form, SDK auto-numbers those starting at 10000) to the pinned dict form above; added the
    location capability (confirmed valid against the SDK's own schema,
    sdk-core/pebble/common/tools/schemas/attributes.json: ["location", "configurable", "health"])
  • docs/DEV.md — new "PebbleKit JS temporary speed feed" section; Project layout updated

https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt

Closes #13. ## What this does Adds a temporary PKJS speed feed so the watchapp has real speed today, before any Android companion work exists. Runs inside PebbleKit JS (the Pebble mobile app's own JS environment), not on the watch. - `navigator.geolocation.watchPosition()` with `enableHighAccuracy: true`, `maximumAge: 0`, and a 15s `timeout`; a real `error` callback on every geolocation call (W3C `PositionError` codes 1-3, all logged and treated the same way: tell the watch `SPEED_MMS` is stale via the sentinel). - Speed/avg-speed/distance forwarded to the watch as `SPEED_MMS` / `AVG_SPEED_MMS` / `DISTANCE_M`. **No raw lat/lon key exists anywhere in `docs/PROTOCOL.md` section 2** (phone -> watch) — `MAP_POS` is unrelated per-slice map-tile geometry, not a live GPS feed — so "position forwarded to the watch" is what position becomes on this wire today: distance and the two speed keys, not coordinates. - Distance and a moving-time average accumulated on the JS side, deliberately simpler than `companion/core`'s `SpeedPipeline`/`StopDetector` (#17/#69): a single stopped-speed clamp (0.8 m/s — the same number as `StopDetector.kt`'s `STOP_THRESHOLD_MPS`, reused for consistency, not the hysteresis/dwell machinery around it), no GPS-quality gating, no wraparound-safe accumulators. - Written in plain ES5 (`var`, no arrow/`let`/`const`/template literals). pypkjs (this SDK's emulator, `pypkjs==2.0.7`) runs PKJS on real V8 via `STPyV8`, so it would accept modern syntax, but I could not find current, official documentation of what JS engine the production Core Devices Pebble mobile app embeds on a real phone, so I did not assume ES2020+ is safe there. - Kept behind a fallback flag, per the issue's own Notes: `localStorage['pkjsSpeedFeedEnabled']`, defaulting to enabled (Phase 1 has no other speed source). Issue #21 ("retire the PKJS feed behind a fallback flag") is expected to flip it off, or change the default, once the real companion ships its own telemetry — see `index.js`'s "FALLBACK FLAG" comment for exactly what that touches. - Known limitations documented in `index.js`'s top comment and in `docs/DEV.md`: background reliability (PKJS has no hook tied to the watchapp being foregrounded, so Android's background execution limits can throttle or kill it silently), no BLE sensors (phone-GPS-only), no GPS-quality gating (single previous fix, no outlier rejection). ## Message keys — where the numbers came from `SPEED_MMS` (10) / `AVG_SPEED_MMS` (11) / `DISTANCE_M` (13) come from `watchapp/package.json`'s `messageKeys` object, which the Core Devices SDK's own `process_message_keys.py` build step turns into `build/js/message_keys.json` (and a matching `MESSAGE_KEY_*` C header) — `index.js` reads them via `require('message_keys')`, not a literal. I confirmed this by inspecting the actual bundled `build/pebble-js-app.js` output: `module.exports = {"AVG_SPEED_MMS":11,"DISTANCE_M":13,"SPEED_MMS":10}`, matching `docs/PROTOCOL.md` section 2.2 exactly. This is still a **second, independent place** those three integers are written down — `tools/gen_message_keys.py` (#7) only generates `proto.h` and `Proto.kt`, it has no JS target, so nothing checks this `package.json` fragment against `shared/message_keys.json` the way `proto.h`/ `Proto.kt` are checked against each other. I hand-copied the three numbers with a comment in `index.js` citing `docs/PROTOCOL.md` section 2.2 and `shared/message_keys.json` directly. Extending `tools/gen_message_keys.py` to also emit/check this `package.json` fragment (or a standalone JS constants module) would close that gap properly — flagging it as a follow-up rather than doing it in this PR, since it touches a generator with its own tests and a companion Kotlin output I did not want to risk regressing here. ## Verified - `pebble build` clean for `emery`, `gabbro` and `basalt` (`pebble clean && pebble build`, all three platforms, `webpack` bundling `src/pkjs/index.js` with no errors). - **A real pypkjs-driven `emery` emulator run**, not just a compile check: `pebble install --emulator emery` + `pebble logs` shows `watchPosition()` getting a genuine fix through pypkjs's own IP-geolocation path (`pypkjs/javascript/navigator/geolocation.py`: a real `https://api.ipify.org` lookup plus a bundled `GeoLiteCity.dat`, not a mock — this sandbox's network egress works, confirmed with `curl`), the resulting `SPEED_MMS`/`AVG_SPEED_MMS`/`DISTANCE_M` AppMessage arriving at the watch's C-side inbox handler (`main.c: AppMessage inbox: message received`), and the watch's ack coming back to PKJS (`pkjs speed feed: sendAppMessage acked by watch (speed=0 avgSpeed=0 distance=0)`). The 0s are correct — it's the first fix, no prior position to compute a delta from yet. Full log excerpt in `docs/DEV.md`. - **Not verified live**: multi-fix accumulation (distance/average building up over several fixes). `pypkjs`'s own `watchPosition()` implementation (2.0.7) fires the success callback exactly once per call rather than actually repeating — a limitation of the emulator's geolocation stub, not of this code — so a second, later fix was never exercised in this sandbox. Verified by reading the accumulation logic instead; a real device (or a future pypkjs with a real recurring `watchPosition()`) would exercise it. - The `error`/`timeout` callback path is implemented per the W3C Geolocation API shape but was not exercised live either: pypkjs's stub only calls its failure callback on an HTTP exception, and this sandbox's network egress works, so no failure ever fired. ## Files - `watchapp/src/pkjs/index.js` — new - `watchapp/package.json` — `messageKeys` changed from the unused scaffold placeholder (`["dummy"]`, list form, SDK auto-numbers those starting at 10000) to the pinned dict form above; added the `location` capability (confirmed valid against the SDK's own schema, `sdk-core/pebble/common/tools/schemas/attributes.json`: `["location", "configurable", "health"]`) - `docs/DEV.md` — new "PebbleKit JS temporary speed feed" section; `Project layout` updated https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
Add temporary PKJS speed feed (issue #13)
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 (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
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
0579e81adc
Gives the watchapp real speed immediately via navigator.geolocation.watchPosition()
running inside PebbleKit JS, before any Android companion work exists.

- enableHighAccuracy: true, maximumAge: 0, a 15s timeout, and a real error
  callback on every geolocation call.
- Speed/avg-speed/distance forwarded to the watch as SPEED_MMS / AVG_SPEED_MMS /
  DISTANCE_M. No raw lat/lon key exists anywhere in PROTOCOL.md section 2
  (phone -> watch) to forward "position" as coordinates, so distance/speed
  derived from position is what "position forwarded to the watch" means here.
- Distance and moving-time average accumulated on the JS side, deliberately
  simpler than companion/core's SpeedPipeline/StopDetector: a single stopped-
  speed clamp (0.8 m/s, the same number as StopDetector.kt's
  STOP_THRESHOLD_MPS), no GPS-quality gating, no hysteresis/dwell state
  machine. Written in plain ES5 since the production PKJS engine's real
  ECMAScript support isn't documented anywhere current-and-official.
- Message keys come from watchapp/package.json's messageKeys object
  ({"SPEED_MMS": 10, "AVG_SPEED_MMS": 11, "DISTANCE_M": 13}), built by the
  SDK's own process_message_keys.py into build/js/message_keys.json and
  read via require('message_keys') in index.js -- confirmed by inspecting
  the actual bundled output, not assumed. Those three numbers are hand-copied
  from docs/PROTOCOL.md section 2.2 / shared/message_keys.json, since
  tools/gen_message_keys.py (#7) has no JS output target yet; flagged as a
  reasonable follow-up rather than done here.
- Kept behind a fallback flag: localStorage['pkjsSpeedFeedEnabled'], default
  enabled. Issue #21 is expected to flip it off (or change the default) once
  the real companion ships its own telemetry.
- Known limitations (background reliability, no BLE sensors, no GPS-quality
  gating) documented in index.js's top comment and in docs/DEV.md.

Verified: `pebble build` clean for emery/gabbro/basalt, and a real
pypkjs-driven emery emulator run -- watchPosition() got a genuine fix via
pypkjs's own IP-geolocation path (api.ipify.org + GeoLiteCity.dat, not a
mock), the resulting AppMessage was received by the watch's C-side inbox
handler, and the watch's ack came back to PKJS. See docs/DEV.md for the full
log excerpt. pypkjs's watchPosition() fires only once rather than repeating,
so multi-fix accumulation was verified by reading the logic, not by a live
run.

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
robert merged commit 9c7253388c into main 2026-09-05 12:21:32 +02:00
Sign in to join this conversation.
No description provided.