meshtastic 0.0.184 → 0.0.186

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +1 -0
  3. data/Gemfile +2 -1
  4. data/documentation/README.md +5 -0
  5. data/documentation/forwarder.md +149 -0
  6. data/documentation/mesh-interface.md +157 -1
  7. data/documentation/payload-formats.md +187 -0
  8. data/documentation/reticulum.md +89 -0
  9. data/lib/meshtastic/atak.rb +98 -13
  10. data/lib/meshtastic/atak_pb.rb +3 -1
  11. data/lib/meshtastic/config_pb.rb +1 -1
  12. data/lib/meshtastic/field_metadata_pb.rb +1 -1
  13. data/lib/meshtastic/forwarder.rb +190 -0
  14. data/lib/meshtastic/forwarder_pb.rb +77 -0
  15. data/lib/meshtastic/mesh_beacon_pb.rb +1 -1
  16. data/lib/meshtastic/mesh_interface.rb +115 -65
  17. data/lib/meshtastic/mesh_pb.rb +2 -1
  18. data/lib/meshtastic/module_config_pb.rb +1 -1
  19. data/lib/meshtastic/mqtt.rb +4 -34
  20. data/lib/meshtastic/payload_compression.rb +90 -0
  21. data/lib/meshtastic/payload_formats.rb +248 -0
  22. data/lib/meshtastic/reticulum.rb +71 -0
  23. data/lib/meshtastic/serial.rb +1 -19
  24. data/lib/meshtastic/telemetry_pb.rb +2 -2
  25. data/lib/meshtastic/unishox2.rb +467 -0
  26. data/lib/meshtastic/version.rb +1 -1
  27. data/lib/meshtastic.rb +4 -0
  28. data/meshtastic.gemspec +2 -0
  29. data/spec/lib/meshtastic/atak_spec.rb +50 -0
  30. data/spec/lib/meshtastic/bluetooth_spec.rb +14 -0
  31. data/spec/lib/meshtastic/forwarder_pb_spec.rb +11 -0
  32. data/spec/lib/meshtastic/forwarder_spec.rb +97 -0
  33. data/spec/lib/meshtastic/mesh_interface_spec.rb +169 -0
  34. data/spec/lib/meshtastic/mqtt_spec.rb +56 -0
  35. data/spec/lib/meshtastic/payload_compression_spec.rb +43 -0
  36. data/spec/lib/meshtastic/payload_formats_spec.rb +160 -0
  37. data/spec/lib/meshtastic/reticulum_spec.rb +91 -0
  38. data/spec/lib/meshtastic/serial_spec.rb +46 -0
  39. data/spec/lib/meshtastic/tcp_spec.rb +15 -0
  40. data/spec/lib/meshtastic/unishox2_spec.rb +34 -0
  41. data/spec/support/payload_fixtures.rb +123 -0
  42. data/spec/support/reticulum_fixtures.json +12 -0
  43. data/spec/support/tak_codec_fixtures.rb +4 -0
  44. data/spec/support/unishox_fixtures.rb +4 -0
  45. metadata +36 -3
@@ -0,0 +1,467 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Meshtastic
4
+ # Default-preset Unishox2, translated from siara-cc/Unishox2 (Apache-2.0),
5
+ # revision 4981d9403de3bdd1938a635ab73f8a2c5a536059.
6
+ # Copyright (C) 2020 Siara Logics (cc).
7
+ # Authors: Arundale Ramanathan, James Z. M. Gao.
8
+ # Ruby adaptation changes: default preset only, bounded strings, strict
9
+ # UTF-8/back-reference validation and explicit malformed-code errors.
10
+ # Apache-2.0 license text is included after __END__ below.
11
+ # No native decoder receives untrusted input. Output and input are bounded.
12
+ module Unishox2
13
+ MAX_DECODED = 4096
14
+
15
+ public_class_method def self.decode(opts = {})
16
+ Decoder.new(opts.fetch(:payload).to_s.b).decode
17
+ end
18
+
19
+ public_class_method def self.authors
20
+ 'Unishox2: Arundale Ramanathan, James Z. M. Gao (Apache-2.0); Ruby adaptation: Meshtastic contributors'
21
+ end
22
+
23
+ public_class_method def self.help
24
+ puts "USAGE:
25
+ # Decode default-preset Unishox2 compressed text.
26
+ #{self}.decode(payload: 'required - compressed bytes, maximum 4096 decoded bytes')
27
+ # Report codec implementation authors.
28
+ #{self}.authors
29
+ "
30
+ end
31
+
32
+ class DecoderState
33
+ V_CODES = %w[00 010 011 1000 1001 1010 1011 1100 11010 11011 111000 111001 111010 1110110 1110111 1111000 1111001 1111010 11110110 11110111 11111000 11111001 11111010 11111011 11111100 11111101 11111110 11111111].freeze
34
+ H_CODES = %w[00 01 10 110 111].freeze
35
+ SETS = ["\0 etaoinsrlcdhupmbgwfyvkqjxz", "\"{}_<>:\n\0[]\\;'\t@*&?!^|\r~`\0\0\0", "\0,.01925-/34678() =+$%#\0\0\0\0\0"].map(&:bytes).freeze
36
+ FREQUENCIES = ['": "', '": ', '</', '="', '":"', '://'].freeze
37
+ TEMPLATES = ['tfff-of-tfTtf:rf:rf.fffZ', 'tfff-of-tf', '(fff) fff-ffff', 'tf:rf:rf'].freeze
38
+ class EndOfBits < StandardError; end
39
+
40
+ def initialize(bytes)
41
+ raise ArgumentError, 'Unishox2 input exceeds 4096 bytes' if bytes.bytesize > MAX_DECODED
42
+ raise ArgumentError, 'invalid Unishox2 magic bit' if !bytes.empty? && bytes.getbyte(0) < 128
43
+
44
+ @bits = bytes.unpack1('B*')
45
+ @pos = bytes.empty? ? 0 : 1
46
+ @out = +''.b
47
+ @state = @h = 0
48
+ @upper = false
49
+ @unicode = 0
50
+ end
51
+
52
+ def decode
53
+ begin
54
+ while @pos < @bits.length
55
+ remaining = @bits[@pos..]
56
+ terminator = { 0 => '001011111111', 2 => '11111111', 4 => '11111101011111111' }.fetch(@state)
57
+ # Default encoder truncates the terminator to the last byte.
58
+ break if remaining.length < 8 && terminator.start_with?(remaining) && !(@h == 4 && @state != 4)
59
+
60
+ decode_symbol
61
+ end
62
+ rescue EndOfBits
63
+ raise ArgumentError, 'truncated Unishox2 code (not terminator padding)'
64
+ end
65
+ text = @out.force_encoding(Encoding::UTF_8)
66
+ raise ArgumentError, 'invalid UTF-8 in Unishox2 text' unless text.valid_encoding?
67
+
68
+ text
69
+ end
70
+
71
+ private
72
+
73
+ def bits(count)
74
+ raise EndOfBits if @pos + count > @bits.length
75
+
76
+ value = @bits[@pos, count].to_i(2)
77
+ @pos += count
78
+ value
79
+ end
80
+
81
+ def code(codes)
82
+ index = codes.index { |word| @bits[@pos, word.length] == word }
83
+ raise EndOfBits unless index
84
+
85
+ @pos += codes[index].length
86
+ index
87
+ end
88
+
89
+ def step(limit)
90
+ index = 0
91
+ index += 1 while index < limit && bits(1) == 1
92
+ index
93
+ end
94
+
95
+ def count
96
+ index = step(4)
97
+ bits([2, 4, 7, 11, 16][index]) + [0, 4, 20, 148, 2196][index]
98
+ end
99
+
100
+ def append(value)
101
+ value = value.chr(Encoding::BINARY) if value.is_a?(Integer)
102
+ raise ArgumentError, 'Unishox2 decoded output exceeds 4096 bytes' if @out.bytesize + value.bytesize > MAX_DECODED
103
+
104
+ @out << value.b
105
+ end
106
+
107
+ def repeat
108
+ length = count + 5
109
+ distance = count + 4
110
+ raise ArgumentError, 'invalid Unishox2 back-reference' if distance > @out.bytesize || length > distance
111
+
112
+ append(@out.byteslice(@out.bytesize - distance, length))
113
+ end
114
+
115
+ def alpha_shift
116
+ if @upper
117
+ @upper = false
118
+ return [0, false, true]
119
+ end
120
+ vertical = code(V_CODES)
121
+ if vertical.zero?
122
+ @h = code(H_CODES)
123
+ if @h.zero?
124
+ @upper = true
125
+ return [0, false, true]
126
+ end
127
+ end
128
+ [vertical, true, false]
129
+ end
130
+ end
131
+
132
+ class Decoder < DecoderState
133
+ private
134
+
135
+ def unicode?
136
+ index = step(5)
137
+ if index == 5
138
+ special = step(4)
139
+ if special == 1
140
+ @h = code(H_CODES)
141
+ if [0, 4].include?(@h)
142
+ @state = @h
143
+ return true
144
+ end
145
+ if @h == 3
146
+ repeat
147
+ @h = @state
148
+ return true
149
+ end
150
+ return false
151
+ end
152
+ append({ 0 => ' ', 2 => ',', 3 => '.', 4 => "\n" }.fetch(special))
153
+ return true
154
+ end
155
+ sign = bits(1)
156
+ delta = bits([6, 12, 14, 16, 21][index]) + [0, 64, 4160, 20_544, 86_080][index]
157
+ @unicode += sign == 1 ? -delta : delta
158
+ raise ArgumentError, 'invalid Unishox2 Unicode codepoint' unless @unicode.between?(128, 0x10ffff) && !@unicode.between?(0xd800, 0xdfff)
159
+
160
+ append([@unicode].pack('U'))
161
+ true
162
+ end
163
+
164
+ def nibble_block
165
+ kind = step(5)
166
+ if kind.zero?
167
+ template = TEMPLATES[step(4)]
168
+ raise ArgumentError, 'invalid Unishox2 template' unless template
169
+
170
+ length = template.length - count
171
+ raise ArgumentError, 'invalid Unishox2 template length' if length.negative?
172
+
173
+ template[0, length].each_char do |char|
174
+ width = { 'f' => 4, 'F' => 4, 'r' => 3, 't' => 2, 'o' => 1 }[char]
175
+ append(if width
176
+ bits(width).to_s(16).public_send(char == 'f' ? :downcase : :upcase)
177
+ else
178
+ char
179
+ end)
180
+ end
181
+ elsif kind == 5
182
+ length = count
183
+ raise ArgumentError, 'invalid Unishox2 binary length' if length.zero?
184
+
185
+ length.times { append(bits(8)) }
186
+ else
187
+ uuid = [2, 4].include?(kind)
188
+ length = uuid ? 32 : count
189
+ raise ArgumentError, 'invalid Unishox2 hex length' if length.zero?
190
+
191
+ length.downto(1) do |remaining|
192
+ char = bits(4).to_s(16)
193
+ append(kind < 3 ? char : char.upcase)
194
+ append('-') if uuid && [25, 21, 17, 13].include?(remaining)
195
+ end
196
+ end
197
+ @h = 4 if @state == 4
198
+ end
199
+
200
+ def decode_symbol
201
+ if @state == 4 || @h == 4
202
+ @h = @state unless @state == 4
203
+ return if unicode?
204
+ else
205
+ @h = @state
206
+ end
207
+ upper = @upper
208
+ vertical = code(V_CODES)
209
+ if vertical.zero? && @h != 1
210
+ @h = code(H_CODES) unless @h == 2 && @state == 4
211
+ if @h.zero?
212
+ if @state.zero?
213
+ vertical, upper, finished = alpha_shift
214
+ return if finished
215
+ else
216
+ @state = 0
217
+ return
218
+ end
219
+ elsif @h == 3
220
+ repeat
221
+ return
222
+ elsif @h == 4
223
+ return
224
+ else
225
+ vertical = code(V_CODES) unless @h == 2 && @state == 4
226
+ return nibble_block if @h == 2 && vertical.zero?
227
+ end
228
+ end
229
+ if upper && vertical == 1
230
+ @state = @h = 4
231
+ return
232
+ end
233
+ char = @h < 3 ? SETS[@h][vertical] : 0
234
+ if char.between?(97, 122)
235
+ @state = 0
236
+ char -= 32 if upper
237
+ elsif char.between?(48, 57)
238
+ @state = 2
239
+ elsif char.zero?
240
+ if vertical == 8
241
+ append("\r\n")
242
+ elsif @h == 2 && vertical == 26
243
+ length = count + 4
244
+ raise ArgumentError, 'invalid Unishox2 repeat' if @out.empty?
245
+ raise ArgumentError, 'Unishox2 decoded output exceeds 4096 bytes' if @out.bytesize + length > MAX_DECODED
246
+
247
+ append(@out.byteslice(-1, 1) * length)
248
+ elsif @h == 1 && vertical > 24
249
+ append(FREQUENCIES[vertical - 25])
250
+ elsif @h == 2 && vertical.between?(23, 25)
251
+ append(FREQUENCIES[vertical - 20])
252
+ else
253
+ @pos = @bits.length
254
+ end
255
+ @h = 4 if @state == 4
256
+ return
257
+ end
258
+ @h = 4 if @state == 4
259
+ append(char)
260
+ end
261
+ end
262
+ private_constant :Decoder, :DecoderState
263
+ end
264
+ end
265
+
266
+ __END__
267
+ Apache License
268
+ Version 2.0, January 2004
269
+ http://www.apache.org/licenses/
270
+
271
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
272
+
273
+ 1. Definitions.
274
+
275
+ "License" shall mean the terms and conditions for use, reproduction,
276
+ and distribution as defined by Sections 1 through 9 of this document.
277
+
278
+ "Licensor" shall mean the copyright owner or entity authorized by
279
+ the copyright owner that is granting the License.
280
+
281
+ "Legal Entity" shall mean the union of the acting entity and all
282
+ other entities that control, are controlled by, or are under common
283
+ control with that entity. For the purposes of this definition,
284
+ "control" means (i) the power, direct or indirect, to cause the
285
+ direction or management of such entity, whether by contract or
286
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
287
+ outstanding shares, or (iii) beneficial ownership of such entity.
288
+
289
+ "You" (or "Your") shall mean an individual or Legal Entity
290
+ exercising permissions granted by this License.
291
+
292
+ "Source" form shall mean the preferred form for making modifications,
293
+ including but not limited to software source code, documentation
294
+ source, and configuration files.
295
+
296
+ "Object" form shall mean any form resulting from mechanical
297
+ transformation or translation of a Source form, including but
298
+ not limited to compiled object code, generated documentation,
299
+ and conversions to other media types.
300
+
301
+ "Work" shall mean the work of authorship, whether in Source or
302
+ Object form, made available under the License, as indicated by a
303
+ copyright notice that is included in or attached to the work
304
+ (an example is provided in the Appendix below).
305
+
306
+ "Derivative Works" shall mean any work, whether in Source or Object
307
+ form, that is based on (or derived from) the Work and for which the
308
+ editorial revisions, annotations, elaborations, or other modifications
309
+ represent, as a whole, an original work of authorship. For the purposes
310
+ of this License, Derivative Works shall not include works that remain
311
+ separable from, or merely link (or bind by name) to the interfaces of,
312
+ the Work and Derivative Works thereof.
313
+
314
+ "Contribution" shall mean any work of authorship, including
315
+ the original version of the Work and any modifications or additions
316
+ to that Work or Derivative Works thereof, that is intentionally
317
+ submitted to Licensor for inclusion in the Work by the copyright owner
318
+ or by an individual or Legal Entity authorized to submit on behalf of
319
+ the copyright owner. For the purposes of this definition, "submitted"
320
+ means any form of electronic, verbal, or written communication sent
321
+ to the Licensor or its representatives, including but not limited to
322
+ communication on electronic mailing lists, source code control systems,
323
+ and issue tracking systems that are managed by, or on behalf of, the
324
+ Licensor for the purpose of discussing and improving the Work, but
325
+ excluding communication that is conspicuously marked or otherwise
326
+ designated in writing by the copyright owner as "Not a Contribution."
327
+
328
+ "Contributor" shall mean Licensor and any individual or Legal Entity
329
+ on behalf of whom a Contribution has been received by Licensor and
330
+ subsequently incorporated within the Work.
331
+
332
+ 2. Grant of Copyright License. Subject to the terms and conditions of
333
+ this License, each Contributor hereby grants to You a perpetual,
334
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
335
+ copyright license to reproduce, prepare Derivative Works of,
336
+ publicly display, publicly perform, sublicense, and distribute the
337
+ Work and such Derivative Works in Source or Object form.
338
+
339
+ 3. Grant of Patent License. Subject to the terms and conditions of
340
+ this License, each Contributor hereby grants to You a perpetual,
341
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
342
+ (except as stated in this section) patent license to make, have made,
343
+ use, offer to sell, sell, import, and otherwise transfer the Work,
344
+ where such license applies only to those patent claims licensable
345
+ by such Contributor that are necessarily infringed by their
346
+ Contribution(s) alone or by combination of their Contribution(s)
347
+ with the Work to which such Contribution(s) was submitted. If You
348
+ institute patent litigation against any entity (including a
349
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
350
+ or a Contribution incorporated within the Work constitutes direct
351
+ or contributory patent infringement, then any patent licenses
352
+ granted to You under this License for that Work shall terminate
353
+ as of the date such litigation is filed.
354
+
355
+ 4. Redistribution. You may reproduce and distribute copies of the
356
+ Work or Derivative Works thereof in any medium, with or without
357
+ modifications, and in Source or Object form, provided that You
358
+ meet the following conditions:
359
+
360
+ (a) You must give any other recipients of the Work or
361
+ Derivative Works a copy of this License; and
362
+
363
+ (b) You must cause any modified files to carry prominent notices
364
+ stating that You changed the files; and
365
+
366
+ (c) You must retain, in the Source form of any Derivative Works
367
+ that You distribute, all copyright, patent, trademark, and
368
+ attribution notices from the Source form of the Work,
369
+ excluding those notices that do not pertain to any part of
370
+ the Derivative Works; and
371
+
372
+ (d) If the Work includes a "NOTICE" text file as part of its
373
+ distribution, then any Derivative Works that You distribute must
374
+ include a readable copy of the attribution notices contained
375
+ within such NOTICE file, excluding those notices that do not
376
+ pertain to any part of the Derivative Works, in at least one
377
+ of the following places: within a NOTICE text file distributed
378
+ as part of the Derivative Works; within the Source form or
379
+ documentation, if provided along with the Derivative Works; or,
380
+ within a display generated by the Derivative Works, if and
381
+ wherever such third-party notices normally appear. The contents
382
+ of the NOTICE file are for informational purposes only and
383
+ do not modify the License. You may add Your own attribution
384
+ notices within Derivative Works that You distribute, alongside
385
+ or as an addendum to the NOTICE text from the Work, provided
386
+ that such additional attribution notices cannot be construed
387
+ as modifying the License.
388
+
389
+ You may add Your own copyright statement to Your modifications and
390
+ may provide additional or different license terms and conditions
391
+ for use, reproduction, or distribution of Your modifications, or
392
+ for any such Derivative Works as a whole, provided Your use,
393
+ reproduction, and distribution of the Work otherwise complies with
394
+ the conditions stated in this License.
395
+
396
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
397
+ any Contribution intentionally submitted for inclusion in the Work
398
+ by You to the Licensor shall be under the terms and conditions of
399
+ this License, without any additional terms or conditions.
400
+ Notwithstanding the above, nothing herein shall supersede or modify
401
+ the terms of any separate license agreement you may have executed
402
+ with Licensor regarding such Contributions.
403
+
404
+ 6. Trademarks. This License does not grant permission to use the trade
405
+ names, trademarks, service marks, or product names of the Licensor,
406
+ except as required for reasonable and customary use in describing the
407
+ origin of the Work and reproducing the content of the NOTICE file.
408
+
409
+ 7. Disclaimer of Warranty. Unless required by applicable law or
410
+ agreed to in writing, Licensor provides the Work (and each
411
+ Contributor provides its Contributions) on an "AS IS" BASIS,
412
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
413
+ implied, including, without limitation, any warranties or conditions
414
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
415
+ PARTICULAR PURPOSE. You are solely responsible for determining the
416
+ appropriateness of using or redistributing the Work and assume any
417
+ risks associated with Your exercise of permissions under this License.
418
+
419
+ 8. Limitation of Liability. In no event and under no legal theory,
420
+ whether in tort (including negligence), contract, or otherwise,
421
+ unless required by applicable law (such as deliberate and grossly
422
+ negligent acts) or agreed to in writing, shall any Contributor be
423
+ liable to You for damages, including any direct, indirect, special,
424
+ incidental, or consequential damages of any character arising as a
425
+ result of this License or out of the use or inability to use the
426
+ Work (including but not limited to damages for loss of goodwill,
427
+ work stoppage, computer failure or malfunction, or any and all
428
+ other commercial damages or losses), even if such Contributor
429
+ has been advised of the possibility of such damages.
430
+
431
+ 9. Accepting Warranty or Additional Liability. While redistributing
432
+ the Work or Derivative Works thereof, You may choose to offer,
433
+ and charge a fee for, acceptance of support, warranty, indemnity,
434
+ or other liability obligations and/or rights consistent with this
435
+ License. However, in accepting such obligations, You may act only
436
+ on Your own behalf and on Your sole responsibility, not on behalf
437
+ of any other Contributor, and only if You agree to indemnify,
438
+ defend, and hold each Contributor harmless for any liability
439
+ incurred by, or claims asserted against, such Contributor by reason
440
+ of your accepting any such warranty or additional liability.
441
+
442
+ END OF TERMS AND CONDITIONS
443
+
444
+ APPENDIX: How to apply the Apache License to your work.
445
+
446
+ To apply the Apache License to your work, attach the following
447
+ boilerplate notice, with the fields enclosed by brackets "[]"
448
+ replaced with your own identifying information. (Don't include
449
+ the brackets!) The text should be enclosed in the appropriate
450
+ comment syntax for the file format. We also recommend that a
451
+ file or class name and description of purpose be included on the
452
+ same "printed page" as the copyright notice for easier
453
+ identification within third-party archives.
454
+
455
+ Copyright 2019 Siara Logics (cc)
456
+
457
+ Licensed under the Apache License, Version 2.0 (the "License");
458
+ you may not use this file except in compliance with the License.
459
+ You may obtain a copy of the License at
460
+
461
+ http://www.apache.org/licenses/LICENSE-2.0
462
+
463
+ Unless required by applicable law or agreed to in writing, software
464
+ distributed under the License is distributed on an "AS IS" BASIS,
465
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
466
+ See the License for the specific language governing permissions and
467
+ limitations under the License.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Meshtastic
4
- VERSION = '0.0.184'
4
+ VERSION = '0.0.186'
5
5
  end
data/lib/meshtastic.rb CHANGED
@@ -44,10 +44,12 @@ module Meshtastic
44
44
  autoload :Clientonly, 'meshtastic/clientonly'
45
45
  autoload :ConnectionStatus, 'meshtastic/connection_status'
46
46
  autoload :Deviceonly, 'meshtastic/deviceonly'
47
+ autoload :Forwarder, 'meshtastic/forwarder'
47
48
  autoload :Localonly, 'meshtastic/localonly'
48
49
  autoload :MeshInterface, 'meshtastic/mesh_interface'
49
50
  autoload :MQTT, 'meshtastic/mqtt'
50
51
  autoload :Portnums, 'meshtastic/portnums'
52
+ autoload :PayloadFormats, 'meshtastic/payload_formats'
51
53
  autoload :RemoteHardware, 'meshtastic/remote_hardware'
52
54
  autoload :RTTTL, 'meshtastic/rtttl'
53
55
  autoload :Serial, 'meshtastic/serial'
@@ -56,6 +58,7 @@ module Meshtastic
56
58
  autoload :TCP, 'meshtastic/tcp'
57
59
  autoload :Traceroute, 'meshtastic/traceroute'
58
60
  autoload :Util, 'meshtastic/util'
61
+ autoload :Unishox2, 'meshtastic/unishox2'
59
62
  autoload :Xmodem, 'meshtastic/xmodem'
60
63
 
61
64
  # Constants
@@ -110,4 +113,5 @@ require 'meshtastic/config'
110
113
  require 'meshtastic/module_config'
111
114
  require 'meshtastic/paxcount'
112
115
  require 'meshtastic/position'
116
+ require 'meshtastic/reticulum'
113
117
  require 'meshtastic/telemetry'
data/meshtastic.gemspec CHANGED
@@ -23,6 +23,8 @@ Gem::Specification.new do |spec|
23
23
  # Protobuf regeneration can introduce dependencies before Git tracks them.
24
24
  # Package Ruby sources and their specs independently of staging state.
25
25
  spec.files |= Dir.glob('{lib,spec}/**/*.rb', base: __dir__)
26
+ # Independent encoder vectors are required by the packaged transport specs.
27
+ spec.files |= Dir.glob('spec/support/**/*.json', base: __dir__)
26
28
  spec.executables = spec.files.grep(%r{^bin/}) do |f|
27
29
  File.basename(f)
28
30
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require_relative '../../support/tak_codec_fixtures'
5
+ require_relative '../../support/unishox_fixtures'
4
6
 
5
7
  describe Meshtastic::ATAK do
6
8
  def fake_serial_obj
@@ -146,6 +148,54 @@ describe Meshtastic::ATAK do
146
148
  packet = decode_to_radio(serial_obj)
147
149
  expect(packet.decoded.portnum).to eq(:ATAK_PLUGIN)
148
150
  end
151
+ end
152
+
153
+ describe Meshtastic::ATAK do
154
+ it 'handles official SDK malformed cases and reserved flag bits' do
155
+ TAK_MALFORMED_FIXTURES.each do |name, hex|
156
+ if name == 'reserved_bits_set'
157
+ expect(described_class.decode_v2(payload: [hex].pack('H*'))).to be_a(Meshtastic::TAKPacketV2)
158
+ else
159
+ expect { described_class.decode_v2(payload: [hex].pack('H*')) }.to raise_error(StandardError), name
160
+ end
161
+ end
162
+ end
163
+
164
+ it 'rejects malformed V1 framing and excessive input before protobuf parsing' do
165
+ ["\x08\x01\x12\x80", "\x08\x01\x00", "\x08\x01\x12\x20", 'a' * 4097].each do |wire|
166
+ expect { described_class.decode(portnum: :ATAK_PLUGIN, payload: wire.b) }.to raise_error(ArgumentError)
167
+ end
168
+ end
169
+
170
+ it 'preserves V1 raw detail and accepts empty proto3 packets' do
171
+ wire = Meshtastic::TAKPacket.new(is_compressed: true, detail: "\xff\x00".b).to_proto
172
+ decoded = described_class.decode(portnum: :ATAK_PLUGIN, payload: wire)
173
+ expect(decoded.detail).to eq("\xff\x00".b)
174
+ expect(decoded.is_compressed).to be(false)
175
+ expect(described_class.decode(portnum: :ATAK_PLUGIN, payload: '').to_h).to eq({})
176
+ end
177
+
178
+ it 'decodes SDK compressed V2 golden protobufs' do
179
+ TAK_CODEC_FIXTURES.each do |name, wire, proto|
180
+ expect(described_class.decode_v2(payload: [wire].pack('H*')).to_h)
181
+ .to eq(Meshtastic::TAKPacketV2.decode([proto].pack('H*')).to_h), name
182
+ end
183
+ end
184
+
185
+ it 'decompresses V1 binary string fields before UTF-8 protobuf parsing' do
186
+ field = ->(tag, data) { [(tag * 8) + 2, data.bytesize].pack('C*') + data }
187
+ compressed = ->(text) { [UNISHOX_FIXTURES.assoc(text).last].pack('H*') }
188
+ contact = field.call(1, compressed.call('ALPHA')) + field.call(2, compressed.call('RADIO-1'))
189
+ chat = field.call(1, compressed.call('ATAK chat')) + field.call(2, compressed.call('ANDROID-aabbccdd')) + field.call(3, compressed.call('ALPHA'))
190
+ wire = "\x08\x01".b + field.call(2, contact) + field.call(6, chat)
191
+ packet = described_class.decode(portnum: :ATAK_PLUGIN, payload: wire)
192
+ expect(packet.is_compressed).to be(false)
193
+ expect(packet.contact.callsign).to eq('ALPHA')
194
+ expect(packet.contact.device_callsign).to eq('RADIO-1')
195
+ expect(packet.chat.message).to eq('ATAK chat')
196
+ expect(packet.chat.to).to eq('ANDROID-aabbccdd')
197
+ expect(packet.chat.to_callsign).to eq('ALPHA')
198
+ end
149
199
 
150
200
  it 'prints usage without raising' do
151
201
  expect { described_class.help }.to output(/ATAK_PLUGIN_V2/).to_stdout
@@ -1,8 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require_relative '../../support/payload_fixtures'
4
5
 
5
6
  describe Meshtastic::Bluetooth do
7
+ include PayloadFixtures
8
+
9
+ it 'decodes every application fixture received from GATT including malformed and default messages' do
10
+ handle = connect
11
+ payload_cases.each do |port, bytes, expected|
12
+ connection.incoming << payload_radio(port, bytes).to_proto
13
+ received = Timeout.timeout(2) do
14
+ described_class.subscribe(bluetooth_obj: handle) { |message| break message }
15
+ end
16
+ expect(received.dig(:packet, :decoded, :payload)).to eq(expected), "port #{port} bytes #{bytes.inspect}"
17
+ end
18
+ end
19
+
6
20
  let(:connection) do
7
21
  Class.new do
8
22
  attr_reader :writes, :incoming
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'meshtastic/forwarder_pb'
5
+
6
+ RSpec.describe Meshtastic::ForwarderProtobuf::CotEvent do
7
+ it 'loads the complete pinned upstream descriptor graph' do
8
+ expect(described_class.decode("\x0a\x04test".b).uid).to eq('test')
9
+ expect(described_class.descriptor.lookup('detail').subtype.count).to eq(22)
10
+ end
11
+ end