Flask backend and ui tweaks

This commit is contained in:
Mikkeli Matlock
2026-01-26 11:35:17 +09:00
parent 38229543af
commit 60a1c1811e
10 changed files with 221 additions and 43 deletions

1
.gitignore vendored
View File

@@ -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__/

View File

@@ -3,7 +3,7 @@
"background": "#404040",
"foreground": "#EAEAEA",
"highlight": "#FA1504",
"subdued": "#E47841"
"subdued": "#fda052"
},
"bright": {
"background": "#fda052",

View File

@@ -76,21 +76,31 @@ class _DashboardScreenState extends State<DashboardScreen> {
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<DashboardScreen> {
// Bottom stats row
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
StatBox(label: 'RPM', value: _rpm.toString()),
StatBox(label: 'ENG', value: '$_temp°C'),

View File

@@ -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,
),
),

View File

@@ -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') {

View File

@@ -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,
),
),
),
],
],
)
);
}
}

View File

@@ -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"
}
```

117
scripts/deploy_backend.py Normal file
View File

@@ -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()

View File

@@ -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"
}

View File

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