569 lines
19 KiB
Bash
Executable File
569 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# ============================================================
|
|
# photo_sanad.sh
|
|
# ------------------------------------------------------------
|
|
# ONE command to run everything:
|
|
# ./photo_sanad.sh
|
|
#
|
|
# What it does:
|
|
# 1) Activates the camera env (py3.10) and starts the Core direct camera service in background
|
|
# 2) Activates gemini (py3.11) and runs Gemini/voice_sanad.py in foreground
|
|
# 3) Sets PulseAudio default sink/source (optional)
|
|
# 4) Sets PYTHONPATH for unitree_sdk2_python (so unitree_sdk2py imports work)
|
|
# 5) On exit, stops the direct camera server
|
|
# ============================================================
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
DEFAULT_BASE_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
BASE_DIR="${BASE_DIR:-$DEFAULT_BASE_DIR}"
|
|
PY_FILE="$BASE_DIR/Gemini/voice_sanad.py"
|
|
RUN_USER="${SUDO_USER:-${USER:-$(id -un)}}"
|
|
RUN_USER_HOME="$(getent passwd "$RUN_USER" 2>/dev/null | cut -d: -f6 || true)"
|
|
if [[ -z "${RUN_USER_HOME:-}" ]]; then
|
|
RUN_USER_HOME="$HOME"
|
|
fi
|
|
RUN_USER_ID="$(id -u "$RUN_USER" 2>/dev/null || true)"
|
|
export HOME="$RUN_USER_HOME"
|
|
if [[ -n "${RUN_USER_ID:-}" ]] && [[ -d "/run/user/$RUN_USER_ID" ]]; then
|
|
export XDG_RUNTIME_DIR="/run/user/$RUN_USER_ID"
|
|
if [[ -S "$XDG_RUNTIME_DIR/pulse/native" ]]; then
|
|
export PULSE_SERVER="unix:$XDG_RUNTIME_DIR/pulse/native"
|
|
fi
|
|
fi
|
|
|
|
# conda
|
|
CONDA_SH="${CONDA_SH:-$RUN_USER_HOME/miniconda3/etc/profile.d/conda.sh}"
|
|
ENV_TELE="teleimager" # Python 3.10 camera env (pyrealsense2)
|
|
ENV_GEM="gemini" # Python 3.11
|
|
SDK_PY_PATH="${SDK_PY_PATH:-$RUN_USER_HOME/unitree_sdk2_python}"
|
|
DIRECT_CAMERA_PORT="${DIRECT_CAMERA_PORT:-8091}"
|
|
DIRECT_CAMERA_HOST="${DIRECT_CAMERA_HOST:-127.0.0.1}"
|
|
DIRECT_CAMERA_SOURCE="${DIRECT_CAMERA_SOURCE:-realsense}"
|
|
DIRECT_CAMERA_SCRIPT="${DIRECT_CAMERA_SCRIPT:-$BASE_DIR/Core/direct_camera_service.py}"
|
|
LEGACY_DIRECT_CAMERA_SCRIPT="$BASE_DIR/Scripts/direct_camera_samples_server.py"
|
|
|
|
# Optional: choose your camera (if needed)
|
|
# export CAMERA_DEVICE=/dev/video2
|
|
# export PHOTO_PREFIX=photo
|
|
export CAMERA_DEVICE="${CAMERA_DEVICE:-/dev/video0}"
|
|
export PHOTO_PREFIX="${PHOTO_PREFIX:-photo}"
|
|
|
|
# RealSense fix/sudo behavior
|
|
SUDO_ENABLE_FIX="${SUDO_ENABLE_FIX:-1}" # 1=cache sudo so /api/fix can run
|
|
SUDO_PROMPT_ON_START="${SUDO_PROMPT_ON_START:-0}" # 1=allow interactive sudo prompt at startup
|
|
AUTO_FIX_ON_START="${AUTO_FIX_ON_START:-0}" # 1=run fix script once on startup
|
|
SUDO_KEEPALIVE_INTERVAL="${SUDO_KEEPALIVE_INTERVAL:-60}"
|
|
FIX_SCRIPT="${FIX_SCRIPT:-$BASE_DIR/Scripts/fix_realsense_usb.sh}"
|
|
SUDO_KEEPALIVE_PID=""
|
|
LOG_DIR="${LOG_DIR:-$BASE_DIR/Logs}"
|
|
PHOTO_SERVER_PORT="${PHOTO_SERVER_PORT:-8080}"
|
|
PRE_KILL_OLD="${PRE_KILL_OLD:-1}" # 1=stop stale sanad/camera services from previous runs
|
|
FREE_PORT_BEFORE_START="${FREE_PORT_BEFORE_START:-1}" # 1=kill listeners on PHOTO_SERVER_PORT
|
|
ALLOW_EXISTING_PORT_SERVER="${ALLOW_EXISTING_PORT_SERVER:-0}" # 1=allow attaching to an already-running gallery server
|
|
|
|
# PulseAudio device names (change if needed)
|
|
SINK="${SINK:-alsa_output.usb-Anker_PowerConf_A3321-DEV-SN1-01.analog-stereo}"
|
|
SOURCE="${SOURCE:-alsa_input.usb-Anker_PowerConf_A3321-DEV-SN1-01.mono-fallback}"
|
|
|
|
# ALSA plugin fix (helps conda envs stop spamming ALSA errors)
|
|
if [[ -d "/usr/lib/aarch64-linux-gnu/alsa-lib" ]]; then
|
|
export ALSA_PLUGIN_DIR="/usr/lib/aarch64-linux-gnu/alsa-lib"
|
|
elif [[ -d "/usr/lib/alsa-lib" ]]; then
|
|
export ALSA_PLUGIN_DIR="/usr/lib/alsa-lib"
|
|
fi
|
|
if [[ -f "/usr/share/alsa/alsa.conf" ]]; then
|
|
export ALSA_CONFIG_PATH="/usr/share/alsa/alsa.conf"
|
|
fi
|
|
export ALSA_LOG_LEVEL=0
|
|
|
|
echo "📁 Base dir: $BASE_DIR"
|
|
echo "👤 Runtime user: $RUN_USER"
|
|
echo "🏠 Runtime home: $RUN_USER_HOME"
|
|
cd "$BASE_DIR"
|
|
|
|
# ---------- logging dir ----------
|
|
if ! mkdir -p "$LOG_DIR" >/dev/null 2>&1; then
|
|
LOG_DIR="$HOME/.ai_photographer_logs"
|
|
mkdir -p "$LOG_DIR"
|
|
fi
|
|
if ! touch "$LOG_DIR/.write_test" >/dev/null 2>&1; then
|
|
LOG_DIR="$HOME/.ai_photographer_logs"
|
|
mkdir -p "$LOG_DIR"
|
|
touch "$LOG_DIR/.write_test" >/dev/null 2>&1 || true
|
|
fi
|
|
rm -f "$LOG_DIR/.write_test" >/dev/null 2>&1 || true
|
|
export AI_PHOTOGRAPHER_LOG_DIR="$LOG_DIR"
|
|
|
|
# ---------- stale-process cleanup ----------
|
|
show_port_owner() {
|
|
local port="$1"
|
|
if command -v ss >/dev/null 2>&1; then
|
|
# shellcheck disable=SC2016
|
|
ss -ltnp 2>/dev/null | awk -v p=":$port" '$4 ~ p {print $0}' || true
|
|
fi
|
|
}
|
|
|
|
start_sudo_keepalive() {
|
|
(
|
|
while true; do
|
|
sleep "$SUDO_KEEPALIVE_INTERVAL"
|
|
sudo -n true >/dev/null 2>&1 || exit 0
|
|
done
|
|
) &
|
|
SUDO_KEEPALIVE_PID=$!
|
|
echo "✅ sudo cached (keepalive PID=$SUDO_KEEPALIVE_PID)"
|
|
}
|
|
|
|
setup_sudo_cache() {
|
|
if [[ "$SUDO_ENABLE_FIX" != "1" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if ! command -v sudo >/dev/null 2>&1; then
|
|
echo "⚠️ sudo not found; /api/fix cannot run privileged fixes."
|
|
return 0
|
|
fi
|
|
|
|
if sudo -n true >/dev/null 2>&1; then
|
|
echo "🔐 sudo credentials already cached."
|
|
start_sudo_keepalive
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$SUDO_PROMPT_ON_START" == "1" && -t 0 ]]; then
|
|
echo "🔐 Preparing sudo for RealSense fix endpoint..."
|
|
if sudo -v; then
|
|
start_sudo_keepalive
|
|
else
|
|
echo "⚠️ sudo auth failed; /api/fix may require manual terminal command."
|
|
fi
|
|
else
|
|
echo "⚠️ sudo not cached. Use SUDO_PROMPT_ON_START=1 to allow startup prompt."
|
|
fi
|
|
}
|
|
|
|
get_port_pids() {
|
|
local port="$1"
|
|
{
|
|
if command -v lsof >/dev/null 2>&1; then
|
|
lsof -t -iTCP:"$port" -sTCP:LISTEN -Pn 2>/dev/null || true
|
|
fi
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -ltnp 2>/dev/null \
|
|
| awk -v p=":$port" '$4 ~ p {print $0}' \
|
|
| sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' || true
|
|
fi
|
|
if command -v fuser >/dev/null 2>&1; then
|
|
fuser -n tcp "$port" 2>/dev/null | tr ' ' '\n' || true
|
|
fi
|
|
} | sed '/^[[:space:]]*$/d' | sort -u
|
|
}
|
|
|
|
get_pid_cmdline() {
|
|
local pid="$1"
|
|
ps -o args= -p "$pid" 2>/dev/null || true
|
|
}
|
|
|
|
kill_one_pid() {
|
|
local pid="$1"
|
|
local signal="${2:-TERM}"
|
|
local pgid=""
|
|
[[ -z "${pid:-}" ]] && return 0
|
|
|
|
if ! kill "-$signal" "$pid" >/dev/null 2>&1; then
|
|
sudo -n kill "-$signal" "$pid" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ' || true)"
|
|
if [[ -n "${pgid:-}" ]]; then
|
|
if ! kill "-$signal" "--" "-$pgid" >/dev/null 2>&1; then
|
|
sudo -n kill "-$signal" "--" "-$pgid" >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
kill_port_owner() {
|
|
local port="$1"
|
|
local pids pid attempt
|
|
pids="$(get_port_pids "$port" || true)"
|
|
[[ -z "${pids:-}" ]] && return 0
|
|
|
|
echo "🛑 Attempting to free port $port (PID(s): $(echo "$pids" | tr '\n' ' '))"
|
|
for attempt in 1 2 3; do
|
|
while IFS= read -r pid; do
|
|
[[ -z "${pid:-}" ]] && continue
|
|
if [[ "$attempt" -lt 3 ]]; then
|
|
kill_one_pid "$pid" TERM
|
|
else
|
|
kill_one_pid "$pid" KILL
|
|
fi
|
|
done <<< "$pids"
|
|
sleep 1
|
|
pids="$(get_port_pids "$port" || true)"
|
|
[[ -z "${pids:-}" ]] && return 0
|
|
if [[ "$attempt" -eq 2 ]]; then
|
|
echo "⚠️ Port $port still busy, sending SIGKILL..."
|
|
fi
|
|
done
|
|
}
|
|
|
|
report_port_owners() {
|
|
local port="$1"
|
|
local pids pid cmd
|
|
pids="$(get_port_pids "$port" || true)"
|
|
[[ -z "${pids:-}" ]] && return 0
|
|
while IFS= read -r pid; do
|
|
[[ -z "${pid:-}" ]] && continue
|
|
cmd="$(get_pid_cmdline "$pid")"
|
|
echo " - PID $pid: ${cmd:-<unknown>}"
|
|
done <<< "$pids"
|
|
}
|
|
|
|
stop_stale_processes() {
|
|
echo "🧽 Checking stale processes..."
|
|
local stopped=0
|
|
|
|
# Previous Sanad app instances from same project copy
|
|
if pgrep -f "$BASE_DIR/Gemini/voice_sanad.py" >/dev/null 2>&1; then
|
|
echo "🛑 Stopping previous voice_sanad.py processes..."
|
|
pkill -f "$BASE_DIR/Gemini/voice_sanad.py" >/dev/null 2>&1 || sudo -n pkill -f "$BASE_DIR/Gemini/voice_sanad.py" >/dev/null 2>&1 || true
|
|
stopped=1
|
|
fi
|
|
# Legacy pre-restructure path support
|
|
if pgrep -f "$BASE_DIR/voice_sanad.py" >/dev/null 2>&1; then
|
|
echo "🛑 Stopping previous legacy voice_sanad.py processes..."
|
|
pkill -f "$BASE_DIR/voice_sanad.py" >/dev/null 2>&1 || sudo -n pkill -f "$BASE_DIR/voice_sanad.py" >/dev/null 2>&1 || true
|
|
stopped=1
|
|
fi
|
|
|
|
# Previous direct camera servers
|
|
if pgrep -f "$DIRECT_CAMERA_SCRIPT" >/dev/null 2>&1; then
|
|
echo "🛑 Stopping previous direct camera service processes..."
|
|
pkill -f "$DIRECT_CAMERA_SCRIPT" >/dev/null 2>&1 || sudo -n pkill -f "$DIRECT_CAMERA_SCRIPT" >/dev/null 2>&1 || true
|
|
stopped=1
|
|
fi
|
|
if [[ "$DIRECT_CAMERA_SCRIPT" != "$LEGACY_DIRECT_CAMERA_SCRIPT" ]] && pgrep -f "$LEGACY_DIRECT_CAMERA_SCRIPT" >/dev/null 2>&1; then
|
|
echo "🛑 Stopping previous legacy direct_camera_samples_server.py processes..."
|
|
pkill -f "$LEGACY_DIRECT_CAMERA_SCRIPT" >/dev/null 2>&1 || sudo -n pkill -f "$LEGACY_DIRECT_CAMERA_SCRIPT" >/dev/null 2>&1 || true
|
|
stopped=1
|
|
fi
|
|
|
|
# Give OS a moment to release sockets/resources.
|
|
if [[ "$stopped" -eq 1 ]]; then
|
|
sleep 1
|
|
fi
|
|
}
|
|
|
|
# Try to cache sudo early so stale cleanup can stop root-owned leftovers.
|
|
setup_sudo_cache
|
|
|
|
if [[ "$PRE_KILL_OLD" == "1" ]]; then
|
|
stop_stale_processes
|
|
fi
|
|
|
|
PORT_OWNER="$(show_port_owner "$PHOTO_SERVER_PORT" || true)"
|
|
if [[ -n "${PORT_OWNER:-}" ]]; then
|
|
echo "⚠️ Port $PHOTO_SERVER_PORT currently in use:"
|
|
echo "$PORT_OWNER"
|
|
fi
|
|
|
|
# ---------- conda init ----------
|
|
if [[ ! -f "$CONDA_SH" ]]; then
|
|
echo "❌ conda.sh not found: $CONDA_SH"
|
|
exit 1
|
|
fi
|
|
# shellcheck disable=SC1090
|
|
source "$CONDA_SH"
|
|
|
|
# ---------- time sync wait ----------
|
|
echo "⏰ Checking system time..."
|
|
while [ "$(date +%Y)" -lt 2025 ]; do
|
|
echo "⏳ Waiting for time sync... $(date)"
|
|
sleep 2
|
|
done
|
|
echo "✅ Time synced: $(date)"
|
|
|
|
# ---------- PulseAudio defaults (optional) ----------
|
|
pick_pactl_device() {
|
|
local kind="$1"
|
|
local requested="$2"
|
|
local current=""
|
|
local names=""
|
|
local chosen=""
|
|
|
|
if [[ "$kind" == "sink" ]]; then
|
|
current="$(pactl info 2>/dev/null | sed -n 's/^Default Sink: //p' | head -n1)"
|
|
names="$(pactl list short sinks 2>/dev/null | awk '{print $2}')"
|
|
else
|
|
current="$(pactl info 2>/dev/null | sed -n 's/^Default Source: //p' | head -n1)"
|
|
names="$(pactl list short sources 2>/dev/null | awk '{print $2}')"
|
|
fi
|
|
|
|
if [[ -n "${requested:-}" ]] && echo "$names" | grep -Fxq "$requested"; then
|
|
chosen="$requested"
|
|
elif [[ -n "${current:-}" ]] && echo "$names" | grep -Fxq "$current"; then
|
|
chosen="$current"
|
|
else
|
|
chosen="$(echo "$names" | grep -Ei 'anker|powerconf|usb' | head -n1 || true)"
|
|
if [[ -z "${chosen:-}" ]]; then
|
|
chosen="$(echo "$names" | head -n1 || true)"
|
|
fi
|
|
fi
|
|
|
|
printf '%s' "$chosen"
|
|
}
|
|
|
|
if command -v pactl >/dev/null 2>&1; then
|
|
echo "🔊 Checking audio server..."
|
|
READY=0
|
|
for _ in {1..20}; do
|
|
if timeout 0.3s pactl info >/dev/null 2>&1; then
|
|
READY=1
|
|
break
|
|
fi
|
|
sleep 0.3
|
|
done
|
|
|
|
if [[ "$READY" -eq 1 ]]; then
|
|
RESOLVED_SINK="$(pick_pactl_device sink "$SINK")"
|
|
RESOLVED_SOURCE="$(pick_pactl_device source "$SOURCE")"
|
|
|
|
if [[ -n "${RESOLVED_SINK:-}" ]]; then
|
|
if [[ "$RESOLVED_SINK" != "$SINK" ]]; then
|
|
echo "⚠️ Requested speaker not found. Using → $RESOLVED_SINK"
|
|
else
|
|
echo "🎧 Setting default speaker → $RESOLVED_SINK"
|
|
fi
|
|
pactl set-default-sink "$RESOLVED_SINK" || true
|
|
else
|
|
echo "⚠️ No PulseAudio sink found. Continuing without speaker selection."
|
|
fi
|
|
|
|
if [[ -n "${RESOLVED_SOURCE:-}" ]]; then
|
|
if [[ "$RESOLVED_SOURCE" != "$SOURCE" ]]; then
|
|
echo "⚠️ Requested microphone not found. Using → $RESOLVED_SOURCE"
|
|
else
|
|
echo "🎤 Setting default microphone → $RESOLVED_SOURCE"
|
|
fi
|
|
pactl set-default-source "$RESOLVED_SOURCE" || true
|
|
else
|
|
echo "⚠️ No PulseAudio source found. Continuing without microphone selection."
|
|
fi
|
|
|
|
ACTIVE_SINK="$(pactl info 2>/dev/null | sed -n 's/^Default Sink: //p' | head -n1 || true)"
|
|
ACTIVE_SOURCE="$(pactl info 2>/dev/null | sed -n 's/^Default Source: //p' | head -n1 || true)"
|
|
[[ -n "${ACTIVE_SINK:-}" ]] && echo "🔈 Active speaker default: $ACTIVE_SINK"
|
|
[[ -n "${ACTIVE_SOURCE:-}" ]] && echo "🎙️ Active microphone default: $ACTIVE_SOURCE"
|
|
else
|
|
echo "⚠️ PulseAudio/PipeWire not ready. Continuing anyway."
|
|
fi
|
|
else
|
|
echo "⚠️ pactl not found. Skipping audio defaults."
|
|
fi
|
|
|
|
# ---------- paths check ----------
|
|
if [[ ! -f "$PY_FILE" ]]; then
|
|
echo "❌ voice_sanad.py not found: $PY_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
# ---------- startup mode / runtime profile ----------
|
|
# Primary config lives in Data/Settings/config.json. Keep legacy Data/config.json
|
|
# and Scripts/config.json fallbacks so older robot copies can still boot during migrations.
|
|
MODE_FROM_CFG="$(python - <<'PY'
|
|
import json
|
|
from pathlib import Path
|
|
p = Path("Data/Settings/config.json")
|
|
legacy_candidates = [Path("Data/config.json"), Path("Scripts/config.json")]
|
|
if not p.exists():
|
|
for legacy in legacy_candidates:
|
|
if legacy.exists():
|
|
p = legacy
|
|
break
|
|
mode = "manual"
|
|
try:
|
|
if p.exists():
|
|
d = json.loads(p.read_text(encoding="utf-8"))
|
|
mode_block = d.get("mode") or {}
|
|
mode = str(mode_block.get("default_mode", mode_block.get("current_mode", "manual"))).strip().lower()
|
|
except Exception:
|
|
mode = "manual"
|
|
if mode == "command":
|
|
mode = "ai"
|
|
try:
|
|
if p.exists():
|
|
d = json.loads(p.read_text(encoding="utf-8"))
|
|
mode_block = d.get("mode") or {}
|
|
mode_block["current_mode"] = mode
|
|
d["mode"] = mode_block
|
|
p.write_text(json.dumps(d, indent=2), encoding="utf-8")
|
|
except Exception:
|
|
pass
|
|
print(mode if mode in ("manual", "ai") else "manual")
|
|
PY
|
|
)"
|
|
echo "✅ Runtime mode reset to default: $MODE_FROM_CFG"
|
|
|
|
PHOTO_OUTPUT_DIR="$(python - <<'PY'
|
|
import json
|
|
from pathlib import Path
|
|
base = Path(".").resolve()
|
|
p = Path("Data/Settings/config.json")
|
|
legacy_candidates = [Path("Data/config.json"), Path("Scripts/config.json")]
|
|
if not p.exists():
|
|
for legacy in legacy_candidates:
|
|
if legacy.exists():
|
|
p = legacy
|
|
break
|
|
value = "AI_Photographer/photos/Captures"
|
|
try:
|
|
if p.exists():
|
|
d = json.loads(p.read_text(encoding="utf-8"))
|
|
paths = d.get("paths") or {}
|
|
value = str(paths.get("photos_dir", value) or value).strip()
|
|
except Exception:
|
|
pass
|
|
target = Path(value).expanduser()
|
|
if not target.is_absolute():
|
|
parts = target.parts
|
|
if parts and parts[0] == base.name:
|
|
target = (base.parent / target).resolve()
|
|
else:
|
|
target = (base / target).resolve()
|
|
print(target)
|
|
PY
|
|
)"
|
|
echo "🗂️ Capture output dir: $PHOTO_OUTPUT_DIR"
|
|
|
|
if [[ -z "${MANUAL_LEAN_RUNTIME:-}" ]]; then
|
|
if [[ "$MODE_FROM_CFG" == "manual" ]]; then
|
|
export MANUAL_LEAN_RUNTIME=0
|
|
else
|
|
export MANUAL_LEAN_RUNTIME=0
|
|
fi
|
|
fi
|
|
if [[ "$MODE_FROM_CFG" == "manual" && "${MANUAL_LEAN_RUNTIME}" == "1" ]]; then
|
|
echo "🪶 Manual lean runtime enabled: direct camera/DDS/replay startup will be skipped."
|
|
fi
|
|
|
|
# ---------- start direct camera server (py3.10) ----------
|
|
TELE_PID=""
|
|
TELE_LOG="$LOG_DIR/direct_camera.log"
|
|
if [[ "$MODE_FROM_CFG" == "manual" && "${MANUAL_LEAN_RUNTIME}" == "1" ]]; then
|
|
echo "📷 Skipping direct camera startup for lean manual runtime."
|
|
else
|
|
kill_port_owner "$DIRECT_CAMERA_PORT"
|
|
echo "🐍 Activating camera env: $ENV_TELE"
|
|
conda activate "$ENV_TELE"
|
|
|
|
echo "🐍 camera env python: $(which python)"
|
|
python -c "import sys; print('✅ camera env sys.executable:', sys.executable); print('✅ camera env sys.version:', sys.version.split()[0])"
|
|
|
|
echo "📷 Starting direct camera server in background..."
|
|
python "$DIRECT_CAMERA_SCRIPT" \
|
|
--camera "$DIRECT_CAMERA_SOURCE" \
|
|
--host "$DIRECT_CAMERA_HOST" \
|
|
--port "$DIRECT_CAMERA_PORT" \
|
|
--samples-dir "$PHOTO_OUTPUT_DIR" \
|
|
--capture-prefix "$PHOTO_PREFIX" \
|
|
--capture-ext jpg >"$TELE_LOG" 2>&1 &
|
|
TELE_PID=$!
|
|
echo "✅ direct camera PID=$TELE_PID log=$TELE_LOG"
|
|
fi
|
|
|
|
cleanup() {
|
|
echo ""
|
|
echo "🧹 Cleaning up..."
|
|
if [[ -n "${SUDO_KEEPALIVE_PID:-}" ]] && kill -0 "$SUDO_KEEPALIVE_PID" >/dev/null 2>&1; then
|
|
echo "🛑 Stopping sudo keepalive PID=$SUDO_KEEPALIVE_PID"
|
|
kill "$SUDO_KEEPALIVE_PID" >/dev/null 2>&1 || true
|
|
wait "$SUDO_KEEPALIVE_PID" >/dev/null 2>&1 || true
|
|
fi
|
|
if [[ -n "${TELE_PID:-}" ]] && kill -0 "$TELE_PID" >/dev/null 2>&1; then
|
|
echo "🛑 Stopping direct camera server PID=$TELE_PID"
|
|
kill "$TELE_PID" >/dev/null 2>&1 || true
|
|
wait "$TELE_PID" >/dev/null 2>&1 || true
|
|
fi
|
|
echo "✅ Done."
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Give server time to start
|
|
if [[ -n "${TELE_PID:-}" ]]; then
|
|
sleep 2
|
|
fi
|
|
|
|
# ---------- run sanad (py3.11) ----------
|
|
echo "🐍 Activating gemini env: $ENV_GEM"
|
|
conda activate "$ENV_GEM"
|
|
|
|
echo "🐍 gemini python: $(which python)"
|
|
python -c "import sys; print('✅ gemini sys.executable:', sys.executable); print('✅ gemini sys.version:', sys.version.split()[0])"
|
|
|
|
# ---------- autonomous enable policy ----------
|
|
# If AUTONOMOUS_ENABLE is not explicitly provided, derive from startup default mode:
|
|
# - full runtime -> arm autonomous services so AI mode can start live without restart
|
|
# - lean manual runtime -> keep disabled
|
|
if [[ -z "${AUTONOMOUS_ENABLE:-}" ]]; then
|
|
if [[ "$MODE_FROM_CFG" == "manual" && "${MANUAL_LEAN_RUNTIME}" == "1" ]]; then
|
|
export AUTONOMOUS_ENABLE=0
|
|
else
|
|
export AUTONOMOUS_ENABLE=1
|
|
fi
|
|
echo "🤖 AUTONOMOUS_ENABLE auto-set to $AUTONOMOUS_ENABLE (mode=$MODE_FROM_CFG, lean_manual=${MANUAL_LEAN_RUNTIME})"
|
|
else
|
|
echo "🤖 AUTONOMOUS_ENABLE preset to $AUTONOMOUS_ENABLE"
|
|
fi
|
|
|
|
# Ensure unitree_sdk2py can import using the local project modules.
|
|
export PYTHONPATH="$SDK_PY_PATH:${BASE_DIR}:${PYTHONPATH:-}"
|
|
echo "🔧 Using local AI_Photographer modules"
|
|
export DIRECT_CAMERA_ENABLED="${DIRECT_CAMERA_ENABLED:-1}"
|
|
export DIRECT_CAMERA_SERVER_URL="${DIRECT_CAMERA_SERVER_URL:-http://$DIRECT_CAMERA_HOST:$DIRECT_CAMERA_PORT}"
|
|
|
|
echo "🔧 PYTHONPATH=$PYTHONPATH"
|
|
echo "📷 DIRECT_CAMERA_SERVER_URL=$DIRECT_CAMERA_SERVER_URL"
|
|
echo "🔧 ALSA_PLUGIN_DIR=${ALSA_PLUGIN_DIR:-"(not set)"}"
|
|
echo "🔧 ALSA_CONFIG_PATH=${ALSA_CONFIG_PATH:-"(not set)"}"
|
|
echo "🎥 CAMERA_DEVICE=$CAMERA_DEVICE"
|
|
echo "🖼️ PHOTO_PREFIX=$PHOTO_PREFIX"
|
|
echo "🔧 FIX_SCRIPT=$FIX_SCRIPT"
|
|
echo "🪵 LOG_DIR=$LOG_DIR"
|
|
|
|
# sudo cache is handled earlier by setup_sudo_cache.
|
|
|
|
if [[ "$AUTO_FIX_ON_START" == "1" ]]; then
|
|
if [[ -f "$FIX_SCRIPT" ]]; then
|
|
echo "🛠️ Running RealSense fix on startup..."
|
|
if sudo bash "$FIX_SCRIPT" --fix; then
|
|
echo "✅ RealSense fix completed."
|
|
else
|
|
echo "⚠️ RealSense fix failed; continuing startup."
|
|
fi
|
|
else
|
|
echo "⚠️ AUTO_FIX_ON_START=1 but fix script not found: $FIX_SCRIPT"
|
|
fi
|
|
fi
|
|
|
|
# Final port cleanup right before launching main app (after sudo caching)
|
|
if [[ "$FREE_PORT_BEFORE_START" == "1" ]]; then
|
|
kill_port_owner "$PHOTO_SERVER_PORT"
|
|
fi
|
|
PORT_OWNER="$(show_port_owner "$PHOTO_SERVER_PORT" || true)"
|
|
if [[ -n "${PORT_OWNER:-}" ]]; then
|
|
echo "⚠️ Port $PHOTO_SERVER_PORT remains in use:"
|
|
echo "$PORT_OWNER"
|
|
report_port_owners "$PHOTO_SERVER_PORT"
|
|
if [[ "$ALLOW_EXISTING_PORT_SERVER" != "1" ]]; then
|
|
echo "❌ Refusing to continue while a stale server still owns port $PHOTO_SERVER_PORT."
|
|
echo " Set ALLOW_EXISTING_PORT_SERVER=1 only if you intentionally want to attach to the existing server."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo "🚀 Starting Sanad AI_Photographer..."
|
|
# Run in foreground (so trap cleanup runs when process exits)
|
|
python "$PY_FILE"
|