Introduction
A Rust CLI tool for interacting with Meshtastic mesh networking devices over TCP, serial, or BLE connections.
What it does
mttctl provides a command-line interface to Meshtastic devices, allowing you to list nodes, send messages, monitor incoming packets, query device info, ping specific nodes, manage channels, control GPIO pins, and more — all from a terminal.
Why it exists
The Meshtastic ecosystem lacks a robust, composable CLI tool built in Rust. This project aims to fill that gap as an open-source contribution, leveraging the official meshtastic Rust crate to interact with real hardware and local simulators alike.
Who it's for
Developers and operators working with Meshtastic mesh networks who want scriptable, terminal-native access to device data without a GUI.
Key Design Decisions
- Strategy pattern for commands: each command is an independent module implementing a shared trait, making it trivial to add new commands without touching existing ones.
- SOLID principles throughout: single responsibility per module, open/closed for command extension, dependency inversion via connection abstraction.
- Thin
main.rs: only parses CLI arguments and dispatches to the appropriate command — no business logic lives there. - Async-first: all I/O uses Tokio, matching the async model of the underlying
meshtasticcrate. - Optional BLE support: compiled in via
--features bleto avoid requiring Bluetooth dependencies in environments that do not need them.
Installation
Pre-built binaries (recommended)
Download the latest binary for your platform from GitHub Releases:
| Platform | Binary |
|---|---|
| Linux x86_64 | mttctl-linux-x86_64 |
| Linux ARM64 | mttctl-linux-aarch64 |
| macOS Intel | mttctl-macos-x86_64 |
| macOS Apple Silicon | mttctl-macos-aarch64 |
| Windows x86_64 | mttctl-windows-x86_64.exe |
# Example: Linux x86_64
curl -L https://github.com/matutetandil/mttctl/releases/latest/download/mttctl-linux-x86_64 -o mttctl
chmod +x mttctl
sudo mv mttctl /usr/local/bin/
# Example: macOS Apple Silicon
curl -L https://github.com/matutetandil/mttctl/releases/latest/download/mttctl-macos-aarch64 -o mttctl
chmod +x mttctl
sudo mv mttctl /usr/local/bin/
Install from crates.io
If you have the Rust toolchain installed:
cargo install mttctl
Build from source
git clone https://github.com/matutetandil/mttctl.git
cd mttctl
cargo build --release
The compiled binary will be at target/release/mttctl.
BLE support
All pre-built binaries include BLE support out of the box. No extra steps needed.
When building from source, add the ble feature flag:
cargo build --release --features ble
# or
cargo install mttctl --features ble
Linux requires BlueZ: sudo apt install libbluetooth-dev
Usage & Connection
CLI Overview
mttctl [OPTIONS] <COMMAND>
Options:
--host <HOST> TCP host to connect to [default: 127.0.0.1]
--port <PORT> TCP port to connect to [default: 4403]
--serial <PATH> Serial device path (e.g. /dev/ttyUSB0). Overrides TCP.
--ble <NAME|MAC> BLE device name or MAC address (requires --features ble build)
--ble-scan Scan for nearby BLE Meshtastic devices and list them
--no-nodes Skip initial node discovery (saves seconds on large meshes)
--json Output results as JSON instead of formatted text
-h, --help Print help
-V, --version Print version
Connection Modes
TCP (default)
Connects to a Meshtastic device or simulator via TCP. This is the default mode when no --serial or --ble flag is provided.
# Default: localhost:4403 (ideal for Docker simulator)
mttctl nodes
# Custom host and port
mttctl --host 192.168.1.100 --port 4403 nodes
Serial
Connect to a physical device over a serial port.
mttctl --serial /dev/ttyUSB0 nodes
BLE
Connect to a nearby Meshtastic device via Bluetooth Low Energy. Requires the binary to be built with --features ble.
# Connect by device name
mttctl --ble "Meshtastic_abcd" nodes
# Connect by MAC address
mttctl --ble "AA:BB:CC:DD:EE:FF" nodes
# Scan for nearby devices
mttctl --ble-scan
Global Flags
--no-nodes
Skip the initial node discovery phase on startup, which can take several seconds on large meshes. Useful for commands like send or device reboot that don't need the full node list.
mttctl --no-nodes send "hello mesh"
--json
Output results as a JSON object or array instead of the default formatted text. Useful for shell scripting, log ingestion, or piping output into tools like jq.
# List nodes as JSON
mttctl --json nodes
# Get device config as JSON
mttctl --json config get lora
# Get local node info as JSON
mttctl --json info
# Pipe into jq for filtering
mttctl --json nodes | jq '[.[] | select(.battery < 20)]'
The flag is a global option and must be placed before the subcommand name. Commands that produce no structured output (e.g., send, device reboot) ignore the flag.
Quick Start with Docker Simulator
The repository includes a config.yaml for the Meshtastic simulator. No hardware required:
# Start the simulator
docker run -d --name meshtasticd \
-v ./config.yaml:/etc/meshtasticd/config.yaml:ro \
-p 4403:4403 \
meshtastic/meshtasticd:latest meshtasticd -s
# List nodes
mttctl nodes
Messaging: nodes, send, listen, reply, info, support
nodes
Lists all nodes currently known to the connected device.
mttctl nodes
Output columns:
| Column | Description |
|---|---|
| ID | Unique node identifier (hex) |
| Name | Human-readable node name from device config |
| Battery | Battery level percentage (if reported) |
| SNR | Signal-to-noise ratio of the last packet |
| Hops | Number of hops from the local node |
| Last Heard | Timestamp of the most recent packet received |
Use --fields to select which columns to display. Separate field names with commas.
# Show only ID, name, and SNR
mttctl nodes --fields id,name,snr
# Show extended fields including hardware model, role, and position
mttctl nodes --fields id,name,hw_model,role,position
Available fields:
| Field | Description | Default |
|---|---|---|
id | Node identifier (hex) | Yes |
name | Node long name | Yes |
battery | Battery level percentage | Yes |
snr | Signal-to-noise ratio | Yes |
hops | Number of hops from local node | Yes |
last_heard | Timestamp of last received packet | Yes |
hw_model | Hardware model name | No |
role | Device role (CLIENT, ROUTER, etc.) | No |
position | Last known GPS coordinates | No |
send
Sends a text message to the mesh network. By default the message is broadcast to all nodes.
# Broadcast a message to all nodes
mttctl send "hello mesh"
# Send to a specific node by hex ID
mttctl send "hello node" --dest 04e1c43b
# Send to a node by name (searches known nodes, case-insensitive)
mttctl send "hello!" --to Pedro
# Send on a specific channel (0-7)
mttctl send "hello channel" --channel 1
# Combine destination and channel
mttctl send "direct message" --dest 04e1c43b --channel 2
# Wait for delivery confirmation (ACK) before returning
mttctl send "confirmed message" --dest 04e1c43b --ack
# Wait for ACK with custom timeout
mttctl send "confirmed message" --to Pedro --ack --timeout 60
# Send as a private message (PRIVATE_APP port instead of text port)
mttctl send "private payload" --dest 04e1c43b --private
Shell note: The
!prefix is optional. If you include it, quote or escape it to prevent shell history expansion:--dest '!04e1c43b'or--dest \!04e1c43b.
| Option | Description |
|---|---|
<MESSAGE> | The text message to send (required, positional) |
--dest | Destination node ID in hex (e.g. 04e1c43b). The ! prefix is optional. Cannot be combined with --to. |
--to | Destination node name (e.g. Pedro). Searches known nodes by name (case-insensitive). If multiple nodes match, shows the list and asks you to use --dest instead. Cannot be combined with --dest. |
--channel | Channel index 0-7 (default: 0) |
--ack | Wait for delivery ACK before returning. Requires --dest or --to (cannot ACK a broadcast). |
--timeout | Seconds to wait for ACK when --ack is set (default: 30). |
--private | Send on PRIVATE_APP port (port 256) instead of the standard text message port. |
listen
Streams all incoming packets from the mesh network in real time. Runs continuously until interrupted with Ctrl+C.
mttctl listen
# Write all received packets as JSON Lines to a log file
mttctl listen --log packets.jsonl
# Continue displaying packets in the terminal while also writing to a log file
mttctl listen --log /var/log/meshtastic/packets.jsonl
Decodes and displays the following packet types:
| Packet Type | Display |
|---|---|
| Text message | Full message text |
| Position | Latitude, longitude, altitude, satellite count |
| Telemetry | Battery, voltage, channel utilization, env data |
| Node info | Long name, short name |
| Routing | ACK/NAK status, route requests/replies |
| Other | Port type and payload size |
| Option | Description |
|---|---|
--log | File path to write received packets as JSON Lines (one JSON object per line). The terminal display continues in parallel. Omit to disable file logging. |
Example output:
-> Listening for packets... Press Ctrl+C to stop.
[15:30:00] !04e1c43b (Pedro) -> broadcast | Text: Hello everyone!
[15:30:05] !a1b2c3d4 (Maria) -> !04e1c43b | Position: 40.41680, -3.70380, 650m, 8 sats
[15:30:10] !04e1c43b (Pedro) -> broadcast | Telemetry: battery 85%, 3.90V, ch_util 12.3%
[15:30:15] !a1b2c3d4 (Maria) -> !04e1c43b | Routing: ACK
reply
Auto-reply mode. Listens for incoming text messages and automatically replies to each sender with signal information (SNR, RSSI, hops). Useful for range testing and network debugging. Runs continuously until interrupted with Ctrl+C.
mttctl reply
Example output:
-> Reply mode active. Listening for messages. Press Ctrl+C to stop.
[15:30:00] Message from Pedro (!04e1c43b): "hello"
-> Replied: "Heard you! SNR: 8.5 dB, RSSI: -85 dBm, Hops: 2"
info
Displays detailed information about the local node and connected device.
mttctl info
Example output:
Node
ID: !04e1c43b
Name: Pedro
Short name: PD
Hardware: HELTEC V3
Role: CLIENT
Firmware
Version: 2.5.6.abc1234
Reboots: 12
Capabilities
Features: WiFi, Bluetooth, PKC
Device Metrics
Battery: 85%
Voltage: 3.90V
Channel util.: 12.3%
Uptime: 2d 5h 30m
Channels
Ch 0: Default (Primary, AES-256)
Ch 1: Team (Secondary, AES-256)
Nodes in mesh: 8
support
Displays a diagnostic summary of the connected device and CLI. Useful for troubleshooting and for sharing device context in bug reports.
mttctl support
Example output:
mttctl v0.3.0
Device
Node ID: !04e1c43b
Firmware: 2.5.6.abc1234
Hardware: HELTEC_V3
Role: CLIENT
Region: EU868
Modem preset: LongFast
Capabilities: WiFi, Bluetooth, PKC
Channels
[0] Default (Primary)
[1] Team (Secondary)
Known nodes: 8
Network: ping, traceroute
ping
Sends a ping to a specific node and measures the round-trip time by waiting for an ACK.
# Ping by node ID
mttctl ping --dest 04e1c43b
# Ping by name
mttctl ping --to Pedro
# Custom timeout (default: 30s)
mttctl ping --dest 04e1c43b --timeout 60
| Option | Description |
|---|---|
--dest | Destination node ID in hex, ! prefix optional (required unless --to is used) |
--to | Destination node name (required unless --dest is used) |
--timeout | Seconds to wait for ACK (default: 30) |
Example output:
-> Pinging !04e1c43b (Pedro) (packet id: a1b2c3d4)...
ok ACK from !04e1c43b (Pedro) in 2.3s
If the node doesn't respond:
-> Pinging !04e1c43b (Pedro) (packet id: a1b2c3d4)...
x Timeout after 30s -- no ACK from !04e1c43b (Pedro)
traceroute
Traces the route to a destination node, showing each hop along the path with SNR (signal-to-noise ratio) values.
# Traceroute by node ID
mttctl traceroute --dest 04e1c43b
# Traceroute by name
mttctl traceroute --to Pedro
# Custom timeout (default: 60s)
mttctl traceroute --dest 04e1c43b --timeout 120
| Option | Description |
|---|---|
--dest | Destination node ID in hex, ! prefix optional (required unless --to is used) |
--to | Destination node name (required unless --dest is used) |
--timeout | Seconds to wait for response (default: 60) |
Example output:
-> Tracing route to Pedro (!04e1c43b)...
1 !a1b2c3d4 (Local)
2 !e5f6a7b8 (Relay-1) SNR: 6.0 dB
3 !04e1c43b (Pedro) SNR: 8.5 dB
ok Route to Pedro (!04e1c43b) completed in 4.2s (2 hops)
If a return path differs from the forward path, both are shown separately.
Configuration: config
Read, write, export, and import device and module configuration. Supports all 8 device config sections and 13 module config sections.
config get
Display current configuration. Optionally specify a section to show only that section.
# Show all configuration sections
mttctl config get
# Show a specific section
mttctl config get lora
mttctl config get mqtt
mttctl config get device
Available sections:
| Device Config | Module Config |
|---|---|
device | mqtt |
position | serial |
power | external-notification |
network | store-forward |
display | range-test |
lora | telemetry |
bluetooth | canned-message |
security | audio |
remote-hardware | |
neighbor-info | |
ambient-lighting | |
detection-sensor | |
paxcounter |
Example output:
LoRa
region: Us
modem_preset: LongFast
use_preset: true
hop_limit: 3
tx_enabled: true
tx_power: 30
...
config set
Set a configuration value. The key uses section.field format. The device will reboot after applying changes.
# Set LoRa region
mttctl config set lora.region Eu868
# Change device role
mttctl config set device.role Router
# Set hop limit
mttctl config set lora.hop_limit 5
# Enable MQTT
mttctl config set mqtt.enabled true
# Set WiFi credentials
mttctl config set network.wifi_ssid "MyNetwork"
mttctl config set network.wifi_psk "MyPassword"
For enum fields, use the human-readable name (case-insensitive). Run config get <section> to see current values and available field names.
Example output:
-> Setting lora.region = Eu868
! Device will reboot to apply changes.
ok Configuration updated.
config begin-edit
Signal the device to begin collecting a batch of configuration changes. Use this before a sequence of config set calls to apply them all in a single transaction rather than rebooting after each change.
mttctl config begin-edit
mttctl config set lora.region Eu868
mttctl config set lora.hop_limit 5
mttctl config set device.role Router
mttctl config commit-edit
config commit-edit
Signal the device to commit and apply all configuration changes queued since the last config begin-edit. The device will reboot once to apply all pending changes.
mttctl config commit-edit
config set-modem-preset
Set the LoRa modem preset directly by name, without having to go through config set lora.modem_preset. Valid preset names are case-insensitive.
mttctl config set-modem-preset LongFast
mttctl config set-modem-preset ShortTurbo
mttctl config set-modem-preset MediumSlow
Available presets:
| Preset | Description |
|---|---|
LongFast | Long range, faster throughput (default) |
LongSlow | Long range, slower throughput |
VeryLongSlow | Maximum range, very slow |
MediumSlow | Medium range, slower |
MediumFast | Medium range, faster |
ShortSlow | Short range, slower |
ShortFast | Short range, fastest throughput |
LongModerate | Long range, moderate throughput |
ShortTurbo | Short range, maximum throughput |
config ch-add-url
Add channels from a meshtastic:// URL without replacing existing channels. This differs from config set-url, which replaces all current channels with those from the URL.
mttctl config ch-add-url "https://meshtastic.org/e/#ENCODED..."
| Option | Description |
|---|---|
<URL> | Meshtastic configuration URL (required) |
config export
Exports the full device configuration (device config, module config, and channels) as YAML. Useful for backups, sharing configurations, or migrating between devices.
# Print config to stdout
mttctl config export
# Save to a file
mttctl config export --file backup.yaml
| Option | Description |
|---|---|
--file | Output file path. If omitted, prints YAML to stdout |
Example output (truncated):
bluetooth:
enabled: true
fixed_pin: 123456
mode: 1
device:
role: 0
node_info_broadcast_secs: 900
...
lora:
region: 1
modem_preset: 3
hop_limit: 3
...
mqtt:
enabled: false
address: mqtt.meshtastic.org
...
channels:
- index: 0
role: PRIMARY
name: ''
psk: '01'
uplink_enabled: false
downlink_enabled: false
position_precision: 0
- index: 1
role: SECONDARY
name: Team
psk: d4f1bb3a2029075960bcffabcf4e6901...
...
config import
Imports and applies configuration from a YAML file. The file format matches the output of config export. Sections not present in the file are left unchanged. The device will reboot after applying config changes.
mttctl config import backup.yaml
| Option | Description |
|---|---|
<FILE> | Path to the YAML configuration file (required) |
Example output:
-> Importing configuration from backup.yaml...
ok Imported 8 config sections, 13 module sections, 2 channels.
! Device will reboot to apply configuration changes.
config set-ham
Configure the device for licensed Ham radio operation. Sets the callsign as the long name, enables long-range LoRa settings, and disables encryption as required by Ham regulations. Optionally set TX power and frequency.
# Set Ham mode with callsign
mttctl config set-ham KD2ABC
# Set Ham mode with custom TX power and frequency
mttctl config set-ham KD2ABC --tx-power 17 --frequency 906.875
| Option | Description |
|---|---|
<CALLSIGN> | Ham radio callsign to set as device name (required) |
--tx-power | Transmit power in dBm (optional) |
--frequency | Frequency in MHz (optional) |
config set-url
Apply channels and LoRa configuration from a meshtastic:// URL. These URLs are typically generated by the Meshtastic app or web client for sharing device configurations. This replaces all existing channels with those defined in the URL. To add channels without replacing existing ones, use config ch-add-url.
mttctl config set-url "https://meshtastic.org/e/#ENCODED..."
| Option | Description |
|---|---|
<URL> | Meshtastic configuration URL (required) |
Channels: channel
Manage device channels: list, add, delete, modify properties, and generate a QR code for sharing.
channel list
List all configured channels with their role, encryption, and uplink/downlink status.
mttctl channel list
Example output:
Channels
[0] Default Primary Default key uplink: false downlink: false
[1] Team Secondary AES-256 uplink: false downlink: false
channel add
Add a new secondary channel. The channel is placed in the first available slot (indices 1-7).
# Add with default encryption key
mttctl channel add "Team"
# Add with a random AES-256 key
mttctl channel add "Secure" --psk random
# Add with no encryption
mttctl channel add "Open" --psk none
# Add with a specific AES-128 key (32 hex characters)
mttctl channel add "Custom" --psk d4f1bb3a2029075960bcffabcf4e6901
| Option | Description |
|---|---|
<NAME> | Channel name, up to 11 characters (required) |
--psk | Pre-shared key: none, default, random, or hex-encoded key (default: default) |
channel del
Delete a channel by index. Cannot delete the primary channel (index 0).
mttctl channel del 1
channel set
Set a property on a specific channel.
# Rename a channel
mttctl channel set 1 name "NewName"
# Change encryption key
mttctl channel set 1 psk random
# Enable MQTT uplink
mttctl channel set 1 uplink_enabled true
# Enable MQTT downlink
mttctl channel set 1 downlink_enabled true
# Set position precision
mttctl channel set 0 position_precision 14
| Field | Description |
|---|---|
name | Channel name (up to 11 characters) |
psk | Pre-shared key (none, default, random, or hex) |
uplink_enabled | Forward mesh messages to MQTT |
downlink_enabled | Forward MQTT messages to mesh |
position_precision | Bits of precision for position data |
channel qr
Generate a QR code and shareable meshtastic:// URL for the current channel configuration. By default the QR code is printed directly to the terminal using Unicode block characters. Use --output to save as a PNG or SVG image file. Use --all to generate a separate QR code for each active channel individually.
# Print combined QR code to terminal (all active channels)
mttctl channel qr
# Export combined QR as PNG image (512x512 minimum)
mttctl channel qr --output channels.png
# Export combined QR as SVG image
mttctl channel qr --output channels.svg
# Print individual QR code per active channel to terminal
mttctl channel qr --all
| Option | Description |
|---|---|
--output | File path for image export. Supports .png and .svg formats. Prints to terminal if omitted. Cannot be combined with --all. |
--all | Generate one QR code per active channel, printed to terminal. Cannot be combined with --output. |
Example output (terminal):
[block character QR code rendered in terminal]
URL: https://meshtastic.org/e/#ENCODED...
Example output (file export):
ok QR code saved to channels.png
URL: https://meshtastic.org/e/#ENCODED...
Example output (--all, two active channels):
Channel 0: Default
[block character QR code]
URL: https://meshtastic.org/e/#ENCODED_CH0...
Channel 1: Team
[block character QR code]
URL: https://meshtastic.org/e/#ENCODED_CH1...
Device Management: device
Device management commands: reboot, reboot-ota, enter-dfu, shutdown, factory reset variants, reset-nodedb, set-time, canned messages, and ringtone. Reboot and shutdown support targeting the local device (default) or a remote node.
device reboot
Reboot the connected device or a remote node.
# Reboot local device (5 second delay)
mttctl device reboot
# Reboot with custom delay
mttctl device reboot --delay 10
# Reboot a remote node by ID
mttctl device reboot --dest 04e1c43b
# Reboot a remote node by name
mttctl device reboot --to Pedro
| Option | Description |
|---|---|
--dest | Target node ID in hex. Omit to target local device |
--to | Target node name. Omit to target local device |
--delay | Seconds before rebooting (default: 5) |
device reboot-ota
Reboot the device into OTA (Over-The-Air) firmware update mode. This is specific to ESP32-based Meshtastic hardware. Supports targeting the local device or a remote node.
# Reboot local device into OTA mode
mttctl device reboot-ota
# Reboot remote node into OTA mode
mttctl device reboot-ota --dest 04e1c43b
mttctl device reboot-ota --to Pedro
# Custom delay
mttctl device reboot-ota --delay 10
| Option | Description |
|---|---|
--dest | Target node ID in hex. Omit to target local device |
--to | Target node name. Omit to target local device |
--delay | Seconds before rebooting into OTA mode (default: 5) |
device enter-dfu
Enter Device Firmware Upgrade (DFU) mode. This is specific to NRF52-based Meshtastic hardware (e.g., RAK devices). The device will appear as a USB mass storage device after entering DFU mode, allowing firmware file drops.
mttctl device enter-dfu
device shutdown
Shut down the connected device or a remote node.
# Shutdown local device
mttctl device shutdown
# Shutdown with custom delay
mttctl device shutdown --delay 10
# Shutdown a remote node
mttctl device shutdown --dest 04e1c43b
| Option | Description |
|---|---|
--dest | Target node ID in hex. Omit to target local device |
--to | Target node name. Omit to target local device |
--delay | Seconds before shutting down (default: 5) |
device factory-reset
Restore the device to factory defaults. This erases all configuration and stored data but preserves BLE bonds.
mttctl device factory-reset
device factory-reset-device
Perform a full factory reset that also wipes all BLE bonds. Use this when you want to completely reset the device as if it were brand new, including removing all previously paired Bluetooth devices.
mttctl device factory-reset-device
device reset-nodedb
Clear the device's entire node database. This removes all known nodes from the local NodeDB.
mttctl device reset-nodedb
device set-time
Set the device clock. Uses the current system time if no timestamp is provided.
# Set time from system clock
mttctl device set-time
# Set time from a specific Unix timestamp
mttctl device set-time 1708444800
| Option | Description |
|---|---|
[TIMESTAMP] | Unix timestamp in seconds. Uses system time if omitted |
device set-canned-message
Set the canned messages stored on the device. Messages are separated by | and can be selected quickly from a compatible Meshtastic client.
mttctl device set-canned-message "Yes|No|Help|On my way|Call me"
| Option | Description |
|---|---|
<MESSAGES> | Pipe-separated list of canned messages (required) |
device get-canned-message
Display the canned messages currently configured on the device. Requests the canned message module config from the device and waits for the response.
mttctl device get-canned-message
# Custom timeout
mttctl device get-canned-message --timeout 60
| Option | Description |
|---|---|
--timeout | Seconds to wait for the device response (default: 30) |
Example output:
Canned messages:
1: Yes
2: No
3: Help
4: On my way
5: Call me
device set-ringtone
Set the notification ringtone on the device. The ringtone is provided in RTTTL (Ring Tone Text Transfer Language) format.
mttctl device set-ringtone "scale:d=4,o=5,b=120:c,e,g,c6"
| Option | Description |
|---|---|
<RINGTONE> | Ringtone string in RTTTL format (required) |
device get-ringtone
Display the notification ringtone currently stored on the device.
mttctl device get-ringtone
# Custom timeout
mttctl device get-ringtone --timeout 60
| Option | Description |
|---|---|
--timeout | Seconds to wait for the device response (default: 30) |
Example output:
Ringtone: scale:d=4,o=5,b=120:c,e,g,c6
Node Management: node
node set-owner
Set the device owner name (long name and short name). The short name is auto-generated from the long name if omitted.
# Set long name (short name auto-generated as "PD")
mttctl node set-owner "Pedro"
# Set both long and short name
mttctl node set-owner "Pedro's Node" --short PN
# Multi-word names generate initials (e.g. "My Cool Node" -> "MCN")
mttctl node set-owner "My Cool Node"
| Option | Description |
|---|---|
<NAME> | Long name for the device, up to 40 characters (required) |
--short | Short name, up to 5 characters. Auto-generated if omitted |
node remove
Remove a specific node from the local NodeDB. The node can be specified by hex ID or by name.
# Remove by node ID
mttctl node remove --dest 04e1c43b
# Remove by name
mttctl node remove --to Pedro
| Option | Description |
|---|---|
--dest | Node ID in hex to remove (required unless --to is used) |
--to | Node name to remove (required unless --dest is used) |
node set-favorite
Mark a node as a favorite. Favorites are stored on the device and can be used for filtering in compatible clients.
# Mark by node ID
mttctl node set-favorite --dest 04e1c43b
# Mark by name
mttctl node set-favorite --to Pedro
| Option | Description |
|---|---|
--dest | Node ID in hex (required unless --to is used) |
--to | Node name (required unless --dest is used) |
node remove-favorite
Remove a node from the favorites list.
mttctl node remove-favorite --dest 04e1c43b
mttctl node remove-favorite --to Pedro
| Option | Description |
|---|---|
--dest | Node ID in hex (required unless --to is used) |
--to | Node name (required unless --dest is used) |
node set-ignored
Mark a node as ignored. Ignored nodes are filtered out of mesh activity on the local device.
mttctl node set-ignored --dest 04e1c43b
mttctl node set-ignored --to Pedro
| Option | Description |
|---|---|
--dest | Node ID in hex (required unless --to is used) |
--to | Node name (required unless --dest is used) |
node remove-ignored
Remove a node from the ignored list.
mttctl node remove-ignored --dest 04e1c43b
mttctl node remove-ignored --to Pedro
| Option | Description |
|---|---|
--dest | Node ID in hex (required unless --to is used) |
--to | Node name (required unless --dest is used) |
node set-unmessageable
Mark the local node as unmessageable (prevents others from sending direct messages to it) or restore it as messageable.
# Mark as unmessageable (default)
mttctl node set-unmessageable
# Explicitly mark as unmessageable
mttctl node set-unmessageable true
# Restore as messageable
mttctl node set-unmessageable false
| Option | Description |
|---|---|
[VALUE] | true to mark as unmessageable, false to mark as messageable (default: true) |
Position: position
GPS position commands: get, set, and remove.
position get
Display the current GPS position of the local node.
mttctl position get
position set
Set a fixed GPS position on the device. Requires latitude and longitude; altitude and broadcast flags are optional. Once a fixed position is set, the device broadcasts this position instead of using live GPS data.
# Set position with latitude and longitude
mttctl position set 40.4168 -3.7038
# Set position with altitude (in meters)
mttctl position set 40.4168 -3.7038 650
# Set position with named broadcast flags
mttctl position set 40.4168 -3.7038 650 --flags "ALTITUDE,TIMESTAMP,SPEED"
# Set position with numeric bitmask (equivalent to above: 1 + 128 + 512 = 641)
mttctl position set 40.4168 -3.7038 650 --flags 641
# Set position with hex bitmask
mttctl position set 40.4168 -3.7038 650 --flags 0x281
| Option | Description |
|---|---|
<LATITUDE> | Latitude in decimal degrees (required) |
<LONGITUDE> | Longitude in decimal degrees (required) |
<ALTITUDE> | Altitude in meters (optional) |
--flags | Position broadcast field flags (optional). Accepts comma-separated names (ALTITUDE, ALTITUDE_MSL, GEOIDAL_SEPARATION, DOP, HVDOP, SATINVIEW, SEQ_NO, TIMESTAMP, HEADING, SPEED) or a numeric bitmask (decimal or 0x hex). |
position remove
Remove the fixed GPS position from the device. After removal, the device will return to using live GPS data if a GPS module is available.
mttctl position remove
Remote Requests: request
Request data from remote nodes.
request telemetry
Request telemetry data from a remote node. Use --type to select a specific telemetry variant (default: device).
# Request device telemetry (battery, voltage, channel utilization)
mttctl request telemetry --dest 04e1c43b
# Request environment telemetry (temperature, humidity, pressure)
mttctl request telemetry --to Pedro --type environment
# Request air quality metrics (PM1.0, PM2.5, PM10.0, CO2, VOC)
mttctl request telemetry --dest 04e1c43b --type air-quality
# Request power metrics (voltage/current per channel)
mttctl request telemetry --dest 04e1c43b --type power
# Request local stats (uptime, packets tx/rx, air utilization)
mttctl request telemetry --dest 04e1c43b --type local-stats
# Request health metrics (heart rate, SpO2)
mttctl request telemetry --dest 04e1c43b --type health
# Request host metrics (free memory, disk, load average)
mttctl request telemetry --dest 04e1c43b --type host
| Option | Description |
|---|---|
--dest | Target node ID in hex (required unless --to is used) |
--to | Target node name (required unless --dest is used) |
--type | Telemetry type: device, environment, air-quality, power, local-stats, health, host (default: device) |
--timeout | Timeout in seconds (default: 30) |
request position
Request position data from a remote node.
# Request by node ID
mttctl request position --dest 04e1c43b
| Option | Description |
|---|---|
--dest | Target node ID in hex (required unless --to is used) |
--to | Target node name (required unless --dest is used) |
request metadata
Request device metadata (firmware version, hardware model, capabilities) from a remote node.
# Request by node ID
mttctl request metadata --dest 04e1c43b
# Request by name with custom timeout
mttctl request metadata --to Pedro --timeout 60
| Option | Description |
|---|---|
--dest | Target node ID in hex (required unless --to is used) |
--to | Target node name (required unless --dest is used) |
--timeout | Seconds to wait for response (default: 30) |
Example output:
Device metadata from Pedro (!04e1c43b):
Firmware: 2.5.6.abc1234
Hardware: HELTEC_V3
Device ID: 04e1c43b
Capabilities: HasWifi, HasBluetooth
GPIO: gpio
Remote GPIO pin operations on mesh nodes. Requires the target node to have the remote hardware module enabled. GPIO mask values can be provided in decimal or 0x hex format.
gpio write
Write a value to GPIO pins on a remote node. The mask specifies which pins to affect; the value specifies the state to write to those pins.
# Set GPIO pin 4 high on a remote node (mask and value in decimal)
mttctl gpio write --dest 04e1c43b --mask 16 --value 16
# Set GPIO pin 4 high (mask and value in hex)
mttctl gpio write --dest 04e1c43b --mask 0x10 --value 0x10
# Set pin 4 high and pin 5 low
mttctl gpio write --to Pedro --mask 0x30 --value 0x10
| Option | Description |
|---|---|
--dest | Target node ID in hex (required unless --to is used) |
--to | Target node name (required unless --dest is used) |
--mask | Bitmask of GPIO pins to write (decimal or 0x hex) |
--value | Values to write to the masked pins (decimal or 0x hex) |
gpio read
Read the current state of GPIO pins from a remote node.
# Read pins 4 and 5 from a remote node (mask in decimal)
mttctl gpio read --dest 04e1c43b --mask 48
# Read using hex mask
mttctl gpio read --to Pedro --mask 0x30
# Custom timeout
mttctl gpio read --dest 04e1c43b --mask 0x10 --timeout 60
| Option | Description |
|---|---|
--dest | Target node ID in hex (required unless --to is used) |
--to | Target node name (required unless --dest is used) |
--mask | Bitmask of GPIO pins to read (decimal or 0x hex) |
--timeout | Seconds to wait for the response (default: 30) |
Example output:
GPIO state from Pedro (!04e1c43b):
Mask: 0x00000030
Value: 0x00000010 (pin 4: HIGH, pin 5: LOW)
gpio watch
Watch for GPIO state changes on a remote node. Runs continuously until interrupted with Ctrl+C. Each state change is printed with a timestamp.
# Watch pins 4 and 5
mttctl gpio watch --dest 04e1c43b --mask 0x30
# Watch by node name
mttctl gpio watch --to Pedro --mask 0x10
| Option | Description |
|---|---|
--dest | Target node ID in hex (required unless --to is used) |
--to | Target node name (required unless --dest is used) |
--mask | Bitmask of GPIO pins to watch (decimal or 0x hex) |
Example output:
-> Watching GPIO on Pedro (!04e1c43b) [mask: 0x00000030]. Press Ctrl+C to stop.
[15:30:02] Value changed: 0x00000010 (pin 4: HIGH, pin 5: LOW)
[15:31:15] Value changed: 0x00000030 (pin 4: HIGH, pin 5: HIGH)
[15:32:40] Value changed: 0x00000000 (pin 4: LOW, pin 5: LOW)
Waypoints: waypoint
Send, delete, and list waypoints on the mesh network. Waypoints are named geographic points that appear on the map in compatible Meshtastic clients.
waypoint send
Broadcast or unicast a new waypoint to the mesh.
# Send a waypoint broadcast to all nodes
mttctl waypoint send --name "Base Camp" --lat 40.4168 --lon -3.7038
# Send a waypoint with full options
mttctl waypoint send \
--name "Checkpoint A" \
--lat 40.4168 \
--lon -3.7038 \
--alt 650 \
--icon 9410 \
--expire 3600 \
--dest 04e1c43b
| Option | Description |
|---|---|
--name | Waypoint name, up to 30 characters (required) |
--lat | Latitude in decimal degrees (required) |
--lon | Longitude in decimal degrees (required) |
--alt | Altitude in meters (optional) |
--icon | Unicode code point for the waypoint icon displayed in clients (optional) |
--expire | Seconds until the waypoint expires (optional, omit for no expiry) |
--dest | Target node ID in hex for a unicast waypoint (omit to broadcast) |
--to | Target node name for a unicast waypoint (omit to broadcast) |
waypoint delete
Delete a waypoint by its numeric ID.
mttctl waypoint delete --id 42
| Option | Description |
|---|---|
--id | Numeric waypoint ID to delete (required) |
waypoint list
List all waypoints known to the local node. Listens for incoming waypoint packets with a configurable timeout.
mttctl waypoint list
Watch: watch
Displays the node table as a live-updating view that refreshes periodically in place, similar to the watch Unix utility applied to the nodes output. Press Ctrl+C to stop.
# Watch node table, refresh every 30 seconds (default)
mttctl watch
# Refresh every 10 seconds
mttctl watch --interval 10
# Watch with a custom field set
mttctl watch --fields id,name,battery,snr --interval 15
| Option | Description |
|---|---|
--interval | Refresh interval in seconds (default: 30) |
--fields | Comma-separated list of columns to display (same values as nodes --fields) |
Example output (refreshes in place):
mttctl watch -- refreshing every 30s -- last update: 15:30:00 -- Ctrl+C to stop
ID Name Battery SNR Hops Last Heard
!04e1c43b Pedro 85% 8.5 0 just now
!a1b2c3d4 Maria 72% 6.0 1 2m ago
!e5f6a7b8 Relay-1 -- 4.5 2 5m ago
MQTT Bridge: mqtt
Bidirectional MQTT bridge. Subscribes to incoming mesh packets and republishes them to an MQTT broker as JSON, and optionally subscribes to an MQTT topic to inject messages back into the mesh. Useful for integrating a Meshtastic mesh into home automation, dashboards, or data pipelines without enabling the built-in MQTT module on the device.
mqtt bridge
# Bridge to a local MQTT broker with default topic prefix
mttctl mqtt bridge --broker mqtt://localhost:1883
# Bridge with authentication
mttctl mqtt bridge \
--broker mqtt://broker.example.com:1883 \
--username myuser \
--password mypassword
# Bridge with a custom topic prefix (default: meshtastic)
mttctl mqtt bridge \
--broker mqtt://localhost:1883 \
--topic my-mesh
# Bridge without bidirectional injection (publish only)
mttctl mqtt bridge \
--broker mqtt://localhost:1883 \
--no-downlink
| Option | Description |
|---|---|
--broker | MQTT broker URL including scheme and port, e.g. mqtt://localhost:1883 (required) |
--username | MQTT username for authenticated brokers (optional) |
--password | MQTT password for authenticated brokers (optional) |
--topic | Topic prefix for all published messages (default: meshtastic) |
--no-downlink | Disable the downlink subscription (publish-only mode) |
Topic Format
Published topics follow this pattern:
<prefix>/<node-id>/<port-name>
Example topics published by the bridge:
meshtastic/04e1c43b/text
meshtastic/04e1c43b/position
meshtastic/04e1c43b/telemetry/device
Each message is a JSON object containing the decoded packet fields plus metadata (timestamp, sender node ID, SNR, RSSI, hops).
Downlink (MQTT to Mesh)
The downlink topic for injecting messages into the mesh:
<prefix>/downlink/send
Post a JSON payload to this topic to send a text message via the bridge:
{ "text": "hello mesh", "channel": 0 }
Shell REPL: shell
Interactive REPL (Read-Eval-Print Loop) for exploratory and interactive use. The shell maintains a single persistent connection to the device for the duration of the session, avoiding the startup overhead of reconnecting for every command. Commands are the same as in non-interactive mode; the connection flags (--host, --serial, --ble) are specified once when launching the shell.
# Start an interactive shell (connects to default TCP host)
mttctl shell
# Start an interactive shell connected to a serial device
mttctl --serial /dev/ttyUSB0 shell
Features
- Command history persisted to
~/.local/share/mttctl/historyacross sessions - Tab completion for all commands, subcommands, and flags (powered by
rustyline) - Single device connection reused for the entire session
helpprints available commandsexitor Ctrl+D to quit
Example Session
mttctl> nodes
ID Name Battery SNR Hops Last Heard
!04e1c43b Pedro 85% 8.5 0 just now
!a1b2c3d4 Maria 72% 6.0 1 2m ago
mttctl> send "hello from shell"
ok Message sent.
mttctl> ping --to Maria
-> Pinging !a1b2c3d4 (Maria) (packet id: 7f3a1b2c)...
ok ACK from !a1b2c3d4 (Maria) in 1.8s
mttctl> exit
Goodbye.
Completions: completions
Generate shell completion scripts. Once installed, completions enable tab completion for all commands, subcommands, flags, and many argument values directly in your shell.
# Print completion script for the current shell to stdout
mttctl completions bash
mttctl completions zsh
mttctl completions fish
mttctl completions powershell
mttctl completions elvish
| Option | Description |
|---|---|
<SHELL> | Target shell: bash, zsh, fish, powershell, elvish (required) |
Installing Completions
Bash
mttctl completions bash > ~/.local/share/bash-completion/completions/mttctl
Zsh
mttctl completions zsh > ~/.zfunc/_mttctl
# Then add to ~/.zshrc if not already present:
# fpath=(~/.zfunc $fpath)
# autoload -Uz compinit && compinit
Fish
mttctl completions fish > ~/.config/fish/completions/mttctl.fish
Config File: config-file
Manage a persistent configuration file stored at ~/.config/mttctl/config.toml. Values set here are applied automatically on every invocation, so you do not have to repeat connection options or other defaults on every command. Command-line flags always override config file values.
# Show current config file contents
mttctl config-file show
# Print the path to the config file
mttctl config-file path
# Set a persistent default value
mttctl config-file set host 192.168.1.100
mttctl config-file set port 4403
mttctl config-file set serial /dev/ttyUSB0
# Remove a previously set value (revert to built-in default)
mttctl config-file unset host
mttctl config-file unset serial
Subcommands
| Subcommand | Description |
|---|---|
show | Print the current config file contents as TOML |
set <KEY> <VALUE> | Set a persistent default value |
unset <KEY> | Remove a key, reverting to the built-in default |
path | Print the filesystem path of the config file |
Available Keys
| Key | Description | Equivalent flag |
|---|---|---|
host | Default TCP host | --host |
port | Default TCP port | --port |
serial | Default serial device path | --serial |
Example Config File
~/.config/mttctl/config.toml:
host = "192.168.1.100"
port = 4403
Architecture
System Design
CLI Input
|
v
main.rs (argument parsing + dispatch only)
|
+---> connection.rs (TCP, Serial, or BLE -> StreamApi)
|
+---> config_file.rs (persistent CLI config at ~/.config/mttctl/config.toml)
|
+---> commands/
mod.rs (Command trait definition)
nodes.rs (implements Command for node listing)
send.rs (implements Command for sending messages)
listen.rs (implements Command for packet streaming)
info.rs (implements Command for device info display)
ping.rs (implements Command for node ping with ACK)
config.rs (implements Command for config get/set)
channel.rs (implements Command for channel management)
traceroute.rs (implements Command for route tracing)
export_import.rs (implements Command for config export/import)
device.rs (implements Command for reboot/shutdown/time/canned/ringtone)
node.rs (implements Command for node management)
position.rs (implements Command for GPS position get/set/remove)
request.rs (implements Command for remote data requests)
reply.rs (implements Command for auto-reply)
gpio.rs (implements Command for remote GPIO operations)
support.rs (implements Command for diagnostic info display)
waypoint.rs (implements Command for waypoint send/delete/list)
watch.rs (implements Command for live-updating node table)
mqtt_bridge.rs (implements Command for bidirectional MQTT bridge)
shell.rs (implements Command for interactive REPL)
Key Patterns
- Command pattern (Strategy):
commands/mod.rsdefines aCommandtrait. Each subcommand implements it independently.main.rsdispatches to the correct implementor based on parsed CLI input. - Connection abstraction:
connection.rsencapsulates TCP (viameshtastic'sStreamApi), serial (viatokio-serial), and BLE connections, exposing a unified interface to commands. - Error types:
error.rsusesthiserrorfor structured, typed errors.anyhowis used at the boundary (main) for ergonomic top-level error handling. - Feature flags: BLE support is gated behind the
bleCargo feature to avoid requiring Bluetooth platform libraries in environments that do not need them. - Persistent config:
config_file.rsreads~/.config/mttctl/config.tomlat startup and merges stored defaults with command-line flags before dispatch, following standard XDG conventions.
Tech Stack
| Component | Crate / Tool | Reason |
|---|---|---|
| Language | Rust 2021 | Safety, performance, strong async ecosystem |
| Async runtime | Tokio | Required by the meshtastic crate |
| Device protocol | meshtastic v0.1.8 | Official Rust crate for Meshtastic protocol |
| CLI parsing | clap (derive) | Ergonomic, zero-boilerplate argument definitions |
| Shell completions | clap_complete | Generate shell completions from clap definitions |
| Error handling | thiserror / anyhow | Typed errors in libraries, ergonomic in binaries |
| Serial I/O | tokio-serial | Async serial port support |
| Terminal output | colored | Readable, colored CLI output |
| Terminal UI | crossterm | Terminal manipulation for the live watch display |
| Serialization | serde / serde_yaml | YAML config export and import |
| JSON output | serde_json | Structured JSON output for --json flag |
| Config file | toml / dirs | Persistent CLI config file parsing and XDG paths |
| QR codes | qrcode | QR code generation for terminal, PNG, and SVG |
| Image output | image | PNG image rendering for QR code export |
| MQTT client | rumqttc | Async MQTT client for the bridge command |
| Interactive REPL | rustyline / shlex | Command history, line editing, and tab completion for shell |
Note: The
meshtasticcrate (v0.1.8) is early-stage. When something appears underdocumented, refer to the source: https://github.com/meshtastic/rust
Project Structure
mttctl/
├── Cargo.toml
├── Cargo.lock
├── config.yaml # Docker simulator config
├── README.md
├── CHANGELOG.md
├── docs/ # mdBook documentation (this site)
│ ├── book.toml
│ └── src/
└── src/
├── main.rs # CLI parsing and command dispatch only
├── cli.rs # Clap argument and subcommand definitions
├── connection.rs # TCP, serial, and BLE connection handling
├── config_file.rs # Persistent CLI config (~/.config/mttctl/config.toml)
├── error.rs # Typed error definitions (thiserror)
├── node_db.rs # Node data model and local node database
├── router.rs # Packet routing and dispatch logic
└── commands/
├── mod.rs # Command trait and module exports
├── nodes.rs # `nodes` command implementation
├── send.rs # `send` command implementation
├── listen.rs # `listen` command implementation
├── info.rs # `info` command implementation
├── ping.rs # `ping` command implementation
├── config.rs # `config get/set/set-ham/set-url` implementation
├── traceroute.rs # `traceroute` command implementation
├── channel.rs # `channel add/del/set/list/qr` implementation
├── export_import.rs # `config export`/`config import` implementation
├── device.rs # `device` subcommands implementation
├── node.rs # `node` subcommands implementation
├── position.rs # `position get/set/remove` implementation
├── request.rs # `request telemetry/position/metadata` implementation
├── reply.rs # `reply` command implementation
├── gpio.rs # `gpio write/read/watch` implementation
├── support.rs # `support` command implementation
├── waypoint.rs # `waypoint send/delete/list` implementation
├── watch.rs # `watch` live node table implementation
├── mqtt_bridge.rs # `mqtt bridge` bidirectional bridge implementation
└── shell.rs # `shell` interactive REPL implementation
Development
Build
cargo build # debug build
cargo build --release # optimized release build
# With BLE support
cargo build --features ble
cargo build --release --features ble
Run (without installing)
# TCP — local simulator
cargo run -- --host 127.0.0.1 --port 4403 nodes
# Serial
cargo run -- --serial /dev/ttyUSB0 nodes
# BLE (requires --features ble build)
cargo run --features ble -- --ble "Meshtastic_abcd" nodes
Tests
cargo test # run all tests
cargo test <test_name> # run a single test by name
Lint and Format
cargo clippy -- -D warnings # lint; treats warnings as errors
cargo fmt --check # check formatting without applying
cargo fmt # apply formatting
Docker Simulator
The repository includes a config.yaml for the Meshtastic simulator. Start it with:
docker run -d --name meshtasticd \
-v ./config.yaml:/etc/meshtasticd/config.yaml:ro \
-p 4403:4403 \
meshtastic/meshtasticd:latest meshtasticd -s
Then interact with it using the default TCP connection:
cargo run -- nodes
cargo run -- send "hello from dev"
cargo run -- listen
Building the Documentation
This documentation is built with mdBook. To preview it locally:
# Install mdBook
cargo install mdbook
# Build
mdbook build docs
# Serve with live reload
mdbook serve docs --open
Contributing
Contributions are welcome! Here are some guidelines to keep in mind.
Code Standards
- All code follows SOLID principles — one responsibility per module, depend on abstractions
- New commands are added as independent modules under
src/commands/ - Each command implements the
Commandtrait defined incommands/mod.rs
Before Submitting
Make sure all checks pass:
cargo clippy -- -D warnings # no warnings allowed
cargo fmt --check # formatting must be applied
cargo test # all tests must pass
cargo build # clean build
Adding a New Command
- Create a new file under
src/commands/(e.g.,my_command.rs) - Implement the
Commandtrait - Add
mod my_command;tosrc/commands/mod.rs - Add the CLI variant to
src/cli.rs - Add the match arm in the
create_command()factory insrc/commands/mod.rs - Update documentation (README command table and relevant docs page)
Project Structure
See the Architecture page for details on the project structure and design patterns.
Reporting Issues
If you find a bug or have a feature request, please open an issue at GitHub Issues.