meshtastic 0.0.179 → 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.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/documentation/README.md +3 -2
- data/documentation/admin-channel.md +64 -0
- data/documentation/admin-config.md +59 -0
- data/documentation/admin-firmware-nordic.md +86 -0
- data/documentation/admin-firmware-serial.md +180 -0
- data/documentation/admin-firmware.md +126 -0
- data/documentation/admin.md +88 -35
- data/documentation/atak.md +93 -10
- data/documentation/channel.md +1 -28
- data/documentation/config.md +1 -27
- data/documentation/mesh-interface.md +7 -0
- data/documentation/meshtastic.md +2 -2
- data/lib/meshtastic/admin/channel.rb +164 -0
- data/lib/meshtastic/admin/config.rb +213 -0
- data/lib/meshtastic/admin/firmware/ble.rb +207 -0
- data/lib/meshtastic/admin/firmware/nordic_dfu.rb +218 -0
- data/lib/meshtastic/admin/firmware/serial_bootloader.rb +233 -0
- data/lib/meshtastic/admin/firmware.rb +288 -0
- data/lib/meshtastic/admin.rb +688 -34
- data/lib/meshtastic/apponly.rb +19 -7
- data/lib/meshtastic/atak.rb +295 -17
- data/lib/meshtastic/bluetooth/bluez.rb +20 -1
- data/lib/meshtastic/bluetooth.rb +70 -59
- data/lib/meshtastic/cannedmessages.rb +13 -6
- data/lib/meshtastic/channel.rb +3 -25
- data/lib/meshtastic/clientonly.rb +19 -7
- data/lib/meshtastic/config.rb +3 -24
- data/lib/meshtastic/config_pb.rb +2 -1
- data/lib/meshtastic/connection_status.rb +12 -5
- data/lib/meshtastic/deviceonly.rb +19 -7
- data/lib/meshtastic/localonly.rb +19 -7
- data/lib/meshtastic/mesh_interface.rb +19 -0
- data/lib/meshtastic/module_config.rb +13 -7
- data/lib/meshtastic/mqtt.rb +72 -35
- data/lib/meshtastic/paxcount.rb +17 -6
- data/lib/meshtastic/portnums.rb +13 -6
- data/lib/meshtastic/position.rb +16 -6
- data/lib/meshtastic/remote_hardware.rb +27 -11
- data/lib/meshtastic/rtttl.rb +17 -8
- data/lib/meshtastic/serial.rb +81 -70
- data/lib/meshtastic/storeforward.rb +13 -6
- data/lib/meshtastic/storeforward_pb.rb +1 -1
- data/lib/meshtastic/stream_interface.rb +11 -0
- data/lib/meshtastic/tcp.rb +73 -22
- data/lib/meshtastic/telemetry.rb +15 -6
- data/lib/meshtastic/traceroute.rb +15 -6
- data/lib/meshtastic/util.rb +3 -1
- data/lib/meshtastic/version.rb +1 -1
- data/lib/meshtastic/xmodem.rb +12 -5
- data/lib/meshtastic.rb +22 -3
- data/spec/conventions_spec.rb +321 -0
- data/spec/lib/meshtastic/admin/channel_spec.rb +202 -0
- data/spec/lib/meshtastic/admin/config_spec.rb +114 -0
- data/spec/lib/meshtastic/admin/firmware/ble_spec.rb +170 -0
- data/spec/lib/meshtastic/admin/firmware/nordic_dfu_spec.rb +248 -0
- data/spec/lib/meshtastic/admin/firmware/serial_bootloader_spec.rb +263 -0
- data/spec/lib/meshtastic/admin/firmware_spec.rb +315 -0
- data/spec/lib/meshtastic/admin_spec.rb +412 -1
- data/spec/lib/meshtastic/apponly_spec.rb +1 -1
- data/spec/lib/meshtastic/atak_spec.rb +129 -4
- data/spec/lib/meshtastic/bluetooth_spec.rb +1 -1
- data/spec/lib/meshtastic/channel_spec.rb +2 -20
- data/spec/lib/meshtastic/config_spec.rb +2 -20
- data/spec/lib/meshtastic/connection_status_spec.rb +1 -1
- data/spec/lib/meshtastic/deviceonly_spec.rb +1 -1
- data/spec/lib/meshtastic/localonly_spec.rb +1 -1
- data/spec/lib/meshtastic/mesh_interface_spec.rb +31 -0
- data/spec/lib/meshtastic/portnums_spec.rb +2 -2
- metadata +24 -6
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
@@ -39,6 +43,413 @@ describe Meshtastic::Admin do
|
|
|
39
43
|
expect(admin.set_owner.short_name).to eq('TN')
|
|
40
44
|
end
|
|
41
45
|
|
|
46
|
+
it 'covers remaining AdminMessage request fields' do
|
|
47
|
+
serial_obj = fake_serial_obj
|
|
48
|
+
examples = {
|
|
49
|
+
get_module_config: %i[get_module_config_request MQTT_CONFIG],
|
|
50
|
+
get_canned_messages: [:get_canned_message_module_messages_request, true],
|
|
51
|
+
get_device_metadata: [:get_device_metadata_request, true],
|
|
52
|
+
get_ringtone: [:get_ringtone_request, true],
|
|
53
|
+
get_device_connection_status: [:get_device_connection_status_request, true],
|
|
54
|
+
get_node_remote_hardware_pins: [:get_node_remote_hardware_pins_request, true],
|
|
55
|
+
get_ui_config: [:get_ui_config_request, true],
|
|
56
|
+
enter_dfu: [:enter_dfu_mode_request, true],
|
|
57
|
+
begin_edit: [:begin_edit_settings, true],
|
|
58
|
+
commit_edit: [:commit_edit_settings, true],
|
|
59
|
+
remove_fixed_position: [:remove_fixed_position, true],
|
|
60
|
+
exit_simulator: [:exit_simulator, true],
|
|
61
|
+
nodedb_reset: [:nodedb_reset, true]
|
|
62
|
+
}
|
|
63
|
+
examples.each do |meth, (field, expected)|
|
|
64
|
+
serial_obj[:written].clear
|
|
65
|
+
described_class.public_send(meth, serial_obj: serial_obj)
|
|
66
|
+
_packet, admin = decode_admin(serial_obj)
|
|
67
|
+
expect(admin.public_send(field)).to eq(expected), meth.to_s
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'sends factory resets, file delete, favorites, and edit transactions' do
|
|
72
|
+
serial_obj = fake_serial_obj
|
|
73
|
+
described_class.factory_reset_config(serial_obj: serial_obj)
|
|
74
|
+
expect(decode_admin(serial_obj).last.factory_reset_config).to eq(1)
|
|
75
|
+
serial_obj[:written].clear
|
|
76
|
+
described_class.factory_reset_device(serial_obj: serial_obj)
|
|
77
|
+
expect(decode_admin(serial_obj).last.factory_reset_device).to eq(1)
|
|
78
|
+
serial_obj[:written].clear
|
|
79
|
+
described_class.delete_file(serial_obj: serial_obj, path: '/prefs.json')
|
|
80
|
+
expect(decode_admin(serial_obj).last.delete_file_request).to eq('/prefs.json')
|
|
81
|
+
serial_obj[:written].clear
|
|
82
|
+
described_class.set_favorite_node(serial_obj: serial_obj, node_num: 0xb0b)
|
|
83
|
+
expect(decode_admin(serial_obj).last.set_favorite_node).to eq(0xb0b)
|
|
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
|
|
452
|
+
|
|
42
453
|
it 'prints usage without raising' do
|
|
43
454
|
expect { described_class.help }.to output(/USAGE/).to_stdout
|
|
44
455
|
end
|
|
@@ -5,6 +5,6 @@ require 'spec_helper'
|
|
|
5
5
|
describe Meshtastic::Apponly do
|
|
6
6
|
it 'round-trips a ChannelSet' do
|
|
7
7
|
set = described_class.encode
|
|
8
|
-
expect(described_class.decode(set.to_proto)).to be_a(Meshtastic::ChannelSet)
|
|
8
|
+
expect(described_class.decode(bytes: set.to_proto)).to be_a(Meshtastic::ChannelSet)
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -16,13 +16,138 @@ describe Meshtastic::ATAK do
|
|
|
16
16
|
{ serial_conn: serial_conn, written: written, my_node_num: 0xb0b }
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
serial_obj = fake_serial_obj
|
|
21
|
-
described_class.send(serial_obj: serial_obj, message: 'ATAK chat')
|
|
19
|
+
def decode_to_radio(serial_obj)
|
|
22
20
|
frame = serial_obj[:written]
|
|
23
|
-
|
|
21
|
+
body = frame.byteslice(4, (frame.getbyte(2) << 8) + frame.getbyte(3))
|
|
22
|
+
Meshtastic::ToRadio.decode(body).packet
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'encodes V1 GeoChat with contact, group, and status' do
|
|
26
|
+
packet = described_class.encode_v1(
|
|
27
|
+
message: 'ATAK chat',
|
|
28
|
+
to: 'ANDROID-aabbccdd',
|
|
29
|
+
callsign: 'ALPHA',
|
|
30
|
+
device_callsign: 'RADIO-1',
|
|
31
|
+
team: :Cyan,
|
|
32
|
+
role: :TeamMember,
|
|
33
|
+
battery: 87
|
|
34
|
+
)
|
|
35
|
+
expect(packet).to be_a(Meshtastic::TAKPacket)
|
|
36
|
+
expect(packet.chat.message).to eq('ATAK chat')
|
|
37
|
+
expect(packet.chat.to).to eq('ANDROID-aabbccdd')
|
|
38
|
+
expect(packet.contact.callsign).to eq('ALPHA')
|
|
39
|
+
expect(packet.group.team).to eq(:Cyan)
|
|
40
|
+
expect(packet.group.role).to eq(:TeamMember)
|
|
41
|
+
expect(packet.status.battery).to eq(87)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'encodes V1 PLI with 1e7-scaled coordinates' do
|
|
45
|
+
packet = described_class.encode_v1(
|
|
46
|
+
lat: 37.7749,
|
|
47
|
+
lon: -122.4194,
|
|
48
|
+
altitude: 12,
|
|
49
|
+
speed: 3,
|
|
50
|
+
course: 90
|
|
51
|
+
)
|
|
52
|
+
expect(packet.pli.latitude_i).to eq(377_749_000)
|
|
53
|
+
expect(packet.pli.longitude_i).to eq(-1_224_194_000)
|
|
54
|
+
expect(packet.pli.altitude).to eq(12)
|
|
55
|
+
expect(packet.pli.speed).to eq(3)
|
|
56
|
+
expect(packet.pli.course).to eq(90)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'sends V1 GeoChat on ATAK_PLUGIN' do
|
|
60
|
+
serial_obj = fake_serial_obj
|
|
61
|
+
described_class.send_chat(serial_obj: serial_obj, message: 'ATAK chat')
|
|
62
|
+
packet = decode_to_radio(serial_obj)
|
|
24
63
|
expect(packet.decoded.portnum).to eq(:ATAK_PLUGIN)
|
|
25
64
|
tak = Meshtastic::TAKPacket.decode(packet.decoded.payload)
|
|
26
65
|
expect(tak.chat.message).to eq('ATAK chat')
|
|
27
66
|
end
|
|
67
|
+
|
|
68
|
+
it 'sends V1 PLI on ATAK_PLUGIN' do
|
|
69
|
+
serial_obj = fake_serial_obj
|
|
70
|
+
described_class.send_pli(serial_obj: serial_obj, lat: 37.7749, lon: -122.4194)
|
|
71
|
+
packet = decode_to_radio(serial_obj)
|
|
72
|
+
expect(packet.decoded.portnum).to eq(:ATAK_PLUGIN)
|
|
73
|
+
tak = Meshtastic::TAKPacket.decode(packet.decoded.payload)
|
|
74
|
+
expect(tak.pli.latitude_i).to eq(377_749_000)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'encodes uncompressed V2 GeoChat wire frames with flags 0xFF' do
|
|
78
|
+
wire = described_class.encode_v2(
|
|
79
|
+
callsign: 'ALPHA',
|
|
80
|
+
team: :Cyan,
|
|
81
|
+
role: :TeamMember,
|
|
82
|
+
lat: 37.7749,
|
|
83
|
+
lon: -122.4194,
|
|
84
|
+
message: 'v2 chat'
|
|
85
|
+
)
|
|
86
|
+
expect(wire.getbyte(0)).to eq(0xFF)
|
|
87
|
+
v2 = Meshtastic::TAKPacketV2.decode(wire.byteslice(1..))
|
|
88
|
+
expect(v2.chat.message).to eq('v2 chat')
|
|
89
|
+
expect(v2.callsign).to eq('ALPHA')
|
|
90
|
+
expect(v2.latitude_i).to eq(377_749_000)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'round-trips V2 typed payloads' do
|
|
94
|
+
{
|
|
95
|
+
aircraft: Meshtastic::AircraftTrack.new(icao: 'ABC123', flight: 'N1'),
|
|
96
|
+
shape: Meshtastic::DrawnShape.new(kind: :Kind_Circle, major_cm: 1000),
|
|
97
|
+
marker: Meshtastic::Marker.new(kind: :Kind_Spot),
|
|
98
|
+
route: Meshtastic::Route.new(prefix: 'R1'),
|
|
99
|
+
casevac: Meshtastic::CasevacReport.new(title: 'CASEVAC'),
|
|
100
|
+
emergency: Meshtastic::EmergencyAlert.new(type: :Type_Alert911),
|
|
101
|
+
task: Meshtastic::TaskRequest.new(task_type: 'recon', note: 'look'),
|
|
102
|
+
taktalk: Meshtastic::TakTalkMessage.new(text: 'voice', chatroom_id: 'room1'),
|
|
103
|
+
taktalk_room: Meshtastic::TakTalkRoomData.new(room_id: 'room1', room_name: 'Ops'),
|
|
104
|
+
rab: Meshtastic::RangeAndBearing.new(range_cm: 500)
|
|
105
|
+
}.each do |field, value|
|
|
106
|
+
v2 = described_class.build_v2(field => value, callsign: 'ALPHA')
|
|
107
|
+
expect(v2.public_send(field)).to eq(value)
|
|
108
|
+
decoded = described_class.decode_v2(wire: described_class.wrap_v2(packet: v2))
|
|
109
|
+
expect(decoded.public_send(field).to_h).to eq(value.to_h)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'sends V2 packets on ATAK_PLUGIN_V2' do
|
|
114
|
+
serial_obj = fake_serial_obj
|
|
115
|
+
described_class.send_v2(serial_obj: serial_obj, message: 'v2 chat', callsign: 'ALPHA')
|
|
116
|
+
packet = decode_to_radio(serial_obj)
|
|
117
|
+
expect(packet.decoded.portnum).to eq(:ATAK_PLUGIN_V2)
|
|
118
|
+
decoded = described_class.decode_v2(payload: packet.decoded.payload)
|
|
119
|
+
expect(decoded.chat.message).to eq('v2 chat')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'zlib-compresses CoT XML on ATAK_FORWARDER' do
|
|
123
|
+
serial_obj = fake_serial_obj
|
|
124
|
+
cot = '<event type="b-m-p-s-m" uid="marker-1"><point lat="37.77" lon="-122.41"/></event>'
|
|
125
|
+
described_class.send_cot(serial_obj: serial_obj, cot: cot)
|
|
126
|
+
packet = decode_to_radio(serial_obj)
|
|
127
|
+
expect(packet.decoded.portnum).to eq(:ATAK_FORWARDER)
|
|
128
|
+
xml = described_class.decompress_cot(payload: packet.decoded.payload)
|
|
129
|
+
expect(xml).to include('marker-1')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'decodes inbound payloads by portnum' do
|
|
133
|
+
v1 = described_class.encode_v1(message: 'hi')
|
|
134
|
+
expect(described_class.decode(payload: v1.to_proto, portnum: :ATAK_PLUGIN).chat.message).to eq('hi')
|
|
135
|
+
|
|
136
|
+
v2 = described_class.encode_v2(message: 'v2')
|
|
137
|
+
expect(described_class.decode(payload: v2, portnum: :ATAK_PLUGIN_V2).chat.message).to eq('v2')
|
|
138
|
+
|
|
139
|
+
cot = described_class.compress_cot(cot: '<event uid="x"/>')
|
|
140
|
+
expect(described_class.decode(payload: cot, portnum: :ATAK_FORWARDER)).to include('uid="x"')
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'keeps send as a V1 GeoChat alias' do
|
|
144
|
+
serial_obj = fake_serial_obj
|
|
145
|
+
described_class.send(serial_obj: serial_obj, message: 'ATAK chat')
|
|
146
|
+
packet = decode_to_radio(serial_obj)
|
|
147
|
+
expect(packet.decoded.portnum).to eq(:ATAK_PLUGIN)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'prints usage without raising' do
|
|
151
|
+
expect { described_class.help }.to output(/ATAK_PLUGIN_V2/).to_stdout
|
|
152
|
+
end
|
|
28
153
|
end
|
|
@@ -136,7 +136,7 @@ describe Meshtastic::Bluetooth do
|
|
|
136
136
|
|
|
137
137
|
it 'scans with the selected adapter and bounded discovery timeout' do
|
|
138
138
|
devices = [{ address: 'AA:BB:CC:DD:EE:FF', name: 'Meshtastic_test', paired: true }]
|
|
139
|
-
expect(described_class::BlueZ).to receive(:scan).with(adapter: 'hci1', timeout: 2).and_return(devices)
|
|
139
|
+
expect(described_class::BlueZ).to receive(:scan).with({ adapter: 'hci1', timeout: 2 }).and_return(devices)
|
|
140
140
|
expect(described_class.scan(adapter: 'hci1', timeout: 2)).to eq(devices)
|
|
141
141
|
end
|
|
142
142
|
|
|
@@ -3,25 +3,7 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe Meshtastic::Channel do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
serial_conn = Object.new
|
|
9
|
-
serial_conn.define_singleton_method(:write) do |b|
|
|
10
|
-
written << b
|
|
11
|
-
b.bytesize
|
|
12
|
-
end
|
|
13
|
-
serial_conn.define_singleton_method(:flush) { true }
|
|
14
|
-
serial_conn.define_singleton_method(:closed?) { false }
|
|
15
|
-
serial_conn.define_singleton_method(:close) { true }
|
|
16
|
-
{ serial_conn: serial_conn, written: written, my_node_num: 0xb0b }
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it 'requests a channel via Admin' do
|
|
20
|
-
serial_obj = fake_serial_obj
|
|
21
|
-
described_class.get(serial_obj: serial_obj, index: 1)
|
|
22
|
-
frame = serial_obj[:written]
|
|
23
|
-
packet = Meshtastic::ToRadio.decode(frame.byteslice(4, (frame.getbyte(2) << 8) + frame.getbyte(3))).packet
|
|
24
|
-
admin = Meshtastic::AdminMessage.decode(packet.decoded.payload)
|
|
25
|
-
expect(admin.get_channel_request).to eq(1)
|
|
6
|
+
it 'is the generated Channel protobuf' do
|
|
7
|
+
expect(described_class.new).to be_a(Google::Protobuf::MessageExts)
|
|
26
8
|
end
|
|
27
9
|
end
|
|
@@ -3,25 +3,7 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe Meshtastic::Config do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
serial_conn = Object.new
|
|
9
|
-
serial_conn.define_singleton_method(:write) do |b|
|
|
10
|
-
written << b
|
|
11
|
-
b.bytesize
|
|
12
|
-
end
|
|
13
|
-
serial_conn.define_singleton_method(:flush) { true }
|
|
14
|
-
serial_conn.define_singleton_method(:closed?) { false }
|
|
15
|
-
serial_conn.define_singleton_method(:close) { true }
|
|
16
|
-
{ serial_conn: serial_conn, written: written, my_node_num: 0xb0b }
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it 'requests LoRa config via Admin' do
|
|
20
|
-
serial_obj = fake_serial_obj
|
|
21
|
-
described_class.get(serial_obj: serial_obj, config_type: :LORA_CONFIG)
|
|
22
|
-
frame = serial_obj[:written]
|
|
23
|
-
packet = Meshtastic::ToRadio.decode(frame.byteslice(4, (frame.getbyte(2) << 8) + frame.getbyte(3))).packet
|
|
24
|
-
admin = Meshtastic::AdminMessage.decode(packet.decoded.payload)
|
|
25
|
-
expect(admin.get_config_request).to eq(:LORA_CONFIG)
|
|
6
|
+
it 'is the generated Config protobuf' do
|
|
7
|
+
expect(described_class.new).to be_a(Google::Protobuf::MessageExts)
|
|
26
8
|
end
|
|
27
9
|
end
|
|
@@ -4,6 +4,6 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
describe Meshtastic::ConnectionStatus do
|
|
6
6
|
it 'decodes DeviceConnectionStatus bytes' do
|
|
7
|
-
expect(described_class.decode(Meshtastic::DeviceConnectionStatus.new.to_proto)).to be_a(Meshtastic::DeviceConnectionStatus)
|
|
7
|
+
expect(described_class.decode(bytes: Meshtastic::DeviceConnectionStatus.new.to_proto)).to be_a(Meshtastic::DeviceConnectionStatus)
|
|
8
8
|
end
|
|
9
9
|
end
|
|
@@ -4,6 +4,6 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
describe Meshtastic::Deviceonly do
|
|
6
6
|
it 'decodes an empty DeviceState' do
|
|
7
|
-
expect(described_class.decode_state(Meshtastic::DeviceState.new.to_proto)).to be_a(Meshtastic::DeviceState)
|
|
7
|
+
expect(described_class.decode_state(bytes: Meshtastic::DeviceState.new.to_proto)).to be_a(Meshtastic::DeviceState)
|
|
8
8
|
end
|
|
9
9
|
end
|