Module: Meshtastic::Admin

Defined in:
lib/meshtastic/admin.rb,
lib/meshtastic/admin/config.rb,
lib/meshtastic/admin/channel.rb,
lib/meshtastic/admin/firmware.rb,
lib/meshtastic/admin/firmware/ble.rb,
lib/meshtastic/admin/firmware/hex.rb,
lib/meshtastic/admin/firmware/uf2.rb,
lib/meshtastic/admin/firmware/nordic_dfu.rb,
lib/meshtastic/admin/firmware/serial_bootloader.rb

Defined Under Namespace

Modules: Channel, Config, Firmware Classes: RoutingError

Constant Summary collapse

SKIP =
%i[
  message transport_obj to from channel want_ack hop_limit
  want_response port_num data via psks seconds owner long_name short_name index config_type
  module_config_type path node_num lat lon altitude time channel_settings channel_pb
  config module_config messages ringtone scale location event event_code kb_char touch_x
  touch_y ham call_sign tx_power frequency position ui_config contact value preserve_favorites
  last_packet_id pki_encrypted public_key topic qos retained timeout auto_session refresh_session
].freeze
PAYLOAD_FIELDS =
Meshtastic::AdminMessage.descriptor.lookup_oneof('payload_variant').map { |field| field.name.to_sym }.freeze

Class Method Summary collapse

Class Method Details

.add_contact(opts = {}) ⇒ Object



414
415
416
# File 'lib/meshtastic/admin.rb', line 414

public_class_method def self.add_contact(opts = {})
  send(opts.merge(add_contact: opts[:contact]))
end

.authorsObject



446
447
448
# File 'lib/meshtastic/admin.rb', line 446

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

.backup_preferences(opts = {}) ⇒ Object



328
329
330
# File 'lib/meshtastic/admin.rb', line 328

public_class_method def self.backup_preferences(opts = {})
  send(opts.merge(backup_preferences: opts[:location] || :FLASH))
end

.begin_edit(opts = {}) ⇒ Object



406
407
408
# File 'lib/meshtastic/admin.rb', line 406

public_class_method def self.begin_edit(opts = {})
  send(opts.merge(begin_edit_settings: true))
end

.commit_edit(opts = {}) ⇒ Object



410
411
412
# File 'lib/meshtastic/admin.rb', line 410

public_class_method def self.commit_edit(opts = {})
  send(opts.merge(commit_edit_settings: true))
end

.decode(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/meshtastic/admin.rb', line 204

public_class_method def self.decode(opts = {})
  reject_legacy_transport_options(opts)
  packet = opts[:packet]
  packet = packet.packet if packet.is_a?(Meshtastic::FromRadio)
  data = packet.is_a?(Meshtastic::MeshPacket) ? packet.decoded : packet
  raise ArgumentError, 'packet must contain decoded ADMIN_APP data' if opts.key?(:packet) && (!data.is_a?(Meshtastic::Data) || data.portnum != :ADMIN_APP)

  payload = data ? data.payload : opts[:payload]
  raise ArgumentError, 'payload bytes or a decoded packet are required' unless payload.is_a?(String)

  Meshtastic::AdminMessage.decode(payload)
end

.delete_file(opts = {}) ⇒ Object



320
321
322
# File 'lib/meshtastic/admin.rb', line 320

public_class_method def self.delete_file(opts = {})
  send(opts.merge(delete_file_request: opts[:path]))
end

.encode(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


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
# File 'lib/meshtastic/admin.rb', line 49

public_class_method def self.encode(opts = {})
  reject_legacy_transport_options(opts)
  original = opts[:message] || Meshtastic::AdminMessage.new
  raise ArgumentError, 'message must be an AdminMessage' unless original.is_a?(Meshtastic::AdminMessage)

  fields = opts.keys & PAYLOAD_FIELDS
  variants = (fields + [original.payload_variant]).compact.uniq
  raise ArgumentError, 'exactly one payload is required' unless variants.length == 1

  unknown = opts.keys - SKIP - PAYLOAD_FIELDS - [:session_passkey]
  raise ArgumentError, "unknown admin options: #{unknown.join(', ')}" unless unknown.empty?

  message = Meshtastic::AdminMessage.decode(original.to_proto)
  fields.each do |key|
    raise ArgumentError, "#{key} cannot be nil" if opts[key].nil?

    message.public_send("#{key}=", opts[key])
  end
  if opts.key?(:session_passkey)
    passkey = opts[:session_passkey]
    raise ArgumentError, 'session_passkey must contain exactly eight bytes' unless passkey.is_a?(String) && passkey.bytesize == 8

    message.session_passkey = passkey
  end
  message
end

.enter_dfu(opts = {}) ⇒ Object



316
317
318
# File 'lib/meshtastic/admin.rb', line 316

public_class_method def self.enter_dfu(opts = {})
  send(opts.merge(enter_dfu_mode_request: true))
end

.exit_simulator(opts = {}) ⇒ Object



434
435
436
# File 'lib/meshtastic/admin.rb', line 434

public_class_method def self.exit_simulator(opts = {})
  send(opts.merge(exit_simulator: true))
end

.factory_reset_config(opts = {}) ⇒ Object



426
427
428
# File 'lib/meshtastic/admin.rb', line 426

public_class_method def self.factory_reset_config(opts = {})
  send(opts.merge(factory_reset_config: opts[:value] || 1))
end

.factory_reset_device(opts = {}) ⇒ Object



422
423
424
# File 'lib/meshtastic/admin.rb', line 422

public_class_method def self.factory_reset_device(opts = {})
  send(opts.merge(factory_reset_device: opts[:value] || 1))
end

.get_canned_messages(opts = {}) ⇒ Object



288
289
290
# File 'lib/meshtastic/admin.rb', line 288

public_class_method def self.get_canned_messages(opts = {})
  send(opts.merge(get_canned_message_module_messages_request: true))
end

.get_channel(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


264
265
266
267
268
269
270
# File 'lib/meshtastic/admin.rb', line 264

public_class_method def self.get_channel(opts = {})
  index = opts[:index]
  index = 0 if index.nil?
  raise ArgumentError, 'index must be an Integer from 0 through 7' unless index.is_a?(Integer) && index.between?(0, 7)

  send(opts.merge(get_channel_request: index + 1))
end

.get_config(opts = {}) ⇒ Object



272
273
274
# File 'lib/meshtastic/admin.rb', line 272

public_class_method def self.get_config(opts = {})
  send(opts.merge(get_config_request: opts[:config_type] || :DEVICE_CONFIG))
end

.get_device_connection_status(opts = {}) ⇒ Object



308
309
310
# File 'lib/meshtastic/admin.rb', line 308

public_class_method def self.get_device_connection_status(opts = {})
  send(opts.merge(get_device_connection_status_request: true))
end

.get_device_metadata(opts = {}) ⇒ Object



296
297
298
# File 'lib/meshtastic/admin.rb', line 296

public_class_method def self.(opts = {})
  send(opts.merge(get_device_metadata_request: true))
end

.get_module_config(opts = {}) ⇒ Object



280
281
282
# File 'lib/meshtastic/admin.rb', line 280

public_class_method def self.get_module_config(opts = {})
  send(opts.merge(get_module_config_request: opts[:module_config_type] || :MQTT_CONFIG))
end

.get_node_remote_hardware_pins(opts = {}) ⇒ Object



312
313
314
# File 'lib/meshtastic/admin.rb', line 312

public_class_method def self.get_node_remote_hardware_pins(opts = {})
  send(opts.merge(get_node_remote_hardware_pins_request: true))
end

.get_owner(opts = {}) ⇒ Object



256
257
258
# File 'lib/meshtastic/admin.rb', line 256

public_class_method def self.get_owner(opts = {})
  send(opts.merge(get_owner_request: true))
end

.get_ringtone(opts = {}) ⇒ Object



300
301
302
# File 'lib/meshtastic/admin.rb', line 300

public_class_method def self.get_ringtone(opts = {})
  send(opts.merge(get_ringtone_request: true))
end

.get_ui_config(opts = {}) ⇒ Object



386
387
388
# File 'lib/meshtastic/admin.rb', line 386

public_class_method def self.get_ui_config(opts = {})
  send(opts.merge(get_ui_config_request: true))
end

.helpObject



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
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
592
593
594
595
596
597
598
599
600
601
602
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
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
# File 'lib/meshtastic/admin.rb', line 450

public_class_method def self.help
  puts "USAGE:
    # Classify the actual connected transport handle.
    #{self}.transport_type(
      transport_obj: 'required - handle from Serial.connect, Bluetooth.connect, TCP.connect or MQTT.connect'
    )
    # Returns :serial, :bluetooth, :tcp or :mqtt; TCP includes serial framing keys.
    # Nil, unknown and ambiguous handles are rejected. Old transport option keys are rejected.
    # All send wrappers below require transport_obj and accept the shared send options.

    # Encode exactly one AdminMessage payload without modifying the caller.
    #{self}.encode(
      message: 'optional - existing AdminMessage to copy instead of a new one',
      session_passkey: 'optional - bytes from a prior get_*_response to authorize sets'
    )

    # Send an AdminMessage on ADMIN_APP over a connected transport.
    #{self}.send(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client',
      to: 'optional - unicast node integer or !eighthex; defaults to connected my_node_num; required for MQTT',
      from: 'optional - sender number; radio default zero denotes the local PhoneAPI client',
      channel: 'optional - routing channel index (default zero)',
      want_ack: 'optional - request routing acknowledgment, distinct from an admin reply',
      hop_limit: 'optional - maximum radio hop count',
      psks: 'optional - MQTT channel key map; not public-key admin authorization',
      pki_encrypted: 'optional - enable device-owned PKI on serial BLE or TCP; unsupported over MQTT',
      public_key: 'optional - recipient public-key bytes for radio PKI',
      last_packet_id: 'optional - previous packet number used by transport packet generation',
      want_response: 'optional - defaults true for get requests and false for state changes',
      auto_session: 'optional - acquire a session for remote writes unless a passkey is supplied (default true)',
      refresh_session: 'optional - discard cached session for this automatic acquisition (default false)',
      timeout: 'optional - positive finite receive deadline in seconds for session acquisition (default ten)'
    )

    # Send admin data and synchronously await correlated readback.
    #{self}.request(
      request_id: 'optional - packet ID from 2 through 0xffffffff; generated when omitted; accepts send options',
      wait: 'optional - false returns only request_id and submission result; true waits for reply (default true)',
      timeout: 'optional - positive finite total response budget in seconds including session acquisition (default ten)'
    )

    # Decode protobuf bytes or an incoming admin packet.
    #{self}.decode(
      payload: 'optional - serialized AdminMessage bytes when packet is omitted',
      packet: 'optional - FromRadio MeshPacket or Data containing decoded ADMIN_APP bytes'
    )

    # Extract a matching response and its session passkey.
    #{self}.response(
      packet: 'required - incoming FromRadio or MeshPacket protobuf',
      request_id: 'optional - require this Data.request_id to match the outgoing packet ID',
      from: 'optional - require this numeric responding node; mismatches return nil'
    )

    # Request legacy ESP32 OTA reboot; deprecated firmware operation.
    #{self}.reboot_ota(
      seconds: 'optional - legacy reboot delay, negative cancels (default five); use ota_request on current firmware'
    )

    # Send the current OTA loader request protobuf.
    #{self}.ota_request(
      event: 'required - AdminMessage::OTAEvent containing mode and firmware hash; see Admin::Firmware'
    )

    # Reboot the node after a delay.
    #{self}.reboot(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client',
      seconds: 'optional - delay before reboot in seconds (default: 5)'
    )

    # Shut down the node after a delay.
    #{self}.shutdown(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client',
      seconds: 'optional - delay before shutdown in seconds (default: 5)'
    )

    # Set the node owner User protobuf.
    #{self}.set_owner(
      owner: 'optional - Meshtastic::User protobuf to send as set_owner',
      long_name: 'optional - owner long name when owner is omitted',
      short_name: 'optional - owner short name when owner is omitted'
    )

    # Request the node owner User protobuf.
    #{self}.get_owner(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Write a Channel protobuf to the node.
    #{self}.set_channel(
      channel_settings: 'optional - Meshtastic::Channel protobuf to write',
      channel_pb: 'optional - Channel protobuf alias used as set_channel'
    )

    # Request a channel by index.
    #{self}.get_channel(
      index: 'optional - zero-based channel index 0 through 7; wire conversion happens here (default zero)'
    )

    # Request a radio Config section.
    #{self}.get_config(
      config_type: 'optional - AdminMessage::ConfigType such as :LORA_CONFIG'
    )

    # Write a Config protobuf to the node.
    #{self}.set_config(
      config: 'required - Meshtastic::Config protobuf to write'
    )

    # Request a ModuleConfig section.
    #{self}.get_module_config(
      module_config_type: 'optional - ModuleConfigType such as :MQTT_CONFIG'
    )

    # Write a ModuleConfig protobuf to the node.
    #{self}.set_module_config(
      module_config: 'required - Meshtastic::ModuleConfig protobuf to write'
    )

    # Request canned-message module strings.
    #{self}.get_canned_messages(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Set canned-message module strings.
    #{self}.set_canned_messages(
      messages: 'required - pipe-separated canned message text; empty string clears it'
    )

    # Request DeviceMetadata (firmware version and hardware).
    #{self}.get_device_metadata(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Request the RTTTL ringtone string.
    #{self}.get_ringtone(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Set the RTTTL ringtone string.
    #{self}.set_ringtone(
      ringtone: 'required - RTTTL ringtone text to store on the node'
    )

    # Request DeviceConnectionStatus.
    #{self}.get_device_connection_status(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Request remote-hardware pin definitions.
    #{self}.get_node_remote_hardware_pins(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Ask the node to enter DFU / UF2 mode.
    #{self}.enter_dfu(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Delete a file on the node filesystem.
    #{self}.delete_file(
      path: 'required - filesystem path on the node to delete'
    )

    # Set the NAU7802 scale calibration value.
    #{self}.set_scale(
      scale: 'required - integer scale factor for the NAU7802 module'
    )

    # Backup preferences to flash or SD.
    #{self}.backup_preferences(
      location: 'optional - :FLASH or :SD backup location (default: :FLASH)'
    )

    # Restore preferences from flash or SD.
    #{self}.restore_preferences(
      location: 'optional - :FLASH or :SD backup location (default: :FLASH)'
    )

    # Remove a preferences backup from flash or SD.
    #{self}.remove_backup_preferences(
      location: 'optional - :FLASH or :SD backup location (default: :FLASH)'
    )

    # Inject a device-UI input event.
    #{self}.send_input_event(
      event: 'optional - AdminMessage::InputEvent protobuf to send',
      event_code: 'optional - input event code when event is omitted',
      kb_char: 'optional - keyboard character code when event is omitted',
      touch_x: 'optional - touch X coordinate when event is omitted',
      touch_y: 'optional - touch Y coordinate when event is omitted'
    )

    # Enable ham-radio identity parameters.
    #{self}.set_ham_mode(
      ham: 'optional - HamParameters protobuf to send',
      call_sign: 'optional - amateur callsign when ham is omitted',
      tx_power: 'optional - transmit power when ham is omitted',
      frequency: 'optional - frequency in MHz when ham is omitted',
      short_name: 'optional - short name when ham is omitted',
      long_name: 'optional - long name when ham is omitted'
    )

    # Remove a node from the local nodedb by number.
    #{self}.remove_by_nodenum(
      node_num: 'required - node number to remove from the nodedb'
    )

    # Mark a node as a favorite.
    #{self}.set_favorite_node(
      node_num: 'required - node number to mark as favorite'
    )

    # Clear favorite status for a node.
    #{self}.remove_favorite_node(
      node_num: 'required - node number to unfavorite'
    )

    # Set a fixed GPS position on the node.
    #{self}.set_fixed_position(
      position: 'optional - Meshtastic::Position protobuf to store',
      lat: 'optional - latitude in decimal degrees when position is omitted',
      lon: 'optional - longitude in decimal degrees when position is omitted',
      altitude: 'optional - altitude in meters when position is omitted'
    )

    # Clear the fixed GPS position on the node.
    #{self}.remove_fixed_position(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Set node wall-clock time.
    #{self}.set_time(
      time: 'required - unix timestamp to set on the node'
    )

    # Request DeviceUIConfig.
    #{self}.get_ui_config(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Store DeviceUIConfig on the node.
    #{self}.store_ui_config(
      ui_config: 'required - Meshtastic::DeviceUIConfig protobuf to store'
    )

    # Ignore a node number.
    #{self}.set_ignored_node(
      node_num: 'required - node number to ignore'
    )

    # Stop ignoring a node number.
    #{self}.remove_ignored_node(
      node_num: 'required - node number to stop ignoring'
    )

    # Toggle mute for a node number.
    #{self}.toggle_muted_node(
      node_num: 'required - node number whose mute flag should toggle'
    )

    # Open a settings edit transaction.
    #{self}.begin_edit(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Commit a settings edit transaction.
    #{self}.commit_edit(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Add a SharedContact to the node.
    #{self}.add_contact(
      contact: 'required - Meshtastic::SharedContact protobuf to add'
    )

    # Send a KeyVerificationAdmin protobuf.
    #{self}.key_verification(
      key_verification: 'required - KeyVerificationAdmin protobuf to send'
    )

    # Factory-reset device state including BLE bonds.
    #{self}.factory_reset_device(
      value: 'optional - int32 factory_reset_device field (default: 1)'
    )

    # Factory-reset configuration only.
    #{self}.factory_reset_config(
      value: 'optional - int32 factory_reset_config field (default: 1)'
    )

    # Clear the node database.
    #{self}.nodedb_reset(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client',
      preserve_favorites: 'optional - retain favorite nodes (default true); some firmware roles always preserve favorites'
    )

    # Exit the firmware simulator if running.
    #{self}.exit_simulator(
      transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
    )

    # Write a SensorConfig protobuf.
    #{self}.sensor_config(
      sensor_config: 'required - Meshtastic::SensorConfig protobuf to write'
    )

    # Send lockdown authentication material.
    #{self}.lockdown_auth(
      lockdown_auth: 'required - Meshtastic::LockdownAuth protobuf to send'
    )

    # Print the AUTHOR(S) string for this module.
    #{self}.authors
  "
end

.key_verification(opts = {}) ⇒ Object



418
419
420
# File 'lib/meshtastic/admin.rb', line 418

public_class_method def self.key_verification(opts = {})
  send(opts.merge(key_verification: opts[:key_verification]))
end

.lockdown_auth(opts = {}) ⇒ Object



442
443
444
# File 'lib/meshtastic/admin.rb', line 442

public_class_method def self.lockdown_auth(opts = {})
  send(opts.merge(lockdown_auth: opts[:lockdown_auth]))
end

.nodedb_reset(opts = {}) ⇒ Object



430
431
432
# File 'lib/meshtastic/admin.rb', line 430

public_class_method def self.nodedb_reset(opts = {})
  send(opts.merge(nodedb_reset: opts.fetch(:preserve_favorites, true)))
end

.ota_request(opts = {}) ⇒ Object



247
248
249
# File 'lib/meshtastic/admin.rb', line 247

public_class_method def self.ota_request(opts = {})
  send(opts.merge(ota_request: opts[:event]))
end

.reboot(opts = {}) ⇒ Object



235
236
237
# File 'lib/meshtastic/admin.rb', line 235

public_class_method def self.reboot(opts = {})
  send(opts.merge(reboot_seconds: opts[:seconds] || 5))
end

.reboot_ota(opts = {}) ⇒ Object



243
244
245
# File 'lib/meshtastic/admin.rb', line 243

public_class_method def self.reboot_ota(opts = {})
  send(opts.merge(reboot_ota_seconds: opts[:seconds] || 5))
end

.remove_backup_preferences(opts = {}) ⇒ Object



336
337
338
# File 'lib/meshtastic/admin.rb', line 336

public_class_method def self.remove_backup_preferences(opts = {})
  send(opts.merge(remove_backup_preferences: opts[:location] || :FLASH))
end

.remove_by_nodenum(opts = {}) ⇒ Object



361
362
363
# File 'lib/meshtastic/admin.rb', line 361

public_class_method def self.remove_by_nodenum(opts = {})
  send(opts.merge(remove_by_nodenum: opts[:node_num]))
end

.remove_favorite_node(opts = {}) ⇒ Object



369
370
371
# File 'lib/meshtastic/admin.rb', line 369

public_class_method def self.remove_favorite_node(opts = {})
  send(opts.merge(remove_favorite_node: opts[:node_num]))
end

.remove_fixed_position(opts = {}) ⇒ Object



378
379
380
# File 'lib/meshtastic/admin.rb', line 378

public_class_method def self.remove_fixed_position(opts = {})
  send(opts.merge(remove_fixed_position: true))
end

.remove_ignored_node(opts = {}) ⇒ Object



398
399
400
# File 'lib/meshtastic/admin.rb', line 398

public_class_method def self.remove_ignored_node(opts = {})
  send(opts.merge(remove_ignored_node: opts[:node_num]))
end

.request(opts = {}) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/meshtastic/admin.rb', line 100

public_class_method def self.request(opts = {})
  type = transport_type(opts)
  request_id = opts.fetch(:request_id) { Random.rand(2..0xffffffff) }
  raise ArgumentError, 'request_id must be an Integer from 2 through 0xffffffff' unless request_id.is_a?(Integer) && request_id.between?(2, 0xffffffff)

  options = opts.except(:request_id, :wait)
  return { request_id: request_id, result: send(options.merge(last_packet_id: request_id - 1)) } unless opts.fetch(:wait, true)

  connection = options[:transport_obj] unless type == :mqtt
  queue = connection && connection[:from_radio_queue]
  raise ArgumentError, 'synchronous Admin requires a connected radio receive queue' unless queue

  timeout = opts.fetch(:timeout, 10)
  raise ArgumentError, 'timeout must be a positive finite number' unless timeout.is_a?(Numeric) && timeout.positive? && timeout.finite?

  lock = connection[:admin_request_lock] ||= Monitor.new
  acquired = lock.try_enter
  raise IOError, 'a synchronous Admin request is already active on this handle' unless acquired

  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
  message = encode(options)
  target = options.fetch(:to, connection[:my_node_num])
  target = target.delete_prefix('!').to_i(16) if target.is_a?(String) && target.match?(/\A![0-9a-fA-F]{8}\z/)
  result = send(options.merge(last_packet_id: request_id - 1, want_response: true, want_ack: true, timeout: timeout))
  variant = message.payload_variant.to_s
  expected = variant.match?(/\Aget_.*_request\z/) ? variant.sub(/_request\z/, '_response').to_sym : :routing
  reply = await_response(connection: connection, queue: queue, deadline: deadline, request_id: request_id, target: target,
                         variant: expected)
  if reply[:session_passkey]&.bytesize == 8
    sessions = connection[:admin_sessions] ||= {}
    sessions[target] = { key: reply[:session_passkey], expires_at: deadline - timeout + 150 }
  end
  reply.merge(result: result)
rescue RoutingError => e
  connection[:admin_sessions]&.delete(target) if connection && e.reason == :ADMIN_BAD_SESSION_KEY
  raise
ensure
  lock.exit if acquired
end

.response(opts = {}) ⇒ Object



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/meshtastic/admin.rb', line 217

public_class_method def self.response(opts = {})
  reject_legacy_transport_options(opts)
  packet = opts[:packet]
  packet = packet.packet if packet.is_a?(Meshtastic::FromRadio)
  return nil unless packet.is_a?(Meshtastic::MeshPacket) && packet.decoded&.portnum == :ADMIN_APP
  return nil if opts.key?(:request_id) && packet.decoded.request_id != opts[:request_id]
  return nil if opts.key?(:from) && packet.from != opts[:from]

  message = decode(packet: packet)
  variant = message.payload_variant
  return nil unless variant.to_s.end_with?('_response')

  {
    message: message, variant: variant, value: message.public_send(variant),
    session_passkey: message.session_passkey, request_id: packet.decoded.request_id, from: packet.from
  }
end

.restore_preferences(opts = {}) ⇒ Object



332
333
334
# File 'lib/meshtastic/admin.rb', line 332

public_class_method def self.restore_preferences(opts = {})
  send(opts.merge(restore_preferences: opts[:location] || :FLASH))
end

.send(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/meshtastic/admin.rb', line 76

public_class_method def self.send(opts = {})
  type = transport_type(opts)
  message = encode(opts)
  connection = opts[:transport_obj] unless type == :mqtt
  destination = opts[:to] || connection&.dig(:my_node_num)
  destination = destination.delete_prefix('!').to_i(16) if destination.is_a?(String) && destination.match?(/\A![0-9a-fA-F]{8}\z/)
  raise ArgumentError, 'an explicit unicast destination or connected my_node_num is required' unless destination.is_a?(Integer) && destination.between?(1, 0xfffffffe)

  if opts.fetch(:auto_session, true) && message.session_passkey.empty? && destination != connection&.dig(:my_node_num) &&
     !message.payload_variant.to_s.match?(/\Aget_.*_(request|response)\z/)
    message.session_passkey = acquire_session(opts.merge(connection: connection, target: destination))
  end
  want_response = opts.fetch(:want_response) { message.payload_variant.to_s.match?(/\Aget_.*_request\z/) }
  data = Meshtastic::Data.new(
    portnum: :ADMIN_APP,
    payload: message.to_proto,
    want_response: want_response
  )
  delivery = opts.except(:transport_obj).merge(data: data, port_num: Meshtastic::PortNum::ADMIN_APP, to: destination)
  delivery[:"#{type}_obj"] = opts[:transport_obj]
  delivery[:from] = 0 if connection && !opts.key?(:from)
  Meshtastic.deliver_data(delivery)
end

.send_input_event(opts = {}) ⇒ Object



340
341
342
343
344
345
346
347
348
# File 'lib/meshtastic/admin.rb', line 340

public_class_method def self.send_input_event(opts = {})
  event = opts[:event] || Meshtastic::AdminMessage::InputEvent.new(
    event_code: opts[:event_code].to_i,
    kb_char: opts[:kb_char].to_i,
    touch_x: opts[:touch_x].to_i,
    touch_y: opts[:touch_y].to_i
  )
  send(opts.merge(send_input_event: event))
end

.sensor_config(opts = {}) ⇒ Object



438
439
440
# File 'lib/meshtastic/admin.rb', line 438

public_class_method def self.sensor_config(opts = {})
  send(opts.merge(sensor_config: opts[:sensor_config]))
end

.set_canned_messages(opts = {}) ⇒ Object



292
293
294
# File 'lib/meshtastic/admin.rb', line 292

public_class_method def self.set_canned_messages(opts = {})
  send(opts.merge(set_canned_message_module_messages: opts[:messages]))
end

.set_channel(opts = {}) ⇒ Object



260
261
262
# File 'lib/meshtastic/admin.rb', line 260

public_class_method def self.set_channel(opts = {})
  send(opts.merge(set_channel: opts[:channel_settings] || opts[:channel_pb]))
end

.set_config(opts = {}) ⇒ Object



276
277
278
# File 'lib/meshtastic/admin.rb', line 276

public_class_method def self.set_config(opts = {})
  send(opts.merge(set_config: opts[:config]))
end

.set_favorite_node(opts = {}) ⇒ Object



365
366
367
# File 'lib/meshtastic/admin.rb', line 365

public_class_method def self.set_favorite_node(opts = {})
  send(opts.merge(set_favorite_node: opts[:node_num]))
end

.set_fixed_position(opts = {}) ⇒ Object



373
374
375
376
# File 'lib/meshtastic/admin.rb', line 373

public_class_method def self.set_fixed_position(opts = {})
  position = opts[:position] || Meshtastic::Position.build(lat: opts[:lat], lon: opts[:lon], altitude: opts[:altitude])
  send(opts.merge(set_fixed_position: position))
end

.set_ham_mode(opts = {}) ⇒ Object



350
351
352
353
354
355
356
357
358
359
# File 'lib/meshtastic/admin.rb', line 350

public_class_method def self.set_ham_mode(opts = {})
  ham = opts[:ham] || Meshtastic::HamParameters.new(
    call_sign: opts[:call_sign].to_s,
    tx_power: opts[:tx_power].to_i,
    frequency: opts[:frequency].to_f,
    short_name: opts[:short_name].to_s,
    long_name: opts[:long_name].to_s
  )
  send(opts.merge(set_ham_mode: ham))
end

.set_ignored_node(opts = {}) ⇒ Object



394
395
396
# File 'lib/meshtastic/admin.rb', line 394

public_class_method def self.set_ignored_node(opts = {})
  send(opts.merge(set_ignored_node: opts[:node_num]))
end

.set_module_config(opts = {}) ⇒ Object



284
285
286
# File 'lib/meshtastic/admin.rb', line 284

public_class_method def self.set_module_config(opts = {})
  send(opts.merge(set_module_config: opts[:module_config]))
end

.set_owner(opts = {}) ⇒ Object



251
252
253
254
# File 'lib/meshtastic/admin.rb', line 251

public_class_method def self.set_owner(opts = {})
  user = opts[:owner] || Meshtastic::User.new(long_name: opts[:long_name], short_name: opts[:short_name])
  send(opts.merge(set_owner: user))
end

.set_ringtone(opts = {}) ⇒ Object



304
305
306
# File 'lib/meshtastic/admin.rb', line 304

public_class_method def self.set_ringtone(opts = {})
  send(opts.merge(set_ringtone_message: opts[:ringtone]))
end

.set_scale(opts = {}) ⇒ Object



324
325
326
# File 'lib/meshtastic/admin.rb', line 324

public_class_method def self.set_scale(opts = {})
  send(opts.merge(set_scale: opts[:scale]))
end

.set_time(opts = {}) ⇒ Object



382
383
384
# File 'lib/meshtastic/admin.rb', line 382

public_class_method def self.set_time(opts = {})
  send(opts.merge(set_time_only: opts[:time]))
end

.shutdown(opts = {}) ⇒ Object



239
240
241
# File 'lib/meshtastic/admin.rb', line 239

public_class_method def self.shutdown(opts = {})
  send(opts.merge(shutdown_seconds: opts[:seconds] || 5))
end

.store_ui_config(opts = {}) ⇒ Object



390
391
392
# File 'lib/meshtastic/admin.rb', line 390

public_class_method def self.store_ui_config(opts = {})
  send(opts.merge(store_ui_config: opts[:ui_config]))
end

.toggle_muted_node(opts = {}) ⇒ Object



402
403
404
# File 'lib/meshtastic/admin.rb', line 402

public_class_method def self.toggle_muted_node(opts = {})
  send(opts.merge(toggle_muted_node: opts[:node_num]))
end

.transport_type(opts = {}) ⇒ Object

Classify a handle returned by a transport's connect method.

Raises:

  • (ArgumentError)


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/meshtastic/admin.rb', line 32

public_class_method def self.transport_type(opts = {})
  reject_legacy_transport_options(opts)
  transport = opts[:transport_obj]
  return :mqtt if transport.is_a?(MQTTClient)
  raise ArgumentError, 'transport_obj has ambiguous transport handle keys' if transport.is_a?(Hash) && transport.key?(:bluetooth_conn) && (transport.key?(:serial_conn) || transport.key?(:tcp_socket))

  if transport.is_a?(Hash) && transport.key?(:tcp_socket)
    raise ArgumentError, 'transport_obj TCP handle requires tcp_socket and serial_conn' unless transport[:tcp_socket] && transport[:serial_conn]

    return :tcp
  end
  return :bluetooth if transport.is_a?(Hash) && transport[:bluetooth_conn]
  return :serial if transport.is_a?(Hash) && transport[:serial_conn]

  raise ArgumentError, 'transport_obj must be a connected Serial, Bluetooth, TCP handle or MQTT client'
end