implement: Phase 25.5 device config from JSON file
- Create config/device.json with owner_name, car_model, device_name, avatar_path - Update app_account.py to load owner_name and avatar_path from config - Update app_car.py to load car_model and device_name from config - Update base.html template to use avatar_path from config instead of hardcoded - Add graceful fallback to hardcoded defaults if config file missing - Document in deploy/readme.md and readme.md how to customize device identity - Update PLAN.md Phase 25.5 to reflect current implementation and future server sync Benefits: - No code edits needed to customize Pi (edit JSON file only) - Offline-first: all values loaded from local file at startup - Connection-driven sync planned for Phase 25.5a (sync when internet available, not on timer) - Server becomes source of truth after Phase 25.5a implementation All 63 tests passing. Database filename still based on owner_name, with migration warning in docs. Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -1482,44 +1482,44 @@ must be fixed, redeployed uncommitted, and the affected in-car test repeated bef
|
||||
and **ready-to-install** sign-off. Afterward, the owner handles permanent installation outside the
|
||||
phase roadmap.
|
||||
|
||||
### Phase 25.5: Device profile & configuration sync — 🚧 PLANNED (local config mgmt)
|
||||
**Unstarted.** Replace hardcoded `account.name`, `car.model`, and avatar with managed configuration editable on the server and synced to the Pi. Enables multi-device/multi-user scenarios and eliminates manual code edits.
|
||||
### Phase 25.5: Device profile & configuration sync — 🚧 IN PROGRESS (config file + server sync planning)
|
||||
**Config file implemented; server sync planned.** Replace hardcoded values with `config/device.json`, editable by users without code changes. Server sync is eventual-consistency, connection-driven (not time-driven) to handle offline-first car scenarios.
|
||||
|
||||
#### Scope
|
||||
#### Current state (implemented)
|
||||
- **`config/device.json`** (new): stores device identity fields:
|
||||
- `owner_name`: "quoije" (displayed in topbar, replaces `account.name` hardcode)
|
||||
- `car_model`: "Yaris 2013" (displayed in sidebar, replaces `car.model` hardcode)
|
||||
- `device_name`: "Yaris OBD" (displayed on portal, not yet used on Pi)
|
||||
- `avatar_path`: "img/yaris.png" (sourced by template, replaces hardcoded path)
|
||||
- **Code changes**:
|
||||
- `modules/app_account.py`: loads `owner_name`, `avatar_path` from JSON on startup with fallback to hardcoded defaults
|
||||
- `modules/app_car.py`: loads `car_model`, `device_name` from JSON on startup with fallback to hardcoded defaults
|
||||
- `templates/base.html`: avatar image now sources from `account.avatar_path` (loaded from config) instead of hardcoded path
|
||||
- **Offline-first design**: All values loaded from local JSON at startup; no network required to boot with customized identity. Server sync is *eventual*, not critical.
|
||||
|
||||
#### Future scope (Phase 25.5a+)
|
||||
- **Server-side config editor**: add a simple "Device Settings" page on the remote portal (login-protected, device-scoped) with editable fields:
|
||||
- Device nickname (device_name, for display on the portal and Pi topbar)
|
||||
- Car model/year (already in DB via Phase 24's `device_id` + `car_model`, but allow user override)
|
||||
- Profile picture upload (stored server-side, served to both portal and Pi via API endpoint)
|
||||
- Owner name / username (replaces hardcoded `account.name`)
|
||||
- **Pi-side config sync**:
|
||||
- Add a new `device_config` table to local SQLite (device_name, owner_name, car_model, avatar_url, sync_timestamp, hash)
|
||||
- On Pi startup and every 30 minutes, fetch the latest config from the remote server via a new `/api/device-config` endpoint (authenticated with device_key, not user login)
|
||||
- If hash differs, update local config table and restart the web app (or hot-reload via settings API)
|
||||
- Fall back gracefully to hardcoded defaults if server is unreachable
|
||||
- **UI updates**:
|
||||
- Pi topbar shows the synced `device_name` instead of hardcoded car model (or car model from synced config)
|
||||
- Pi sidebar shows the synced owner_name instead of hardcoded `account.name`
|
||||
- Avatar image sourced from synced `avatar_url` (served from server, fallback to local placeholder)
|
||||
- All trip/tracking records use synced values at capture time (no retroactive changes to history)
|
||||
- **Device authentication**: use an existing `device_key` (generated on first Pi provisioning, stored in `/home/yaris/app/yaris-obd/.device-key` or env var) to authenticate the `/api/device-config` request. Server validates device_key against the Postgres `devices` table.
|
||||
|
||||
#### Implementation phases
|
||||
1. **Server (Phase 25.5a)**: add `device_name`, `owner_name`, `avatar_blob` columns to Postgres `devices` table; create `/api/device-config` read endpoint and `/api/device-config` POST endpoint for edits; add Settings UI to portal for these fields.
|
||||
2. **Pi config table & sync (Phase 25.5b)**: add `device_config` SQLite table; implement 30-min sync fetch on Pi; hot-update app globals if config changes.
|
||||
3. **UI updates (Phase 25.5c)**: update topbar, sidebar, avatar rendering to use synced config; test full cycle (edit on server → Pi syncs → UI reflects change).
|
||||
4. **Acceptance (Phase 25.5d)**: multi-sync cycle test, network loss recovery, avatar upload/display, retroactive trip behavior.
|
||||
- Device nickname (device_name)
|
||||
- Car model/year (car_model)
|
||||
- Profile picture upload (avatar stored on server)
|
||||
- Owner name / username (owner_name)
|
||||
- **Pi-side sync** (connectivity-driven, not time-driven):
|
||||
- Pi syncs config from server **only when internet is available** (when cellular/WiFi connects), not on a fixed 30-min timer
|
||||
- Server-side changes → Pi receives on next connectivity event → updates `config/device.json` → restarts web app or hot-reloads globals
|
||||
- If server unreachable, Pi continues with last-synced config from local JSON (already loaded at startup)
|
||||
- **Device authentication**: use an existing `device_key` to authenticate `/api/device-config` requests, validated server-side against Postgres `devices` table.
|
||||
|
||||
#### Benefits
|
||||
- No code edit needed to rebrand a deployed Pi
|
||||
- Server becomes the source of truth for device identity (enables future multi-Pi/multi-car scenarios)
|
||||
- Avatar management centralized
|
||||
- Graceful fallback if server unavailable (Pi uses last-synced config)
|
||||
- Enables future features: device rename, device status on portal, bulk config across devices
|
||||
- ✅ No code edit needed to customize deployed Pi (today: edit JSON file manually)
|
||||
- ✅ Offline-first: Pi boots with identity immediately, no network dependency
|
||||
- ✅ Connection-driven sync: sync happens when internet available, not wasting resources on offline car
|
||||
- 🚧 Server becomes source of truth (after Phase 25.5a)
|
||||
- 🚧 Enables multi-Pi/multi-car scenarios (after Phase 25.5a)
|
||||
|
||||
#### Trade-offs
|
||||
- Adds config sync complexity and latency (eventual consistency)
|
||||
- Requires device authentication key rotation strategy (future)
|
||||
- Metadata storage (avatar blob) requires server-side image handling
|
||||
- Sync is eventual-consistency (changes on server may take hours to reach offline car)
|
||||
- Manual JSON edit required today (before Phase 25.5a server UI is built)
|
||||
- Avatar must be placed in `static/img/` directory (no upload yet)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"owner_name": "quoije",
|
||||
"car_model": "Yaris 2013",
|
||||
"device_name": "Yaris OBD",
|
||||
"avatar_path": "img/yaris.png"
|
||||
}
|
||||
@@ -179,6 +179,40 @@ Then test from another device:
|
||||
- `http://yaris.local`
|
||||
- if mDNS is not available on that client, try `http://10.42.0.1`
|
||||
|
||||
## Customizing device identity
|
||||
|
||||
The Pi displays a username, car model, device name, and avatar image. Instead of editing Python code, edit `config/device.json`:
|
||||
|
||||
```bash
|
||||
nano /home/yaris/app/yaris-obd/config/device.json
|
||||
```
|
||||
|
||||
**Example `config/device.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"owner_name": "Your Name",
|
||||
"car_model": "Toyota Yaris 2013",
|
||||
"device_name": "My Yaris OBD",
|
||||
"avatar_path": "img/yaris.png"
|
||||
}
|
||||
```
|
||||
|
||||
- **`owner_name`**: Displayed in topbar (top-right corner of the web UI)
|
||||
- **`car_model`**: Displayed in sidebar (left panel, appears next to car icon)
|
||||
- **`device_name`**: Reserved for server-side display (Phase 25.5 feature, not yet used)
|
||||
- **`avatar_path`**: Avatar image shown in topbar; must exist in `static/img/` (replace `static/img/yaris.png` with your own image, or point to a different file)
|
||||
|
||||
After editing, restart the app:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart yaris-obd.service
|
||||
```
|
||||
|
||||
**Important**: The SQLite database filename is based on the username. If you change `owner_name` and restart, a new database will be created and the old data will be orphaned at `db/sql/quoije_Yaris2013.db`. Either:
|
||||
- Rename the old database file to match the new username, or
|
||||
- Delete the old database if you want a fresh start
|
||||
|
||||
## 7) Updating later
|
||||
|
||||
```bash
|
||||
|
||||
+18
-1
@@ -1,5 +1,22 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
class Account:
|
||||
def __init__(self):
|
||||
self.name = "quoije"
|
||||
# Load from config/device.json if available, else use defaults
|
||||
config = self._load_config()
|
||||
self.name = config.get("owner_name", "quoije")
|
||||
self.avatar_path = config.get("avatar_path", "img/yaris.png")
|
||||
self.pp_image = None
|
||||
self.car = None
|
||||
|
||||
def _load_config(self):
|
||||
"""Load device config from JSON file, return dict with defaults."""
|
||||
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "device.json")
|
||||
try:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r') as f:
|
||||
return json.load(f)
|
||||
except (IOError, json.JSONDecodeError):
|
||||
pass
|
||||
return {}
|
||||
+18
-1
@@ -1,4 +1,21 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
class Car:
|
||||
def __init__(self):
|
||||
self.model = "Yaris 2013"
|
||||
# Load from config/device.json if available, else use defaults
|
||||
config = self._load_config()
|
||||
self.model = config.get("car_model", "Yaris 2013")
|
||||
self.device_name = config.get("device_name", "Yaris OBD")
|
||||
self.obd = None
|
||||
|
||||
def _load_config(self):
|
||||
"""Load device config from JSON file, return dict with defaults."""
|
||||
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "device.json")
|
||||
try:
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r') as f:
|
||||
return json.load(f)
|
||||
except (IOError, json.JSONDecodeError):
|
||||
pass
|
||||
return {}
|
||||
@@ -114,33 +114,70 @@ in [deploy/](./deploy). For a clean-install walkthrough on a fresh Pi, use
|
||||
|
||||
For the remote server (receives trip logs + dashcam uploads), see [server/README.md](./server/README.md).
|
||||
|
||||
## Customizing username and profile picture
|
||||
## Customizing device identity
|
||||
|
||||
The top-right username and avatar in the local app are currently code-configured.
|
||||
The Pi displays a username, car model, and avatar image. All customizable values are stored in `config/device.json` (no code edits needed).
|
||||
|
||||
### Change username
|
||||
### Edit device configuration
|
||||
|
||||
Edit [modules/app_account.py](./modules/app_account.py):
|
||||
Edit [config/device.json](./config/device.json):
|
||||
|
||||
```python
|
||||
self.name = "quoije"
|
||||
```json
|
||||
{
|
||||
"owner_name": "quoije",
|
||||
"car_model": "Yaris 2013",
|
||||
"device_name": "Yaris OBD",
|
||||
"avatar_path": "img/yaris.png"
|
||||
}
|
||||
```
|
||||
|
||||
Set it to your preferred name, then restart the app/service.
|
||||
| Field | Location | Purpose |
|
||||
|-------|----------|---------|
|
||||
| `owner_name` | Top-right of app topbar | Your name/username |
|
||||
| `car_model` | Left sidebar (next to car icon) | Vehicle model/year |
|
||||
| `device_name` | (Reserved) | Future server-side use in Phase 25.5 |
|
||||
| `avatar_path` | Top-right avatar circle | Profile picture (relative to `static/`) |
|
||||
|
||||
> Important: `account.name` is also used in the local SQLite filename (`db/sql/<name>_<car>.db` via [modules/app_sql.py](./modules/app_sql.py)).
|
||||
> If you already have trip data and change the name, the app will create a new DB file unless you rename/migrate the existing one.
|
||||
After editing, restart the app:
|
||||
|
||||
### Change profile picture
|
||||
```bash
|
||||
# If running as a systemd service:
|
||||
sudo systemctl restart yaris-obd.service
|
||||
|
||||
Current avatar is hardcoded in [templates/base.html](./templates/base.html) to:
|
||||
|
||||
```html
|
||||
src="{{ url_for('static', filename='img/yaris.png') }}"
|
||||
# If running manually:
|
||||
# Ctrl+C and python3 app.py
|
||||
```
|
||||
|
||||
Simplest option: replace [static/img/yaris.png](./static/img/yaris.png) with your image (same filename/path).
|
||||
Alternative: edit the `filename='img/yaris.png'` value in [templates/base.html](./templates/base.html).
|
||||
### Changing the profile picture
|
||||
|
||||
Option 1: Replace the default image file:
|
||||
```bash
|
||||
cp /path/to/your/image.png static/img/yaris.png
|
||||
```
|
||||
|
||||
Option 2: Add your image to `static/img/` and update `avatar_path`:
|
||||
```json
|
||||
{
|
||||
"avatar_path": "img/my-avatar.png"
|
||||
}
|
||||
```
|
||||
|
||||
### Important: Username and database filename
|
||||
|
||||
The local SQLite database filename is based on `owner_name`: `db/sql/<owner_name>_<car_model>.db` (via [modules/app_sql.py](./modules/app_sql.py)).
|
||||
|
||||
If you **change `owner_name`** and already have trip data:
|
||||
- The app will create a **new database** with the new name
|
||||
- Your old database remains at `db/sql/quoije_Yaris2013.db` (orphaned)
|
||||
|
||||
To preserve your data:
|
||||
```bash
|
||||
# Option 1: Rename the database file
|
||||
mv db/sql/quoije_Yaris2013.db "db/sql/YourName_YourCar.db"
|
||||
|
||||
# Option 2: Start fresh
|
||||
rm db/sql/quoije_Yaris2013.db
|
||||
```
|
||||
|
||||
## Project status
|
||||
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@
|
||||
</div>
|
||||
<span class="text-muted small d-none d-sm-inline">{{ account.name }}</span>
|
||||
<img class="rounded-circle border" style="width:2rem;height:2rem;object-fit:cover;"
|
||||
src="{{ url_for('static', filename='img/yaris.png') }}" alt="">
|
||||
src="{{ url_for('static', filename=account.avatar_path) }}" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user