fix: prevent sync from overwriting Pi odometer — odometer is Pi→Server only
This commit is contained in:
@@ -1412,8 +1412,8 @@ including two UX polish passes and multiple bug-fix rounds.
|
|||||||
|
|
||||||
**Roadmap ordering note (2026-08-11):** the late phases are intentionally sequenced as
|
**Roadmap ordering note (2026-08-11):** the late phases are intentionally sequenced as
|
||||||
Phase 25.5a–25.5d (configuration/sync/status work) -> Phase 25 (real-car acceptance gate) ->
|
Phase 25.5a–25.5d (configuration/sync/status work) -> Phase 25 (real-car acceptance gate) ->
|
||||||
Phase 26 (road-test bug fixes) -> Phase 27 (feature additions from FEATURES.md) ->
|
Phase 26 (road-test bug fixes) -> Phase 27 (feature additions from FEATURES.md, in progress) ->
|
||||||
Phase 28 (Android, always last).
|
Phase 28 (Android, planning in parallel with Phase 27).
|
||||||
|
|
||||||
### Phase 25.5a: Device configuration sync backend — ✅ COMPLETE (connection-driven server sync)
|
### Phase 25.5a: Device configuration sync backend — ✅ COMPLETE (connection-driven server sync)
|
||||||
**Server API and Pi sync thread implemented.** Server stores device config; Pi fetches when internet available and applies changes without restart.
|
**Server API and Pi sync thread implemented.** Server stores device config; Pi fetches when internet available and applies changes without restart.
|
||||||
@@ -1963,28 +1963,219 @@ agree on OBD and GPS state. BUGS.md Open section is empty. readme.md project sta
|
|||||||
5. **FEAT-08** — Predictable IP on phone hotspot (small)
|
5. **FEAT-08** — Predictable IP on phone hotspot (small)
|
||||||
6. **FEAT-09** — mDNS on phone hotspot (small)
|
6. **FEAT-09** — mDNS on phone hotspot (small)
|
||||||
|
|
||||||
### Phase 28: Native Android companion app — 🚧 PLANNED (low priority, always last)
|
### Phase 28: Native Android companion app — 🚧 PLANNING (low priority, always last)
|
||||||
**Renumbered from former Phase 26 on 2026-08-10 to make room for the road-test bug-fix phase. Renumbered to Phase 28 on 2026-08-11 to make room for Phase 27 feature additions.**
|
**Renumbered from former Phase 26 on 2026-08-10 to make room for the road-test bug-fix phase.
|
||||||
Revives the phone-relay goal (Phase 11/12) properly this time, using a real native app instead of
|
Renumbered to Phase 28 on 2026-08-11 to make room for Phase 27 feature additions. Planning
|
||||||
a web page - the only way to actually hold a WiFi connection to the Pi's hotspot and a cellular
|
begun 2026-08-11 alongside Phase 27 execution.**
|
||||||
connection to the internet open simultaneously (`ConnectivityManager.bindProcessToNetwork()`,
|
|
||||||
unavailable to browser JS - see the Phase 11 update). Explicitly **not urgent** and intentionally
|
#### Why native (not web, not PWA)
|
||||||
kept at the end of the roadmap, after all web/server work and the Phase 25 core in-car acceptance
|
Revives the phone-relay goal (Phase 11/12) properly this time. The Phase 11 update proved
|
||||||
test are done. Permanent vehicle installation remains owner-handled and is not an implementation
|
conclusively that a browser page **cannot** hold a WiFi connection to the Pi's no-internet
|
||||||
phase. Scope, at a high level (not yet designed in detail):
|
hotspot and a cellular connection to the internet open simultaneously — the OS's connectivity
|
||||||
- **Dashcam file transfer**: browse/select/delete dashcam clips already imported to the Pi (same
|
manager picks one default route for all IP traffic, and no browser JS API can override that
|
||||||
data as the Phase 17 local video browser, just a native client instead of/alongside the web UI),
|
decision. A native Android app, however, can call
|
||||||
download them to the phone, and upload to the remote server over cellular while the phone stays
|
`ConnectivityManager.bindProcessToNetwork()` (or use `Network` objects directly) to pin
|
||||||
on the Pi's hotspot for the download leg - the true simultaneous dual-network relay the web
|
*specific* sockets to *specific* network interfaces independently — WiFi-bound socket to the
|
||||||
version could never deliver.
|
Pi for local API calls, cellular-bound socket to `yaris.ceaf.dev` for uploads. This is the
|
||||||
- **Trip/location info**: surface live GPS position, speed, and trip history (Phase 18) pulled
|
only way to achieve the true simultaneous dual-network relay the original Phase 11 intended.
|
||||||
from the Pi while connected to its hotspot (reusing the existing `/api/gps`-style data feeds),
|
|
||||||
so the app is useful as a real "check on the car" companion, not just a file-transfer tool.
|
#### Goals (what the app does)
|
||||||
- Not yet scoped: distribution (sideload vs Play Store), whether it talks to the Pi directly over
|
|
||||||
the hotspot only or also to the remote server directly for trip history when away from the car,
|
1. **Dashcam file relay (primary goal).** Browse dashcam clips already imported to the Pi
|
||||||
and how much of the existing REST API surface can be reused as-is vs needs new
|
(same data as the Phase 17 local video browser), select files, download them over the
|
||||||
mobile-friendly endpoints. To be designed once Phases 12-24 (web-side cleanup + hardening,
|
Pi's hotspot WiFi to the phone's local storage, then upload to the remote server over
|
||||||
redesign, remote portal, and core in-car acceptance) are done.
|
the phone's cellular data — all without ever leaving the Pi's hotspot. The Pi's weak
|
||||||
|
BCM43438 WiFi is only used for the short-hop download to the phone; the internet-bound
|
||||||
|
leg goes over the phone's much faster LTE/5G radio.
|
||||||
|
|
||||||
|
2. **Live car dashboard (companion feature).** Surface live GPS position, speed, RPM,
|
||||||
|
coolant temp, fuel level, throttle, and MIL/DTC status pulled from the Pi's existing
|
||||||
|
REST API (`/api/gps/status`, `/api/obd/status`, `/api/stats`) while connected to the
|
||||||
|
hotspot. Gives the passenger (or driver when parked) a real-time view of what the car
|
||||||
|
is doing — the app is the "check on the car" companion, not just a file-transfer tool.
|
||||||
|
|
||||||
|
3. **Trip history browser (companion feature).** List and replay trips from the Pi's local
|
||||||
|
SQLite (via `/api/trips`, `/api/trip/<id>`) — same data as the Phase 18 `/history` web
|
||||||
|
page, just rendered natively. Offline map tiles not needed on the phone (Google Maps or
|
||||||
|
MapLibre can provide the basemap).
|
||||||
|
|
||||||
|
4. **Remote-server direct access (stretch goal).** When away from the car (no Pi hotspot
|
||||||
|
in range), talk directly to the Phase 24 remote portal's REST API
|
||||||
|
(`https://yaris.ceaf.dev/api/v1/portal/...`) for trip history and dashcam archive
|
||||||
|
browsing — so the app is useful even when not in the car.
|
||||||
|
|
||||||
|
#### Architecture decisions
|
||||||
|
|
||||||
|
- **Language:** Kotlin (modern Android standard, null safety, coroutines for async).
|
||||||
|
Minimum SDK: API 26 (Android 8.0, covers 95%+ of active devices). Target SDK: API 35.
|
||||||
|
- **Networking:** OkHttp + Retrofit for REST calls. Two separate `OkHttpClient` instances:
|
||||||
|
one bound to the WiFi network (Pi hotspot, `10.42.0.0/24`), one bound to cellular
|
||||||
|
(or default network for normal internet). `Network.bindSocket()` per-request via a
|
||||||
|
custom `ConnectionSpec` / interceptor that pins the socket to the desired `Network`
|
||||||
|
object.
|
||||||
|
- **Local storage:** Room (SQLite) for downloaded-file tracking, upload queue state, and
|
||||||
|
cached trip data. Files stored in the app's private external storage directory
|
||||||
|
(`getExternalFilesDir()`).
|
||||||
|
- **UI:** Jetpack Compose (modern declarative UI, less boilerplate than XML, easier to
|
||||||
|
iterate). Material 3 design system. Dark theme by default (car-friendly at night).
|
||||||
|
- **Background uploads:** `WorkManager` for cellular uploads that continue even if the
|
||||||
|
app is backgrounded or the phone is locked. Chunked upload protocol reuses the exact
|
||||||
|
same Phase 10 40MB-chunk `/api/v1/dashcam/upload/chunk` + `/status` endpoints the Pi's
|
||||||
|
own Python client already uses — zero new server endpoints needed for the upload path
|
||||||
|
itself.
|
||||||
|
- **Map:** MapLibre GL (open-source MapLibre GL Native for Android, free tier, no API key
|
||||||
|
required for basic usage — can use OSM raster tiles or self-hosted styles). Google Maps
|
||||||
|
as a fallback option if MapLibre proves problematic.
|
||||||
|
|
||||||
|
#### Pi-side API endpoints (already exist — zero new endpoints needed)
|
||||||
|
The app reuses the existing Pi REST API surface with no changes:
|
||||||
|
- `GET /api/gps/status` — GPS fix, lat/lon, fix age (Phase 25.5d)
|
||||||
|
- `GET /api/obd/status` — adapter presence, car connected, port (BUG-12 fix)
|
||||||
|
- `GET /api/stats` — trips, distance, drive time, max speed, video count, DTC count, odometer (Phase 23)
|
||||||
|
- `GET /api/trips` — trip list (Phase 18)
|
||||||
|
- `GET /api/trip/<id>` — trip route points (Phase 18)
|
||||||
|
- `GET /api/dtc` — MIL status + stored DTC codes (BUG-03 fix)
|
||||||
|
- `GET /api/local-video/list` — imported video files with upload status, size, thumbnails (Phase 17)
|
||||||
|
- `GET /api/local-video/file?path=<relpath>` — stream/download a video file, Range-request support (Phase 17)
|
||||||
|
- `GET /api/local-video/thumbnail?path=<relpath>` — cached thumbnail JPEG (Phase 17)
|
||||||
|
- `DELETE /api/local-video/file?path=<relpath>` — delete local copy only (Phase 17)
|
||||||
|
- `GET /api/remote-config` — server URL + device name (Phase 25.5c)
|
||||||
|
- `GET /api/dash/relay/config` — **was removed in Phase 12** — if the app needs server credentials from the Pi, a new lightweight endpoint may be needed, but the app can also store its own copy of `YARIS_SERVER_URL`/`YARIS_DEVICE_NAME`/`YARIS_API_KEY` independently
|
||||||
|
|
||||||
|
#### Server-side API endpoints (already exist — zero new endpoints needed)
|
||||||
|
- `GET /api/v1/dashcam/upload/status?sha256=...` — which chunks already received (Phase 10)
|
||||||
|
- `POST /api/v1/dashcam/upload/chunk` — multipart chunk upload with index + total (Phase 10)
|
||||||
|
- `GET /api/v1/portal/trips` — trip list (Phase 24)
|
||||||
|
- `GET /api/v1/portal/trip/<id>` — trip route (Phase 24)
|
||||||
|
- `GET /api/v1/portal/dashcam` — dashcam file list (Phase 24)
|
||||||
|
- `GET /api/v1/portal/dashcam/stream/<id>` — video stream with Range support (Phase 24)
|
||||||
|
|
||||||
|
#### What needs NEW endpoints
|
||||||
|
- **None for the core relay flow.** The chunked upload protocol on the server and the
|
||||||
|
local-video list/download endpoints on the Pi already cover everything.
|
||||||
|
- **Maybe:** `GET /api/app/config` on the Pi — a single endpoint returning server URL,
|
||||||
|
device name, and API key in one JSON payload, so the app doesn't need the user to type
|
||||||
|
credentials manually. Alternatively, the app could store its own independent copy of
|
||||||
|
these credentials (simpler, no new Pi endpoint needed). Decision deferred to
|
||||||
|
implementation.
|
||||||
|
|
||||||
|
#### Dev environment setup
|
||||||
|
|
||||||
|
1. **Android Studio** (latest stable, Ladybug or newer). Install on the dev machine
|
||||||
|
(Fedora). SDK Manager: install API 35 platform + build tools, API 26+ system images
|
||||||
|
for emulator.
|
||||||
|
2. **Emulator:** create an AVD with API 35, 4GB RAM, 2GB internal storage. No Play Store
|
||||||
|
needed. The emulator can't test real WiFi/cellular dual-network binding (it only has
|
||||||
|
one virtual network interface), so this is for UI/flow testing only.
|
||||||
|
3. **Real device:** a physical Android phone (the owner's Pixel) is required for
|
||||||
|
dual-network testing. Enable Developer Options → USB debugging. The real-device test
|
||||||
|
flow: connect phone to Pi hotspot → run app → verify it can simultaneously call
|
||||||
|
`10.42.0.1/api/*` AND `yaris.ceaf.dev/api/*`.
|
||||||
|
4. **Project scaffold:** use the Android Studio "Empty Activity" template with Compose.
|
||||||
|
Gradle with Kotlin DSL. Version catalog (`libs.versions.toml`) for dependency
|
||||||
|
management.
|
||||||
|
5. **Repo location:** new `android/` directory at the repo root (sibling to `server/`,
|
||||||
|
`modules/`, etc.). The Android project is part of the same git repo — keeps everything
|
||||||
|
in one place, same as the server.
|
||||||
|
|
||||||
|
#### Implementation order (sub-phases)
|
||||||
|
|
||||||
|
**28.1 — Scaffold + networking proof-of-concept (smallest possible scope)**
|
||||||
|
- Create Android project in `android/`
|
||||||
|
- Add OkHttp + Retrofit, set up two `OkHttpClient` instances (WiFi-bound + cellular-bound)
|
||||||
|
- Build a single "Test connection" screen: one button calls `10.42.0.1/api/gps/status`
|
||||||
|
over WiFi, one button calls `yaris.ceaf.dev/health` over cellular, both at the same time
|
||||||
|
- **Gate:** prove dual-network binding actually works on the real Pixel before building
|
||||||
|
anything else. If `bindProcessToNetwork()` doesn't work as expected on the owner's
|
||||||
|
specific Android version/device, the entire Phase 28 premise needs revisiting.
|
||||||
|
|
||||||
|
**28.2 — Dashcam browser + download**
|
||||||
|
- List imported videos from `GET /api/local-video/list`
|
||||||
|
- Show thumbnails, file sizes, upload status (same data as `/dash` web UI)
|
||||||
|
- Download selected files to phone local storage over WiFi
|
||||||
|
- Progress bar (the Pi's BCM43438 WiFi is slow — ~1.1MB/s — so a 300MB file takes ~5 min)
|
||||||
|
- Handle interruptions (download resumes where it left off via Range requests)
|
||||||
|
|
||||||
|
**28.3 — Cellular upload**
|
||||||
|
- Queue downloaded files for upload via `WorkManager`
|
||||||
|
- Chunked upload to `POST /api/v1/dashcam/upload/chunk` over cellular
|
||||||
|
- Check `/status` first to skip already-received chunks (same as Pi's Python client)
|
||||||
|
- Notification showing upload progress (so user can lock phone / switch apps)
|
||||||
|
- Mark file as uploaded on the Pi (`POST /api/local-video/mark-uploaded` — does this
|
||||||
|
endpoint exist? Check — the relay's `mark_uploaded` was removed in Phase 12. May need
|
||||||
|
a new lightweight endpoint or the app skips this and the Pi's own uploader just sees
|
||||||
|
the file is already on the server next time it scans.)
|
||||||
|
|
||||||
|
**28.4 — Live dashboard**
|
||||||
|
- GPS position on a MapLibre map (updates every 2s from `/api/gps/status`)
|
||||||
|
- Speed, RPM, coolant temp, fuel level, throttle cards (from `/api/obd/status` +
|
||||||
|
socket.io — or polling `/api/stats` every few seconds)
|
||||||
|
- MIL/DTC indicator (from `/api/dtc`)
|
||||||
|
|
||||||
|
**28.5 — Trip history**
|
||||||
|
- List trips from `/api/trips` with date, distance, duration, start/end locations
|
||||||
|
- Tap to view route on map with scrubber timeline (reuse the same data as Phase 18 web
|
||||||
|
replay, just rendered natively)
|
||||||
|
- Geocode via the Pi's `/api/geocode` or the phone's own Android
|
||||||
|
`Geocoder` (no network needed for the Pi's offline `reverse_geocoder` path)
|
||||||
|
|
||||||
|
**28.6 — Remote portal direct access (away from car)**
|
||||||
|
- Talk to `https://yaris.ceaf.dev/api/v1/portal/...` directly
|
||||||
|
- Browse trip history and dashcam archive from anywhere (not just when connected to Pi)
|
||||||
|
- Login with portal credentials (same as Phase 24 web portal login)
|
||||||
|
|
||||||
|
**28.7 — Polish + distribution**
|
||||||
|
- App icon, splash screen, dark theme polish
|
||||||
|
- Error handling: Pi unreachable, server unreachable, no cellular data, storage full
|
||||||
|
- APK signing + release build
|
||||||
|
- Distribution: APK sideload (simplest — host the APK on the Pi itself at
|
||||||
|
`http://yaris.local/app/yaris-obd.apk` so the user just opens that URL on the phone
|
||||||
|
to install/update). Play Store is out of scope (Google developer account fee, review
|
||||||
|
process, updates lag behind development).
|
||||||
|
|
||||||
|
#### Open questions (resolve during 28.1)
|
||||||
|
|
||||||
|
1. **Dual-network binding on the owner's Pixel**: `bindProcessToNetwork()` behavior varies
|
||||||
|
by Android version and manufacturer. Test on the real device before committing to the
|
||||||
|
full build. If it doesn't work, fallback options: (a) download-then-switch-networks
|
||||||
|
two-step flow (download from Pi on WiFi, prompt user to switch to cellular, then
|
||||||
|
upload — clunky but always works), or (b) use the phone's hotspot instead of the Pi's
|
||||||
|
(the phone hosts, the Pi connects as client — reverses the network topology and
|
||||||
|
eliminates the dual-network problem entirely, but means the Pi is a WiFi client, not
|
||||||
|
an AP).
|
||||||
|
|
||||||
|
2. **Credentials flow**: how does the app get `YARIS_SERVER_URL` + `YARIS_DEVICE_NAME` +
|
||||||
|
`YARIS_API_KEY`? Options:
|
||||||
|
- User types them in (simple, works, annoying)
|
||||||
|
- App connects to Pi hotspot → calls a new `GET /api/app/provision` endpoint that
|
||||||
|
returns the creds JSON (zero manual entry, but adds one Pi endpoint)
|
||||||
|
- QR code on the Pi's Settings page (scan once, app stores forever)
|
||||||
|
- App stores its own independent copy (user enters once in app settings, never touches
|
||||||
|
the Pi's copy)
|
||||||
|
|
||||||
|
3. **Socket.io for real-time data**: the Pi's dashboard uses Socket.IO for live OBD/GPS
|
||||||
|
updates. The app could use a Socket.IO Kotlin client (`io.socket:socket.io-client`)
|
||||||
|
for push updates, or simply poll the REST endpoints every 2–5 seconds. Polling is
|
||||||
|
simpler and the Pi is on the same local WiFi (low latency, trivial load). Decision:
|
||||||
|
start with polling, only add Socket.IO if the polling feels laggy.
|
||||||
|
|
||||||
|
4. **Video playback on the phone**: after downloading a dashcam clip to the phone, the
|
||||||
|
app should be able to play it. Android's `ExoPlayer` (Media3) handles H.264 MP4/MOV
|
||||||
|
natively — same codec the dashcam records. No transcoding needed.
|
||||||
|
|
||||||
|
#### Explicitly out of scope
|
||||||
|
|
||||||
|
- **iOS app**: not planned. Android-only. If an iOS user wants the same features, they
|
||||||
|
can use the web UI (with the Phase 11 limitation that they can't do simultaneous
|
||||||
|
dual-network relay — download+upload would be two separate manual steps).
|
||||||
|
- **Play Store listing**: sideload only for now. Lowers the barrier to ship and iterate.
|
||||||
|
- **Background GPS tracking**: the Pi already does this. The app is a display/relay
|
||||||
|
client, not a replacement tracker.
|
||||||
|
- **Push notifications from server**: the Pi's alert engine (Phase 20) handles
|
||||||
|
ntfy-based push. The app doesn't need its own push channel.
|
||||||
|
- **Offline mode when neither Pi nor internet is available**: the app is a connected
|
||||||
|
companion, not a standalone tool. Cached trip data from the last sync is a nice-to-have
|
||||||
|
but not required for v1.
|
||||||
|
|
||||||
## Working Notes / Environment Gotchas
|
## Working Notes / Environment Gotchas
|
||||||
|
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ def _store_synced_config(config):
|
|||||||
app_sql.set_setting("device_avatar_path", config.get("avatar_path", "img/yaris.png"))
|
app_sql.set_setting("device_avatar_path", config.get("avatar_path", "img/yaris.png"))
|
||||||
app_sql.set_setting("device_config_version", str(config.get("config_version", 0)))
|
app_sql.set_setting("device_config_version", str(config.get("config_version", 0)))
|
||||||
app_sql.set_setting("device_config_synced_at", str(int(time.time())))
|
app_sql.set_setting("device_config_synced_at", str(int(time.time())))
|
||||||
if config.get("odometer_baseline") is not None:
|
# odometer_baseline is Pi→Server only — never overwrite the Pi's
|
||||||
app_sql.set_setting("odometer_baseline", str(config["odometer_baseline"]))
|
# local value with whatever the server has (which could be stale).
|
||||||
|
|
||||||
|
|
||||||
def _on_config_changed():
|
def _on_config_changed():
|
||||||
|
|||||||
Reference in New Issue
Block a user