setup-pi.sh 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. #!/bin/bash
  2. #
  3. # Dune Weaver Raspberry Pi Setup Script
  4. #
  5. # ONE-COMMAND INSTALL (recommended):
  6. # curl -fsSL https://raw.githubusercontent.com/tuanchris/dune-weaver/main/setup-pi.sh | bash
  7. #
  8. # OR from existing clone:
  9. # git clone https://github.com/tuanchris/dune-weaver --single-branch
  10. # cd dune-weaver
  11. # bash setup-pi.sh
  12. #
  13. # Options:
  14. # --no-wifi-fix Skip WiFi stability fix
  15. # --no-hotspot Skip autohotspot setup
  16. # --help Show help
  17. #
  18. set -e
  19. # Colors for output
  20. RED='\033[0;31m'
  21. GREEN='\033[0;32m'
  22. YELLOW='\033[1;33m'
  23. BLUE='\033[0;34m'
  24. NC='\033[0m' # No Color
  25. # Default options
  26. FIX_WIFI=true # Applied by default for stability
  27. SETUP_HOTSPOT=true # Autohotspot for first-time WiFi setup
  28. REAL_USER="${SUDO_USER:-$USER}"
  29. REAL_HOME="$(eval echo ~"$REAL_USER")"
  30. # Fallback: if tilde didn't expand, read from /etc/passwd
  31. if [[ "$REAL_HOME" == "~$REAL_USER" ]]; then
  32. REAL_HOME="$(grep "^$REAL_USER:" /etc/passwd | cut -d: -f6)"
  33. fi
  34. INSTALL_DIR="$REAL_HOME/dune-weaver"
  35. REPO_URL="https://github.com/tuanchris/dune-weaver"
  36. # Parse arguments
  37. while [[ $# -gt 0 ]]; do
  38. case $1 in
  39. --no-wifi-fix)
  40. FIX_WIFI=false
  41. shift
  42. ;;
  43. --no-hotspot)
  44. SETUP_HOTSPOT=false
  45. shift
  46. ;;
  47. --help|-h)
  48. echo "Dune Weaver Raspberry Pi Setup Script"
  49. echo ""
  50. echo "One-command install:"
  51. echo " curl -fsSL https://raw.githubusercontent.com/tuanchris/dune-weaver/main/setup-pi.sh | bash"
  52. echo ""
  53. echo "Or from existing clone:"
  54. echo " cd ~/dune-weaver && bash setup-pi.sh [OPTIONS]"
  55. echo ""
  56. echo "Options:"
  57. echo " --no-wifi-fix Skip WiFi stability fix (applied by default)"
  58. echo " --no-hotspot Skip autohotspot setup"
  59. echo " --help, -h Show this help message"
  60. exit 0
  61. ;;
  62. *)
  63. echo -e "${RED}Unknown option: $1${NC}"
  64. echo "Use --help for usage information"
  65. exit 1
  66. ;;
  67. esac
  68. done
  69. # ── Permission helpers ──────────────────────────────────────────────
  70. # Run a command as the real (non-root) user.
  71. # When the script is invoked with sudo, commands like git clone, pip install,
  72. # and venv creation should run as the real user so files are owned correctly
  73. # from the start — no chown needed afterward.
  74. run_as_user() {
  75. if [[ $EUID -eq 0 && -n "$SUDO_USER" ]]; then
  76. sudo -u "$SUDO_USER" -- "$@"
  77. else
  78. "$@"
  79. fi
  80. }
  81. # Ensure the entire repo tree is owned by the real user.
  82. # Call this as a safety net after any operation that may have created
  83. # files as root (e.g. an older version of this script, or a plugin).
  84. fix_repo_ownership() {
  85. if [[ $EUID -eq 0 && -n "$SUDO_USER" ]]; then
  86. chown -R "$SUDO_USER:$SUDO_USER" "$INSTALL_DIR"
  87. fi
  88. }
  89. # ────────────────────────────────────────────────────────────────────
  90. # Helper functions
  91. print_step() {
  92. echo -e "\n${BLUE}==>${NC} ${GREEN}$1${NC}"
  93. }
  94. print_warning() {
  95. echo -e "${YELLOW}Warning:${NC} $1"
  96. }
  97. print_error() {
  98. echo -e "${RED}Error:${NC} $1"
  99. }
  100. print_success() {
  101. echo -e "${GREEN}$1${NC}"
  102. }
  103. # Install system dependencies
  104. install_system_deps() {
  105. print_step "Installing system dependencies..."
  106. sudo apt update
  107. sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \
  108. python3-venv python3-pip python3-dev \
  109. gcc g++ make swig unzip wget \
  110. libjpeg-dev zlib1g-dev \
  111. libgpiod-dev gpiod \
  112. nginx git vim
  113. print_success "System dependencies installed"
  114. }
  115. # Ensure lgpio C library is available for pip to build against
  116. install_lgpio() {
  117. local syslib="/usr/lib/aarch64-linux-gnu"
  118. # Raspberry Pi OS Trixie ships liblgpio.so.1 but no unversioned
  119. # liblgpio.so symlink (normally provided by a -dev package).
  120. # The build-time linker needs the unversioned name (-llgpio → liblgpio.so).
  121. if [[ ! -e "$syslib/liblgpio.so" ]]; then
  122. # Check if the versioned library exists (from Pi OS)
  123. local versioned
  124. versioned=$(ls "$syslib"/liblgpio.so.* 2>/dev/null | head -1)
  125. if [[ -n "$versioned" ]]; then
  126. print_step "Creating liblgpio.so build symlink..."
  127. sudo ln -sf "$versioned" "$syslib/liblgpio.so"
  128. sudo ldconfig
  129. print_success "liblgpio.so symlink created (-> $(basename "$versioned"))"
  130. else
  131. # Not in system paths — build from source
  132. print_step "Building lgpio C library from source..."
  133. local tmpdir
  134. tmpdir=$(mktemp -d)
  135. cd "$tmpdir"
  136. wget -q https://github.com/joan2937/lg/archive/master.zip
  137. unzip -q master.zip
  138. cd lg-master
  139. make
  140. sudo make install
  141. cd /
  142. rm -rf "$tmpdir"
  143. # Symlink from /usr/local/lib into system path
  144. if [[ -f /usr/local/lib/liblgpio.so ]]; then
  145. sudo ln -sf /usr/local/lib/liblgpio.so "$syslib/liblgpio.so"
  146. fi
  147. sudo ldconfig
  148. print_success "lgpio C library built and installed"
  149. fi
  150. else
  151. echo "liblgpio.so already available for linking"
  152. fi
  153. }
  154. # Check if running on Raspberry Pi
  155. check_raspberry_pi() {
  156. print_step "Checking system compatibility..."
  157. if [[ ! -f /proc/device-tree/model ]]; then
  158. print_warning "Could not detect Raspberry Pi model. Continuing anyway..."
  159. return
  160. fi
  161. MODEL=$(tr -d '\0' < /proc/device-tree/model)
  162. echo "Detected: $MODEL"
  163. # Check for 64-bit OS
  164. ARCH=$(uname -m)
  165. if [[ "$ARCH" != "aarch64" && "$ARCH" != "arm64" ]]; then
  166. print_error "64-bit OS required. Detected: $ARCH"
  167. echo "Please reinstall Raspberry Pi OS (64-bit) using Raspberry Pi Imager"
  168. exit 1
  169. fi
  170. print_success "64-bit OS detected ($ARCH)"
  171. }
  172. # Disable WLAN power save
  173. disable_wlan_powersave() {
  174. print_step "Disabling WLAN power save for better stability..."
  175. # Check if already disabled
  176. if iwconfig wlan0 2>/dev/null | grep -q "Power Management:off"; then
  177. echo "WLAN power save already disabled"
  178. return
  179. fi
  180. # Create config to persist across reboots
  181. sudo tee /etc/NetworkManager/conf.d/wifi-powersave-off.conf > /dev/null << 'EOF'
  182. [connection]
  183. wifi.powersave = 2
  184. EOF
  185. # Also try immediate disable
  186. sudo iwconfig wlan0 power off 2>/dev/null || true
  187. print_success "WLAN power save disabled"
  188. }
  189. # Apply WiFi stability fix
  190. apply_wifi_fix() {
  191. print_step "Applying WiFi stability fix..."
  192. CMDLINE_FILE="/boot/firmware/cmdline.txt"
  193. if [[ ! -f "$CMDLINE_FILE" ]]; then
  194. CMDLINE_FILE="/boot/cmdline.txt"
  195. fi
  196. if [[ ! -f "$CMDLINE_FILE" ]]; then
  197. print_warning "Could not find cmdline.txt, skipping WiFi fix"
  198. return
  199. fi
  200. # Check if fix already applied
  201. if grep -q "brcmfmac.feature_disable=0x82000" "$CMDLINE_FILE"; then
  202. echo "WiFi fix already applied"
  203. return
  204. fi
  205. # Backup and apply fix
  206. sudo cp "$CMDLINE_FILE" "${CMDLINE_FILE}.backup"
  207. sudo sed -i 's/$/ brcmfmac.feature_disable=0x82000/' "$CMDLINE_FILE"
  208. print_success "WiFi fix applied. A reboot is recommended after setup."
  209. NEEDS_REBOOT=true
  210. }
  211. # Verify we're in the dune-weaver directory
  212. ensure_repo() {
  213. print_step "Setting up dune-weaver repository..."
  214. # Check if we're already in the dune-weaver directory
  215. if [[ -f "main.py" ]] && [[ -f "requirements.txt" ]]; then
  216. INSTALL_DIR="$(pwd)"
  217. print_success "Using existing repo at $INSTALL_DIR"
  218. fix_repo_ownership
  219. return
  220. fi
  221. # Check if repo exists in home directory
  222. if [[ -d "$INSTALL_DIR" ]] && [[ -f "$INSTALL_DIR/main.py" ]]; then
  223. print_success "Found existing repo at $INSTALL_DIR"
  224. cd "$INSTALL_DIR"
  225. echo "Pulling latest changes..."
  226. run_as_user git pull
  227. fix_repo_ownership
  228. return
  229. fi
  230. # Clone the repository as the real user so files are owned correctly
  231. print_step "Cloning dune-weaver repository..."
  232. run_as_user git clone "$REPO_URL" --single-branch "$INSTALL_DIR"
  233. cd "$INSTALL_DIR"
  234. print_success "Cloned to $INSTALL_DIR"
  235. }
  236. # Deploy native (venv + systemd + nginx)
  237. deploy_native() {
  238. print_step "Setting up Python virtual environment..."
  239. cd "$INSTALL_DIR"
  240. # Safety net: fix ownership in case repo was cloned/pulled as root
  241. # by an older version of this script or manual sudo git operations
  242. fix_repo_ownership
  243. # Create venv as real user
  244. run_as_user python3 -m venv .venv
  245. source .venv/bin/activate
  246. # Install dependencies as real user (pip writes to user-owned .venv)
  247. print_step "Installing Python packages..."
  248. run_as_user .venv/bin/pip install --upgrade pip
  249. run_as_user .venv/bin/pip install -r requirements.txt
  250. # Ensure nginx (www-data) can traverse to static files
  251. # chmod o+x grants traversal only, not directory listing
  252. local dir="$INSTALL_DIR"
  253. while [[ "$dir" != "/" ]]; do
  254. sudo chmod o+x "$dir"
  255. dir=$(dirname "$dir")
  256. done
  257. # Configure nginx
  258. print_step "Configuring nginx..."
  259. sudo cp "$INSTALL_DIR/nginx/dune-weaver.conf" /etc/nginx/sites-available/dune-weaver.conf
  260. sudo sed -i "s|INSTALL_DIR_PLACEHOLDER|$INSTALL_DIR|g" /etc/nginx/sites-available/dune-weaver.conf
  261. sudo ln -sf /etc/nginx/sites-available/dune-weaver.conf /etc/nginx/sites-enabled/dune-weaver.conf
  262. sudo rm -f /etc/nginx/sites-enabled/default
  263. sudo nginx -t
  264. sudo systemctl restart nginx
  265. sudo systemctl enable nginx
  266. # Create systemd service
  267. print_step "Creating systemd service..."
  268. sudo cp "$INSTALL_DIR/dune-weaver.service" /etc/systemd/system/dune-weaver.service
  269. sudo sed -i "s|INSTALL_DIR_PLACEHOLDER|$INSTALL_DIR|g" /etc/systemd/system/dune-weaver.service
  270. # Enable and start service
  271. sudo systemctl daemon-reload
  272. sudo systemctl enable dune-weaver
  273. sudo systemctl start dune-weaver
  274. # Create sudoers entry for passwordless systemctl commands
  275. # Use REAL_USER (not $USER which is root under sudo)
  276. print_step "Configuring sudo permissions..."
  277. sudo tee /etc/sudoers.d/dune-weaver > /dev/null << EOF
  278. $REAL_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart dune-weaver
  279. $REAL_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop dune-weaver
  280. $REAL_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl start dune-weaver
  281. $REAL_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl poweroff
  282. $REAL_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
  283. EOF
  284. sudo chmod 0440 /etc/sudoers.d/dune-weaver
  285. print_success "Native deployment complete!"
  286. }
  287. # Install dw CLI command
  288. install_cli() {
  289. print_step "Installing 'dw' command..."
  290. # Copy dw script to /usr/local/bin
  291. sudo cp "$INSTALL_DIR/dw" /usr/local/bin/dw
  292. sudo chmod +x /usr/local/bin/dw
  293. print_success "'dw' command installed"
  294. }
  295. # Setup autohotspot
  296. setup_autohotspot() {
  297. print_step "Setting up autohotspot..."
  298. if [[ ! -f "$INSTALL_DIR/wifi/setup-wifi.sh" ]]; then
  299. print_warning "wifi/setup-wifi.sh not found, skipping autohotspot setup"
  300. return
  301. fi
  302. bash "$INSTALL_DIR/wifi/setup-wifi.sh"
  303. print_success "Autohotspot setup complete"
  304. }
  305. # Configure UART for GPIO pin connection to DLC32/ESP32
  306. configure_uart() {
  307. local CONFIG_FILE="/boot/firmware/config.txt"
  308. if [[ ! -f "$CONFIG_FILE" ]]; then
  309. CONFIG_FILE="/boot/config.txt"
  310. fi
  311. echo ""
  312. echo -e "${GREEN}How is your Raspberry Pi connected to the sand table controller (DLC32/ESP32)?${NC}"
  313. echo ""
  314. echo " 1) USB cable"
  315. echo " 2) UART over GPIO pins (TX/RX wired to header pins)"
  316. echo ""
  317. echo -e " ${YELLOW}Note: USB is not reliable on Pi 3B+. Use UART for Pi 3B+.${NC}"
  318. echo ""
  319. read -p "Enter choice [1/2] (default: 1): " -n 1 -r uart_choice
  320. echo ""
  321. if [[ "$uart_choice" == "2" ]]; then
  322. echo ""
  323. echo -e "${YELLOW}============================================${NC}"
  324. echo -e "${YELLOW} UART Setup — raspi-config will run next${NC}"
  325. echo -e "${YELLOW}============================================${NC}"
  326. echo ""
  327. echo -e " When prompted, select:"
  328. echo -e " Login shell over serial? → ${GREEN}No${NC}"
  329. echo -e " Serial port hardware? → ${GREEN}Yes${NC}"
  330. echo ""
  331. read -p "Press Enter to continue..." -r
  332. echo ""
  333. # Disable serial console, enable serial hardware
  334. if command -v raspi-config &> /dev/null; then
  335. sudo raspi-config nonint do_serial 2
  336. echo "Serial console disabled, serial hardware enabled"
  337. else
  338. print_warning "raspi-config not found, please run 'sudo raspi-config' manually"
  339. print_warning "Go to: 3 Interface Options > I6 Serial Port > No (console) > Yes (hardware)"
  340. fi
  341. print_step "Configuring UART overlays..."
  342. # Add UART overlays to config.txt if not already present
  343. local needs_change=false
  344. for overlay in "dtoverlay=pi3-miniuart-bt" "dtoverlay=miniuart-bt" "enable_uart=1"; do
  345. if ! grep -q "^${overlay}$" "$CONFIG_FILE" 2>/dev/null; then
  346. echo "$overlay" | sudo tee -a "$CONFIG_FILE" > /dev/null
  347. needs_change=true
  348. fi
  349. done
  350. if [[ "$needs_change" == "true" ]]; then
  351. echo "Added UART overlays to $CONFIG_FILE"
  352. else
  353. echo "UART overlays already present in $CONFIG_FILE"
  354. fi
  355. NEEDS_REBOOT=true
  356. print_success "UART configured. A reboot is required for changes to take effect."
  357. else
  358. # USB mode — check if UART config exists and offer to clean it up
  359. local has_uart=false
  360. for overlay in "dtoverlay=pi3-miniuart-bt" "dtoverlay=miniuart-bt" "enable_uart=1"; do
  361. if grep -q "^${overlay}$" "$CONFIG_FILE" 2>/dev/null; then
  362. has_uart=true
  363. break
  364. fi
  365. done
  366. if [[ "$has_uart" == "true" ]]; then
  367. echo -e "${YELLOW}UART overlays found in $CONFIG_FILE from a previous setup.${NC}"
  368. read -p "Remove them? (y/N): " -n 1 -r remove_uart
  369. echo ""
  370. if [[ "$remove_uart" =~ ^[Yy]$ ]]; then
  371. sudo sed -i '/^dtoverlay=pi3-miniuart-bt$/d' "$CONFIG_FILE"
  372. sudo sed -i '/^dtoverlay=miniuart-bt$/d' "$CONFIG_FILE"
  373. sudo sed -i '/^enable_uart=1$/d' "$CONFIG_FILE"
  374. NEEDS_REBOOT=true
  375. print_success "UART overlays removed. A reboot is recommended."
  376. fi
  377. else
  378. echo "USB connection selected, no UART changes needed."
  379. fi
  380. fi
  381. }
  382. # Remove software that is no longer needed on the Pi
  383. cleanup_unused() {
  384. print_step "Cleaning up unused software..."
  385. # Remove Node.js / npm if present from a prior install
  386. if dpkg -l nodejs 2>/dev/null | grep -q '^ii'; then
  387. echo "Removing Node.js (no longer needed — frontend is pre-built)..."
  388. sudo apt purge -y nodejs || true
  389. sudo rm -f /etc/apt/sources.list.d/nodesource.list \
  390. /etc/apt/keyrings/nodesource.gpg 2>/dev/null || true
  391. sudo apt autoremove -y
  392. print_success "Node.js removed"
  393. fi
  394. # Remove Docker if present from old Docker-based deployment
  395. if dpkg -l docker-ce 2>/dev/null | grep -q '^ii'; then
  396. echo "Removing Docker (old deployment method)..."
  397. # Stop running containers
  398. sudo docker stop $(sudo docker ps -aq) 2>/dev/null || true
  399. sudo apt purge -y docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null || true
  400. sudo rm -rf /var/lib/docker
  401. sudo apt autoremove -y
  402. print_success "Docker removed"
  403. fi
  404. }
  405. # Get IP address
  406. get_ip_address() {
  407. # Try multiple methods to get IP
  408. IP=$(hostname -I 2>/dev/null | awk '{print $1}')
  409. if [[ -z "$IP" ]]; then
  410. IP=$(ip route get 1 2>/dev/null | awk '{print $7}' | head -1)
  411. fi
  412. if [[ -z "$IP" ]]; then
  413. IP="<your-pi-ip>"
  414. fi
  415. echo "$IP"
  416. }
  417. # Print final instructions
  418. print_final_instructions() {
  419. IP=$(get_ip_address)
  420. HOSTNAME=$(hostname)
  421. echo ""
  422. echo -e "${GREEN}============================================${NC}"
  423. echo -e "${GREEN} Dune Weaver Setup Complete!${NC}"
  424. echo -e "${GREEN}============================================${NC}"
  425. echo ""
  426. echo -e "Access the web interface at:"
  427. echo -e " ${BLUE}http://$IP${NC}"
  428. echo -e " ${BLUE}http://$HOSTNAME.local${NC}"
  429. echo ""
  430. echo "Manage with the 'dw' command:"
  431. echo " dw logs View live logs"
  432. echo " dw restart Restart Dune Weaver"
  433. echo " dw update Pull latest and restart"
  434. echo " dw stop Stop Dune Weaver"
  435. echo " dw status Show status"
  436. echo " dw wifi help WiFi and hotspot management"
  437. echo " dw help Show all commands"
  438. echo ""
  439. if [[ "$SETUP_HOTSPOT" == "true" ]]; then
  440. echo -e "${BLUE}Autohotspot:${NC} If no known WiFi is found on boot,"
  441. echo "a 'Dune Weaver' hotspot will be created automatically."
  442. echo "Connect to it and open the app to configure WiFi."
  443. echo ""
  444. fi
  445. if [[ "$NEEDS_REBOOT" == "true" ]]; then
  446. print_warning "A reboot is required to apply configuration changes"
  447. read -p "Reboot now? (y/N) " -n 1 -r
  448. echo
  449. if [[ $REPLY =~ ^[Yy]$ ]]; then
  450. sudo reboot
  451. fi
  452. fi
  453. }
  454. # Main installation flow
  455. main() {
  456. echo -e "${GREEN}"
  457. echo " ____ __ __ "
  458. echo " | _ \ _ _ _ __ ___\ \ / /__ __ ___ _____ _ __ "
  459. echo " | | | | | | | '_ \ / _ \\ \ /\ / / _ \/ _\` \ \ / / _ \ '__|"
  460. echo " | |_| | |_| | | | | __/ \ V V / __/ (_| |\ V / __/ | "
  461. echo " |____/ \__,_|_| |_|\___| \_/\_/ \___|\__,_| \_/ \___|_| "
  462. echo -e "${NC}"
  463. echo "Raspberry Pi Setup Script"
  464. echo ""
  465. echo "Install directory: $INSTALL_DIR"
  466. echo ""
  467. # Ask connection type upfront (before long-running installs)
  468. configure_uart
  469. # Run setup steps
  470. check_raspberry_pi
  471. install_system_deps
  472. ensure_repo
  473. disable_wlan_powersave
  474. if [[ "$FIX_WIFI" == "true" ]]; then
  475. apply_wifi_fix
  476. fi
  477. if [[ "$SETUP_HOTSPOT" == "true" ]]; then
  478. setup_autohotspot
  479. fi
  480. install_lgpio
  481. deploy_native
  482. install_cli
  483. cleanup_unused
  484. print_final_instructions
  485. }
  486. # Run main
  487. main