install.sh 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134
  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. KIOSK_USER="" # auto-detected from $SUDO_USER
  50. KIOSK_URL="" # derived from $BAMBUDDY_URL/spoolbuddy?token=$API_KEY
  51. # ─────────────────────────────────────────────────────────────────────────────
  52. # Helpers
  53. # ─────────────────────────────────────────────────────────────────────────────
  54. info() { echo -e "${CYAN}[INFO]${NC} $1"; }
  55. success() { echo -e "${GREEN}[OK]${NC} $1"; }
  56. warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
  57. error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
  58. # Run a long-running command with a spinner + live progress output.
  59. # Usage: run_with_progress "description" command [args...]
  60. run_with_progress() {
  61. local desc="$1"
  62. shift
  63. local log_file
  64. log_file=$(mktemp /tmp/spoolbuddy-install.XXXXXX)
  65. local start_time=$SECONDS
  66. # Run command in background, capture stdout+stderr
  67. "$@" > "$log_file" 2>&1 &
  68. local pid=$!
  69. # Spinner frames (braille pattern)
  70. local -a spin=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
  71. local i=0
  72. while kill -0 "$pid" 2>/dev/null; do
  73. local elapsed=$(( SECONDS - start_time ))
  74. local time_str
  75. if (( elapsed >= 60 )); then
  76. time_str="$(( elapsed / 60 ))m$(printf '%02d' $(( elapsed % 60 )))s"
  77. else
  78. time_str="${elapsed}s"
  79. fi
  80. # Last chunk of output (handles \r progress lines and regular \n lines)
  81. local last_line=""
  82. 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
  83. printf "\r ${spin[$((i % 10))]} %-36s ${CYAN}%6s${NC} %s\033[K" "$desc" "$time_str" "$last_line"
  84. i=$(( i + 1 ))
  85. sleep 0.15
  86. done
  87. local exit_code=0
  88. wait "$pid" || exit_code=$?
  89. # Clear spinner line
  90. printf "\r\033[K"
  91. # Format elapsed time for summary
  92. local elapsed=$(( SECONDS - start_time ))
  93. local time_suffix=""
  94. if (( elapsed >= 60 )); then
  95. time_suffix=" ($(( elapsed / 60 ))m $(( elapsed % 60 ))s)"
  96. elif (( elapsed >= 5 )); then
  97. time_suffix=" (${elapsed}s)"
  98. fi
  99. if [[ $exit_code -eq 0 ]]; then
  100. success "${desc}${time_suffix}"
  101. rm -f "$log_file"
  102. else
  103. echo -e "${RED}[FAIL]${NC} ${desc}${time_suffix}"
  104. echo ""
  105. echo -e " ${YELLOW}Last 20 lines:${NC}"
  106. tail -20 "$log_file" 2>/dev/null | sed 's/^/ /'
  107. echo ""
  108. echo -e " Full log: ${CYAN}$log_file${NC}"
  109. exit 1
  110. fi
  111. }
  112. prompt() {
  113. local prompt_text="$1"
  114. local default_value="$2"
  115. local var_name="$3"
  116. if [[ "$NON_INTERACTIVE" == "true" ]]; then
  117. eval "$var_name=\"$default_value\""
  118. return
  119. fi
  120. if [[ -n "$default_value" ]]; then
  121. echo -en "${BOLD}$prompt_text${NC} [${CYAN}$default_value${NC}]: "
  122. else
  123. echo -en "${BOLD}$prompt_text${NC}: "
  124. fi
  125. read -r input
  126. if [[ -z "$input" ]]; then
  127. eval "$var_name=\"$default_value\""
  128. else
  129. eval "$var_name=\"$input\""
  130. fi
  131. }
  132. prompt_yes_no() {
  133. local prompt_text="$1"
  134. local default="$2"
  135. if [[ "$NON_INTERACTIVE" == "true" ]]; then
  136. [[ "$default" == "y" ]] && return 0 || return 1
  137. fi
  138. local yn_hint="[y/n]"
  139. [[ "$default" == "y" ]] && yn_hint="[Y/n]"
  140. [[ "$default" == "n" ]] && yn_hint="[y/N]"
  141. while true; do
  142. echo -en "${BOLD}$prompt_text${NC} $yn_hint: "
  143. read -r yn
  144. [[ -z "$yn" ]] && yn="$default"
  145. case "$yn" in
  146. [Yy]* ) return 0;;
  147. [Nn]* ) return 1;;
  148. * ) echo "Please answer yes or no.";;
  149. esac
  150. done
  151. }
  152. show_help() {
  153. echo "SpoolBuddy Installation Script for Raspberry Pi"
  154. echo ""
  155. echo "Usage: sudo $0 [OPTIONS]"
  156. echo ""
  157. echo "Options:"
  158. echo " --mode MODE \"spoolbuddy\" (companion only) or \"full\" (Bambuddy + SpoolBuddy)"
  159. echo " --bambuddy-url URL Bambuddy server URL (required for spoolbuddy mode)"
  160. echo " --api-key KEY Bambuddy API key (required for spoolbuddy mode)"
  161. echo " --path PATH Installation directory (default: /opt/spoolbuddy or /opt/bambuddy)"
  162. echo " --port PORT Bambuddy port (full mode only, default: 8000)"
  163. echo " --yes, -y Non-interactive mode, accept defaults"
  164. echo " --help, -h Show this help message"
  165. echo ""
  166. echo "Examples:"
  167. echo " Interactive:"
  168. echo " sudo ./install.sh"
  169. echo ""
  170. echo " SpoolBuddy companion (unattended):"
  171. echo " sudo ./install.sh --mode spoolbuddy --bambuddy-url http://192.168.1.100:8000 --api-key bb_xxx -y"
  172. echo ""
  173. echo " Full install (unattended):"
  174. echo " sudo ./install.sh --mode full --port 8000 -y"
  175. exit 0
  176. }
  177. # ─────────────────────────────────────────────────────────────────────────────
  178. # Pre-flight Checks
  179. # ─────────────────────────────────────────────────────────────────────────────
  180. check_root() {
  181. if [[ $EUID -ne 0 ]]; then
  182. error "This script must be run as root (use sudo)"
  183. fi
  184. }
  185. check_raspberry_pi() {
  186. if ! grep -q "Raspberry Pi\|BCM2" /proc/cpuinfo 2>/dev/null; then
  187. error "This script is designed for Raspberry Pi only"
  188. fi
  189. # Detect Pi model for hardware recommendations
  190. local model
  191. model=$(tr -d '\0' < /proc/device-tree/model 2>/dev/null) || model="Unknown"
  192. success "Detected: $model"
  193. }
  194. check_raspberry_pi_os() {
  195. if [[ ! -f /etc/os-release ]]; then
  196. error "Cannot detect operating system"
  197. fi
  198. . /etc/os-release
  199. if [[ "$ID" != "raspbian" && "$ID" != "debian" ]]; then
  200. warn "Expected Raspberry Pi OS (Debian-based), found: $ID"
  201. if ! prompt_yes_no "Continue anyway?" "n"; then
  202. exit 0
  203. fi
  204. fi
  205. success "OS: $PRETTY_NAME"
  206. }
  207. detect_python() {
  208. local cmd=""
  209. if command -v python3 &>/dev/null; then
  210. cmd="python3"
  211. elif command -v python &>/dev/null; then
  212. local ver
  213. ver=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1)
  214. if [[ "$ver" -ge 3 ]]; then
  215. cmd="python"
  216. fi
  217. fi
  218. if [[ -z "$cmd" ]]; then
  219. return 1
  220. fi
  221. local version
  222. version=$($cmd -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
  223. local major minor
  224. major=$(echo "$version" | cut -d'.' -f1)
  225. minor=$(echo "$version" | cut -d'.' -f2)
  226. if [[ "$major" -lt 3 ]] || { [[ "$major" -eq 3 ]] && [[ "$minor" -lt 10 ]]; }; then
  227. warn "Python $version found, but 3.10+ is required"
  228. return 1
  229. fi
  230. PYTHON_CMD="$cmd"
  231. success "Found Python $version"
  232. return 0
  233. }
  234. # ─────────────────────────────────────────────────────────────────────────────
  235. # Raspberry Pi Hardware Configuration
  236. # ─────────────────────────────────────────────────────────────────────────────
  237. enable_spi() {
  238. if raspi-config nonint get_spi 2>/dev/null | grep -q "1"; then
  239. info "Enabling SPI..."
  240. raspi-config nonint do_spi 0
  241. REBOOT_NEEDED="true"
  242. success "SPI enabled"
  243. else
  244. success "SPI already enabled"
  245. fi
  246. }
  247. enable_i2c() {
  248. if raspi-config nonint get_i2c 2>/dev/null | grep -q "1"; then
  249. info "Enabling I2C..."
  250. raspi-config nonint do_i2c 0
  251. REBOOT_NEEDED="true"
  252. success "I2C enabled"
  253. else
  254. success "I2C already enabled"
  255. fi
  256. }
  257. configure_boot_config() {
  258. # Find the boot config file (Bookworm+ uses /boot/firmware/config.txt)
  259. local boot_config="/boot/firmware/config.txt"
  260. if [[ ! -f "$boot_config" ]]; then
  261. boot_config="/boot/config.txt"
  262. fi
  263. if [[ ! -f "$boot_config" ]]; then
  264. warn "Boot config not found at /boot/firmware/config.txt or /boot/config.txt"
  265. warn "You may need to manually add: dtparam=i2c_vc=on and dtoverlay=spi0-0cs"
  266. return
  267. fi
  268. info "Configuring $boot_config..."
  269. # Enable I2C bus 0 (GPIO0/GPIO1) for NAU7802 scale
  270. if ! grep -q "^dtparam=i2c_vc=on" "$boot_config"; then
  271. echo "" >> "$boot_config"
  272. echo "# SpoolBuddy: I2C bus 0 for NAU7802 scale (GPIO0/GPIO1)" >> "$boot_config"
  273. echo "dtparam=i2c_vc=on" >> "$boot_config"
  274. REBOOT_NEEDED="true"
  275. success "Added dtparam=i2c_vc=on"
  276. else
  277. success "dtparam=i2c_vc=on already set"
  278. fi
  279. # Disable SPI auto chip-select (manual CS on GPIO23 for PN5180)
  280. if ! grep -q "^dtoverlay=spi0-0cs" "$boot_config"; then
  281. echo "" >> "$boot_config"
  282. echo "# SpoolBuddy: Disable SPI auto CS (manual CS on GPIO23 for PN5180)" >> "$boot_config"
  283. echo "dtoverlay=spi0-0cs" >> "$boot_config"
  284. REBOOT_NEEDED="true"
  285. success "Added dtoverlay=spi0-0cs"
  286. else
  287. success "dtoverlay=spi0-0cs already set"
  288. fi
  289. }
  290. # ─────────────────────────────────────────────────────────────────────────────
  291. # Package Installation
  292. # ─────────────────────────────────────────────────────────────────────────────
  293. install_system_packages() {
  294. run_with_progress "Updating package lists" apt-get update
  295. run_with_progress "Installing system packages" apt-get install -y $SYSTEM_PACKAGES
  296. }
  297. # ─────────────────────────────────────────────────────────────────────────────
  298. # SpoolBuddy Installation
  299. # ─────────────────────────────────────────────────────────────────────────────
  300. create_spoolbuddy_user() {
  301. if id "$SPOOLBUDDY_SERVICE_USER" &>/dev/null; then
  302. info "User '$SPOOLBUDDY_SERVICE_USER' already exists"
  303. else
  304. info "Creating service user '$SPOOLBUDDY_SERVICE_USER'..."
  305. useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$SPOOLBUDDY_SERVICE_USER"
  306. success "Service user created"
  307. fi
  308. # Add to hardware access groups (gpio, spi, i2c, video for backlight)
  309. for group in gpio spi i2c video; do
  310. if getent group "$group" &>/dev/null; then
  311. usermod -aG "$group" "$SPOOLBUDDY_SERVICE_USER" 2>/dev/null || true
  312. fi
  313. done
  314. success "User added to gpio, spi, i2c, video groups"
  315. }
  316. download_spoolbuddy() {
  317. if [[ -d "$INSTALL_PATH/.git" ]]; then
  318. info "Existing installation found, updating..."
  319. git config --global --add safe.directory "$INSTALL_PATH" 2>/dev/null || true
  320. cd "$INSTALL_PATH"
  321. run_with_progress "Fetching updates" git fetch origin
  322. git reset --hard origin/main > /dev/null 2>&1
  323. cd "$INSTALL_PATH"
  324. git checkout 0.2.2b1
  325. else
  326. mkdir -p "$INSTALL_PATH"
  327. run_with_progress "Cloning repository" git clone "$GITHUB_REPO" "$INSTALL_PATH"
  328. cd "$INSTALL_PATH"
  329. git checkout 0.2.2b1
  330. fi
  331. chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$INSTALL_PATH"
  332. }
  333. setup_spoolbuddy_venv() {
  334. cd "$INSTALL_PATH/spoolbuddy"
  335. run_with_progress "Creating SpoolBuddy venv" $PYTHON_CMD -m venv --system-site-packages venv
  336. run_with_progress "Upgrading pip" "$INSTALL_PATH/spoolbuddy/venv/bin/pip" install --upgrade pip
  337. run_with_progress "Installing SpoolBuddy packages" "$INSTALL_PATH/spoolbuddy/venv/bin/pip" install $SPOOLBUDDY_PIP_PACKAGES
  338. chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$INSTALL_PATH/spoolbuddy/venv"
  339. }
  340. create_spoolbuddy_env() {
  341. info "Creating SpoolBuddy configuration..."
  342. local env_file="$INSTALL_PATH/spoolbuddy/.env"
  343. cat > "$env_file" << EOF
  344. # SpoolBuddy Configuration
  345. # Generated by install.sh on $(date)
  346. # Bambuddy backend URL
  347. SPOOLBUDDY_BACKEND_URL=$BAMBUDDY_URL
  348. # API key (create one in Bambuddy Settings -> API Keys)
  349. SPOOLBUDDY_API_KEY=$API_KEY
  350. EOF
  351. chown "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$env_file"
  352. chmod 600 "$env_file"
  353. success "Configuration saved to $env_file"
  354. }
  355. create_spoolbuddy_service() {
  356. info "Creating SpoolBuddy systemd service..."
  357. local after_line="After=network-online.target"
  358. if [[ "$INSTALL_MODE" == "full" ]]; then
  359. after_line="After=network-online.target bambuddy.service"
  360. fi
  361. cat > /etc/systemd/system/spoolbuddy.service << EOF
  362. [Unit]
  363. Description=SpoolBuddy - NFC Spool Management Daemon
  364. Documentation=https://github.com/maziggy/bambuddy
  365. $after_line
  366. Wants=network-online.target
  367. [Service]
  368. Type=simple
  369. User=$SPOOLBUDDY_SERVICE_USER
  370. WorkingDirectory=$INSTALL_PATH/spoolbuddy
  371. EnvironmentFile=$INSTALL_PATH/spoolbuddy/.env
  372. ExecStart=$INSTALL_PATH/spoolbuddy/venv/bin/python -m daemon.main
  373. Restart=always
  374. RestartSec=5
  375. StandardOutput=journal
  376. StandardError=journal
  377. [Install]
  378. WantedBy=multi-user.target
  379. EOF
  380. systemctl daemon-reload
  381. systemctl enable spoolbuddy.service
  382. success "SpoolBuddy service created and enabled"
  383. }
  384. # ─────────────────────────────────────────────────────────────────────────────
  385. # Bambuddy Installation (full mode only)
  386. # ─────────────────────────────────────────────────────────────────────────────
  387. create_bambuddy_user() {
  388. if id "$BAMBUDDY_SERVICE_USER" &>/dev/null; then
  389. info "User '$BAMBUDDY_SERVICE_USER' already exists"
  390. return
  391. fi
  392. info "Creating service user '$BAMBUDDY_SERVICE_USER'..."
  393. useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$BAMBUDDY_SERVICE_USER"
  394. success "Service user created"
  395. }
  396. setup_bambuddy_venv() {
  397. cd "$INSTALL_PATH"
  398. run_with_progress "Creating Bambuddy venv" $PYTHON_CMD -m venv venv
  399. run_with_progress "Upgrading pip" "$INSTALL_PATH/venv/bin/pip" install --upgrade pip
  400. run_with_progress "Installing Bambuddy dependencies" "$INSTALL_PATH/venv/bin/pip" install -r requirements.txt
  401. chown -R "$BAMBUDDY_SERVICE_USER:$BAMBUDDY_SERVICE_USER" "$INSTALL_PATH/venv"
  402. }
  403. install_nodejs() {
  404. if command -v node &>/dev/null; then
  405. local version
  406. version=$(node --version 2>/dev/null | sed 's/^v//')
  407. local major
  408. major=$(echo "$version" | cut -d'.' -f1)
  409. if [[ "$major" -ge 20 ]]; then
  410. success "Found Node.js v$version"
  411. return
  412. fi
  413. fi
  414. apt-get remove -y nodejs npm > /dev/null 2>&1 || true
  415. run_with_progress "Setting up Node.js repository" bash -c "curl -fsSL https://deb.nodesource.com/setup_22.x | bash -"
  416. run_with_progress "Installing Node.js" apt-get install -y nodejs
  417. hash -r 2>/dev/null || true
  418. success "Node.js installed: $(node --version)"
  419. }
  420. build_frontend() {
  421. cd "$INSTALL_PATH/frontend"
  422. run_with_progress "Installing frontend dependencies" npm ci
  423. run_with_progress "Building frontend" npm run build
  424. }
  425. create_bambuddy_env() {
  426. info "Creating Bambuddy configuration..."
  427. local env_file="$INSTALL_PATH/.env"
  428. cat > "$env_file" << EOF
  429. # Bambuddy Configuration
  430. # Generated by install.sh on $(date)
  431. DEBUG=false
  432. LOG_LEVEL=INFO
  433. LOG_TO_FILE=true
  434. EOF
  435. chown "$BAMBUDDY_SERVICE_USER:$BAMBUDDY_SERVICE_USER" "$env_file"
  436. chmod 600 "$env_file"
  437. success "Configuration saved to $env_file"
  438. }
  439. create_bambuddy_directories() {
  440. mkdir -p "$INSTALL_PATH/data" "$INSTALL_PATH/logs"
  441. chown -R "$BAMBUDDY_SERVICE_USER:$BAMBUDDY_SERVICE_USER" "$INSTALL_PATH/data" "$INSTALL_PATH/logs"
  442. success "Data directories created"
  443. }
  444. create_bambuddy_service() {
  445. info "Creating Bambuddy systemd service..."
  446. cat > /etc/systemd/system/bambuddy.service << EOF
  447. [Unit]
  448. Description=Bambuddy - Bambu Lab Print Management
  449. Documentation=https://github.com/maziggy/bambuddy
  450. After=network.target
  451. [Service]
  452. Type=simple
  453. User=$BAMBUDDY_SERVICE_USER
  454. Group=$BAMBUDDY_SERVICE_USER
  455. WorkingDirectory=$INSTALL_PATH
  456. EnvironmentFile=$INSTALL_PATH/.env
  457. Environment="DATA_DIR=$INSTALL_PATH/data"
  458. Environment="LOG_DIR=$INSTALL_PATH/logs"
  459. ExecStart=$INSTALL_PATH/venv/bin/uvicorn backend.app.main:app --host 0.0.0.0 --port $BAMBUDDY_PORT
  460. Restart=on-failure
  461. RestartSec=5
  462. StandardOutput=journal
  463. StandardError=journal
  464. NoNewPrivileges=true
  465. PrivateTmp=true
  466. ProtectSystem=strict
  467. ProtectHome=true
  468. ReadWritePaths=$INSTALL_PATH/data $INSTALL_PATH/logs $INSTALL_PATH
  469. [Install]
  470. WantedBy=multi-user.target
  471. EOF
  472. systemctl daemon-reload
  473. systemctl enable bambuddy.service
  474. success "Bambuddy service created and enabled"
  475. }
  476. # ─────────────────────────────────────────────────────────────────────────────
  477. # System Strip-Down (dedicated appliance — remove unnecessary services/packages)
  478. # ─────────────────────────────────────────────────────────────────────────────
  479. strip_services() {
  480. info "Disabling unnecessary services..."
  481. local services=(
  482. bluetooth.service
  483. lightdm.service
  484. cloud-init-local.service
  485. cloud-init.service
  486. cloud-init-network.service
  487. cloud-config.service
  488. cloud-final.service
  489. cloud-init-hotplugd.socket
  490. avahi-daemon.service
  491. avahi-daemon.socket
  492. ModemManager.service
  493. udisks2.service
  494. apparmor.service
  495. man-db.timer
  496. e2scrub_all.timer
  497. e2scrub_reap.service
  498. )
  499. local disabled=0
  500. for svc in "${services[@]}"; do
  501. if systemctl is-enabled "$svc" &>/dev/null; then
  502. systemctl disable "$svc" 2>/dev/null || true
  503. (( ++disabled ))
  504. fi
  505. done
  506. if (( disabled > 0 )); then
  507. success "Disabled $disabled unnecessary services"
  508. else
  509. success "No unnecessary services to disable"
  510. fi
  511. }
  512. strip_packages() {
  513. info "Removing unnecessary packages..."
  514. local packages=(
  515. mkvtoolnix
  516. firmware-atheros
  517. firmware-mediatek
  518. cloud-init
  519. rpi-cloud-init-mods
  520. rpi-connect-lite
  521. avahi-daemon
  522. modemmanager
  523. udisks2
  524. )
  525. local to_remove=()
  526. for pkg in "${packages[@]}"; do
  527. if dpkg -l "$pkg" &>/dev/null 2>&1; then
  528. to_remove+=("$pkg")
  529. fi
  530. done
  531. if (( ${#to_remove[@]} > 0 )); then
  532. run_with_progress "Removing ${#to_remove[@]} packages" apt-get remove --purge -y "${to_remove[@]}"
  533. run_with_progress "Cleaning up dependencies" apt-get autoremove --purge -y
  534. else
  535. success "No unnecessary packages to remove"
  536. fi
  537. }
  538. # ─────────────────────────────────────────────────────────────────────────────
  539. # Kiosk Setup (labwc + Chromium + Plymouth splash)
  540. # ─────────────────────────────────────────────────────────────────────────────
  541. setup_kiosk() {
  542. info "Setting up touchscreen kiosk..."
  543. # Detect kiosk user (the human user who ran sudo)
  544. KIOSK_USER="${SUDO_USER:-$(logname 2>/dev/null || echo pi)}"
  545. KIOSK_URL="${BAMBUDDY_URL}/spoolbuddy?token=${API_KEY}"
  546. local KIOSK_HOME
  547. KIOSK_HOME=$(eval echo "~$KIOSK_USER")
  548. info "Kiosk user: $KIOSK_USER (home: $KIOSK_HOME)"
  549. info "Kiosk URL: $KIOSK_URL"
  550. # ── Install kiosk packages ────────────────────────────────────────────
  551. run_with_progress "Installing kiosk packages" apt-get install -y labwc chromium plymouth wlr-randr
  552. # ── config.txt tweaks ─────────────────────────────────────────────────
  553. local boot_config="/boot/firmware/config.txt"
  554. if [[ ! -f "$boot_config" ]]; then
  555. boot_config="/boot/config.txt"
  556. fi
  557. if [[ -f "$boot_config" ]]; then
  558. info "Configuring $boot_config for kiosk..."
  559. # Disable audio (change existing on→off)
  560. sed -i 's/^dtparam=audio=on/dtparam=audio=off/' "$boot_config"
  561. # Disable camera auto-detect (change existing 1→0)
  562. sed -i 's/^camera_auto_detect=1/camera_auto_detect=0/' "$boot_config"
  563. # Append if missing: gpu_mem=32
  564. if ! grep -q "^gpu_mem=" "$boot_config"; then
  565. echo "" >> "$boot_config"
  566. echo "# Kiosk: Minimal GPU firmware memory (KMS uses CMA from system RAM)" >> "$boot_config"
  567. echo "gpu_mem=32" >> "$boot_config"
  568. fi
  569. # Append if missing: dtoverlay=disable-bt
  570. if ! grep -q "^dtoverlay=disable-bt" "$boot_config"; then
  571. echo "" >> "$boot_config"
  572. echo "# Kiosk: Disable Bluetooth hardware" >> "$boot_config"
  573. echo "dtoverlay=disable-bt" >> "$boot_config"
  574. fi
  575. # Append if missing: disable_splash=1
  576. if ! grep -q "^disable_splash=" "$boot_config"; then
  577. echo "" >> "$boot_config"
  578. echo "# Kiosk: Disable Raspberry Pi firmware splash, use custom splash.png" >> "$boot_config"
  579. echo "disable_splash=1" >> "$boot_config"
  580. fi
  581. success "Boot config updated"
  582. fi
  583. # ── cmdline.txt tweaks ────────────────────────────────────────────────
  584. local cmdline="/boot/firmware/cmdline.txt"
  585. if [[ ! -f "$cmdline" ]]; then
  586. cmdline="/boot/cmdline.txt"
  587. fi
  588. if [[ -f "$cmdline" ]]; then
  589. info "Configuring $cmdline for kiosk..."
  590. # Remove serial console (Plymouth needs tty-only console)
  591. sed -i 's/console=serial0,[0-9]* //' "$cmdline"
  592. # Add splash quiet loglevel=3 logo.nologo if missing
  593. grep -q "splash" "$cmdline" || sed -i 's/$/ splash quiet loglevel=3 logo.nologo/' "$cmdline"
  594. # Add video mode if missing
  595. grep -q "video=HDMI-A-1" "$cmdline" || sed -i 's/$/ video=HDMI-A-1:1024x600@60/' "$cmdline"
  596. success "Kernel cmdline updated"
  597. fi
  598. # ── Plymouth splash theme ─────────────────────────────────────────────
  599. info "Installing Plymouth boot splash..."
  600. local theme_dir="/usr/share/plymouth/themes/spoolbuddy"
  601. mkdir -p "$theme_dir"
  602. # Copy bundled splash image from the install directory
  603. local script_dir
  604. script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  605. if [[ -f "$script_dir/splash.png" ]]; then
  606. cp "$script_dir/splash.png" "$theme_dir/splash.png"
  607. elif [[ -f "$INSTALL_PATH/spoolbuddy/install/splash.png" ]]; then
  608. cp "$INSTALL_PATH/spoolbuddy/install/splash.png" "$theme_dir/splash.png"
  609. else
  610. warn "splash.png not found — Plymouth splash will not display an image"
  611. fi
  612. # Write .plymouth theme file
  613. cat > "$theme_dir/spoolbuddy.plymouth" << 'EOF'
  614. [Plymouth Theme]
  615. Name=SpoolBuddy
  616. Description=SpoolBuddy boot splash
  617. ModuleName=script
  618. [script]
  619. ImageDir=/usr/share/plymouth/themes/spoolbuddy
  620. ScriptFile=/usr/share/plymouth/themes/spoolbuddy/spoolbuddy.script
  621. EOF
  622. # Write .script theme file
  623. cat > "$theme_dir/spoolbuddy.script" << 'EOF'
  624. wallpaper_image = Image("splash.png");
  625. screen_width = Window.GetWidth();
  626. screen_height = Window.GetHeight();
  627. resized_wallpaper_image = wallpaper_image.Scale(screen_width, screen_height);
  628. wallpaper_sprite = Sprite(resized_wallpaper_image);
  629. wallpaper_sprite.SetZ(-100);
  630. EOF
  631. plymouth-set-default-theme spoolbuddy
  632. run_with_progress "Updating initramfs" update-initramfs -u
  633. success "Plymouth splash installed"
  634. # ── Auto-login on tty1 ────────────────────────────────────────────────
  635. info "Configuring auto-login for $KIOSK_USER..."
  636. mkdir -p /etc/systemd/system/getty@tty1.service.d
  637. cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << EOF
  638. [Service]
  639. ExecStart=
  640. ExecStart=-/sbin/agetty --autologin $KIOSK_USER --noclear %I \$TERM
  641. EOF
  642. success "Auto-login configured"
  643. # ── labwc rc.xml (no decorations, no keybinds) ────────────────────────
  644. info "Configuring labwc window manager..."
  645. local labwc_dir="$KIOSK_HOME/.config/labwc"
  646. mkdir -p "$labwc_dir"
  647. cat > "$labwc_dir/rc.xml" << 'EOF'
  648. <?xml version="1.0"?>
  649. <labwc_config>
  650. <theme>
  651. <name></name>
  652. <cornerRadius>0</cornerRadius>
  653. </theme>
  654. <!-- Disable all keybindings - kiosk lockdown -->
  655. <keyboard>
  656. </keyboard>
  657. <!-- Disable right-click menu -->
  658. <mouse>
  659. <default />
  660. </mouse>
  661. <!-- Remove window decorations, maximize Chromium, prevent unfullscreen -->
  662. <windowRules>
  663. <windowRule identifier="*">
  664. <serverDecoration>no</serverDecoration>
  665. </windowRule>
  666. <windowRule identifier="chromium">
  667. <skipTaskbar>yes</skipTaskbar>
  668. <fixedPosition>yes</fixedPosition>
  669. </windowRule>
  670. </windowRules>
  671. </labwc_config>
  672. EOF
  673. # ── labwc autostart ───────────────────────────────────────────────────
  674. cat > "$labwc_dir/autostart" << EOF
  675. # Force 1024x600 (panel doesn't advertise this natively)
  676. wlr-randr --output HDMI-A-1 --custom-mode 1024x600@60 &
  677. # Launch Chromium in kiosk mode (virtual keyboard is embedded in the web app)
  678. chromium --kiosk --no-first-run --disable-infobars \\
  679. --disable-session-crashed-bubble --disable-features=TranslateUI \\
  680. --noerrdialogs --disable-component-update \\
  681. --overscroll-history-navigation=0 \\
  682. --ozone-platform=wayland \\
  683. $KIOSK_URL &
  684. EOF
  685. chown -R "$KIOSK_USER:$KIOSK_USER" "$labwc_dir"
  686. # ── .bash_profile (source .bashrc, exec labwc on tty1) ────────────────
  687. cat > "$KIOSK_HOME/.bash_profile" << 'EOF'
  688. # Source .bashrc if it exists
  689. if [ -f ~/.bashrc ]; then
  690. . ~/.bashrc
  691. fi
  692. # Auto-start kiosk on tty1
  693. if [ "$(tty)" = "/dev/tty1" ]; then
  694. exec labwc
  695. fi
  696. EOF
  697. chown "$KIOSK_USER:$KIOSK_USER" "$KIOSK_HOME/.bash_profile"
  698. REBOOT_NEEDED="true"
  699. success "Kiosk setup complete"
  700. }
  701. # ─────────────────────────────────────────────────────────────────────────────
  702. # User Prompts
  703. # ─────────────────────────────────────────────────────────────────────────────
  704. parse_args() {
  705. while [[ $# -gt 0 ]]; do
  706. case "$1" in
  707. --mode)
  708. INSTALL_MODE="$2"
  709. shift 2
  710. ;;
  711. --bambuddy-url)
  712. BAMBUDDY_URL="$2"
  713. shift 2
  714. ;;
  715. --api-key)
  716. API_KEY="$2"
  717. shift 2
  718. ;;
  719. --path)
  720. INSTALL_PATH="$2"
  721. shift 2
  722. ;;
  723. --port)
  724. BAMBUDDY_PORT="$2"
  725. shift 2
  726. ;;
  727. --yes|-y)
  728. NON_INTERACTIVE="true"
  729. shift
  730. ;;
  731. --help|-h)
  732. show_help
  733. ;;
  734. *)
  735. error "Unknown option: $1 (use --help for usage)"
  736. ;;
  737. esac
  738. done
  739. }
  740. ask_install_mode() {
  741. if [[ -n "$INSTALL_MODE" ]]; then
  742. return
  743. fi
  744. echo ""
  745. echo -e "${BOLD}How would you like to set up SpoolBuddy?${NC}"
  746. echo ""
  747. echo -e " ${CYAN}1)${NC} SpoolBuddy only"
  748. echo " NFC reader + scale on this RPi, Bambuddy runs on another device"
  749. echo ""
  750. echo -e " ${CYAN}2)${NC} SpoolBuddy + Bambuddy"
  751. echo " Both running natively on this Raspberry Pi"
  752. echo ""
  753. while true; do
  754. echo -en "${BOLD}Choose${NC} [${CYAN}1${NC}/${CYAN}2${NC}]: "
  755. read -r choice
  756. case "$choice" in
  757. 1) INSTALL_MODE="spoolbuddy"; return;;
  758. 2) INSTALL_MODE="full"; return;;
  759. *) echo "Please enter 1 or 2.";;
  760. esac
  761. done
  762. }
  763. gather_config() {
  764. echo ""
  765. echo -e "${BOLD}Configuration${NC}"
  766. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  767. echo ""
  768. # Set default install path based on mode
  769. if [[ -z "$INSTALL_PATH" ]]; then
  770. if [[ "$INSTALL_MODE" == "full" ]]; then
  771. INSTALL_PATH="/opt/bambuddy"
  772. else
  773. INSTALL_PATH="/opt/bambuddy"
  774. fi
  775. fi
  776. prompt "Installation directory" "$INSTALL_PATH" INSTALL_PATH
  777. if [[ "$INSTALL_MODE" == "spoolbuddy" ]]; then
  778. # Need remote Bambuddy URL and API key
  779. echo ""
  780. info "SpoolBuddy needs to connect to your Bambuddy server."
  781. info "You can find/create an API key in Bambuddy under Settings -> API Keys."
  782. echo ""
  783. while [[ -z "$BAMBUDDY_URL" ]]; do
  784. prompt "Bambuddy server URL (e.g. http://192.168.1.100:8000)" "" BAMBUDDY_URL
  785. if [[ -z "$BAMBUDDY_URL" ]]; then
  786. warn "Bambuddy URL is required"
  787. fi
  788. done
  789. while [[ -z "$API_KEY" ]]; do
  790. prompt "Bambuddy API key" "" API_KEY
  791. if [[ -z "$API_KEY" ]]; then
  792. warn "API key is required"
  793. fi
  794. done
  795. else
  796. # Full mode — Bambuddy runs locally
  797. prompt "Bambuddy port" "$BAMBUDDY_PORT" BAMBUDDY_PORT
  798. BAMBUDDY_URL="http://localhost:$BAMBUDDY_PORT"
  799. echo ""
  800. info "After installation, create an API key in Bambuddy (Settings -> API Keys)"
  801. info "and update it in: $INSTALL_PATH/spoolbuddy/.env"
  802. API_KEY="CHANGE_ME_AFTER_SETUP"
  803. fi
  804. # Summary
  805. echo ""
  806. echo -e "${BOLD}Installation Summary${NC}"
  807. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  808. echo -e " Mode: ${GREEN}$([ "$INSTALL_MODE" == "full" ] && echo "Bambuddy + SpoolBuddy" || echo "SpoolBuddy only")${NC}"
  809. echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}"
  810. if [[ "$INSTALL_MODE" == "full" ]]; then
  811. echo -e " Bambuddy port: ${GREEN}$BAMBUDDY_PORT${NC}"
  812. echo -e " Bambuddy URL: ${GREEN}$BAMBUDDY_URL${NC}"
  813. else
  814. echo -e " Bambuddy URL: ${GREEN}$BAMBUDDY_URL${NC}"
  815. fi
  816. echo ""
  817. if ! prompt_yes_no "Proceed with installation?" "y"; then
  818. echo "Installation cancelled."
  819. exit 0
  820. fi
  821. }
  822. # ─────────────────────────────────────────────────────────────────────────────
  823. # Main
  824. # ─────────────────────────────────────────────────────────────────────────────
  825. main() {
  826. parse_args "$@"
  827. echo ""
  828. echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
  829. echo -e "${CYAN}║ ║${NC}"
  830. echo -e "${CYAN}║ ____ _ ____ _ _ ║${NC}"
  831. echo -e "${CYAN}║ / ___| _ __ ___ ___ | | __ ) _ _ __| | __| |_ _ ║${NC}"
  832. echo -e "${CYAN}║ \\___ \\| '_ \\ / _ \\ / _ \\| | _ \\| | | |/ _\` |/ _\` | | | |║${NC}"
  833. echo -e "${CYAN}║ ___) | |_) | (_) | (_) | | |_) | |_| | (_| | (_| | |_| |║${NC}"
  834. echo -e "${CYAN}║ |____/| .__/ \\___/ \\___/|_|____/ \\__,_|\\__,_|\\__,_|\\__, |║${NC}"
  835. echo -e "${CYAN}║ |_| |___/ ║${NC}"
  836. echo -e "${CYAN}║ ║${NC}"
  837. echo -e "${CYAN}║ NFC Spool Management for Bambuddy ║${NC}"
  838. echo -e "${CYAN}║ ║${NC}"
  839. echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
  840. echo ""
  841. # Check if running via pipe without -y
  842. if [[ ! -t 0 ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then
  843. error "Interactive mode requires a terminal. Use -y for unattended install, or download and run directly."
  844. fi
  845. # Pre-flight checks
  846. check_root
  847. check_raspberry_pi
  848. check_raspberry_pi_os
  849. if ! detect_python; then
  850. info "Python 3.10+ not found, will install..."
  851. fi
  852. # Gather user preferences
  853. ask_install_mode
  854. gather_config
  855. # Validate mode
  856. if [[ "$INSTALL_MODE" != "spoolbuddy" && "$INSTALL_MODE" != "full" ]]; then
  857. error "Invalid mode: $INSTALL_MODE (must be 'spoolbuddy' or 'full')"
  858. fi
  859. echo ""
  860. echo -e "${BOLD}Starting Installation${NC}"
  861. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  862. echo ""
  863. # ── Step 1: Raspberry Pi hardware config ──────────────────────────────
  864. info "Configuring Raspberry Pi hardware..."
  865. enable_spi
  866. enable_i2c
  867. configure_boot_config
  868. echo ""
  869. # ── Step 2: System packages ───────────────────────────────────────────
  870. install_system_packages
  871. detect_python || error "Failed to install Python 3.10+"
  872. echo ""
  873. # ── Step 2b: Strip unnecessary services & packages ────────────────────
  874. strip_services
  875. strip_packages
  876. echo ""
  877. # ── Step 3: Download source code ──────────────────────────────────────
  878. create_spoolbuddy_user
  879. download_spoolbuddy
  880. echo ""
  881. # ── Step 3b: Kiosk setup (labwc + Chromium + squeekboard + Plymouth) ──
  882. setup_kiosk
  883. echo ""
  884. # ── Step 4: SpoolBuddy setup ──────────────────────────────────────────
  885. info "Setting up SpoolBuddy..."
  886. setup_spoolbuddy_venv
  887. create_spoolbuddy_env
  888. create_spoolbuddy_service
  889. echo ""
  890. # ── Step 5: Bambuddy setup (full mode only) ───────────────────────────
  891. if [[ "$INSTALL_MODE" == "full" ]]; then
  892. info "Setting up Bambuddy..."
  893. create_bambuddy_user
  894. setup_bambuddy_venv
  895. install_nodejs
  896. build_frontend
  897. create_bambuddy_directories
  898. create_bambuddy_env
  899. create_bambuddy_service
  900. echo ""
  901. fi
  902. # ── Done ──────────────────────────────────────────────────────────────
  903. echo ""
  904. echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}"
  905. echo -e "${GREEN}║ ║${NC}"
  906. echo -e "${GREEN}║ Installation Complete! ║${NC}"
  907. echo -e "${GREEN}║ ║${NC}"
  908. echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}"
  909. echo ""
  910. local ip_addr
  911. ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}') || ip_addr="<your-ip>"
  912. if [[ "$INSTALL_MODE" == "full" ]]; then
  913. echo -e " ${BOLD}Bambuddy:${NC} ${CYAN}http://$ip_addr:$BAMBUDDY_PORT${NC}"
  914. else
  915. echo -e " ${BOLD}SpoolBuddy:${NC} Connecting to ${CYAN}$BAMBUDDY_URL${NC}"
  916. fi
  917. echo -e " ${BOLD}Kiosk URL:${NC} ${CYAN}$KIOSK_URL${NC}"
  918. echo -e " ${BOLD}Kiosk user:${NC} ${CYAN}$KIOSK_USER${NC}"
  919. echo ""
  920. if [[ "$INSTALL_MODE" == "full" ]]; then
  921. echo -e " ${BOLD}Next steps:${NC}"
  922. echo -e " 1. Reboot (required for kiosk, Plymouth splash, and hardware changes)"
  923. echo -e " 2. The touchscreen kiosk will start automatically after reboot"
  924. echo -e " 3. On another device, open ${CYAN}http://$ip_addr:$BAMBUDDY_PORT${NC}"
  925. echo -e " 4. Go to Settings -> API Keys and create an API key"
  926. echo -e " 5. Update the API key in: ${CYAN}$INSTALL_PATH/spoolbuddy/.env${NC}"
  927. echo -e " 6. Restart SpoolBuddy: ${CYAN}sudo systemctl restart spoolbuddy${NC}"
  928. fi
  929. echo ""
  930. echo -e " ${BOLD}Manage services:${NC}"
  931. echo -e " SpoolBuddy status: ${CYAN}sudo systemctl status spoolbuddy${NC}"
  932. echo -e " SpoolBuddy logs: ${CYAN}sudo journalctl -u spoolbuddy -f${NC}"
  933. if [[ "$INSTALL_MODE" == "full" ]]; then
  934. echo -e " Bambuddy status: ${CYAN}sudo systemctl status bambuddy${NC}"
  935. echo -e " Bambuddy logs: ${CYAN}sudo journalctl -u bambuddy -f${NC}"
  936. fi
  937. echo ""
  938. echo -e " ${BOLD}Configuration:${NC} ${CYAN}$INSTALL_PATH/spoolbuddy/.env${NC}"
  939. echo -e " ${BOLD}Hardware wiring:${NC} ${CYAN}$INSTALL_PATH/spoolbuddy/README.md${NC}"
  940. echo -e " ${BOLD}Diagnostics:${NC} ${CYAN}sudo $INSTALL_PATH/spoolbuddy/venv/bin/python $INSTALL_PATH/spoolbuddy/pn5180_diag.py${NC}"
  941. echo ""
  942. echo -e " ${YELLOW}A reboot is required to apply all changes (kiosk, Plymouth splash, hardware).${NC}"
  943. echo ""
  944. if prompt_yes_no "Reboot now?" "y"; then
  945. reboot
  946. else
  947. echo -e " Run ${CYAN}sudo reboot${NC} when ready."
  948. fi
  949. echo ""
  950. }
  951. main "$@"