lib/meshtastic.rb in meshtastic-0.0.42 vs lib/meshtastic.rb in meshtastic-0.0.67
- old
+ new
@@ -1,9 +1,10 @@
# frozen_string_literal: true
# Plugin used to interact with Meshtastic nodes
module Meshtastic
+ require 'base64'
# Protocol Buffers for Meshtastic
require 'meshtastic/admin_pb'
require 'nanopb_pb'
require 'meshtastic/apponly_pb'
require 'meshtastic/atak_pb'
@@ -23,10 +24,11 @@
require 'meshtastic/rtttl_pb'
require 'meshtastic/storeforward_pb'
require 'meshtastic/telemetry_pb'
require 'meshtastic/version'
require 'meshtastic/xmodem_pb'
+ require 'openssl'
autoload :Admin, 'meshtastic/admin'
autoload :Apponly, 'meshtastic/apponly'
autoload :ATAK, 'meshtastic/atak'
autoload :Cannedmessages, 'meshtastic/cannedmessages'
@@ -44,9 +46,278 @@
autoload :RemoteHardware, 'meshtastic/remote_hardware'
autoload :RTTTL, 'meshtastic/rtttl'
autoload :Storeforward, 'meshtastic/storeforward'
autoload :Telemetry, 'meshtastic/telemetry'
autoload :Xmodem, 'meshtastic/xmodem'
+
+ # Supported Method Parameters::
+ # Meshtastic.send_text(
+ # from: 'required - From ID (String or Integer)',
+ # to: 'optional - Destination ID (Default: 0xFFFFFFFF)',
+ # last_packet_id: 'optional - Last Packet ID (Default: 0)',
+ # via: 'optional - :radio || :mqtt (Default: :radio)',
+ # channel: 'optional - Channel ID (Default: 6)',
+ # text: 'optional - Text Message (Default: SYN)',
+ # want_ack: 'optional - Want Acknowledgement (Default: false)',
+ # want_response: 'optional - Want Response (Default: false)',
+ # hop_limit: 'optional - Hop Limit (Default: 3)',
+ # on_response: 'optional - Callback on Response',
+ # psks: 'optional - hash of :channel => psk key value pairs (default: { LongFast: "AQ==" })'
+ # )
+ public_class_method def self.send_text(opts = {})
+ # Send a text message to a node
+ from = opts[:from]
+ from_hex = from.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if from.is_a?(String)
+ from = from_hex.to_i(16) if from_hex
+ raise 'ERROR: from parameter is required.' unless from
+
+ to = opts[:to] ||= 0xFFFFFFFF
+ to_hex = to.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if to.is_a?(String)
+ to = to_hex.to_i(16) if to_hex
+
+ last_packet_id = opts[:last_packet_id] ||= 0
+ via = opts[:via] ||= :radio
+ channel = opts[:channel] ||= 6
+ text = opts[:text] ||= 'SYN'
+ want_ack = opts[:want_ack] ||= false
+ want_response = opts[:want_response] ||= false
+ hop_limit = opts[:hop_limit] ||= 3
+ on_response = opts[:on_response]
+
+ public_psk = '1PG7OiApB1nwvP+rz05pAQ=='
+ psks = opts[:psks] ||= { LongFast: public_psk }
+ raise 'ERROR: psks parameter must be a hash of :channel => psk key value pairs' unless psks.is_a?(Hash)
+
+ psks[:LongFast] = public_psk if psks[:LongFast] == 'AQ=='
+
+ # TODO: verify text length validity
+ max_txt_len = Meshtastic::Constants::DATA_PAYLOAD_LEN
+ raise "ERROR: Text Length > #{max_txt_len} Bytes" if text.length > max_txt_len
+
+ port_num = Meshtastic::PortNum::TEXT_MESSAGE_APP
+
+ data = Meshtastic::Data.new
+ data.payload = text.force_encoding('ASCII-8BIT')
+ data.portnum = port_num
+ data.want_response = want_response
+ # puts data.to_h
+
+ send_data(
+ from: from,
+ to: to,
+ last_packet_id: last_packet_id,
+ via: via,
+ channel: channel,
+ data: data,
+ want_ack: want_ack,
+ want_response: want_response,
+ hop_limit: hop_limit,
+ port_num: port_num,
+ on_response: on_response,
+ psks: psks
+ )
+ rescue StandardError => e
+ raise e
+ end
+
+ # Supported Method Parameters::
+ # Meshtastic.send_data(
+ # from: 'required - From ID (String or Integer)',
+ # to: 'optional - Destination ID (Default: 0xFFFFFFFF)',
+ # last_packet_id: 'optional - Last Packet ID (Default: 0)',
+ # via: 'optional - :radio || :mqtt (Default: :radio)',
+ # channel: 'optional - Channel ID (Default: 0)',
+ # data: 'required - Data to Send',
+ # want_ack: 'optional - Want Acknowledgement (Default: false)',
+ # hop_limit: 'optional - Hop Limit (Default: 3)',
+ # port_num: 'optional - (Default: Meshtastic::PortNum::PRIVATE_APP)',
+ # psks: 'optional - hash of :channel => psk key value pairs (default: { LongFast: "AQ==" })'
+ # )
+ public_class_method def self.send_data(opts = {})
+ # Send a text message to a node
+ from = opts[:from]
+ from_hex = from.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if from.is_a?(String)
+ from = from_hex.to_i(16) if from_hex
+ raise 'ERROR: from parameter is required.' unless from
+
+ to = opts[:to] ||= 0xFFFFFFFF
+ to_hex = to.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if to.is_a?(String)
+ to = to_hex.to_i(16) if to_hex
+
+ last_packet_id = opts[:last_packet_id] ||= 0
+ via = opts[:via] ||= :radio
+ channel = opts[:channel] ||= 0
+ data = opts[:data]
+ want_ack = opts[:want_ack] ||= false
+ hop_limit = opts[:hop_limit] ||= 3
+ port_num = opts[:port_num] ||= Meshtastic::PortNum::PRIVATE_APP
+ max_port_num = Meshtastic::PortNum::MAX
+ raise "ERROR: Invalid port_num" unless port_num.positive? && port_num < max_port_num
+
+ public_psk = '1PG7OiApB1nwvP+rz05pAQ=='
+ psks = opts[:psks] ||= { LongFast: public_psk }
+ raise 'ERROR: psks parameter must be a hash of :channel => psk key value pairs' unless psks.is_a?(Hash)
+
+ psks[:LongFast] = public_psk if psks[:LongFast] == 'AQ=='
+
+ data_len = data.payload.length
+ max_len = Meshtastic::Constants::DATA_PAYLOAD_LEN
+ raise "ERROR: Data Length > #{max_len} Bytes" if data_len > max_len
+
+ mesh_packet = Meshtastic::MeshPacket.new
+ mesh_packet.decoded = data
+
+ send_packet(
+ mesh_packet: mesh_packet,
+ from: from,
+ to: to,
+ last_packet_id: last_packet_id,
+ via: via,
+ channel: channel,
+ want_ack: want_ack,
+ hop_limit: hop_limit,
+ psks: psks
+ )
+ rescue StandardError => e
+ raise e
+ end
+
+ # Supported Method Parameters::
+ # Meshtastic.send_packet(
+ # mesh_packet: 'required - Mesh Packet to Send',
+ # from: 'required - From ID (String or Integer)',
+ # to: 'optional - Destination ID (Default: 0xFFFFFFFF)',
+ # last_packet_id: 'optional - Last Packet ID (Default: 0)',
+ # via: 'optional - :radio || :mqtt (Default: :radio)',
+ # channel: 'optional - Channel ID (Default: 0)',
+ # want_ack: 'optional - Want Acknowledgement (Default: false)',
+ # hop_limit: 'optional - Hop Limit (Default: 3)',
+ # psks: 'optional - hash of :channel => psk key value pairs (default: { LongFast: "AQ==" })'
+ # )
+ public_class_method def self.send_packet(opts = {})
+ mesh_packet = opts[:mesh_packet]
+ from = opts[:from]
+ from_hex = from.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if from.is_a?(String)
+ from = from_hex.to_i(16) if from_hex
+ raise 'ERROR: from parameter is required.' unless from
+
+ to = opts[:to] ||= 0xFFFFFFFF
+ to_hex = to.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if to.is_a?(String)
+ to = to_hex.to_i(16) if to_hex
+
+ last_packet_id = opts[:last_packet_id] ||= 0
+ via = opts[:via] ||= :radio
+ channel = opts[:channel] ||= 0
+ want_ack = opts[:want_ack] ||= false
+ hop_limit = opts[:hop_limit] ||= 3
+
+ public_psk = '1PG7OiApB1nwvP+rz05pAQ=='
+ psks = opts[:psks] ||= { LongFast: public_psk }
+ raise 'ERROR: psks parameter must be a hash of :channel => psk key value pairs' unless psks.is_a?(Hash)
+
+ psks[:LongFast] = public_psk if psks[:LongFast] == 'AQ=='
+
+ # my_info = Meshtastic::FromRadio.my_info
+ # wait_connected if to != my_info.my_node_num && my_info.is_a(Meshtastic::Deviceonly::MyInfo)
+
+ mesh_packet.from = from
+ mesh_packet.to = to
+ mesh_packet.channel = channel
+ mesh_packet.want_ack = want_ack
+ mesh_packet.hop_limit = hop_limit
+ mesh_packet.id = generate_packet_id(last_packet_id: last_packet_id)
+
+ if psks
+ nonce_packet_id = [mesh_packet.id].pack('V').ljust(8, "\x00")
+ nonce_from_node = [from].pack('V').ljust(8, "\x00")
+ nonce = "#{nonce_packet_id}#{nonce_from_node}"
+
+ psk = psks[psks.keys.first]
+ dec_psk = Base64.strict_decode64(psk)
+ cipher = OpenSSL::Cipher.new('AES-128-CTR')
+ cipher = OpenSSL::Cipher.new('AES-256-CTR') if dec_psk.length == 32
+ cipher.encrypt
+ cipher.key = dec_psk
+ cipher.iv = nonce
+
+ decrypted_payload = mesh_packet.decoded.to_proto
+ encrypted_payload = cipher.update(decrypted_payload) + cipher.final
+
+ mesh_packet.encrypted = encrypted_payload
+ end
+ # puts mesh_packet.to_h
+
+ # puts "Sending Packet via: #{via}"
+ case via
+ when :radio
+ # Sending a to_radio message over mqtt
+ # causes unpredictable behavior
+ # (e.g. disconnecting node(s) from bluetooth)
+ to_radio = Meshtastic::ToRadio.new
+ to_radio.packet = mesh_packet
+ send_to_radio(to_radio: to_radio)
+ when :mqtt
+ service_envelope = Meshtastic::ServiceEnvelope.new
+ service_envelope.packet = mesh_packet
+ service_envelope.channel_id = psks.keys.first
+ service_envelope.gateway_id = "!#{from.to_s(16).downcase}"
+ send_to_mqtt(service_envelope: service_envelope)
+ else
+ raise "ERROR: Invalid via parameter: #{via}"
+ end
+ rescue StandardError => e
+ raise e
+ end
+
+ # Supported Method Parameters::
+ # packet_id = Meshtastic.generate_packet_id(
+ # last_packet_id: 'optional - Last Packet ID (Default: 0)'
+ # )
+ public_class_method def self.generate_packet_id(opts = {})
+ last_packet_id = opts[:last_packet_id] ||= 0
+ last_packet_id = 0 if last_packet_id.negative?
+
+ packet_id = Random.rand(0xffffffff) if last_packet_id.zero?
+ packet_id = (last_packet_id + 1) & 0xffffffff if last_packet_id.positive?
+
+ packet_id
+ end
+
+ # Supported Method Parameters::
+ # Meshtastic.send_to_radio(
+ # to_radio: 'required - ToRadio Message to Send'
+ # )
+ public_class_method def self.send_to_radio(opts = {})
+ to_radio = opts[:to_radio]
+
+ raise 'ERROR: Invalid ToRadio Message' unless to_radio.is_a?(Meshtastic::ToRadio)
+
+ to_radio.to_proto
+ rescue StandardError => e
+ raise e
+ end
+
+ # Supported Method Parameters::
+ # Meshtastic.send_to_mqtt(
+ # service_envelope: 'required - ServiceEnvelope Message to Send'
+ # )
+ public_class_method def self.send_to_mqtt(opts = {})
+ service_envelope = opts[:service_envelope]
+
+ raise 'ERROR: Invalid ServiceEnvelope Message' unless service_envelope.is_a?(Meshtastic::ServiceEnvelope)
+
+ service_envelope.to_proto
+ rescue StandardError => e
+ raise e
+ end
+
+ # Author(s):: 0day Inc. <support@0dayinc.com>
+
+ public_class_method def self.authors
+ "AUTHOR(S):
+ 0day Inc. <support@0dayinc.com>
+ "
+ end
# Display a List of Every Meshtastic Module
public_class_method def self.help
constants.sort