Class: Meshtastic::MeshInterface

Inherits:
Object
  • Object
show all
Defined in:
lib/meshtastic/mesh_interface.rb

Constant Summary collapse

PROTOBUF_PAYLOADS =

Wire schemas, not similarly named transport/status messages.

{
  REMOTE_HARDWARE_APP: HardwareMessage, POSITION_APP: Position, NODEINFO_APP: User,
  ROUTING_APP: Routing, ADMIN_APP: AdminMessage, WAYPOINT_APP: Waypoint,
  KEY_VERIFICATION_APP: KeyVerification, REMOTE_SHELL_APP: RemoteShell,
  PAXCOUNTER_APP: Paxcount, STORE_FORWARD_PLUSPLUS_APP: StoreForwardPlusPlus,
  NODE_STATUS_APP: StatusMessage, MESH_BEACON_APP: MeshBeacon,
  STORE_FORWARD_APP: StoreAndForward, TELEMETRY_APP: Telemetry,
  SIMULATOR_APP: Compressed, TRACEROUTE_APP: RouteDiscovery,
  NEIGHBORINFO_APP: NeighborInfo, ATAK_PLUGIN: TAKPacket, MAP_REPORT_APP: MapReport,
  POWERSTRESS_APP: PowerStressMessage, LORAWAN_BRIDGE: LoRaWANBridge, ATAK_PLUGIN_V2: TAKPacketV2
}.freeze
TEXT_PAYLOADS =
%i[TEXT_MESSAGE_APP DETECTION_SENSOR_APP ALERT_APP REPLY_APP RANGE_TEST_APP].freeze
BINARY_PAYLOADS =
%i[TEXT_MESSAGE_COMPRESSED_APP AUDIO_APP IP_TUNNEL_APP ZPS_APP
CAYENNE_APP LORA_OTA_APP ATAK_FORWARDER RETICULUM_TUNNEL_APP].freeze
RAW_PAYLOADS =

No guessed protobuf decoding for application-owned or undefined bytes.

%i[UNKNOWN_APP PAGING_APP SERIAL_APP GROUPALARM_APP MAX].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ MeshInterface

Returns a new instance of MeshInterface.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/meshtastic/mesh_interface.rb', line 47

def initialize(opts = {})
  @acknowledgment = Meshtastic::Util::Acknowledgement.new

  @current_packet_id = generate_packet_id

  @debug_out = opts[:debug_out]

  @failure = nil

  @got_response = false

  @heartbeat_timer = nil

  @is_connected = true if opts[:is_connected]
  @is_connected = false unless opts[:is_connected] # TODO: threading.Event = threading.Event()

  @is_proto = true if opts[:is_proto]
  @is_proto = false unless opts[:is_proto]

  @local_channels = nil

  @mask = nil

  @metadata = Meshtastic::DeviceMetadata.new

  @my_info = Meshtastic::MyNodeInfo

  @no_nodes = true if opts[:no_nodes]
  @no_nodes = false unless opts[:no_nodes]

  @nodes = { nodes: {} }

  @nodes_by_num = {}

  @queue = {}
  @queue_status = Meshtastic::QueueStatus.new

  @response_handlers = {}

  @timeout = Meshtastic::Util::Timeout.new

  @config_id = NODELESS_WANT_CONFIG_ID if no_nodes
  @config_id = nil unless no_nodes
rescue StandardError => e
  raise e
end

Instance Attribute Details

#acknowledgment ⇒ Object

Returns the value of attribute acknowledgment.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def acknowledgment
  @acknowledgment
end

#config_id ⇒ Object

Returns the value of attribute config_id.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def config_id
  @config_id
end

#current_packet_id ⇒ Object

Returns the value of attribute current_packet_id.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def current_packet_id
  @current_packet_id
end

#debug_out ⇒ Object

Returns the value of attribute debug_out.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def debug_out
  @debug_out
end

#failure ⇒ Object

Returns the value of attribute failure.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def failure
  @failure
end

#got_response ⇒ Object

Returns the value of attribute got_response.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def got_response
  @got_response
end

#heartbeat_timer ⇒ Object

Returns the value of attribute heartbeat_timer.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def heartbeat_timer
  @heartbeat_timer
end

#is_connected ⇒ Object

Returns the value of attribute is_connected.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def is_connected
  @is_connected
end

#is_proto ⇒ Object

Returns the value of attribute is_proto.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def is_proto
  @is_proto
end

#local_channels ⇒ Object

Returns the value of attribute local_channels.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def local_channels
  @local_channels
end

#mask ⇒ Object

Returns the value of attribute mask.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def mask
  @mask
end

#metadata ⇒ Object

Returns the value of attribute metadata.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def 
  @metadata
end

#my_info ⇒ Object

Returns the value of attribute my_info.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def my_info
  @my_info
end

#no_nodes ⇒ Object

Returns the value of attribute no_nodes.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def no_nodes
  @no_nodes
end

#nodes ⇒ Object

Returns the value of attribute nodes.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def nodes
  @nodes
end

#nodes_by_num ⇒ Object

Returns the value of attribute nodes_by_num.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def nodes_by_num
  @nodes_by_num
end

#queue ⇒ Object

Returns the value of attribute queue.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def queue
  @queue
end

#queue_status ⇒ Object

Returns the value of attribute queue_status.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def queue_status
  @queue_status
end

#response_handlers ⇒ Object

Returns the value of attribute response_handlers.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def response_handlers
  @response_handlers
end

#timeout ⇒ Object

Returns the value of attribute timeout.



26
27
28
# File 'lib/meshtastic/mesh_interface.rb', line 26

def timeout
  @timeout
end

Class Method Details

.authors ⇒ Object



675
676
677
# File 'lib/meshtastic/mesh_interface.rb', line 675

public_class_method def self.authors
  "AUTHOR(S):\n        0day Inc. <[email protected]>\n      "
end

.help ⇒ Object



679
680
681
682
683
684
# File 'lib/meshtastic/mesh_interface.rb', line 679

public_class_method def self.help
  puts "USAGE:
    # Print the AUTHOR(S) string for this module.
    #{self}.authors
  "
end

Instance Method Details

#authors ⇒ Object

Author(s)

0day Inc. [email protected]



595
596
597
598
599
# File 'lib/meshtastic/mesh_interface.rb', line 595

def authors
  "AUTHOR(S):
    0day Inc. <[email protected]>
  "
end

#decode_payload(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::MeshInterface.decode_payload( payload: 'required - bytes to decode; nil decodes known proto3 defaults; unknown absent payload remains nil', msg_type: 'required - message type (e.g. :TEXT_MESSAGE_APP)', gps_metadata: 'optional - include GPS metadata in output (default: false)', )



459
460
461
# File 'lib/meshtastic/mesh_interface.rb', line 459

def decode_payload(opts = {})
  decode_application_payload(opts.merge(simulator_depth: 0))
end

#decrypt_packet(opts = {}) ⇒ Object

Decode only the selected channel key; never reinterpret PKI as PSK crypto. AES-CTR parsing is not authentication or proof of origin.



549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
# File 'lib/meshtastic/mesh_interface.rb', line 549

def decrypt_packet(opts = {})
  message = opts[:message]
  return message if message[:decoded] || !message.key?(:encrypted)

  if message[:pki_encrypted]
    message[:decryption_error] = 'PKI ciphertext requires device-owned private keys'
    return message
  end

  channel = opts[:channel]
  psks = opts[:psks] || {}
  psk = psks[channel] || psks[channel.to_s] || psks[channel.to_s.to_sym] unless channel.nil?
  if channel.nil?
    # Encrypted radio packets carry an eight-bit name/key XOR hash, not a slot.
    candidates = psks.select do |name, value|
      bytes = Base64.strict_decode64(value)
      [16, 32].include?(bytes.bytesize) && (name.to_s.bytes + bytes.bytes).reduce(0, :^) == message.fetch(:channel, 0)
    rescue ArgumentError
      false
    end
    psk = candidates.values.first if candidates.size == 1
  end
  unless psk
    message[:decryption_error] = 'No PSK for the selected channel'
    return message
  end

  key = Base64.strict_decode64(psk)
  raise ArgumentError, 'PSK must contain 16 or 32 bytes' unless [16, 32].include?(key.bytesize)

  cipher = OpenSSL::Cipher.new(key.bytesize == 32 ? 'AES-256-CTR' : 'AES-128-CTR')
  cipher.decrypt
  cipher.key = key
  cipher.iv = [message.fetch(:id, 0), 0, message.fetch(:from, 0), 0].pack('V4')
  ciphertext = message[:encrypted]
  plaintext = (ciphertext.empty? ? ''.b : cipher.update(ciphertext)) + cipher.final
  message[:decoded] = Meshtastic::Data.decode(plaintext).to_h
  message[:encrypted] = :decrypted
  message
rescue ArgumentError, OpenSSL::Cipher::CipherError, Google::Protobuf::ParseError
  message[:decryption_error] = 'Unable to decode with the selected channel PSK'
  message
end

#generate_packet_id(opts = {}) ⇒ Object

Supported Method Parameters

packet_id = Meshtastic.generate_packet_id( last_packet_id: 'optional - Last Packet ID (Default: 0)' )



98
99
100
101
102
103
104
105
106
# File 'lib/meshtastic/mesh_interface.rb', line 98

def 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

#get_cipher_keys(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic.get_cipher_keys( psks: 'required - hash of channel / pre-shared key value pairs' )



113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/meshtastic/mesh_interface.rb', line 113

def get_cipher_keys(opts = {})
  psks = opts[:psks]

  psks.each_key do |key|
    psk = psks[key]
    padded_psk = psk.ljust(psk.length + ((4 - (psk.length % 4)) % 4), '=')
    replaced_psk = padded_psk.gsub('-', '+').gsub('_', '/')
    psks[key] = replaced_psk
  end

  psks
rescue StandardError => e
  raise e
end

#gps_search(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic.gps_search( lat: 'required - latitude float (e.g. 37.7749)', lon: 'required - longitude float (e.g. -122.4194)' )



133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/meshtastic/mesh_interface.rb', line 133

def gps_search(opts = {})
  lat = opts[:lat]
  lon = opts[:lon]

  raise 'ERROR: Latitude and Longitude are required' unless lat && lon

  gps_arr = [lat.to_f, lon.to_f]

  Geocoder.search(gps_arr).first.data
rescue StandardError => e
  raise e
end

#help ⇒ Object

Display Usage for this Module



603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/meshtastic/mesh_interface.rb', line 603

def help
  puts "USAGE:
    #{self}.send_to_radio(
      payload: 'required - ToRadio Message to Send'
    )

    #{self}.send_to_mqtt(
      service_envelope: 'required - ServiceEnvelope Message to Send'
    )

    #{self}.start_config

    #{self}.my_node_info

    #{self}.send_packet(
      mesh_packet: 'required - Mesh Packet to Send',
      from: 'required - From ID (String or Integer) (Default: \"!00000b0b\")',
      to: 'optional - Destination ID (Default: \"!ffffffff\")',
      last_packet_id: 'optional - Last Packet ID (Default: 0)',
      via: 'optional - :radio || :mqtt (Default: :radio)',
      channel: 'optional - Channel (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==\" })'
    )

    #{self}.send_data(
      from: 'required - From ID (String or Integer) (Default: \"!00000b0b\")',
      to: 'optional - Destination ID (Default: \"!ffffffff\")',
      last_packet_id: 'optional - Last Packet ID (Default: 0)',
      via: 'optional - :radio || :mqtt (Default: :radio)',
      channel: 'optional - Channel (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==\" })'
    )

    #{self}.send_text(
      from: 'required - From ID (String or Integer) (Default: \"!00000b0b\")',
      to: 'optional - Destination ID (Default: \"!ffffffff\")',
      last_packet_id: 'optional - Last Packet ID (Default: 0)',
      via: 'optional - :radio || :mqtt (Default: :radio)',
      channel: 'optional - Channel (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==\" })'
    )

    # Binary codecs: Unishox2, ATAK V1/V2, Forwarder, Reticulum and PayloadFormats.
    # EXI unsupported; fragments are not automatically grouped.
    # PCM and experimental ZPS profiles require the direct PayloadFormats API.
    #{self}.decode_payload(
      payload: 'required - bytes to decode; nil decodes known proto3 defaults; unknown absent payload remains nil',
      msg_type: 'required - message type (e.g. :TEXT_MESSAGE_APP)',
      gps_metadata: 'optional - include GPS metadata in output (default: false)',
    )

    #{self}.decrypt_packet(
      message: 'required - decoded MeshPacket hash, updated in place',
      psks: 'required - channel names mapped to full Base64 AES keys',
      channel: 'optional - exact MQTT channel name; omitted selects unique radio channel hash'
    )

    #{self}.authors
  "
end

#my_node_info ⇒ Object

Supported Method Parameters

Meshtastic::Mesh.my_node_info



195
196
197
198
199
200
# File 'lib/meshtastic/mesh_interface.rb', line 195

def my_node_info
  mni = Meshtastic::MyNodeInfo.new
  mni.to_h
rescue StandardError => e
  raise e
end

#send_data(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::MeshInterface.send_data( from: 'required - From ID (String or Integer) (Default: "!00000b0b")', to: 'optional - Destination ID (Default: "!ffffffff")', last_packet_id: 'optional - Last Packet ID (Default: 0)', via: 'optional - :radio || :mqtt (Default: :radio)', channel: 'optional - Channel (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==" })' )



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/meshtastic/mesh_interface.rb', line 317

def send_data(opts = {})
  # Send a text message to a node
  from = opts[:from] ||= '!00000b0b'
  # from_hex = from.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if from.is_a?(String)
  from_hex = from.delete('!') 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] ||= '!ffffffff'
  # to_hex = to.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if to.is_a?(String)
  to_hex = to.delete('!') 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=='
  if opts.key?(:psks) && opts[:psks].nil?
    psks = nil
  else
    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 = psks.dup
    psks[:LongFast] = public_psk if psks[:LongFast] == 'AQ=='
  end

  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,
    pki_encrypted: opts.fetch(:pki_encrypted, false),
    public_key: opts[:public_key],
    psks: psks
  )
rescue StandardError => e
  raise e
end

#send_packet(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::MeshInterface.send_packet( mesh_packet: 'required - Mesh Packet to Send', from: 'required - From ID (String or Integer) (Default: "!00000b0b")', to: 'optional - Destination ID (Default: "!ffffffff")', last_packet_id: 'optional - Last Packet ID (Default: 0)', via: 'optional - :radio || :mqtt (Default: :radio)', channel: 'optional - Channel (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==" })' )



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/meshtastic/mesh_interface.rb', line 214

def send_packet(opts = {})
  mesh_packet = opts[:mesh_packet]
  from = opts[:from] ||= '!00000b0b'
  # from_hex = from.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if from.is_a?(String)
  from_hex = from.delete('!') 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] ||= '!ffffffff'
  # to_hex = to.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if to.is_a?(String)
  to_hex = to.delete('!') 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=='
  # Explicit nil means "do not encrypt" (serial/radio path — device owns PSK).
  if opts.key?(:psks) && opts[:psks].nil?
    psks = nil
  else
    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 = psks.dup
    psks[:LongFast] = public_psk if psks[:LongFast] == 'AQ=='
  end

  # 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)
  mesh_packet.pki_encrypted = opts[:pki_encrypted] if opts.key?(:pki_encrypted)
  mesh_packet.public_key = opts[:public_key] if opts[:public_key]
  if mesh_packet.pki_encrypted
    raise ArgumentError, 'PKI encryption requires a radio transport without host PSK encryption' unless via == :radio && (psks.nil? || psks.empty?)
    raise ArgumentError, 'recipient public_key must contain 32 bytes' unless mesh_packet.public_key.bytesize == 32
  end

  # When psks is nil/empty, leave the packet decoded so the radio (serial/TCP)
  # device can apply the channel PSK itself. MQTT callers must pass psks so the
  # ServiceEnvelope payload is pre-encrypted for the mesh.
  if psks && !psks.empty?
    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
    payload = Meshtastic::ToRadio.new
    payload.packet = mesh_packet
    send_to_radio(payload: payload)
  when :mqtt
    service_envelope = Meshtastic::ServiceEnvelope.new
    service_envelope.packet = mesh_packet
    # TODO: Add support for multiple PSKs by accepting channel_id
    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

#send_text(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::MeshInterface.send_text( from: 'required - From ID (String or Integer) (Default: "!00000b0b")', to: 'optional - Destination ID (Default: "!ffffffff")', last_packet_id: 'optional - Last Packet ID (Default: 0)', via: 'optional - :radio || :mqtt (Default: :radio)', channel: 'optional - Channel (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==" })' )



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/meshtastic/mesh_interface.rb', line 389

def send_text(opts = {})
  # Send a text message to a node
  from = opts[:from] ||= '!00000b0b'
  # from_hex = from.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if from.is_a?(String)
  from_hex = from.delete('!') 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] ||= '!ffffffff'
  # to_hex = to.delete('!').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if to.is_a?(String)
  to_hex = to.delete('!') 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=='
  if opts.key?(:psks) && opts[:psks].nil?
    psks = nil
  else
    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 = psks.dup
    psks[:LongFast] = public_psk if psks[:LongFast] == 'AQ=='
  end

  # 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.to_s.dup.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

#send_to_mqtt(opts = {}) ⇒ Object

Supported Method Parameters

Meshtastic::MeshInterface.send_to_mqtt( service_envelope: 'required - ServiceEnvelope Message to Send' )



163
164
165
166
167
168
169
170
171
# File 'lib/meshtastic/mesh_interface.rb', line 163

def 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

#send_to_radio(opts = {}) ⇒ Object

Meshtastic::MeshInterface.send_to_radio( payload: 'required - ToRadio Message to Send' )



149
150
151
152
153
154
155
156
157
# File 'lib/meshtastic/mesh_interface.rb', line 149

def send_to_radio(opts = {})
  payload = opts[:payload]

  raise 'ERROR: Invalid ToRadio Message' unless payload.is_a?(Meshtastic::ToRadio)

  payload.to_proto
rescue StandardError => e
  raise e
end

#start_config ⇒ Object

Supported Method Parameters

Meshtastic::Mesh.start_config( )



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/meshtastic/mesh_interface.rb', line 176

def start_config
  @my_info = nil
  @nodes = {}
  @nodes_by_num = {}
  @local_channels = []

  to_radio_start_config = Meshtastic::ToRadio.new
  if @config_id.nil? || !@no_nodes
    @config_id = Random.rand(0xFFFFFFFF)
    @config_id += 1 if @config_id == NODELESS_WANT_CONFIG_ID
  end
  to_radio_start_config.want_config_id = @config_id
  send_to_radio(payload: to_radio_start_config)
rescue StandardError => e
  raise e
end