Initial public release
This commit is contained in:
@@ -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.
|
||||
@@ -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))
|
||||
@@ -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%
|
||||
@@ -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))
|
||||
@@ -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%
|
||||
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
cd /d "%~dp0.."
|
||||
python wipe_device.py %1
|
||||
echo.
|
||||
pause
|
||||
Reference in New Issue
Block a user