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 meshtastic crate.
  • Optional BLE support: compiled in via --features ble to avoid requiring Bluetooth dependencies in environments that do not need them.

Installation

Download the latest binary for your platform from GitHub Releases:

PlatformBinary
Linux x86_64mttctl-linux-x86_64
Linux ARM64mttctl-linux-aarch64
macOS Intelmttctl-macos-x86_64
macOS Apple Siliconmttctl-macos-aarch64
Windows x86_64mttctl-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:

ColumnDescription
IDUnique node identifier (hex)
NameHuman-readable node name from device config
BatteryBattery level percentage (if reported)
SNRSignal-to-noise ratio of the last packet
HopsNumber of hops from the local node
Last HeardTimestamp 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:

FieldDescriptionDefault
idNode identifier (hex)Yes
nameNode long nameYes
batteryBattery level percentageYes
snrSignal-to-noise ratioYes
hopsNumber of hops from local nodeYes
last_heardTimestamp of last received packetYes
hw_modelHardware model nameNo
roleDevice role (CLIENT, ROUTER, etc.)No
positionLast known GPS coordinatesNo

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.

OptionDescription
<MESSAGE>The text message to send (required, positional)
--destDestination node ID in hex (e.g. 04e1c43b). The ! prefix is optional. Cannot be combined with --to.
--toDestination 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.
--channelChannel index 0-7 (default: 0)
--ackWait for delivery ACK before returning. Requires --dest or --to (cannot ACK a broadcast).
--timeoutSeconds to wait for ACK when --ack is set (default: 30).
--privateSend 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 TypeDisplay
Text messageFull message text
PositionLatitude, longitude, altitude, satellite count
TelemetryBattery, voltage, channel utilization, env data
Node infoLong name, short name
RoutingACK/NAK status, route requests/replies
OtherPort type and payload size
OptionDescription
--logFile 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
OptionDescription
--destDestination node ID in hex, ! prefix optional (required unless --to is used)
--toDestination node name (required unless --dest is used)
--timeoutSeconds 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
OptionDescription
--destDestination node ID in hex, ! prefix optional (required unless --to is used)
--toDestination node name (required unless --dest is used)
--timeoutSeconds 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 ConfigModule Config
devicemqtt
positionserial
powerexternal-notification
networkstore-forward
displayrange-test
loratelemetry
bluetoothcanned-message
securityaudio
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:

PresetDescription
LongFastLong range, faster throughput (default)
LongSlowLong range, slower throughput
VeryLongSlowMaximum range, very slow
MediumSlowMedium range, slower
MediumFastMedium range, faster
ShortSlowShort range, slower
ShortFastShort range, fastest throughput
LongModerateLong range, moderate throughput
ShortTurboShort 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..."
OptionDescription
<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
OptionDescription
--fileOutput 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
OptionDescription
<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
OptionDescription
<CALLSIGN>Ham radio callsign to set as device name (required)
--tx-powerTransmit power in dBm (optional)
--frequencyFrequency 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..."
OptionDescription
<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
OptionDescription
<NAME>Channel name, up to 11 characters (required)
--pskPre-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
FieldDescription
nameChannel name (up to 11 characters)
pskPre-shared key (none, default, random, or hex)
uplink_enabledForward mesh messages to MQTT
downlink_enabledForward MQTT messages to mesh
position_precisionBits 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
OptionDescription
--outputFile path for image export. Supports .png and .svg formats. Prints to terminal if omitted. Cannot be combined with --all.
--allGenerate 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
OptionDescription
--destTarget node ID in hex. Omit to target local device
--toTarget node name. Omit to target local device
--delaySeconds 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
OptionDescription
--destTarget node ID in hex. Omit to target local device
--toTarget node name. Omit to target local device
--delaySeconds 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
OptionDescription
--destTarget node ID in hex. Omit to target local device
--toTarget node name. Omit to target local device
--delaySeconds 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
OptionDescription
[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"
OptionDescription
<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
OptionDescription
--timeoutSeconds 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"
OptionDescription
<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
OptionDescription
--timeoutSeconds 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"
OptionDescription
<NAME>Long name for the device, up to 40 characters (required)
--shortShort 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
OptionDescription
--destNode ID in hex to remove (required unless --to is used)
--toNode 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
OptionDescription
--destNode ID in hex (required unless --to is used)
--toNode 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
OptionDescription
--destNode ID in hex (required unless --to is used)
--toNode 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
OptionDescription
--destNode ID in hex (required unless --to is used)
--toNode 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
OptionDescription
--destNode ID in hex (required unless --to is used)
--toNode 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
OptionDescription
[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
OptionDescription
<LATITUDE>Latitude in decimal degrees (required)
<LONGITUDE>Longitude in decimal degrees (required)
<ALTITUDE>Altitude in meters (optional)
--flagsPosition 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
OptionDescription
--destTarget node ID in hex (required unless --to is used)
--toTarget node name (required unless --dest is used)
--typeTelemetry type: device, environment, air-quality, power, local-stats, health, host (default: device)
--timeoutTimeout in seconds (default: 30)

request position

Request position data from a remote node.

# Request by node ID
mttctl request position --dest 04e1c43b
OptionDescription
--destTarget node ID in hex (required unless --to is used)
--toTarget 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
OptionDescription
--destTarget node ID in hex (required unless --to is used)
--toTarget node name (required unless --dest is used)
--timeoutSeconds 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
OptionDescription
--destTarget node ID in hex (required unless --to is used)
--toTarget node name (required unless --dest is used)
--maskBitmask of GPIO pins to write (decimal or 0x hex)
--valueValues 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
OptionDescription
--destTarget node ID in hex (required unless --to is used)
--toTarget node name (required unless --dest is used)
--maskBitmask of GPIO pins to read (decimal or 0x hex)
--timeoutSeconds 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
OptionDescription
--destTarget node ID in hex (required unless --to is used)
--toTarget node name (required unless --dest is used)
--maskBitmask 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
OptionDescription
--nameWaypoint name, up to 30 characters (required)
--latLatitude in decimal degrees (required)
--lonLongitude in decimal degrees (required)
--altAltitude in meters (optional)
--iconUnicode code point for the waypoint icon displayed in clients (optional)
--expireSeconds until the waypoint expires (optional, omit for no expiry)
--destTarget node ID in hex for a unicast waypoint (omit to broadcast)
--toTarget node name for a unicast waypoint (omit to broadcast)

waypoint delete

Delete a waypoint by its numeric ID.

mttctl waypoint delete --id 42
OptionDescription
--idNumeric 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
OptionDescription
--intervalRefresh interval in seconds (default: 30)
--fieldsComma-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
OptionDescription
--brokerMQTT broker URL including scheme and port, e.g. mqtt://localhost:1883 (required)
--usernameMQTT username for authenticated brokers (optional)
--passwordMQTT password for authenticated brokers (optional)
--topicTopic prefix for all published messages (default: meshtastic)
--no-downlinkDisable 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).

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/history across sessions
  • Tab completion for all commands, subcommands, and flags (powered by rustyline)
  • Single device connection reused for the entire session
  • help prints available commands
  • exit or 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
OptionDescription
<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

SubcommandDescription
showPrint 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
pathPrint the filesystem path of the config file

Available Keys

KeyDescriptionEquivalent flag
hostDefault TCP host--host
portDefault TCP port--port
serialDefault 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.rs defines a Command trait. Each subcommand implements it independently. main.rs dispatches to the correct implementor based on parsed CLI input.
  • Connection abstraction: connection.rs encapsulates TCP (via meshtastic's StreamApi), serial (via tokio-serial), and BLE connections, exposing a unified interface to commands.
  • Error types: error.rs uses thiserror for structured, typed errors. anyhow is used at the boundary (main) for ergonomic top-level error handling.
  • Feature flags: BLE support is gated behind the ble Cargo feature to avoid requiring Bluetooth platform libraries in environments that do not need them.
  • Persistent config: config_file.rs reads ~/.config/mttctl/config.toml at startup and merges stored defaults with command-line flags before dispatch, following standard XDG conventions.

Tech Stack

ComponentCrate / ToolReason
LanguageRust 2021Safety, performance, strong async ecosystem
Async runtimeTokioRequired by the meshtastic crate
Device protocolmeshtastic v0.1.8Official Rust crate for Meshtastic protocol
CLI parsingclap (derive)Ergonomic, zero-boilerplate argument definitions
Shell completionsclap_completeGenerate shell completions from clap definitions
Error handlingthiserror / anyhowTyped errors in libraries, ergonomic in binaries
Serial I/Otokio-serialAsync serial port support
Terminal outputcoloredReadable, colored CLI output
Terminal UIcrosstermTerminal manipulation for the live watch display
Serializationserde / serde_yamlYAML config export and import
JSON outputserde_jsonStructured JSON output for --json flag
Config filetoml / dirsPersistent CLI config file parsing and XDG paths
QR codesqrcodeQR code generation for terminal, PNG, and SVG
Image outputimagePNG image rendering for QR code export
MQTT clientrumqttcAsync MQTT client for the bridge command
Interactive REPLrustyline / shlexCommand history, line editing, and tab completion for shell

Note: The meshtastic crate (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 Command trait defined in commands/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

  1. Create a new file under src/commands/ (e.g., my_command.rs)
  2. Implement the Command trait
  3. Add mod my_command; to src/commands/mod.rs
  4. Add the CLI variant to src/cli.rs
  5. Add the match arm in the create_command() factory in src/commands/mod.rs
  6. 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.