gps manipulations tailored to sim7600h hat
This commit is contained in:
@@ -1,88 +1,88 @@
|
||||
#!/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
|
||||
from deploy_backend import deploy as deploy_backend
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ui",
|
||||
action="store_true",
|
||||
help="Build/deploy UI only (no backend)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backend",
|
||||
action="store_true",
|
||||
help="Deploy backend only (no UI, no build)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Default: both UI and backend if neither flag specified
|
||||
do_ui = args.ui or not args.backend
|
||||
do_backend = args.backend or not args.ui
|
||||
|
||||
restart = not args.no_restart
|
||||
|
||||
# Build UI (only if doing UI and not deploy-only)
|
||||
if do_ui and not args.deploy_only:
|
||||
print()
|
||||
if not build(clean=args.clean):
|
||||
print("UI build failed!")
|
||||
sys.exit(1)
|
||||
|
||||
# Deploy backend FIRST (no build step needed - it's Python)
|
||||
# Backend must be up before UI connects to WebSocket
|
||||
if do_backend and not args.build_only:
|
||||
print()
|
||||
if not deploy_backend(restart=restart):
|
||||
print("Backend deploy failed!")
|
||||
sys.exit(1)
|
||||
|
||||
# Deploy UI after backend is ready
|
||||
if do_ui and not args.build_only:
|
||||
print()
|
||||
if not deploy(restart=restart):
|
||||
print("UI deploy failed!")
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
print("=== All done! ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
#!/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
|
||||
from deploy_backend import deploy as deploy_backend
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ui",
|
||||
action="store_true",
|
||||
help="Build/deploy UI only (no backend)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--backend",
|
||||
action="store_true",
|
||||
help="Deploy backend only (no UI, no build)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Default: both UI and backend if neither flag specified
|
||||
do_ui = args.ui or not args.backend
|
||||
do_backend = args.backend or not args.ui
|
||||
|
||||
restart = not args.no_restart
|
||||
|
||||
# Build UI (only if doing UI and not deploy-only)
|
||||
if do_ui and not args.deploy_only:
|
||||
print()
|
||||
if not build(clean=args.clean):
|
||||
print("UI build failed!")
|
||||
sys.exit(1)
|
||||
|
||||
# Deploy backend FIRST (no build step needed - it's Python)
|
||||
# Backend must be up before UI connects to WebSocket
|
||||
if do_backend and not args.build_only:
|
||||
print()
|
||||
if not deploy_backend(restart=restart):
|
||||
print("Backend deploy failed!")
|
||||
sys.exit(1)
|
||||
|
||||
# Deploy UI after backend is ready
|
||||
if do_ui and not args.build_only:
|
||||
print()
|
||||
if not deploy(restart=restart):
|
||||
print("UI deploy failed!")
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
print("=== All done! ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,157 +1,157 @@
|
||||
#!/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"
|
||||
SERVICE_FILE = SCRIPT_DIR / "smartserow-backend.service"
|
||||
|
||||
|
||||
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",
|
||||
"--exclude", "uv.lock", # Let Pi generate its own lockfile
|
||||
f"{BACKEND_DIR}/",
|
||||
f"{ssh_target}:{remote_path}/",
|
||||
])
|
||||
|
||||
# Ensure system GPIO package is installed (pip version needs compilation)
|
||||
print()
|
||||
print("Ensuring system GPIO package...")
|
||||
run(
|
||||
["ssh", ssh_target, "dpkg -s python3-rpi.gpio >/dev/null 2>&1 || sudo apt install -y python3-rpi.gpio"],
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Create venv with system-site-packages if it doesn't exist
|
||||
# This allows access to apt-installed packages like python3-rpi.gpio
|
||||
print()
|
||||
print("Ensuring venv with system-site-packages...")
|
||||
run(
|
||||
["ssh", ssh_target, f"cd {remote_path} && [ -d .venv ] || ~/.local/bin/uv venv --system-site-packages"],
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Run uv sync to install/update dependencies
|
||||
# Use full path since non-interactive SSH doesn't load .bashrc
|
||||
print()
|
||||
print("Running uv sync...")
|
||||
result = run(
|
||||
["ssh", ssh_target, f"cd {remote_path} && ~/.local/bin/uv sync"],
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print("WARNING: uv sync failed - dependencies may be out of date")
|
||||
print("Make sure uv is installed on Pi: curl -LsSf https://astral.sh/uv/install.sh | sh")
|
||||
|
||||
# Deploy service file if it exists
|
||||
if SERVICE_FILE.exists():
|
||||
print()
|
||||
print("Deploying systemd service file...")
|
||||
run(["scp", str(SERVICE_FILE), f"{ssh_target}:/tmp/"])
|
||||
run([
|
||||
"ssh", ssh_target,
|
||||
f"sudo mv /tmp/{SERVICE_FILE.name} /etc/systemd/system/ && sudo systemctl daemon-reload"
|
||||
])
|
||||
|
||||
# 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(" curl -LsSf https://astral.sh/uv/install.sh | sh # Install uv")
|
||||
print(" sudo apt install python3-rpi.gpio # GPIO support")
|
||||
|
||||
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()
|
||||
#!/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"
|
||||
SERVICE_FILE = SCRIPT_DIR / "smartserow-backend.service"
|
||||
|
||||
|
||||
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",
|
||||
"--exclude", "uv.lock", # Let Pi generate its own lockfile
|
||||
f"{BACKEND_DIR}/",
|
||||
f"{ssh_target}:{remote_path}/",
|
||||
])
|
||||
|
||||
# Ensure system GPIO package is installed (pip version needs compilation)
|
||||
print()
|
||||
print("Ensuring system GPIO package...")
|
||||
run(
|
||||
["ssh", ssh_target, "dpkg -s python3-rpi.gpio >/dev/null 2>&1 || sudo apt install -y python3-rpi.gpio"],
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Create venv with system-site-packages if it doesn't exist
|
||||
# This allows access to apt-installed packages like python3-rpi.gpio
|
||||
print()
|
||||
print("Ensuring venv with system-site-packages...")
|
||||
run(
|
||||
["ssh", ssh_target, f"cd {remote_path} && [ -d .venv ] || ~/.local/bin/uv venv --system-site-packages"],
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Run uv sync to install/update dependencies
|
||||
# Use full path since non-interactive SSH doesn't load .bashrc
|
||||
print()
|
||||
print("Running uv sync...")
|
||||
result = run(
|
||||
["ssh", ssh_target, f"cd {remote_path} && ~/.local/bin/uv sync"],
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print("WARNING: uv sync failed - dependencies may be out of date")
|
||||
print("Make sure uv is installed on Pi: curl -LsSf https://astral.sh/uv/install.sh | sh")
|
||||
|
||||
# Deploy service file if it exists
|
||||
if SERVICE_FILE.exists():
|
||||
print()
|
||||
print("Deploying systemd service file...")
|
||||
run(["scp", str(SERVICE_FILE), f"{ssh_target}:/tmp/"])
|
||||
run([
|
||||
"ssh", ssh_target,
|
||||
f"sudo mv /tmp/{SERVICE_FILE.name} /etc/systemd/system/ && sudo systemctl daemon-reload"
|
||||
])
|
||||
|
||||
# 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(" curl -LsSf https://astral.sh/uv/install.sh | sh # Install uv")
|
||||
print(" sudo apt install python3-rpi.gpio # GPIO support")
|
||||
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user