Meshtastic

Ruby gem for interfacing with Meshtastic nodes / network.

Setting Expectations

This gem was created to support alt-comm capabilities w/in a security research framework known as PWN. Contributors of this effort cannot guarantee full functionality or support for all Meshtastic features.

Objectives

  • Consume the latest Meshtastic Protobof Specs and auto-generate Ruby protobuf modules for Meshtastic using the grpc_tools_ruby_protoc command: Complete
  • Integrate auto-generated Ruby protobuf modules into a working Ruby gem: Complete
  • Scale out Meshtastic Ruby Modules for their respective protobufs within the meshtastic gem (e.g. Meshtastic::MQTTPB is auto-generated based on latest Meshtastic protobuf specs and extended via Meshtastic::MQTT for more MQTT interaction as desired): Ongoing Effort

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add meshtastic

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install meshtastic

Usage

The primary interaction modules today are Meshtastic::MQTT (broker) and Meshtastic::SerialInterface (USB/UART). Examples for each follow.

MQTT

To view MQTT messages, and include only messages containing _APP and LongFast strings, use the following code:

require 'meshtastic'
Meshtastic::MQTT.help
mqtt_obj = Meshtastic::MQTT.connect
puts mqtt_obj.inspect
Meshtastic::MQTT.subscribe(
  mqtt_obj: mqtt_obj,
  include: '_APP, LongFast'
)

This code will dump the contents of every message:

require 'meshtastic'
mqtt_obj = Meshtastic::MQTT.connect
Meshtastic::MQTT.subscribe(
  mqtt_obj: mqtt_obj,
  root_topic: 'msh',
  region: 'US',
  topic: '2/e/LongFast/#',
  psks: { LongFast: 'AQ==' }
) do |message|
  puts message.inspect
end

Sending a message over MQTT:

require 'meshtastic'
mqtt_obj = Meshtastic::MQTT.connect
client_id = "!#{mqtt_obj.client_id}"
Meshtastic::MQTT.send_text(
  mqtt_obj: mqtt_obj,
  from: client_id,
  to: '!ffffffff',
  root_topic: 'msh',
  region: 'US',
  topic: '2/e/LongFast/#',
  channel: 93,
  text: 'Hello, World!',
  psks: { LongFast: 'AQ==' }
)

One of the "gotchas" when sending messages is ensuring you're sending over the proper integer for the channel parameter. The best way to determine the proper channel value is by sending a test message from within the meshtastic app and then viewing the MQTT message similar to the following:

require 'meshtastic'
mqtt_obj = Meshtastic::MQTT.connect
Meshtastic::MQTT.subscribe(
  mqtt_obj: mqtt_obj,
  root_topic: 'msh',
  region: 'US',
  topic: '2/e/LongFast/#',
  psks: { LongFast: 'AQ==' },
  include: '!YOUR_CLIENT_ID'
) do |message|
  puts message.inspect
end

You should see something like this:

{packet: {from: 4080917205, to: 4294967295, channel: 93, id: 1198634591, rx_time: 1738614021, rx_snr: 0.0, hop_limit: 3, want_ack: false, priority: :HIGH, rx_rssi: 0, delayed: :NO_DELAY, via_mqtt: false, hop_start: 3, public_key: "", pki_encrypted: false, next_hop: 0, relay_node: 0, tx_after: 0, decoded: {portnum: :TEXT_MESSAGE_APP, payload: "WHAT IS MY channel VALUE?", want_response: false, dest: 0, source: 0, request_id: 0, reply_id: 0, emoji: 0, bitfield: 0}, encrypted: :decrypted, topic: "msh/US/2/e/LongFast/!f33ddad5", node_id_from: "!f33ddad5", node_id_to: "!ffffffff", rx_time_utc: "2025-01-01 07:00:00 UTC"}, channel_id: "LongFast", gateway_id: "!f33ddad5"}

Note where is says channel: 93. This is the channel value required to send messages in this particular example.

Serial Interface

Talk directly to a Meshtastic node over USB/UART (/dev/ttyUSB*, /dev/ttyACM*). Unlike MQTT, the radio owns channel crypto for serial: payloads are sent decoded and the device encrypts with its configured channel key.

To inspect available methods and open a serial session:

require 'meshtastic'
Meshtastic::SerialInterface.help
serial_obj = Meshtastic::SerialInterface.connect(
  block_dev: '/dev/ttyUSB0', # or /dev/ttyACM0
  baud: 115_200
)
puts serial_obj.inspect

This code will dump every FromRadio packet (blocks until CTRL+C):

require 'meshtastic'
serial_obj = Meshtastic::SerialInterface.connect(
  block_dev: '/dev/ttyUSB0',
  baud: 115_200
)
Meshtastic::SerialInterface.subscribe(
  serial_obj: serial_obj
) do |message|
  puts message.inspect
end

Filter with include / exclude (comma-delimited substrings), same idea as MQTT:

require 'meshtastic'
serial_obj = Meshtastic::SerialInterface.connect(block_dev: '/dev/ttyUSB0')
Meshtastic::SerialInterface.subscribe(
  serial_obj: serial_obj,
  include: 'TEXT_MESSAGE_APP',
  exclude: 'TELEMETRY_APP'
) do |message|
  puts message.inspect
end

Sending a message over serial:

require 'meshtastic'
serial_obj = Meshtastic::SerialInterface.connect(
  block_dev: '/dev/ttyUSB0',
  baud: 115_200
)
Meshtastic::SerialInterface.send_text(
  serial_obj: serial_obj,
  to: '!ffffffff',   # broadcast; or a node id like '!f33ddad5'
  channel: 0,        # primary channel index on the radio
  text: 'Hello over serial!'
)
Meshtastic::SerialInterface.disconnect(serial_obj: serial_obj)

A typical end-to-end send + receive session looks like this (connect once, send, then subscribe):

require 'meshtastic'

serial_obj = Meshtastic::SerialInterface.connect(
  block_dev: '/dev/ttyUSB0',
  baud: 115_200
)

# Optional: give the device a moment to finish want_config / my_info
sleep 2
puts "local node: !#{serial_obj[:my_node_num].to_s(16)}" if serial_obj[:my_node_num]

Meshtastic::SerialInterface.send_text(
  serial_obj: serial_obj,
  to: '!ffffffff',
  channel: 0,
  text: 'Hello over serial!'
)

# Blocks; CTRL+C disconnects cleanly
Meshtastic::SerialInterface.subscribe(serial_obj: serial_obj) do |message|
  puts message.inspect
end

Non-blocking receive helpers (useful when you drive the loop yourself):

# Pop one framed FromRadio (or nil on timeout)
fr = Meshtastic::SerialInterface.recv_from_radio(timeout: 2)
puts fr&.to_h

# Drain whatever is already queued
Meshtastic::SerialInterface.drain_from_radio.each { |fr| puts fr.to_h }

# Debug console / proto dumps collected by the RX thread
puts Meshtastic::SerialInterface.dump_stdout_data(type: :console)
puts Meshtastic::SerialInterface.dump_stdout_data(type: :proto).size

Note: Serial path leaves mesh packets decoded and lets the radio encrypt with its configured channel key. The MQTT path still pre-encrypts with the PSK you supply via psks:. Channel index (channel:) on serial is the index configured on the device, not the MQTT channel hash.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/0dayinc/meshtastic. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

Code of Conduct

Everyone interacting in the Meshtastic project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.