From 60a1c1811e622fa9898c3e536db0041f18934c9b Mon Sep 17 00:00:00 2001 From: Mikkeli Matlock Date: Mon, 26 Jan 2026 11:35:17 +0900 Subject: [PATCH] Flask backend and ui tweaks --- .gitignore | 1 + extra/themes/rei.json | 2 +- pi/ui/lib/screens/dashboard_screen.dart | 36 ++++-- pi/ui/lib/screens/splash_screen.dart | 5 +- pi/ui/lib/services/test_flipflop_service.dart | 2 +- pi/ui/lib/widgets/stat_box.dart | 35 +++--- scripts/README.md | 39 ++++-- scripts/deploy_backend.py | 117 ++++++++++++++++++ scripts/deploy_target.sample.json | 8 +- scripts/smartserow-backend.service.sample | 19 +++ 10 files changed, 221 insertions(+), 43 deletions(-) create mode 100644 scripts/deploy_backend.py create mode 100644 scripts/smartserow-backend.service.sample diff --git a/.gitignore b/.gitignore index 31d6fac..8348934 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ pi_sysroot/ # personal deploy target info scripts/deploy_target.json scripts/smartserow-ui.service +scripts/smartserow-backend.service # script python artifacts scripts/__pycache__/ diff --git a/extra/themes/rei.json b/extra/themes/rei.json index 7505fec..062fff4 100644 --- a/extra/themes/rei.json +++ b/extra/themes/rei.json @@ -3,7 +3,7 @@ "background": "#404040", "foreground": "#EAEAEA", "highlight": "#FA1504", - "subdued": "#E47841" + "subdued": "#fda052" }, "bright": { "background": "#fda052", diff --git a/pi/ui/lib/screens/dashboard_screen.dart b/pi/ui/lib/screens/dashboard_screen.dart index 70eff1f..7e26a4a 100644 --- a/pi/ui/lib/screens/dashboard_screen.dart +++ b/pi/ui/lib/screens/dashboard_screen.dart @@ -76,21 +76,31 @@ class _DashboardScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Text( - 'CHASSIS VOLTAGE ', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontSize: 80, - color: theme.subdued, - letterSpacing: 1, + Expanded( + flex: 3, + child: Container(), + ), + Expanded( + flex: 3, + child: Text( + 'Chassis voltage ', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontSize: 60, + color: theme.subdued, + letterSpacing: 1, + ), ), ), - Text( - '${_voltage.toStringAsFixed(1)}V', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontSize: 80, - color: _voltage < 11.9 ? theme.highlight : theme.foreground, + Expanded( + flex: 1, + child: Text( + '${_voltage.toStringAsFixed(1)}V', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontSize: 80, + color: _voltage < 11.9 ? theme.highlight : theme.foreground, + ), ), - ), + ) ], ), @@ -125,7 +135,7 @@ class _DashboardScreenState extends State { // Bottom stats row Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ StatBox(label: 'RPM', value: _rpm.toString()), StatBox(label: 'ENG', value: '$_temp°C'), diff --git a/pi/ui/lib/screens/splash_screen.dart b/pi/ui/lib/screens/splash_screen.dart index 9be01fe..4cf541c 100644 --- a/pi/ui/lib/screens/splash_screen.dart +++ b/pi/ui/lib/screens/splash_screen.dart @@ -20,13 +20,15 @@ class SplashScreen extends StatelessWidget { children: [ Icon( Icons.terrain, - size: 120, + size: 240, color: theme.subdued, + // replace with custom logo later ), const SizedBox(height: 24), Text( 'Smart Serow', style: Theme.of(context).textTheme.headlineLarge?.copyWith( + fontSize: 160, color: theme.foreground, fontWeight: FontWeight.bold, ), @@ -35,6 +37,7 @@ class SplashScreen extends StatelessWidget { Text( status, style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontSize: 80, color: theme.subdued, ), ), diff --git a/pi/ui/lib/services/test_flipflop_service.dart b/pi/ui/lib/services/test_flipflop_service.dart index 0a1f607..c68f6cb 100644 --- a/pi/ui/lib/services/test_flipflop_service.dart +++ b/pi/ui/lib/services/test_flipflop_service.dart @@ -30,7 +30,7 @@ class TestFlipFlopService { _timer = Timer.periodic(const Duration(seconds: 2), (_) { // Toggle theme - ThemeService.instance.toggle(); + // ThemeService.instance.toggle(); // Surprise the navigator if (navigatorKey.currentState?.emotion == 'surprise') { diff --git a/pi/ui/lib/widgets/stat_box.dart b/pi/ui/lib/widgets/stat_box.dart index a9b520e..5cd23cc 100644 --- a/pi/ui/lib/widgets/stat_box.dart +++ b/pi/ui/lib/widgets/stat_box.dart @@ -13,24 +13,27 @@ class StatBox extends StatelessWidget { Widget build(BuildContext context) { final theme = AppTheme.of(context); - return Column( - children: [ - Text( - value, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontSize: 100, - color: theme.foreground, + return Expanded( + flex: 1, + child: Column( + children: [ + Text( + value, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontSize: 100, + color: theme.foreground, + ), ), - ), - Text( - label, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 80, - color: theme.subdued, - letterSpacing: 1, + Text( + label, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 80, + color: theme.subdued, + letterSpacing: 1, + ), ), - ), - ], + ], + ) ); } } diff --git a/scripts/README.md b/scripts/README.md index 1a5240f..1ed8b24 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -2,24 +2,34 @@ Build, deploy, and setup helpers for the Smart Serow project. -## Build & Deploy +## UI Build & Deploy | Script | Purpose | |--------|---------| | `build.py` | Cross-compile Flutter app for ARM64. Runs `generate_theme.py` first. | -| `deploy.py` | rsync bundle to Pi, optionally restart service | +| `deploy.py` | rsync UI bundle to Pi, optionally restart service | | `build-deploy.py` | Convenience wrapper: build → deploy → restart | ```bash -# Typical workflow python3 build.py # Build only python3 deploy.py --restart # Deploy and restart service python3 build-deploy.py # All-in-one - -# Clean rebuild (clears CMake cache) -python3 build.py --clean +python3 build.py --clean # Clean rebuild ``` +## Backend Deploy + +| Script | Purpose | +|--------|---------| +| `deploy_backend.py` | rsync Python backend to Pi, optionally restart service | + +```bash +python3 deploy_backend.py # Deploy only +python3 deploy_backend.py --restart # Deploy and restart service +``` + +Backend and UI are **completely independent** — separate paths, separate services, separate deploys. + ## Theme Generation | Script | Purpose | @@ -32,13 +42,19 @@ Called automatically by `build.py`. Looks for theme matching `navigator` in `con | Script | Purpose | |--------|---------| -| `pi_setup.sh` | First-time Pi configuration (deps, permissions, systemd service) | -| `smartserow-ui.service.sample` | Systemd unit file template | +| `pi_setup.sh` | First-time Pi config (deps, permissions, UI systemd service) | +| `smartserow-ui.service.sample` | UI systemd unit template | +| `smartserow-backend.service.sample` | Backend systemd unit template | ```bash -# On the Pi +# On the Pi (UI) chmod +x pi_setup.sh ./pi_setup.sh + +# Backend service (manual for now) +sudo cp smartserow-backend.service.sample /etc/systemd/system/smartserow-backend.service +sudo systemctl daemon-reload +sudo systemctl enable smartserow-backend ``` ## Configuration @@ -53,7 +69,10 @@ chmod +x pi_setup.sh "user": "pi", "host": "raspberrypi.local", "remote_path": "/opt/smartserow", - "service_name": "smartserow-ui" + "service_name": "smartserow-ui", + "assets_path": "~/smartserow-ui/assets", + "backend_path": "/opt/smartserow-backend", + "backend_service": "smartserow-backend" } ``` diff --git a/scripts/deploy_backend.py b/scripts/deploy_backend.py new file mode 100644 index 0000000..de7f1d4 --- /dev/null +++ b/scripts/deploy_backend.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Deploy script for Smart Serow Python backend. + +Pushes backend source to Pi and optionally restarts service. +Completely independent from UI deploy. +""" + +import argparse +import json +import subprocess +import sys +import time +from pathlib import Path + + +SCRIPT_DIR = Path(__file__).parent.resolve() +PROJECT_ROOT = SCRIPT_DIR.parent +CONFIG_FILE = SCRIPT_DIR / "deploy_target.json" +BACKEND_DIR = PROJECT_ROOT / "pi" / "backend" + + +def run(cmd: list[str], check: bool = True, **kwargs) -> subprocess.CompletedProcess: + """Run a command.""" + print(f" → {' '.join(cmd)}") + return subprocess.run(cmd, check=check, **kwargs) + + +def load_config() -> dict: + """Load deploy target configuration.""" + if not CONFIG_FILE.exists(): + print(f"ERROR: Config file not found: {CONFIG_FILE}") + print("Create it based on deploy_target.sample.json") + sys.exit(1) + + with open(CONFIG_FILE) as f: + return json.load(f) + + +def deploy(restart: bool = False) -> bool: + """Deploy backend to Pi. Returns True on success.""" + config = load_config() + + pi_user = config["user"] + pi_host = config["host"] + + # Backend-specific config (with defaults) + remote_path = config.get("backend_path", "/opt/smartserow-backend") + service_name = config.get("backend_service", "smartserow-backend") + + ssh_target = f"{pi_user}@{pi_host}" + + print("=== Smart Serow Backend Deploy ===") + print(f"Target: {ssh_target}:{remote_path}") + print(f"Source: {BACKEND_DIR}") + + if not BACKEND_DIR.exists(): + print(f"ERROR: Backend directory not found: {BACKEND_DIR}") + return False + + # Ensure remote directory exists + print() + print("Ensuring remote directory...") + run(["ssh", ssh_target, f"mkdir -p {remote_path}"]) + + # Sync backend source to Pi + # Exclude __pycache__, .venv, etc. + print() + print("Syncing files...") + run([ + "rsync", "-avz", "--delete", + "--exclude", "__pycache__", + "--exclude", "*.pyc", + "--exclude", ".venv", + "--exclude", ".ruff_cache", + f"{BACKEND_DIR}/", + f"{ssh_target}:{remote_path}/", + ]) + + # Restart service if requested + if restart: + print() + print(f"Restarting service: {service_name}") + run(["ssh", ssh_target, f"sudo systemctl restart {service_name}"], check=False) + time.sleep(2) + run(["ssh", ssh_target, f"systemctl status {service_name} --no-pager"], check=False) + else: + print() + print("Deploy complete. To restart service, run:") + print(f" ssh {ssh_target} 'sudo systemctl restart {service_name}'") + print() + print("Or run this script with --restart flag") + + print() + print("Note: First-time setup on Pi requires:") + print(f" ssh {ssh_target}") + print(f" cd {remote_path}") + print(" curl -LsSf https://astral.sh/uv/install.sh | sh") + print(" uv sync") + + return True + + +def main(): + parser = argparse.ArgumentParser(description="Deploy Smart Serow backend to Pi") + parser.add_argument( + "--restart", "-r", + action="store_true", + help="Restart the systemd service after deploy", + ) + args = parser.parse_args() + + success = deploy(restart=args.restart) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/scripts/deploy_target.sample.json b/scripts/deploy_target.sample.json index f0aa055..dfe7450 100644 --- a/scripts/deploy_target.sample.json +++ b/scripts/deploy_target.sample.json @@ -1,7 +1,13 @@ { "user": "pi", "host": "raspberrypi.local", + + "_comment_ui": "Flutter UI settings (deploy.py)", "remote_path": "/opt/smartserow", "service_name": "smartserow-ui", - "assets_path": "~/smartserow-ui/assets" + "assets_path": "~/smartserow-ui/assets", + + "_comment_backend": "Python backend settings (deploy_backend.py)", + "backend_path": "/opt/smartserow-backend", + "backend_service": "smartserow-backend" } diff --git a/scripts/smartserow-backend.service.sample b/scripts/smartserow-backend.service.sample new file mode 100644 index 0000000..33fc5de --- /dev/null +++ b/scripts/smartserow-backend.service.sample @@ -0,0 +1,19 @@ +[Unit] +Description=Smart Serow GPS Backend +After=network.target gpsd.service +Wants=gpsd.service + +[Service] +Type=simple +User=pi +Group=pi +WorkingDirectory=/opt/smartserow-backend +ExecStart=/home/pi/.local/bin/uv run python main.py +Restart=always +RestartSec=5 + +# Environment +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target