mirror of
https://github.com/furyhawk/ESP32-X-Desktop.git
synced 2026-07-21 09:05:33 +00:00
310 lines
9.5 KiB
Bash
Executable File
310 lines
9.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# dump_flash.sh - Backup ESP32-S3 external SPI flash using esptool.py
|
|
# Requirements: esptool.py (pip install esptool)
|
|
|
|
set -euo pipefail
|
|
|
|
CHIP="auto"
|
|
PORT="${ESPPORT:-}"
|
|
BAUD="${ESPBAUD:-921600}"
|
|
SIZE="" # e.g. 4M, 8M, 16M, or bytes. If empty, attempt auto-detect.
|
|
OFFSET="0" # e.g. 0, 0x0
|
|
OUTPUT="" # If empty, generate based on date/time
|
|
GZIP="1"
|
|
FORCE="0"
|
|
QUIET="0"
|
|
|
|
# --- Logging helpers --------------------------------------------------------
|
|
ts() { date '+%Y-%m-%d %H:%M:%S'; }
|
|
STEP=0
|
|
step() { STEP=$(( STEP + 1 )); echo "==> [$(ts)] Step ${STEP}: $*"; }
|
|
info() { echo " [$(ts)] $*"; }
|
|
warn() { echo " [$(ts)] WARNING: $*" >&2; }
|
|
|
|
usage() {
|
|
echo "Usage: $0 [-p PORT] [-b BAUD] [-c CHIP] [-s SIZE] [-O OFFSET] [-o OUTPUT] [--gzip] [--force] [--quiet]"
|
|
echo " -p, --port Serial port (default: auto-detect from /dev/ttyACM*, /dev/ttyUSB*, /dev/tty.usbmodem*, or /dev/tty.usbserial*)"
|
|
echo " -b, --baud Baud rate (default: ${BAUD})"
|
|
echo " -c, --chip Chip type (default: auto-detect)"
|
|
echo " -s, --size Flash size to read, e.g. 4M, 8M, 16M, or bytes. If omitted, try detect."
|
|
echo " -O, --offset Start offset (default: 0)"
|
|
echo " -o, --output Output filename (default: flash_YYYYmmdd_HHMMSS.bin)"
|
|
echo " --gzip Gzip the resulting .bin (default: enabled)"
|
|
echo " --no-gzip Do not gzip the resulting .bin"
|
|
echo " --force Overwrite existing output file"
|
|
echo " -q, --quiet Suppress progress output during flash reading"
|
|
echo "Examples:"
|
|
echo " $0 -p /dev/ttyACM0 -s 8M -o backup.bin"
|
|
echo " $0 --no-gzip (auto-detect port, chip, and size, do not gzip)"
|
|
echo " $0 -c esp32s3 (force specific chip type)"
|
|
echo " $0 --quiet (suppress verbose progress output)"
|
|
exit 1
|
|
}
|
|
|
|
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
die() { echo "Error: $*" >&2; exit 1; }
|
|
|
|
get_esptool_version() {
|
|
local v=""
|
|
v="$($ESPTOOL version 2>&1 | head -n1 || true)"
|
|
if [[ -z "$v" || "$v" == usage:* ]]; then
|
|
v="$($ESPTOOL --version 2>&1 | head -n1 || true)"
|
|
fi
|
|
if [[ -z "$v" || "$v" == usage:* ]]; then
|
|
v="unknown"
|
|
fi
|
|
echo "$v"
|
|
}
|
|
|
|
file_size_bytes() {
|
|
local f="$1"
|
|
if stat -f%z "$f" >/dev/null 2>&1; then
|
|
stat -f%z "$f"
|
|
else
|
|
stat -c%s "$f"
|
|
fi
|
|
}
|
|
|
|
parse_size() {
|
|
# Accept integers, hex (0x...), and suffixes K/M/G (binary: *1024)
|
|
local s="${1:-}"
|
|
[[ -z "$s" ]] && echo "" && return 0
|
|
if [[ "$s" =~ ^0x[0-9A-Fa-f]+$ ]]; then
|
|
printf "%d" "$((s))"
|
|
elif [[ "$s" =~ ^[0-9]+$ ]]; then
|
|
printf "%d" "$s"
|
|
elif [[ "$s" =~ ^([0-9]+)([KkMmGg])$ ]]; then
|
|
local n="${BASH_REMATCH[1]}"
|
|
local u="${BASH_REMATCH[2]}"
|
|
case "$u" in
|
|
K|k) printf "%d" "$(( n * 1024 ))" ;;
|
|
M|m) printf "%d" "$(( n * 1024 * 1024 ))" ;;
|
|
G|g) printf "%d" "$(( n * 1024 * 1024 * 1024 ))" ;;
|
|
esac
|
|
else
|
|
die "Invalid size format: $s"
|
|
fi
|
|
}
|
|
|
|
bytes_to_h() {
|
|
# Pretty print bytes as human-readable binary units
|
|
local b="$1"
|
|
local unit=("B" "KiB" "MiB" "GiB")
|
|
local i=0
|
|
local v="$b"
|
|
while (( v >= 1024 && i < 3 )); do
|
|
v=$(( (v + 512) / 1024 ))
|
|
((i++))
|
|
done
|
|
echo "${v}${unit[$i]}"
|
|
}
|
|
|
|
autodetect_port() {
|
|
local ports=()
|
|
local pat
|
|
local p
|
|
for pat in "/dev/ttyACM*" "/dev/ttyUSB*" "/dev/tty.usbmodem*" "/dev/tty.usbserial*"; do
|
|
while IFS= read -r p; do
|
|
ports+=("$p")
|
|
done < <(compgen -G "$pat" || true)
|
|
done
|
|
if [[ ${#ports[@]} -eq 0 ]]; then
|
|
die "No serial ports found. Specify with -p /dev/ttyACM0 (press BOOT while plugging in if needed)."
|
|
elif [[ ${#ports[@]} -eq 1 ]]; then
|
|
echo "${ports[0]}"
|
|
else
|
|
echo "Multiple serial ports found:" >&2
|
|
printf ' %s\n' "${ports[@]}" >&2
|
|
die "Please specify one with -p."
|
|
fi
|
|
}
|
|
|
|
detect_chip_type() {
|
|
# Detect the actual chip type using esptool chip_id
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
if "$ESPTOOL" --chip auto --port "$PORT" --baud "$BAUD" --before default_reset --after no_reset chip_id 2>&1 | tee "$tmp" >&2; then
|
|
local detected_chip
|
|
# Extract chip type from "Detecting chip type... ESP32" line
|
|
detected_chip="$(grep 'Detecting chip type\.\.\.' "$tmp" | sed -E 's/.*Detecting chip type\.\.\. (ESP32[A-Z0-9-]*).*/\1/' | tr '[:upper:]' '[:lower:]' | sed 's/-//g')"
|
|
rm -f "$tmp"
|
|
if [[ -n "${detected_chip:-}" ]]; then
|
|
echo "$detected_chip"
|
|
return 0
|
|
fi
|
|
fi
|
|
rm -f "$tmp"
|
|
return 1
|
|
}
|
|
|
|
detect_flash_size_bytes() {
|
|
# Try esptool flash-id and parse 'Detected flash size: XMB' line
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
# no-reset here avoids toggling after detection; the next command will reconnect anyway
|
|
# Send tool output to stderr so function stdout stays clean for the numeric size
|
|
if "$ESPTOOL" --chip "$CHIP" --port "$PORT" --baud "$BAUD" --before default_reset --after no_reset flash_id | tee "$tmp" >&2; then
|
|
:
|
|
else
|
|
rm -f "$tmp"
|
|
return 1
|
|
fi
|
|
local mb
|
|
mb="$(grep -Eo 'Detected flash size: [0-9]+MB' "$tmp" | awk '{print $4}' | tr -d 'MB')"
|
|
rm -f "$tmp"
|
|
if [[ -n "${mb:-}" ]]; then
|
|
printf "%d" "$(( mb * 1024 * 1024 ))"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Parse args
|
|
ARGS=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-p|--port) PORT="$2"; shift 2 ;;
|
|
-b|--baud) BAUD="$2"; shift 2 ;;
|
|
-c|--chip) CHIP="$2"; shift 2 ;;
|
|
-s|--size) SIZE="$2"; shift 2 ;;
|
|
-O|--offset) OFFSET="$2"; shift 2 ;;
|
|
-o|--output) OUTPUT="$2"; shift 2 ;;
|
|
--gzip) GZIP="1"; shift ;;
|
|
--no-gzip) GZIP="0"; shift ;;
|
|
--force) FORCE="1"; shift ;;
|
|
-q|--quiet) QUIET="1"; shift ;;
|
|
-h|--help) usage ;;
|
|
*) echo "Unknown argument: $1" >&2; usage ;;
|
|
esac
|
|
done
|
|
|
|
# Find esptool
|
|
ESPTOOL=""
|
|
if have_cmd esptool; then ESPTOOL="esptool"
|
|
elif have_cmd esptool.py; then ESPTOOL="esptool.py"
|
|
else die "esptool not found. Install with: pip install esptool"
|
|
fi
|
|
|
|
step "Checking prerequisites (esptool)"
|
|
info "Using esptool binary: $(command -v "$ESPTOOL")"
|
|
info "esptool version: $(get_esptool_version)"
|
|
|
|
# Port
|
|
if [[ -z "${PORT:-}" ]]; then
|
|
step "Detecting serial port"
|
|
info "Looking for /dev/ttyACM*, /dev/ttyUSB*, /dev/tty.usbmodem*, or /dev/tty.usbserial* (hold BOOT while plugging in if needed)"
|
|
PORT="$(autodetect_port)"
|
|
fi
|
|
info "Serial port: ${PORT}"
|
|
|
|
# Chip detection
|
|
if [[ "$CHIP" == "auto" ]]; then
|
|
step "Detecting chip type"
|
|
info "Querying target for chip identification"
|
|
if DETECTED_CHIP="$(detect_chip_type)"; then
|
|
CHIP="$DETECTED_CHIP"
|
|
info "Detected: $CHIP"
|
|
else
|
|
warn "Auto-detect failed; defaulting to esp32s3"
|
|
CHIP="esp32s3"
|
|
fi
|
|
else
|
|
step "Using specified chip type"
|
|
info "Chip: $CHIP"
|
|
fi
|
|
|
|
# Offset
|
|
step "Parsing start offset"
|
|
OFFSET_BYTES="$(parse_size "$OFFSET")"
|
|
info "Offset (bytes): ${OFFSET_BYTES}"
|
|
|
|
# Output
|
|
step "Preparing output filename"
|
|
if [[ -z "${OUTPUT:-}" ]]; then
|
|
ts="$(date +%Y%m%d_%H%M%S)"
|
|
OUTPUT="flash_${CHIP}_${ts}.bin"
|
|
fi
|
|
if [[ -e "$OUTPUT" && "$FORCE" != "1" ]]; then
|
|
die "Output file exists: $OUTPUT (use --force to overwrite)"
|
|
fi
|
|
info "Output path: ${OUTPUT}"
|
|
|
|
# Size: detect if not provided
|
|
if [[ -z "${SIZE:-}" ]]; then
|
|
step "Determining flash size"
|
|
info "Querying target with: flash-id"
|
|
if SIZE_BYTES="$(detect_flash_size_bytes)"; then
|
|
SIZE="$SIZE_BYTES"
|
|
info "Detected: $(bytes_to_h "$SIZE")"
|
|
else
|
|
warn "Auto-detect failed; defaulting to 8 MiB"
|
|
SIZE="8M"
|
|
fi
|
|
fi
|
|
SIZE_BYTES="$(parse_size "$SIZE")"
|
|
|
|
step "Summary"
|
|
echo " Chip : $CHIP"
|
|
echo " Port : $PORT"
|
|
echo " Baud : $BAUD"
|
|
echo " Offset : $OFFSET_BYTES bytes"
|
|
echo " Size : $SIZE_BYTES bytes ($(bytes_to_h "$SIZE_BYTES"))"
|
|
echo " Output : $OUTPUT"
|
|
|
|
# Read flash
|
|
step "Reading flash contents (this may take a while)"
|
|
SECONDS=0
|
|
|
|
if [[ "$QUIET" == "1" ]]; then
|
|
info "Running in quiet mode - progress will be minimal"
|
|
"$ESPTOOL" --chip "$CHIP" --port "$PORT" --baud "$BAUD" read_flash "$OFFSET_BYTES" "$SIZE_BYTES" "$OUTPUT" > /dev/null 2>&1 &
|
|
esptool_pid=$!
|
|
|
|
# Simple progress indicator
|
|
while kill -0 "$esptool_pid" 2>/dev/null; do
|
|
if [[ -f "$OUTPUT" ]]; then
|
|
current_size="$(file_size_bytes "$OUTPUT" 2>/dev/null || echo 0)"
|
|
percentage=$(( current_size * 100 / SIZE_BYTES ))
|
|
printf "\r [$(ts)] Progress: %d%% (%s / %s)" "$percentage" "$(bytes_to_h "$current_size")" "$(bytes_to_h "$SIZE_BYTES")"
|
|
fi
|
|
sleep 2
|
|
done
|
|
printf "\n"
|
|
|
|
# Wait for esptool to finish and check exit code
|
|
wait "$esptool_pid"
|
|
esptool_exit_code=$?
|
|
if [[ "$esptool_exit_code" -ne 0 ]]; then
|
|
die "esptool failed with exit code $esptool_exit_code"
|
|
fi
|
|
else
|
|
set -x
|
|
"$ESPTOOL" --chip "$CHIP" --port "$PORT" --baud "$BAUD" read_flash "$OFFSET_BYTES" "$SIZE_BYTES" "$OUTPUT"
|
|
set +x
|
|
fi
|
|
|
|
info "Read completed in ${SECONDS}s"
|
|
|
|
# Verify size
|
|
step "Verifying output size"
|
|
actual_size="$(file_size_bytes "$OUTPUT" 2>/dev/null || echo 0)"
|
|
if [[ "$actual_size" -ne "$SIZE_BYTES" ]]; then
|
|
warn "Output file size ($actual_size) differs from expected ($SIZE_BYTES)."
|
|
fi
|
|
|
|
# Checksum
|
|
if have_cmd sha256sum; then
|
|
step "Computing checksum (SHA256)"
|
|
sha256sum "$OUTPUT" || true
|
|
fi
|
|
|
|
# Optional gzip
|
|
if [[ "$GZIP" == "1" ]]; then
|
|
step "Compressing output with gzip"
|
|
gzip -f "$OUTPUT"
|
|
echo "Compressed to ${OUTPUT}.gz"
|
|
fi
|
|
|
|
step "Completed"
|
|
echo "Done." |