Flask backend and ui tweaks
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -53,6 +53,7 @@ pi_sysroot/
|
|||||||
# personal deploy target info
|
# personal deploy target info
|
||||||
scripts/deploy_target.json
|
scripts/deploy_target.json
|
||||||
scripts/smartserow-ui.service
|
scripts/smartserow-ui.service
|
||||||
|
scripts/smartserow-backend.service
|
||||||
|
|
||||||
# script python artifacts
|
# script python artifacts
|
||||||
scripts/__pycache__/
|
scripts/__pycache__/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"background": "#404040",
|
"background": "#404040",
|
||||||
"foreground": "#EAEAEA",
|
"foreground": "#EAEAEA",
|
||||||
"highlight": "#FA1504",
|
"highlight": "#FA1504",
|
||||||
"subdued": "#E47841"
|
"subdued": "#fda052"
|
||||||
},
|
},
|
||||||
"bright": {
|
"bright": {
|
||||||
"background": "#fda052",
|
"background": "#fda052",
|
||||||
|
|||||||
@@ -76,21 +76,31 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
'CHASSIS VOLTAGE ',
|
flex: 3,
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Text(
|
||||||
|
'Chassis voltage ',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontSize: 80,
|
fontSize: 60,
|
||||||
color: theme.subdued,
|
color: theme.subdued,
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Text(
|
||||||
'${_voltage.toStringAsFixed(1)}V',
|
'${_voltage.toStringAsFixed(1)}V',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontSize: 80,
|
fontSize: 80,
|
||||||
color: _voltage < 11.9 ? theme.highlight : theme.foreground,
|
color: _voltage < 11.9 ? theme.highlight : theme.foreground,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -125,7 +135,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
|
|
||||||
// Bottom stats row
|
// Bottom stats row
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
StatBox(label: 'RPM', value: _rpm.toString()),
|
StatBox(label: 'RPM', value: _rpm.toString()),
|
||||||
StatBox(label: 'ENG', value: '$_temp°C'),
|
StatBox(label: 'ENG', value: '$_temp°C'),
|
||||||
|
|||||||
@@ -20,13 +20,15 @@ class SplashScreen extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.terrain,
|
Icons.terrain,
|
||||||
size: 120,
|
size: 240,
|
||||||
color: theme.subdued,
|
color: theme.subdued,
|
||||||
|
// replace with custom logo later
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
'Smart Serow',
|
'Smart Serow',
|
||||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||||
|
fontSize: 160,
|
||||||
color: theme.foreground,
|
color: theme.foreground,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -35,6 +37,7 @@ class SplashScreen extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
status,
|
status,
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
fontSize: 80,
|
||||||
color: theme.subdued,
|
color: theme.subdued,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class TestFlipFlopService {
|
|||||||
|
|
||||||
_timer = Timer.periodic(const Duration(seconds: 2), (_) {
|
_timer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
// Toggle theme
|
// Toggle theme
|
||||||
ThemeService.instance.toggle();
|
// ThemeService.instance.toggle();
|
||||||
|
|
||||||
// Surprise the navigator
|
// Surprise the navigator
|
||||||
if (navigatorKey.currentState?.emotion == 'surprise') {
|
if (navigatorKey.currentState?.emotion == 'surprise') {
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ class StatBox extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = AppTheme.of(context);
|
final theme = AppTheme.of(context);
|
||||||
|
|
||||||
return Column(
|
return Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
@@ -31,6 +33,7 @@ class StatBox extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,34 @@
|
|||||||
|
|
||||||
Build, deploy, and setup helpers for the Smart Serow project.
|
Build, deploy, and setup helpers for the Smart Serow project.
|
||||||
|
|
||||||
## Build & Deploy
|
## UI Build & Deploy
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| `build.py` | Cross-compile Flutter app for ARM64. Runs `generate_theme.py` first. |
|
| `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 |
|
| `build-deploy.py` | Convenience wrapper: build → deploy → restart |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Typical workflow
|
|
||||||
python3 build.py # Build only
|
python3 build.py # Build only
|
||||||
python3 deploy.py --restart # Deploy and restart service
|
python3 deploy.py --restart # Deploy and restart service
|
||||||
python3 build-deploy.py # All-in-one
|
python3 build-deploy.py # All-in-one
|
||||||
|
python3 build.py --clean # Clean rebuild
|
||||||
# Clean rebuild (clears CMake cache)
|
|
||||||
python3 build.py --clean
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
## Theme Generation
|
||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
@@ -32,13 +42,19 @@ Called automatically by `build.py`. Looks for theme matching `navigator` in `con
|
|||||||
|
|
||||||
| Script | Purpose |
|
| Script | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| `pi_setup.sh` | First-time Pi configuration (deps, permissions, systemd service) |
|
| `pi_setup.sh` | First-time Pi config (deps, permissions, UI systemd service) |
|
||||||
| `smartserow-ui.service.sample` | Systemd unit file template |
|
| `smartserow-ui.service.sample` | UI systemd unit template |
|
||||||
|
| `smartserow-backend.service.sample` | Backend systemd unit template |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# On the Pi
|
# On the Pi (UI)
|
||||||
chmod +x pi_setup.sh
|
chmod +x pi_setup.sh
|
||||||
./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
|
## Configuration
|
||||||
@@ -53,7 +69,10 @@ chmod +x pi_setup.sh
|
|||||||
"user": "pi",
|
"user": "pi",
|
||||||
"host": "raspberrypi.local",
|
"host": "raspberrypi.local",
|
||||||
"remote_path": "/opt/smartserow",
|
"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
117
scripts/deploy_backend.py
Normal 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()
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"user": "pi",
|
"user": "pi",
|
||||||
"host": "raspberrypi.local",
|
"host": "raspberrypi.local",
|
||||||
|
|
||||||
|
"_comment_ui": "Flutter UI settings (deploy.py)",
|
||||||
"remote_path": "/opt/smartserow",
|
"remote_path": "/opt/smartserow",
|
||||||
"service_name": "smartserow-ui",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
19
scripts/smartserow-backend.service.sample
Normal file
19
scripts/smartserow-backend.service.sample
Normal 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
|
||||||
Reference in New Issue
Block a user