saltylab-firmware/jetson/systemd/install_systemd.sh
sl-perception 26e71d7a14 feat: systemd auto-start for ROS2 + dashboard on Orin boot (bd-1hyn)
Implements full boot-time auto-start for the SaltyBot ROS2 stack on
Jetson Orin. Everything comes up automatically after power-on with
correct dependency ordering and restart-on-failure for each service.

New systemd services:
  saltybot-ros2.service         full_stack.launch.py (perception + SLAM + Nav2)
  saltybot-esp32-serial.service ESP32-S3 BALANCE UART bridge (bd-wim1, PR #727)
  saltybot-here4.service        Here4 DroneCAN GPS bridge (bd-p47c, PR #728)
  saltybot-dashboard.service    Web dashboard on port 8080

Updated:
  saltybot.target               now Wants all four new services with
                                boot-order comments
  can-bringup.service           bitrate 500 kbps → 1 Mbps (DroneCAN for Here4)
  70-canable.rules              remove bitrate from udev RUN+=; let service
                                own the bitrate, add TAG+=systemd for device unit
  install_systemd.sh            installs all services + udev rules, colcon
                                build, enables mosquitto, usermod dialout
  full_stack.launch.py          resolve 8 merge conflict markers (ESP32-S3
                                rename) and fix missing indent on
                                enable_mission_logging_arg — file was
                                un-launchable with SyntaxError

New:
  scripts/ros2-launch.sh        sources ROS2 Humble + workspace overlay,
                                then exec ros2 launch — used by all
                                ROS2 service units via ExecStart=
  udev/80-esp32.rules           /dev/esp32-balance (CH343) and
                                /dev/esp32-io (ESP32-S3 native USB CDC)

Resolves bd-1hyn

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 22:20:40 -04:00

195 lines
7.1 KiB
Bash

#!/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://<orin-ip>:8080"
log "rosbridge: ws://<orin-ip>: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."