323 lines
13 KiB
Markdown
323 lines
13 KiB
Markdown
# Bincio Mobile App — Design Document
|
||
|
||
## Vision
|
||
|
||
The long-term goal is full independence from Garmin Connect, Strava, and similar
|
||
platforms. Today those platforms act as mandatory intermediaries: your device
|
||
syncs to their cloud, you authorise third parties to pull from their API, and your
|
||
data effectively lives on their servers.
|
||
|
||
The Bincio mobile app removes that dependency:
|
||
|
||
- Your FIT/GPX/TCX files live on your device.
|
||
- The app reads them directly — no platform sync required.
|
||
- A Bincio instance (bincio.org or self-hosted) is an optional upgrade for backup,
|
||
sharing, and web access — not a prerequisite.
|
||
- Devices like the **Karoo 2** (Android-based) are a first-class target: activities
|
||
are already saved locally as FIT files, so the app can pick them up directly from
|
||
the filesystem without any export step.
|
||
|
||
---
|
||
|
||
## Philosophy
|
||
|
||
**Local-first.** All activity data lives on the device. The app works fully offline
|
||
— no account, no internet connection, no platform authorisation required.
|
||
|
||
**Original files as source of truth.** The raw FIT/GPX/TCX file is always stored on
|
||
device alongside the extracted BAS JSON. This means:
|
||
|
||
- You can re-extract at any time (e.g. when the algorithm improves, or to apply DEM
|
||
correction after connecting to an instance).
|
||
- Sync to a remote instance is just pushing the original file — the server
|
||
re-extracts with the full Python pipeline.
|
||
- No data is ever locked into a proprietary representation.
|
||
|
||
**The algorithm travels to the data — not the other way around.** When internet is
|
||
available, the app downloads a fresh copy of the extraction algorithm from bincio.org
|
||
and runs it locally. Your activity files never touch the server. Only the Python
|
||
wheel (the code) is downloaded; the data stays on device.
|
||
|
||
**Sync is optional and explicit.** Connecting to a Bincio instance (bincio.org or
|
||
self-hosted) adds cloud backup, the web feed, and the ability to share activities.
|
||
The app never silently overwrites local data. Sync is user-initiated.
|
||
|
||
**Open format.** Activities are stored in the BAS schema (the same JSON format the
|
||
server uses). Any tool — in any language — can read them.
|
||
|
||
---
|
||
|
||
## What already exists
|
||
|
||
Several pieces of the mobile app are already implemented or proven:
|
||
|
||
| Piece | Where | Notes |
|
||
|---|---|---|
|
||
| BAS schema | `docs/schema.md` | The on-device data format — identical to the server format |
|
||
| Pyodide-based extraction | `site/src/pages/convert/` | FIT/GPX/TCX parsing via CPython→WASM running in the browser. **This is the proof of concept for mobile extraction** — a hidden WebView in the app uses the exact same mechanism. |
|
||
| Bincio wheel | served at `/bincio-0.1.0-py3-none-any.whl` | The extraction code packaged as a pure-Python wheel. Already downloaded and run by the `/convert/` page. |
|
||
| Local activity storage | `site/src/pages/convert/` | IndexedDB + service worker in the web app. Proves the concept; the mobile app uses SQLite instead. |
|
||
| Content-addressed dedup | `bincio/extract/dedup.py` | `source_hash` (SHA-256 of raw file) prevents duplicates on upload |
|
||
| Sync-ready REST API | `bincio/serve/server.py` | Login, upload, activity detail, index.json — the sync primitives are already there |
|
||
| Settings persistence | `bincio/serve/db.py` | `settings` table (key/value) for instance URL, auth token, sync preferences |
|
||
|
||
---
|
||
|
||
## Technology choice
|
||
|
||
### Cross-platform framework: Expo (React Native)
|
||
|
||
**Expo** (React Native + the Expo SDK) is the recommended platform:
|
||
|
||
- TypeScript-first, large ecosystem
|
||
- `expo-sqlite` (v2+) — fast on-device SQLite with WAL mode
|
||
- File picking from device storage: `expo-document-picker`
|
||
- Direct filesystem access (critical for Karoo): `expo-file-system`
|
||
- Maps: MapLibre React Native (`@maplibre/maplibre-react-native`) — same tile
|
||
standard as the web app, self-hostable
|
||
- Background tasks: `expo-background-fetch` / `expo-task-manager`
|
||
- Expo Go for rapid development without a build step
|
||
- EAS Build for iOS and Android distribution
|
||
|
||
**Why not alternatives:**
|
||
|
||
| Option | Reason for skipping |
|
||
|---|---|
|
||
| Capacitor + Svelte | WebView performance is poor for map-heavy activity detail; same hidden-WebView trick for Pyodide applies either way |
|
||
| Flutter | Dart is a new language; no practical advantage for this use case |
|
||
| PWA | iOS limits background sync, local storage quotas, and filesystem access — not viable for an activity logger |
|
||
|
||
---
|
||
|
||
## Extraction: Pyodide in a hidden WebView
|
||
|
||
This is the core technical insight. The `/convert/` page already demonstrates that
|
||
the full Python extraction pipeline can run in a browser via **Pyodide** (CPython
|
||
compiled to WebAssembly). A React Native app can host a hidden `WebView` component
|
||
running the exact same code. No rewrite required.
|
||
|
||
### How the /convert/ page does it today
|
||
|
||
```
|
||
Browser tab
|
||
└── Pyodide (CPython → WASM, ~30 MB)
|
||
├── lxml (pre-compiled in Pyodide — XML/GPX parsing)
|
||
├── fitdecode (pure Python — FIT parsing)
|
||
├── gpxpy (pure Python — GPX parsing)
|
||
├── pyyaml (pure Python)
|
||
└── bincio wheel (pure Python — metrics, hysteresis, writers)
|
||
fetched from: /bincio-0.1.0-py3-none-any.whl
|
||
```
|
||
|
||
All dependencies are either pre-compiled in Pyodide or **pure Python with no C
|
||
extensions**. This is the key: there is nothing to recompile for mobile.
|
||
|
||
### How the mobile app does it
|
||
|
||
```
|
||
React Native app
|
||
└── Hidden WebView (WKWebView on iOS, Chrome WebView on Android)
|
||
└── Same Pyodide environment as the /convert/ page
|
||
├── Pyodide runtime (cached on device after first download)
|
||
├── lxml, fitdecode, gpxpy, pyyaml (cached)
|
||
└── bincio wheel (fetched from bincio.org on startup / version check)
|
||
|
||
Data flow:
|
||
1. App reads FIT file bytes from device filesystem
|
||
2. Sends bytes to WebView via postMessage
|
||
3. WebView writes bytes to Pyodide's virtual FS
|
||
4. Python runs the extraction → BAS JSON dict
|
||
5. WebView sends JSON back via postMessage
|
||
6. App stores BAS JSON in SQLite, original file on disk
|
||
```
|
||
|
||
**Data never leaves the device.** The only network traffic is:
|
||
- Pyodide runtime (CDN or bundled, ~30 MB, cached)
|
||
- Common packages (CDN or bundled, cached)
|
||
- The bincio wheel from bincio.org (~50 KB, updated on version bump)
|
||
|
||
### Algorithm updates without app store releases
|
||
|
||
The bincio wheel is versioned and served from bincio.org. On app startup (or
|
||
periodically), the app checks the current wheel version:
|
||
|
||
```
|
||
GET https://bincio.org/bincio-latest.whl (or a version manifest endpoint)
|
||
```
|
||
|
||
If a new version is available, the wheel is downloaded and cached. The next
|
||
extraction uses the updated algorithm. Improvements to hysteresis thresholds,
|
||
DEM correction, lap detection, or any other metric are live on all devices
|
||
within hours of deployment — **no App Store submission required**.
|
||
|
||
### Performance
|
||
|
||
- **First extraction after install**: ~5–8 s (Pyodide startup + package load)
|
||
- **Subsequent extractions (warm WebView)**: ~1–3 s per activity
|
||
- **Pyodide memory footprint**: ~100–150 MB RAM while active; the WebView can
|
||
be suspended between extractions
|
||
- **Wheel size**: the bincio extract code is ~50 KB; Pyodide + packages ~30 MB
|
||
(downloaded once, cached on device)
|
||
|
||
For batch import (many files at once), the WebView is kept warm across
|
||
extractions, making the per-file cost just the Python execution time (~0.5–1 s
|
||
per typical activity).
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
```
|
||
Bincio Mobile
|
||
├── UI Layer (React Native / Expo)
|
||
│ ├── Feed screen — list of local activities, sorted by date
|
||
│ ├── Activity detail — map + elevation chart + stats
|
||
│ ├── Import screen — pick FIT/GPX/TCX from device or share sheet
|
||
│ ├── Sync screen — configure instance URL, push/pull
|
||
│ └── Settings screen — account, preferences, storage info
|
||
│
|
||
├── Extraction Engine (Pyodide in hidden WebView)
|
||
│ ├── WebView host — manages lifecycle, message passing
|
||
│ ├── Wheel cache — versioned bincio wheel stored on device
|
||
│ └── Python runtime — Pyodide + fitdecode + gpxpy + lxml
|
||
│ identical to the /convert/ page on the web
|
||
│
|
||
├── Local Store (expo-sqlite)
|
||
│ ├── activities — BAS detail JSON + indexed summary columns
|
||
│ ├── timeseries — 1 Hz arrays as JSON blob per activity
|
||
│ ├── geojson — simplified GPS track per activity
|
||
│ ├── originals — original file paths per activity
|
||
│ └── settings — instance_url, handle, auth_token, sync prefs
|
||
│
|
||
└── Sync Layer (optional)
|
||
├── Auth — POST /api/auth/login → Bearer token
|
||
├── Push — POST /api/upload (original file)
|
||
└── Pull — GET index.json + activity/{id}.json + timeseries
|
||
```
|
||
|
||
---
|
||
|
||
## Data model on device
|
||
|
||
```sql
|
||
-- activities table
|
||
id TEXT PRIMARY KEY, -- BAS ID: "2026-04-17T074238Z"
|
||
source_hash TEXT NOT NULL, -- SHA-256 of original file (dedup key)
|
||
detail_json TEXT NOT NULL, -- full BAS detail JSON blob
|
||
timeseries_json TEXT, -- 1 Hz arrays (loaded lazily)
|
||
geojson TEXT, -- simplified GPS track
|
||
original_path TEXT NOT NULL, -- path to original file in app storage
|
||
synced_at INTEGER, -- unix timestamp of last push to remote
|
||
origin TEXT NOT NULL, -- "local" | "remote"
|
||
created_at INTEGER NOT NULL
|
||
|
||
-- settings table
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
```
|
||
|
||
**Settings keys:**
|
||
|
||
| Key | Example value |
|
||
|---|---|
|
||
| `instance_url` | `https://bincio.org` |
|
||
| `handle` | `brutsalvadi` |
|
||
| `session_token` | `abc123…` |
|
||
| `last_sync_at` | `2026-04-24T10:00:00Z` |
|
||
| `wheel_version` | `0.1.0` |
|
||
| `auto_import_path` | `/sdcard/Karoo/Rides/` (Android only) |
|
||
|
||
---
|
||
|
||
## Karoo and Android-first devices
|
||
|
||
Devices like the **Karoo 2** run Android and write FIT files directly to the
|
||
filesystem (e.g. `/sdcard/Karoo/Rides/`). The app can monitor this directory and
|
||
auto-import new files as rides complete — no manual export step, no Hammerhead
|
||
cloud sync, no Garmin Connect, no Strava required.
|
||
|
||
On Karoo specifically:
|
||
- Install the Bincio Android APK directly (sideload or via a store).
|
||
- Configure `auto_import_path` to point at the Karoo's ride directory.
|
||
- When a new FIT file appears, the app imports it automatically (Pyodide
|
||
extraction), stores the original file, and shows the ride in the feed.
|
||
- When WiFi is available and an instance is configured, rides can be pushed to
|
||
the instance for web access and backup.
|
||
|
||
This makes Bincio a complete replacement for Hammerhead's own sync infrastructure
|
||
for users who want full control of their data.
|
||
|
||
---
|
||
|
||
## Sync protocol
|
||
|
||
Sync is a two-way, hash-based diff — no custom server protocol needed.
|
||
|
||
### Push (local → server)
|
||
|
||
1. Fetch `{instance_url}/{handle}/index.json` to get remote activity IDs.
|
||
2. Find local activities where `synced_at IS NULL`.
|
||
3. For each unsynced activity, `POST /api/upload` with the original file.
|
||
4. On 200, set `synced_at = now()`.
|
||
|
||
### Pull (server → local)
|
||
|
||
1. Fetch `{instance_url}/{handle}/index.json` (and yearly shards).
|
||
2. Find remote IDs not in the local DB.
|
||
3. For each missing activity:
|
||
- `GET {instance_url}/activities/{id}.json` → `detail_json`
|
||
- `GET {instance_url}/activities/{id}.timeseries.json` → `timeseries_json`
|
||
- `GET {instance_url}/activities/{id}.geojson` → `geojson`
|
||
4. Insert with `origin = "remote"`, `synced_at = now()`.
|
||
|
||
Note: pulled activities don't have a local original file. If re-extraction is
|
||
needed (e.g. for a DEM correction), the original must be uploaded to the instance
|
||
first so the server can serve it back.
|
||
|
||
### Conflict handling
|
||
|
||
Activities are immutable once created. The `source_hash` prevents double-counting:
|
||
if the same file is imported on two devices before sync, whichever arrives at the
|
||
server first wins; the duplicate is rejected with 409.
|
||
|
||
---
|
||
|
||
## Authentication
|
||
|
||
The multi-user server currently uses HTTP session cookies. For the mobile client,
|
||
a **Bearer token** is cleaner:
|
||
|
||
```
|
||
POST /api/auth/token
|
||
{ "handle": "…", "password": "…" }
|
||
→ { "token": "abc123…", "expires_at": "…" }
|
||
```
|
||
|
||
The token is stored in the `settings` table and sent as
|
||
`Authorization: Bearer abc123…` on all subsequent requests.
|
||
|
||
---
|
||
|
||
## What is out of scope for v1
|
||
|
||
- **Live activity recording** — GPS + sensor recording during a ride. This is a
|
||
much harder problem (background GPS, Bluetooth/ANT+ sensors, real-time display)
|
||
and is the eventual goal for full platform independence.
|
||
- **Offline map tiles** — v1 requires network for map rendering.
|
||
- **Photo sync** — deferred.
|
||
- **Watch / ANT+ / Bluetooth sensors** — deferred.
|
||
- **Editing activities on mobile** — read-only in v1; edits happen on the web.
|
||
|
||
---
|
||
|
||
## Roadmap
|
||
|
||
| Phase | Scope |
|
||
|---|---|
|
||
| **0 — Foundation** | Expo project scaffold, SQLite store, settings screen, file picker, display a BAS JSON read from disk |
|
||
| **1 — Import** | Hidden WebView + Pyodide extraction, wheel download and caching, local feed, activity detail with map and chart, original file storage |
|
||
| **2 — Karoo integration** | Auto-import from a watched directory, Android-specific filesystem access |
|
||
| **3 — Sync** | Bearer token auth, push/pull sync with a Bincio instance |
|
||
| **4 — Polish** | Offline map tiles, share sheet, home screen widget, batch import performance |
|
||
| **Future** | Live recording, Bluetooth/ANT+ sensors, full Garmin/Wahoo/Hammerhead replacement |
|