#!/usr/bin/env bash # install_systemd.sh — Deploy and enable saltybot systemd services on Orin # # Run as root: sudo ./systemd/install_systemd.sh # # What this does: # 1. Deploy repo to /opt/saltybot/jetson # 2. Build ROS2 workspace (colcon) # 3. Install systemd unit files # 4. Install udev rules (CAN, ESP32) # 5. Enable and optionally start all services # # Services installed (start order): # can-bringup.service CANable2 @ 1 Mbps DroneCAN (Here4 GPS) # saltybot-esp32-serial.service ESP32-S3 BALANCE UART bridge (bd-wim1) # saltybot-here4.service Here4 GPS DroneCAN bridge (bd-p47c) # saltybot-ros2.service Full ROS2 stack (perception + nav) # saltybot-dashboard.service Web dashboard on port 8080 # saltybot-social.service Social-bot stack (speech + LLM + face) # tailscale-vpn.service Tailscale VPN for remote access # # Prerequisites: # apt install ros-humble-desktop python3-colcon-common-extensions # pip install dronecan (for Here4 GPS node) # usermod -aG dialout orin (for serial port access) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(dirname "${SCRIPT_DIR}")" SYSTEMD_DIR="/etc/systemd/system" DEPLOY_DIR="/opt/saltybot/jetson" SCRIPTS_DIR="${DEPLOY_DIR}/scripts" UDEV_DIR="/etc/udev/rules.d" BRINGUP_SYSTEMD="${REPO_DIR}/ros2_ws/src/saltybot_bringup/systemd" BRINGUP_UDEV="${REPO_DIR}/ros2_ws/src/saltybot_bringup/udev" WS_SRC="${REPO_DIR}/ros2_ws/src" WS_BUILD="${DEPLOY_DIR}/ros2_ws" ROS_DISTRO="${ROS_DISTRO:-humble}" ROS_SETUP="/opt/ros/${ROS_DISTRO}/setup.bash" ORIN_USER="${SALTYBOT_USER:-orin}" log() { echo "[install_systemd] $*"; } warn() { echo "[install_systemd] WARN: $*" >&2; } die() { echo "[install_systemd] ERROR: $*" >&2; exit 1; } [[ "$(id -u)" == "0" ]] || die "Run as root (sudo $0)" [[ -f "${ROS_SETUP}" ]] || die "ROS2 ${ROS_DISTRO} not found at ${ROS_SETUP}" # ── 1. Deploy repo ───────────────────────────────────────────────────────────── log "Deploying repo to ${DEPLOY_DIR}..." mkdir -p "${DEPLOY_DIR}" rsync -a \ --exclude='.git' \ --exclude='__pycache__' \ --exclude='*.pyc' \ --exclude='.pytest_cache' \ --exclude='build/' \ --exclude='install/' \ --exclude='log/' \ "${REPO_DIR}/" "${DEPLOY_DIR}/" # Install launch wrapper script log "Installing ros2-launch.sh..." install -m 755 "${SCRIPT_DIR}/../scripts/ros2-launch.sh" "/opt/saltybot/scripts/ros2-launch.sh" # ── 2. Build ROS2 workspace ──────────────────────────────────────────────────── if command -v colcon &>/dev/null; then log "Building ROS2 workspace (colcon)..." # shellcheck disable=SC1090 source "${ROS_SETUP}" ( cd "${WS_BUILD}" colcon build \ --symlink-install \ --cmake-args -DCMAKE_BUILD_TYPE=Release \ 2>&1 | tee /tmp/colcon-build.log ) log "Workspace build complete." else warn "colcon not found — skipping workspace build." warn "Run manually: cd ${WS_BUILD} && colcon build --symlink-install" fi # ── 3. Install systemd units ─────────────────────────────────────────────────── log "Installing systemd units..." # Units from jetson/systemd/ for unit in \ saltybot.target \ saltybot-ros2.service \ saltybot-esp32-serial.service \ saltybot-here4.service \ saltybot-dashboard.service \ saltybot-social.service \ tailscale-vpn.service do if [[ -f "${SCRIPT_DIR}/${unit}" ]]; then cp "${SCRIPT_DIR}/${unit}" "${SYSTEMD_DIR}/" log " Installed ${unit}" else warn " ${unit} not found in ${SCRIPT_DIR}/ — skipping" fi done # Units from saltybot_bringup/systemd/ for unit in \ can-bringup.service \ chromium-kiosk.service \ magedok-display.service \ salty-face-server.service do if [[ -f "${BRINGUP_SYSTEMD}/${unit}" ]]; then cp "${BRINGUP_SYSTEMD}/${unit}" "${SYSTEMD_DIR}/" log " Installed ${unit} (from bringup)" else warn " ${unit} not found in bringup systemd/ — skipping" fi done # ── 4. Install udev rules ────────────────────────────────────────────────────── log "Installing udev rules..." mkdir -p "${UDEV_DIR}" for rule in \ 70-canable.rules \ 80-esp32.rules \ 90-magedok-touch.rules do if [[ -f "${BRINGUP_UDEV}/${rule}" ]]; then cp "${BRINGUP_UDEV}/${rule}" "${UDEV_DIR}/" log " Installed ${rule}" else warn " ${rule} not found — skipping" fi done udevadm control --reload udevadm trigger --subsystem-match=net --action=add udevadm trigger --subsystem-match=tty --action=add log "udev rules reloaded." # ── 5. Set permissions ───────────────────────────────────────────────────────── log "Ensuring '${ORIN_USER}' is in dialout group (serial port access)..." if id "${ORIN_USER}" &>/dev/null; then usermod -aG dialout "${ORIN_USER}" || warn "usermod failed — add ${ORIN_USER} to dialout manually" else warn "User '${ORIN_USER}' not found — skip usermod" fi # ── 6. Reload systemd and enable services ───────────────────────────────────── log "Reloading systemd daemon..." systemctl daemon-reload log "Enabling services..." systemctl enable \ saltybot.target \ can-bringup.service \ saltybot-esp32-serial.service \ saltybot-here4.service \ saltybot-ros2.service \ saltybot-dashboard.service \ saltybot-social.service \ tailscale-vpn.service # Enable mosquitto (MQTT broker) if installed if systemctl list-unit-files mosquitto.service &>/dev/null; then systemctl enable mosquitto.service log " Enabled mosquitto.service" fi # ── 7. Summary ──────────────────────────────────────────────────────────────── log "" log "Installation complete." log "" log "Services will start automatically on next reboot." log "To start now without rebooting:" log " sudo systemctl start saltybot.target" log "" log "Check status:" log " systemctl status can-bringup saltybot-esp32-serial saltybot-here4 saltybot-ros2 saltybot-dashboard" log "" log "Live logs:" log " journalctl -fu can-bringup" log " journalctl -fu saltybot-esp32-serial" log " journalctl -fu saltybot-here4" log " journalctl -fu saltybot-ros2" log " journalctl -fu saltybot-dashboard" log "" log "Dashboard: http://:8080" log "rosbridge: ws://:9090" log "" log "Note: saltybot-esp32-serial and saltybot-here4 require packages" log " from bd-wim1 (PR #727) and bd-p47c (PR #728) to be merged."