Scripts that automates the process of selecting, downloading, and applying firmware updates from the meshcore-dev/MeshCore & meshtastic/firmware GitHub repository via the USB port.
Download firmware.cmd (right click save)
Make sure file is named firmware.cmd and not firmware.cmd.txt
double click and run the file firmware.cmd
On an existing installation, startup checks the local tools without waiting
for package upgrades. After device detection, upgrades run in the background
while you choose firmware. Backups, flashing, and reconnect probes wait for
the updater if it is still running. Missing tools are installed before first
use. Updater diagnostics are saved in .tmp/firmware-tool-update.<pid>.log;
failed upgrades are reported and available installed tools can still be used.
Firmware windows share a lock to keep upgrades from changing tools during
another window's device operation.
USB identification uses short, read-only MeshCore/Meshtastic protocol queries instead of starting the full Meshtastic CLI for every port. Independent radios are scanned in parallel (up to eight at a time); interfaces on the same radio are kept sequential. Confirmed DFU devices and MeshCore logging-only siblings skip application queries. USB descriptions and the last successful protocol only choose query order, never prove firmware type. A silent device remains Unknown; selecting it runs the slower compatibility check for that port only. USB enumeration is reused and Windows identity properties are read in batches without loading the slower PnP module on the normal path.
The Windows flasher recognizes the serial-only Seeed XIAO/XIAO Sense DFU
products (2886:0044 / 2886:0045) as well as T1000-E (2886:0057), while
still rejecting their application-mode IDs and rechecking the selected USB
identity after COM-port changes. It selects esptool 4/5 command spellings
automatically. Native stderr warnings do not abort successful operations in
Windows PowerShell 5; a nonzero tool exit still stops the flash.
Windows nRF52 bootloader entry attempts its serial touch once. An Open/close error can mean the radio has already started resetting, so it waits for the same radio's verified DFU port without sending a second Python touch. It keeps RTS deasserted, explicitly lowers DTR after the hold and identity check, and stops if serial-handle cleanup cannot be confirmed. A successful nRF52 transfer leaves application startup unverified in the final message.
Normal MeshCore identification leaves DTR/RTS deasserted on native ESP32 USB-Serial/JTAG devices (including Heltec V4), whose hardware interprets those signals as reset/download controls. This prevents the probe from accidentally entering download mode. nRF52 CDC and USB-UART probing retain their existing control-line behavior; intentional flashing resets are unchanged.
Regression checks (no connected radio needed):
powershell -NoProfile -ExecutionPolicy Bypass -File tests/test_firmware_usb_identity.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/test_firmware_native_commands.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/test_firmware_backup_dependencies.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/test_firmware_backup_prompt_order.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/test_firmware_background_updates.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/test_firmware_protocol_probe.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/test_firmware_parallel_probe.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/test_firmware_protocol_inventory.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/test_firmware_usb_properties.ps1Both firmware.cmd (Windows) and mcfirmware.sh (Linux) offer a verified
logical USB backup for nRF52 and ESP32 before the final flash confirmation.
For nRF52, the backup also precedes the update/wipe action menu. You can make
a backup and then decline flashing (Windows) or choose echo-only (Linux);
the backup is retained and no erase/write is performed.
The current firmware must expose a supported MeshCore USB API; a device
already in ROM/DFU mode cannot provide a logical backup. Failed backups
require explicit confirmation before updating, and a wipe without a complete
wipe-safe backup still requires typing WIPE WITHOUT BACKUP. These are
logical snapshots, not full flash images, and may contain private identity
and channel secrets. Windows saves new backups at the checkout root as
mc.config_backup.*.json (MeshCore) or mt.config_backup.*.yaml
(Meshtastic); Linux keeps its existing backup location. Older MeshCore JSON
backups in the current user's .meshfirmware/backups directory also remain
usable.
New companion archives keep Bluetooth name as name plus a boolean
default, rather than embedding "(default from node name)" in a text value.
They also retain device_info.ver_full from the read-only version command;
the protocol's original 20-byte ver field remains unchanged. The restore
tool accepts older text-format archives too.
On Windows, double-click restore.cmd. It offers the newest JSON backup or
lets you enter a full path, checks archive integrity, identifies the USB
device, and asks before changing any settings. To move the backup to a
different board, choose different hardware, enter its displayed USB
serial, and explicitly approve identity replacement. The target must already
run MeshCore companion firmware and have room for the saved populated
channels and contacts. The new board's existing identity and matching
settings will be overwritten; its unrelated contacts are not deleted. After
restore, Bluetooth is cycled once if it was enabled, so the phone app can
reconnect and reload settings.
For Linux, use python3 tools/meshcore_restore.py ports to see available
devices, then probe --input BACKUP.json --target-serial SERIAL --new-hardware
and finally restore --input BACKUP.json --target-serial SERIAL --new-hardware --confirmed --replace-identity. Omit the target-serial and new-hardware
options to restore to the original USB device. Run resync with the same
target options and --confirmed to cycle enabled Bluetooth after using the
Python helper directly. This is a logical settings
restore, not a firmware, unread-message, or every optional CLI-only setting restore.
An unresponsive device in DFU or a non-companion role is never written.
If a Full Companion is in its text terminal with USB logging on, the Windows
launcher asks before temporarily switching that setting off to enter binary
mode. A successful restore leaves USB logging at the state recorded in the
backup. Use --allow-mode-switch for this behavior with the Python helper.
Linux regression check: bash tests/test_mcfirmware_backup_integration.sh.
On a running MeshCore Station G2, open its USB serial console at 115200 baud
and enter get storage.layout. This is read-only and reports flash size,
the active app slot (*), both app slot offsets and sizes, NVS, and the
filesystem. For example, Bellevue's expanded 16 MB layout reports
app0@0x10000+6400K,app1@0x650000+6400K. An app-only .bin must fit in
the smallest app slot; use the partition migration procedure only if the
existing slots cannot hold it. A merged .bin replaces the partition table
and is a new install, not an in-place app update.
If the application does not answer, put the ESP32 into its ROM download mode and read the physical table without erasing or writing:
esptool --chip esp32s3 --port /dev/serial/by-id/YOUR_DEVICE \
read-flash 0x8000 0x1000 partitions.binWith esptool 4, use esptool.py and read_flash instead. On native USB,
the ROM device may enumerate with a different by-id name; verify its MAC
address before using that port.
Both flashers print get storage.layout when available, then verify the actual
ROM-readable partition table, physical flash capacity, OTA selection and NVS
migration state before a confirmed MeshCore app-only update. A silent serial
console is not evidence that expansion is necessary. An image that already
fits uses the normal checked, inactive-first/active-last write path.
When expansion is necessary, Windows and Linux select the exact board/role migration package matching the selected Full application's release and SHA-256. They offer its verified USB migration bridge as a middle step, without a full-chip erase. The bridge stages private identity and supported configuration, expands partitions, reboots and verifies restoration. Native ESP32-S3/C3 USB returns itself to ROM download mode; classic ESP32 reports settled status over its USB-UART adapter before the host requests ROM mode. No physical BOOT-button step is used. Native USB can disappear temporarily during this process.
The final image is written only after the original USB identity/chip MAC, exact expanded table and NVS restoration records verify. Interrupted runs keep a public per-device recovery record and cannot bypass unfinished restoration by selecting another image or moving the device to another host. Temporary NVS snapshots are removed after use. Logs and unsupported optional files are not preserved; keep a logical backup and stable power. Partition-table replacement is not power-loss atomic and a power failure can still require physical recovery.
If an exact package or USB-safe bridge is unavailable, the update stops with a
reason; it never substitutes a Wi-Fi-only bridge that might require physical
BOOT access. MESHFIRMWARE_PARTITION_PACKAGE can select an exact local migration
ZIP; it is subject to the same checks. Merged images remain a separately
confirmed new install that erases settings. Echo-only mode never runs a bridge.
For existing releases whose exact migration ZIP contains only the Wi-Fi bridge,
the helper automatically downloads a qualified, versioned six-profile USB
utility set. Its release URL and archive SHA-256 are pinned in the helper;
it never follows an untested latest bridge. The download happens only after
the exact Full package qualifies and only when expansion needs that bridge.
Cached ZIPs are checksum-verified again before use.
For offline use or an explicitly selected set, set
MESHFIRMWARE_USB_MIGRATION_SET to that ZIP, or put it at
tools/meshcore-usb-partition-bridges.zip beside the flasher. The set has its own
source provenance and checksums; it does not relabel or replace the selected
Full application's release. Its chip/flash header, NVS startup guard and all six
profile hashes are checked before use.
In the MeshCore checkout, build only this utility set on Linux/WSL with:
python3 scripts/build_esp32_partition_migration.py --usb-onlyThis builds the six intermediate images serially, not every Full application.
After writing, the flashers finish the identity-qualified ESP32 session using the reset method for its USB transport. Native ESP32-S3 USB/JTAG requires the masked forced-download clear and watchdog reset described below; UART bridges keep their ordinary hard reset. A verified flash write or a matching USB descriptor does not confirm application startup. Verify the expected firmware through its application CLI before treating the update as complete.
Before a logical USB backup, both flashers display the selected Python interpreter and the installed, supported, and repair versions of each package:
| Package | Supported stable versions | Pinned repair version |
|---|---|---|
MeshCore Python API (meshcore) |
>=2.3.9,<3 |
2.3.9.1 |
MeshCore CLI (meshcore-cli) |
>=1.6.3,<2 |
1.6.3 |
| PyNaCl | >=1.5,<2 |
1.6.2 |
Python 3.10 or newer is required. Compatible installations are reused after
checking that the USB backup APIs can be imported. Missing, incompatible, or
broken packages trigger one repair attempt using the exact versions above,
followed by a fresh-process API check. Installation failures remain visible
and stop the backup; the existing explicit confirmation before continuing
without a complete backup is unchanged. Linux uses its dedicated backup venv;
Windows uses the Python interpreter selected by firmware.cmd.
To display versions and check APIs without installing anything or connecting
to a radio (use python3 on Linux):
python tools/meshcore_backup.py dependencies --text
Add --install to repair that interpreter's backup dependencies. The Windows
launcher runs this helper as a file, avoiding Windows PowerShell 5's loss of
embedded quotes in inline Python version checks.
2025-05-23.23-47-50.mp4
Copy and run this in your linux terminal
cd ~ && git clone https://github.com/mikecarper/meshfirmware.git && cd meshfirmware && ./mcfirmware.shReadable Code
cd ~
git clone https://github.com/mikecarper/meshfirmware.git
cd meshfirmware
./mcfirmware.shmcfirmware.sh, mcsetup.sh, and mtfirmware.sh check whether they are running
on a Raspberry Pi whose active USB host controller uses the legacy dwc_otg
driver. When that bus is still at its default high speed, the scripts warn that
some hub/radio combinations can repeatedly re-enumerate or lock the host. The
warning specifically identifies a connected Terminus 1a40:0101 hub when
present.
The scripts can add dwc_otg.speed=1 to the Pi's single-line cmdline.txt and
create a one-time .meshfirmware-backup, but only after an explicit Yes.
They ask separately before rebooting. A reboot is required to activate the
setting. The mitigation caps that DWC USB bus at 12 Mbps, so USB Ethernet and
storage become slower; USB radio serial devices commonly already run at 12
Mbps. Set MESHFIRMWARE_PI_USB_CHECK=0 to suppress this startup check.
mcsetup.sh has a U) Reset USB connection action and offers recovery when
the radio does not answer its initial clock query. The MeshCore flasher,
mcfirmware.sh, records the selected identity without resetting it and offers
device-only USB recovery only from its manual post-failure recovery menu.
This is the device-only USB reset used to recover a stalled Station G2 USB
interface. It reconnects USB; it does not reboot the radio CPU, enter the
bootloader, erase flash, change settings, or toggle a GPIO relay. It is
separate from the firmware's reboot command and the flasher's bootloader-entry
sequence. It cannot repair a radio whose firmware itself is hung.
Recovery is optional (default No) and requires Linux and sudo. No healthy
probe triggers it automatically. Close any
serial terminal or stop the service using that radio first; recovery refuses
busy ports rather than stopping programs. The tools save the selected USB
identity before probing, reject hubs or a different/replaced device, and only
continue with a verified returning port. If recovery fails, select the radio
again instead of retrying a possibly reused tty number. The flasher's
MCFIRMWARE_NO_SUDO=1 mode does not perform USB resets.
On Raspberry Pi, the reset helper also resolves the live host-controller
driver from sysfs. It refuses to issue USBDEVFS_RESET through the legacy
dwc_otg driver because cancelling an active USB request can freeze the Pi.
When mcsetup.sh finds an unresponsive radio on that driver, it offers to test
the existing tty with Picocom, reboot the Raspberry Pi after confirmation,
continue without recovery, or quit. Picocom is installed through the detected
package manager when needed and is launched without resetting the serial
control lines on exit. If the tty is unavailable, use the radio's normal
bootloader entry or physically reconnect only that radio, then reselect it.
The script does not offer to replace dwc_otg with dwc2: live testing on a
Zero 2 W with nested hubs found repeated whole-tree disconnects under dwc2.
Before reading the clock and repeater settings, mcsetup.sh checks
get usb.logging. When logging is enabled, it offers to send
set usb.logging off reboot once and then exits cleanly while the radio
reconnects. Continuous debug and packet output can otherwise prevent the
serial reader's idle timeout and make individual setting reads appear stalled.
When logging is off, or an nRF52 exposes its logging stream on the separate
interface 02 tty, setup retains the verified baud and uses a 0.2-second idle
window with a 0.5-second hard deadline for single-value settings. A known noisy command tty is restricted to
the verified baud and a two-second ceiling; multiline commands retain their
separate, longer read behavior.
The shared Python helper is included in a checkout. Single-script downloads
fetch a copy with a checksum pinned by the script; a missing or mismatched
helper disables recovery. This Linux feature does not change mcsetup.cmd
or firmware.cmd on Windows.
Bootloader entry after the user confirms a flash is separate and still uses the mode transition required by that board. Generic ESP32 UART recovery no longer combines a 1200-baud touch with DTR/RTS: after a failed connection it offers one identity-resolved DTR/RTS toggle, default No. Native ESP32 1200-baud fallback is also explicitly confirmed before it can re-enumerate a device.
The MeshCore flashers distinguish software TinyUSB CDC from hardware
USB-Serial/JTAG using the USB product/interface descriptors. Both can use
VID/PID 303a:1001, so those numbers alone do not select a reset method.
For a confirmed TinyUSB application, such as Meshtastic, mcfirmware.sh sends
the CDC 1200-baud line-coding request directly after the flash is confirmed.
It does not open the tty first, so serial-open DTR/RTS handling cannot prevent
that request. The dependency-free Linux helper uses only the selected CDC
interface, checks its physical identity and all sibling tty owners, and
restores any detached driver if no handoff occurs. This control request is not
a whole-device or host USB reset; the dwc_otg reset restriction stays in place.
firmware.cmd uses an identity-guarded 1200-baud serial touch for TinyUSB on
Windows. Hardware USB/JTAG keeps its own reset sequence; external UART bridges
keep the normal UART reset. Unknown native modes stop instead of guessing.
No Windows USB driver replacement is required.
A request acknowledgement or disconnect is not considered a successful flash session. The flasher must find the same physical radio after the handoff and verify its ROM chip MAC before erase/write. Native 12-hex USB serials are also checked against that MAC. Each later destructive phase retains its existing identity gate.
For native ESP32-S3 USB/JTAG, automatic return to the application requires
esptool 5 or newer. The flasher clears only the FORCE_DOWNLOAD_BOOT bit
and uses an RTC watchdog reset, which re-samples the boot strapping pins.
An ordinary USB/JTAG RTS reset can leave the radio in download mode. External
UART bridges keep their normal hard-reset behavior.
The hardware USB/JTAG product name and tty can remain unchanged when an application boots. They identify the transport, not whether ROM or application code is running. A successful final reset ends the verified ROM session; the flasher must not send another blind ROM probe just because an application reply is missing. Confirm application startup through its CLI. This reset path was tested on a Heltec V4 with byte-for-byte partition and both app-slot readbacks, followed by the expected running-image hash through the MeshCore CLI.
Native ESP32-S3 automation adapters that reuse the Linux session functions
should keep writes and readbacks at --after "$NORESET", then call
finish_esp32_flash_session "$DEVICE_PORT" within the already identity-qualified
session. Keep its immediate chip-MAC checks and guarded finish together; do not
replace the native-S3 finish with --after hard-reset run or another ROM probe
after reset.
The short 8-12 second probe/reset timeout is not a bulk-transfer budget. An external adapter should allow at least 600 seconds for a roughly 2.56 MB application readback at 115200 baud, with a larger budget for larger images. The Linux data-read path does not impose the short probe timeout. Finish all readback comparisons before the guarded reset, then verify the running application separately through its CLI.
See Espressif's USB/JTAG reset/strapping explanation.
This TinyUSB behavior comes from Espressif's 1200-baud bootloader hook and ESP32-S3 ROM handoff. It is separate from Meshtastic's nRF52 DFU and firmware OTA commands.
If Linux cannot enumerate the USB device at all (for example descriptor error
-71), there is no CDC interface to send this request to. Changing reset
syntax cannot repair that condition. Re-establish the selected device's USB
connection first; do not reset a whole hub or replace the Pi's USB host driver.
A battery or second supply can keep the radio CPU running even when USB power
is removed. A missing device is never replaced with another cached tty.
mcfirmware.sh keeps the original USB selection while a radio briefly
disconnects or changes tty numbers. It waits up to ten seconds for that same
selection, without choosing another available radio. Before a bootloader
touch, the checksum-pinned helper requires CDC0, a matching physical by-path
alias, and the same USB identity and opened character-device inode before
changing line coding. It uses the existing adafruit-nrfutil Python environment
and sends one 1200-baud/DTR transition, with no application payload or serial
capture. The touch itself is bounded to ten seconds; read-only qualification
can retry a disappearing candidate, but the serial open is attempted once.
Complete serial-owner inspection includes the radio's logging siblings. Normal
mode uses cached sudo only for that bounded, read-only inspection; serial
controls still run as the user. No-sudo mode never elevates and refuses the
touch if it cannot inspect every owner's descriptors. No package installation
or helper download runs inside this privileged inspection.
A disconnect during the touch can leave its delivery uncertain. The script
then checks only that radio for a recognized DFU bootloader, without repeating
the touch. A returning application or an unverified endpoint cannot authorize
DFU. If the selected device does not return, use its physical bootloader entry
and reselect it. MCFIRMWARE_NRF52_SELECTION_WAIT_SECONDS accepts 0-30 seconds;
MCFIRMWARE_SERIAL_TOUCH_TIMEOUT_SECONDS accepts 1-30 seconds.
The DFU manifest summary distinguishes application-only packages from packages containing bootloader or SoftDevice images. An application-only package contains neither bootloader nor SoftDevice replacement; intentional combined packages remain supported. Update/wipe choices and backup defaults are unchanged. Successful DFU output confirms the package transfer. The final message explicitly leaves application startup unverified, so a completed transfer is not mistaken for a working radio.
Offline checks:
python3 -B tests/test_meshcore_serial_touch.py
bash tests/test_mcfirmware_serial_touch.shmcfirmware.sh checks RAK3401 and RAK4631/WisMesh Tag firmware before any
erase or DFU command. It compares identity strings embedded in the firmware
payload with the node's reported board/model and stable USB identity. A
matching pair continues automatically.
A mismatch, ambiguous identity, or unknown RAK payload cancels by default. If
the mismatch is intentional, the script displays a one-time token such as
unknown-to-rak4631 (connected-device-to-firmware-target). Type that exact token at
the prompt to continue. Enter or any other response cancels without erasing or
flashing.
For deliberate unattended use, pass only the exact token printed by the script:
MCFIRMWARE_BOARD_OVERRIDE=unknown-to-rak4631 ./mcfirmware.shThis override is intentionally specific to the detected firmware/device pair;
yes, force, and a token for another pair are rejected.
For nRF52 flash-wipe + flash, a firmware provider or cached catalog may be
missing the board's .erase entry. The script looks up the selected board in
the official MeshCore catalog and refreshes that catalog if needed. It keeps
the selected firmware or bootloader file and the active provider unchanged.
Only an exact, unique board match is selected automatically; otherwise you
must choose an erase package for the physical board or cancel. If no erase
packages are available, the script stops before erase or DFU. The one-time
board override does not bypass this erase-package lookup.
2026-07-09.11-05-48-no-audio.mp4
Copy and run this in your linux terminal
cd ~ && git clone https://github.com/mikecarper/meshfirmware.git && cd meshfirmware && ./mtfirmware.shReadable Code
cd ~
git clone https://github.com/mikecarper/meshfirmware.git
cd meshfirmware
./mtfirmware.shoutput_10mb2.mp4
Copy and run this in your linux terminal
cd ~ && git clone https://github.com/mikecarper/meshfirmware.git && cd meshfirmware && chmod +x mtcompile.sh && ./mtcompile.shReadable Code
cd ~
git clone https://github.com/mikecarper/meshfirmware.git
cd meshfirmware
chmod +x mtcompile.sh
./mtcompile.shfinal_output.mp4
The script does the following:
-
Updates a local cache file with GitHub release data if it is older than 6 hours.
-
Falls back to using the cached data if no internet connection is detected.
-
Parses the JSON release data to build a list of firmware release versions.
-
Appends labels (such as (alpha), (beta), (rc), or (pre-release)) based on the release tag.
-
Prepends the ! label if the release has known issues.
-
Uses lsusb to detect connected USB devices.
-
If more than one matching USB device exists, the user is prompted to choose the correct one.
-
Matches the detected device against available firmware files.
-
If more than one matching firmware file exists, the user is prompted to choose the correct one.
-
For ESP32 devices, the script adjusts the update script (e.g., changes baud rate from 115200 to 1200) as required.
Also allows the user to choose between an update or an install operation -
Stops any systemd service locking the device before proceeding and restarts it afterward.
On Raspberry Pis using the legacy dwc_otg USB host, mcfirmware.sh, mcsetup.sh, and mtfirmware.sh count physical devices on the affected bus (excluding hubs). The recommendation uses a three-tier scale:
- Favor USB 1.1: only nodes, node bootloaders, or serial adapters.
- Middle ground: USB Ethernet/Wi-Fi adapters alone or alongside nodes. USB 1.1 may suit light traffic; USB 2.0 preserves higher throughput. The script does not measure traffic demand, and the 12 Mbps cap is shared across the entire USB bus.
- Favor USB 2.0: storage/SD-card readers or unknown peripherals, even when nodes or networking are also present. An empty bus also defaults to this tier.
Generic serial adapters are labeled as possible nodes because USB descriptors cannot prove which firmware is running. Node DFU/UF2 drives are recognized through bootloader identity or mounted UF2 metadata belonging to that same USB device; ordinary storage is not assumed to be a bootloader just because it also has a serial interface. The check does not mount drives or reset devices.
Tiers 1 and 2 offer the existing opt-in boot setting and reboot prompts, with the network throughput tradeoff stated in the tier-2 prompt. Tier 3 only gives advice, including how to remove an existing 12 Mbps cap. Devices on other USB controllers do not affect the recommendation.
Run the script with the following syntax:
./mtfirmware.sh [OPTIONS] -
--version VERSION
Specify a firmware release version to auto-select (searches for tags containing the provided string). -
--install
Set the operation mode to install (used instead of update). -
--update
Set the operation mode to update (this is the default if not otherwise specified). -
--run
Automatically update firmware without prompting the user. -
-h, --help
Display the help message and exit.