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
dialoutgroup, 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
-
meshtastic --infotimed out and the serial port printed nothing, so it probably was not running Meshtastic. -
lsusbshowed10c4:ea60 Silicon Labs CP210x UART Bridge(unplug and rerun to see which line is the board), and/dev/serial/by-id/mapped it tottyUSB0. -
esptool flash-idreported:Chip type: ESP32-S3 (QFN56) (revision v0.2) MAC: d8:85:ac:11:22:33 Detected flash size: 8MBCP210x + ESP32-S3 + 8 MB points at Heltec V3, but only as a deduction.
-
The
stringssearch foundheltec_testandHELTEC_WIFI_LORA_32_V3: Heltec’s factory test firmware for the V3. -
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:
- Device: Heltec V3.
- Firmware: 2.7.26.54e0d8d (Beta).
- 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
- Install the Meshtastic app and turn on Bluetooth. Android also needs the location or nearby-devices permission.
- 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.
- Enter the PIN shown on the node’s screen, or
123456if 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(orlsof) 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):
- Check what holds the port:
fuser -v /dev/ttyUSB0. - Press RST once without BOOT; a reset with BOOT held leaves it in the silent bootloader.
- 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 falseover 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
- Flasher — the browser-based firmware flasher
- Web client releases — the build self-hosted in Section 7; also hosted at client.meshtastic.org
- Meshtastic software documentation — the CLI, the apps and the API
- Public map — where the node appears once MQTT map reporting is on
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.