Initial public release

This commit is contained in:
2026-07-17 15:29:53 -04:00
commit 2d71ce77a1
81 changed files with 32056 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
# ATOMS3 MacroPad
Node-based macro keyboard firmware and programmer for the M5Stack AtomS3.
---
## Scripts
### `python/GUI.bat` — Macro Editor
Launches the graphical macro editor. Use this to create, edit, and upload macro
programs to the device. The GUI handles connecting to the device automatically.
---
### `compile.bat` — Compile Firmware
Compiles the Arduino firmware without uploading. Useful for verifying that the
firmware builds cleanly after making code changes. Output is saved to `build.log`.
```
compile.bat # standard build
compile.bat --clean # force a full rebuild from scratch
```
---
### `upload.bat` — Upload Firmware
Compiles and uploads firmware to the device. The device must already be in
download mode (ROM bootloader) before running this. Requires a COM port argument.
```
upload.bat COM4
```
To enter download mode: hold the side button on the AtomS3 while plugging in USB,
or send `{"cmd":"bootloader"}` to the device over serial if firmware is already running.
---
### `wipe_device.bat` — Wipe Device
Sends the bootloader command to the running firmware, then erases both the NVS
(settings) and LittleFS (macros) flash partitions, leaving the device in a
completely fresh state. Run this before uploading firmware to ensure a clean slate.
```
wipe_device.bat # auto-detect COM port
wipe_device.bat COM3 # use a specific COM port
```
---
### `test_serial.bat` — Serial Communication Test
Runs a quick ping/reconnect test against the device to verify serial communication
is working correctly. Useful for diagnosing connection issues. Output is saved to
`serial_test.log`.
```
test_serial.bat
```
---
## Uploading New Firmware
Follow these steps any time you want to flash updated firmware onto the device.
### Step 1 — Verify the build
```
compile.bat
```
Check the output for `BUILD SUCCESSFUL` and no errors. Fix any compile errors before
continuing. The full output is saved to `build.log`.
### Step 2 — Wipe the device and enter download mode
```
wipe_device.bat
```
This will:
1. Auto-detect the device on its normal COM port (e.g. COM3)
2. Send a reboot-to-bootloader command
3. Wait for the device to re-enumerate as a USB-Serial/JTAG port (e.g. COM4)
4. Erase the settings (NVS) and macro storage (LittleFS) partitions
> **Note:** This erases all saved macros. Re-upload them from the GUI afterwards.
### Step 3 — Flash the firmware
```
upload.bat COM4
```
Replace `COM4` with whatever download-mode port appeared in Step 2. If you are
unsure, check Device Manager — the download-mode port is listed under
*Universal Serial Bus devices* as **USB JTAG/serial debug unit**.
The script compiles, erases the full flash, and uploads the firmware. After it
finishes the device will reboot and re-enumerate on its normal COM port (e.g. COM3).
### Step 4 — Re-upload macros
```
python/GUI.bat
```
Open the editor, connect to the device, and click **Upload All** to restore your
macros.
---
## Typical Workflow
**First-time setup or clean firmware update:**
1. `wipe_device.bat` — wipe the device
2. `upload.bat COM4` — flash fresh firmware (use the download mode COM port)
**Code change / firmware-only update:**
1. `compile.bat` — verify the build
2. `wipe_device.bat` — wipe and enter download mode
3. `upload.bat COM4` — flash updated firmware
**Editing macros:**
1. `python/GUI.bat` — open the editor, connect, and upload macros
---
## Device COM Ports
The AtomS3 enumerates as two different COM ports depending on its state:
| State | Port | Description |
|---|---|---|
| Normal (firmware running) | e.g. COM3 | TinyUSB CDC — used by the GUI and wipe script |
| Download mode (bootloader) | e.g. COM4 | USB-Serial/JTAG — used by `upload.bat` and esptool |
The active port number can vary between machines. Check Device Manager if unsure.
+179
View File
@@ -0,0 +1,179 @@
"""Auto-detect a running ATOMS3 MacroPad and trigger ROM download mode.
Used by upload.bat to skip the manual "hold the side button while plugging
in" dance when a connected device is already running our firmware (which
exposes the `bootloader` serial command). On success, writes the detected
download-mode port to the file given as argv[1] and exits 0. On any
failure (no device, ping miss, bootloader command rejected, device fails
to re-enumerate) exits non-zero so the .bat falls back to manual mode.
This only works for devices already running our firmware — stock ATOMS3
firmware does not support the `bootloader` command, which is why
upload.bat keeps the manual flow as a fallback.
"""
import json
import sys
import time
import serial
import serial.tools.list_ports
DEVICE_ID = "ATOMS3-MACROPAD"
ESPRESSIF_VID = 0x303A
PING_TIMEOUT_S = 2.0
REENUMERATE_TIMEOUT_S = 8.0
def _espressif_ports() -> list:
return [p for p in serial.tools.list_ports.comports() if p.vid == ESPRESSIF_VID]
def _ping(port: str) -> dict | None:
"""Open the port without resetting, send a ping, return parsed reply or None."""
try:
ser = serial.Serial()
ser.port = port
ser.baudrate = 115200
ser.timeout = PING_TIMEOUT_S
ser.dtr = False
ser.rts = False
ser.open()
except (serial.SerialException, OSError):
return None
try:
time.sleep(0.1)
ser.dtr = True
time.sleep(0.3)
ser.reset_input_buffer()
ser.write(b'{"cmd":"ping"}\n')
line = ser.readline().decode("utf-8", errors="ignore").strip()
if not line:
return None
return json.loads(line)
except (serial.SerialException, json.JSONDecodeError, OSError):
return None
finally:
try:
ser.close()
except Exception:
pass
def _find_macropad() -> str | None:
for p in _espressif_ports():
reply = _ping(p.device)
if reply and reply.get("id") == DEVICE_ID:
print(f" Found {DEVICE_ID} v{reply.get('ver', '?')} on {p.device}",
file=sys.stderr)
return p.device
return None
def _send_bootloader(port: str) -> bool:
"""Tell the firmware to call usb_persist_restart(RESTART_BOOTLOADER).
The device acks with `{"rsp":"ok"}` then disappears off this port and
re-enumerates as USB-Serial-JTAG (the native ESP32-S3 bootloader USB
interface). We don't wait for the ack here beyond the read timeout —
even a successful command vanishes the port within ~100 ms.
"""
try:
ser = serial.Serial()
ser.port = port
ser.baudrate = 115200
ser.timeout = 1.0
ser.dtr = False
ser.rts = False
ser.open()
except (serial.SerialException, OSError) as exc:
print(f" ERROR: could not open {port}: {exc}", file=sys.stderr)
return False
try:
time.sleep(0.1)
ser.dtr = True
time.sleep(0.3)
ser.reset_input_buffer()
ser.write(b'{"cmd":"bootloader"}\n')
ser.flush()
# Best-effort read of the ack; absence of one is fine — the chip
# is already on its way to the ROM bootloader.
try:
line = ser.readline().decode("utf-8", errors="ignore").strip()
print(f" bootloader cmd reply: {line or '(none)'}", file=sys.stderr)
except Exception:
pass
return True
except (serial.SerialException, OSError) as exc:
print(f" ERROR sending bootloader command: {exc}", file=sys.stderr)
return False
finally:
try:
ser.close()
except Exception:
pass
def _wait_for_download_port(prev_macropad_port: str) -> str | None:
"""Wait for an Espressif device to appear in download mode.
Strategy: poll the COM enumeration. Accept the first Espressif port we
can see that either (a) is a *different* port than the one the firmware
was on, or (b) is the same port but `ping` no longer answers (firmware
is gone, USB-Serial-JTAG is up). The same-port case is common on
Windows because the OS often reuses the COM number across the PHY
switch.
"""
deadline = time.monotonic() + REENUMERATE_TIMEOUT_S
# Give the device a moment to vanish before we start polling.
time.sleep(0.5)
while time.monotonic() < deadline:
ports = [p.device for p in _espressif_ports()]
for port in ports:
if port != prev_macropad_port:
return port
# Same port re-appeared (or never disappeared): confirm firmware is
# no longer responding. If ping fails, this is the bootloader.
if prev_macropad_port in ports:
if _ping(prev_macropad_port) is None:
return prev_macropad_port
time.sleep(0.4)
return None
def main(argv: list) -> int:
if len(argv) < 2:
print("usage: auto_enter_bootloader.py <output_port_file>", file=sys.stderr)
return 2
out_path = argv[1]
print("Scanning for a running ATOMS3 MacroPad...", file=sys.stderr)
fw_port = _find_macropad()
if not fw_port:
print(" No running MacroPad detected on any Espressif COM port.",
file=sys.stderr)
return 1
print(f"Asking {fw_port} to enter download mode...", file=sys.stderr)
if not _send_bootloader(fw_port):
return 1
print("Waiting for the device to re-enumerate as USB-Serial-JTAG...",
file=sys.stderr)
dl_port = _wait_for_download_port(fw_port)
if not dl_port:
print(" Timed out waiting for download-mode port.", file=sys.stderr)
return 1
print(f" Download-mode port: {dl_port}", file=sys.stderr)
try:
with open(out_path, "w", encoding="ascii") as f:
f.write(dl_port)
except OSError as exc:
print(f" ERROR writing {out_path}: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+35
View File
@@ -0,0 +1,35 @@
@echo off
setlocal
set FQBN=m5stack:esp32:m5stack_atoms3:USBMode=default,CDCOnBoot=cdc,UploadMode=default,PartitionScheme=default_8MB
set SKETCH=%~dp0..\firmware\MacroPad
set LOGFILE=%~dp0build.log
echo ============================================
echo Compiling MacroPad firmware...
echo FQBN: %FQBN%
echo Sketch: %SKETCH%
echo ============================================
:: Check for --clean flag
set CLEAN_FLAG=
if "%1"=="--clean" set CLEAN_FLAG=--clean
arduino-cli compile --fqbn "%FQBN%" "%SKETCH%" %CLEAN_FLAG% > "%LOGFILE%" 2>&1
set RESULT=%ERRORLEVEL%
:: Show last 20 lines of output
echo.
echo --- Build Output (last 20 lines) ---
powershell -Command "Get-Content '%LOGFILE%' | Select-Object -Last 20"
echo.
if %RESULT%==0 (
echo BUILD SUCCESSFUL
) else (
echo BUILD FAILED (exit code %RESULT%)
echo Full log: %LOGFILE%
)
echo.
exit /b %RESULT%
+196
View File
@@ -0,0 +1,196 @@
"""Initial BLE key sync, run at the end of a firmware flash.
A flash does a full chip erase (see upload.bat), which wipes the device's
LittleFS — so the ATOMS3 generates a BRAND-NEW per-device AES-256 key on
its first boot. Whatever key the host had stored is now stale, and live
BLE recording / variable sync would fail GCM auth until the next profile
upload re-synced it. This script closes that gap automatically right after
flashing: it waits for the device to boot, pulls the fresh key over USB,
and stores it in the host keystore (both the per-MAC entry and the legacy
single-key file — exactly what serial_manager.upload_all does).
Robustness:
* The COM port commonly changes across the post-flash reboot, so we
don't trust any hint — we scan every Espressif port each poll.
* We wait up to BOOT_WAIT_S for the device to come up. If it hasn't
appeared within REBOOT_HINT_S we print a one-time nudge to unplug/
replug (covers boards that don't auto-reboot out of download mode),
then keep polling.
* get_ble_key is retried a few times per session — the firmware needs
LittleFS mounted and the keystore initialised, which finishes a beat
into setup().
* Ports are opened with DTR/RTS low (the same non-resetting probe
auto_enter_bootloader.py uses) and the firmware disables DTR-reboot,
so connecting never knocks the device back into the bootloader.
* Serial output is line-noisy (the firmware mirrors [BLE.dbg]/[BOOT]
prints onto the same CDC pipe), so we skip non-JSON lines instead of
failing on the first one.
* Exit 0 on success, 1 otherwise — a failure is reported clearly but
never marks the flash itself as failed (the .bat ignores our code).
"""
import json
import os
import sys
import time
# Make the repo root importable so `ble_keystore` (and its
# `utils.constants` dependency) resolve when run from CompileAndUpload/.
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
try:
import serial
import serial.tools.list_ports
except ImportError:
print(" ERROR: pyserial not installed — cannot sync the BLE key.",
file=sys.stderr)
sys.exit(1)
import ble_keystore
DEVICE_ID = "ATOMS3-MACROPAD"
ESPRESSIF_VID = 0x303A
# Total time to wait for the device to boot + answer.
BOOT_WAIT_S = 40.0
# Initial settle before the first probe (USB re-enumeration after reset).
INITIAL_SETTLE_S = 2.0
# If we still haven't seen the device by this point, nudge the user.
REBOOT_HINT_S = 9.0
# Per-command read window. Generous because boot prints can precede the
# JSON response on the shared CDC pipe.
CMD_TIMEOUT_S = 3.0
# How many times to ask for the key once the device is confirmed.
KEY_RETRIES = 5
def _espressif_ports() -> list:
return [p for p in serial.tools.list_ports.comports()
if p.vid == ESPRESSIF_VID]
def _open(port: str):
"""Open the port without triggering a reset (DTR/RTS low, then raise
DTR once the line is up — matches auto_enter_bootloader.py)."""
ser = serial.Serial()
ser.port = port
ser.baudrate = 115200
ser.timeout = CMD_TIMEOUT_S
ser.dtr = False
ser.rts = False
ser.open()
time.sleep(0.1)
ser.dtr = True
time.sleep(0.3)
ser.reset_input_buffer()
return ser
def _cmd(ser, obj: dict, expect_rsp: str) -> dict | None:
"""Send one JSON command and return the first reply whose "rsp"
matches expect_rsp. Skips boot/debug noise lines."""
try:
ser.reset_input_buffer()
ser.write((json.dumps(obj) + "\n").encode("ascii"))
ser.flush()
except (serial.SerialException, OSError):
return None
deadline = time.monotonic() + CMD_TIMEOUT_S
while time.monotonic() < deadline:
try:
line = ser.readline().decode("utf-8", errors="ignore").strip()
except (serial.SerialException, OSError):
return None
if not line:
continue
try:
d = json.loads(line)
except json.JSONDecodeError:
continue # firmware [BLE.dbg]/[BOOT] line — ignore
if isinstance(d, dict) and d.get("rsp") == expect_rsp:
return d
return None
def _try_port(port: str):
"""Confirm a MacroPad on `port` and pull its key.
Returns (key_bytes, tag_or_None) on success, else None."""
try:
ser = _open(port)
except (serial.SerialException, OSError):
return None
try:
ping = _cmd(ser, {"cmd": "ping"}, "pong")
if not ping or ping.get("id") != DEVICE_ID:
return None
print(f" Found {DEVICE_ID} v{ping.get('ver', '?')} on {port}.")
for attempt in range(KEY_RETRIES):
rsp = _cmd(ser, {"cmd": "get_ble_key"}, "ble_key")
if rsp:
try:
key = bytes.fromhex(rsp["key"])
except (KeyError, ValueError):
key = None
if key and len(key) == ble_keystore.KEY_LEN:
tag = rsp.get("tag")
return key, (tag if isinstance(tag, str) else None)
time.sleep(1.0)
print(" Device responded but did not return a valid BLE key.")
return None
finally:
try:
ser.close()
except Exception:
pass
def _persist(key: bytes, tag: str | None) -> None:
# Always update the legacy single-key file (back-compat), and the
# per-MAC store when the device reported its tag. Both calls reset
# replay-protection counters on a key change, which is exactly what we
# want for a freshly-reflashed device (new key + new boot session).
ble_keystore.save_key(key)
if tag:
ble_keystore.save_key_for_mac(tag, key)
def main(argv: list) -> int:
print("Syncing BLE key — waiting for the ATOMS3 to boot after flashing...")
time.sleep(INITIAL_SETTLE_S)
deadline = time.monotonic() + BOOT_WAIT_S
nudged = False
while time.monotonic() < deadline:
for port in _espressif_ports():
res = _try_port(port.device)
if res:
key, tag = res
try:
_persist(key, tag)
except OSError as exc:
print(f" ERROR: could not write keystore: {exc}",
file=sys.stderr)
return 1
where = tag if tag else "(legacy key file — no device tag)"
print(f" BLE key synced for {where}.")
print(" Live BLE recording and variable sync are ready.")
return 0
if not nudged and time.monotonic() > (deadline - BOOT_WAIT_S
+ REBOOT_HINT_S):
print(" Still waiting... if the device did not reboot into the "
"app on its own,")
print(" unplug and replug it now (it will sync as soon as it "
"boots).")
nudged = True
time.sleep(1.0)
print(" Could not sync the BLE key (device never answered).")
print(" No harm done — uploading a profile from the app syncs the key "
"too. You can also re-run this step.")
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv))
+292
View File
@@ -0,0 +1,292 @@
@echo off
setlocal EnableDelayedExpansion
set FQBN=m5stack:esp32:m5stack_atoms3:USBMode=default,CDCOnBoot=cdc,UploadMode=default,PartitionScheme=default_8MB
set SKETCH=%~dp0..\firmware\MacroPad
set BEFORE=%TEMP%\macropad_ports_before.txt
set AFTER=%TEMP%\macropad_ports_after.txt
set DIFF=%TEMP%\macropad_ports_diff.txt
set ESPTOOL=
for /f "delims=" %%E in ('powershell -NoProfile -Command "Get-ChildItem -Path \"$env:LOCALAPPDATA\Arduino15\packages\" -Recurse -Filter esptool.exe -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName"') do set ESPTOOL=%%E
if not defined ESPTOOL (
echo ERROR: esptool.exe not found under %%LOCALAPPDATA%%\Arduino15\packages
echo The Arduino M5Stack/ESP32 board package may not be installed.
pause
exit /b 1
)
set EXIT_CODE=0
set AUTO_PORT_FILE=%TEMP%\macropad_auto_port.txt
:: --lite only changes the on-screen instructions (the AtomS3 Lite has no
:: display to watch). The firmware itself is universal - same FQBN, same
:: binary - and detects the board at boot.
set LITE=
set EXPLICIT_PORT=
if /i "%~1"=="--lite" (
set LITE=1
set EXPLICIT_PORT=%2
) else (
set EXPLICIT_PORT=%1
if /i "%~2"=="--lite" set LITE=1
)
:: Power-user shortcut: explicit port skips the interactive flow.
if not "%EXPLICIT_PORT%"=="" (
set PORT=%EXPLICIT_PORT%
goto :compile
)
echo.
if defined LITE (
echo ===============================================
echo ATOM S3 Lite MacroPad - Interactive Flasher
echo ===============================================
echo.
echo NOTE: the Lite has no screen. Status comes from
echo its RGB LED - after flashing, a short white pulse
echo means the firmware booted.
) else (
echo ===============================================
echo ATOM S3 MacroPad - Interactive Flasher
echo ===============================================
)
echo.
:: -----------------------------------------------
:: Auto path: if our firmware is already running on
:: a connected ATOMS3, ask it to drop into the ROM
:: bootloader and skip the hold-the-button dance.
:: Falls through to the manual flow on any failure.
:: -----------------------------------------------
echo Checking for a running MacroPad on USB...
echo.
if exist "%AUTO_PORT_FILE%" del "%AUTO_PORT_FILE%" >nul 2>&1
python "%~dp0auto_enter_bootloader.py" "%AUTO_PORT_FILE%"
set AUTO_RESULT=%ERRORLEVEL%
if %AUTO_RESULT%==0 if exist "%AUTO_PORT_FILE%" (
set /p PORT=<"%AUTO_PORT_FILE%"
if not "!PORT!"=="" (
echo.
echo Auto-detected ATOMS3 in download mode on !PORT!.
echo Skipping manual reconnect steps.
echo.
goto :compile
)
)
echo.
echo Auto-detection did not find a running MacroPad.
echo Falling back to manual reconnect flow.
echo.
echo This script will detect the device automatically by
echo watching which COM port appears when you plug the ATOMS3 in.
echo.
:: ---- Step 1: snapshot ports while device is unplugged ----
echo -----------------------------------------------
echo STEP 1 of 4: Disconnect the ATOM S3
echo -----------------------------------------------
echo.
echo Unplug the ATOM S3 from your computer if it's
echo currently plugged in. If it's not plugged in,
echo no action is needed.
echo.
pause
powershell -NoProfile -Command "$ports = @([System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object -Unique); [System.IO.File]::WriteAllLines('%BEFORE%', $ports)"
if not exist "%BEFORE%" (
echo Failed to enumerate COM ports. PowerShell may be restricted on this system.
set EXIT_CODE=1
goto :end
)
echo.
echo Currently visible COM ports:
set _ANY=
for /f "usebackq delims=" %%L in ("%BEFORE%") do (
echo %%L
set _ANY=1
)
if not defined _ANY echo (none)
echo.
:: ---- Step 2: ask user to plug in + enter download mode ----
:reconnect_step
echo -----------------------------------------------
echo STEP 2 of 4: Plug in and enter download mode
echo -----------------------------------------------
echo.
if defined LITE (
echo Plug the ATOM S3 Lite in, then press AND HOLD
echo the small side button until the green light
echo flashes. That puts it into download mode.
echo.
echo ^(The Lite's main RGB LED stays dark in download
echo mode - the green flash is the small internal
echo light next to the USB port.^)
) else (
echo Plug the ATOM S3 in, then press AND HOLD the
echo small side button until the internal green
echo light flashes. That puts it into download mode.
)
echo.
echo (If you release too early it'll boot normally.
echo Just unplug, replug, and try again.)
echo.
pause
powershell -NoProfile -Command "$ports = @([System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object -Unique); [System.IO.File]::WriteAllLines('%AFTER%', $ports)"
if not exist "%AFTER%" (
echo Failed to enumerate COM ports.
set EXIT_CODE=1
goto :end
)
:: ---- Step 3: diff (ports in AFTER not in BEFORE) -> %DIFF% ----
powershell -NoProfile -Command "$b = @(Get-Content '%BEFORE%' -ErrorAction SilentlyContinue); $a = @(Get-Content '%AFTER%' -ErrorAction SilentlyContinue); $new = @($a | Where-Object { $b -notcontains $_ }); [System.IO.File]::WriteAllLines('%DIFF%', $new)"
set NEW_COUNT=0
set PORT=
for /f "usebackq delims=" %%P in ("%DIFF%") do (
set /a NEW_COUNT+=1
if not defined PORT set PORT=%%P
)
if !NEW_COUNT! GTR 1 (
echo.
echo More than one new COM port appeared:
for /f "usebackq delims=" %%L in ("%DIFF%") do echo %%L
echo.
echo Cannot determine which is the ATOM S3. Unplug
echo any other USB-serial devices and try again, or
echo run "upload.bat COMx" with the port specified.
set EXIT_CODE=1
goto :end
)
if !NEW_COUNT! EQU 0 (
echo.
echo No new COM port detected.
echo Most likely you didn't hold the side button long
echo enough, or didn't reconnect the device.
echo.
set RETRY=
set /p RETRY= Try step 2 again? [Y/N]:
if /i "!RETRY!"=="Y" goto :reconnect_step
echo Aborted.
set EXIT_CODE=1
goto :end
)
:: ---- Step 4: confirm + countdown ----
echo.
echo -----------------------------------------------
echo STEP 3 of 4: Confirm
echo -----------------------------------------------
echo.
echo Detected ATOM S3 in download mode on !PORT!.
echo.
set CONFIRM=
set /p CONFIRM= Flash this device? [Y/N]:
if /i not "!CONFIRM!"=="Y" (
echo Aborted.
set EXIT_CODE=1
goto :end
)
echo.
echo -----------------------------------------------
echo STEP 4 of 4: Flashing in 5 seconds...
echo -----------------------------------------------
echo Press Ctrl+C now to abort.
echo.
for /l %%i in (5,-1,1) do (
echo %%i...
ping -n 2 127.0.0.1 >nul
)
echo.
:compile
echo ===============================================
echo Flashing MacroPad firmware
echo Port: %PORT%
echo FQBN: %FQBN%
echo ===============================================
echo.
echo -----------------------------------------------
echo Step 1 of 3: Compiling firmware
echo -----------------------------------------------
arduino-cli compile --fqbn "%FQBN%" "%SKETCH%"
set RESULT=%ERRORLEVEL%
if not %RESULT%==0 (
echo.
echo COMPILE FAILED - cannot upload.
set EXIT_CODE=%RESULT%
goto :end
)
echo.
echo Compile OK
echo.
echo -----------------------------------------------
echo Step 2 of 3: Wiping device flash
echo -----------------------------------------------
echo Full chip erase - clears firmware, NVS, and
echo LittleFS so the device boots up clean.
echo.
"%ESPTOOL%" --chip esp32s3 --port %PORT% erase_flash
set RESULT=%ERRORLEVEL%
if not %RESULT%==0 (
echo.
echo ERASE FAILED.
echo.
echo If the device dropped out of download mode, hold the
echo side button while plugging it in, then re-run.
set EXIT_CODE=%RESULT%
goto :end
)
echo.
echo Wipe OK - device flash is now empty
echo.
echo -----------------------------------------------
echo Step 3 of 3: Uploading new firmware
echo -----------------------------------------------
arduino-cli upload -v --fqbn "%FQBN%" --port %PORT% "%SKETCH%"
set RESULT=%ERRORLEVEL%
echo.
if %RESULT%==0 (
echo ===============================================
echo UPLOAD SUCCESSFUL
echo ===============================================
echo The ATOM S3 will reboot into the new firmware.
echo The COM port number may change after the reboot.
) else (
echo ===============================================
echo UPLOAD FAILED (exit code %RESULT%)
echo ===============================================
)
set EXIT_CODE=%RESULT%
:: ---- Post-flash: sync the freshly-generated BLE key ----
:: A full chip erase wipes LittleFS, so the device makes a NEW per-device
:: encryption key on first boot. Pull it over USB now so BLE recording /
:: variable sync work immediately without a manual profile upload. This
:: never changes the flasher's exit code — a sync miss isn't a flash
:: failure (the script prints its own guidance and the app re-syncs on the
:: next profile upload anyway).
if %RESULT%==0 (
echo.
echo -----------------------------------------------
echo Syncing BLE encryption key
echo -----------------------------------------------
python "%~dp0sync_key.py"
)
:end
echo.
pause
exit /b %EXIT_CODE%
+7
View File
@@ -0,0 +1,7 @@
@echo off
setlocal
cd /d "%~dp0.."
python wipe_device.py %1
echo.
pause