#!/usr/bin/env bash # p1ctl.sh — run / stop Sanad Package 1 (Basic Communication) on the robot # in the existing gemini_sdk conda env (dev mode; no Docker required). # # ./p1ctl.sh start # launch P1 dashboard on :8011 (coexists with Sanad :8000) # ./p1ctl.sh stop # stop P1 # ./p1ctl.sh restart # ./p1ctl.sh status # process + /api/health # ./p1ctl.sh logs [N] # tail N lines of the P1 log # # Self-contained: runs against the vendored engine in ./vendor — no sibling # Sanad/ or Packages/ checkout needed. Overridable env: SANAD_P1_PY, # SANAD_DASHBOARD_PORT (8011), SANAD_AUDIO_PROFILE (builtin), SANAD_DDS_INTERFACE (eth0), # SANAD_LICENSE / SANAD_PUBKEY. set -u PKG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PY="${SANAD_P1_PY:-$HOME/miniconda3/envs/gemini_sdk/bin/python}" PORT="${SANAD_DASHBOARD_PORT:-8011}" APP="$PKG_DIR/app_p1.py" LOG="$PKG_DIR/p1.log" LIC="${SANAD_LICENSE:-$PKG_DIR/license/sanad.lic}"; [ -f "$LIC" ] || LIC="$PKG_DIR/license/sanad.lic.example" _start() { if pgrep -f app_p1.py >/dev/null 2>&1; then echo "P1 already running on :$PORT"; return 0 fi [ -f "$APP" ] || { echo "ERROR: $APP not found (deploy first)"; return 1; } cd "$PKG_DIR" export SANAD_APP_DIR="$PKG_DIR/vendor" \ SANAD_LICENSE="$LIC" \ SANAD_PUBKEY="${SANAD_PUBKEY:-$PKG_DIR/license/pubkey.ed25519}" \ SANAD_P1_STATIC="$PKG_DIR/static" \ PYTHONPATH="$PKG_DIR/vendor" \ SANAD_DASHBOARD_PORT="$PORT" SANAD_DASHBOARD_HOST="0.0.0.0" \ SANAD_VOICE_BRAIN="gemini" \ SANAD_AUDIO_PROFILE="${SANAD_AUDIO_PROFILE:-builtin}" \ SANAD_DDS_INTERFACE="${SANAD_DDS_INTERFACE:-eth0}" \ PYTHONUNBUFFERED=1 [ -f /usr/lib/aarch64-linux-gnu/libgomp.so.1 ] && export LD_PRELOAD=/usr/lib/aarch64-linux-gnu/libgomp.so.1 nohup "$PY" "$APP" > "$LOG" 2>&1 & sleep 3 if pgrep -f app_p1.py >/dev/null 2>&1; then echo "P1 started -> http://$(hostname -I | awk '{print $1}'):$PORT (log: $LOG)" else echo "P1 failed to start. Last log lines:"; tail -20 "$LOG" fi } _stop() { pgrep -f app_p1.py >/dev/null 2>&1 || { echo "P1 was not running."; return 0; } pkill -f app_p1.py 2>/dev/null # SIGTERM (graceful) # uvicorn waits for open websockets (e.g. a browser on /ws/logs) — force after 8s for _ in $(seq 1 8); do pgrep -f app_p1.py >/dev/null 2>&1 || break; sleep 1; done pgrep -f app_p1.py >/dev/null 2>&1 && pkill -9 -f app_p1.py 2>/dev/null sleep 1 pgrep -f app_p1.py >/dev/null 2>&1 && echo "P1 still running (could not kill)." || echo "P1 stopped." } _status() { if pgrep -af app_p1.py; then echo -n "health: "; curl -s --max-time 4 "http://127.0.0.1:$PORT/api/health"; echo; else echo "P1 not running."; fi; } _logs() { tail -n "${1:-40}" "$LOG" 2>/dev/null || echo "no log at $LOG"; } case "${1:-}" in start) _start ;; stop) _stop ;; restart) _stop; sleep 2; _start ;; status) _status ;; logs) shift; _logs "${1:-40}" ;; *) echo "usage: $0 {start|stop|restart|status|logs [N]}"; exit 2 ;; esac