meshtastic 0.0.176 → 0.0.177

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.
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'pty'
5
+
6
+ describe Meshtastic::Serial do # rubocop:disable Metrics/BlockLength
7
+ def with_serial_link(opts = {})
8
+ PTY.open do |device, host|
9
+ serial_obj = described_class.connect({ block_dev: host.path, want_config: false }.merge(opts))
10
+ begin
11
+ yield serial_obj, device
12
+ ensure
13
+ described_class.disconnect(serial_obj: serial_obj)
14
+ end
15
+ end
16
+ end
17
+
18
+ def radio_frame(message)
19
+ body = message.to_proto
20
+ [Meshtastic::START1, Meshtastic::START2, body.bytesize].pack('CCn') + body
21
+ end
22
+
23
+ describe '.wait_for_config' do
24
+ it 'times out rather than accepting a mismatched configuration ID' do
25
+ with_serial_link(want_config: true) do |serial_obj, device|
26
+ wrong_id = serial_obj[:config_id] ^ 1
27
+ device.write(radio_frame(Meshtastic::FromRadio.new(config_complete_id: wrong_id)))
28
+ expect do
29
+ described_class.wait_for_config(serial_obj: serial_obj, timeout: 0.05)
30
+ end.to raise_error(Timeout::Error, /No configuration response/)
31
+ end
32
+ end
33
+
34
+ it 'waits for the matching configuration response without consuming received messages' do
35
+ with_serial_link(want_config: true) do |serial_obj, device|
36
+ request = Timeout.timeout(1) do
37
+ expect(device.read(32)).to eq(([Meshtastic::START2] * 32).pack('C*'))
38
+ header = device.read(4)
39
+ Meshtastic::ToRadio.decode(device.read(header.unpack('CCn').last))
40
+ end
41
+ device.write(radio_frame(Meshtastic::FromRadio.new(my_info: Meshtastic::MyNodeInfo.new(my_node_num: 123))))
42
+ device.write(radio_frame(Meshtastic::FromRadio.new(config_complete_id: request.want_config_id)))
43
+ expect(described_class.wait_for_config(serial_obj: serial_obj, timeout: 1)).to eq(serial_obj)
44
+ expect(serial_obj[:my_node_num]).to eq(123)
45
+ expect(described_class.recv_from_radio(serial_obj: serial_obj, timeout: 0).payload_variant).to eq(:my_info)
46
+ end
47
+ end
48
+ end
49
+
50
+ describe 'serial reception' do
51
+ it 'receives after an idle interval longer than the UART read timeout' do
52
+ with_serial_link do |serial_obj, device|
53
+ expect(described_class.recv_from_radio(serial_obj: serial_obj, timeout: 0.6)).to be_nil
54
+ message = Meshtastic::FromRadio.new(my_info: Meshtastic::MyNodeInfo.new(my_node_num: 123))
55
+ device.write(radio_frame(message))
56
+ expect(described_class.recv_from_radio(serial_obj: serial_obj, timeout: 1)).to eq(message)
57
+ end
58
+ end
59
+
60
+ it 'skips oversized headers and malformed protobufs before the next valid frame' do
61
+ with_serial_link do |serial_obj, device|
62
+ message = Meshtastic::FromRadio.new(my_info: Meshtastic::MyNodeInfo.new(my_node_num: 123))
63
+ expect do
64
+ device.write([Meshtastic::START1, Meshtastic::START2, 513].pack('CCn'))
65
+ device.write([Meshtastic::START1, Meshtastic::START2, 1, 255].pack('CCnC'))
66
+ device.write(radio_frame(message))
67
+ expect(described_class.recv_from_radio(serial_obj: serial_obj, timeout: 1)).to eq(message)
68
+ end.to output(/failed to decode FromRadio/).to_stderr
69
+ end
70
+ end
71
+
72
+ it 'unblocks an indefinite receive when disconnected' do
73
+ with_serial_link do |serial_obj, _device|
74
+ receiver = Thread.new { described_class.recv_from_radio(serial_obj: serial_obj, timeout: nil) }
75
+ begin
76
+ described_class.disconnect(serial_obj: serial_obj)
77
+ expect(receiver.join(1)).to eq(receiver)
78
+ expect(receiver.value).to be_nil
79
+ expect(described_class.drain_from_radio(serial_obj: serial_obj)).to eq([])
80
+ ensure
81
+ receiver.kill.join
82
+ end
83
+ end
84
+ end
85
+
86
+ it 'treats text payloads as UTF-8, never as nested protobuf messages' do
87
+ with_serial_link do |serial_obj, device|
88
+ text = "\n\x02hi"
89
+ device.write(radio_frame(Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(
90
+ decoded: Meshtastic::Data.new(portnum: :TEXT_MESSAGE_APP, payload: text)
91
+ ))))
92
+ Timeout.timeout(1) do
93
+ described_class.subscribe(serial_obj: serial_obj) do |message|
94
+ expect(message.dig(:packet, :decoded, :payload)).to eq(text)
95
+ expect(message.dig(:packet, :decoded, :payload).encoding).to eq(Encoding::UTF_8)
96
+ break
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ it 'reports an unplugged device to blocked receivers' do
103
+ with_serial_link do |serial_obj, device|
104
+ device.close
105
+ expect { described_class.recv_from_radio(serial_obj: serial_obj, timeout: 1) }.to raise_error(IOError)
106
+ end
107
+ end
108
+
109
+ it 'keeps independent receive queues and disconnect state for each device' do
110
+ with_serial_link do |first, first_device|
111
+ with_serial_link do |second, second_device|
112
+ first_message = Meshtastic::FromRadio.new(my_info: Meshtastic::MyNodeInfo.new(my_node_num: 1))
113
+ second_message = Meshtastic::FromRadio.new(my_info: Meshtastic::MyNodeInfo.new(my_node_num: 2))
114
+ second_device.write(radio_frame(second_message))
115
+ first_device.write(radio_frame(first_message))
116
+ expect(described_class.recv_from_radio(serial_obj: first, timeout: 1)).to eq(first_message)
117
+ expect(described_class.recv_from_radio(serial_obj: second, timeout: 1)).to eq(second_message)
118
+ described_class.disconnect(serial_obj: first)
119
+ second_device.write(radio_frame(second_message))
120
+ expect(described_class.recv_from_radio(serial_obj: second, timeout: 1)).to eq(second_message)
121
+ end
122
+ end
123
+ end
124
+
125
+ it 'returns immediately when polling an empty queue with timeout zero' do
126
+ with_serial_link do |_serial_obj, _device|
127
+ result = Timeout.timeout(0.2) { described_class.recv_from_radio(timeout: 0) }
128
+ expect(result).to be_nil
129
+ end
130
+ end
131
+
132
+ it 'yields received text through subscription filters' do
133
+ with_serial_link do |serial_obj, device|
134
+ %w[hidden hello].each do |text|
135
+ device.write(radio_frame(Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(
136
+ from: 123, decoded: Meshtastic::Data.new(portnum: :TEXT_MESSAGE_APP, payload: text)
137
+ ))))
138
+ end
139
+ received = nil
140
+ Timeout.timeout(2) do
141
+ described_class.subscribe(serial_obj: serial_obj, include: 'TEXT_MESSAGE_APP', exclude: 'hidden') do |message|
142
+ received = message.dig(:packet, :decoded, :payload)
143
+ break
144
+ end
145
+ end
146
+ expect(received).to eq('hello')
147
+ end
148
+ end
149
+
150
+ it 'resynchronizes overlapping start bytes and receives a fragmented text packet' do
151
+ with_serial_link do |_serial_obj, device|
152
+ message = Meshtastic::FromRadio.new(packet: Meshtastic::MeshPacket.new(
153
+ from: 123, decoded: Meshtastic::Data.new(portnum: :TEXT_MESSAGE_APP, payload: 'hello')
154
+ ))
155
+ device.write("boot\n\x94".b)
156
+ radio_frame(message).each_byte { |byte| device.write([byte].pack('C')) }
157
+ expect(described_class.recv_from_radio(timeout: 1)).to eq(message)
158
+ end
159
+ end
160
+ end
161
+
162
+ def fake_serial_obj
163
+ written = +''.b
164
+ serial_conn = Object.new
165
+ serial_conn.define_singleton_method(:write) do |b|
166
+ written << b
167
+ b.bytesize
168
+ end
169
+ serial_conn.define_singleton_method(:flush) { true }
170
+ serial_conn.define_singleton_method(:closed?) { false }
171
+ serial_conn.define_singleton_method(:close) { true }
172
+ serial_conn.define_singleton_method(:written) { written }
173
+ { serial_conn: serial_conn, written: written, my_node_num: 0xb0b }
174
+ end
175
+
176
+ describe '.send_to_radio' do
177
+ it 'writes the entire frame when the transport accepts only part of each write' do
178
+ serial_obj = fake_serial_obj
179
+ written = serial_obj[:written]
180
+ serial_obj[:serial_conn].define_singleton_method(:write) do |bytes|
181
+ written << bytes.byteslice(0, 2)
182
+ [bytes.bytesize, 2].min
183
+ end
184
+ message = Meshtastic::ToRadio.new(want_config_id: 42)
185
+ described_class.send_to_radio(serial_obj: serial_obj, to_radio: message)
186
+ expect(written).to eq(radio_frame(message))
187
+ end
188
+
189
+ it 'frames a ToRadio with START1/START2 and big-endian length' do
190
+ serial_obj = fake_serial_obj
191
+ to_radio = Meshtastic::ToRadio.new
192
+ to_radio.want_config_id = 42
193
+ body = to_radio.to_proto
194
+
195
+ described_class.send_to_radio(serial_obj: serial_obj, to_radio: to_radio)
196
+ frame = serial_obj[:serial_conn].written
197
+
198
+ expect(frame.getbyte(0)).to eq(Meshtastic::START1)
199
+ expect(frame.getbyte(1)).to eq(Meshtastic::START2)
200
+ expect((frame.getbyte(2) << 8) + frame.getbyte(3)).to eq(body.bytesize)
201
+ expect(frame.byteslice(4, body.bytesize)).to eq(body)
202
+ end
203
+
204
+ it 'accepts a pre-serialized String body' do
205
+ serial_obj = fake_serial_obj
206
+ body = Meshtastic::ToRadio.new.tap { |t| t.want_config_id = 7 }.to_proto
207
+ described_class.send_to_radio(serial_obj: serial_obj, to_radio: body)
208
+ frame = serial_obj[:serial_conn].written
209
+ expect(frame.bytesize).to eq(4 + body.bytesize)
210
+ end
211
+ end
212
+
213
+ describe '.send_text' do
214
+ it 'rejects text exceeding the byte limit without closing the connection' do
215
+ serial_obj = fake_serial_obj
216
+ expect do
217
+ described_class.send_text(serial_obj: serial_obj, text: 'é' * Meshtastic::Constants::DATA_PAYLOAD_LEN)
218
+ end.to raise_error(ArgumentError, /Bytes/)
219
+ expect(serial_obj[:written]).to be_empty
220
+ expect(serial_obj[:closing]).to be_nil
221
+ end
222
+
223
+ it 'lets the firmware supply the sender before my_info arrives' do
224
+ serial_obj = fake_serial_obj
225
+ serial_obj[:my_node_num] = nil
226
+ described_class.send_text(serial_obj: serial_obj, text: 'ping')
227
+ expect(Meshtastic::ToRadio.decode(serial_obj[:written].byteslice(4..)).packet.from).to eq(0)
228
+ end
229
+
230
+ it 'writes a decoded ToRadio mesh packet for the radio to encrypt' do
231
+ serial_obj = fake_serial_obj
232
+ described_class.send_text(
233
+ serial_obj: serial_obj,
234
+ text: 'ping',
235
+ to: '!ffffffff',
236
+ channel: 0
237
+ )
238
+ frame = serial_obj[:serial_conn].written
239
+ body_len = (frame.getbyte(2) << 8) + frame.getbyte(3)
240
+ tr = Meshtastic::ToRadio.decode(frame.byteslice(4, body_len))
241
+
242
+ expect(tr.packet).not_to be_nil
243
+ expect(tr.packet.decoded).not_to be_nil
244
+ expect(tr.packet.decoded.payload).to eq('ping')
245
+ expect(tr.packet.encrypted.to_s).to eq('')
246
+ expect(tr.packet.from).to eq(0xb0b)
247
+ end
248
+ end
249
+
250
+ describe '.send_data' do
251
+ it 'sends a binary application packet over a real serial transport' do
252
+ with_serial_link do |serial_obj, device|
253
+ data = Meshtastic::Data.new(portnum: :PRIVATE_APP, payload: "\x00\xff".b)
254
+ described_class.send_data(serial_obj: serial_obj, data: data, to: '!0000007b', channel: 1, want_ack: true)
255
+ packet = Timeout.timeout(1) do
256
+ device.read(32)
257
+ header = device.read(4)
258
+ expect(header.bytes.first(2)).to eq([Meshtastic::START1, Meshtastic::START2])
259
+ Meshtastic::ToRadio.decode(device.read(header.unpack('CCn').last)).packet
260
+ end
261
+ expect(packet.decoded).to eq(data)
262
+ expect(packet.to).to eq(123)
263
+ expect(packet.from).to eq(0)
264
+ expect(packet.channel).to eq(1)
265
+ expect(packet.want_ack).to be(true)
266
+ expect(packet.id).to be_positive
267
+ end
268
+ end
269
+ end
270
+
271
+ describe '.disconnect' do
272
+ it 'closes a failed transport without recursively sending disconnect packets' do
273
+ serial_obj = fake_serial_obj
274
+ writes = 0
275
+ serial_obj[:serial_conn].define_singleton_method(:write) do |_bytes|
276
+ writes += 1
277
+ raise IOError, 'disconnected USB device'
278
+ end
279
+ expect(serial_obj[:serial_conn]).to receive(:close)
280
+ expect { described_class.disconnect(serial_obj: serial_obj) }.not_to raise_error
281
+ expect(writes).to eq(1)
282
+ end
283
+
284
+ it 'returns nil and closes the serial connection' do
285
+ serial_obj = fake_serial_obj
286
+ expect(described_class.disconnect(serial_obj: serial_obj)).to be_nil
287
+ end
288
+ end
289
+
290
+ describe '.help' do
291
+ it 'prints usage without raising' do
292
+ expect { described_class.help }.to output(/USAGE/).to_stdout
293
+ end
294
+ end
295
+ 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.176
4
+ version: 0.0.177
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -205,6 +205,20 @@ dependencies:
205
205
  - - '='
206
206
  - !ruby/object:Gem::Version
207
207
  version: 3.10.2
208
+ - !ruby/object:Gem::Dependency
209
+ name: ruby-dbus
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - '='
213
+ - !ruby/object:Gem::Version
214
+ version: 0.25.0
215
+ type: :runtime
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - '='
220
+ - !ruby/object:Gem::Version
221
+ version: 0.25.0
208
222
  - !ruby/object:Gem::Dependency
209
223
  name: rvm
210
224
  requirement: !ruby/object:Gem::Requirement
@@ -278,6 +292,8 @@ files:
278
292
  - lib/meshtastic/apponly_pb.rb
279
293
  - lib/meshtastic/atak.rb
280
294
  - lib/meshtastic/atak_pb.rb
295
+ - lib/meshtastic/bluetooth.rb
296
+ - lib/meshtastic/bluetooth/bluez.rb
281
297
  - lib/meshtastic/cannedmessages.rb
282
298
  - lib/meshtastic/cannedmessages_pb.rb
283
299
  - lib/meshtastic/channel.rb
@@ -295,6 +311,7 @@ files:
295
311
  - lib/meshtastic/interdevice_pb.rb
296
312
  - lib/meshtastic/localonly.rb
297
313
  - lib/meshtastic/localonly_pb.rb
314
+ - lib/meshtastic/lorawan_bridge_pb.rb
298
315
  - lib/meshtastic/mesh_beacon_pb.rb
299
316
  - lib/meshtastic/mesh_interface.rb
300
317
  - lib/meshtastic/mesh_pb.rb
@@ -311,8 +328,8 @@ files:
311
328
  - lib/meshtastic/remote_hardware_pb.rb
312
329
  - lib/meshtastic/rtttl.rb
313
330
  - lib/meshtastic/rtttl_pb.rb
331
+ - lib/meshtastic/serial.rb
314
332
  - lib/meshtastic/serial_hal_pb.rb
315
- - lib/meshtastic/serial_interface.rb
316
333
  - lib/meshtastic/storeforward.rb
317
334
  - lib/meshtastic/storeforward_pb.rb
318
335
  - lib/meshtastic/stream_interface.rb
@@ -332,6 +349,8 @@ files:
332
349
  - spec/lib/meshtastic/apponly_spec.rb
333
350
  - spec/lib/meshtastic/atak_pb_spec.rb
334
351
  - spec/lib/meshtastic/atak_spec.rb
352
+ - spec/lib/meshtastic/bluetooth/bluez_spec.rb
353
+ - spec/lib/meshtastic/bluetooth_spec.rb
335
354
  - spec/lib/meshtastic/cannedmessages_pb_spec.rb
336
355
  - spec/lib/meshtastic/cannedmessages_spec.rb
337
356
  - spec/lib/meshtastic/channel_pb_spec.rb
@@ -349,6 +368,7 @@ files:
349
368
  - spec/lib/meshtastic/interdevice_pb_spec.rb
350
369
  - spec/lib/meshtastic/localonly_pb_spec.rb
351
370
  - spec/lib/meshtastic/localonly_spec.rb
371
+ - spec/lib/meshtastic/lorawan_bridge_pb_spec.rb
352
372
  - spec/lib/meshtastic/mesh_beacon_pb_spec.rb
353
373
  - spec/lib/meshtastic/mesh_interface_spec.rb
354
374
  - spec/lib/meshtastic/mesh_pb_spec.rb
@@ -366,7 +386,7 @@ files:
366
386
  - spec/lib/meshtastic/rtttl_pb_spec.rb
367
387
  - spec/lib/meshtastic/rtttl_spec.rb
368
388
  - spec/lib/meshtastic/serial_hal_pb_spec.rb
369
- - spec/lib/meshtastic/serial_interface_spec.rb
389
+ - spec/lib/meshtastic/serial_spec.rb
370
390
  - spec/lib/meshtastic/storeforward_pb_spec.rb
371
391
  - spec/lib/meshtastic/storeforward_spec.rb
372
392
  - spec/lib/meshtastic/stream_interface_spec.rb
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- describe Meshtastic::SerialInterface do
6
- def fake_serial_obj
7
- written = +''.b
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.define_singleton_method(:written) { written }
17
- { serial_conn: serial_conn, written: written, my_node_num: 0xb0b }
18
- end
19
-
20
- describe '.send_to_radio' do
21
- it 'frames a ToRadio with START1/START2 and big-endian length' do
22
- serial_obj = fake_serial_obj
23
- to_radio = Meshtastic::ToRadio.new
24
- to_radio.want_config_id = 42
25
- body = to_radio.to_proto
26
-
27
- described_class.send_to_radio(serial_obj: serial_obj, to_radio: to_radio)
28
- frame = serial_obj[:serial_conn].written
29
-
30
- expect(frame.getbyte(0)).to eq(Meshtastic::START1)
31
- expect(frame.getbyte(1)).to eq(Meshtastic::START2)
32
- expect((frame.getbyte(2) << 8) + frame.getbyte(3)).to eq(body.bytesize)
33
- expect(frame.byteslice(4, body.bytesize)).to eq(body)
34
- end
35
-
36
- it 'accepts a pre-serialized String body' do
37
- serial_obj = fake_serial_obj
38
- body = Meshtastic::ToRadio.new.tap { |t| t.want_config_id = 7 }.to_proto
39
- described_class.send_to_radio(serial_obj: serial_obj, to_radio: body)
40
- frame = serial_obj[:serial_conn].written
41
- expect(frame.bytesize).to eq(4 + body.bytesize)
42
- end
43
- end
44
-
45
- describe '.send_text' do
46
- it 'writes a decoded ToRadio mesh packet for the radio to encrypt' do
47
- serial_obj = fake_serial_obj
48
- described_class.send_text(
49
- serial_obj: serial_obj,
50
- text: 'ping',
51
- to: '!ffffffff',
52
- channel: 0
53
- )
54
- frame = serial_obj[:serial_conn].written
55
- body_len = (frame.getbyte(2) << 8) + frame.getbyte(3)
56
- tr = Meshtastic::ToRadio.decode(frame.byteslice(4, body_len))
57
-
58
- expect(tr.packet).not_to be_nil
59
- expect(tr.packet.decoded).not_to be_nil
60
- expect(tr.packet.decoded.payload).to eq('ping')
61
- expect(tr.packet.encrypted.to_s).to eq('')
62
- expect(tr.packet.from).to eq(0xb0b)
63
- end
64
- end
65
-
66
- describe '.disconnect' do
67
- it 'returns nil and closes the serial connection' do
68
- serial_obj = fake_serial_obj
69
- expect(described_class.disconnect(serial_obj: serial_obj)).to be_nil
70
- end
71
- end
72
-
73
- describe '.help' do
74
- it 'prints usage without raising' do
75
- expect { described_class.help }.to output(/USAGE/).to_stdout
76
- end
77
- end
78
- end