meshtastic 0.0.180 → 0.0.181

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe Meshtastic::Admin do
5
+ module AdminSpecHelpers
6
6
  def fake_serial_obj
7
7
  written = +''.b
8
8
  serial_conn = Object.new
@@ -22,6 +22,10 @@ describe Meshtastic::Admin do
22
22
  packet = Meshtastic::ToRadio.decode(body).packet
23
23
  [packet, Meshtastic::AdminMessage.decode(packet.decoded.payload)]
24
24
  end
25
+ end
26
+
27
+ describe Meshtastic::Admin do
28
+ include AdminSpecHelpers
25
29
 
26
30
  it 'sends a reboot AdminMessage on ADMIN_APP' do
27
31
  serial_obj = fake_serial_obj
@@ -78,6 +82,373 @@ describe Meshtastic::Admin do
78
82
  described_class.set_favorite_node(serial_obj: serial_obj, node_num: 0xb0b)
79
83
  expect(decode_admin(serial_obj).last.set_favorite_node).to eq(0xb0b)
80
84
  end
85
+ end
86
+
87
+ module AdminSynchronousSpecHelpers
88
+ include AdminSpecHelpers
89
+
90
+ def responding_serial(&responder)
91
+ handle = fake_serial_obj.merge(from_radio_queue: Queue.new, sent: [])
92
+ handle[:serial_conn].define_singleton_method(:write) do |bytes|
93
+ packet = Meshtastic::ToRadio.decode(bytes.byteslice(4..)).packet
94
+ handle[:sent] << packet
95
+ responder.call(handle, packet)
96
+ bytes.bytesize
97
+ end
98
+ handle
99
+ end
100
+
101
+ def admin_reply(packet, from: packet.to, request_id: packet.id, message: nil)
102
+ message ||= Meshtastic::AdminMessage.new(get_device_metadata_response: Meshtastic::DeviceMetadata.new(firmware_version: 'test-version'))
103
+ Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(from: from, decoded: Meshtastic::Data.new(
104
+ portnum: :ADMIN_APP, request_id: request_id, payload: message.to_proto
105
+ )))
106
+ end
107
+ end
108
+
109
+ describe Meshtastic::Admin, 'automatic sessions' do
110
+ include AdminSynchronousSpecHelpers
111
+
112
+ it 'automatically acquires and reuses a target-scoped session before remote writes' do
113
+ handle = responding_serial do |connection, packet|
114
+ message = Meshtastic::AdminMessage.decode(packet.decoded.payload)
115
+ next unless message.get_config_request == :SESSIONKEY_CONFIG && message.payload_variant == :get_config_request
116
+
117
+ reply = Meshtastic::AdminMessage.new(get_config_response: Meshtastic::Config.new, session_passkey: '12345678')
118
+ connection[:from_radio_queue] << admin_reply(packet, message: reply)
119
+ end
120
+ 2.times { described_class.set_owner(serial_obj: handle, to: '!aabbccdd', long_name: 'Remote', timeout: 0.2) }
121
+ messages = handle[:sent].map { |packet| Meshtastic::AdminMessage.decode(packet.decoded.payload) }
122
+ expect(messages.map(&:payload_variant)).to eq(%i[get_config_request set_owner set_owner])
123
+ expect(messages.drop(1).map(&:session_passkey)).to eq(%w[12345678 12345678])
124
+ described_class.set_owner(serial_obj: handle, to: '!aabbccee', long_name: 'Other', timeout: 0.2)
125
+ expect(handle[:sent].length).to eq(5)
126
+ end
127
+
128
+ it 'returns a target ACK for a write and invalidates rejected sessions without replaying the write' do
129
+ reason = :NONE
130
+ handle = responding_serial do |connection, packet|
131
+ message = Meshtastic::AdminMessage.decode(packet.decoded.payload)
132
+ if message.payload_variant == :get_config_request
133
+ reply = Meshtastic::AdminMessage.new(get_config_response: Meshtastic::Config.new, session_passkey: '12345678')
134
+ connection[:from_radio_queue] << admin_reply(packet, message: reply)
135
+ else
136
+ connection[:from_radio_queue] << Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(from: packet.to,
137
+ decoded: Meshtastic::Data.new(portnum: :ROUTING_APP, request_id: packet.id, payload: Meshtastic::Routing.new(error_reason: reason).to_proto)))
138
+ end
139
+ end
140
+ options = { serial_obj: handle, to: '!aabbccdd', reboot_seconds: 5, timeout: 0.2 }
141
+ expect(described_class.request(options)).to include(variant: :routing, value: :NONE)
142
+ reason = :ADMIN_BAD_SESSION_KEY
143
+ expect { described_class.request(options) }.to raise_error(Meshtastic::Admin::RoutingError)
144
+ expect(handle[:sent].length).to eq(3)
145
+ reason = :NONE
146
+ described_class.request(options)
147
+ expect(handle[:sent].length).to eq(5)
148
+ end
149
+
150
+ it 'caches the newest readback passkey, expires conservatively and supports explicit refresh' do
151
+ key = 'freshkey'
152
+ handle = responding_serial do |connection, packet|
153
+ message = Meshtastic::AdminMessage.decode(packet.decoded.payload)
154
+ next unless message.payload_variant.to_s.start_with?('get_')
155
+
156
+ reply = Meshtastic::AdminMessage.new(get_config_response: Meshtastic::Config.new, session_passkey: key)
157
+ connection[:from_radio_queue] << admin_reply(packet, message: reply)
158
+ end
159
+ options = { serial_obj: handle, to: '!aabbccdd', timeout: 0.5 }
160
+ described_class.request(options.merge(get_config_request: :SESSIONKEY_CONFIG))
161
+ described_class.reboot(options)
162
+ expect(handle[:sent].length).to eq(2)
163
+ handle[:admin_sessions][0xaabbccdd][:expires_at] = 0
164
+ key = 'newerkey'
165
+ described_class.reboot(options)
166
+ expect(handle[:sent].length).to eq(4)
167
+ expect(Meshtastic::AdminMessage.decode(handle[:sent].last.decoded.payload).session_passkey).to eq(key)
168
+ described_class.reboot(options.merge(refresh_session: true))
169
+ expect(handle[:sent].length).to eq(6)
170
+ end
171
+
172
+ it 'discards the old cache before a forced refresh which fails' do
173
+ handle = responding_serial do |connection, packet|
174
+ connection[:from_radio_queue] << admin_reply(packet, message: Meshtastic::AdminMessage.new(get_config_response: Meshtastic::Config.new))
175
+ end
176
+ handle[:admin_sessions] = { 0xaabbccdd => { key: 'old-key!', expires_at: Process.clock_gettime(Process::CLOCK_MONOTONIC) + 100 } }
177
+ options = { serial_obj: handle, to: '!aabbccdd', timeout: 0.2 }
178
+ expect { described_class.reboot(options.merge(refresh_session: true)) }.to raise_error(ArgumentError, /eight-byte/)
179
+ expect { described_class.reboot(options) }.to raise_error(ArgumentError, /eight-byte/)
180
+ expect(handle[:sent].map { |packet| Meshtastic::AdminMessage.decode(packet.decoded.payload).payload_variant }).to eq(%i[get_config_request get_config_request])
181
+ end
182
+
183
+ it 'honors the session timeout even when only submission is requested' do
184
+ handle = responding_serial { |_connection, _packet| nil }
185
+ expect do
186
+ Timeout.timeout(0.5, RuntimeError, 'outer deadline exceeded') do
187
+ described_class.request(serial_obj: handle, to: '!aabbccdd', reboot_seconds: 5, wait: false, timeout: 0.15)
188
+ end
189
+ end.to raise_error(Timeout::Error, /Admin response/)
190
+ expect(handle[:sent].length).to eq(1)
191
+ end
192
+
193
+ it 'rejects overlapping synchronous consumers on one handle without sending a second request' do
194
+ entered = Queue.new
195
+ release = Queue.new
196
+ handle = responding_serial do |connection, packet|
197
+ entered << true
198
+ release.pop
199
+ connection[:from_radio_queue] << admin_reply(packet)
200
+ end
201
+ worker = Thread.new { described_class.request(serial_obj: handle, get_device_metadata_request: true, timeout: 1) }
202
+ entered.pop
203
+ begin
204
+ expect do
205
+ Timeout.timeout(0.3) { described_class.request(serial_obj: handle, get_device_metadata_request: true, timeout: 0.1) }
206
+ end.to raise_error(IOError, /already active/)
207
+ ensure
208
+ release << true
209
+ release << true
210
+ worker.value
211
+ end
212
+ expect(handle[:sent].length).to eq(1)
213
+ end
214
+ end
215
+
216
+ describe Meshtastic::Admin, 'synchronous requests' do
217
+ include AdminSynchronousSpecHelpers
218
+
219
+ it 'ignores malformed routing payloads from unrelated senders' do
220
+ unrelated = nil
221
+ handle = responding_serial do |connection, packet|
222
+ unrelated = Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(from: 77,
223
+ decoded: Meshtastic::Data.new(portnum: :ROUTING_APP, request_id: packet.id, payload: "\xff".b)))
224
+ connection[:from_radio_queue] << unrelated
225
+ connection[:from_radio_queue] << admin_reply(packet)
226
+ end
227
+ expect(described_class.request(serial_obj: handle, get_device_metadata_request: true, timeout: 0.2)[:value].firmware_version).to eq('test-version')
228
+ expect(handle[:from_radio_queue].pop(true)).to eq(unrelated)
229
+ end
230
+
231
+ %i[tcp_obj bluetooth_obj].each do |transport|
232
+ it "reads correlated replies through the real #{transport} send path" do
233
+ handle = if transport == :tcp_obj
234
+ responding_serial { |connection, packet| connection[:from_radio_queue] << admin_reply(packet) }
235
+ else
236
+ { my_node_num: 0xb0b, from_radio_queue: Queue.new, tx_mutex: Mutex.new, bluetooth_conn: Object.new }
237
+ end
238
+ if transport == :bluetooth_obj
239
+ make_reply = method(:admin_reply)
240
+ handle[:bluetooth_conn].define_singleton_method(:write) do |bytes|
241
+ packet = Meshtastic::ToRadio.decode(bytes).packet
242
+ handle[:from_radio_queue] << make_reply.call(packet)
243
+ bytes.bytesize
244
+ end
245
+ end
246
+ result = described_class.request(transport => handle, get_device_metadata_request: true, timeout: 0.5)
247
+ expect(result[:value].firmware_version).to eq('test-version')
248
+ end
249
+ end
250
+
251
+ it 'bounds a silent receive and retains wrong-variant replies and text traffic on timeout' do
252
+ preserved = []
253
+ handle = responding_serial do |connection, packet|
254
+ preserved << admin_reply(packet, message: Meshtastic::AdminMessage.new(get_owner_response: Meshtastic::User.new))
255
+ preserved << Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(from: packet.to,
256
+ decoded: Meshtastic::Data.new(portnum: :TEXT_MESSAGE_APP, payload: 'keep')))
257
+ preserved.each { |message| connection[:from_radio_queue] << message }
258
+ end
259
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
260
+ expect { described_class.request(serial_obj: handle, get_device_metadata_request: true, timeout: 0.15) }.to raise_error(Timeout::Error)
261
+ expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start).to be < 0.6
262
+ expect(preserved.map { handle[:from_radio_queue].pop(true) }).to eq(preserved)
263
+ end
264
+
265
+ it 'fails closed before writing when automatic session acquisition has no valid passkey' do
266
+ handle = responding_serial do |connection, packet|
267
+ connection[:from_radio_queue] << admin_reply(packet, message: Meshtastic::AdminMessage.new(get_config_response: Meshtastic::Config.new))
268
+ end
269
+ expect do
270
+ described_class.reboot(serial_obj: handle, to: '!aabbccdd', timeout: 0.2)
271
+ end.to raise_error(ArgumentError, /eight-byte passkey/)
272
+ expect(handle[:sent].map { |packet| Meshtastic::AdminMessage.decode(packet.decoded.payload).payload_variant }).to eq([:get_config_request])
273
+ expect do
274
+ described_class.reboot(mqtt_obj: Object.new, to: '!aabbccdd')
275
+ end.to raise_error(ArgumentError, /supply session_passkey/)
276
+ end
277
+
278
+ it 'rejects invalid timeouts before submitting any data' do
279
+ handle = responding_serial { |_connection, _packet| raise 'must not send' }
280
+ [0, -1, nil, Float::INFINITY, Float::NAN, '1'].each do |timeout|
281
+ expect { described_class.request(serial_obj: handle, get_owner_request: true, timeout: timeout) }.to raise_error(ArgumentError, /timeout/)
282
+ end
283
+ expect(handle[:sent]).to be_empty
284
+ end
285
+
286
+ it 'keeps unrelated packets readable after the transport closes during a request' do
287
+ unrelated = Meshtastic::FromRadio.new(log_record: Meshtastic::LogRecord.new(message: 'keep'))
288
+ handle = responding_serial do |connection, _packet|
289
+ connection[:from_radio_queue] << unrelated
290
+ connection[:from_radio_queue].close
291
+ end
292
+ expect do
293
+ described_class.request(serial_obj: handle, get_owner_request: true, timeout: 0.2)
294
+ end.to raise_error(Timeout::Error)
295
+ expect(handle[:from_radio_queue].pop(true)).to eq(unrelated)
296
+ expect(handle[:from_radio_queue]).to be_closed
297
+ end
298
+
299
+ it 'reports correlated routing failures without mistaking a local ACK for remote readback' do
300
+ handle = responding_serial do |connection, packet|
301
+ [connection[:my_node_num], packet.to].each do |sender|
302
+ routing = Meshtastic::Routing.new(error_reason: sender == packet.to ? :NOT_AUTHORIZED : :NONE)
303
+ connection[:from_radio_queue] << Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(
304
+ from: sender, decoded: Meshtastic::Data.new(portnum: :ROUTING_APP, request_id: packet.id, payload: routing.to_proto)
305
+ ))
306
+ end
307
+ end
308
+ expect do
309
+ described_class.request(serial_obj: handle, to: '!aabbccdd', get_device_metadata_request: true, timeout: 0.1)
310
+ end.to raise_error(Meshtastic::Admin::RoutingError, /NOT_AUTHORIZED/)
311
+ expect(handle[:from_radio_queue].size).to eq(1)
312
+ end
313
+
314
+ it 'waits for the target-correlated metadata response and preserves unrelated packets' do
315
+ unrelated = []
316
+ handle = responding_serial do |connection, packet|
317
+ unrelated << admin_reply(packet, from: 123)
318
+ unrelated << admin_reply(packet, request_id: packet.id + 1)
319
+ unrelated << Meshtastic::FromRadio.new(log_record: Meshtastic::LogRecord.new(message: 'other traffic'))
320
+ unrelated.each { |message| connection[:from_radio_queue] << message }
321
+ connection[:from_radio_queue] << admin_reply(packet)
322
+ end
323
+ result = described_class.request(serial_obj: handle, message: Meshtastic::AdminMessage.new(get_device_metadata_request: true), timeout: 0.2)
324
+ expect(result[:value].firmware_version).to eq('test-version')
325
+ expect(result[:request_id]).to eq(handle[:sent].last.id)
326
+ expect(unrelated.map { handle[:from_radio_queue].pop(true) }).to eq(unrelated)
327
+ end
328
+ end
329
+
330
+ describe Meshtastic::Admin, 'generated schema coverage' do
331
+ Meshtastic::AdminMessage.descriptor.lookup_oneof('payload_variant').each do |field|
332
+ it "round-trips the #{field.name} payload without changing its oneof selection" do
333
+ value = case field.type
334
+ when :message then field.subtype.msgclass.new
335
+ when :bool then false
336
+ when :string then ''
337
+ when :enum then 0
338
+ else 1
339
+ end
340
+ message = described_class.encode(field.name.to_sym => value)
341
+ decoded = described_class.decode(payload: message.to_proto)
342
+ expect(decoded.payload_variant).to eq(field.name.to_sym)
343
+ expect(decoded.public_send(field.name)).to eq(message.public_send(field.name))
344
+ end
345
+ end
346
+ end
347
+
348
+ describe Meshtastic::Admin, 'validation and response handling' do
349
+ include AdminSpecHelpers
350
+
351
+ it 'converts a zero-based channel index exactly once and rejects invalid indexes' do
352
+ serial_obj = fake_serial_obj
353
+ [0, 7].each do |index|
354
+ serial_obj[:written].clear
355
+ described_class.get_channel(serial_obj: serial_obj, index: index)
356
+ expect(decode_admin(serial_obj).last.get_channel_request).to eq(index + 1)
357
+ end
358
+ [-1, 8, '1', 1.5].each do |index|
359
+ expect { described_class.get_channel(serial_obj: serial_obj, index: index) }.to raise_error(ArgumentError)
360
+ end
361
+ end
362
+
363
+ it 'rejects conflicting, empty and unknown payloads without mutating caller messages' do
364
+ original = Meshtastic::AdminMessage.new(get_owner_request: true)
365
+ expect { described_class.encode(message: original, reboot_seconds: 5) }.to raise_error(ArgumentError, /one payload/)
366
+ expect(original.payload_variant).to eq(:get_owner_request)
367
+ expect { described_class.encode(get_owner_request: true, sensor_config: Meshtastic::SensorConfig.new) }.to raise_error(ArgumentError, /one payload/)
368
+ expect { described_class.encode(set_owner: nil) }.to raise_error(ArgumentError)
369
+ expect { described_class.encode(rebot_seconds: 5) }.to raise_error(ArgumentError)
370
+ encoded = described_class.encode(message: original, session_passkey: '12345678')
371
+ expect(encoded.session_passkey).to eq('12345678')
372
+ expect(original.session_passkey).to eq('')
373
+ expect(described_class.encode(get_config_request: :DEVICE_CONFIG).payload_variant).to eq(:get_config_request)
374
+ expect(described_class.encode(nodedb_reset: false).payload_variant).to eq(:nodedb_reset)
375
+ end
376
+
377
+ it 'targets the connected node locally and requests replies only for getters by default' do
378
+ serial_obj = fake_serial_obj
379
+ described_class.reboot(serial_obj: serial_obj)
380
+ packet, = decode_admin(serial_obj)
381
+ expect(packet.to).to eq(0xb0b)
382
+ expect(packet.from).to eq(0)
383
+ expect(packet.decoded.want_response).to be(false)
384
+ serial_obj[:written].clear
385
+ described_class.get_owner(serial_obj: serial_obj)
386
+ expect(decode_admin(serial_obj).first.decoded.want_response).to be(true)
387
+ serial_obj[:written].clear
388
+ described_class.reboot(serial_obj: serial_obj, to: '!aabbccdd', want_response: true, auto_session: false)
389
+ expect(decode_admin(serial_obj).first.to).to eq(0xaabbccdd)
390
+ expect(decode_admin(serial_obj).first.decoded.want_response).to be(true)
391
+ expect { described_class.get_owner(mqtt_obj: Object.new) }.to raise_error(ArgumentError, /destination/)
392
+ expect { described_class.get_owner(serial_obj: serial_obj, to: '!ffffffff') }.to raise_error(ArgumentError, /destination/)
393
+ end
394
+
395
+ it 'decodes admin responses and correlates without consuming transport queues' do
396
+ admin = Meshtastic::AdminMessage.new(get_owner_response: Meshtastic::User.new(long_name: 'Remote'), session_passkey: '12345678')
397
+ packet = Meshtastic::MeshPacket.new(from: 0xaabbccdd, decoded: Meshtastic::Data.new(portnum: :ADMIN_APP, payload: admin.to_proto, request_id: 42))
398
+ frame = Meshtastic::FromRadio.new(packet: packet)
399
+ expect(described_class.decode(payload: admin.to_proto)).to eq(admin)
400
+ expect(described_class.decode(packet: frame)).to eq(admin)
401
+ result = described_class.response(packet: frame, request_id: 42, from: 0xaabbccdd)
402
+ expect(result).to include(variant: :get_owner_response, value: admin.get_owner_response, session_passkey: '12345678', request_id: 42, from: 0xaabbccdd)
403
+ expect(described_class.response(packet: frame, request_id: 43)).to be_nil
404
+ expect(described_class.response(packet: frame, from: 123)).to be_nil
405
+ packet.decoded.portnum = :TEXT_MESSAGE_APP
406
+ expect(described_class.response(packet: packet)).to be_nil
407
+ expect { described_class.decode(packet: packet) }.to raise_error(ArgumentError, /ADMIN_APP/)
408
+ end
409
+
410
+ it 'supports OTA wire operations and resetting all nodes explicitly' do
411
+ serial_obj = fake_serial_obj
412
+ event = Meshtastic::AdminMessage::OTAEvent.new(reboot_ota_mode: :OTA_BLE, ota_hash: 'x' * 32)
413
+ described_class.ota_request(serial_obj: serial_obj, event: event)
414
+ expect(decode_admin(serial_obj).last.ota_request).to eq(event)
415
+ serial_obj[:written].clear
416
+ described_class.reboot_ota(serial_obj: serial_obj, seconds: -1)
417
+ expect(decode_admin(serial_obj).last.reboot_ota_seconds).to eq(-1)
418
+ serial_obj[:written].clear
419
+ described_class.nodedb_reset(serial_obj: serial_obj, preserve_favorites: false)
420
+ expect(decode_admin(serial_obj).last.payload_variant).to eq(:nodedb_reset)
421
+ expect(decode_admin(serial_obj).last.nodedb_reset).to be(false)
422
+ end
423
+
424
+ it 'does not turn omitted required values into zero or empty destructive commands' do
425
+ serial_obj = fake_serial_obj
426
+ %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, serial_obj: serial_obj) }.to raise_error(ArgumentError), method.to_s
428
+ end
429
+ expect(serial_obj[:written]).to be_empty
430
+ described_class.set_canned_messages(serial_obj: serial_obj, messages: '')
431
+ expect(decode_admin(serial_obj).last.payload_variant).to eq(:set_canned_message_module_messages)
432
+ end
433
+
434
+ it 'returns the actual transmitted request ID for later response correlation' do
435
+ serial_obj = fake_serial_obj
436
+ result = described_class.request(serial_obj: serial_obj, get_owner_request: true, request_id: 1234, wait: false)
437
+ expect(result[:request_id]).to eq(1234)
438
+ expect(decode_admin(serial_obj).first.id).to eq(1234)
439
+ expect { described_class.request(serial_obj: serial_obj, get_owner_request: true, request_id: 0) }.to raise_error(ArgumentError)
440
+ serial_obj[:written].clear
441
+ result = described_class.request(serial_obj: serial_obj, get_config_request: :SESSIONKEY_CONFIG, wait: false)
442
+ expect(result[:request_id]).to eq(decode_admin(serial_obj).first.id)
443
+ expect(result[:request_id]).to be_positive
444
+ end
445
+
446
+ it 'requires an eight-byte session passkey when explicitly supplied' do
447
+ [nil, '', 'short', 'x' * 9].each do |key|
448
+ expect { described_class.encode(get_owner_request: true, session_passkey: key) }.to raise_error(ArgumentError, /eight bytes/)
449
+ end
450
+ expect(described_class.encode(get_owner_request: true, session_passkey: "\x00" * 8).session_passkey.bytesize).to eq(8)
451
+ end
81
452
 
82
453
  it 'prints usage without raising' do
83
454
  expect { described_class.help }.to output(/USAGE/).to_stdout
@@ -3,4 +3,35 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Meshtastic::MeshInterface do
6
+ it 'rejects PKI on MQTT instead of falling back to channel encryption' do
7
+ expect do
8
+ described_class.new.send_data(
9
+ data: Meshtastic::Data.new(portnum: :ADMIN_APP, payload: 'request'),
10
+ port_num: Meshtastic::PortNum::ADMIN_APP,
11
+ from: 1, to: 2, via: :mqtt, pki_encrypted: true, public_key: 'k' * 32
12
+ )
13
+ end.to raise_error(ArgumentError, /PKI.*radio/)
14
+ end
15
+
16
+ it 'rejects malformed recipient public keys' do
17
+ expect do
18
+ described_class.new.send_data(
19
+ data: Meshtastic::Data.new(payload: 'request'),
20
+ from: 1, to: 2, psks: nil, pki_encrypted: true, public_key: 'short'
21
+ )
22
+ end.to raise_error(ArgumentError, /32 bytes/)
23
+ end
24
+
25
+ it 'preserves explicit radio PKI parameters for remote administrative packets' do
26
+ wire = described_class.new.send_data(
27
+ data: Meshtastic::Data.new(portnum: :ADMIN_APP, payload: 'request'),
28
+ port_num: Meshtastic::PortNum::ADMIN_APP,
29
+ from: 1, to: 2, via: :radio, psks: nil,
30
+ pki_encrypted: true, public_key: 'k' * 32
31
+ )
32
+ packet = Meshtastic::ToRadio.decode(wire).packet
33
+ expect(packet.pki_encrypted).to be true
34
+ expect(packet.public_key).to eq('k' * 32)
35
+ expect(packet.decoded.payload).to eq('request')
36
+ end
6
37
  end
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.180
4
+ version: 0.0.181
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 4.0.20
18
+ version: 4.0.21
19
19
  type: :development
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 4.0.20
25
+ version: 4.0.21
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bundler-audit
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -99,14 +99,14 @@ dependencies:
99
99
  requirements:
100
100
  - - '='
101
101
  - !ruby/object:Gem::Version
102
- version: 1.83.1
102
+ version: 1.84.0
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - '='
108
108
  - !ruby/object:Gem::Version
109
- version: 1.83.1
109
+ version: 1.84.0
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: mqtt
112
112
  requirement: !ruby/object:Gem::Requirement
@@ -287,6 +287,8 @@ files:
287
287
  - documentation/README.md
288
288
  - documentation/admin-channel.md
289
289
  - documentation/admin-config.md
290
+ - documentation/admin-firmware-nordic.md
291
+ - documentation/admin-firmware-serial.md
290
292
  - documentation/admin-firmware.md
291
293
  - documentation/admin.md
292
294
  - documentation/apponly.md
@@ -324,6 +326,9 @@ files:
324
326
  - lib/meshtastic/admin/channel.rb
325
327
  - lib/meshtastic/admin/config.rb
326
328
  - lib/meshtastic/admin/firmware.rb
329
+ - lib/meshtastic/admin/firmware/ble.rb
330
+ - lib/meshtastic/admin/firmware/nordic_dfu.rb
331
+ - lib/meshtastic/admin/firmware/serial_bootloader.rb
327
332
  - lib/meshtastic/admin_pb.rb
328
333
  - lib/meshtastic/apponly.rb
329
334
  - lib/meshtastic/apponly_pb.rb
@@ -386,6 +391,9 @@ files:
386
391
  - spec/conventions_spec.rb
387
392
  - spec/lib/meshtastic/admin/channel_spec.rb
388
393
  - spec/lib/meshtastic/admin/config_spec.rb
394
+ - spec/lib/meshtastic/admin/firmware/ble_spec.rb
395
+ - spec/lib/meshtastic/admin/firmware/nordic_dfu_spec.rb
396
+ - spec/lib/meshtastic/admin/firmware/serial_bootloader_spec.rb
389
397
  - spec/lib/meshtastic/admin/firmware_spec.rb
390
398
  - spec/lib/meshtastic/admin_pb_spec.rb
391
399
  - spec/lib/meshtastic/admin_spec.rb
@@ -469,7 +477,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
469
477
  - !ruby/object:Gem::Version
470
478
  version: '0'
471
479
  requirements: []
472
- rubygems_version: 4.0.20
480
+ rubygems_version: 4.0.21
473
481
  specification_version: 4
474
482
  summary: Ruby gem for Meshtastic
475
483
  test_files: []