Skip to content

About

Linux USB firmware updater for meshcore & meshtastic devices

Resources

Stars

61 stars

Watchers

7 watching

Forks

Repository files navigation

Alt CLI firmware selector for MeshCore & Meshtastic projects

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.

Windows Quick start - MeshCore & Meshtastic in one script

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.ps1

MeshCore backup before flashing

Both 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.

Restore a MeshCore companion backup

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.

Check ESP32 partitions before an update

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.bin

With 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-only

This 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.

MeshCore USB backup dependencies

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.

Windows Video

2025-05-23.23-47-50.mp4

Linux

MeshCore Quick start

Copy and run this in your linux terminal

cd ~ && git clone https://github.com/mikecarper/meshfirmware.git && cd meshfirmware && ./mcfirmware.sh
Readable Code
cd ~
git clone https://github.com/mikecarper/meshfirmware.git
cd meshfirmware
./mcfirmware.sh

Raspberry Pi USB host warning

mcfirmware.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.

Recover a stalled USB connection (Linux)

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.

Button-free ESP32-S3 handoff from Meshtastic/TinyUSB

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.

Linux nRF52 USB bootloader entry

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.sh

nRF52 RAK board safety check

mcfirmware.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.sh

This 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.

Linux Video

2026-07-09.11-05-48-no-audio.mp4

Meshtastic Quick start

Copy and run this in your linux terminal

cd ~ && git clone https://github.com/mikecarper/meshfirmware.git && cd meshfirmware && ./mtfirmware.sh
Readable Code
cd ~
git clone https://github.com/mikecarper/meshfirmware.git
cd meshfirmware
./mtfirmware.sh

Linux Video

output_10mb2.mp4

Linux Compile the firmware

Copy and run this in your linux terminal

cd ~ && git clone https://github.com/mikecarper/meshfirmware.git && cd meshfirmware && chmod +x mtcompile.sh && ./mtcompile.sh
Readable Code
cd ~
git clone https://github.com/mikecarper/meshfirmware.git
cd meshfirmware
chmod +x mtcompile.sh
./mtcompile.sh

Linux Video

final_output.mp4

Overview

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.

Raspberry Pi USB speed recommendation

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:

  1. Favor USB 1.1: only nodes, node bootloaders, or serial adapters.
  2. 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.
  3. 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.

Usage

Run the script with the following syntax:

./mtfirmware.sh [OPTIONS]   

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.

About

Linux USB firmware updater for meshcore & meshtastic devices

Resources

Stars

61 stars

Watchers

7 watching

Forks

Releases

Packages

Contributors

Languages