flash-fluidnc.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. #!/bin/bash
  2. #
  3. # Flash FluidNC firmware onto a connected ESP32 board
  4. #
  5. # Usage: bash flash-fluidnc.sh
  6. #
  7. # Interactive script that:
  8. # 1. Lets user choose FluidNC version (v3.9.5 for all tables, v3.8.3 for Mini Dune Weaver)
  9. # 2. Lets user select a serial port
  10. # 3. Downloads, extracts, and flashes the firmware
  11. # 4. Confirms success by checking esptool output
  12. #
  13. set -e
  14. # Warn if running as root/sudo — venv and pip won't work correctly
  15. if [[ $EUID -eq 0 ]]; then
  16. echo -e "\033[1;33mWarning:\033[0m Running as root is not recommended."
  17. echo "If you need serial port access, add your user to the dialout group instead:"
  18. echo " sudo usermod -a -G dialout \$USER"
  19. echo "Then log out and back in, and run: bash flash-fluidnc.sh"
  20. echo ""
  21. read -p "Continue anyway? [y/N]: " root_confirm
  22. if [[ ! "$root_confirm" =~ ^[Yy] ]]; then
  23. exit 1
  24. fi
  25. fi
  26. # Colors
  27. RED='\033[0;31m'
  28. GREEN='\033[0;32m'
  29. YELLOW='\033[1;33m'
  30. BLUE='\033[0;34m'
  31. CYAN='\033[0;36m'
  32. BOLD='\033[1m'
  33. NC='\033[0m'
  34. # Script directory (for finding firmware configs)
  35. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  36. # Versions
  37. VERSION_ALL="3.9.5"
  38. VERSION_MINI="3.8.3"
  39. GITHUB_BASE="https://github.com/bdring/FluidNC/releases/download"
  40. cleanup() {
  41. if [[ -n "${WORK_DIR:-}" && -d "$WORK_DIR" ]]; then
  42. echo -e "\n${BLUE}Cleaning up temporary files...${NC}"
  43. rm -rf "$WORK_DIR"
  44. fi
  45. }
  46. trap cleanup EXIT
  47. # ─── Step 1: Choose table type and version ────────────────────────────────
  48. echo -e "${BOLD}${CYAN}"
  49. echo " ╔═══════════════════════════════════════╗"
  50. echo " ║ FluidNC Firmware Flasher ║"
  51. echo " ║ for Dune Weaver ║"
  52. echo " ╚═══════════════════════════════════════╝"
  53. echo -e "${NC}"
  54. echo -e "${BOLD}Select your table type:${NC}"
  55. echo ""
  56. echo -e " ${GREEN}1)${NC} Dune Weaver"
  57. echo -e " ${GREEN}2)${NC} Dune Weaver Pro"
  58. echo -e " ${GREEN}3)${NC} Dune Weaver Gold"
  59. echo -e " ${GREEN}4)${NC} Dune Weaver Mini Pro"
  60. echo -e " ${GREEN}5)${NC} Dune Weaver Mini"
  61. echo ""
  62. read -p "Enter choice [1-5]: " table_choice
  63. case "$table_choice" in
  64. 1)
  65. TABLE_NAME="Dune Weaver"
  66. CONFIG_DIR="dune_weaver"
  67. VERSION="$VERSION_ALL"
  68. ;;
  69. 2)
  70. TABLE_NAME="Dune Weaver Pro"
  71. CONFIG_DIR="dune_weaver_pro"
  72. VERSION="$VERSION_ALL"
  73. ;;
  74. 3)
  75. TABLE_NAME="Dune Weaver Gold"
  76. CONFIG_DIR="dune_weaver_gold"
  77. VERSION="$VERSION_ALL"
  78. ;;
  79. 4)
  80. TABLE_NAME="Dune Weaver Mini Pro"
  81. CONFIG_DIR="dune_weaver_mini_pro"
  82. VERSION="$VERSION_ALL"
  83. ;;
  84. 5)
  85. TABLE_NAME="Dune Weaver Mini"
  86. CONFIG_DIR="dune_weaver_mini"
  87. VERSION="$VERSION_MINI"
  88. ;;
  89. *)
  90. echo -e "${RED}Invalid choice. Exiting.${NC}"
  91. exit 1
  92. ;;
  93. esac
  94. echo -e "\nSelected: ${GREEN}${TABLE_NAME}${NC} (FluidNC v${VERSION})"
  95. # Locate the config file for the selected table
  96. CONFIG_FILE="$SCRIPT_DIR/firmware/${CONFIG_DIR}/config.yaml"
  97. if [[ ! -f "$CONFIG_FILE" ]]; then
  98. echo -e "${RED}Config file not found: ${CONFIG_FILE}${NC}"
  99. exit 1
  100. fi
  101. echo -e "Config: ${GREEN}${CONFIG_FILE}${NC}"
  102. # ─── Step 2: Detect and select serial port ─────────────────────────────────
  103. echo ""
  104. echo -e "${BOLD}Detecting serial ports...${NC}"
  105. # Collect available serial ports (Linux + macOS patterns)
  106. PORTS=()
  107. for port in /dev/ttyUSB* /dev/ttyACM* /dev/cu.usbserial* /dev/cu.wchusbserial* /dev/cu.SLAB_USBtoUART*; do
  108. if [[ -e "$port" ]]; then
  109. PORTS+=("$port")
  110. fi
  111. done
  112. if [[ ${#PORTS[@]} -eq 0 ]]; then
  113. echo -e "${RED}No serial ports found!${NC}"
  114. echo ""
  115. echo "Make sure your ESP32 board is connected via USB."
  116. echo "On Linux, you may need to add your user to the 'dialout' group:"
  117. echo " sudo usermod -a -G dialout \$USER"
  118. exit 1
  119. fi
  120. if [[ ${#PORTS[@]} -eq 1 ]]; then
  121. PORT="${PORTS[0]}"
  122. echo -e "Found port: ${GREEN}${PORT}${NC}"
  123. read -p "Use this port? [Y/n]: " confirm
  124. if [[ "$confirm" =~ ^[Nn] ]]; then
  125. echo -e "${RED}Exiting.${NC}"
  126. exit 1
  127. fi
  128. else
  129. echo -e "Found ${GREEN}${#PORTS[@]}${NC} serial ports:"
  130. echo ""
  131. for i in "${!PORTS[@]}"; do
  132. echo -e " ${GREEN}$((i + 1)))${NC} ${PORTS[$i]}"
  133. done
  134. echo ""
  135. read -p "Select port [1-${#PORTS[@]}]: " port_choice
  136. if [[ "$port_choice" -ge 1 && "$port_choice" -le ${#PORTS[@]} ]] 2>/dev/null; then
  137. PORT="${PORTS[$((port_choice - 1))]}"
  138. else
  139. echo -e "${RED}Invalid choice. Exiting.${NC}"
  140. exit 1
  141. fi
  142. fi
  143. echo -e "Using port: ${GREEN}${PORT}${NC}"
  144. # ─── Step 3: Check for esptool ─────────────────────────────────────────────
  145. # Find a working pip/venv to install esptool (avoids PEP 668 externally-managed errors)
  146. VENV_DIR=""
  147. if [[ -f "$SCRIPT_DIR/.venv/bin/activate" ]]; then
  148. VENV_DIR="$SCRIPT_DIR/.venv"
  149. elif [[ -f "$HOME/dune-weaver/.venv/bin/activate" ]]; then
  150. VENV_DIR="$HOME/dune-weaver/.venv"
  151. fi
  152. if [[ -n "$VENV_DIR" ]]; then
  153. source "$VENV_DIR/bin/activate"
  154. fi
  155. if ! command -v esptool.py &> /dev/null && ! command -v esptool &> /dev/null; then
  156. echo ""
  157. echo -e "${YELLOW}esptool not found. Installing...${NC}"
  158. # Try the venv pip directly first, then fall back to creating a temp venv
  159. if [[ -n "$VENV_DIR" && -x "$VENV_DIR/bin/pip" ]]; then
  160. "$VENV_DIR/bin/pip" install esptool
  161. elif python3 -m venv /tmp/esptool-venv 2>/dev/null; then
  162. echo -e "${YELLOW}Creating temporary venv for esptool...${NC}"
  163. source /tmp/esptool-venv/bin/activate
  164. pip install esptool
  165. else
  166. # Last resort: override PEP 668 restriction
  167. pip install --break-system-packages esptool 2>/dev/null || pip install esptool
  168. fi
  169. fi
  170. # Determine the esptool command name
  171. if command -v esptool.py &> /dev/null; then
  172. ESPTOOL="esptool.py"
  173. elif command -v esptool &> /dev/null; then
  174. ESPTOOL="esptool"
  175. else
  176. echo -e "${RED}Failed to install esptool. Please install manually: pip install esptool${NC}"
  177. exit 1
  178. fi
  179. # ─── Step 4: Download and extract ──────────────────────────────────────────
  180. ZIP_NAME="fluidnc-v${VERSION}-posix.zip"
  181. DOWNLOAD_URL="${GITHUB_BASE}/v${VERSION}/${ZIP_NAME}"
  182. EXTRACT_DIR="fluidnc-v${VERSION}-posix"
  183. WORK_DIR=$(mktemp -d)
  184. echo ""
  185. echo -e "${BLUE}Downloading FluidNC v${VERSION}...${NC}"
  186. if ! curl -L --fail --progress-bar "$DOWNLOAD_URL" -o "$WORK_DIR/$ZIP_NAME"; then
  187. echo -e "${RED}Download failed!${NC}"
  188. echo "URL: $DOWNLOAD_URL"
  189. exit 1
  190. fi
  191. echo -e "${BLUE}Extracting...${NC}"
  192. unzip -q "$WORK_DIR/$ZIP_NAME" -d "$WORK_DIR"
  193. if [[ ! -d "$WORK_DIR/$EXTRACT_DIR" ]]; then
  194. echo -e "${RED}Expected directory $EXTRACT_DIR not found after extraction.${NC}"
  195. exit 1
  196. fi
  197. FLASH_DIR="$WORK_DIR/$EXTRACT_DIR"
  198. # ─── Step 5: Flash filesystem ─────────────────────────────────────────────
  199. echo ""
  200. echo -e "${BOLD}${CYAN}══════════════════════════════════════════${NC}"
  201. echo -e "${BOLD} Flashing FluidNC v${VERSION} to ${PORT}${NC}"
  202. echo -e "${BOLD}${CYAN}══════════════════════════════════════════${NC}"
  203. echo ""
  204. echo -e "${YELLOW}Do NOT disconnect the board during flashing!${NC}"
  205. echo ""
  206. FLASH_SUCCESS=true
  207. # Flash filesystem (LittleFS) only — firmware is already on the board
  208. echo -e "${BLUE}[1/2] Flashing filesystem (LittleFS)...${NC}"
  209. echo ""
  210. FS_OUTPUT=$($ESPTOOL --chip esp32 --port "$PORT" --baud 460800 \
  211. --before default_reset --after hard_reset \
  212. write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect \
  213. 0x3d0000 "$FLASH_DIR/wifi/littlefs.bin" 2>&1) || true
  214. echo "$FS_OUTPUT"
  215. echo ""
  216. if echo "$FS_OUTPUT" | grep -q "Hard resetting via RTS pin"; then
  217. echo -e "${GREEN}Filesystem flash successful!${NC}"
  218. else
  219. echo -e "${RED}Filesystem flash may have failed!${NC}"
  220. FLASH_SUCCESS=false
  221. fi
  222. # ─── Step 6: Upload config via xmodem ────────────────────────────────────
  223. if [[ "$FLASH_SUCCESS" == true ]]; then
  224. echo ""
  225. echo -e "${BLUE}[2/2] Uploading ${TABLE_NAME} config...${NC}"
  226. echo ""
  227. # Ensure xmodem and pyserial are available
  228. if [[ -n "$VENV_DIR" && -x "$VENV_DIR/bin/pip" ]]; then
  229. "$VENV_DIR/bin/pip" install -q xmodem pyserial 2>/dev/null
  230. elif [[ -d "/tmp/esptool-venv" ]]; then
  231. /tmp/esptool-venv/bin/pip install -q xmodem pyserial 2>/dev/null
  232. else
  233. pip install -q xmodem pyserial 2>/dev/null || true
  234. fi
  235. # Wait for the board to boot after flash reset
  236. echo -e "${YELLOW}Waiting for board to boot...${NC}"
  237. sleep 4
  238. # Upload config.yaml via xmodem using a Python helper
  239. PYTHON_CMD="python3"
  240. if [[ -n "$VENV_DIR" && -x "$VENV_DIR/bin/python3" ]]; then
  241. PYTHON_CMD="$VENV_DIR/bin/python3"
  242. elif [[ -x "/tmp/esptool-venv/bin/python3" ]]; then
  243. PYTHON_CMD="/tmp/esptool-venv/bin/python3"
  244. fi
  245. UPLOAD_OUTPUT=$($PYTHON_CMD - "$PORT" "$CONFIG_FILE" <<'PYEOF'
  246. import sys
  247. import time
  248. import serial
  249. from xmodem import XMODEM
  250. port = sys.argv[1]
  251. config_path = sys.argv[2]
  252. dest_name = "config.yaml"
  253. try:
  254. ser = serial.Serial(port, 115200, timeout=2)
  255. time.sleep(1)
  256. # Drain any boot messages
  257. while ser.in_waiting:
  258. ser.read(ser.in_waiting)
  259. time.sleep(0.1)
  260. # Send xmodem receive command
  261. cmd = f"$Xmodem/Receive={dest_name}\n"
  262. ser.write(cmd.encode())
  263. time.sleep(1)
  264. # Drain response to get into xmodem receive mode
  265. while ser.in_waiting:
  266. ser.read(ser.in_waiting)
  267. time.sleep(0.1)
  268. def getc(size, timeout=1):
  269. ser.timeout = timeout
  270. data = ser.read(size)
  271. return data or None
  272. def putc(data, timeout=1):
  273. return ser.write(data) or None
  274. modem = XMODEM(getc, putc, mode='xmodem')
  275. with open(config_path, 'rb') as f:
  276. success = modem.send(f)
  277. if success:
  278. print("CONFIG_UPLOAD_OK")
  279. else:
  280. print("CONFIG_UPLOAD_FAIL")
  281. # Set this config as the active one and restart to apply
  282. time.sleep(1)
  283. ser.write(b"$Config/Filename=config.yaml\n")
  284. time.sleep(0.5)
  285. ser.write(b"$Bye\n")
  286. ser.close()
  287. except Exception as e:
  288. print(f"CONFIG_UPLOAD_ERROR: {e}")
  289. PYEOF
  290. ) 2>&1 || true
  291. echo "$UPLOAD_OUTPUT"
  292. echo ""
  293. if echo "$UPLOAD_OUTPUT" | grep -q "CONFIG_UPLOAD_OK"; then
  294. echo -e "${GREEN}Config uploaded successfully!${NC}"
  295. else
  296. echo -e "${RED}Config upload failed!${NC}"
  297. echo ""
  298. echo "You can upload the config manually:"
  299. echo " 1. Connect to the 'FluidNC' WiFi network"
  300. echo " 2. Open http://192.168.0.1 in a browser"
  301. echo " 3. Upload: firmware/${CONFIG_DIR}/config.yaml"
  302. FLASH_SUCCESS=false
  303. fi
  304. fi
  305. # ─── Step 7: Summary ──────────────────────────────────────────────────────
  306. echo ""
  307. echo -e "${BOLD}${CYAN}══════════════════════════════════════════${NC}"
  308. if [[ "$FLASH_SUCCESS" == true ]]; then
  309. echo -e "${GREEN} ${TABLE_NAME} setup complete!${NC}"
  310. echo -e "${BOLD}${CYAN}══════════════════════════════════════════${NC}"
  311. echo ""
  312. echo "FluidNC v${VERSION} filesystem + ${TABLE_NAME} config flashed."
  313. echo "The board is restarting with the new configuration."
  314. else
  315. echo -e "${RED} Setup completed with errors!${NC}"
  316. echo -e "${BOLD}${CYAN}══════════════════════════════════════════${NC}"
  317. echo ""
  318. echo "Review the output above for error details."
  319. echo "Common fixes:"
  320. echo " - Hold the BOOT button on the ESP32 while flashing"
  321. echo " - Try a different USB cable (data cable, not charge-only)"
  322. echo " - Check port permissions: sudo chmod 666 $PORT"
  323. echo " - Try a lower baud rate by editing this script (change 460800 to 115200)"
  324. exit 1
  325. fi