setup-pi.sh 18 KB

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