setup-pi.sh 18 KB

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