# 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 |