setup-pi.sh 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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. return
  206. fi
  207. # Clone the repository
  208. print_step "Cloning dune-weaver repository..."
  209. git clone "$REPO_URL" --single-branch "$INSTALL_DIR"
  210. cd "$INSTALL_DIR"
  211. print_success "Cloned to $INSTALL_DIR"
  212. }
  213. # Deploy native (venv + systemd + nginx)
  214. deploy_native() {
  215. print_step "Setting up Python virtual environment..."
  216. cd "$INSTALL_DIR"
  217. # Create venv — if running as root (sudo), create as the real user
  218. local real_user="${SUDO_USER:-$USER}"
  219. if [[ $EUID -eq 0 && -n "$SUDO_USER" ]]; then
  220. sudo -u "$SUDO_USER" python3 -m venv .venv
  221. else
  222. python3 -m venv .venv
  223. fi
  224. source .venv/bin/activate
  225. # Install dependencies
  226. print_step "Installing Python packages..."
  227. pip install --upgrade pip
  228. pip install -r requirements.txt
  229. # Ensure venv is owned by the real user (not root)
  230. if [[ $EUID -eq 0 && -n "$SUDO_USER" ]]; then
  231. chown -R "$SUDO_USER:$SUDO_USER" .venv
  232. fi
  233. # Ensure nginx (www-data) can traverse to static files
  234. # chmod o+x grants traversal only, not directory listing
  235. local dir="$INSTALL_DIR"
  236. while [[ "$dir" != "/" ]]; do
  237. sudo chmod o+x "$dir"
  238. dir=$(dirname "$dir")
  239. done
  240. # Configure nginx
  241. print_step "Configuring nginx..."
  242. sudo cp "$INSTALL_DIR/nginx/dune-weaver.conf" /etc/nginx/sites-available/dune-weaver.conf
  243. sudo sed -i "s|INSTALL_DIR_PLACEHOLDER|$INSTALL_DIR|g" /etc/nginx/sites-available/dune-weaver.conf
  244. sudo ln -sf /etc/nginx/sites-available/dune-weaver.conf /etc/nginx/sites-enabled/dune-weaver.conf
  245. sudo rm -f /etc/nginx/sites-enabled/default
  246. sudo nginx -t
  247. sudo systemctl restart nginx
  248. sudo systemctl enable nginx
  249. # Create systemd service
  250. print_step "Creating systemd service..."
  251. sudo cp "$INSTALL_DIR/dune-weaver.service" /etc/systemd/system/dune-weaver.service
  252. sudo sed -i "s|USER_PLACEHOLDER|$USER|g" /etc/systemd/system/dune-weaver.service
  253. sudo sed -i "s|INSTALL_DIR_PLACEHOLDER|$INSTALL_DIR|g" /etc/systemd/system/dune-weaver.service
  254. # Enable and start service
  255. sudo systemctl daemon-reload
  256. sudo systemctl enable dune-weaver
  257. sudo systemctl start dune-weaver
  258. # Create sudoers entry for passwordless systemctl commands
  259. print_step "Configuring sudo permissions..."
  260. sudo tee /etc/sudoers.d/dune-weaver > /dev/null << EOF
  261. $USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart dune-weaver
  262. $USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop dune-weaver
  263. $USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl start dune-weaver
  264. $USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl poweroff
  265. $USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
  266. EOF
  267. sudo chmod 0440 /etc/sudoers.d/dune-weaver
  268. print_success "Native deployment complete!"
  269. }
  270. # Install dw CLI command
  271. install_cli() {
  272. print_step "Installing 'dw' command..."
  273. # Copy dw script to /usr/local/bin
  274. sudo cp "$INSTALL_DIR/dw" /usr/local/bin/dw
  275. sudo chmod +x /usr/local/bin/dw
  276. print_success "'dw' command installed"
  277. }
  278. # Setup autohotspot
  279. setup_autohotspot() {
  280. print_step "Setting up autohotspot..."
  281. if [[ ! -f "$INSTALL_DIR/wifi/setup-wifi.sh" ]]; then
  282. print_warning "wifi/setup-wifi.sh not found, skipping autohotspot setup"
  283. return
  284. fi
  285. bash "$INSTALL_DIR/wifi/setup-wifi.sh"
  286. print_success "Autohotspot setup complete"
  287. }
  288. # Configure UART for GPIO pin connection to DLC32/ESP32
  289. configure_uart() {
  290. local CONFIG_FILE="/boot/firmware/config.txt"
  291. if [[ ! -f "$CONFIG_FILE" ]]; then
  292. CONFIG_FILE="/boot/config.txt"
  293. fi
  294. echo ""
  295. echo -e "${GREEN}How is your Raspberry Pi connected to the sand table controller (DLC32/ESP32)?${NC}"
  296. echo ""
  297. echo " 1) USB cable"
  298. echo " 2) UART over GPIO pins (TX/RX wired to header pins)"
  299. echo ""
  300. echo -e " ${YELLOW}Note: USB is not reliable on Pi 3B+. Use UART for Pi 3B+.${NC}"
  301. echo ""
  302. read -p "Enter choice [1/2] (default: 1): " -n 1 -r uart_choice
  303. echo ""
  304. if [[ "$uart_choice" == "2" ]]; then
  305. echo ""
  306. echo -e "${YELLOW}============================================${NC}"
  307. echo -e "${YELLOW} UART Setup — raspi-config will run next${NC}"
  308. echo -e "${YELLOW}============================================${NC}"
  309. echo ""
  310. echo -e " When prompted, select:"
  311. echo -e " Login shell over serial? → ${GREEN}No${NC}"
  312. echo -e " Serial port hardware? → ${GREEN}Yes${NC}"
  313. echo ""
  314. read -p "Press Enter to continue..." -r
  315. echo ""
  316. # Disable serial console, enable serial hardware
  317. if command -v raspi-config &> /dev/null; then
  318. sudo raspi-config nonint do_serial 2
  319. echo "Serial console disabled, serial hardware enabled"
  320. else
  321. print_warning "raspi-config not found, please run 'sudo raspi-config' manually"
  322. print_warning "Go to: 3 Interface Options > I6 Serial Port > No (console) > Yes (hardware)"
  323. fi
  324. print_step "Configuring UART overlays..."
  325. # Add UART overlays to config.txt if not already present
  326. local needs_change=false
  327. for overlay in "dtoverlay=pi3-miniuart-bt" "dtoverlay=miniuart-bt" "enable_uart=1"; do
  328. if ! grep -q "^${overlay}$" "$CONFIG_FILE" 2>/dev/null; then
  329. echo "$overlay" | sudo tee -a "$CONFIG_FILE" > /dev/null
  330. needs_change=true
  331. fi
  332. done
  333. if [[ "$needs_change" == "true" ]]; then
  334. echo "Added UART overlays to $CONFIG_FILE"
  335. else
  336. echo "UART overlays already present in $CONFIG_FILE"
  337. fi
  338. NEEDS_REBOOT=true
  339. print_success "UART configured. A reboot is required for changes to take effect."
  340. else
  341. # USB mode — check if UART config exists and offer to clean it up
  342. local has_uart=false
  343. for overlay in "dtoverlay=pi3-miniuart-bt" "dtoverlay=miniuart-bt" "enable_uart=1"; do
  344. if grep -q "^${overlay}$" "$CONFIG_FILE" 2>/dev/null; then
  345. has_uart=true
  346. break
  347. fi
  348. done
  349. if [[ "$has_uart" == "true" ]]; then
  350. echo -e "${YELLOW}UART overlays found in $CONFIG_FILE from a previous setup.${NC}"
  351. read -p "Remove them? (y/N): " -n 1 -r remove_uart
  352. echo ""
  353. if [[ "$remove_uart" =~ ^[Yy]$ ]]; then
  354. sudo sed -i '/^dtoverlay=pi3-miniuart-bt$/d' "$CONFIG_FILE"
  355. sudo sed -i '/^dtoverlay=miniuart-bt$/d' "$CONFIG_FILE"
  356. sudo sed -i '/^enable_uart=1$/d' "$CONFIG_FILE"
  357. NEEDS_REBOOT=true
  358. print_success "UART overlays removed. A reboot is recommended."
  359. fi
  360. else
  361. echo "USB connection selected, no UART changes needed."
  362. fi
  363. fi
  364. }
  365. # Remove software that is no longer needed on the Pi
  366. cleanup_unused() {
  367. print_step "Cleaning up unused software..."
  368. # Remove Node.js / npm if present from a prior install
  369. if dpkg -l nodejs 2>/dev/null | grep -q '^ii'; then
  370. echo "Removing Node.js (no longer needed — frontend is pre-built)..."
  371. sudo apt purge -y nodejs || true
  372. sudo rm -f /etc/apt/sources.list.d/nodesource.list \
  373. /etc/apt/keyrings/nodesource.gpg 2>/dev/null || true
  374. sudo apt autoremove -y
  375. print_success "Node.js removed"
  376. fi
  377. # Remove Docker if present from old Docker-based deployment
  378. if dpkg -l docker-ce 2>/dev/null | grep -q '^ii'; then
  379. echo "Removing Docker (old deployment method)..."
  380. # Stop running containers
  381. sudo docker stop $(sudo docker ps -aq) 2>/dev/null || true
  382. sudo apt purge -y docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>/dev/null || true
  383. sudo rm -rf /var/lib/docker
  384. sudo apt autoremove -y
  385. print_success "Docker removed"
  386. fi
  387. }
  388. # Get IP address
  389. get_ip_address() {
  390. # Try multiple methods to get IP
  391. IP=$(hostname -I 2>/dev/null | awk '{print $1}')
  392. if [[ -z "$IP" ]]; then
  393. IP=$(ip route get 1 2>/dev/null | awk '{print $7}' | head -1)
  394. fi
  395. if [[ -z "$IP" ]]; then
  396. IP="<your-pi-ip>"
  397. fi
  398. echo "$IP"
  399. }
  400. # Print final instructions
  401. print_final_instructions() {
  402. IP=$(get_ip_address)
  403. HOSTNAME=$(hostname)
  404. echo ""
  405. echo -e "${GREEN}============================================${NC}"
  406. echo -e "${GREEN} Dune Weaver Setup Complete!${NC}"
  407. echo -e "${GREEN}============================================${NC}"
  408. echo ""
  409. echo -e "Access the web interface at:"
  410. echo -e " ${BLUE}http://$IP${NC}"
  411. echo -e " ${BLUE}http://$HOSTNAME.local${NC}"
  412. echo ""
  413. echo "Manage with the 'dw' command:"
  414. echo " dw logs View live logs"
  415. echo " dw restart Restart Dune Weaver"
  416. echo " dw update Pull latest and restart"
  417. echo " dw stop Stop Dune Weaver"
  418. echo " dw status Show status"
  419. echo " dw wifi help WiFi and hotspot management"
  420. echo " dw help Show all commands"
  421. echo ""
  422. if [[ "$SETUP_HOTSPOT" == "true" ]]; then
  423. echo -e "${BLUE}Autohotspot:${NC} If no known WiFi is found on boot,"
  424. echo "a 'Dune Weaver' hotspot will be created automatically."
  425. echo "Connect to it and open the app to configure WiFi."
  426. echo ""
  427. fi
  428. if [[ "$NEEDS_REBOOT" == "true" ]]; then
  429. print_warning "A reboot is required to apply configuration changes"
  430. read -p "Reboot now? (y/N) " -n 1 -r
  431. echo
  432. if [[ $REPLY =~ ^[Yy]$ ]]; then
  433. sudo reboot
  434. fi
  435. fi
  436. }
  437. # Main installation flow
  438. main() {
  439. echo -e "${GREEN}"
  440. echo " ____ __ __ "
  441. echo " | _ \ _ _ _ __ ___\ \ / /__ __ ___ _____ _ __ "
  442. echo " | | | | | | | '_ \ / _ \\ \ /\ / / _ \/ _\` \ \ / / _ \ '__|"
  443. echo " | |_| | |_| | | | | __/ \ V V / __/ (_| |\ V / __/ | "
  444. echo " |____/ \__,_|_| |_|\___| \_/\_/ \___|\__,_| \_/ \___|_| "
  445. echo -e "${NC}"
  446. echo "Raspberry Pi Setup Script"
  447. echo ""
  448. echo "Install directory: $INSTALL_DIR"
  449. echo ""
  450. # Ask connection type upfront (before long-running installs)
  451. configure_uart
  452. # Run setup steps
  453. check_raspberry_pi
  454. install_system_deps
  455. ensure_repo
  456. disable_wlan_powersave
  457. if [[ "$FIX_WIFI" == "true" ]]; then
  458. apply_wifi_fix
  459. fi
  460. if [[ "$SETUP_HOTSPOT" == "true" ]]; then
  461. setup_autohotspot
  462. fi
  463. install_lgpio
  464. deploy_native
  465. install_cli
  466. cleanup_unused
  467. print_final_instructions
  468. }
  469. # Run main
  470. main