PRE-RELEASE β v0.1.1 beta. Core functionality is working. Some features are incomplete and rough edges remain. Feedback and bug reports welcome via GitHub Issues.
MeshTTY is a terminal-based (TUI) client for Meshtastic LoRa mesh radio networks. It runs on a Raspberry Pi or any Linux/macOS system with a terminal. Built with the Textual framework, it connects to a Meshtastic node over USB/serial, TCP/WiFi, or Bluetooth (BLE) and renders a classic 80Γ24 box-drawing UI that works in any terminal β no desktop environment, GPU, or X server required.
- Installation
- 1.1 Raspberry Pi and DietPi
- 1.2 macOS and Ubuntu
- 1.3 Manual install
- Updating
- Running MeshTTY
- Command-Line Flags
- Connection Screen
- Main Screen
- 6.1 Messages Tab
- 6.2 Channels Tab
- 6.3 Nodes Tab
- 6.4 Node Detail Modal
- 6.5 Settings Tab
- 6.6 DM Slash Commands
- Keyboard Shortcuts
- Configuration File
- Message & Node Database
- Themes
- Debug Logging
- Serial Port Permissions (Linux)
- Known Issues & Missing Features
- Troubleshooting
- Headless / Kiosk Operation (Raspberry Pi)
git clone https://github.com/kendelmccarrey/MeshTTY.git
cd MeshTTY
bash install-pi.shSupported hardware:
| Hardware | OS | Notes |
|---|---|---|
| Pi Zero W | Raspberry Pi OS Lite, DietPi 32-bit | ARMv6 β install takes 30β90 min |
| Pi Zero 2 W | Raspberry Pi OS Lite, DietPi 32-bit | ARMv7 |
| Pi 3B / 3B+ | Raspberry Pi OS Lite / Desktop | Full support |
| Pi 4B | Raspberry Pi OS Lite / Desktop | Full support |
| Pi 5 | Raspberry Pi OS Lite / Desktop | Full support |
The installer will:
- Install system packages (
python3-full,python3-venv,bluez,fonts-terminus, etc.) - Create a Python virtualenv at
~/.venv/meshtty(using--copiesto avoid PEP 668 issues on Bookworm/Trixie) - Handle ARMv6 pip failures gracefully (grpcio wheel incompatibility on Pi Zero W)
- Add your user to the
dialout,bluetooth, andvideogroups - Generate
meshtty.shand makelaunch-pi.shexecutable - Optionally configure tty1 auto-launch for kiosk operation
- Optionally enable
loginctl enable-lingerfor user systemd services
Log out and back in after the install for group membership to take effect.
git clone https://github.com/kendelmccarrey/MeshTTY.git
cd MeshTTY
bash install.shmacOS prerequisite: Homebrew must be installed first.
The installer handles system packages, virtualenv creation, and generates meshtty.sh.
python3 -m venv ~/.venv/meshtty
source ~/.venv/meshtty/bin/activate
pip install -e .Dependencies installed automatically:
| Package | Purpose |
|---|---|
| textual β₯ 0.80 | TUI framework (rendering, widgets, key bindings) |
| meshtastic | Meshtastic Python library (serial/TCP/BLE) |
| bleak | Bluetooth BLE scanning |
| pyserial | Serial port enumeration and communication |
| pypubsub | Event pub/sub (used internally by meshtastic) |
| protobuf | Meshtastic protocol buffer serialization |
Python 3.11 or newer is required.
bash update.shupdate.sh will:
- Warn if there are uncommitted local changes before pulling
- Run
git pull --ff-only - Print a changelog of commits since the last update
- Re-run
pip installonly ifrequirements.txtchanged - Re-install the local package to pick up source changes
- Ensure all launch scripts are executable
| Script | Use when |
|---|---|
./launch-pi.sh |
Recommended. Physical screen β loads large Terminus font so 80 columns fills the display. Also works over SSH (skips font load). |
./meshtty.sh |
Any terminal β launches directly with no font adjustment. |
./launch-pi.sh
./launch-pi.sh --bot --log # pass flags throughlaunch-pi.sh detects whether it is running on the Linux framebuffer console ($TERM=linux, not SSH, not X). On the physical screen it reads the framebuffer resolution and loads the largest Terminus font that still fits at least 80 columns, so the UI fills whatever display is connected at a comfortable size. It then sets TERM=xterm-256color before launching MeshTTY.
./launch.sh # thin wrapper around meshtty.sh
./meshtty.sh # direct launcher
python -m meshtty.main # using active virtualenvThe app starts on the Connection Screen. Once connected it switches automatically to the Main Screen.
| Flag | Description |
|---|---|
--debug |
Enable DEBUG-level logging to /tmp/meshtty.log. |
--bot |
Enable the DM slash-command bot (see section 6.6). |
--log |
Log all inbound and outbound messages to /tmp/meshtty-messages.log. |
--noargs |
Clear saved startup flags and launch with no flags active. |
-h |
Print help and exit. |
Flags are persisted across reboots. The last set of flags is saved to ~/.config/meshtty/last_flags and replayed on the next launch unless new flags are passed explicitly or --noargs is used.
The first screen shown on launch. Choose how to connect to your radio node.
- The app scans for serial ports matching common Meshtastic USB chips and lists them in a table.
- Click a row or type the port path manually (e.g.
/dev/ttyUSB0,/dev/ttyACM0). - Press Enter or click CONNECT.
- Enter the hostname or IP address of the node and the port (default 4403).
- Press Enter or click CONNECT.
- Click SCAN FOR BLE DEVICES to perform a 5-second scan.
- Click a row or enter a MAC address manually.
- Click CONNECT.
The Remember this device switch (on by default) saves connection details so the same transport and address are pre-filled on the next launch.
If a device was remembered from a previous session, the connection screen starts a 5-second countdown and connects automatically. Press any key, switch tabs, or click a button to cancel.
Progress is shown in a status line: Connectingβ¦, Connected β downloading nodesβ¦, Download completeβ¦, Connected! (N nodes loaded). Errors appear in red.
After a successful connection the app switches to the Main Screen, which has four tabs. The status bar at the top shows connection state and node count.
A unified, scrollable message history for all channels and direct messages, plus a compose bar at the bottom.
HH:MM ChannelName/SenderName: message text β channel broadcast
HH:MM SenderName: message text β direct message (indented)
- Channel messages show
ChannelName/SenderShortNameas the prefix (e.g.Primary/HAK5). - Direct messages show just the sender's short node name.
- Outgoing messages are indented two spaces and displayed in accent colour.
- Long lines wrap aligned under the message text.
- The 200 most recent messages are loaded from the local database on startup.
- Node short names are resolved from the local database so names appear correctly even after reconnect.
The bottom row is split into two focus areas:
[ Channel ][ type your message here⦠][ SEND ]
- Left area (12 chars) β shows the current channel name or DM node short name. Tab to focus it, then use Up/Down to cycle through conversations sorted by most recent message. Press Enter to advance to the message input.
- Right area β type your message and press Enter or click SEND.
Routing:
- If the selected prefix matches a channel name β broadcasts on that channel.
- If the selected prefix matches a node short name β sends as a direct message to that node.
- Up/Down β scroll one line when the message history or compose input has focus.
- PageUp/PageDown β scroll one screen from any focus.
Lists all channels configured on the radio. Updates when the connection is established.
Live table of all mesh nodes heard on the network.
| Column | Description |
|---|---|
| Short | 4-character callsign |
| Long Name | Full node name |
| SNR | Last signal-to-noise ratio (dB) |
| Last Heard | Time of last packet (HH:MM:SS local) |
| Battery | Battery level (%) |
| Position | GPS coordinates (lat, lon) |
| HW Model | Hardware model string |
Ctrl+R forces a refresh. Click any row to open the Node Detail Modal.
Shows node ID, short/long name, hardware model, last SNR, last heard, battery level, and GPS position. Close with CLOSE, Escape, or Q.
Configure transport, display, and messaging defaults. Shows current connection status and a Disconnect button. Press Tab / Shift+Tab to move between fields. Use Space, Enter, β/β on option fields to cycle through choices. Press Save to write changes to disk. The Theme setting takes effect immediately.
Enable with --bot. Incoming DMs starting with / trigger automatic replies:
| Command | Response |
|---|---|
/HELP |
Lists all available commands |
/INFO |
Returns the MeshTTY repository URL |
/JOKE |
Returns the next joke from the joke file |
/GPIO |
Returns exported GPIO pin states |
/NULL |
Returns "All is nothingness" |
Commands are case-insensitive. /JOKE requires meshtty/data/shortjokes.csv (not included β compatible file at Kaggle Short Jokes dataset).
| Key | Action |
|---|---|
| Enter | Connect (from any input field) |
| Ctrl+Q | Quit |
| Key | Action |
|---|---|
| F1 | Help overlay |
| Ctrl+T | Cycle to next tab (Messages β Channels β Nodes β Settings β wrap) |
| Ctrl+R | Refresh node table |
| Ctrl+D | Disconnect β Connection Screen |
| Ctrl+Q | Quit |
| Key | Focus | Action |
|---|---|---|
| Tab | anywhere | Cycle focus: history β channel β input β wrap |
| Shift+Tab | anywhere | Reverse focus cycle |
| Up / Down | channel selector | Cycle through channels and DM nodes |
| Enter | channel selector | Advance focus to message input |
| Up / Down | history or input | Scroll message history |
| PageUp/PageDown | anywhere | Scroll message history |
| Enter | message input | Send message |
| Key | Action |
|---|---|
| Escape | Close |
| Q | Close |
Location: ~/.config/meshtty/config.json
Created automatically on first run.
{
"default_transport": "serial",
"last_serial_port": "/dev/ttyUSB0",
"last_tcp_host": "",
"last_tcp_port": 4403,
"last_ble_address": "",
"auto_connect": true,
"log_level": "WARNING",
"db_path": "~/.config/meshtty/messages.db",
"default_channel": 0,
"node_short_name_display": true,
"theme": "crt-amber"
}| Key | Type | Default | Description |
|---|---|---|---|
default_transport |
string | "serial" |
"serial", "tcp", or "ble" |
last_serial_port |
string | "" |
Serial device path |
last_tcp_host |
string | "" |
TCP hostname or IP |
last_tcp_port |
integer | 4403 |
TCP port |
last_ble_address |
string | "" |
BLE MAC address |
auto_connect |
boolean | true |
Start countdown if device remembered |
log_level |
string | "WARNING" |
DEBUG, INFO, WARNING, ERROR |
db_path |
string | (see above) | SQLite database path |
default_channel |
integer | 0 |
Default channel index (0β7) |
node_short_name_display |
boolean | true |
Use short names in Nodes table |
theme |
string | "crt-amber" |
UI theme (see section 10) |
Location: ~/.config/meshtty/messages.db
SQLite database created automatically.
| Column | Type | Description |
|---|---|---|
id |
INTEGER | Auto-increment primary key |
packet_id |
TEXT | Meshtastic packet ID (NULL for sent messages) |
from_id |
TEXT | Sender node ID (e.g. !abcd1234) |
to_id |
TEXT | Recipient node ID or ^all |
channel |
INTEGER | Channel index (0β7) |
text |
TEXT | Message text |
rx_time |
INTEGER | Unix timestamp (seconds) |
is_mine |
INTEGER | 1 = sent, 0 = received |
display_prefix |
TEXT | Sender short name at time of receipt |
| Column | Type | Description |
|---|---|---|
node_id |
TEXT | Primary key |
short_name |
TEXT | 4-character callsign |
long_name |
TEXT | Full node name |
hw_model |
TEXT | Hardware model string |
last_snr |
REAL | Last SNR (dB) |
last_lat |
REAL | Last latitude |
last_lon |
REAL | Last longitude |
last_alt |
INTEGER | Last altitude (metres) |
battery |
INTEGER | Battery level (%) |
last_heard |
INTEGER | Unix timestamp of last packet |
updated_at |
INTEGER | Unix timestamp of last database write |
-- All messages, newest first
SELECT datetime(rx_time,'unixepoch','localtime') AS time, display_prefix, text
FROM messages ORDER BY rx_time DESC LIMIT 50;
-- All known nodes
SELECT node_id, short_name, long_name, battery, last_snr FROM nodes;Three built-in themes selectable from Settings β Theme. Each uses a phosphor-inspired colour palette designed to look good in any terminal.
| Config value | Label | Appearance |
|---|---|---|
crt-amber |
Amber | Warm #ff8100 amber on black. Default. |
crt-phosphor |
Green Phosphor | Classic #0ccc68 green on black |
crt-ibm |
IBM / Grey | Cool #c0c0c0 grey on black |
The Theme setting in the Settings tab takes effect immediately without restarting.
./launch-pi.sh --debug # Pi
./launch.sh --debug # macOS / Ubuntu
tail -f /tmp/meshtty.log| Error | Likely cause |
|---|---|
PermissionError: [Errno 13] ... '/dev/ttyUSB0' |
Not in dialout group β see section 12 |
serial.serialutil.SerialException: could not open port |
Wrong port or device not plugged in |
ConnectionRefusedError |
TCP host/port wrong or node unreachable |
_waitConnected timed out but N nodes present |
Noisy serial stream β transport forces connection and proceeds |
sudo usermod -aG dialout $USERLog out and back in. Verify with groups β output should include dialout.
Both install.sh and install-pi.sh handle this automatically.
No group membership required. Serial devices appear as /dev/cu.usbserial-XXXX or /dev/cu.SLAB_USBtoUART. Run ls /dev/cu.* with the radio plugged in.
- Messages stored before the
display_prefixcolumn was added fall back to displaying the rawfrom_idnode string. - BLE transport is largely untested.
- Message acknowledgement display
- Persistent compose prefix across sessions
- Node position map view
- Channel creation / management
- Firmware version / radio config display
- New message notification on non-active tab
Run with --debug and watch the log. Timeouts like _waitConnected timed out but N nodes present are usually harmless β the transport forces a connection and node records continue arriving. If completely stuck, quit (Ctrl+Q) and reconnect.
- Run with
--debug, check/tmp/meshtty.log - Confirm
dialoutgroup membership (section 12) - Unplug and re-plug the USB cable
- Test with the CLI:
meshtastic --port /dev/ttyUSB0 --info
The scanner filters by USB vendor ID. Supported chips:
| Chip | USB VID |
|---|---|
| Silicon Labs CP210x | 10C4 |
| WCH CH340 / CH341 | 1A86 |
| FTDI | 0403 |
| Espressif USB-JTAG | 303A |
If your adapter uses a different chip, enter the port path manually.
The Pi Zero W's combined Wi-Fi/BT chip (CYW43438) can cause interference. If BLE scanning fails:
- Ensure BlueZ is version 5.55 or newer:
bluetoothctl --version - Try disabling Wi-Fi:
rfkill block wifi - Use serial or TCP transport instead β more reliable on Zero hardware.
Expected install time is 30β90 minutes. Do not interrupt pip once it starts.
If pip fails with a grpcio or "illegal instruction" error, install-pi.sh retries without grpcio automatically (meshtastic β₯ 2.5 no longer requires it).
dietpi-config β Performance Options β Swap
Set to 1024 MB for the install, then reduce back to 256 MB afterward.
python3 -m json.tool ~/.config/meshtty/config.jsonIf "theme" contains an unrecognised value, the app silently resets it to crt-amber.
MeshTTY runs as a full-screen terminal application on a headless Pi with a physical display β no desktop environment, X server, or GPU required.
- Configure console auto-login:
- Raspberry Pi OS:
sudo raspi-configβ System Options β Boot / Auto Login β Console Autologin - DietPi:
dietpi-configβ AutoStart Options β set to0(console autologin)
- Raspberry Pi OS:
- Run
install-pi.shand answer Y when asked about auto-launch on tty1.
The installer adds a block to ~/.bash_profile:
# MeshTTY auto-launch on tty1 (physical Pi screen)
if [[ "$(tty)" == "/dev/tty1" ]]; then
while true; do
/path/to/MeshTTY/launch-pi.sh
sleep 2
done
fi| Step | What happens |
|---|---|
| Pi boots, user auto-logs in on tty1 | ~/.bash_profile starts restart loop |
launch-pi.sh runs |
Loads large Terminus font for framebuffer scaling |
Sets TERM=xterm-256color |
Ensures Textual renders correctly |
Launches meshtty.sh |
MeshTTY starts, connection screen appears |
| App exits for any reason | sleep 2, then restarts |
launch-pi.sh skips the font load automatically when running over SSH or inside X/Wayland.
SSH in to investigate without disturbing the console:
ssh user@<pi-hostname>
tail -f /tmp/meshtty.log| Symptom | Likely cause | Fix |
|---|---|---|
| Blank screen / nothing starts | User not in dialout group |
sudo usermod -aG dialout $USER then reboot |
| "Waiting for USB serial deviceβ¦" then fails | Radio not plugged in | Plug in before booting; check dmesg | grep tty |
| Auto-connect does not fire | No saved device | Connect once manually with Remember this device checked |
| Text too small on physical screen | Terminus font not installed | sudo apt install fonts-terminus |
| App crashes immediately | TERM not set |
Ensure TERM=xterm-256color is exported before launch |