1
0

flash-fluidnc.sh 14 KB

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