This tutorial flashes a Meshtastic node — a Heltec WiFi LoRa 32 V3 in my case — puts it on a Wi-Fi network, and manages it from Linux with the meshtastic and esptool CLIs. Both run through uvx, so nothing is installed globally; uv downloads and caches them on first use.

There are two ways to run the node:

Wi-Fi (the main path here) Bluetooth + phone (see Section 10)
Best for A fixed node at home: base station, rooftop, gateway A portable node you carry, controlled from the phone app
How you reach it --host <ip> from any computer on the LAN, web client, phone app over the network Meshtastic app over Bluetooth
Internet / public map Direct, no phone needed Through the phone app, only while it is connected
Location Fixed position Phone’s GPS

On ESP32 boards, Meshtastic cannot run Wi-Fi and Bluetooth at the same time. Turning Wi-Fi on disables Bluetooth.

/dev/ttyUSB0 and 192.168.1.50 below are placeholders for your serial port (Section 2) and your node’s IP (Section 6.2).


1. Prerequisites

  • uv, which provides uvx, plus a few tools (Debian/Ubuntu):

    sudo apt install curl jq unzip binutils
    
  • Your user in the dialout group, so you can open serial ports:

    id | grep dialout || sudo usermod -aG dialout "$USER"   # then log out and in
    
  • Chrome or Chromium for the web flasher (it needs Web Serial), unless you flash from the command line.

  • A data USB cable. A charge-only cable powers the board, but no serial port appears.

  • A 2.4 GHz Wi-Fi network. ESP32 boards cannot join 5 GHz networks.

  • An antenna attached before the radio transmits. Transmitting without one can damage the radio.


2. Find the Serial Port

USB is only needed for flashing and the first setup; after that, everything happens over Wi-Fi.

lsusb                        # what is plugged in
ls -l /dev/serial/by-id/     # stable name -> ttyUSBx mapping
sudo dmesg -w                # watch while plugging in ("attached to ttyUSB0")

The port can change (ttyUSB0 ↔ ttyUSB1) after a flash or a replug. The /dev/serial/by-id/... path stays the same, so you can pass that to --port instead.


3. Identify the Board

Boards from the same maker often share a processor and USB chip but differ in what is wired around them. Firmware for the wrong one can boot and look healthy while the screen or radio stays dead, so pin down the target first.

If Meshtastic is already running, ask it:

uvx --from 'meshtastic[cli]' meshtastic --port /dev/ttyUSB0 --info \
    | grep -E 'pioEnv|hwModel'

Otherwise, work it out from the hardware. The USB chip, from lsusb:

ID USB chip Typical boards
10c4:ea60 CP210x Heltec V3, T-Beam, older LilyGo
1a86:55d4 / 7523 CH9102 / CH340 T-Beam (newer), LilyGo T3
303a:xxxx Native ESP32-S3 USB Heltec V4, T3-S3, T-Deck, Wireless Tracker
239a:xxxx Native nRF52 USB RAK4631, T-Echo (no Wi-Fi, so Bluetooth only)

The processor, flash and MAC, from esptool:

uvx esptool --port /dev/ttyUSB0 chip-id    # processor model and features
uvx esptool --port /dev/ttyUSB0 flash-id   # flash size
uvx esptool --port /dev/ttyUSB0 read-mac   # MAC; last 4 bytes = node ID

Together they narrow it down:

Clues Board Flasher target
CP210x + ESP32-S3 + 8MB + screen Heltec WiFi LoRa 32 V3 Heltec V3
CP210x/CH9102 + ESP32 + GPS + 18650 holder T-Beam T-Beam
Native USB + ESP32-S3 + 16MB + screen Heltec V4 Heltec V4
Native USB + ESP32-S3 + SD slot LilyGo T3-S3 T-LoRa T3-S3

Boards with the same chips look identical to these tools, so read the silkscreen on the board to settle it. If the board still has its factory program, that often names the board too:

uvx esptool --port /dev/ttyUSB0 read-flash 0x10000 0x100000 app.bin  # first 1 MB
strings -n 5 app.bin \
    | grep -iE 'heltec|lilygo|t-beam|wireless|lora32|meshtastic' | sort -u

Worked Example: A Board Fresh from the Factory

  1. meshtastic --info timed out and the serial port printed nothing, so it probably was not running Meshtastic.

  2. lsusb showed 10c4:ea60 Silicon Labs CP210x UART Bridge (unplug and rerun to see which line is the board), and /dev/serial/by-id/ mapped it to ttyUSB0.

  3. esptool flash-id reported:

    Chip type:          ESP32-S3 (QFN56) (revision v0.2)
    MAC:                d8:85:ac:11:22:33
    Detected flash size: 8MB
    

    CP210x + ESP32-S3 + 8 MB points at Heltec V3, but only as a deduction.

  4. The strings search found heltec_test and HELTEC_WIFI_LORA_32_V3: Heltec’s factory test firmware for the V3.

  5. The silkscreen reads WiFi LoRa 32 V3, and the board has a screen.

So the flasher target is Heltec V3, and the node ID will be the last 4 bytes of the MAC: !ac112233.


4. Flash the Firmware

4.1 Pick the Version

Use the newest Beta release; that is Meshtastic’s stable channel. Alpha is for testing and Revoked releases were pulled for bugs. List recent releases:

curl -fsS "https://api.github.com/repos/meshtastic/firmware/releases?per_page=10" \
    | jq -r '.[] | "\(.tag_name)\t\(.name)\t\(.published_at[0:10])"'

On 2026-10-03 that gave:

v2.8.1.8e6a88d   Meshtastic Firmware 2.8.1.8e6a88d Alpha              2026-10-01
v2.8.0.47db0e3   Meshtastic Firmware 2.8.0.47db0e3 Alpha (Revoked)    2026-09-01
v2.8.0.7239fe8   Meshtastic Firmware 2.8.0.7239fe8 (Revoked)          2026-08-30
v2.7.26.54e0d8d  Meshtastic Firmware 2.7.26.54e0d8d Beta              2026-06-24
v2.7.25.104df5f  Meshtastic Firmware 2.7.25.104df5f Alpha             2026-06-10

So 2.7.26.54e0d8d, even though 2.8.1 is four months newer.

4.2 Web Flasher

Close anything else using the port (other flasher tabs, the web client, a meshtastic session), open https://flash.meshtastic.org, and choose:

  1. Device: Heltec V3.
  2. Firmware: 2.7.26.54e0d8d (Beta).
  3. Flash → Full Erase.

Full Erase also wipes settings and keys. That is fine for a factory board; for a configured node, back it up first with uvx esptool --port /dev/ttyUSB0 read-flash 0 ALL backup.bin.

4.3 Alternative: Command Line

Download the ESP32-S3 bundle (about 200 MB, every ESP32-S3 board) and extract only the Heltec V3 files and the install scripts:

VER=2.7.26.54e0d8d
mkdir -p /tmp/meshtastic-fw && cd /tmp/meshtastic-fw
curl -LO "https://github.com/meshtastic/firmware/releases/download/v$VER/firmware-esp32s3-$VER.zip"
unzip -o -j firmware-esp32s3-$VER.zip \
    "*heltec-v3-$VER*" "*device-install.sh" "*device-update.sh" "*mt-esp32s3-ota.bin" -d heltec-v3
cd heltec-v3 && ls

The names should say heltec-v3, not heltec-wsl-v3.

The scripts call esptool by name, so run them through uvx --from esptool bash, which puts esptool on PATH (uv’s warning that bash is not part of esptool is harmless). Always pass -p, or esptool tries every serial port:

# Full install: erases everything, including settings and keys
uvx --from esptool bash ./device-install.sh -p /dev/ttyUSB0 -f firmware-heltec-v3-$VER.factory.bin

# Update: keeps settings, for a board already running Meshtastic
uvx --from esptool bash ./device-update.sh -p /dev/ttyUSB0 -f firmware-heltec-v3-$VER.bin

Use the scripts rather than a bare esptool write-flash: they also write the filesystem partition (littlefs-*.bin).

4.4 Check It Worked

Press RST, then:

uvx --from 'meshtastic[cli]' meshtastic --port /dev/ttyUSB0 --info \
    | grep -oE '"firmwareVersion": "[^"]+"|"pioEnv": "[^"]+"'

You should see "pioEnv": "heltec-v3" and "firmwareVersion": "2.7.26...". Any other pioEnv means the wrong target was flashed; reflash. If flashing failed, see Section 11.


5. Basic Configuration (over USB)

Most --set commands reboot the node, so wait a few seconds between commands.

Region (required). The radio will not transmit while it is UNSET. Use the code for where you operate — US, EU_868 (also UK), ANZ, IN, JP, … — see the region guide:

uvx --from 'meshtastic[cli]' meshtastic --port /dev/ttyUSB0 --set lora.region US

Name. Every node broadcasts two names along with its node ID:

Example Where it shows Limit
Long name (--set-owner) Cedar Base Station Node lists, message headers, the public map About 39 characters
Short name (--set-owner-short) CDR1 Map icons, message bubbles, small screens 4 characters (or one emoji)

Out of the box they come from the last 4 hex digits of the node ID, for example Meshtastic 2233 and 2233. Names are only labels: they don’t have to be unique, and the node ID (!ac112233) is what actually identifies the node. Everyone on the mesh and the public map can see them, so avoid your full name or address.

uvx --from 'meshtastic[cli]' meshtastic --port /dev/ttyUSB0 \
    --set-owner "Cedar Base Station" --set-owner-short "CDR1"

Check the result; the Owner: line shows both names:

uvx --from 'meshtastic[cli]' meshtastic --port /dev/ttyUSB0 --info | head -5

6. Put the Node on Wi-Fi

6.1 Join Your Network

Check that the network is on 2.4 GHz:

nmcli device wifi list

In the CHAN column, channels 1–14 are 2.4 GHz and 36+ are 5 GHz. A dual-band router may list the same name twice; that is fine as long as one entry is on channel 1–14. If not, enable the router’s 2.4 GHz band first.

Then send the credentials:

uvx --from 'meshtastic[cli]' meshtastic --port /dev/ttyUSB0 \
    --set network.wifi_enabled true \
    --set network.wifi_ssid "YourNetworkName" \
    --set network.wifi_psk "YourWifiPassword"

Double quotes preserve spaces; use single quotes if the name or password contains $, backticks or !.

The node reboots and joins the network. Bluetooth is now off. Watch the logs for a WiFi connected line with the IP (Ctrl-C to stop):

uvx --from 'meshtastic[cli]' meshtastic --port /dev/ttyUSB0 --noproto

6.2 Find the Node’s IP Address

Screen    press the button to cycle screens; the Wi-Fi screen shows the IP
Logs      the --noproto output from 6.1
Router    its list of connected devices, for example Meshtastic_2233
mDNS      avahi-browse -rt _meshtastic._tcp

Set up a DHCP reservation in your router so the IP does not change (or set a static IP on the node with network.address_mode STATIC and network.ipv4_config.*).

6.3 Unplug USB and Talk to It over Wi-Fi

From now on, use --host <ip> in place of --port /dev/ttyUSB0; the node only needs power. An alias saves typing, and the rest of the tutorial uses it:

alias mesh="uvx --from 'meshtastic[cli]' meshtastic --host 192.168.1.50"

mesh --info
mesh --nodes
mesh --sendtext "hello mesh"

7. Other Ways to Use the Node over Wi-Fi

The web client, hosted locally. The Meshtastic web client is a static site, so download a release build and serve it yourself:

mkdir -p /tmp/meshtastic-web && cd /tmp/meshtastic-web && {
  [ -n "$(find . -name index.html)" ] || {
    curl -fL https://github.com/meshtastic/web/releases/latest/download/build.tar | tar x &&
      find . -name '*.gz' -exec gunzip -f {} +
  }
} && python3 -m http.server 8080 -d "$(dirname "$(find . -name index.html | head -1)")"

The release files are gzipped, so they get unpacked after extraction. The download only runs when there is no index.html yet, so rerunning just starts the server. /tmp is cleared on reboot; use another directory to keep it.

Open http://localhost:8080, add an HTTP connection to 192.168.1.50, and leave Use HTTPS off. Because the client is served over plain HTTP, the browser lets it reach the node over plain HTTP too, with no certificate to accept. Stop the server with Ctrl+C.

The same client is hosted at https://client.meshtastic.org, but as an HTTPS page it can only reach the node over HTTPS: first open https://192.168.1.50 in a new tab and accept the node’s self-signed certificate. See the web client documentation.

The phone app over the network. On the same Wi-Fi, add a network/TCP device with the node’s IP instead of pairing over Bluetooth.


8. Set the Location

A home node usually has no GPS, so give it a fixed position:

mesh --setlat 42.0000 --setlon -71.0000 --setalt 15
mesh --remove-position     # undo it later

Script: Look Up an Address and Set It as the Position

This looks the address up with OpenStreetMap Nominatim (so the address is sent to their servers), shows the result, and asks before setting it. The second argument is an IP/hostname or a /dev/... port; if omitted, the script requires exactly one serial port, to avoid configuring the wrong device.

cat > /tmp/mesh-setloc.sh <<'EOF'
#!/usr/bin/env bash
# Look up an address and optionally set it as the Meshtastic fixed position.
# Usage: /tmp/mesh-setloc.sh ["address"] [ip-or-hostname | /dev/serial-port]
set -euo pipefail
addr="${1:-}"
target="${2:-}"
if [[ -z "$target" ]]; then
  shopt -s nullglob
  ports=(/dev/ttyUSB* /dev/ttyACM*)
  if (( ${#ports[@]} != 1 )); then
    echo "Specify a node IP/hostname or serial port (found ${#ports[@]} serial ports)." >&2
    exit 1
  fi
  target="${ports[0]}"
fi
[[ -z "$addr" ]] && read -rp "Address: " addr
[[ -z "$addr" ]] && { echo "No address given." >&2; exit 1; }
result=$(curl --fail --show-error --silent --connect-timeout 10 --max-time 30 -G "https://nominatim.openstreetmap.org/search" \
  --data-urlencode "q=$addr" -d format=json -d limit=1 \
  -H "User-Agent: meshtastic-setloc-script")
jq -e 'type == "array"' <<<"$result" >/dev/null
[[ "$(jq 'length' <<<"$result")" -eq 0 ]] && { echo "Address not found." >&2; exit 1; }
lat=$(jq -r '.[0].lat | tonumber | . * 10000 | round / 10000' <<<"$result")
lon=$(jq -r '.[0].lon | tonumber | . * 10000 | round / 10000' <<<"$result")
echo "Found:    $(jq -r '.[0].display_name' <<<"$result")"
echo "Lat/Lon:  $lat, $lon"
[[ -z "$target" ]] && { echo "No Meshtastic node given or found." >&2; exit 1; }
if [[ "$target" == /dev/* ]]; then conn=(--port "$target"); else conn=(--host "$target"); fi
read -rp "Set this position on Meshtastic at $target? [y/N] " ans
if [[ "$ans" =~ ^[Yy]$ ]]; then
  uvx --from 'meshtastic[cli]' meshtastic "${conn[@]}" --setlat "$lat" --setlon "$lon"
else
  echo "Not set."
fi
EOF
chmod +x /tmp/mesh-setloc.sh

/tmp/mesh-setloc.sh "1 Main St, Somewhere" 192.168.1.50    # over Wi-Fi
/tmp/mesh-setloc.sh "1 Main St, Somewhere" /dev/ttyUSB0    # over USB
/tmp/mesh-setloc.sh                                        # prompts for the address

On privacy. Positions shared on a public channel are visible to other users. Four decimal places is roughly 11 m. Radio position precision is set per channel; MQTT map reports have their own precision setting and are sent unencrypted, so review both before sharing a home location.


9. Appear on the Public Map

Public maps are built from MQTT, which bridges the mesh to the internet. On Wi-Fi the node connects to MQTT directly, without a phone (proxy_to_client_enabled false):

mesh --set mqtt.enabled true \
     --set mqtt.proxy_to_client_enabled false \
     --set mqtt.map_reporting_enabled true \
     --set lora.config_ok_to_mqtt true

Map reports go out hourly at most, so allow an hour or more, plus time for the map to ingest it. The node needs a valid position and the broker and topic the map expects; see the MQTT documentation. To check, look the node up by its decimal number (!ac112233 → 2886804019):

printf '%d\n' 0xac112233
curl -s https://meshmap.net/nodes.json | jq '."2886804019" // "not on map yet"'

10. Use Case: Follow Along on Your Phone (Bluetooth)

Use this instead of Wi-Fi for a portable node you carry: the phone app is the screen and keyboard, and the phone’s GPS gives the location. The computer is only needed once, to turn Wi-Fi off.

10.1 Switch the Node to Bluetooth Mode

mesh --set network.wifi_enabled false      # or --port /dev/ttyUSB0 if not on Wi-Fi

After the reboot, Bluetooth is available if bluetooth.enabled is true. If not, enable it over USB with --set bluetooth.enabled true.

10.2 Pair the Phone

  1. Install the Meshtastic app and turn on Bluetooth. Android also needs the location or nearby-devices permission.
  2. In the app, tap + (Android) or open Connect (iOS) and choose your node. Pair from inside the app, not from the phone’s Bluetooth settings.
  3. Enter the PIN shown on the node’s screen, or 123456 if it has no screen.

“Incorrect PIN” usually means an old pairing is in the way: Forget the device in the phone’s Bluetooth settings, reset the board, and pair again.

10.3 Do the Setup in the App

Menu names vary by platform and version. If you set a fixed position earlier, remove it before using the phone’s GPS.

Task In the app
Region (required) Settings → LoRa → Region
Name Settings → User → Long name / Short name (4 characters max)
Bluetooth PIN Settings → Bluetooth → Pairing mode (Random PIN / Fixed PIN / No PIN)
Location Settings → Position → Provide phone location to mesh
Public map Settings → MQTT → Enabled + Proxy to client + Map reporting; Settings → LoRa → OK to MQTT
Messages Messages tab; the primary channel (LongFast) is shared with the public mesh
Map of nearby nodes Map tab

With Bluetooth, the node only reaches the internet and the public map while the app is connected.

10.4 The Same Settings from the CLI

Choose one pairing mode (see the Bluetooth documentation), then let the phone relay MQTT:

meshusb() { uvx --from 'meshtastic[cli]' meshtastic --port /dev/ttyUSB0 "$@"; }

meshusb --set bluetooth.mode RANDOM_PIN                                   # PIN on screen
meshusb --set bluetooth.mode FIXED_PIN --set bluetooth.fixed_pin 654321   # fixed PIN
meshusb --set bluetooth.mode NO_PIN                                       # anyone nearby can connect

meshusb --get bluetooth
meshusb --set mqtt.proxy_to_client_enabled true

11. Troubleshooting

Flashing fails (“Failed to connect”, “Port busy”, “Device Connection Failed … may already be in DFU mode”):

  • Find what holds the port with fuser -v /dev/ttyUSB0 (or lsof) and close it. Often it is the browser from an earlier attempt: quit it fully and replug.
  • Stop ModemManager while flashing, since it probes new serial ports: sudo systemctl stop ModemManager (start it again afterwards).
  • Force bootloader mode: hold BOOT, tap RST, release BOOT, then retry.
  • The port name may have changed after flashing: check /dev/serial/by-id/.

The board is silent over USB (“Connection timed out”, no logs):

  1. Check what holds the port: fuser -v /dev/ttyUSB0.
  2. Press RST once without BOOT; a reset with BOOT held leaves it in the silent bootloader.
  3. Try uvx esptool --port /dev/ttyUSB0 chip-id. If it answers, USB and the ROM bootloader work (that says nothing about the firmware). Press RST and retry Meshtastic before reflashing.

The node will not join Wi-Fi:

  • Check the network is 2.4 GHz (Section 6.1) and the name and password are right; both are case-sensitive.
  • Watch the logs: uvx --from 'meshtastic[cli]' meshtastic --port /dev/ttyUSB0 --noproto.
  • Check the stored settings: ... --port /dev/ttyUSB0 --get network.
  • To get back to Bluetooth, run --set network.wifi_enabled false over USB.

--host times out:

  • Make sure the computer is on the same network, and ping 192.168.1.50.
  • The IP may have changed: find it again (Section 6.2) and add a DHCP reservation.
  • Guest networks and “client isolation” stop devices talking to each other.

esptool v5 uses hyphenated commands (flash-id); the old underscore forms still work but print a warning.


12. Additional Resources


Summary

esptool       identifies the chip, flash and MAC
flasher       writes the firmware for the right board target
meshtastic    configures region, name, Wi-Fi, position and MQTT
--port        over USB, for flashing and the first setup
--host        over Wi-Fi, for everything after that

The only awkward part is the start: picking the right firmware target for a board whose chips look identical to a neighbour’s. After that, the USB cable comes out and the node is just another host on the LAN.