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
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'tempfile'
|
|
7
|
+
|
|
8
|
+
RSpec.describe Meshtastic::Admin::Firmware do
|
|
9
|
+
it 'dispatches explicitly to independent native bootloader protocols' do
|
|
10
|
+
%i[esp_rom nordic_dfu].each do |protocol|
|
|
11
|
+
name = protocol == :esp_rom ? :SerialBootloader : :NordicDFU
|
|
12
|
+
implementation = described_class.const_get(name)
|
|
13
|
+
options = { protocol: protocol, bytes: 'abc' }
|
|
14
|
+
expect(implementation).to receive(:install).with(options).and_return(status: :verified)
|
|
15
|
+
expect(described_class.install(options)).to eq(status: :verified)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'validates reboot verification before upload and only verifies after loader success' do
|
|
20
|
+
backend = double('BLE backend')
|
|
21
|
+
verify = { transport: :tcp, connection: { host: '192.0.2.1' }, expected_version: '2.7.1' }
|
|
22
|
+
options = { protocol: :unified_ble, backend: backend, bytes: 'abc' }
|
|
23
|
+
expect(described_class::BLE).to receive(:install).with(options).ordered.and_return(status: :verified, bytes: 3, reboot_verified: false, boot_verified: false)
|
|
24
|
+
expect(described_class).to receive(:verify_reboot).with(verify).ordered.and_return(status: :boot_verified, firmware_version: '2.7.1')
|
|
25
|
+
expect(described_class.install(options.merge(verify: verify))).to include(status: :boot_verified, loader_status: :verified, bytes: 3, reboot_verified: true, boot_verified: true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'rejects invalid verification settings before any destructive transfer' do
|
|
29
|
+
base = { transport: :tcp, connection: { host: '192.0.2.1' }, expected_version: '2.7.1' }
|
|
30
|
+
invalid = [true, {}, base.merge(timeout: 0), base.merge(timeout: Float::INFINITY),
|
|
31
|
+
base.merge(reboot_delay: -1), base.merge(expected_version: ''), base.merge(transport: :mqtt),
|
|
32
|
+
base.merge(connection: {}), base.merge(reconnect: true), base.merge(expected_node: '!bad'),
|
|
33
|
+
base.merge(connection: { host: '192.0.2.1', socket: Object.new }),
|
|
34
|
+
base.merge(connection: { host: '192.0.2.1', port: 0 }),
|
|
35
|
+
base.merge(transport: :bluetooth, connection: { address: 'not-a-mac' })]
|
|
36
|
+
invalid.each do |verify|
|
|
37
|
+
expect(described_class::BLE).not_to receive(:install)
|
|
38
|
+
expect { described_class.install(protocol: :unified_ble, bytes: 'abc', verify: verify) }.to raise_error(ArgumentError)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'ignores cached metadata and checks a fresh correlated Admin reply after callback reconnect' do
|
|
43
|
+
queue = Queue.new
|
|
44
|
+
writer = Object.new
|
|
45
|
+
writer.define_singleton_method(:write) do |bytes|
|
|
46
|
+
request = Meshtastic::ToRadio.decode(bytes.byteslice(4..)).packet
|
|
47
|
+
payload = Meshtastic::AdminMessage.new(get_device_metadata_response: Meshtastic::DeviceMetadata.new(firmware_version: 'wrong')).to_proto
|
|
48
|
+
queue << Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(from: 123, decoded: Meshtastic::Data.new(portnum: :ADMIN_APP, request_id: request.id, payload: payload)))
|
|
49
|
+
bytes.bytesize
|
|
50
|
+
end
|
|
51
|
+
writer.define_singleton_method(:flush) { true }
|
|
52
|
+
handle = { serial_conn: writer, from_radio_queue: queue, my_node_num: 123, metadata: { firmware_version: 'expected' } }
|
|
53
|
+
attempts = 0
|
|
54
|
+
reconnect = lambda do |_options|
|
|
55
|
+
attempts += 1
|
|
56
|
+
raise Errno::ECONNREFUSED if attempts == 1
|
|
57
|
+
|
|
58
|
+
handle
|
|
59
|
+
end
|
|
60
|
+
expect(Meshtastic::Serial).to receive(:wait_for_config).with(serial_obj: handle, timeout: 1).and_return(handle)
|
|
61
|
+
expect(Meshtastic::Serial).to receive(:disconnect).with(serial_obj: handle)
|
|
62
|
+
expect do
|
|
63
|
+
described_class.verify_reboot(transport: :serial, reconnect: reconnect, expected_version: 'expected', reboot_delay: 0, timeout: 1)
|
|
64
|
+
end.to raise_error(IOError, /version mismatch/)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'verifies fresh post-reboot metadata over a new production TCP PhoneAPI connection' do
|
|
68
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
69
|
+
worker = Thread.new do
|
|
70
|
+
socket = server.accept
|
|
71
|
+
buffer = +''.b
|
|
72
|
+
loop do
|
|
73
|
+
buffer << socket.read(1)
|
|
74
|
+
next unless buffer.end_with?("\x94\xC3".b)
|
|
75
|
+
|
|
76
|
+
length = socket.read(2).unpack1('n')
|
|
77
|
+
request = Meshtastic::ToRadio.decode(socket.read(length))
|
|
78
|
+
replies = if request.want_config_id.positive?
|
|
79
|
+
[Meshtastic::FromRadio.new(my_info: Meshtastic::MyNodeInfo.new(my_node_num: 123)),
|
|
80
|
+
Meshtastic::FromRadio.new(metadata: Meshtastic::DeviceMetadata.new(firmware_version: 'STALE')),
|
|
81
|
+
Meshtastic::FromRadio.new(config_complete_id: request.want_config_id)]
|
|
82
|
+
elsif request.packet
|
|
83
|
+
admin = Meshtastic::AdminMessage.decode(request.packet.decoded.payload)
|
|
84
|
+
expect(admin.get_device_metadata_request).to be true
|
|
85
|
+
payload = Meshtastic::AdminMessage.new(get_device_metadata_response: Meshtastic::DeviceMetadata.new(firmware_version: '2.7.1', hw_model: :HELTEC_V3)).to_proto
|
|
86
|
+
[Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(from: 123, decoded: Meshtastic::Data.new(portnum: :ADMIN_APP, request_id: request.packet.id, payload: payload)))]
|
|
87
|
+
else
|
|
88
|
+
[]
|
|
89
|
+
end
|
|
90
|
+
replies.each do |reply|
|
|
91
|
+
bytes = reply.to_proto
|
|
92
|
+
socket.write("\x94\xC3".b + [bytes.bytesize].pack('n') + bytes)
|
|
93
|
+
end
|
|
94
|
+
break if request.packet
|
|
95
|
+
end
|
|
96
|
+
ensure
|
|
97
|
+
socket&.close
|
|
98
|
+
end
|
|
99
|
+
worker.report_on_exception = false
|
|
100
|
+
result = described_class.verify_reboot(transport: :tcp, connection: { host: '127.0.0.1', port: server.addr[1] }, expected_version: '2.7.1', expected_node: 123, reboot_delay: 0, timeout: 2)
|
|
101
|
+
expect(result).to include(status: :boot_verified, firmware_version: '2.7.1', node_num: 123)
|
|
102
|
+
worker.value
|
|
103
|
+
ensure
|
|
104
|
+
worker&.kill
|
|
105
|
+
worker&.join
|
|
106
|
+
server&.close
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
RSpec.shared_context 'a unified OTA TCP loader' do
|
|
111
|
+
def with_loader(options = {}, &handler)
|
|
112
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
113
|
+
worker = Thread.new do
|
|
114
|
+
client = server.accept
|
|
115
|
+
handler.call(client)
|
|
116
|
+
ensure
|
|
117
|
+
client&.close
|
|
118
|
+
end
|
|
119
|
+
worker.report_on_exception = false
|
|
120
|
+
yield_options = { protocol: :unified_wifi, host: '127.0.0.1', port: server.addr[1], bytes: 'abc', timeout: 0.3 }
|
|
121
|
+
result = described_class.install(yield_options.merge(options))
|
|
122
|
+
raise 'loader handshake did not finish' unless worker.join(1)
|
|
123
|
+
|
|
124
|
+
worker.value
|
|
125
|
+
result
|
|
126
|
+
ensure
|
|
127
|
+
worker&.kill
|
|
128
|
+
worker&.join
|
|
129
|
+
server&.close
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
describe Meshtastic::Admin::Firmware do
|
|
134
|
+
include_context 'a unified OTA TCP loader'
|
|
135
|
+
|
|
136
|
+
it 'performs the unified WiFi handshake and waits for final verified OK' do
|
|
137
|
+
result = with_loader do |client|
|
|
138
|
+
expect(client.gets).to eq("VERSION\n")
|
|
139
|
+
client.write("OK 1 2.7.0 3 v1.0\n")
|
|
140
|
+
expect(client.gets).to eq("OTA 3 #{Digest::SHA256.hexdigest('abc')}\n")
|
|
141
|
+
client.write("ERASING\nOK\n")
|
|
142
|
+
expect(client.read(3)).to eq('abc')
|
|
143
|
+
client.write("ACK\nOK\n")
|
|
144
|
+
end
|
|
145
|
+
expect(result).to include(status: :verified, bytes: 3, sha256: Digest::SHA256.hexdigest('abc'))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'retries refused loader connections before issuing the OTA command' do
|
|
149
|
+
attempts = 0
|
|
150
|
+
allow(Socket).to receive(:tcp).and_wrap_original do |original, *args, **keywords|
|
|
151
|
+
attempts += 1
|
|
152
|
+
raise Errno::ECONNREFUSED if attempts == 1
|
|
153
|
+
|
|
154
|
+
original.call(*args, **keywords)
|
|
155
|
+
end
|
|
156
|
+
result = with_loader do |client|
|
|
157
|
+
expect(client.gets).to eq("VERSION\n")
|
|
158
|
+
client.write("OK 1 2.7.0 3 v1.0\n")
|
|
159
|
+
client.gets
|
|
160
|
+
client.write("OK\n")
|
|
161
|
+
client.read(3)
|
|
162
|
+
client.write("OK\n")
|
|
163
|
+
end
|
|
164
|
+
expect(result[:status]).to eq(:verified)
|
|
165
|
+
expect(attempts).to eq(2)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'validates all install options before connecting or reading files' do
|
|
169
|
+
defaults = { protocol: :unified_wifi, host: '127.0.0.1', bytes: 'abc' }
|
|
170
|
+
[{ bytes: '' }, { bytes: 123 }, { bytes: nil }, { firmware: '/missing', bytes: 'abc' },
|
|
171
|
+
{ host: '' }, { port: 0 }, { timeout: 0 }, { timeout: Float::INFINITY },
|
|
172
|
+
{ retries: -1 }, { retry_delay: -1 }, { mode: :OTA_BLE }, { tcp_obj: Object.new },
|
|
173
|
+
{ to: '!aabbccdd' }, { protocol: :unified_wifi, bluetooth_obj: Object.new }].each do |invalid|
|
|
174
|
+
expect(Socket).not_to receive(:tcp)
|
|
175
|
+
expect { described_class.install(defaults.merge(invalid)) }.to raise_error(ArgumentError)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'rejects invalid OTA modes and inconsistent or nonbinary supplied hashes before sending' do
|
|
180
|
+
[{ mode: :UNKNOWN, bytes: 'abc' }, { ota_hash: Object.new },
|
|
181
|
+
{ ota_hash: 'a' * 32, bytes: 'abc' }, { ota_hash: 'a' * 64 }].each do |invalid|
|
|
182
|
+
expect(Meshtastic::Admin).not_to receive(:send)
|
|
183
|
+
expect { described_class.request_ota(invalid) }.to raise_error(ArgumentError)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
describe Meshtastic::Admin::Firmware do
|
|
189
|
+
include_context 'a unified OTA TCP loader'
|
|
190
|
+
|
|
191
|
+
it 'hashes files and sends the real OTA and DFU Admin protobuf fields' do
|
|
192
|
+
written = +''.b
|
|
193
|
+
connection = Object.new
|
|
194
|
+
connection.define_singleton_method(:write) do |data|
|
|
195
|
+
written << data
|
|
196
|
+
data.bytesize
|
|
197
|
+
end
|
|
198
|
+
connection.define_singleton_method(:flush) { true }
|
|
199
|
+
serial = { serial_conn: connection, my_node_num: 0xb0b }
|
|
200
|
+
described_class.request_ota(serial_obj: serial, bytes: 'abc', mode: :OTA_WIFI)
|
|
201
|
+
length = written.byteslice(2, 2).unpack1('n')
|
|
202
|
+
packet = Meshtastic::ToRadio.decode(written.byteslice(4, length)).packet
|
|
203
|
+
admin = Meshtastic::AdminMessage.decode(packet.decoded.payload)
|
|
204
|
+
expect(packet.decoded.portnum).to eq(:ADMIN_APP)
|
|
205
|
+
expect(admin.ota_request.reboot_ota_mode).to eq(:OTA_WIFI)
|
|
206
|
+
expect(admin.ota_request.ota_hash).to eq(Digest::SHA256.digest('abc'))
|
|
207
|
+
written.clear
|
|
208
|
+
described_class.enter_dfu(serial_obj: serial)
|
|
209
|
+
length = written.byteslice(2, 2).unpack1('n')
|
|
210
|
+
packet = Meshtastic::ToRadio.decode(written.byteslice(4, length)).packet
|
|
211
|
+
expect(Meshtastic::AdminMessage.decode(packet.decoded.payload).enter_dfu_mode_request).to be true
|
|
212
|
+
Tempfile.create('firmware') do |file|
|
|
213
|
+
file.write('abc')
|
|
214
|
+
file.flush
|
|
215
|
+
expect(described_class.sha256(firmware: file.path)).to eq(Digest::SHA256.digest('abc'))
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it 'rejects malformed VERSION responses before sending OTA' do
|
|
220
|
+
["ERR Unknown Command\n", "OK\n", 'x' * 513, "OK 1 fw 2 loader"].each do |reply|
|
|
221
|
+
expect do
|
|
222
|
+
with_loader do |client|
|
|
223
|
+
client.gets
|
|
224
|
+
client.write(reply)
|
|
225
|
+
end
|
|
226
|
+
end.to raise_error(IOError)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'surfaces loader handshake and final integrity errors without retrying OTA' do
|
|
231
|
+
[false, true].each do |after_upload|
|
|
232
|
+
expect do
|
|
233
|
+
with_loader do |client|
|
|
234
|
+
client.gets
|
|
235
|
+
client.write("OK 1 fw 2 loader\n")
|
|
236
|
+
client.gets
|
|
237
|
+
if after_upload
|
|
238
|
+
client.write("OK\n")
|
|
239
|
+
client.read(3)
|
|
240
|
+
end
|
|
241
|
+
client.write("ERR Hash Mismatch\n")
|
|
242
|
+
end
|
|
243
|
+
end.to raise_error(IOError, /ERR Hash Mismatch/)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'does not treat disconnect or ACK as final verification' do
|
|
248
|
+
expect do
|
|
249
|
+
with_loader do |client|
|
|
250
|
+
client.gets
|
|
251
|
+
client.write("OK 1 fw 2 loader\n")
|
|
252
|
+
client.gets
|
|
253
|
+
client.write("OK\n")
|
|
254
|
+
client.read(3)
|
|
255
|
+
client.write("ACK\n")
|
|
256
|
+
end
|
|
257
|
+
end.to raise_error(IOError, /closed before confirmation/)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it 'bounds a silent loader handshake and closes its connection' do
|
|
261
|
+
expect do
|
|
262
|
+
with_loader do |client|
|
|
263
|
+
client.gets
|
|
264
|
+
expect(client.read).to eq('')
|
|
265
|
+
end
|
|
266
|
+
end.to raise_error(Timeout::Error)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'exhausts bounded connection retries without sending any admin commands' do
|
|
270
|
+
expect(Meshtastic::Admin).not_to receive(:send)
|
|
271
|
+
expect(Socket).to receive(:tcp).exactly(3).times.and_raise(Errno::ECONNREFUSED)
|
|
272
|
+
expect do
|
|
273
|
+
described_class.install(protocol: :unified_wifi, host: '127.0.0.1', bytes: 'abc', retries: 2, retry_delay: 0)
|
|
274
|
+
end.to raise_error(Errno::ECONNREFUSED)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it 'drains TCP ACKs during upload rather than deadlocking on backpressure' do
|
|
278
|
+
allow(Socket).to receive(:tcp).and_wrap_original do |original, *args, **keywords|
|
|
279
|
+
socket = original.call(*args, **keywords)
|
|
280
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 1024)
|
|
281
|
+
socket
|
|
282
|
+
end
|
|
283
|
+
bytes = 'a' * 1_048_576
|
|
284
|
+
result = with_loader(bytes: bytes, timeout: 5) do |client|
|
|
285
|
+
client.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 1024)
|
|
286
|
+
client.gets
|
|
287
|
+
client.write("OK 1 fw 2 loader\n")
|
|
288
|
+
client.gets
|
|
289
|
+
client.write("OK\n")
|
|
290
|
+
received = +''
|
|
291
|
+
while received.bytesize < bytes.bytesize
|
|
292
|
+
received << client.read(1024)
|
|
293
|
+
client.write("ACK\n" * 1024)
|
|
294
|
+
end
|
|
295
|
+
expect(received).to eq(bytes)
|
|
296
|
+
client.write("OK\n")
|
|
297
|
+
end
|
|
298
|
+
expect(result[:bytes]).to eq(bytes.bytesize)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it 'rejects obsolete firmware XModem helpers and unhandled legacy OTA reboot' do
|
|
302
|
+
%i[xmodem_blocks send_xmodem reboot_ota].each do |method|
|
|
303
|
+
expect { described_class.public_send(method, bytes: 'abc') }
|
|
304
|
+
.to raise_error(NotImplementedError)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'rejects PhoneAPI and MQTT installation without sending any commands' do
|
|
309
|
+
%i[serial_obj tcp_obj bluetooth_obj mqtt_obj].each do |transport|
|
|
310
|
+
expect(Meshtastic::Admin).not_to receive(:send)
|
|
311
|
+
expect { described_class.install(transport => Object.new, bytes: 'abc') }
|
|
312
|
+
.to raise_error(NotImplementedError, /unified_wifi/)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|