new python build-deploy chain
This commit is contained in:
44
README.md
44
README.md
@@ -26,13 +26,14 @@ smart-serow/
|
|||||||
│ ├── pubspec.yaml
|
│ ├── pubspec.yaml
|
||||||
│ └── elinux/ # Generated by flutter-elinux (gitignored)
|
│ └── elinux/ # Generated by flutter-elinux (gitignored)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── build.sh # Cross-compile for ARM64
|
│ ├── build.py # Cross-compile for ARM64
|
||||||
│ ├── deploy.sh # Push to Pi via rsync
|
│ ├── deploy.py # Push to Pi via rsync
|
||||||
│ ├── deploy_target.json
|
│ ├── build-deploy.py # One-click build + deploy
|
||||||
|
│ ├── deploy_target.sample.json
|
||||||
│ ├── pi_setup.sh # One-time Pi setup
|
│ ├── pi_setup.sh # One-time Pi setup
|
||||||
│ └── smartserow-ui.service
|
│ └── smartserow-ui.service.sample
|
||||||
├── pi_sysroot/ # Pi libraries for cross-linking (gitignored)
|
├── pi_sysroot/ # Pi libraries for cross-linking (gitignored)
|
||||||
└── LICENSE # MIT
|
└── LICENSE
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -100,33 +101,40 @@ This installs:
|
|||||||
|
|
||||||
## Build & Deploy
|
## Build & Deploy
|
||||||
|
|
||||||
### Build (in WSL2)
|
### One-liner (recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/build.sh # Normal build
|
python3 scripts/build-deploy.py # Build, deploy, restart
|
||||||
./scripts/build.sh --clean # Clean CMake cache first
|
python3 scripts/build-deploy.py --clean # Clean build first
|
||||||
|
python3 scripts/build-deploy.py --no-restart # Don't restart service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Individual scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build only
|
||||||
|
python3 scripts/build.py
|
||||||
|
python3 scripts/build.py --clean
|
||||||
|
|
||||||
|
# Deploy only
|
||||||
|
python3 scripts/deploy.py
|
||||||
|
python3 scripts/deploy.py --restart
|
||||||
```
|
```
|
||||||
|
|
||||||
Build output: `pi/ui/build/elinux/arm64/release/bundle/`
|
Build output: `pi/ui/build/elinux/arm64/release/bundle/`
|
||||||
|
|
||||||
### Deploy
|
### Deploy config
|
||||||
|
|
||||||
Edit `scripts/deploy_target.json`:
|
Copy and edit `scripts/deploy_target.sample.json` → `scripts/deploy_target.json`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"user": "mikkeli",
|
"user": "pi",
|
||||||
"host": "smartserow.local",
|
"host": "raspberrypi.local",
|
||||||
"remote_path": "/opt/smartserow",
|
"remote_path": "/opt/smartserow",
|
||||||
"service_name": "smartserow-ui"
|
"service_name": "smartserow-ui"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploy:
|
|
||||||
```bash
|
|
||||||
./scripts/deploy.sh # Just copy files
|
|
||||||
./scripts/deploy.sh --restart # Copy and restart service
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verify
|
### Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -19,13 +21,64 @@ class SmartSerowApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
home: const HomePage(),
|
home: const AppRoot(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomePage extends StatelessWidget {
|
/// Root widget that manages app state transitions
|
||||||
const HomePage({super.key});
|
class AppRoot extends StatefulWidget {
|
||||||
|
const AppRoot({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppRoot> createState() => _AppRootState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppRootState extends State<AppRoot> {
|
||||||
|
bool _initialized = false;
|
||||||
|
String _initStatus = 'Starting...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_runInitSequence();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runInitSequence() async {
|
||||||
|
// Simulate init checks - replace with real checks later
|
||||||
|
// (UART, GPS, sensors, etc.)
|
||||||
|
|
||||||
|
setState(() => _initStatus = 'Checking systems...');
|
||||||
|
await Future.delayed(const Duration(milliseconds: 800));
|
||||||
|
|
||||||
|
setState(() => _initStatus = 'UART: standby');
|
||||||
|
await Future.delayed(const Duration(milliseconds: 400));
|
||||||
|
|
||||||
|
setState(() => _initStatus = 'GPS: standby');
|
||||||
|
await Future.delayed(const Duration(milliseconds: 400));
|
||||||
|
|
||||||
|
setState(() => _initStatus = 'Ready');
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
setState(() => _initialized = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
child: _initialized
|
||||||
|
? const DashboardScreen(key: ValueKey('dashboard'))
|
||||||
|
: SplashScreen(key: const ValueKey('splash'), status: _initStatus),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splash screen - shown during initialization
|
||||||
|
class SplashScreen extends StatelessWidget {
|
||||||
|
final String status;
|
||||||
|
|
||||||
|
const SplashScreen({super.key, required this.status});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -48,9 +101,9 @@ class HomePage extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'System Ready',
|
status,
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
@@ -61,3 +114,141 @@ class HomePage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Main dashboard - placeholder with random updating values
|
||||||
|
class DashboardScreen extends StatefulWidget {
|
||||||
|
const DashboardScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DashboardScreenState extends State<DashboardScreen> {
|
||||||
|
final _random = Random();
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
int _speed = 0;
|
||||||
|
int _rpm = 0;
|
||||||
|
double _voltage = 12.6;
|
||||||
|
int _temp = 25;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Update random values every 500ms - simulates live data
|
||||||
|
_timer = Timer.periodic(const Duration(milliseconds: 500), (_) {
|
||||||
|
setState(() {
|
||||||
|
_speed = _random.nextInt(120);
|
||||||
|
_rpm = 1000 + _random.nextInt(8000);
|
||||||
|
_voltage = 11.5 + _random.nextDouble() * 2;
|
||||||
|
_temp = 20 + _random.nextInt(60);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'SMART SEROW',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: Colors.teal,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${_voltage.toStringAsFixed(1)}V',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: _voltage < 12.0 ? Colors.red : Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
|
// Main speed display
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$_speed',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 180,
|
||||||
|
fontWeight: FontWeight.w200,
|
||||||
|
color: Colors.white,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'km/h',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom stats row
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_StatBox(label: 'RPM', value: _rpm.toString()),
|
||||||
|
_StatBox(label: 'TEMP', value: '$_temp°C'),
|
||||||
|
_StatBox(label: 'GEAR', value: '—'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatBox extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
const _StatBox({required this.label, required this.value});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.grey,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
64
scripts/build-deploy.py
Normal file
64
scripts/build-deploy.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""One-click build and deploy for Smart Serow.
|
||||||
|
|
||||||
|
Combines build.py and deploy.py with sensible defaults.
|
||||||
|
Defaults to --restart since that's usually what you want.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Import sibling modules
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from build import build
|
||||||
|
from deploy import deploy
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Build and deploy Smart Serow in one step",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--clean", "-c",
|
||||||
|
action="store_true",
|
||||||
|
help="Clean CMake cache before building",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-restart",
|
||||||
|
action="store_true",
|
||||||
|
help="Don't restart service after deploy (default: restart)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--build-only",
|
||||||
|
action="store_true",
|
||||||
|
help="Only build, don't deploy",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--deploy-only",
|
||||||
|
action="store_true",
|
||||||
|
help="Only deploy, don't build",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Build
|
||||||
|
if not args.deploy_only:
|
||||||
|
print()
|
||||||
|
if not build(clean=args.clean):
|
||||||
|
print("Build failed!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
if not args.build_only:
|
||||||
|
print()
|
||||||
|
restart = not args.no_restart
|
||||||
|
if not deploy(restart=restart):
|
||||||
|
print("Deploy failed!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=== All done! ===")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
6
scripts/build-deploy.sh
Normal file
6
scripts/build-deploy.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Wrapper for build-deploy.py
|
||||||
|
# Usage: ./build-deploy.sh [options]
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
exec python3 "$SCRIPT_DIR/build-deploy.py" "$@"
|
||||||
142
scripts/build.py
Normal file
142
scripts/build.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build script for Smart Serow Flutter UI.
|
||||||
|
|
||||||
|
Run this in WSL2 with flutter-elinux installed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
|
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||||
|
UI_DIR = PROJECT_ROOT / "pi" / "ui"
|
||||||
|
BUILD_OUTPUT = UI_DIR / "build" / "elinux" / "arm64" / "release" / "bundle"
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
|
||||||
|
"""Run a command, exit on failure."""
|
||||||
|
print(f" → {' '.join(cmd)}")
|
||||||
|
result = subprocess.run(cmd, **kwargs)
|
||||||
|
if result.returncode != 0:
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_flutter_elinux() -> str:
|
||||||
|
"""Check if flutter-elinux is available, return path."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["which", "flutter-elinux"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("ERROR: flutter-elinux not found in PATH")
|
||||||
|
print("Install it or check your PATH")
|
||||||
|
print(f"\nCurrent PATH: {os.environ.get('PATH', '')}")
|
||||||
|
sys.exit(1)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def set_cross_compile_env():
|
||||||
|
"""Set environment variables for ARM64 cross-compilation."""
|
||||||
|
env_vars = {
|
||||||
|
"CC": "aarch64-linux-gnu-gcc",
|
||||||
|
"CXX": "aarch64-linux-gnu-g++",
|
||||||
|
"AR": "aarch64-linux-gnu-ar",
|
||||||
|
"LD": "aarch64-linux-gnu-ld",
|
||||||
|
"CMAKE_C_COMPILER": "aarch64-linux-gnu-gcc",
|
||||||
|
"CMAKE_CXX_COMPILER": "aarch64-linux-gnu-g++",
|
||||||
|
}
|
||||||
|
os.environ.update(env_vars)
|
||||||
|
return env_vars
|
||||||
|
|
||||||
|
|
||||||
|
def build(clean: bool = False) -> bool:
|
||||||
|
"""Run the build process. Returns True on success."""
|
||||||
|
print("=== Smart Serow Build ===")
|
||||||
|
print(f"Project: {UI_DIR}")
|
||||||
|
|
||||||
|
# Check flutter-elinux
|
||||||
|
flutter_path = check_flutter_elinux()
|
||||||
|
print(f"Using: {flutter_path}")
|
||||||
|
|
||||||
|
# Set cross-compilation env
|
||||||
|
env_vars = set_cross_compile_env()
|
||||||
|
print(f"Cross-compiler: {env_vars['CXX']}")
|
||||||
|
|
||||||
|
os.chdir(UI_DIR)
|
||||||
|
|
||||||
|
# Initialize elinux project if needed
|
||||||
|
elinux_dir = UI_DIR / "elinux"
|
||||||
|
if not elinux_dir.exists():
|
||||||
|
print("Initializing elinux project structure...")
|
||||||
|
run([
|
||||||
|
"flutter-elinux", "create", ".",
|
||||||
|
"--project-name", "smartserow_ui",
|
||||||
|
"--org", "com.smartserow",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Clean if requested
|
||||||
|
if clean:
|
||||||
|
cache_dir = UI_DIR / "build" / "elinux" / "arm64"
|
||||||
|
if cache_dir.exists():
|
||||||
|
print("Cleaning CMake cache...")
|
||||||
|
shutil.rmtree(cache_dir)
|
||||||
|
|
||||||
|
# Fetch dependencies
|
||||||
|
print("Fetching dependencies...")
|
||||||
|
run(["flutter-elinux", "pub", "get"])
|
||||||
|
|
||||||
|
# Build command
|
||||||
|
print("Building for ARM64 (elinux) with DRM-GBM backend...")
|
||||||
|
|
||||||
|
build_cmd = [
|
||||||
|
"flutter-elinux", "build", "elinux",
|
||||||
|
"--target-arch=arm64",
|
||||||
|
"--target-backend-type=gbm",
|
||||||
|
"--target-compiler-triple=aarch64-linux-gnu",
|
||||||
|
"--release",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add sysroot if available
|
||||||
|
sysroot = PROJECT_ROOT / "pi_sysroot"
|
||||||
|
if sysroot.exists():
|
||||||
|
print(f"Using Pi sysroot: {sysroot}")
|
||||||
|
build_cmd.append(f"--target-sysroot={sysroot}")
|
||||||
|
|
||||||
|
run(build_cmd)
|
||||||
|
|
||||||
|
# Verify output
|
||||||
|
if BUILD_OUTPUT.exists():
|
||||||
|
print()
|
||||||
|
print("=== Build Complete ===")
|
||||||
|
print(f"Output: {BUILD_OUTPUT}")
|
||||||
|
for f in BUILD_OUTPUT.iterdir():
|
||||||
|
size = f.stat().st_size
|
||||||
|
print(f" {f.name}: {size:,} bytes")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"ERROR: Build output not found at {BUILD_OUTPUT}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Build Smart Serow Flutter UI")
|
||||||
|
parser.add_argument(
|
||||||
|
"--clean", "-c",
|
||||||
|
action="store_true",
|
||||||
|
help="Clean CMake cache before building",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
success = build(clean=args.clean)
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
97
scripts/deploy.py
Normal file
97
scripts/deploy.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deploy script for Smart Serow Flutter UI.
|
||||||
|
|
||||||
|
Pushes build bundle to Pi and optionally restarts service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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"
|
||||||
|
BUILD_DIR = PROJECT_ROOT / "pi" / "ui" / "build" / "elinux" / "arm64" / "release" / "bundle"
|
||||||
|
|
||||||
|
|
||||||
|
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 to Pi. Returns True on success."""
|
||||||
|
config = load_config()
|
||||||
|
|
||||||
|
pi_user = config["user"]
|
||||||
|
pi_host = config["host"]
|
||||||
|
remote_path = config["remote_path"]
|
||||||
|
service_name = config["service_name"]
|
||||||
|
|
||||||
|
ssh_target = f"{pi_user}@{pi_host}"
|
||||||
|
|
||||||
|
print("=== Smart Serow Deploy ===")
|
||||||
|
print(f"Target: {ssh_target}:{remote_path}")
|
||||||
|
print(f"Source: {BUILD_DIR}")
|
||||||
|
|
||||||
|
if not BUILD_DIR.exists():
|
||||||
|
print("ERROR: Build directory not found. Run build.py first.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Sync build to Pi
|
||||||
|
print()
|
||||||
|
print("Syncing files...")
|
||||||
|
run([
|
||||||
|
"rsync", "-avz", "--delete",
|
||||||
|
f"{BUILD_DIR}/",
|
||||||
|
f"{ssh_target}:{remote_path}/bundle/",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Restart service if requested
|
||||||
|
if restart:
|
||||||
|
print()
|
||||||
|
print(f"Restarting service: {service_name}")
|
||||||
|
run(["ssh", ssh_target, f"sudo systemctl restart {service_name}"])
|
||||||
|
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")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Deploy Smart Serow 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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"user": "pi",
|
"user": "pi",
|
||||||
"host": "192.168.114.5",
|
"host": "raspberrypi.local",
|
||||||
"remote_path": "/opt/smartserow",
|
"remote_path": "/opt/smartserow",
|
||||||
"service_name": "smartserow-ui"
|
"service_name": "smartserow-ui"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user