module Meshtastic::SerialInterface

def self.authors

def self.authors
rt@0dayinc.com>

def self.connect(opts = {})

def self.connect(opts = {})
block_dev] ||= '/dev/ttyUSB0'
ck device: #{block_dev}" unless File.exist?(block_dev)
 ||= 115_200
data_bits] ||= 8
stop_bits] ||= 1
ity] ||= :none
o_sym
ity: #{opts[:parity]}" if parity.nil?
s}#{parity}#{stop_bits}"
.open(
_conn] = serial_conn
e_thread] = init_stdout_thread(
ial_conn,
thread] = init_stdout_thread(
ial_conn,
ial_obj: serial_obj)
MeshInterface.new
 => e
obj: serial_obj) unless serial_obj.nil?

def self.disconnect(opts = {})

)
serial_obj: 'required - serial_obj returned from #connect method'
serial_obj = Meshtastic.disconnect(
Supported Method Parameters::
def self.disconnect(opts = {})
:serial_obj]
 serial_obj[:console_thread]
erial_obj[:proto_thread]
rial_obj[:serial_conn]
terminate
rminate
se
 => e

def self.dump_stdout_data(opts = {})

def self.dump_stdout_data(opts = {})

roto console]
lid type: #{type}. Supported types are :proto or :console" unless valid_types.include?(type)
 { |proto_hash| yield proto_hash } if type == :proto
in.split("\n").each{ |line| yield line.force_encoding('UTF-8') } if type == :console
roto_data if type == :proto
onsole_data.join if type == :console
 => e

def self.flush_data(opts = {})

def self.flush_data(opts = {})

roto console]
lid type: #{type}. Supported types are :proto or :console" unless valid_types.include?(type)
r if type == :console
if type == :proto
 => e

def self.help

def self.help
elf}.connect(
l - mqtt host (default: mqtt.meshtastic.org)',
l - mqtt port (defaults: 1883)',
 - use TLS (default: false)',
ional - mqtt username (default: meshdev)',
ional - (default: large4cats)',
tional - client ID (default: random 4-byte hex string)',
ptional - keep alive interval (default: 15)',
optional - acknowledgement timeout (default: 30)'
device(
equired - serial_obj returned from #connect method'

equired serial_obj returned from #connect method',
ired - array of bytes OR string to write to serial device (e.g. [0x00, 0x41, 0x90, 0x00] OR \"\\x00\\x41\\c90\\x00\\r\\n\"'
self}.dump_stdout_data(
d - :proto or :console'
ta(
l - :console or :proto (default: nil)'
stdout(
d - :proto or :console',
onal - refresh interval (default: 3)',
onal - comma-delimited string(s) to include in message (default: nil)',
onal - comma-delimited string(s) to exclude in message (default: nil)',
e(
equired - serial_obj object returned from #connect method',
ptional - root topic (default: msh)',
nal - region e.g. 'US/VA', etc (default: US)',
 'optional - channel ID path e.g. '2/stat/#' (default: '2/e/LongFast/#')',
l - hash of :channel_id => psk key value pairs (default: { LongFast: 'AQ==' })',
 - quality of service (default: 0)',
l - JSON output (default: false)',
onal - comma-delimited string(s) to exclude in message (default: nil)',
onal - comma-delimited string(s) to include on in message (default: nil)',
'optional - include GPS metadata in output (default: false)'
t(
equired - serial_obj returned from #connect method',
d - From ID (String or Integer) (Default: \"!00000b0b\")',
- Destination ID (Default: \"!ffffffff\")',
al - topic to publish to (default: 'msh/US/2/e/LongFast/1')',
onal - channel (Default: 6)',
l - Text Message (Default: SYN)',
ional - Want Acknowledgement (Default: false)',
 'optional - Want Response (Default: false)',
tional - Hop Limit (Default: 3)',
optional - Callback on Response',
l - hash of :channel => psk key value pairs (default: { LongFast: 'AQ==' })'
elf}.disconnect(
equired - serial_obj object returned from #connect method'

def self.init_stdout_thread(opts = {})

def self.init_stdout_thread(opts = {})
:serial_conn]
oto console]
id type: #{type}. Supported types are :proto or :console" unless valid_types.include?(type)
obj console_thread
d_timeout = -1

tastic::FromRadio.new
t_readable
s into @console_data,
adable bytes if need-be
from_radio.to_h if type == :proto
< serial_conn.readchar.force_encoding('UTF-8') if type == :console
=> e

def self.monitor_stdout(opts = {})

def self.monitor_stdout(opts = {})
:serial_obj]

roto console]
lid type: #{type}. Supported types are :proto or :console" unless valid_types.include?(type)
fresh] ||= 3
clude]
clude]
clude.to_s.split(',').map(&:strip)
clude.to_s.split(',').map(&:strip)
(type: type) do |data|
 exclude_arr.none? { |exclude| data.include?(exclude) } && (
   include_arr.empty? ||
   include_arr.all? { |include| data.include?(include) }
 )
isp
e: type)
ected. Breaking out of console mode..."
obj: serial_obj) unless serial_obj.nil?
 => e
obj: serial_obj) unless serial_obj.nil?

def self.request(opts = {})

def self.request(opts = {})
:serial_obj]
al_obj[:serial_conn]
yload]
 if payload.instance_of?(Array)
.chars if payload.instance_of?(String)
lid payload type: #{payload.class}" if byte_arr.nil?
byte|
(byte)
 => e
obj: serial_obj) unless serial_obj.nil?

def self.send_text(opts = {})

)
psks: 'optional - hash of :channel_id => psk key value pairs (default: { LongFast: "AQ==" })'
on_response: 'optional - Callback on Response',
hop_limit: 'optional - Hop Limit (Default: 3)',
want_response: 'optional - Want Response (Default: false)',
want_ack: 'optional - Want Acknowledgement (Default: false)',
text: 'optional - Text Message (Default: SYN)',
channel: 'optional - channel (Default: 6)',
topic: 'optional - topic to publish to (Default: "msh/US/2/e/LongFast/1")',
to: 'optional - Destination ID (Default: "!ffffffff")',
from: 'required - From ID (String or Integer) (Default: "!00000b0b")',
serial_obj: 'required - serial_obj returned from #connect method',
Meshtastic::SerialInterface.send_text(
Supported Method Parameters::
def self.send_text(opts = {})
s[:serial_obj]
pic] ||= 'msh/US/2/e/LongFast/#'
o
chunked message to deal with large messages
MeshInterface.new
)
ivalent of publish
sh(topic, protobuf_text)
 => e
nnect(serial_obj: serial_obj) unless serial_obj.nil?

def self.subscribe(opts = {})

def self.subscribe(opts = {})
:serial_obj]
:root_topic] ||= 'msh'
ion] ||= 'US'
ts[:channel_topic] ||= '2/e/LongFast/#'
ray of PSKs and attempt each until decrypted
OiApB1nwvP+rz05pAQ=='
 ||= { LongFast: public_psk }
 parameter must be a hash of :channel_id => psk key value pairs' unless psks.is_a?(Hash)
public_psk if psks[:LongFast] == 'AQ=='
MeshInterface.new
her_keys(psks: psks)
|= 0
 ||= false
clude]
clude]
s[:gps_metadata] ||= false
[:include_raw] ||= false
xplorer for topic discovery
ot_topic}/#{region}/#{channel_topic}"
ot_topic}/#{region}" if region == '#'
to: #{full_topic}"
be(full_topic, qos)
ception: No Ping Response received for 23 seconds (MQTT::ProtocolException)
ude.to_s.split(',').map(&:strip)
ude.to_s.split(',').map(&:strip)
ket do |packet_bytes|
ket_bytes.to_s if include_raw
et_bytes.topic ||= ''
cket_bytes.payload ||= ''
d_hash = {}
 = ''
oad_hash = JSON.parse(raw_payload, symbolize_names: true)
yload = Meshtastic::ToRadio.decode(raw_payload)
oad = Meshtastic::FromRadio.decode(raw_payload)
oad_hash = decoded_payload.to_h
coded_payload_hash[:packet].is_a?(Hash)
ded_payload_hash[:packet] if decoded_payload_hash.keys.include?(:packet)
] = raw_topic
id_from] = "!#{message[:from].to_i.to_s(16)}"
id_to] = "!#{message[:to].to_i.to_s(16)}"
s.include?(:rx_time)
= message[:rx_time]
nt.is_a?(Integer)
c = Time.at(rx_time_int).utc.to_s
x_time_utc] = rx_time_utc
s.include?(:public_key)
ey = message[:public_key]
lic_key] = Base64.strict_encode64(raw_public_key)
_message is not nil, then decrypt
prior to decoding.
age = message[:encrypted]
essage.to_s.length.positive? &&
pic]
[:pki_encrypted]
Display Decrypted PKI Message
ey = message[:public_key]
ic_key = Base64.strict_decode64(public_key)
message[:id]
= message[:from]
_id = [packet_id].pack('V').ljust(8, "\x00")
ode = [packet_from].pack('V').ljust(8, "\x00")
once_packet_id}#{nonce_from_node}"
LongFast]
el = message[:topic].split('/')[-2].to_sym
arget_channel] if psks.keys.include?(target_channel)
se64.strict_decode64(psk)
nSSL::Cipher.new('AES-128-CTR')
nSSL::Cipher.new('AES-256-CTR') if dec_psk.length == 32
pt
 dec_psk
nonce
cipher.update(encrypted_message) + cipher.final
oded] = Meshtastic::Data.decode(decrypted).to_h
rypted] = :decrypted
coded]
Meshtastic::Data.decode(message[:decoded][:payload]).to_h
ssage[:decoded][:payload]
essage[:decoded][:portnum]
stic::MeshInterface.new
oded][:payload] = mui.decode_payload(
ayload,
msg_type,
ta: gps_metadata
acket] = raw_packet if include_raw
d_hash[:packet] = message
iven?
out] = 'pretty'
ge = JSON.pretty_generate(decoded_payload_hash)
:CompatibilityError,
rotobuf::ParseError,
eratorError,
rror => e
(Encoding::CompatibilityError)
rypted] = e.message if e.message.include?('key must be')
rypted] = 'unable to decrypt - psk?' if e.message.include?('occurred during parsing')
oad_hash[:packet] = message
_given?
ING: #{e.inspect} - MSG IS >>>"
acktrace
tdout] = 'inspect'
sage = decoded_payload_hash.inspect
[message[:id].to_s] if include_arr.empty?
a?(Hash)
 = message.values.join(' ')
if exclude_arr.none? { |exclude| flat_message.include?(exclude) } && (
     include_arr.first == message[:id] ||
     include_arr.all? { |include| flat_message.include?(include) }
   )
iven?
coded_payload_hash
"
 * 80
G:'
out_message
 * 80
\n\n"
'
ected. Exiting..."
nnect(serial_obj: serial_obj) unless serial_obj.nil?
 => e
nnect(serial_obj: serial_obj) unless serial_obj.nil?

def self.wake_up_device(opts = {})

)
serial_obj: 'required - serial_obj returned from #connect method'
wake_up_device(
Supported Method Parameters::
def self.wake_up_device(opts = {})
:serial_obj]
[START2].pack('C') * 32
: serial_obj, payload: start2_byte_arr)
 => e
obj: serial_obj) unless serial_obj.nil?