meshtastic 0.0.182 → 0.0.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  require 'timeout'
4
4
  require 'monitor'
5
5
  require 'meshtastic/admin_pb'
6
+ require 'meshtastic/mqtt'
6
7
 
7
8
  module Meshtastic
8
9
  module Admin
@@ -18,7 +19,7 @@ module Meshtastic
18
19
  end
19
20
 
20
21
  SKIP = %i[
21
- message serial_obj bluetooth_obj tcp_obj mqtt_obj to from channel want_ack hop_limit
22
+ message transport_obj to from channel want_ack hop_limit
22
23
  want_response port_num data via psks seconds owner long_name short_name index config_type
23
24
  module_config_type path node_num lat lon altitude time channel_settings channel_pb
24
25
  config module_config messages ringtone scale location event event_code kb_char touch_x
@@ -27,7 +28,26 @@ module Meshtastic
27
28
  ].freeze
28
29
  PAYLOAD_FIELDS = Meshtastic::AdminMessage.descriptor.lookup_oneof('payload_variant').map { |field| field.name.to_sym }.freeze
29
30
 
31
+ # Classify a handle returned by a transport's connect method.
32
+ public_class_method def self.transport_type(opts = {})
33
+ reject_legacy_transport_options(opts)
34
+ transport = opts[:transport_obj]
35
+ return :mqtt if transport.is_a?(MQTTClient)
36
+ 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))
37
+
38
+ if transport.is_a?(Hash) && transport.key?(:tcp_socket)
39
+ raise ArgumentError, 'transport_obj TCP handle requires tcp_socket and serial_conn' unless transport[:tcp_socket] && transport[:serial_conn]
40
+
41
+ return :tcp
42
+ end
43
+ return :bluetooth if transport.is_a?(Hash) && transport[:bluetooth_conn]
44
+ return :serial if transport.is_a?(Hash) && transport[:serial_conn]
45
+
46
+ raise ArgumentError, 'transport_obj must be a connected Serial, Bluetooth, TCP handle or MQTT client'
47
+ end
48
+
30
49
  public_class_method def self.encode(opts = {})
50
+ reject_legacy_transport_options(opts)
31
51
  original = opts[:message] || Meshtastic::AdminMessage.new
32
52
  raise ArgumentError, 'message must be an AdminMessage' unless original.is_a?(Meshtastic::AdminMessage)
33
53
 
@@ -54,8 +74,9 @@ module Meshtastic
54
74
  end
55
75
 
56
76
  public_class_method def self.send(opts = {})
77
+ type = transport_type(opts)
57
78
  message = encode(opts)
58
- connection = opts[:serial_obj] || opts[:bluetooth_obj] || opts[:tcp_obj]
79
+ connection = opts[:transport_obj] unless type == :mqtt
59
80
  destination = opts[:to] || connection&.dig(:my_node_num)
60
81
  destination = destination.delete_prefix('!').to_i(16) if destination.is_a?(String) && destination.match?(/\A![0-9a-fA-F]{8}\z/)
61
82
  raise ArgumentError, 'an explicit unicast destination or connected my_node_num is required' unless destination.is_a?(Integer) && destination.between?(1, 0xfffffffe)
@@ -70,19 +91,21 @@ module Meshtastic
70
91
  payload: message.to_proto,
71
92
  want_response: want_response
72
93
  )
73
- delivery = opts.merge(data: data, port_num: Meshtastic::PortNum::ADMIN_APP, to: destination)
94
+ delivery = opts.except(:transport_obj).merge(data: data, port_num: Meshtastic::PortNum::ADMIN_APP, to: destination)
95
+ delivery[:"#{type}_obj"] = opts[:transport_obj]
74
96
  delivery[:from] = 0 if connection && !opts.key?(:from)
75
97
  Meshtastic.deliver_data(delivery)
76
98
  end
77
99
 
78
100
  public_class_method def self.request(opts = {})
101
+ type = transport_type(opts)
79
102
  request_id = opts.fetch(:request_id) { Random.rand(2..0xffffffff) }
80
103
  raise ArgumentError, 'request_id must be an Integer from 2 through 0xffffffff' unless request_id.is_a?(Integer) && request_id.between?(2, 0xffffffff)
81
104
 
82
105
  options = opts.except(:request_id, :wait)
83
106
  return { request_id: request_id, result: send(options.merge(last_packet_id: request_id - 1)) } unless opts.fetch(:wait, true)
84
107
 
85
- connection = options[:serial_obj] || options[:bluetooth_obj] || options[:tcp_obj]
108
+ connection = options[:transport_obj] unless type == :mqtt
86
109
  queue = connection && connection[:from_radio_queue]
87
110
  raise ArgumentError, 'synchronous Admin requires a connected radio receive queue' unless queue
88
111
 
@@ -114,6 +137,11 @@ module Meshtastic
114
137
  lock.exit if acquired
115
138
  end
116
139
 
140
+ private_class_method def self.reject_legacy_transport_options(opts = {})
141
+ legacy = opts.keys & %i[serial_obj bluetooth_obj tcp_obj mqtt_obj]
142
+ raise ArgumentError, "Admin no longer accepts #{legacy.join(', ')}; use transport_obj: with the connected handle" unless legacy.empty?
143
+ end
144
+
117
145
  private_class_method def self.acquire_session(opts = {})
118
146
  connection = opts[:connection]
119
147
  raise ArgumentError, 'automatic Admin sessions require a radio receive queue; supply session_passkey for MQTT' unless connection && connection[:from_radio_queue]
@@ -174,6 +202,7 @@ module Meshtastic
174
202
  end
175
203
 
176
204
  public_class_method def self.decode(opts = {})
205
+ reject_legacy_transport_options(opts)
177
206
  packet = opts[:packet]
178
207
  packet = packet.packet if packet.is_a?(Meshtastic::FromRadio)
179
208
  data = packet.is_a?(Meshtastic::MeshPacket) ? packet.decoded : packet
@@ -186,6 +215,7 @@ module Meshtastic
186
215
  end
187
216
 
188
217
  public_class_method def self.response(opts = {})
218
+ reject_legacy_transport_options(opts)
189
219
  packet = opts[:packet]
190
220
  packet = packet.packet if packet.is_a?(Meshtastic::FromRadio)
191
221
  return nil unless packet.is_a?(Meshtastic::MeshPacket) && packet.decoded&.portnum == :ADMIN_APP
@@ -419,6 +449,14 @@ module Meshtastic
419
449
 
420
450
  public_class_method def self.help
421
451
  puts "USAGE:
452
+ # Classify the actual connected transport handle.
453
+ #{self}.transport_type(
454
+ transport_obj: 'required - handle from Serial.connect, Bluetooth.connect, TCP.connect or MQTT.connect'
455
+ )
456
+ # Returns :serial, :bluetooth, :tcp or :mqtt; TCP includes serial framing keys.
457
+ # Nil, unknown and ambiguous handles are rejected. Old transport option keys are rejected.
458
+ # All send wrappers below require transport_obj and accept the shared send options.
459
+
422
460
  # Encode exactly one AdminMessage payload without modifying the caller.
423
461
  #{self}.encode(
424
462
  message: 'optional - existing AdminMessage to copy instead of a new one',
@@ -427,10 +465,7 @@ module Meshtastic
427
465
 
428
466
  # Send an AdminMessage on ADMIN_APP over a connected transport.
429
467
  #{self}.send(
430
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
431
- bluetooth_obj: 'optional - BLE handle from Meshtastic::Bluetooth.connect',
432
- tcp_obj: 'optional - TCP handle from Meshtastic::TCP.connect',
433
- mqtt_obj: 'optional - MQTT client from Meshtastic::MQTT.connect',
468
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client',
434
469
  to: 'optional - unicast node integer or !eighthex; defaults to connected my_node_num; required for MQTT',
435
470
  from: 'optional - sender number; radio default zero denotes the local PhoneAPI client',
436
471
  channel: 'optional - routing channel index (default zero)',
@@ -478,13 +513,13 @@ module Meshtastic
478
513
 
479
514
  # Reboot the node after a delay.
480
515
  #{self}.reboot(
481
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
516
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client',
482
517
  seconds: 'optional - delay before reboot in seconds (default: 5)'
483
518
  )
484
519
 
485
520
  # Shut down the node after a delay.
486
521
  #{self}.shutdown(
487
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
522
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client',
488
523
  seconds: 'optional - delay before shutdown in seconds (default: 5)'
489
524
  )
490
525
 
@@ -497,7 +532,7 @@ module Meshtastic
497
532
 
498
533
  # Request the node owner User protobuf.
499
534
  #{self}.get_owner(
500
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
535
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
501
536
  )
502
537
 
503
538
  # Write a Channel protobuf to the node.
@@ -533,7 +568,7 @@ module Meshtastic
533
568
 
534
569
  # Request canned-message module strings.
535
570
  #{self}.get_canned_messages(
536
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
571
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
537
572
  )
538
573
 
539
574
  # Set canned-message module strings.
@@ -543,12 +578,12 @@ module Meshtastic
543
578
 
544
579
  # Request DeviceMetadata (firmware version and hardware).
545
580
  #{self}.get_device_metadata(
546
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
581
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
547
582
  )
548
583
 
549
584
  # Request the RTTTL ringtone string.
550
585
  #{self}.get_ringtone(
551
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
586
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
552
587
  )
553
588
 
554
589
  # Set the RTTTL ringtone string.
@@ -558,17 +593,17 @@ module Meshtastic
558
593
 
559
594
  # Request DeviceConnectionStatus.
560
595
  #{self}.get_device_connection_status(
561
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
596
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
562
597
  )
563
598
 
564
599
  # Request remote-hardware pin definitions.
565
600
  #{self}.get_node_remote_hardware_pins(
566
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
601
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
567
602
  )
568
603
 
569
604
  # Ask the node to enter DFU / UF2 mode.
570
605
  #{self}.enter_dfu(
571
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
606
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
572
607
  )
573
608
 
574
609
  # Delete a file on the node filesystem.
@@ -640,7 +675,7 @@ module Meshtastic
640
675
 
641
676
  # Clear the fixed GPS position on the node.
642
677
  #{self}.remove_fixed_position(
643
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
678
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
644
679
  )
645
680
 
646
681
  # Set node wall-clock time.
@@ -650,7 +685,7 @@ module Meshtastic
650
685
 
651
686
  # Request DeviceUIConfig.
652
687
  #{self}.get_ui_config(
653
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
688
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
654
689
  )
655
690
 
656
691
  # Store DeviceUIConfig on the node.
@@ -675,12 +710,12 @@ module Meshtastic
675
710
 
676
711
  # Open a settings edit transaction.
677
712
  #{self}.begin_edit(
678
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
713
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
679
714
  )
680
715
 
681
716
  # Commit a settings edit transaction.
682
717
  #{self}.commit_edit(
683
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
718
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
684
719
  )
685
720
 
686
721
  # Add a SharedContact to the node.
@@ -705,13 +740,13 @@ module Meshtastic
705
740
 
706
741
  # Clear the node database.
707
742
  #{self}.nodedb_reset(
708
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
743
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client',
709
744
  preserve_favorites: 'optional - retain favorite nodes (default true); some firmware roles always preserve favorites'
710
745
  )
711
746
 
712
747
  # Exit the firmware simulator if running.
713
748
  #{self}.exit_simulator(
714
- serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
749
+ transport_obj: 'required - actual connected Serial, Bluetooth, TCP handle or MQTT client'
715
750
  )
716
751
 
717
752
  # Write a SensorConfig protobuf.
@@ -5,11 +5,25 @@ require 'meshtastic/module_config_pb'
5
5
  module Meshtastic
6
6
  class ModuleConfig
7
7
  public_class_method def self.get(opts = {})
8
- Admin.send(opts.merge(get_module_config_request: opts.fetch(:module_config_type, :MQTT_CONFIG)))
8
+ Admin.send(admin_options(opts).merge(get_module_config_request: opts.fetch(:module_config_type, :MQTT_CONFIG)))
9
9
  end
10
10
 
11
11
  public_class_method def self.set(opts = {})
12
- Admin.send(opts.merge(set_module_config: opts.fetch(:module_config)))
12
+ Admin.send(admin_options(opts).merge(set_module_config: opts.fetch(:module_config)))
13
+ end
14
+
15
+ # Keep the external transport aliases out of the Admin API.
16
+ private_class_method def self.admin_options(opts = {})
17
+ options = opts.merge({})
18
+ keys = %i[transport_obj serial_obj bluetooth_obj tcp_obj mqtt_obj]
19
+ selected = keys.reject { |name| options[name].nil? }
20
+ raise ArgumentError, 'provide exactly one transport connection' if selected.length > 1
21
+
22
+ key = selected.first
23
+ connection = options[key] if key
24
+ keys.each { |name| options.delete(name) }
25
+ options[:transport_obj] = connection if key
26
+ options
13
27
  end
14
28
 
15
29
  public_class_method def self.authors
@@ -19,10 +33,14 @@ module Meshtastic
19
33
  public_class_method def self.help
20
34
  puts " USAGE:
21
35
  # Run the get class method for this module.
22
- #{self}.get
36
+ #{self}.get(transport_obj: connection, module_config_type: :MQTT_CONFIG)
23
37
 
24
38
  # Run the set class method for this module.
25
- #{self}.set
39
+ #{self}.set(transport_obj: connection, module_config: module_config)
40
+
41
+ # Legacy serial_obj:, bluetooth_obj:, tcp_obj:, mqtt_obj: remain accepted.
42
+ # Supply one non-nil connection only; mixed connection options are rejected.
43
+ # Remote setters inherit Admin automatic sessions, not persistence readback.
26
44
 
27
45
  # Run the authors class method for this module.
28
46
  #{self}.authors
@@ -11,11 +11,25 @@ module Meshtastic
11
11
  end
12
12
 
13
13
  public_class_method def self.set(opts = {})
14
- Admin.send(opts.merge(set_ringtone_message: opts.fetch(:ringtone)))
14
+ Admin.send(admin_options(opts).merge(set_ringtone_message: opts.fetch(:ringtone)))
15
15
  end
16
16
 
17
17
  public_class_method def self.get(opts = {})
18
- Admin.send(opts.merge(get_ringtone_request: true))
18
+ Admin.send(admin_options(opts).merge(get_ringtone_request: true))
19
+ end
20
+
21
+ # Keep the external transport aliases out of the Admin API.
22
+ private_class_method def self.admin_options(opts = {})
23
+ options = opts.merge({})
24
+ keys = %i[transport_obj serial_obj bluetooth_obj tcp_obj mqtt_obj]
25
+ selected = keys.reject { |name| options[name].nil? }
26
+ raise ArgumentError, 'provide exactly one transport connection' if selected.length > 1
27
+
28
+ key = selected.first
29
+ connection = options[key] if key
30
+ keys.each { |name| options.delete(name) }
31
+ options[:transport_obj] = connection if key
32
+ options
19
33
  end
20
34
 
21
35
  public_class_method def self.authors
@@ -28,10 +42,14 @@ module Meshtastic
28
42
  #{self}.encode
29
43
 
30
44
  # Run the set class method for this module.
31
- #{self}.set
45
+ #{self}.set(transport_obj: connection, ringtone: ringtone)
32
46
 
33
47
  # Run the get class method for this module.
34
- #{self}.get
48
+ #{self}.get(transport_obj: connection)
49
+
50
+ # Legacy serial_obj:, bluetooth_obj:, tcp_obj:, mqtt_obj: remain accepted.
51
+ # Supply one non-nil connection only; mixed connection options are rejected.
52
+ # Remote setters inherit Admin automatic sessions, not persistence readback.
35
53
 
36
54
  # Run the authors class method for this module.
37
55
  #{self}.authors
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Meshtastic
4
- VERSION = '0.0.182'
4
+ VERSION = '0.0.183'
5
5
  end
@@ -2,6 +2,13 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
+ RSpec.describe Meshtastic::Admin::Channel do
6
+ it 'documents transport_obj rather than legacy connection keywords' do
7
+ expect { described_class.help }.to output(/transport_obj:/).to_stdout
8
+ expect { described_class.help }.not_to output(/serial_obj:|bluetooth_obj:|tcp_obj:|mqtt_obj:/).to_stdout
9
+ end
10
+ end
11
+
5
12
  shared_context 'channel serial framing' do
6
13
  def fake_serial_obj
7
14
  written = +''.b
@@ -26,16 +33,23 @@ end
26
33
  describe Meshtastic::Admin::Channel do
27
34
  include_context 'channel serial framing'
28
35
 
36
+ it 'rejects legacy connection keys before filtering or building channel options' do
37
+ %i[serial_obj bluetooth_obj tcp_obj mqtt_obj].each do |key|
38
+ expect { described_class.set(transport_obj: fake_serial_obj, key => nil, index: -1) }
39
+ .to raise_error(ArgumentError, /#{key}.*transport_obj/)
40
+ end
41
+ end
42
+
29
43
  it 'requests a channel by index via Admin' do
30
44
  serial_obj = fake_serial_obj
31
- described_class.get(serial_obj: serial_obj, index: 1)
45
+ described_class.get(transport_obj: serial_obj, index: 1)
32
46
  expect(decode_admin(serial_obj).get_channel_request).to eq(2)
33
47
  end
34
48
 
35
49
  [0, 7].each do |index|
36
50
  it "requests zero-based slot #{index} with its one-based wire index" do
37
51
  serial_obj = fake_serial_obj
38
- described_class.get(serial_obj: serial_obj, index: index)
52
+ described_class.get(transport_obj: serial_obj, index: index)
39
53
  expect(decode_admin(serial_obj).get_channel_request).to eq(index + 1)
40
54
  end
41
55
  end
@@ -43,7 +57,7 @@ describe Meshtastic::Admin::Channel do
43
57
  it 'sets a Channel protobuf including settings and role' do
44
58
  serial_obj = fake_serial_obj
45
59
  settings = described_class.build_settings(name: 'LongFast', uplink_enabled: true)
46
- described_class.set(serial_obj: serial_obj, index: 0, role: :PRIMARY, settings: settings)
60
+ described_class.set(transport_obj: serial_obj, index: 0, role: :PRIMARY, settings: settings)
47
61
  channel = decode_admin(serial_obj).set_channel
48
62
  expect(channel.index).to eq(0)
49
63
  expect(channel.role).to eq(:PRIMARY)
@@ -124,7 +138,7 @@ describe Meshtastic::Admin::Channel, 'channel URL support' do
124
138
  it 'rejects invalid settings lengths and slot indexes before any write' do
125
139
  [-1, 8, 0.5, 'junk'].each do |index|
126
140
  serial_obj = fake_serial_obj
127
- expect { described_class.set(serial_obj: serial_obj, index: index, role: :PRIMARY) }.to raise_error(ArgumentError)
141
+ expect { described_class.set(transport_obj: serial_obj, index: index, role: :PRIMARY) }.to raise_error(ArgumentError)
128
142
  expect(serial_obj[:written]).to be_empty
129
143
  end
130
144
  expect { described_class.build_settings(psk: 'bad') }.to raise_error(ArgumentError)
@@ -137,7 +151,7 @@ describe Meshtastic::Admin::Channel, 'channel URL support' do
137
151
  it 'validates and overlays supplied channel/settings before transmission' do
138
152
  serial_obj = fake_serial_obj
139
153
  original = Meshtastic::Channel.new(index: 2, role: :SECONDARY, settings: { name: 'before', uplink_enabled: true })
140
- described_class.set(serial_obj: serial_obj, channel: original, name: 'after', uplink_enabled: false)
154
+ described_class.set(transport_obj: serial_obj, channel: original, name: 'after', uplink_enabled: false)
141
155
  written = decode_admin(serial_obj).set_channel
142
156
  expect(written.index).to eq(2)
143
157
  expect(written.settings.name).to eq('after')
@@ -145,7 +159,7 @@ describe Meshtastic::Admin::Channel, 'channel URL support' do
145
159
  expect(original.settings.name).to eq('before')
146
160
  expect(described_class.build(settings: { name: 'hash' }).settings.name).to eq('hash')
147
161
  invalid = Meshtastic::Channel.new(index: 8)
148
- expect { described_class.set(serial_obj: fake_serial_obj, channel: invalid) }.to raise_error(ArgumentError)
162
+ expect { described_class.set(transport_obj: fake_serial_obj, channel: invalid) }.to raise_error(ArgumentError)
149
163
  expect { described_class.build(settings: { psk: 'bad' }) }.to raise_error(ArgumentError)
150
164
  end
151
165
 
@@ -153,7 +167,7 @@ describe Meshtastic::Admin::Channel, 'channel URL support' do
153
167
  serial_obj = fake_serial_obj
154
168
  channel_set = Meshtastic::ChannelSet.new(settings: [{ name: 'main' }, { name: 'other' }], lora_config: { region: :US })
155
169
  url = "https://meshtastic.org/e/##{Base64.urlsafe_encode64(channel_set.to_proto, padding: false)}"
156
- results = described_class.apply_url(serial_obj: serial_obj, url: url, session_passkey: 'test-key')
170
+ results = described_class.apply_url(transport_obj: serial_obj, url: url, session_passkey: 'test-key')
157
171
  frames = serial_obj[:written].dup
158
172
  messages = []
159
173
  until frames.empty?
@@ -176,11 +190,11 @@ describe Meshtastic::Admin::Channel, 'channel URL support' do
176
190
  serial_obj = fake_serial_obj
177
191
  invalid = Meshtastic::ChannelSet.new(settings: [{ name: 'good' }, { psk: 'bad' }])
178
192
  url = "https://meshtastic.org/e/##{Base64.urlsafe_encode64(invalid.to_proto, padding: false)}"
179
- expect { described_class.apply_url(serial_obj: serial_obj, url: url) }.to raise_error(ArgumentError)
193
+ expect { described_class.apply_url(transport_obj: serial_obj, url: url) }.to raise_error(ArgumentError)
180
194
  expect(serial_obj[:written]).to be_empty
181
195
  valid = Meshtastic::ChannelSet.new(settings: [{ name: 'test' }])
182
196
  url = "https://meshtastic.org/e/?add=true##{Base64.urlsafe_encode64(valid.to_proto, padding: false)}"
183
- expect { described_class.apply_url(serial_obj: serial_obj, url: url) }.to raise_error(ArgumentError, /add-only/)
197
+ expect { described_class.apply_url(transport_obj: serial_obj, url: url) }.to raise_error(ArgumentError, /add-only/)
184
198
  expect(serial_obj[:written]).to be_empty
185
199
  end
186
200
 
@@ -2,6 +2,13 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
+ RSpec.describe Meshtastic::Admin::Config do
6
+ it 'documents transport_obj rather than legacy connection keywords' do
7
+ expect { described_class.help }.to output(/transport_obj:/).to_stdout
8
+ expect { described_class.help }.not_to output(/serial_obj:|bluetooth_obj:|tcp_obj:|mqtt_obj:/).to_stdout
9
+ end
10
+ end
11
+
5
12
  describe Meshtastic::Admin::Config do
6
13
  def fake_serial_obj
7
14
  written = +''.b
@@ -22,9 +29,18 @@ describe Meshtastic::Admin::Config do
22
29
  Meshtastic::AdminMessage.decode(Meshtastic::ToRadio.decode(body).packet.decoded.payload)
23
30
  end
24
31
 
32
+ it 'rejects legacy connection keys rather than silently filtering them' do
33
+ %i[serial_obj bluetooth_obj tcp_obj mqtt_obj].each do |key|
34
+ handle = fake_serial_obj
35
+ expect { described_class.set_device_ui(transport_obj: handle, key => nil, device_ui: {}) }
36
+ .to raise_error(ArgumentError, /#{key}/)
37
+ expect(handle[:written]).to be_empty
38
+ end
39
+ end
40
+
25
41
  it 'requests LoRa config via Admin' do
26
42
  serial_obj = fake_serial_obj
27
- described_class.get(serial_obj: serial_obj, config_type: :LORA_CONFIG)
43
+ described_class.get(transport_obj: serial_obj, config_type: :LORA_CONFIG)
28
44
  expect(decode_admin(serial_obj).get_config_request).to eq(:LORA_CONFIG)
29
45
  end
30
46
 
@@ -32,13 +48,13 @@ describe Meshtastic::Admin::Config do
32
48
  serial_obj = fake_serial_obj
33
49
  config = Meshtastic::Config.new
34
50
  config.device = Meshtastic::Config::DeviceConfig.new(role: :CLIENT)
35
- described_class.set(serial_obj: serial_obj, config: config)
51
+ described_class.set(transport_obj: serial_obj, config: config)
36
52
  expect(decode_admin(serial_obj).set_config.device.role).to eq(:CLIENT)
37
53
  end
38
54
 
39
55
  it 'requests each ConfigType via named getters' do
40
56
  serial_obj = fake_serial_obj
41
- described_class.get_lora(serial_obj: serial_obj)
57
+ described_class.get_lora(transport_obj: serial_obj)
42
58
  expect(decode_admin(serial_obj).get_config_request).to eq(:LORA_CONFIG)
43
59
  end
44
60
 
@@ -48,7 +64,7 @@ describe Meshtastic::Admin::Config do
48
64
  it "writes the #{field.name} section through real serial framing" do
49
65
  serial_obj = fake_serial_obj
50
66
  section = field.subtype.msgclass.new
51
- described_class.public_send("set_#{field.name}", serial_obj: serial_obj, field.name.to_sym => section)
67
+ described_class.public_send("set_#{field.name}", transport_obj: serial_obj, field.name.to_sym => section)
52
68
  config = decode_admin(serial_obj).set_config
53
69
  expect(config.payload_variant).to eq(field.name.to_sym)
54
70
  expect(config[field.name]).to eq(section)
@@ -57,7 +73,7 @@ describe Meshtastic::Admin::Config do
57
73
 
58
74
  it 'rejects an empty config before writing bytes' do
59
75
  serial_obj = fake_serial_obj
60
- expect { described_class.set(serial_obj: serial_obj, config: Meshtastic::Config.new) }.to raise_error(ArgumentError)
76
+ expect { described_class.set(transport_obj: serial_obj, config: Meshtastic::Config.new) }.to raise_error(ArgumentError)
61
77
  expect(serial_obj[:written]).to be_empty
62
78
  end
63
79
 
@@ -66,31 +82,31 @@ describe Meshtastic::Admin::Config do
66
82
 
67
83
  it "accepts a field hash for #{field.name}" do
68
84
  serial_obj = fake_serial_obj
69
- described_class.public_send("set_#{field.name}", serial_obj: serial_obj, field.name.to_sym => {})
85
+ described_class.public_send("set_#{field.name}", transport_obj: serial_obj, field.name.to_sym => {})
70
86
  expect(decode_admin(serial_obj).set_config.payload_variant).to eq(field.name.to_sym)
71
87
  end
72
88
 
73
89
  it "rejects missing #{field.name} before writing bytes" do
74
90
  serial_obj = fake_serial_obj
75
- expect { described_class.public_send("set_#{field.name}", serial_obj: serial_obj) }.to(raise_error { |error| expect([ArgumentError, KeyError]).to include(error.class) })
91
+ expect { described_class.public_send("set_#{field.name}", transport_obj: serial_obj) }.to(raise_error { |error| expect([ArgumentError, KeyError]).to include(error.class) })
76
92
  expect(serial_obj[:written]).to be_empty
77
93
  end
78
94
  end
79
95
 
80
96
  it 'uses dedicated device UI requests and stores instead of firmware no-op Config fields' do
81
97
  serial_obj = fake_serial_obj
82
- described_class.get_device_ui(serial_obj: serial_obj)
98
+ described_class.get_device_ui(transport_obj: serial_obj)
83
99
  expect(decode_admin(serial_obj).payload_variant).to eq(:get_ui_config_request)
84
100
  serial_obj = fake_serial_obj
85
- described_class.set_device_ui(serial_obj: serial_obj, device_ui: {})
101
+ described_class.set_device_ui(transport_obj: serial_obj, device_ui: {})
86
102
  expect(decode_admin(serial_obj).payload_variant).to eq(:store_ui_config)
87
103
  end
88
104
 
89
105
  it 'rejects the read-only session-key placeholder without transmitting' do
90
106
  serial_obj = fake_serial_obj
91
107
  config = Meshtastic::Config.new(sessionkey: {})
92
- expect { described_class.set(serial_obj: serial_obj, config: config) }.to raise_error(ArgumentError, /request-only/)
93
- expect { described_class.set_sessionkey(serial_obj: serial_obj, sessionkey: {}) }.to raise_error(ArgumentError, /request-only/)
108
+ expect { described_class.set(transport_obj: serial_obj, config: config) }.to raise_error(ArgumentError, /request-only/)
109
+ expect { described_class.set_sessionkey(transport_obj: serial_obj, sessionkey: {}) }.to raise_error(ArgumentError, /request-only/)
94
110
  expect(serial_obj[:written]).to be_empty
95
111
  end
96
112
 
@@ -101,7 +117,7 @@ describe Meshtastic::Admin::Config do
101
117
  }.each do |section, config_type|
102
118
  it "requests #{section} using its protocol ConfigType" do
103
119
  serial_obj = fake_serial_obj
104
- described_class.public_send("get_#{section}", serial_obj: serial_obj)
120
+ described_class.public_send("get_#{section}", transport_obj: serial_obj)
105
121
  message = decode_admin(serial_obj)
106
122
  expect(message.payload_variant).to eq(:get_config_request)
107
123
  expect(message.get_config_request).to eq(config_type)