install.sh 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. #!/usr/bin/env bash
  2. #
  3. # SpoolBuddy Installation Script for Raspberry Pi
  4. #
  5. # Supports two scenarios:
  6. # 1) SpoolBuddy only — NFC/scale companion connecting to a remote Bambuddy instance
  7. # 2) SpoolBuddy + Bambuddy — both running natively on this Raspberry Pi
  8. #
  9. # Usage:
  10. # Interactive: curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/spoolbuddy/install.sh -o install.sh && chmod +x install.sh && sudo ./install.sh
  11. # Unattended: sudo ./install.sh --mode spoolbuddy --bambuddy-url http://192.168.1.100:8000 --api-key bb_xxx --yes
  12. #
  13. # Options:
  14. # --mode MODE Installation mode: "spoolbuddy" (companion only) or "full" (both)
  15. # --bambuddy-url URL Bambuddy server URL (required for spoolbuddy mode)
  16. # --api-key KEY Bambuddy API key (required for spoolbuddy mode)
  17. # --path PATH Installation directory (default: /opt/spoolbuddy or /opt/bambuddy)
  18. # --port PORT Bambuddy port (full mode only, default: 8000)
  19. # --yes, -y Non-interactive mode, accept defaults
  20. # --help, -h Show this help message
  21. #
  22. set -e
  23. # ─────────────────────────────────────────────────────────────────────────────
  24. # Constants
  25. # ─────────────────────────────────────────────────────────────────────────────
  26. RED='\033[0;31m'
  27. GREEN='\033[0;32m'
  28. YELLOW='\033[1;33m'
  29. CYAN='\033[0;36m'
  30. BOLD='\033[1m'
  31. NC='\033[0m'
  32. GITHUB_REPO="https://github.com/maziggy/bambuddy.git"
  33. SPOOLBUDDY_SERVICE_USER="spoolbuddy"
  34. BAMBUDDY_SERVICE_USER="bambuddy"
  35. # Packages needed for SpoolBuddy hardware (NFC reader + scale)
  36. SYSTEM_PACKAGES="python3 python3-pip python3-venv python3-dev python3-spidev python3-libgpiod gpiod libgpiod-dev i2c-tools git"
  37. # Python packages for SpoolBuddy daemon
  38. SPOOLBUDDY_PIP_PACKAGES="spidev gpiod smbus2 httpx"
  39. # ─────────────────────────────────────────────────────────────────────────────
  40. # Variables (set by args or prompts)
  41. # ─────────────────────────────────────────────────────────────────────────────
  42. INSTALL_MODE="" # "spoolbuddy" or "full"
  43. INSTALL_PATH=""
  44. BAMBUDDY_URL=""
  45. API_KEY=""
  46. BAMBUDDY_PORT="8000"
  47. NON_INTERACTIVE="false"
  48. REBOOT_NEEDED="false"
  49. # ─────────────────────────────────────────────────────────────────────────────
  50. # Helpers
  51. # ─────────────────────────────────────────────────────────────────────────────
  52. info() { echo -e "${CYAN}[INFO]${NC} $1"; }
  53. success() { echo -e "${GREEN}[OK]${NC} $1"; }
  54. warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
  55. error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
  56. # Run a long-running command with a spinner + live progress output.
  57. # Usage: run_with_progress "description" command [args...]
  58. run_with_progress() {
  59. local desc="$1"
  60. shift
  61. local log_file
  62. log_file=$(mktemp /tmp/spoolbuddy-install.XXXXXX)
  63. local start_time=$SECONDS
  64. # Run command in background, capture stdout+stderr
  65. "$@" > "$log_file" 2>&1 &
  66. local pid=$!
  67. # Spinner frames (braille pattern)
  68. local -a spin=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
  69. local i=0
  70. while kill -0 "$pid" 2>/dev/null; do
  71. local elapsed=$(( SECONDS - start_time ))
  72. local time_str
  73. if (( elapsed >= 60 )); then
  74. time_str="$(( elapsed / 60 ))m$(printf '%02d' $(( elapsed % 60 )))s"
  75. else
  76. time_str="${elapsed}s"
  77. fi
  78. # Last chunk of output (handles \r progress lines and regular \n lines)
  79. local last_line=""
  80. last_line=$(tail -c 4096 "$log_file" 2>/dev/null | tr '\r' '\n' | sed 's/\x1b\[[0-9;]*[mGKHJ]//g' | sed '/^[[:space:]]*$/d' | tail -1 | sed 's/^[[:space:]]*//' | cut -c1-50) || true
  81. printf "\r ${spin[$((i % 10))]} %-36s ${CYAN}%6s${NC} %s\033[K" "$desc" "$time_str" "$last_line"
  82. i=$(( i + 1 ))
  83. sleep 0.15
  84. done
  85. local exit_code=0
  86. wait "$pid" || exit_code=$?
  87. # Clear spinner line
  88. printf "\r\033[K"
  89. # Format elapsed time for summary
  90. local elapsed=$(( SECONDS - start_time ))
  91. local time_suffix=""
  92. if (( elapsed >= 60 )); then
  93. time_suffix=" ($(( elapsed / 60 ))m $(( elapsed % 60 ))s)"
  94. elif (( elapsed >= 5 )); then
  95. time_suffix=" (${elapsed}s)"
  96. fi
  97. if [[ $exit_code -eq 0 ]]; then
  98. success "${desc}${time_suffix}"
  99. rm -f "$log_file"
  100. else
  101. echo -e "${RED}[FAIL]${NC} ${desc}${time_suffix}"
  102. echo ""
  103. echo -e " ${YELLOW}Last 20 lines:${NC}"
  104. tail -20 "$log_file" 2>/dev/null | sed 's/^/ /'
  105. echo ""
  106. echo -e " Full log: ${CYAN}$log_file${NC}"
  107. exit 1
  108. fi
  109. }
  110. prompt() {
  111. local prompt_text="$1"
  112. local default_value="$2"
  113. local var_name="$3"
  114. if [[ "$NON_INTERACTIVE" == "true" ]]; then
  115. eval "$var_name=\"$default_value\""
  116. return
  117. fi
  118. if [[ -n "$default_value" ]]; then
  119. echo -en "${BOLD}$prompt_text${NC} [${CYAN}$default_value${NC}]: "
  120. else
  121. echo -en "${BOLD}$prompt_text${NC}: "
  122. fi
  123. read -r input
  124. if [[ -z "$input" ]]; then
  125. eval "$var_name=\"$default_value\""
  126. else
  127. eval "$var_name=\"$input\""
  128. fi
  129. }
  130. prompt_yes_no() {
  131. local prompt_text="$1"
  132. local default="$2"
  133. if [[ "$NON_INTERACTIVE" == "true" ]]; then
  134. [[ "$default" == "y" ]] && return 0 || return 1
  135. fi
  136. local yn_hint="[y/n]"
  137. [[ "$default" == "y" ]] && yn_hint="[Y/n]"
  138. [[ "$default" == "n" ]] && yn_hint="[y/N]"
  139. while true; do
  140. echo -en "${BOLD}$prompt_text${NC} $yn_hint: "
  141. read -r yn
  142. [[ -z "$yn" ]] && yn="$default"
  143. case "$yn" in
  144. [Yy]* ) return 0;;
  145. [Nn]* ) return 1;;
  146. * ) echo "Please answer yes or no.";;
  147. esac
  148. done
  149. }
  150. show_help() {
  151. echo "SpoolBuddy Installation Script for Raspberry Pi"
  152. echo ""
  153. echo "Usage: sudo $0 [OPTIONS]"
  154. echo ""
  155. echo "Options:"
  156. echo " --mode MODE \"spoolbuddy\" (companion only) or \"full\" (Bambuddy + SpoolBuddy)"
  157. echo " --bambuddy-url URL Bambuddy server URL (required for spoolbuddy mode)"
  158. echo " --api-key KEY Bambuddy API key (required for spoolbuddy mode)"
  159. echo " --path PATH Installation directory (default: /opt/spoolbuddy or /opt/bambuddy)"
  160. echo " --port PORT Bambuddy port (full mode only, default: 8000)"
  161. echo " --yes, -y Non-interactive mode, accept defaults"
  162. echo " --help, -h Show this help message"
  163. echo ""
  164. echo "Examples:"
  165. echo " Interactive:"
  166. echo " sudo ./install.sh"
  167. echo ""
  168. echo " SpoolBuddy companion (unattended):"
  169. echo " sudo ./install.sh --mode spoolbuddy --bambuddy-url http://192.168.1.100:8000 --api-key bb_xxx -y"
  170. echo ""
  171. echo " Full install (unattended):"
  172. echo " sudo ./install.sh --mode full --port 8000 -y"
  173. exit 0
  174. }
  175. # ─────────────────────────────────────────────────────────────────────────────
  176. # Pre-flight Checks
  177. # ─────────────────────────────────────────────────────────────────────────────
  178. check_root() {
  179. if [[ $EUID -ne 0 ]]; then
  180. error "This script must be run as root (use sudo)"
  181. fi
  182. }
  183. check_raspberry_pi() {
  184. if ! grep -q "Raspberry Pi\|BCM2" /proc/cpuinfo 2>/dev/null; then
  185. error "This script is designed for Raspberry Pi only"
  186. fi
  187. # Detect Pi model for hardware recommendations
  188. local model
  189. model=$(tr -d '\0' < /proc/device-tree/model 2>/dev/null) || model="Unknown"
  190. success "Detected: $model"
  191. }
  192. check_raspberry_pi_os() {
  193. if [[ ! -f /etc/os-release ]]; then
  194. error "Cannot detect operating system"
  195. fi
  196. . /etc/os-release
  197. if [[ "$ID" != "raspbian" && "$ID" != "debian" ]]; then
  198. warn "Expected Raspberry Pi OS (Debian-based), found: $ID"
  199. if ! prompt_yes_no "Continue anyway?" "n"; then
  200. exit 0
  201. fi
  202. fi
  203. success "OS: $PRETTY_NAME"
  204. }
  205. detect_python() {
  206. local cmd=""
  207. if command -v python3 &>/dev/null; then
  208. cmd="python3"
  209. elif command -v python &>/dev/null; then
  210. local ver
  211. ver=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1)
  212. if [[ "$ver" -ge 3 ]]; then
  213. cmd="python"
  214. fi
  215. fi
  216. if [[ -z "$cmd" ]]; then
  217. return 1
  218. fi
  219. local version
  220. version=$($cmd -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
  221. local major minor
  222. major=$(echo "$version" | cut -d'.' -f1)
  223. minor=$(echo "$version" | cut -d'.' -f2)
  224. if [[ "$major" -lt 3 ]] || { [[ "$major" -eq 3 ]] && [[ "$minor" -lt 10 ]]; }; then
  225. warn "Python $version found, but 3.10+ is required"
  226. return 1
  227. fi
  228. PYTHON_CMD="$cmd"
  229. success "Found Python $version"
  230. return 0
  231. }
  232. # ─────────────────────────────────────────────────────────────────────────────
  233. # Raspberry Pi Hardware Configuration
  234. # ─────────────────────────────────────────────────────────────────────────────
  235. enable_spi() {
  236. if raspi-config nonint get_spi 2>/dev/null | grep -q "1"; then
  237. info "Enabling SPI..."
  238. raspi-config nonint do_spi 0
  239. REBOOT_NEEDED="true"
  240. success "SPI enabled"
  241. else
  242. success "SPI already enabled"
  243. fi
  244. }
  245. enable_i2c() {
  246. if raspi-config nonint get_i2c 2>/dev/null | grep -q "1"; then
  247. info "Enabling I2C..."
  248. raspi-config nonint do_i2c 0
  249. REBOOT_NEEDED="true"
  250. success "I2C enabled"
  251. else
  252. success "I2C already enabled"
  253. fi
  254. }
  255. configure_boot_config() {
  256. # Find the boot config file (Bookworm+ uses /boot/firmware/config.txt)
  257. local boot_config="/boot/firmware/config.txt"
  258. if [[ ! -f "$boot_config" ]]; then
  259. boot_config="/boot/config.txt"
  260. fi
  261. if [[ ! -f "$boot_config" ]]; then
  262. warn "Boot config not found at /boot/firmware/config.txt or /boot/config.txt"
  263. warn "You may need to manually add: dtparam=i2c_vc=on and dtoverlay=spi0-0cs"
  264. return
  265. fi
  266. info "Configuring $boot_config..."
  267. # Enable I2C bus 0 (GPIO0/GPIO1) for NAU7802 scale
  268. if ! grep -q "^dtparam=i2c_vc=on" "$boot_config"; then
  269. echo "" >> "$boot_config"
  270. echo "# SpoolBuddy: I2C bus 0 for NAU7802 scale (GPIO0/GPIO1)" >> "$boot_config"
  271. echo "dtparam=i2c_vc=on" >> "$boot_config"
  272. REBOOT_NEEDED="true"
  273. success "Added dtparam=i2c_vc=on"
  274. else
  275. success "dtparam=i2c_vc=on already set"
  276. fi
  277. # Disable SPI auto chip-select (manual CS on GPIO23 for PN5180)
  278. if ! grep -q "^dtoverlay=spi0-0cs" "$boot_config"; then
  279. echo "" >> "$boot_config"
  280. echo "# SpoolBuddy: Disable SPI auto CS (manual CS on GPIO23 for PN5180)" >> "$boot_config"
  281. echo "dtoverlay=spi0-0cs" >> "$boot_config"
  282. REBOOT_NEEDED="true"
  283. success "Added dtoverlay=spi0-0cs"
  284. else
  285. success "dtoverlay=spi0-0cs already set"
  286. fi
  287. }
  288. # ─────────────────────────────────────────────────────────────────────────────
  289. # Package Installation
  290. # ─────────────────────────────────────────────────────────────────────────────
  291. install_system_packages() {
  292. run_with_progress "Updating package lists" apt-get update
  293. run_with_progress "Installing system packages" apt-get install -y $SYSTEM_PACKAGES
  294. }
  295. # ─────────────────────────────────────────────────────────────────────────────
  296. # SpoolBuddy Installation
  297. # ─────────────────────────────────────────────────────────────────────────────
  298. create_spoolbuddy_user() {
  299. if id "$SPOOLBUDDY_SERVICE_USER" &>/dev/null; then
  300. info "User '$SPOOLBUDDY_SERVICE_USER' already exists"
  301. else
  302. info "Creating service user '$SPOOLBUDDY_SERVICE_USER'..."
  303. useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$SPOOLBUDDY_SERVICE_USER"
  304. success "Service user created"
  305. fi
  306. # Add to hardware access groups (gpio, spi, i2c)
  307. for group in gpio spi i2c; do
  308. if getent group "$group" &>/dev/null; then
  309. usermod -aG "$group" "$SPOOLBUDDY_SERVICE_USER" 2>/dev/null || true
  310. fi
  311. done
  312. success "User added to gpio, spi, i2c groups"
  313. }
  314. download_spoolbuddy() {
  315. if [[ -d "$INSTALL_PATH/.git" ]]; then
  316. info "Existing installation found, updating..."
  317. git config --global --add safe.directory "$INSTALL_PATH" 2>/dev/null || true
  318. cd "$INSTALL_PATH"
  319. run_with_progress "Fetching updates" git fetch origin
  320. git reset --hard origin/main > /dev/null 2>&1
  321. else
  322. mkdir -p "$INSTALL_PATH"
  323. run_with_progress "Cloning repository" git clone "$GITHUB_REPO" "$INSTALL_PATH"
  324. fi
  325. chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$INSTALL_PATH"
  326. }
  327. setup_spoolbuddy_venv() {
  328. cd "$INSTALL_PATH/spoolbuddy"
  329. run_with_progress "Creating SpoolBuddy venv" $PYTHON_CMD -m venv --system-site-packages venv
  330. run_with_progress "Upgrading pip" "$INSTALL_PATH/spoolbuddy/venv/bin/pip" install --upgrade pip
  331. run_with_progress "Installing SpoolBuddy packages" "$INSTALL_PATH/spoolbuddy/venv/bin/pip" install $SPOOLBUDDY_PIP_PACKAGES
  332. chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$INSTALL_PATH/spoolbuddy/venv"
  333. }
  334. create_spoolbuddy_env() {
  335. info "Creating SpoolBuddy configuration..."
  336. local env_file="$INSTALL_PATH/spoolbuddy/.env"
  337. cat > "$env_file" << EOF
  338. # SpoolBuddy Configuration
  339. # Generated by install.sh on $(date)
  340. # Bambuddy backend URL
  341. SPOOLBUDDY_BACKEND_URL=$BAMBUDDY_URL
  342. # API key (create one in Bambuddy Settings -> API Keys)
  343. SPOOLBUDDY_API_KEY=$API_KEY
  344. EOF
  345. chown "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$env_file"
  346. chmod 600 "$env_file"
  347. success "Configuration saved to $env_file"
  348. }
  349. create_spoolbuddy_service() {
  350. info "Creating SpoolBuddy systemd service..."
  351. local after_line="After=network-online.target"
  352. if [[ "$INSTALL_MODE" == "full" ]]; then
  353. after_line="After=network-online.target bambuddy.service"
  354. fi
  355. cat > /etc/systemd/system/spoolbuddy.service << EOF
  356. [Unit]
  357. Description=SpoolBuddy - NFC Spool Management Daemon
  358. Documentation=https://github.com/maziggy/bambuddy
  359. $after_line
  360. Wants=network-online.target
  361. [Service]
  362. Type=simple
  363. User=$SPOOLBUDDY_SERVICE_USER
  364. WorkingDirectory=$INSTALL_PATH/spoolbuddy
  365. EnvironmentFile=$INSTALL_PATH/spoolbuddy/.env
  366. ExecStart=$INSTALL_PATH/spoolbuddy/venv/bin/python -m daemon.main
  367. Restart=always
  368. RestartSec=5
  369. StandardOutput=journal
  370. StandardError=journal
  371. [Install]
  372. WantedBy=multi-user.target
  373. EOF
  374. systemctl daemon-reload
  375. systemctl enable spoolbuddy.service
  376. success "SpoolBuddy service created and enabled"
  377. }
  378. # ─────────────────────────────────────────────────────────────────────────────
  379. # Bambuddy Installation (full mode only)
  380. # ─────────────────────────────────────────────────────────────────────────────
  381. create_bambuddy_user() {
  382. if id "$BAMBUDDY_SERVICE_USER" &>/dev/null; then
  383. info "User '$BAMBUDDY_SERVICE_USER' already exists"
  384. return
  385. fi
  386. info "Creating service user '$BAMBUDDY_SERVICE_USER'..."
  387. useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$BAMBUDDY_SERVICE_USER"
  388. success "Service user created"
  389. }
  390. setup_bambuddy_venv() {
  391. cd "$INSTALL_PATH"
  392. run_with_progress "Creating Bambuddy venv" $PYTHON_CMD -m venv venv
  393. run_with_progress "Upgrading pip" "$INSTALL_PATH/venv/bin/pip" install --upgrade pip
  394. run_with_progress "Installing Bambuddy dependencies" "$INSTALL_PATH/venv/bin/pip" install -r requirements.txt
  395. chown -R "$BAMBUDDY_SERVICE_USER:$BAMBUDDY_SERVICE_USER" "$INSTALL_PATH/venv"
  396. }
  397. install_nodejs() {
  398. if command -v node &>/dev/null; then
  399. local version
  400. version=$(node --version 2>/dev/null | sed 's/^v//')
  401. local major
  402. major=$(echo "$version" | cut -d'.' -f1)
  403. if [[ "$major" -ge 20 ]]; then
  404. success "Found Node.js v$version"
  405. return
  406. fi
  407. fi
  408. apt-get remove -y nodejs npm > /dev/null 2>&1 || true
  409. run_with_progress "Setting up Node.js repository" bash -c "curl -fsSL https://deb.nodesource.com/setup_22.x | bash -"
  410. run_with_progress "Installing Node.js" apt-get install -y nodejs
  411. hash -r 2>/dev/null || true
  412. success "Node.js installed: $(node --version)"
  413. }
  414. build_frontend() {
  415. cd "$INSTALL_PATH/frontend"
  416. run_with_progress "Installing frontend dependencies" npm ci
  417. run_with_progress "Building frontend" npm run build
  418. }
  419. create_bambuddy_env() {
  420. info "Creating Bambuddy configuration..."
  421. local env_file="$INSTALL_PATH/.env"
  422. cat > "$env_file" << EOF
  423. # Bambuddy Configuration
  424. # Generated by install.sh on $(date)
  425. DEBUG=false
  426. LOG_LEVEL=INFO
  427. LOG_TO_FILE=true
  428. EOF
  429. chown "$BAMBUDDY_SERVICE_USER:$BAMBUDDY_SERVICE_USER" "$env_file"
  430. chmod 600 "$env_file"
  431. success "Configuration saved to $env_file"
  432. }
  433. create_bambuddy_directories() {
  434. mkdir -p "$INSTALL_PATH/data" "$INSTALL_PATH/logs"
  435. chown -R "$BAMBUDDY_SERVICE_USER:$BAMBUDDY_SERVICE_USER" "$INSTALL_PATH/data" "$INSTALL_PATH/logs"
  436. success "Data directories created"
  437. }
  438. create_bambuddy_service() {
  439. info "Creating Bambuddy systemd service..."
  440. cat > /etc/systemd/system/bambuddy.service << EOF
  441. [Unit]
  442. Description=Bambuddy - Bambu Lab Print Management
  443. Documentation=https://github.com/maziggy/bambuddy
  444. After=network.target
  445. [Service]
  446. Type=simple
  447. User=$BAMBUDDY_SERVICE_USER
  448. Group=$BAMBUDDY_SERVICE_USER
  449. WorkingDirectory=$INSTALL_PATH
  450. EnvironmentFile=$INSTALL_PATH/.env
  451. Environment="DATA_DIR=$INSTALL_PATH/data"
  452. Environment="LOG_DIR=$INSTALL_PATH/logs"
  453. ExecStart=$INSTALL_PATH/venv/bin/uvicorn backend.app.main:app --host 0.0.0.0 --port $BAMBUDDY_PORT
  454. Restart=on-failure
  455. RestartSec=5
  456. StandardOutput=journal
  457. StandardError=journal
  458. NoNewPrivileges=true
  459. PrivateTmp=true
  460. ProtectSystem=strict
  461. ProtectHome=true
  462. ReadWritePaths=$INSTALL_PATH/data $INSTALL_PATH/logs $INSTALL_PATH
  463. [Install]
  464. WantedBy=multi-user.target
  465. EOF
  466. systemctl daemon-reload
  467. systemctl enable bambuddy.service
  468. success "Bambuddy service created and enabled"
  469. }
  470. # ─────────────────────────────────────────────────────────────────────────────
  471. # User Prompts
  472. # ─────────────────────────────────────────────────────────────────────────────
  473. parse_args() {
  474. while [[ $# -gt 0 ]]; do
  475. case "$1" in
  476. --mode)
  477. INSTALL_MODE="$2"
  478. shift 2
  479. ;;
  480. --bambuddy-url)
  481. BAMBUDDY_URL="$2"
  482. shift 2
  483. ;;
  484. --api-key)
  485. API_KEY="$2"
  486. shift 2
  487. ;;
  488. --path)
  489. INSTALL_PATH="$2"
  490. shift 2
  491. ;;
  492. --port)
  493. BAMBUDDY_PORT="$2"
  494. shift 2
  495. ;;
  496. --yes|-y)
  497. NON_INTERACTIVE="true"
  498. shift
  499. ;;
  500. --help|-h)
  501. show_help
  502. ;;
  503. *)
  504. error "Unknown option: $1 (use --help for usage)"
  505. ;;
  506. esac
  507. done
  508. }
  509. ask_install_mode() {
  510. if [[ -n "$INSTALL_MODE" ]]; then
  511. return
  512. fi
  513. echo ""
  514. echo -e "${BOLD}How would you like to set up SpoolBuddy?${NC}"
  515. echo ""
  516. echo -e " ${CYAN}1)${NC} SpoolBuddy only"
  517. echo " NFC reader + scale on this RPi, Bambuddy runs on another device"
  518. echo ""
  519. echo -e " ${CYAN}2)${NC} SpoolBuddy + Bambuddy"
  520. echo " Both running natively on this Raspberry Pi"
  521. echo ""
  522. while true; do
  523. echo -en "${BOLD}Choose${NC} [${CYAN}1${NC}/${CYAN}2${NC}]: "
  524. read -r choice
  525. case "$choice" in
  526. 1) INSTALL_MODE="spoolbuddy"; return;;
  527. 2) INSTALL_MODE="full"; return;;
  528. *) echo "Please enter 1 or 2.";;
  529. esac
  530. done
  531. }
  532. gather_config() {
  533. echo ""
  534. echo -e "${BOLD}Configuration${NC}"
  535. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  536. echo ""
  537. # Set default install path based on mode
  538. if [[ -z "$INSTALL_PATH" ]]; then
  539. if [[ "$INSTALL_MODE" == "full" ]]; then
  540. INSTALL_PATH="/opt/bambuddy"
  541. else
  542. INSTALL_PATH="/opt/bambuddy"
  543. fi
  544. fi
  545. prompt "Installation directory" "$INSTALL_PATH" INSTALL_PATH
  546. if [[ "$INSTALL_MODE" == "spoolbuddy" ]]; then
  547. # Need remote Bambuddy URL and API key
  548. echo ""
  549. info "SpoolBuddy needs to connect to your Bambuddy server."
  550. info "You can find/create an API key in Bambuddy under Settings -> API Keys."
  551. echo ""
  552. while [[ -z "$BAMBUDDY_URL" ]]; do
  553. prompt "Bambuddy server URL (e.g. http://192.168.1.100:8000)" "" BAMBUDDY_URL
  554. if [[ -z "$BAMBUDDY_URL" ]]; then
  555. warn "Bambuddy URL is required"
  556. fi
  557. done
  558. while [[ -z "$API_KEY" ]]; do
  559. prompt "Bambuddy API key" "" API_KEY
  560. if [[ -z "$API_KEY" ]]; then
  561. warn "API key is required"
  562. fi
  563. done
  564. else
  565. # Full mode — Bambuddy runs locally
  566. prompt "Bambuddy port" "$BAMBUDDY_PORT" BAMBUDDY_PORT
  567. BAMBUDDY_URL="http://localhost:$BAMBUDDY_PORT"
  568. echo ""
  569. info "After installation, create an API key in Bambuddy (Settings -> API Keys)"
  570. info "and update it in: $INSTALL_PATH/spoolbuddy/.env"
  571. API_KEY="CHANGE_ME_AFTER_SETUP"
  572. fi
  573. # Summary
  574. echo ""
  575. echo -e "${BOLD}Installation Summary${NC}"
  576. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  577. echo -e " Mode: ${GREEN}$([ "$INSTALL_MODE" == "full" ] && echo "Bambuddy + SpoolBuddy" || echo "SpoolBuddy only")${NC}"
  578. echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}"
  579. if [[ "$INSTALL_MODE" == "full" ]]; then
  580. echo -e " Bambuddy port: ${GREEN}$BAMBUDDY_PORT${NC}"
  581. echo -e " Bambuddy URL: ${GREEN}$BAMBUDDY_URL${NC}"
  582. else
  583. echo -e " Bambuddy URL: ${GREEN}$BAMBUDDY_URL${NC}"
  584. fi
  585. echo ""
  586. if ! prompt_yes_no "Proceed with installation?" "y"; then
  587. echo "Installation cancelled."
  588. exit 0
  589. fi
  590. }
  591. # ─────────────────────────────────────────────────────────────────────────────
  592. # Main
  593. # ─────────────────────────────────────────────────────────────────────────────
  594. main() {
  595. parse_args "$@"
  596. echo ""
  597. echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
  598. echo -e "${CYAN}║ ║${NC}"
  599. echo -e "${CYAN}║ ____ _ ____ _ _ ║${NC}"
  600. echo -e "${CYAN}║ / ___| _ __ ___ ___ | | __ ) _ _ __| | __| |_ _ ║${NC}"
  601. echo -e "${CYAN}║ \\___ \\| '_ \\ / _ \\ / _ \\| | _ \\| | | |/ _\` |/ _\` | | | |║${NC}"
  602. echo -e "${CYAN}║ ___) | |_) | (_) | (_) | | |_) | |_| | (_| | (_| | |_| |║${NC}"
  603. echo -e "${CYAN}║ |____/| .__/ \\___/ \\___/|_|____/ \\__,_|\\__,_|\\__,_|\\__, |║${NC}"
  604. echo -e "${CYAN}║ |_| |___/ ║${NC}"
  605. echo -e "${CYAN}║ ║${NC}"
  606. echo -e "${CYAN}║ NFC Spool Management for Bambuddy ║${NC}"
  607. echo -e "${CYAN}║ ║${NC}"
  608. echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
  609. echo ""
  610. # Check if running via pipe without -y
  611. if [[ ! -t 0 ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then
  612. error "Interactive mode requires a terminal. Use -y for unattended install, or download and run directly."
  613. fi
  614. # Pre-flight checks
  615. check_root
  616. check_raspberry_pi
  617. check_raspberry_pi_os
  618. if ! detect_python; then
  619. info "Python 3.10+ not found, will install..."
  620. fi
  621. # Gather user preferences
  622. ask_install_mode
  623. gather_config
  624. # Validate mode
  625. if [[ "$INSTALL_MODE" != "spoolbuddy" && "$INSTALL_MODE" != "full" ]]; then
  626. error "Invalid mode: $INSTALL_MODE (must be 'spoolbuddy' or 'full')"
  627. fi
  628. echo ""
  629. echo -e "${BOLD}Starting Installation${NC}"
  630. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  631. echo ""
  632. # ── Step 1: Raspberry Pi hardware config ──────────────────────────────
  633. info "Configuring Raspberry Pi hardware..."
  634. enable_spi
  635. enable_i2c
  636. configure_boot_config
  637. echo ""
  638. # ── Step 2: System packages ───────────────────────────────────────────
  639. install_system_packages
  640. detect_python || error "Failed to install Python 3.10+"
  641. echo ""
  642. # ── Step 3: Download source code ──────────────────────────────────────
  643. create_spoolbuddy_user
  644. download_spoolbuddy
  645. echo ""
  646. # ── Step 4: SpoolBuddy setup ──────────────────────────────────────────
  647. info "Setting up SpoolBuddy..."
  648. setup_spoolbuddy_venv
  649. create_spoolbuddy_env
  650. create_spoolbuddy_service
  651. echo ""
  652. # ── Step 5: Bambuddy setup (full mode only) ───────────────────────────
  653. if [[ "$INSTALL_MODE" == "full" ]]; then
  654. info "Setting up Bambuddy..."
  655. create_bambuddy_user
  656. setup_bambuddy_venv
  657. install_nodejs
  658. build_frontend
  659. create_bambuddy_directories
  660. create_bambuddy_env
  661. create_bambuddy_service
  662. echo ""
  663. fi
  664. # ── Done ──────────────────────────────────────────────────────────────
  665. echo ""
  666. echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
  667. echo -e "${GREEN}║ ║${NC}"
  668. echo -e "${GREEN}║ Installation Complete! ║${NC}"
  669. echo -e "${GREEN}║ ║${NC}"
  670. echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
  671. echo ""
  672. if [[ "$INSTALL_MODE" == "full" ]]; then
  673. local ip_addr
  674. ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}') || ip_addr="<your-ip>"
  675. echo -e " ${BOLD}Bambuddy:${NC} ${CYAN}http://$ip_addr:$BAMBUDDY_PORT${NC}"
  676. echo ""
  677. echo -e " ${BOLD}Next steps:${NC}"
  678. echo -e " 1. Reboot to apply hardware changes"
  679. echo -e " 2. Open Bambuddy in your browser"
  680. echo -e " 3. Go to Settings -> API Keys and create an API key"
  681. echo -e " 4. Update the API key in: ${CYAN}$INSTALL_PATH/spoolbuddy/.env${NC}"
  682. echo -e " 5. Restart SpoolBuddy: ${CYAN}sudo systemctl restart spoolbuddy${NC}"
  683. else
  684. echo -e " ${BOLD}SpoolBuddy:${NC} Connecting to ${CYAN}$BAMBUDDY_URL${NC}"
  685. fi
  686. echo ""
  687. echo -e " ${BOLD}Manage services:${NC}"
  688. echo -e " SpoolBuddy status: ${CYAN}sudo systemctl status spoolbuddy${NC}"
  689. echo -e " SpoolBuddy logs: ${CYAN}sudo journalctl -u spoolbuddy -f${NC}"
  690. if [[ "$INSTALL_MODE" == "full" ]]; then
  691. echo -e " Bambuddy status: ${CYAN}sudo systemctl status bambuddy${NC}"
  692. echo -e " Bambuddy logs: ${CYAN}sudo journalctl -u bambuddy -f${NC}"
  693. fi
  694. echo ""
  695. echo -e " ${BOLD}Configuration:${NC} ${CYAN}$INSTALL_PATH/spoolbuddy/.env${NC}"
  696. echo -e " ${BOLD}Hardware wiring:${NC} ${CYAN}$INSTALL_PATH/spoolbuddy/README.md${NC}"
  697. echo -e " ${BOLD}Diagnostics:${NC} ${CYAN}sudo $INSTALL_PATH/spoolbuddy/venv/bin/python $INSTALL_PATH/spoolbuddy/pn5180_diag.py${NC}"
  698. echo ""
  699. if [[ "$REBOOT_NEEDED" == "true" ]]; then
  700. echo -e " ${YELLOW}A reboot is required to apply SPI/I2C changes.${NC}"
  701. echo ""
  702. if prompt_yes_no "Reboot now?" "y"; then
  703. reboot
  704. else
  705. echo -e " Run ${CYAN}sudo reboot${NC} when ready."
  706. fi
  707. else
  708. # SPI/I2C already configured — start services now
  709. if prompt_yes_no "Start services now?" "y"; then
  710. if [[ "$INSTALL_MODE" == "full" ]]; then
  711. systemctl start bambuddy
  712. sleep 2
  713. if systemctl is-active --quiet bambuddy; then
  714. success "Bambuddy is running"
  715. else
  716. warn "Bambuddy may have failed to start. Check: sudo journalctl -u bambuddy -f"
  717. fi
  718. fi
  719. systemctl start spoolbuddy
  720. sleep 2
  721. if systemctl is-active --quiet spoolbuddy; then
  722. success "SpoolBuddy is running"
  723. else
  724. warn "SpoolBuddy may have failed to start. Check: sudo journalctl -u spoolbuddy -f"
  725. fi
  726. fi
  727. fi
  728. echo ""
  729. }
  730. main "$@"