mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-10-28 11:14:58 +00:00
update scripts for md5 check
This commit is contained in:
@@ -22,6 +22,7 @@ pnpm update
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm format
|
pnpm format
|
||||||
pnpm build-webUI
|
pnpm build-webUI
|
||||||
|
pnpm build-recovery
|
||||||
pnpm lint
|
pnpm lint
|
||||||
|
|
||||||
cd ../mock-api
|
cd ../mock-api
|
||||||
|
|||||||
+63
-35
@@ -111,6 +111,52 @@ def build_headers(host_ip, emsesp_url, content_type='application/json', access_t
|
|||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def post_file(session, upload_url, host_ip, emsesp_url, access_token, filename, data, progress_desc=None):
|
||||||
|
"""POST one multipart file to /rest/uploadFile (WebUI-compatible)."""
|
||||||
|
encoder = MultipartEncoder(fields={
|
||||||
|
'file': (filename, data, 'application/octet-stream')
|
||||||
|
})
|
||||||
|
|
||||||
|
bar = None
|
||||||
|
if progress_desc is not None:
|
||||||
|
bar = tqdm(
|
||||||
|
desc=progress_desc,
|
||||||
|
total=encoder.len,
|
||||||
|
dynamic_ncols=True,
|
||||||
|
unit='B',
|
||||||
|
unit_scale=True,
|
||||||
|
unit_divisor=1024,
|
||||||
|
mininterval=0.2
|
||||||
|
)
|
||||||
|
monitor = MultipartEncoderMonitor(
|
||||||
|
encoder, lambda m: bar.update(m.bytes_read - bar.n))
|
||||||
|
body = monitor
|
||||||
|
content_type = monitor.content_type
|
||||||
|
content_length = monitor.len
|
||||||
|
else:
|
||||||
|
body = encoder
|
||||||
|
content_type = encoder.content_type
|
||||||
|
content_length = encoder.len
|
||||||
|
|
||||||
|
headers = build_headers(
|
||||||
|
host_ip,
|
||||||
|
emsesp_url,
|
||||||
|
content_type=content_type,
|
||||||
|
access_token=access_token,
|
||||||
|
extra_headers={
|
||||||
|
'Content-Length': str(content_length),
|
||||||
|
'Origin': emsesp_url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
started = time.perf_counter()
|
||||||
|
response = session.post(upload_url, data=body, headers=headers, timeout=None)
|
||||||
|
elapsed = time.perf_counter() - started
|
||||||
|
if bar is not None:
|
||||||
|
bar.close()
|
||||||
|
return response, encoder.len, elapsed
|
||||||
|
|
||||||
|
|
||||||
def on_upload(source, target, env):
|
def on_upload(source, target, env):
|
||||||
|
|
||||||
# make sure we have set the upload_protocol to custom
|
# make sure we have set the upload_protocol to custom
|
||||||
@@ -166,48 +212,30 @@ def on_upload(source, target, env):
|
|||||||
|
|
||||||
md5 = hashlib.md5(firmware).hexdigest()
|
md5 = hashlib.md5(firmware).hexdigest()
|
||||||
filename = os.path.basename(firmware_path)
|
filename = os.path.basename(firmware_path)
|
||||||
|
# Device enables MD5 check only after a file whose name ends in .md5
|
||||||
encoder = MultipartEncoder(fields={
|
# (same as the WebUI). An ElegantOTA "MD5" form field is ignored.
|
||||||
'MD5': md5,
|
md5_name = os.path.splitext(filename)[0] + '.md5'
|
||||||
'file': (filename, firmware, 'application/octet-stream')}
|
|
||||||
)
|
|
||||||
|
|
||||||
bar = tqdm(desc='Upload Progress',
|
|
||||||
total=encoder.len,
|
|
||||||
dynamic_ncols=True,
|
|
||||||
unit='B',
|
|
||||||
unit_scale=True,
|
|
||||||
unit_divisor=1024,
|
|
||||||
mininterval=0.2
|
|
||||||
)
|
|
||||||
|
|
||||||
monitor = MultipartEncoderMonitor(
|
|
||||||
encoder, lambda monitor: bar.update(monitor.bytes_read - bar.n))
|
|
||||||
|
|
||||||
post_headers = build_headers(
|
|
||||||
host_ip,
|
|
||||||
emsesp_url,
|
|
||||||
content_type=monitor.content_type,
|
|
||||||
access_token=access_token,
|
|
||||||
extra_headers={
|
|
||||||
'Content-Length': str(monitor.len),
|
|
||||||
'Origin': emsesp_url
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
upload_url = f"{emsesp_url}/rest/uploadFile"
|
upload_url = f"{emsesp_url}/rest/uploadFile"
|
||||||
started = time.perf_counter()
|
|
||||||
response = session.post(
|
|
||||||
upload_url, data=monitor, headers=post_headers, timeout=None)
|
|
||||||
elapsed = time.perf_counter() - started
|
|
||||||
|
|
||||||
bar.close()
|
print(f"Uploading MD5 ({md5})")
|
||||||
|
response, _, _ = post_file(
|
||||||
|
session, upload_url, host_ip, emsesp_url, access_token,
|
||||||
|
md5_name, (md5 + '\n').encode('ascii'))
|
||||||
|
if response.status_code != 200:
|
||||||
|
print_fail(f"MD5 upload failed (code {response.status_code}).")
|
||||||
|
return
|
||||||
|
print_success("MD5 accepted")
|
||||||
|
|
||||||
|
print(f"Uploading firmware ({filename})")
|
||||||
|
response, size, elapsed = post_file(
|
||||||
|
session, upload_url, host_ip, emsesp_url, access_token,
|
||||||
|
filename, firmware, progress_desc='Upload Progress')
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print_fail(f"Upload failed (code {response.status_code}).")
|
print_fail(f"Upload failed (code {response.status_code}).")
|
||||||
else:
|
else:
|
||||||
kibs = (encoder.len / 1024) / elapsed if elapsed > 0 else 0
|
kibs = (size / 1024) / elapsed if elapsed > 0 else 0
|
||||||
print_success(
|
print_success(
|
||||||
f"Upload successful ({kibs:.0f} KiB/s in {elapsed:.1f}s). Rebooting device.")
|
f"Upload successful ({kibs:.0f} KiB/s in {elapsed:.1f}s). Rebooting device.")
|
||||||
restart_headers = build_headers(
|
restart_headers = build_headers(
|
||||||
|
|||||||
+60
-33
@@ -82,6 +82,49 @@ def create_base_headers(host_ip, emsesp_url):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def post_file(session, upload_url, host_ip, emsesp_url, access_token, filename, data, progress_desc=None):
|
||||||
|
"""POST one multipart file to /rest/uploadFile (WebUI-compatible)."""
|
||||||
|
encoder = MultipartEncoder(fields={
|
||||||
|
'file': (filename, data, 'application/octet-stream')
|
||||||
|
})
|
||||||
|
|
||||||
|
bar = None
|
||||||
|
if progress_desc is not None:
|
||||||
|
bar = tqdm(
|
||||||
|
desc=progress_desc,
|
||||||
|
total=encoder.len,
|
||||||
|
dynamic_ncols=True,
|
||||||
|
unit='B',
|
||||||
|
unit_scale=True,
|
||||||
|
unit_divisor=1024,
|
||||||
|
mininterval=0.2
|
||||||
|
)
|
||||||
|
monitor = MultipartEncoderMonitor(
|
||||||
|
encoder, lambda m: bar.update(m.bytes_read - bar.n))
|
||||||
|
body = monitor
|
||||||
|
content_type = monitor.content_type
|
||||||
|
content_length = monitor.len
|
||||||
|
else:
|
||||||
|
body = encoder
|
||||||
|
content_type = encoder.content_type
|
||||||
|
content_length = encoder.len
|
||||||
|
|
||||||
|
headers = create_base_headers(host_ip, emsesp_url)
|
||||||
|
headers.update({
|
||||||
|
'Content-Type': content_type,
|
||||||
|
'Content-Length': str(content_length),
|
||||||
|
'Origin': emsesp_url,
|
||||||
|
'Authorization': f'Bearer {access_token}'
|
||||||
|
})
|
||||||
|
|
||||||
|
started = time.perf_counter()
|
||||||
|
response = session.post(upload_url, data=body, headers=headers, timeout=None)
|
||||||
|
elapsed = time.perf_counter() - started
|
||||||
|
if bar is not None:
|
||||||
|
bar.close()
|
||||||
|
return response, encoder.len, elapsed
|
||||||
|
|
||||||
|
|
||||||
def upload(file, ip, username, password):
|
def upload(file, ip, username, password):
|
||||||
"""Upload firmware to EMS-ESP device."""
|
"""Upload firmware to EMS-ESP device."""
|
||||||
print()
|
print()
|
||||||
@@ -127,45 +170,29 @@ def upload(file, ip, username, password):
|
|||||||
|
|
||||||
firmware = file_path.read_bytes()
|
firmware = file_path.read_bytes()
|
||||||
md5 = hashlib.md5(firmware).hexdigest()
|
md5 = hashlib.md5(firmware).hexdigest()
|
||||||
|
# Device enables MD5 check only after a file whose name ends in .md5
|
||||||
encoder = MultipartEncoder(fields={
|
# (same as the WebUI). An ElegantOTA "MD5" form field is ignored.
|
||||||
'MD5': md5,
|
md5_name = file_path.with_suffix('.md5').name
|
||||||
'file': (file_path.name, firmware, 'application/octet-stream')
|
|
||||||
})
|
|
||||||
|
|
||||||
bar = tqdm(
|
|
||||||
desc='Upload Progress',
|
|
||||||
total=encoder.len,
|
|
||||||
dynamic_ncols=True,
|
|
||||||
unit='B',
|
|
||||||
unit_scale=True,
|
|
||||||
unit_divisor=1024,
|
|
||||||
mininterval=0.2
|
|
||||||
)
|
|
||||||
|
|
||||||
monitor = MultipartEncoderMonitor(
|
|
||||||
encoder, lambda monitor: bar.update(monitor.bytes_read - bar.n))
|
|
||||||
|
|
||||||
post_headers = create_base_headers(host_ip, emsesp_url)
|
|
||||||
post_headers.update({
|
|
||||||
'Content-Type': monitor.content_type,
|
|
||||||
'Content-Length': str(monitor.len),
|
|
||||||
'Origin': emsesp_url,
|
|
||||||
'Authorization': f'Bearer {access_token}'
|
|
||||||
})
|
|
||||||
|
|
||||||
upload_url = f"{emsesp_url}/rest/uploadFile"
|
upload_url = f"{emsesp_url}/rest/uploadFile"
|
||||||
started = time.perf_counter()
|
|
||||||
response = session.post(
|
|
||||||
upload_url, data=monitor, headers=post_headers, timeout=None)
|
|
||||||
elapsed = time.perf_counter() - started
|
|
||||||
|
|
||||||
bar.close()
|
print(f"Uploading MD5 ({md5})")
|
||||||
|
response, _, _ = post_file(
|
||||||
|
session, upload_url, host_ip, emsesp_url, access_token,
|
||||||
|
md5_name, (md5 + '\n').encode('ascii'))
|
||||||
|
if response.status_code != 200:
|
||||||
|
print_fail(f"MD5 upload failed (code {response.status_code})")
|
||||||
|
return
|
||||||
|
print_success("MD5 accepted")
|
||||||
|
|
||||||
|
print(f"Uploading firmware ({file_path.name})")
|
||||||
|
response, size, elapsed = post_file(
|
||||||
|
session, upload_url, host_ip, emsesp_url, access_token,
|
||||||
|
file_path.name, firmware, progress_desc='Upload Progress')
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print_fail(f"Upload failed (code {response.status_code})")
|
print_fail(f"Upload failed (code {response.status_code})")
|
||||||
else:
|
else:
|
||||||
kibs = (encoder.len / 1024) / elapsed if elapsed > 0 else 0
|
kibs = (size / 1024) / elapsed if elapsed > 0 else 0
|
||||||
print_success(
|
print_success(
|
||||||
f"Upload successful ({kibs:.0f} KiB/s in {elapsed:.1f}s). Rebooting device.")
|
f"Upload successful ({kibs:.0f} KiB/s in {elapsed:.1f}s). Rebooting device.")
|
||||||
restart_headers = create_base_headers(host_ip, emsesp_url)
|
restart_headers = create_base_headers(host_ip, emsesp_url)
|
||||||
|
|||||||
Executable
+196
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# EMS-ESP firmware upload via curl (same job as upload_cli.py).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/upload_curl.sh -i 10.10.10.175 -f build/firmware/EMS-ESP-….bin
|
||||||
|
# ./scripts/upload_curl.sh -i ems-esp.local -f firmware.bin -u admin -p admin
|
||||||
|
#
|
||||||
|
# Prefer a numeric IP over .local — mDNS (especially under WSL2) can stall on IPv6.
|
||||||
|
#
|
||||||
|
# Requires: curl, and either md5sum or openssl. python3 is used only to parse the
|
||||||
|
# sign-in JSON and (when needed) resolve a hostname to IPv4.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
FILE=""
|
||||||
|
IP="ems-esp.local"
|
||||||
|
USERNAME="admin"
|
||||||
|
PASSWORD="admin"
|
||||||
|
|
||||||
|
USER_AGENT='Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0'
|
||||||
|
|
||||||
|
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||||
|
red() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") -f FILE [-i IP] [-u USERNAME] [-p PASSWORD]
|
||||||
|
|
||||||
|
-f, --file firmware .bin file (required)
|
||||||
|
-i, --ip device address (default: ems-esp.local)
|
||||||
|
-u, --username admin user (default: admin)
|
||||||
|
-p, --password admin password (default: admin)
|
||||||
|
-h, --help show this help
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-f|--file) FILE="${2:-}"; shift 2 ;;
|
||||||
|
-i|--ip) IP="${2:-}"; shift 2 ;;
|
||||||
|
-u|--username) USERNAME="${2:-}"; shift 2 ;;
|
||||||
|
-p|--password) PASSWORD="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) red "Unknown option: $1"; usage; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$FILE" ]]; then
|
||||||
|
red "Missing -f/--file"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$FILE" ]]; then
|
||||||
|
red "File not found: $FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v curl >/dev/null; then
|
||||||
|
red "curl is required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v python3 >/dev/null; then
|
||||||
|
red "python3 is required (sign-in JSON / IPv4 resolve)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strip optional http:// and trailing slash / path
|
||||||
|
HOST="$IP"
|
||||||
|
HOST="${HOST#http://}"
|
||||||
|
HOST="${HOST#https://}"
|
||||||
|
HOST="${HOST%%/*}"
|
||||||
|
# Drop :port for resolve (device always serves HTTP on 80 for this flow)
|
||||||
|
HOST_NO_PORT="${HOST%%:*}"
|
||||||
|
if [[ "$HOST" == \[* ]]; then
|
||||||
|
# literal IPv6 — leave as-is
|
||||||
|
HOST_IP="$HOST"
|
||||||
|
else
|
||||||
|
HOST_IP="$(python3 - "$HOST_NO_PORT" <<'PY'
|
||||||
|
import socket, sys
|
||||||
|
host = sys.argv[1]
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET, host)
|
||||||
|
print(host)
|
||||||
|
except OSError:
|
||||||
|
infos = socket.getaddrinfo(host, 80, socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
if not infos:
|
||||||
|
sys.exit(f"No IPv4 address for {host}")
|
||||||
|
print(infos[0][4][0])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BASE="http://${HOST_IP}"
|
||||||
|
FILE_PATH="$(cd "$(dirname "$FILE")" && pwd)/$(basename "$FILE")"
|
||||||
|
FILE_NAME="$(basename "$FILE_PATH")"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "EMS-ESP Firmware Upload (curl)"
|
||||||
|
if [[ "$HOST_IP" != "$HOST_NO_PORT" ]]; then
|
||||||
|
green "Resolved ${HOST_NO_PORT} -> ${HOST_IP}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- sign in ---
|
||||||
|
SIGNIN_RESP="$(curl -sS -f \
|
||||||
|
-H "Host: ${HOST_IP}" \
|
||||||
|
-H "User-Agent: ${USER_AGENT}" \
|
||||||
|
-H "Accept: */*" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Referer: ${BASE}" \
|
||||||
|
-d "{\"username\":\"${USERNAME}\",\"password\":\"${PASSWORD}\"}" \
|
||||||
|
"${BASE}/rest/signIn")" || {
|
||||||
|
red "Authentication failed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
TOKEN="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])' <<<"$SIGNIN_RESP")" || {
|
||||||
|
red "Authentication failed (no access_token)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
green "Authentication successful"
|
||||||
|
|
||||||
|
# MD5 of the firmware — upload as a .md5 file first (same as the WebUI / upload_cli.py).
|
||||||
|
if command -v md5sum >/dev/null; then
|
||||||
|
MD5="$(md5sum "$FILE_PATH" | awk '{print $1}')"
|
||||||
|
elif command -v openssl >/dev/null; then
|
||||||
|
MD5="$(openssl md5 -r "$FILE_PATH" | awk '{print $1}')"
|
||||||
|
else
|
||||||
|
red "Need md5sum or openssl to compute the digest"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
MD5_TMP="$(mktemp "${TMPDIR:-/tmp}/EMS-ESP.XXXXXX.md5")"
|
||||||
|
RESP_TMP="$(mktemp "${TMPDIR:-/tmp}/EMS-ESP.XXXXXX.rsp")"
|
||||||
|
trap 'rm -f "$MD5_TMP" "$RESP_TMP"' EXIT
|
||||||
|
printf '%s\n' "$MD5" >"$MD5_TMP"
|
||||||
|
|
||||||
|
AUTH_HDR=( -H "Authorization: Bearer ${TOKEN}" )
|
||||||
|
COMMON_HDR=(
|
||||||
|
-H "Host: ${HOST_IP}"
|
||||||
|
-H "User-Agent: ${USER_AGENT}"
|
||||||
|
-H "Accept: */*"
|
||||||
|
-H "Origin: ${BASE}"
|
||||||
|
-H "Referer: ${BASE}"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Uploading MD5 (${MD5})"
|
||||||
|
MD5_HTTP="$(curl -sS -o "$RESP_TMP" -w '%{http_code}' \
|
||||||
|
"${COMMON_HDR[@]}" "${AUTH_HDR[@]}" \
|
||||||
|
-F "file=@${MD5_TMP};filename=${FILE_NAME%.bin}.md5;type=application/octet-stream" \
|
||||||
|
"${BASE}/rest/uploadFile")" || true
|
||||||
|
MD5_BODY="$(cat "$RESP_TMP" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ "$MD5_HTTP" != "200" ]]; then
|
||||||
|
red "MD5 upload failed (HTTP ${MD5_HTTP}${MD5_BODY:+: $MD5_BODY})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
green "MD5 accepted"
|
||||||
|
|
||||||
|
echo "Uploading firmware (${FILE_NAME})"
|
||||||
|
START_NS="$(date +%s%N 2>/dev/null || echo 0)"
|
||||||
|
BIN_HTTP="$(curl -sS --progress-bar -o "$RESP_TMP" -w '%{http_code}' \
|
||||||
|
"${COMMON_HDR[@]}" "${AUTH_HDR[@]}" \
|
||||||
|
-F "file=@${FILE_PATH};filename=${FILE_NAME};type=application/octet-stream" \
|
||||||
|
"${BASE}/rest/uploadFile")" || true
|
||||||
|
BIN_BODY="$(cat "$RESP_TMP" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ "$BIN_HTTP" != "200" ]]; then
|
||||||
|
red "Upload failed (HTTP ${BIN_HTTP}${BIN_BODY:+: $BIN_BODY})"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
END_NS="$(date +%s%N 2>/dev/null || echo 0)"
|
||||||
|
SIZE="$(wc -c <"$FILE_PATH" | tr -d ' ')"
|
||||||
|
if [[ "$START_NS" != "0" && "$END_NS" != "0" ]]; then
|
||||||
|
ELAPSED_MS=$(( (END_NS - START_NS) / 1000000 ))
|
||||||
|
if [[ "$ELAPSED_MS" -gt 0 ]]; then
|
||||||
|
KIBS=$(( SIZE * 1000 / ELAPSED_MS / 1024 ))
|
||||||
|
ELAPSED_S="$(python3 -c "print(f'{${ELAPSED_MS}/1000:.1f}')")"
|
||||||
|
green "Upload successful (${KIBS} KiB/s in ${ELAPSED_S}s). Rebooting device."
|
||||||
|
else
|
||||||
|
green "Upload successful. Rebooting device."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
green "Upload successful. Rebooting device."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Device may drop the connection as it reboots — ignore curl failures here.
|
||||||
|
curl -sS -m 10 \
|
||||||
|
"${COMMON_HDR[@]}" "${AUTH_HDR[@]}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${BASE}/api/system/restart" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
echo
|
||||||
Reference in New Issue
Block a user