Reflects the completed deployment: server running on unraid, exposed at https://yaris.ceaf.dev via Cloudflare Tunnel, Pi-side upload client live and backlog-drain tuned. Co-authored-by: Copilot <[email protected]>
29 KiB
yaris-obd Revival Plan
This document tracks the plan to revive and finish this project: a Raspberry Pi 3-based OBD-II + GPS car tracker for a 2013 Toyota Yaris, hosting a local web UI over a self-broadcast WiFi hotspot, with dashcam file retrieval and remote upload of itinerary/dashcam data.
Keep this file up to date as phases complete or plans change, so any future session/agent can pick up the project without needing prior chat history.
Hardware
- Board: Raspberry Pi 3 Model B Rev 1.2, 906MB RAM. Confirmed too weak to run an AI coding
agent — all agent-assisted dev work happens on a separate local machine; the Pi is only used
for hardware bring-up/testing/deployment via SSH (
yaris@<pi-ip>). - GPS/GSM: Waveshare SIM868 GSM/GPRS/GNSS/Bluetooth HAT. Also has unused cellular (GSM/GPRS) data capability, earmarked as a fallback internet uplink (see Remote Server phase).
- OBD-II adapter: a cheap unbranded USB adapter using a PL2303 USB-to-serial chip,
presumed ELM327-compatible (works with the
python-obdlibrary). Originally the project used a Bluetooth OBD adapter; this has been switched to USB. - Car: 2013 Toyota Yaris.
SIM868 HAT bring-up notes (important gotchas)
- UART routing is a 3-position jumper: A = USB↔SIM868 (via onboard CP2102), B = Pi GPIO↔SIM868, C = USB↔Pi console. Must be on B for the Pi to talk to the module over its own GPIO UART.
- Pi-side UART setup required:
enable_uart=1anddtoverlay=disable-btin/boot/firmware/config.txt(frees the full PL011 UART —/dev/ttyAMA0//dev/serial0— from the onboard Bluetooth controller),raspi-config nonint do_serial_cons 1(stops a login shell from claiming the serial port), and disable thehciuartsystemd service. - The SIM868 module chip itself needs its PWRKEY button pressed and held (~1-2s) to power on — this is separate from the board's 5V/PWR LED, which can be lit while the module chip is fully off. AT commands get zero response if the module chip isn't powered, even with perfect UART wiring/config. This is a manual step needed on every power cycle unless a P4 GPIO-power-on hardware mod (per Waveshare FAQ) is done — not pursued so far.
- Confirmed AT command baud rate: 115200.
- GNSS workflow:
AT+CGNSPWR?to check GNSS power state,AT+CGNSPWR=1to power on,AT+CGNSINFto poll for a fix. Response format:+CGNSINF: run,fix,utc_time,lat,lon,alt,speed,course,fixmode,reserved1,hdop,pdop,vdop,reserved2,gps_sat_view,gnss_sat_used,gnss_sat_view,cn0,hpa,vpa— field 1 (fix) must be'1'for a valid fix. - The GPS/GNSS antenna is separate from the GSM antenna — both need to be physically connected to their respective u.FL connectors on the HAT. A real fix was obtained only after discovering the GPS antenna wasn't plugged in (only GSM was).
- Confirmed real fix obtained near a window: ~80s cold-start TTFF, 5-7 satellites, HDOP ~1.2-1.6.
Software Architecture Decisions
- Offline maps: Pre-cache/download offline map tiles for the car's usual driving area so
the live map works fully in the car with no internet (not just numeric GPS readout).
Tooling: MapTiler Cloud free tier (raster XYZ tiles,
streets-v2style) — one-time download on the dev machine viascripts/download_map_tiles.py, output shipped to the Pi as static PNGs. Bulk-downloading directly fromtile.openstreetmap.orgwas ruled out — it violates the OSM tile usage policy. Bounding box is a required CLI arg (the user's own driving area, not committed here), zoom 8-15 by default (~14k tiles for a city-plus-suburbs-sized area, well under MapTiler's 100k/month free quota). - WiFi hotspot mode: Auto-switch — the Pi joins home WiFi when in range (for syncing/updates), and falls back to broadcasting its own AP otherwise. Important scope clarification: the hotspot is only for local web-UI viewing on a phone/laptop while driving — it is not part of the upload path. Remote-server uploads happen independently via home WiFi (when in range) or the SIM868 cellular fallback (decision #5) — the phone connecting to the hotspot never relays/carries upload traffic.
- Local/remote persistence: Keep SQLite locally (reliable local write path per trip) + MongoDB Atlas for remote sync when online. Fix existing bugs rather than replace the design.
- Remote server: Self-hosted — a custom server/API (e.g. on a home server or VPS) receives itinerary and dashcam uploads, rather than 3rd-party managed cloud services.
- Remote upload uplink: Use the SIM868's GSM/GPRS cellular connection as a fallback uplink for uploads when no WiFi is available (e.g. while driving), not just "sync when on home WiFi."
Phases
Phase 1: Real GPS integration — ✅ DONE
modules/app_gps.py rewritten to read real SIM868 GNSS data via serial AT commands
(/dev/serial0 @ 115200 baud) instead of hardcoded fake coordinates. Background daemon thread
polls AT+CGNSINF every 2s, parses fix data, emits gps_location via socketio only on an actual
fix. Verified against real hardware (see bring-up notes above).
Phase 2: OBD fixes for USB adapter — ✅ DONE
modules/app_obd.py fixed (commit "Fix app_obd.py bugs and adapt for USB OBD-II adapter",
pushed to origin/shameless-aislop):
connectionis never left undefined on failed connect (was aNameErrorcrash).- Added
_find_obd_port()/connect_obd(): auto-detects/dev/ttyUSB*(the PL2303 adapter), honors anOBD_PORTenv override, falls back to python-obd's own port auto-scan (still covers the old/dev/rfcomm*Bluetooth path). - Fixed stale-response bug:
obd_speed()/obd_rpm()now re-query every loop iteration instead of reusing one response forever. - Fixed
obd_rpm()using the wrong lock, never updating the globalrpm, and emitting to the wrong socketio event (obd_speedinstead ofobd_rpm). - Fixed
kill_obd()being a no-op — now usesthreading.Eventflags actually checked in the polling loops. - Fixed inverted thread-start logic (threads were starting on failed connection, not success).
- Fixed
obm_error→obd_errortypo (frontend listens forobd_error). - Added
obd(python-obd) torequirements.txt. - Still untested: a real connection to the actual car (adapter has only been tested standalone on the Pi so far — correctly reports "no connection to car" with nothing attached).
Phase 3: Web UI must work fully offline — ✅ DONE
The whole point of the project is a Pi hotspot with no internet in the car, but the current UI silently depended on the internet for core functionality:
templates/index.html/dash.html/dtc.htmlloaded Google Fonts, FontAwesome, Leaflet JS/CSS, and the Socket.IO client from public CDNs — none of this loads offline, breaking layout, the websocket connection itself, and the map entirely.static/js/yaris.jsloaded live map tiles fromtile.openstreetmap.org(also needs internet).- A local
static/js/chart.min.jsexisted but was unused —index.htmlloaded Chart.js fromcdn.jsdelivr.netinstead (turned out the local copy was a stale Chart.js v2.8.0 whileyaris.js's config uses v3/v4-stylescales: {x:{...}, y:{...}}syntax — that mismatch is exactly why the CDN was used instead. Replaced the local file with a current v4.5.0 UMD build).
What was done:
- Vendored Socket.IO client (4.8.1, matches Flask-SocketIO 5.6.1's Engine.IO v4 protocol) under
static/vendor/socketio/, and Leaflet (1.9.4 JS/CSS + marker/layers images) understatic/vendor/leaflet/. - FontAwesome 5.12.0 was already vendored locally at
static/fonts/(self-consistent, just never referenced) — repointed all three templates to it instead of the CDN. - Dropped the Google Fonts (Nunito)
<link>entirely rather than self-hosting 12 font-weight files — Bootstrap's--bs-font-sans-serifvariable already falls back cleanly to system fonts (-apple-system, Segoe UI, Roboto, ...) after Nunito, so this is a clean no-cost simplification. - Replaced
static/js/chart.min.jswith a current Chart.js v4.5.0 UMD build (auto-registering, same drop-in behavior as the old CDN<script src="https://cdn.jsdelivr.net/npm/chart.js">). - Offline map tiles: added
scripts/download_map_tiles.py, a dev-machine-only one-time tile downloader using the MapTiler Cloud free tier (raster XYZ,streets-v2style, 256px tiles). ReadsMAPTILER_KEYfrom an env var/CLI arg only — the key itself is never committed or deployed, only the resulting static PNG tiles are. Bounding box is a required CLI arg (your own driving area's coordinates - not committed/hardcoded) at zoom 8-15 (~14k tiles for a city-plus-suburbs-sized area, well under MapTiler's 100k tiles/month free quota). Output goes tostatic/vendor/tiles/{z}/{x}/{y}.png(gitignored — regenerate locally, rsync to the Pi as part of deployment, don't bloat the git repo with binary tile data).- Tiles populated: user signed up for a free MapTiler key and the full zoom 8-15 download
ran to completion — 14,241 downloaded, 24 skipped (out-of-bounds), 0 failed (14,265 tiles
total, ~285MB). Rsynced to the Pi's
static/vendor/tiles/(tile counts verified identical on both sides). Live Pi instance re-confirmed serving HTTP 200 with the complete offline tile set.
- Tiles populated: user signed up for a free MapTiler key and the full zoom 8-15 download
ran to completion — 14,241 downloaded, 24 skipped (out-of-bounds), 0 failed (14,265 tiles
total, ~285MB). Rsynced to the Pi's
- Updated
static/js/yaris.js's Leaflet init to point at/static/vendor/tiles/{z}/{x}/{y}.png(minZoom/maxZoom8/15 to match). The initial map center is just a generic placeholder (not a real location) - it re-centers automatically to the real live GPS fix once one comes in, and the marker is exposed ascarMarkerso Phase 4 can move it on live fixes. - Added a repo-root
.gitignore(Python caches, venvs,.vscode/,static/vendor/tiles/,*.envexcept the already-trackeddb/creds.envplaceholder) — there wasn't one before, which is why stray__pycache__/.pycfiles kept showing up in source control. - Verified: ran the Flask app in a local venv, confirmed all three pages (
/,/dtc,/dash) render with zero external URLs in the served HTML, every local static asset (JS, CSS, fonts, Leaflet images) returns HTTP 200, and all vendored JS files pass a Node.js syntax check.
Phase 4: Map/GPS display bug — ✅ DONE
yaris.js's Leaflet marker (carMarker) was never moved on incoming gps_location socket
events — only two text spans (gpsLocationX/Y) got updated. Fixed: carMarker.setLatLng(...)
is now called on every gps_location event, and the map re-centers (zoom 15) on the first fix
only (mapCentered flag), so the user isn't fighting the map re-centering under them while
panning/zooming during a trip.
Verified: also used this as an opportunity to test Phase 3 end-to-end — ran
scripts/download_map_tiles.py for real with the user's actual MapTiler API key (smoke-tested
zoom 8-10 first, confirmed idempotent re-run skips existing tiles, confirmed a downloaded tile
is a real map image of the target driving area), then started the full zoom 8-15
download. Took a headless Chromium screenshot of the running app's / page and confirmed:
sidebar/icons/fonts render correctly (FontAwesome working), the Speed Graph (Chart.js v4) renders,
and the Leaflet map renders real offline map tiles with the marker in place — full offline
end-to-end proof, not just "no CDN URLs in the HTML".
- Fixed (was noted here as out of scope, now addressed in Phase 8):
/dashand/dtcused to render near-identical body content (both showed "DTC Codes") —dash.htmlnow has real dashcam-specific content. - Full tile download completed (see Phase 3 update above) and rsynced to the Pi — Phase 3/4's offline map is now 100% functional end-to-end on the actual deployed device, not just the dev machine.
Phase 5: Pi deployment / autostart — ✅ DONE
Set up a proper venv (/home/yaris/app/yaris-obd/venv), installed all requirements (all found
as prebuilt ARM wheels via piwheels — no slow source compiles needed, even for gevent).
Added deploy/yaris-obd.service (systemd unit: runs as the yaris user, Restart=on-failure,
5s ExecStartPre sleep to let USB/serial devices enumerate after boot, logs to
yaris-obd.log) and deploy/install_service.sh (idempotent one-time install helper — also
creates the venv if missing). Installed and enabled on the Pi (systemctl enable — survives
reboot).
Also had to fix Pi → GitHub auth for git pull: the repo is private, and the Pi's existing
~/.ssh/id_ed25519.pub turned out to be an inbound SSH key (its private half lives on the dev
machine, not the Pi) — useless for the Pi to authenticate outbound. Generated a dedicated
ed25519 keypair on the Pi itself (~/.ssh/yaris_deploy_key), registered it as a read-only
deploy key on the GitHub repo (least-privilege — the Pi only ever needs to pull), and added an
~/.ssh/config entry so git transparently uses it for github.com. Switched the Pi's origin
remote to SSH. git pull now works standalone on the Pi with no PAT/interactive auth needed.
Verified end-to-end: pulled latest code onto the Pi (was 5 commits stale), confirmed all
Python deps import cleanly, ran the app manually first (HTTP 200, all local static assets —
Leaflet/Socket.IO/Chart.js/FontAwesome — served correctly from the Pi itself), then installed +
started the systemd service and confirmed active (running) + enabled. Tested crash
recovery: sent SIGKILL to the running process, confirmed systemd auto-restarted it (through the
5s ExecStartPre delay) and the app was serving HTTP 200 again within ~10s, with zero manual
intervention.
Phase 6: WiFi hotspot mode — ✅ DONE
Used NetworkManager (already the network manager in use, not hostapd/dnsmasq directly — those
were leftover/unused) since it natively supports AP mode via nmcli. Created two connection
profiles on the Pi: home-wifi (your real home WiFi network, autoconnect-priority 10) and
Hotspot (AP mode, ipv4.method shared, its own dedicated SSID/password set locally on the Pi -
not committed here, WPA2-PSK, autoconnect-priority -10). NM's own autoconnect logic handles
"leaving home WiFi range -> fall back to hotspot" automatically since home-wifi has higher
priority and NM always tries it first.
For the reverse direction (home WiFi back in range while sitting in AP mode) added
deploy/yaris-netwatch.sh + .service/.timer: since the Pi's onboard BCM43438 radio can't be
an AP and background-scan for client networks simultaneously, a systemd timer runs every 3
minutes, briefly attempts reconnecting to home-wifi, and restores the hotspot if home WiFi still
isn't in range. Installed via deploy/install_netwatch.sh.
Root-caused and fixed a real bug during testing: the hotspot's SSID broadcast worked
immediately, but its DHCP server initially failed (dnsmasq: failed to create listening socket for 10.42.0.1: Address already in use) — a pre-existing orphaned system-wide dnsmasq.service
(noted as "likely unused" in earlier investigation) was already bound to the address space
NetworkManager's own internal dnsmasq needs for the hotspot. Confirmed the system dnsmasq's
config was genuinely empty/default (nothing in this project depends on it) and disabled it
(systemctl disable --now dnsmasq).
Also added easy access without needing to know the Pi's IP:
- Renamed the Pi's hostname from
yarisobdtoyaris—avahi-daemon(already installed and running) now publishes it asyaris.localvia mDNS. - Port 80 -> 5000 redirect via nftables (
deploy/nftables-yaris.conf+install_port_redirect.sh, loaded through anincludein/etc/nftables.conf), so the app is reachable at plainhttp://yaris.local/(no port number) instead of:5000.
Verified on real hardware, very carefully since the SSH session used to do all this testing
itself runs over wlan0: used a "dead-man's-switch" pattern for every risky test — a detached
background command (setsid nohup bash -c 'sleep N && nmcli con up home-wifi' & disown) armed
before each hotspot activation, so the Pi automatically reverts to home WiFi (and SSH access
comes back) even if something goes wrong, no physical access needed. Confirmed: hotspot broadcasts
and a real phone gets a genuine DHCP lease from NetworkManager's own DHCP server and can load the
web UI; yaris-netwatch correctly detects AP mode and reconnects to home WiFi when in range (and
is a no-op when already on home WiFi); http://yaris.local/ and http://<pi-lan-ip>/ both return
HTTP 200 with no port number from another machine on the network. (Real hotspot SSID/password and
the Pi's LAN IP are intentionally not recorded in this file — see local, non-committed notes.)
Phase 7: app_db.py / app_sql.py fixes — DONE
Neither file was called from app.py or anywhere else - orphaned from a prior iteration. Fixed:
app_db.py: applied a prior-session stash fix - resolves.envpath via__file__(was cwd-relative), fixesos.getenv()calls to pass the actual key names (was called with zero args, an immediateTypeErrorif ever invoked), added a clearRuntimeErrorguard if credentials are still empty placeholders, returnsclient[mDB].app_sql.py: rewritten - resolvesdb/sql/via__file__+os.makedirs(was cwd-relative), fixedcreate_conn()closing the connection immediately and returningNone(so table creation never ran and every caller got a dead connection), addedget_conn()(lazy singleton- table creation for both
trackinganddtctables),insert_tracking(), and a backgroundstart_logging()/_log_loop()thread (5s interval) that snapshots GPS (app_gps.LocationX/Y) - OBD speed (
app_obd.speed) into the localtrackingtable. Used a deferred (function-local) import ofapp_obdinside_log_loop()to keep the pre-existingapp_obd<->app_sqltop-level circular import safe/explicit.
- table creation for both
- Also fixed a Python 3.12+/3.14 compat bug found during testing:
sqlite3.version(the sqlite3 module's version) was removed/deprecated - switched tosqlite3.sqlite_version(the actual SQLite library version), which is what was intended. - Wired
app_sql.start_logging()intoapp.pyalongside the other startup calls (app_gps.start_gps(),app_obd.OBD()). db/sql/added to.gitignore(per-device local trip data, not source).- Verified locally:
get_conn()createsdb/sql/<account>_<car>.dbwith both tables;insert_tracking()writes a real row;start_logging()thread starts and logs "SQL tracking-logger thread started, alive: True"; fullapp.pyrun confirms the thread starts cleanly and (with no real GPS/OBD hardware on the dev machine) correctly logs read errors instead of crashing;app_db.get_database()raises the new clearRuntimeErrorinstead of crashing, sincedb/creds.envstill holds empty placeholder credentials (real Mongo credentials are a user-provided follow-up, out of scope here).
Phase 8: Dashcam retrieval (app_dash.py) — DONE (tested without physical hardware)
Rewrote app_dash.py to be brand/device-agnostic: detection via lsblk -J, matching partitions
whose parent disk reports TRAN == "usb" (this is what safely distinguishes a plugged-in
dashcam/USB stick from the Pi's own onboard storage, which is always mmc transport - no more
hardcoded /dev/sda). Walks the whole mounted filesystem for video file extensions
(.mp4/.mov/.avi/.ts/.mkv) instead of assuming a specific brand's folder layout (DCIM, MOVIE,
NORM/EVENT/PARKING, etc. all just work), preserves each file's relative sub-folder path when
copying (avoids filename collisions across dashcam event/date folders), and skips files already
copied (same relative path + size) so re-running an import is cheap. Mounts read-only to avoid
any risk of corrupting the dashcam's SD card. Dropped the unused/uninstalled tqdm CLI-progress
dependency (never actually in requirements.txt) since this now runs headless inside the always-
on web server, not interactively.
Wired into app.py: GET /api/dash/devices (list detected USB storage) and
POST /api/dash/import (run the mount->copy->unmount pipeline), both JSON. Fixed the pre-existing
/dash vs /dtc duplicate-template bug — dash.html now has real dashcam content (scan/import
buttons + live status) instead of a copy-pasted "DTC Codes" body. Added a dedicated dash.js
(not the shared yaris.js, which unconditionally initializes the /index-only map/chart and
would throw/crash on /dash and /dtc since those pages don't have those DOM elements - a
pre-existing bug, worked around by not depending on that shared file for this page's new JS).
Added dash/videos/ to .gitignore (imported footage is per-device data, not source).
Tested thoroughly without the physical dashcam (user didn't have it on hand this session):
- Unit-tested the
lsblk-JSON detection/filtering logic against a realistic synthetic sample (Pi's ownmmcblk0boot+root partitions plus a simulated USB dashcam partition) - confirmed it correctly finds only the USB partition and ignores the Pi's own storage. - Integration-tested the real mount -> find-videos -> copy (preserving structure) -> dedupe ->
unmount pipeline on the actual Pi using a loop-device-backed FAT32 image (
mkfs.vfat+losetup) populated with fake nested dashcam video files (DCIM/100EVENT/...,DCIM/100NORMAL/...) - confirmed real mount/copy/unmount all work, confirmed a second import run correctly skips already-copied files, confirmed non-video files (e.g.thumbs.db) are ignored. All simulation artifacts (loop device, test image, temp files) cleaned up afterward. - Verified the full
/dashpage + both new API routes locally (Flask dev run + headless browser check): auto-scan on page load correctly reports "No USB dashcam/storage device detected" (accurate, since none is plugged into the dev machine), Import button correctly stays disabled until a device is found, no JS console errors, no regression on/or/dtc.
Still to verify once the physical dashcam is available: plug it into the Pi, hit "Scan for
dashcam" on /dash, confirm it's detected and its real videos import correctly into
dash/videos/ on the Pi. No code changes should be needed for that - only the transport (loop
device vs. real USB) differs, and the detection/copy logic is transport-agnostic by design.
Phase 9: Remote server for itinerary/dashcam upload — DONE
Per architecture decisions #4 and #5: self-hosted server/API to receive itinerary (from the SQLite pipeline) and dashcam file uploads.
Decided/built: Flask API + dedicated Postgres, deployed as a docker-compose stack on the user's
unraid server (separate from any existing containers/DBs there), living in server/ in this
same repo. Auth: simple X-Device-Name/X-Api-Key header pair per device, key hash stored
server-side, real key never committed (raw key printed once by server/scripts/create_device.py,
saved locally on the Pi only, in gitignored db/upload_creds.env). External access: exposed at
https://yaris.ceaf.dev via the user's existing Cloudflare Tunnel (public-hostname rule added in
the Cloudflare dashboard, pointing at 192.168.10.10:8098) — router port-forward + the user's
Unifi firewall is the documented fallback if ever needed instead. Large dashcam uploads can
still prefer a local-network path (e.g. an AdGuard DNS override to bypass the tunnel while on
home WiFi) since tunnels typically cap per-request upload size — noted in server/README.md,
not yet needed/built.
Built (server/ folder):
server/sql/init.sql— Postgres schema:devices(name + sha256 api-key-hash),tracking_points(device_id FK, GPS/speed/odometer +recorded_at, unique per device+timestamp so retried batches can't duplicate rows),dashcam_files(device_id FK, relative_path + sha256, unique per device+path+hash so re-uploads are no-ops).server/modules/db.py— psycopg3 connection pool + query helpers (bulk tracking insert withON CONFLICT DO NOTHING, dashcam dedupe-check-before-write).server/modules/auth.py—require_device_authdecorator checking the two headers against the stored key hash.server/app.py— Flask app:GET /health(unauthenticated),POST /api/v1/tracking/batch(bulk JSON insert, capped at 5000 points/request as a sanity guard),POST /api/v1/dashcam/upload(multipart, streamed to disk with sha256 verification + path-traversal guard onrelative_path).server/Dockerfile+server/docker-compose.yml—api(gunicorn, published on host port 8098 by default) +db(Postgres 17, internal-only, not published to the LAN) services, data volume defaults toserver/data/but pointed at/mnt/user/appdata/yaris-obd-serveron unraid.server/.env.example— template forPOSTGRES_PASSWORD/API_PORT/DATA_DIR/upload size cap.server/scripts/create_device.py— run once per device inside theapicontainer to register it and print its one-time API key. Ran once for deviceyaris-2013.server/README.md— full unraid deployment + Cloudflare Tunnel exposure + device-registration instructions.
Pi-side upload client (modules/app_upload.py):
- Background thread, uploads unsent
trackingrows (batches of 500) and not-yet-uploaded dashcam files (sha256-verified) via the API above, with exponential retry/backoff on failure. - Independent dashcam-upload-only pause toggle (persisted
settingstable + a switch on the/dashweb UI) — trip/GPS logs always keep uploading regardless of the toggle, per explicit user request (useful on a slow/metered connection). - Backlog-drain tuning: since tracking rows log every 5s locally, an offline/stale device can queue up a large backlog. The loop drains back-to-back (short 1s pause between batches) while a batch comes back full, only settling into the slow 30s idle cadence once actually caught up — a simulated 1250-row backlog now drains in 3 batches (~0.35s) instead of 13 batches over ~6.5 minutes with the original fixed 100-row/30s-sleep design.
modules/app_sql.pyextended:UPLOADEDcolumn + migration ontracking,settingstable,dashcam_uploadsdedupe table, and helper functions backing all of the above.
Tested end-to-end against the real deployed server (tracking batch + dedupe, dashcam upload +
dedupe + path-traversal rejection, bad-API-key rejection, toggle-off correctly pausing only
dashcam uploads) and on the real Pi (its actual ~1800-row pre-feature backlog drained and
confirmed matching row counts on the server via direct psql query, live upload status visible on
/dash). Committed+pushed (4ff99f0, c4db1ab, 8dc304e), deployed on both unraid and the Pi.
Not attempted (future, no action needed unless requested): SIM868 GSM/GPRS cellular fallback
uplink for uploads when no network path to the server is available at all (today uploads need
the Pi to reach yaris.ceaf.dev, e.g. via home WiFi) — remains a possible future enhancement.
Working Notes / Environment Gotchas
- Pi credentials/SSH:
ssh yaris@<pi-ip>. Confirm current IP before connecting (varies by network — check locally, not recorded here) — usually reachable viayaris.localtoo (mDNS, see Phase 6). - Local dev repo: work happens on a separate dev machine, not on the Pi. Push to
origin/shameless-aislop(not yet merged tomain) when work is ready. - Git push auth: if
git pushfails with a credential/auth error, rungh auth login(device-code browser flow) thengh auth setup-gitto wire git to use theghCLI's stored credentials — this fixed push auth issues in this environment without needing SSH keys or PATs. - Pi environment gap: Debian Bookworm blocks system-wide
pip install(PEP 668, "externally-managed-environment"). Use a venv for the real app dependency install (Phase 5) rather than--break-system-packageseverywhere. (Resolved in Phase 5 — venv at/home/yaris/app/yaris-obd/venv.) - Pi → GitHub auth for
git pull(private repo): the Pi's pre-existing~/.ssh/id_ed25519.pubis an inbound key only (private half is on the dev machine) — can't be reused for the Pi's own outbound git auth. Generated a dedicated keypair on the Pi (~/.ssh/yaris_deploy_key), added it as a GitHub read-only deploy key on this repo, and added an~/.ssh/configentry pinninggithub.comto that key.[email protected]:...(SSH remote) then works standalone on the Pi, no PAT/interactive login needed. - piwheels: pip on the Pi automatically uses
piwheels.orgas an extra index (Raspberry Pi OS defaultpip.conf) — this is whygevent(which normally needs a C compile) installed in seconds from a prebuilt ARM wheel instead of taking many minutes to build from source. - python-obd note:
obd.OBD()(or withportstr=) performs the ELM327 handshake at connection time; if there's no car on the other end, it raises rather than silently returning a dead handle —app_obd.py'sconnect_obd()already catches this and setsconnection = None.