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.
- checksums.yaml +4 -4
- data/documentation/README.md +4 -0
- data/documentation/admin-channel.md +6 -6
- data/documentation/admin-config.md +5 -5
- data/documentation/admin-firmware-hex.md +57 -0
- data/documentation/admin-firmware-serial.md +1 -1
- data/documentation/admin-firmware-uf2.md +94 -0
- data/documentation/admin-firmware.md +52 -10
- data/documentation/admin.md +8 -2
- data/documentation/module-config.md +8 -4
- data/documentation/rtttl.md +8 -4
- data/lib/meshtastic/admin/channel.rb +6 -3
- data/lib/meshtastic/admin/config.rb +14 -14
- data/lib/meshtastic/admin/firmware/hex.rb +209 -0
- data/lib/meshtastic/admin/firmware/uf2.rb +166 -0
- data/lib/meshtastic/admin/firmware.rb +55 -13
- data/lib/meshtastic/admin.rb +58 -23
- data/lib/meshtastic/module_config.rb +22 -4
- data/lib/meshtastic/rtttl.rb +22 -4
- data/lib/meshtastic/version.rb +1 -1
- data/spec/lib/meshtastic/admin/channel_spec.rb +23 -9
- data/spec/lib/meshtastic/admin/config_spec.rb +28 -12
- data/spec/lib/meshtastic/admin/firmware/hex_spec.rb +160 -0
- data/spec/lib/meshtastic/admin/firmware/uf2_spec.rb +234 -0
- data/spec/lib/meshtastic/admin/firmware_spec.rb +106 -3
- data/spec/lib/meshtastic/admin_spec.rb +106 -40
- data/spec/lib/meshtastic/module_config_spec.rb +47 -0
- data/spec/lib/meshtastic/rtttl_spec.rb +47 -0
- metadata +7 -1
|
@@ -27,9 +27,68 @@ end
|
|
|
27
27
|
describe Meshtastic::Admin do
|
|
28
28
|
include AdminSpecHelpers
|
|
29
29
|
|
|
30
|
+
it 'classifies actual connected transport shapes with TCP taking precedence over serial framing' do
|
|
31
|
+
serial = fake_serial_obj
|
|
32
|
+
expect(described_class.transport_type(transport_obj: serial)).to eq(:serial)
|
|
33
|
+
expect(described_class.transport_type(transport_obj: serial.merge(tcp_socket: serial[:serial_conn]))).to eq(:tcp)
|
|
34
|
+
expect(described_class.transport_type(transport_obj: { bluetooth_conn: Object.new })).to eq(:bluetooth)
|
|
35
|
+
expect(described_class.transport_type(transport_obj: MQTTClient.new)).to eq(:mqtt)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'rejects missing, unknown and ambiguous transport handles' do
|
|
39
|
+
[nil, Object.new, {}, { serial_conn: nil }, { tcp_socket: Object.new }, { tcp_socket: nil, serial_conn: Object.new },
|
|
40
|
+
{ bluetooth_conn: Object.new, serial_conn: Object.new },
|
|
41
|
+
{ bluetooth_conn: Object.new, tcp_socket: Object.new, serial_conn: Object.new }].each do |handle|
|
|
42
|
+
expect { described_class.transport_type(transport_obj: handle) }.to raise_error(ArgumentError, /transport_obj/)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'rejects legacy transport options with migration guidance even alongside transport_obj' do
|
|
47
|
+
%i[serial_obj bluetooth_obj tcp_obj mqtt_obj].each do |key|
|
|
48
|
+
%i[transport_type encode decode response send request reboot].each do |method|
|
|
49
|
+
[nil, fake_serial_obj].each do |handle|
|
|
50
|
+
options = { key => nil, transport_obj: handle }
|
|
51
|
+
options[:get_owner_request] = true unless %i[transport_type reboot].include?(method)
|
|
52
|
+
expect { described_class.public_send(method, options) }.to raise_error(ArgumentError, /#{key}.*use transport_obj:/)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'maps only the inferred transport key at the delivery boundary without changing caller options' do
|
|
59
|
+
serial = fake_serial_obj
|
|
60
|
+
handles = { serial: serial, tcp: serial.merge(tcp_socket: serial[:serial_conn]),
|
|
61
|
+
bluetooth: { bluetooth_conn: Object.new, my_node_num: 0xb0b }, mqtt: MQTTClient.new }
|
|
62
|
+
handles.each do |type, handle|
|
|
63
|
+
options = { transport_obj: handle, to: '!aabbccdd', get_owner_request: true }
|
|
64
|
+
expect(Meshtastic).to receive(:deliver_data) do |delivery|
|
|
65
|
+
expect(delivery.keys & %i[serial_obj bluetooth_obj tcp_obj mqtt_obj]).to eq([:"#{type}_obj"])
|
|
66
|
+
expect(delivery[:"#{type}_obj"]).to equal(handle)
|
|
67
|
+
expect(delivery).not_to have_key(:transport_obj)
|
|
68
|
+
end
|
|
69
|
+
described_class.send(options)
|
|
70
|
+
expect(options).to eq(transport_obj: handle, to: '!aabbccdd', get_owner_request: true)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'publishes MQTT Admin protobufs with an explicit passkey but rejects synchronous readback and PKI' do
|
|
75
|
+
client = MQTTClient.new(client_id: 'aabbccdd')
|
|
76
|
+
published = []
|
|
77
|
+
client.define_singleton_method(:publish) { |topic, bytes| published << [topic, bytes] }
|
|
78
|
+
options = { transport_obj: client, to: '!aabbccee', session_passkey: '12345678', psks: { LongFast: 'AQ==' } }
|
|
79
|
+
described_class.reboot(options)
|
|
80
|
+
expect(published.length).to eq(1)
|
|
81
|
+
envelope = Meshtastic::ServiceEnvelope.decode(published.first.last)
|
|
82
|
+
expect(envelope.packet.to).to eq(0xaabbccee)
|
|
83
|
+
expect(envelope.packet.encrypted).not_to be_empty
|
|
84
|
+
expect { described_class.request(options.merge(get_owner_request: true)) }.to raise_error(ArgumentError, /receive queue/)
|
|
85
|
+
expect { described_class.reboot(options.merge(pki_encrypted: true)) }.to raise_error(ArgumentError, /PKI|pki|public.key/)
|
|
86
|
+
expect(published.length).to eq(1)
|
|
87
|
+
end
|
|
88
|
+
|
|
30
89
|
it 'sends a reboot AdminMessage on ADMIN_APP' do
|
|
31
90
|
serial_obj = fake_serial_obj
|
|
32
|
-
described_class.reboot(
|
|
91
|
+
described_class.reboot(transport_obj: serial_obj, seconds: 7)
|
|
33
92
|
packet, admin = decode_admin(serial_obj)
|
|
34
93
|
expect(packet.decoded.portnum).to eq(:ADMIN_APP)
|
|
35
94
|
expect(admin.reboot_seconds).to eq(7)
|
|
@@ -37,7 +96,7 @@ describe Meshtastic::Admin do
|
|
|
37
96
|
|
|
38
97
|
it 'sends a set_owner AdminMessage' do
|
|
39
98
|
serial_obj = fake_serial_obj
|
|
40
|
-
described_class.set_owner(
|
|
99
|
+
described_class.set_owner(transport_obj: serial_obj, long_name: 'Test Node', short_name: 'TN')
|
|
41
100
|
_packet, admin = decode_admin(serial_obj)
|
|
42
101
|
expect(admin.set_owner.long_name).to eq('Test Node')
|
|
43
102
|
expect(admin.set_owner.short_name).to eq('TN')
|
|
@@ -62,7 +121,7 @@ describe Meshtastic::Admin do
|
|
|
62
121
|
}
|
|
63
122
|
examples.each do |meth, (field, expected)|
|
|
64
123
|
serial_obj[:written].clear
|
|
65
|
-
described_class.public_send(meth,
|
|
124
|
+
described_class.public_send(meth, transport_obj: serial_obj)
|
|
66
125
|
_packet, admin = decode_admin(serial_obj)
|
|
67
126
|
expect(admin.public_send(field)).to eq(expected), meth.to_s
|
|
68
127
|
end
|
|
@@ -70,16 +129,16 @@ describe Meshtastic::Admin do
|
|
|
70
129
|
|
|
71
130
|
it 'sends factory resets, file delete, favorites, and edit transactions' do
|
|
72
131
|
serial_obj = fake_serial_obj
|
|
73
|
-
described_class.factory_reset_config(
|
|
132
|
+
described_class.factory_reset_config(transport_obj: serial_obj)
|
|
74
133
|
expect(decode_admin(serial_obj).last.factory_reset_config).to eq(1)
|
|
75
134
|
serial_obj[:written].clear
|
|
76
|
-
described_class.factory_reset_device(
|
|
135
|
+
described_class.factory_reset_device(transport_obj: serial_obj)
|
|
77
136
|
expect(decode_admin(serial_obj).last.factory_reset_device).to eq(1)
|
|
78
137
|
serial_obj[:written].clear
|
|
79
|
-
described_class.delete_file(
|
|
138
|
+
described_class.delete_file(transport_obj: serial_obj, path: '/prefs.json')
|
|
80
139
|
expect(decode_admin(serial_obj).last.delete_file_request).to eq('/prefs.json')
|
|
81
140
|
serial_obj[:written].clear
|
|
82
|
-
described_class.set_favorite_node(
|
|
141
|
+
described_class.set_favorite_node(transport_obj: serial_obj, node_num: 0xb0b)
|
|
83
142
|
expect(decode_admin(serial_obj).last.set_favorite_node).to eq(0xb0b)
|
|
84
143
|
end
|
|
85
144
|
end
|
|
@@ -117,11 +176,11 @@ describe Meshtastic::Admin, 'automatic sessions' do
|
|
|
117
176
|
reply = Meshtastic::AdminMessage.new(get_config_response: Meshtastic::Config.new, session_passkey: '12345678')
|
|
118
177
|
connection[:from_radio_queue] << admin_reply(packet, message: reply)
|
|
119
178
|
end
|
|
120
|
-
2.times { described_class.set_owner(
|
|
179
|
+
2.times { described_class.set_owner(transport_obj: handle, to: '!aabbccdd', long_name: 'Remote', timeout: 0.2) }
|
|
121
180
|
messages = handle[:sent].map { |packet| Meshtastic::AdminMessage.decode(packet.decoded.payload) }
|
|
122
181
|
expect(messages.map(&:payload_variant)).to eq(%i[get_config_request set_owner set_owner])
|
|
123
182
|
expect(messages.drop(1).map(&:session_passkey)).to eq(%w[12345678 12345678])
|
|
124
|
-
described_class.set_owner(
|
|
183
|
+
described_class.set_owner(transport_obj: handle, to: '!aabbccee', long_name: 'Other', timeout: 0.2)
|
|
125
184
|
expect(handle[:sent].length).to eq(5)
|
|
126
185
|
end
|
|
127
186
|
|
|
@@ -137,7 +196,7 @@ describe Meshtastic::Admin, 'automatic sessions' do
|
|
|
137
196
|
decoded: Meshtastic::Data.new(portnum: :ROUTING_APP, request_id: packet.id, payload: Meshtastic::Routing.new(error_reason: reason).to_proto)))
|
|
138
197
|
end
|
|
139
198
|
end
|
|
140
|
-
options = {
|
|
199
|
+
options = { transport_obj: handle, to: '!aabbccdd', reboot_seconds: 5, timeout: 0.2 }
|
|
141
200
|
expect(described_class.request(options)).to include(variant: :routing, value: :NONE)
|
|
142
201
|
reason = :ADMIN_BAD_SESSION_KEY
|
|
143
202
|
expect { described_class.request(options) }.to raise_error(Meshtastic::Admin::RoutingError)
|
|
@@ -156,7 +215,7 @@ describe Meshtastic::Admin, 'automatic sessions' do
|
|
|
156
215
|
reply = Meshtastic::AdminMessage.new(get_config_response: Meshtastic::Config.new, session_passkey: key)
|
|
157
216
|
connection[:from_radio_queue] << admin_reply(packet, message: reply)
|
|
158
217
|
end
|
|
159
|
-
options = {
|
|
218
|
+
options = { transport_obj: handle, to: '!aabbccdd', timeout: 0.5 }
|
|
160
219
|
described_class.request(options.merge(get_config_request: :SESSIONKEY_CONFIG))
|
|
161
220
|
described_class.reboot(options)
|
|
162
221
|
expect(handle[:sent].length).to eq(2)
|
|
@@ -174,7 +233,7 @@ describe Meshtastic::Admin, 'automatic sessions' do
|
|
|
174
233
|
connection[:from_radio_queue] << admin_reply(packet, message: Meshtastic::AdminMessage.new(get_config_response: Meshtastic::Config.new))
|
|
175
234
|
end
|
|
176
235
|
handle[:admin_sessions] = { 0xaabbccdd => { key: 'old-key!', expires_at: Process.clock_gettime(Process::CLOCK_MONOTONIC) + 100 } }
|
|
177
|
-
options = {
|
|
236
|
+
options = { transport_obj: handle, to: '!aabbccdd', timeout: 0.2 }
|
|
178
237
|
expect { described_class.reboot(options.merge(refresh_session: true)) }.to raise_error(ArgumentError, /eight-byte/)
|
|
179
238
|
expect { described_class.reboot(options) }.to raise_error(ArgumentError, /eight-byte/)
|
|
180
239
|
expect(handle[:sent].map { |packet| Meshtastic::AdminMessage.decode(packet.decoded.payload).payload_variant }).to eq(%i[get_config_request get_config_request])
|
|
@@ -184,7 +243,7 @@ describe Meshtastic::Admin, 'automatic sessions' do
|
|
|
184
243
|
handle = responding_serial { |_connection, _packet| nil }
|
|
185
244
|
expect do
|
|
186
245
|
Timeout.timeout(0.5, RuntimeError, 'outer deadline exceeded') do
|
|
187
|
-
described_class.request(
|
|
246
|
+
described_class.request(transport_obj: handle, to: '!aabbccdd', reboot_seconds: 5, wait: false, timeout: 0.15)
|
|
188
247
|
end
|
|
189
248
|
end.to raise_error(Timeout::Error, /Admin response/)
|
|
190
249
|
expect(handle[:sent].length).to eq(1)
|
|
@@ -198,11 +257,11 @@ describe Meshtastic::Admin, 'automatic sessions' do
|
|
|
198
257
|
release.pop
|
|
199
258
|
connection[:from_radio_queue] << admin_reply(packet)
|
|
200
259
|
end
|
|
201
|
-
worker = Thread.new { described_class.request(
|
|
260
|
+
worker = Thread.new { described_class.request(transport_obj: handle, get_device_metadata_request: true, timeout: 1) }
|
|
202
261
|
entered.pop
|
|
203
262
|
begin
|
|
204
263
|
expect do
|
|
205
|
-
Timeout.timeout(0.3) { described_class.request(
|
|
264
|
+
Timeout.timeout(0.3) { described_class.request(transport_obj: handle, get_device_metadata_request: true, timeout: 0.1) }
|
|
206
265
|
end.to raise_error(IOError, /already active/)
|
|
207
266
|
ensure
|
|
208
267
|
release << true
|
|
@@ -224,14 +283,16 @@ describe Meshtastic::Admin, 'synchronous requests' do
|
|
|
224
283
|
connection[:from_radio_queue] << unrelated
|
|
225
284
|
connection[:from_radio_queue] << admin_reply(packet)
|
|
226
285
|
end
|
|
227
|
-
expect(described_class.request(
|
|
286
|
+
expect(described_class.request(transport_obj: handle, get_device_metadata_request: true, timeout: 0.2)[:value].firmware_version).to eq('test-version')
|
|
228
287
|
expect(handle[:from_radio_queue].pop(true)).to eq(unrelated)
|
|
229
288
|
end
|
|
230
289
|
|
|
231
290
|
%i[tcp_obj bluetooth_obj].each do |transport|
|
|
232
291
|
it "reads correlated replies through the real #{transport} send path" do
|
|
233
292
|
handle = if transport == :tcp_obj
|
|
234
|
-
responding_serial { |connection, packet| connection[:from_radio_queue] << admin_reply(packet) }
|
|
293
|
+
responding_serial { |connection, packet| connection[:from_radio_queue] << admin_reply(packet) }.tap do |tcp|
|
|
294
|
+
tcp[:tcp_socket] = tcp[:serial_conn]
|
|
295
|
+
end
|
|
235
296
|
else
|
|
236
297
|
{ my_node_num: 0xb0b, from_radio_queue: Queue.new, tx_mutex: Mutex.new, bluetooth_conn: Object.new }
|
|
237
298
|
end
|
|
@@ -243,7 +304,7 @@ describe Meshtastic::Admin, 'synchronous requests' do
|
|
|
243
304
|
bytes.bytesize
|
|
244
305
|
end
|
|
245
306
|
end
|
|
246
|
-
result = described_class.request(
|
|
307
|
+
result = described_class.request(transport_obj: handle, get_device_metadata_request: true, timeout: 0.5)
|
|
247
308
|
expect(result[:value].firmware_version).to eq('test-version')
|
|
248
309
|
end
|
|
249
310
|
end
|
|
@@ -257,7 +318,7 @@ describe Meshtastic::Admin, 'synchronous requests' do
|
|
|
257
318
|
preserved.each { |message| connection[:from_radio_queue] << message }
|
|
258
319
|
end
|
|
259
320
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
260
|
-
expect { described_class.request(
|
|
321
|
+
expect { described_class.request(transport_obj: handle, get_device_metadata_request: true, timeout: 0.15) }.to raise_error(Timeout::Error)
|
|
261
322
|
expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start).to be < 0.6
|
|
262
323
|
expect(preserved.map { handle[:from_radio_queue].pop(true) }).to eq(preserved)
|
|
263
324
|
end
|
|
@@ -267,18 +328,18 @@ describe Meshtastic::Admin, 'synchronous requests' do
|
|
|
267
328
|
connection[:from_radio_queue] << admin_reply(packet, message: Meshtastic::AdminMessage.new(get_config_response: Meshtastic::Config.new))
|
|
268
329
|
end
|
|
269
330
|
expect do
|
|
270
|
-
described_class.reboot(
|
|
331
|
+
described_class.reboot(transport_obj: handle, to: '!aabbccdd', timeout: 0.2)
|
|
271
332
|
end.to raise_error(ArgumentError, /eight-byte passkey/)
|
|
272
333
|
expect(handle[:sent].map { |packet| Meshtastic::AdminMessage.decode(packet.decoded.payload).payload_variant }).to eq([:get_config_request])
|
|
273
334
|
expect do
|
|
274
|
-
described_class.reboot(
|
|
335
|
+
described_class.reboot(transport_obj: MQTTClient.new, to: '!aabbccdd')
|
|
275
336
|
end.to raise_error(ArgumentError, /supply session_passkey/)
|
|
276
337
|
end
|
|
277
338
|
|
|
278
339
|
it 'rejects invalid timeouts before submitting any data' do
|
|
279
340
|
handle = responding_serial { |_connection, _packet| raise 'must not send' }
|
|
280
341
|
[0, -1, nil, Float::INFINITY, Float::NAN, '1'].each do |timeout|
|
|
281
|
-
expect { described_class.request(
|
|
342
|
+
expect { described_class.request(transport_obj: handle, get_owner_request: true, timeout: timeout) }.to raise_error(ArgumentError, /timeout/)
|
|
282
343
|
end
|
|
283
344
|
expect(handle[:sent]).to be_empty
|
|
284
345
|
end
|
|
@@ -290,7 +351,7 @@ describe Meshtastic::Admin, 'synchronous requests' do
|
|
|
290
351
|
connection[:from_radio_queue].close
|
|
291
352
|
end
|
|
292
353
|
expect do
|
|
293
|
-
described_class.request(
|
|
354
|
+
described_class.request(transport_obj: handle, get_owner_request: true, timeout: 0.2)
|
|
294
355
|
end.to raise_error(Timeout::Error)
|
|
295
356
|
expect(handle[:from_radio_queue].pop(true)).to eq(unrelated)
|
|
296
357
|
expect(handle[:from_radio_queue]).to be_closed
|
|
@@ -306,7 +367,7 @@ describe Meshtastic::Admin, 'synchronous requests' do
|
|
|
306
367
|
end
|
|
307
368
|
end
|
|
308
369
|
expect do
|
|
309
|
-
described_class.request(
|
|
370
|
+
described_class.request(transport_obj: handle, to: '!aabbccdd', get_device_metadata_request: true, timeout: 0.1)
|
|
310
371
|
end.to raise_error(Meshtastic::Admin::RoutingError, /NOT_AUTHORIZED/)
|
|
311
372
|
expect(handle[:from_radio_queue].size).to eq(1)
|
|
312
373
|
end
|
|
@@ -320,7 +381,7 @@ describe Meshtastic::Admin, 'synchronous requests' do
|
|
|
320
381
|
unrelated.each { |message| connection[:from_radio_queue] << message }
|
|
321
382
|
connection[:from_radio_queue] << admin_reply(packet)
|
|
322
383
|
end
|
|
323
|
-
result = described_class.request(
|
|
384
|
+
result = described_class.request(transport_obj: handle, message: Meshtastic::AdminMessage.new(get_device_metadata_request: true), timeout: 0.2)
|
|
324
385
|
expect(result[:value].firmware_version).to eq('test-version')
|
|
325
386
|
expect(result[:request_id]).to eq(handle[:sent].last.id)
|
|
326
387
|
expect(unrelated.map { handle[:from_radio_queue].pop(true) }).to eq(unrelated)
|
|
@@ -352,11 +413,11 @@ describe Meshtastic::Admin, 'validation and response handling' do
|
|
|
352
413
|
serial_obj = fake_serial_obj
|
|
353
414
|
[0, 7].each do |index|
|
|
354
415
|
serial_obj[:written].clear
|
|
355
|
-
described_class.get_channel(
|
|
416
|
+
described_class.get_channel(transport_obj: serial_obj, index: index)
|
|
356
417
|
expect(decode_admin(serial_obj).last.get_channel_request).to eq(index + 1)
|
|
357
418
|
end
|
|
358
419
|
[-1, 8, '1', 1.5].each do |index|
|
|
359
|
-
expect { described_class.get_channel(
|
|
420
|
+
expect { described_class.get_channel(transport_obj: serial_obj, index: index) }.to raise_error(ArgumentError)
|
|
360
421
|
end
|
|
361
422
|
end
|
|
362
423
|
|
|
@@ -376,20 +437,20 @@ describe Meshtastic::Admin, 'validation and response handling' do
|
|
|
376
437
|
|
|
377
438
|
it 'targets the connected node locally and requests replies only for getters by default' do
|
|
378
439
|
serial_obj = fake_serial_obj
|
|
379
|
-
described_class.reboot(
|
|
440
|
+
described_class.reboot(transport_obj: serial_obj)
|
|
380
441
|
packet, = decode_admin(serial_obj)
|
|
381
442
|
expect(packet.to).to eq(0xb0b)
|
|
382
443
|
expect(packet.from).to eq(0)
|
|
383
444
|
expect(packet.decoded.want_response).to be(false)
|
|
384
445
|
serial_obj[:written].clear
|
|
385
|
-
described_class.get_owner(
|
|
446
|
+
described_class.get_owner(transport_obj: serial_obj)
|
|
386
447
|
expect(decode_admin(serial_obj).first.decoded.want_response).to be(true)
|
|
387
448
|
serial_obj[:written].clear
|
|
388
|
-
described_class.reboot(
|
|
449
|
+
described_class.reboot(transport_obj: serial_obj, to: '!aabbccdd', want_response: true, auto_session: false)
|
|
389
450
|
expect(decode_admin(serial_obj).first.to).to eq(0xaabbccdd)
|
|
390
451
|
expect(decode_admin(serial_obj).first.decoded.want_response).to be(true)
|
|
391
|
-
expect { described_class.get_owner(
|
|
392
|
-
expect { described_class.get_owner(
|
|
452
|
+
expect { described_class.get_owner(transport_obj: MQTTClient.new) }.to raise_error(ArgumentError, /destination/)
|
|
453
|
+
expect { described_class.get_owner(transport_obj: serial_obj, to: '!ffffffff') }.to raise_error(ArgumentError, /destination/)
|
|
393
454
|
end
|
|
394
455
|
|
|
395
456
|
it 'decodes admin responses and correlates without consuming transport queues' do
|
|
@@ -410,13 +471,13 @@ describe Meshtastic::Admin, 'validation and response handling' do
|
|
|
410
471
|
it 'supports OTA wire operations and resetting all nodes explicitly' do
|
|
411
472
|
serial_obj = fake_serial_obj
|
|
412
473
|
event = Meshtastic::AdminMessage::OTAEvent.new(reboot_ota_mode: :OTA_BLE, ota_hash: 'x' * 32)
|
|
413
|
-
described_class.ota_request(
|
|
474
|
+
described_class.ota_request(transport_obj: serial_obj, event: event)
|
|
414
475
|
expect(decode_admin(serial_obj).last.ota_request).to eq(event)
|
|
415
476
|
serial_obj[:written].clear
|
|
416
|
-
described_class.reboot_ota(
|
|
477
|
+
described_class.reboot_ota(transport_obj: serial_obj, seconds: -1)
|
|
417
478
|
expect(decode_admin(serial_obj).last.reboot_ota_seconds).to eq(-1)
|
|
418
479
|
serial_obj[:written].clear
|
|
419
|
-
described_class.nodedb_reset(
|
|
480
|
+
described_class.nodedb_reset(transport_obj: serial_obj, preserve_favorites: false)
|
|
420
481
|
expect(decode_admin(serial_obj).last.payload_variant).to eq(:nodedb_reset)
|
|
421
482
|
expect(decode_admin(serial_obj).last.nodedb_reset).to be(false)
|
|
422
483
|
end
|
|
@@ -424,21 +485,21 @@ describe Meshtastic::Admin, 'validation and response handling' do
|
|
|
424
485
|
it 'does not turn omitted required values into zero or empty destructive commands' do
|
|
425
486
|
serial_obj = fake_serial_obj
|
|
426
487
|
%i[delete_file set_scale set_time set_canned_messages set_ringtone remove_by_nodenum set_favorite_node remove_favorite_node set_ignored_node remove_ignored_node toggle_muted_node].each do |method|
|
|
427
|
-
expect { described_class.public_send(method,
|
|
488
|
+
expect { described_class.public_send(method, transport_obj: serial_obj) }.to raise_error(ArgumentError), method.to_s
|
|
428
489
|
end
|
|
429
490
|
expect(serial_obj[:written]).to be_empty
|
|
430
|
-
described_class.set_canned_messages(
|
|
491
|
+
described_class.set_canned_messages(transport_obj: serial_obj, messages: '')
|
|
431
492
|
expect(decode_admin(serial_obj).last.payload_variant).to eq(:set_canned_message_module_messages)
|
|
432
493
|
end
|
|
433
494
|
|
|
434
495
|
it 'returns the actual transmitted request ID for later response correlation' do
|
|
435
496
|
serial_obj = fake_serial_obj
|
|
436
|
-
result = described_class.request(
|
|
497
|
+
result = described_class.request(transport_obj: serial_obj, get_owner_request: true, request_id: 1234, wait: false)
|
|
437
498
|
expect(result[:request_id]).to eq(1234)
|
|
438
499
|
expect(decode_admin(serial_obj).first.id).to eq(1234)
|
|
439
|
-
expect { described_class.request(
|
|
500
|
+
expect { described_class.request(transport_obj: serial_obj, get_owner_request: true, request_id: 0) }.to raise_error(ArgumentError)
|
|
440
501
|
serial_obj[:written].clear
|
|
441
|
-
result = described_class.request(
|
|
502
|
+
result = described_class.request(transport_obj: serial_obj, get_config_request: :SESSIONKEY_CONFIG, wait: false)
|
|
442
503
|
expect(result[:request_id]).to eq(decode_admin(serial_obj).first.id)
|
|
443
504
|
expect(result[:request_id]).to be_positive
|
|
444
505
|
end
|
|
@@ -450,6 +511,11 @@ describe Meshtastic::Admin, 'validation and response handling' do
|
|
|
450
511
|
expect(described_class.encode(get_owner_request: true, session_passkey: "\x00" * 8).session_passkey.bytesize).to eq(8)
|
|
451
512
|
end
|
|
452
513
|
|
|
514
|
+
it 'documents the public classifier and the sole Admin transport option' do
|
|
515
|
+
expect { described_class.help }.to output(/Meshtastic::Admin.transport_type\(.*transport_obj:.*:serial.*:bluetooth.*:tcp.*:mqtt/m).to_stdout
|
|
516
|
+
expect { described_class.help }.not_to output(/(?:serial_obj|bluetooth_obj|tcp_obj|mqtt_obj):/).to_stdout
|
|
517
|
+
end
|
|
518
|
+
|
|
453
519
|
it 'prints usage without raising' do
|
|
454
520
|
expect { described_class.help }.to output(/USAGE/).to_stdout
|
|
455
521
|
end
|
|
@@ -3,6 +3,53 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe Meshtastic::ModuleConfig do
|
|
6
|
+
%i[serial_obj bluetooth_obj tcp_obj mqtt_obj transport_obj].each do |key|
|
|
7
|
+
it "translates #{key} to the Admin connection option without mutating options" do
|
|
8
|
+
connection = { marker: Object.new }
|
|
9
|
+
options = { key => connection, to: '!aabbccdd' }.freeze
|
|
10
|
+
expect(Meshtastic::Admin).to receive(:send).with({ transport_obj: connection, to: '!aabbccdd', get_module_config_request: :MQTT_CONFIG }).and_return(:submitted)
|
|
11
|
+
expect(described_class.get(options)).to eq(:submitted)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "translates #{key} for setters" do
|
|
15
|
+
connection = { marker: Object.new }
|
|
16
|
+
value = Meshtastic::ModuleConfig.new
|
|
17
|
+
expect(Meshtastic::Admin).to receive(:send).with({ transport_obj: connection, module_config: value, set_module_config: value }).and_return(:submitted)
|
|
18
|
+
expect(described_class.set({ key => connection, module_config: value }.freeze)).to eq(:submitted)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
%i[get set].each do |operation|
|
|
23
|
+
%i[transport_obj serial_obj bluetooth_obj tcp_obj mqtt_obj].combination(2) do |first, second|
|
|
24
|
+
it "rejects ambiguous #{first}/#{second} connections for #{operation}" do
|
|
25
|
+
connection = fake_serial_obj
|
|
26
|
+
expect(Meshtastic::Admin).not_to receive(:send)
|
|
27
|
+
expect { described_class.public_send(operation, { first => connection, second => connection, module_config: Meshtastic::ModuleConfig.new }) }
|
|
28
|
+
.to raise_error(ArgumentError, /connection|transport/i)
|
|
29
|
+
expect(connection[:written]).to be_empty
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'documents the canonical connection option in help' do
|
|
35
|
+
expect { described_class.help }.to output(/transport_obj: connection/).to_stdout
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
%i[serial_obj transport_obj].each do |key|
|
|
39
|
+
%i[get set].each do |operation|
|
|
40
|
+
it "encodes a real Admin packet for #{operation} with #{key}" do
|
|
41
|
+
connection = fake_serial_obj
|
|
42
|
+
options = { key => connection, module_config: Meshtastic::ModuleConfig.new }
|
|
43
|
+
options[:bluetooth_obj] = nil
|
|
44
|
+
described_class.public_send(operation, options.freeze)
|
|
45
|
+
frame = connection[:written]
|
|
46
|
+
packet = Meshtastic::ToRadio.decode(frame.byteslice(4, (frame.getbyte(2) << 8) + frame.getbyte(3))).packet
|
|
47
|
+
admin = Meshtastic::AdminMessage.decode(packet.decoded.payload)
|
|
48
|
+
expect(admin.payload_variant).to eq(operation == :get ? :get_module_config_request : :set_module_config)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
6
53
|
def fake_serial_obj
|
|
7
54
|
written = +''.b
|
|
8
55
|
serial_conn = Object.new
|
|
@@ -3,6 +3,53 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe Meshtastic::RTTTL do
|
|
6
|
+
%i[serial_obj bluetooth_obj tcp_obj mqtt_obj transport_obj].each do |key|
|
|
7
|
+
it "translates #{key} to the Admin connection option without mutating options" do
|
|
8
|
+
connection = { marker: Object.new }
|
|
9
|
+
options = { key => connection, to: '!aabbccdd' }.freeze
|
|
10
|
+
expect(Meshtastic::Admin).to receive(:send).with({ transport_obj: connection, to: '!aabbccdd', get_ringtone_request: true }).and_return(:submitted)
|
|
11
|
+
expect(described_class.get(options)).to eq(:submitted)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "translates #{key} for setters" do
|
|
15
|
+
connection = { marker: Object.new }
|
|
16
|
+
value = 'beep:d=4,o=5,b=120:16c6'
|
|
17
|
+
expect(Meshtastic::Admin).to receive(:send).with({ transport_obj: connection, ringtone: value, set_ringtone_message: value }).and_return(:submitted)
|
|
18
|
+
expect(described_class.set({ key => connection, ringtone: value }.freeze)).to eq(:submitted)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
%i[get set].each do |operation|
|
|
23
|
+
%i[transport_obj serial_obj bluetooth_obj tcp_obj mqtt_obj].combination(2) do |first, second|
|
|
24
|
+
it "rejects ambiguous #{first}/#{second} connections for #{operation}" do
|
|
25
|
+
connection = fake_serial_obj
|
|
26
|
+
expect(Meshtastic::Admin).not_to receive(:send)
|
|
27
|
+
expect { described_class.public_send(operation, { first => connection, second => connection, ringtone: 'beep' }) }
|
|
28
|
+
.to raise_error(ArgumentError, /connection|transport/i)
|
|
29
|
+
expect(connection[:written]).to be_empty
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'documents the canonical connection option in help' do
|
|
35
|
+
expect { described_class.help }.to output(/transport_obj: connection/).to_stdout
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
%i[serial_obj transport_obj].each do |key|
|
|
39
|
+
%i[get set].each do |operation|
|
|
40
|
+
it "encodes a real Admin packet for #{operation} with #{key}" do
|
|
41
|
+
connection = fake_serial_obj
|
|
42
|
+
options = { key => connection, ringtone: 'beep' }
|
|
43
|
+
options[:bluetooth_obj] = nil
|
|
44
|
+
described_class.public_send(operation, options.freeze)
|
|
45
|
+
frame = connection[:written]
|
|
46
|
+
packet = Meshtastic::ToRadio.decode(frame.byteslice(4, (frame.getbyte(2) << 8) + frame.getbyte(3))).packet
|
|
47
|
+
admin = Meshtastic::AdminMessage.decode(packet.decoded.payload)
|
|
48
|
+
expect(admin.payload_variant).to eq(operation == :get ? :get_ringtone_request : :set_ringtone_message)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
6
53
|
def fake_serial_obj
|
|
7
54
|
written = +''.b
|
|
8
55
|
serial_conn = Object.new
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: meshtastic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.183
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 0day Inc.
|
|
@@ -287,8 +287,10 @@ files:
|
|
|
287
287
|
- documentation/README.md
|
|
288
288
|
- documentation/admin-channel.md
|
|
289
289
|
- documentation/admin-config.md
|
|
290
|
+
- documentation/admin-firmware-hex.md
|
|
290
291
|
- documentation/admin-firmware-nordic.md
|
|
291
292
|
- documentation/admin-firmware-serial.md
|
|
293
|
+
- documentation/admin-firmware-uf2.md
|
|
292
294
|
- documentation/admin-firmware.md
|
|
293
295
|
- documentation/admin.md
|
|
294
296
|
- documentation/apponly.md
|
|
@@ -327,8 +329,10 @@ files:
|
|
|
327
329
|
- lib/meshtastic/admin/config.rb
|
|
328
330
|
- lib/meshtastic/admin/firmware.rb
|
|
329
331
|
- lib/meshtastic/admin/firmware/ble.rb
|
|
332
|
+
- lib/meshtastic/admin/firmware/hex.rb
|
|
330
333
|
- lib/meshtastic/admin/firmware/nordic_dfu.rb
|
|
331
334
|
- lib/meshtastic/admin/firmware/serial_bootloader.rb
|
|
335
|
+
- lib/meshtastic/admin/firmware/uf2.rb
|
|
332
336
|
- lib/meshtastic/admin_pb.rb
|
|
333
337
|
- lib/meshtastic/apponly.rb
|
|
334
338
|
- lib/meshtastic/apponly_pb.rb
|
|
@@ -393,8 +397,10 @@ files:
|
|
|
393
397
|
- spec/lib/meshtastic/admin/channel_spec.rb
|
|
394
398
|
- spec/lib/meshtastic/admin/config_spec.rb
|
|
395
399
|
- spec/lib/meshtastic/admin/firmware/ble_spec.rb
|
|
400
|
+
- spec/lib/meshtastic/admin/firmware/hex_spec.rb
|
|
396
401
|
- spec/lib/meshtastic/admin/firmware/nordic_dfu_spec.rb
|
|
397
402
|
- spec/lib/meshtastic/admin/firmware/serial_bootloader_spec.rb
|
|
403
|
+
- spec/lib/meshtastic/admin/firmware/uf2_spec.rb
|
|
398
404
|
- spec/lib/meshtastic/admin/firmware_spec.rb
|
|
399
405
|
- spec/lib/meshtastic/admin_pb_spec.rb
|
|
400
406
|
- spec/lib/meshtastic/admin_spec.rb
|