meshtastic 0.0.179 → 0.0.180

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/README.md +3 -2
  3. data/documentation/admin-channel.md +33 -0
  4. data/documentation/admin-config.md +28 -0
  5. data/documentation/admin-firmware.md +69 -0
  6. data/documentation/admin.md +2 -1
  7. data/documentation/atak.md +93 -10
  8. data/documentation/channel.md +1 -28
  9. data/documentation/config.md +1 -27
  10. data/documentation/meshtastic.md +2 -2
  11. data/lib/meshtastic/admin/channel.rb +90 -0
  12. data/lib/meshtastic/admin/config.rb +154 -0
  13. data/lib/meshtastic/admin/firmware.rb +173 -0
  14. data/lib/meshtastic/admin.rb +458 -28
  15. data/lib/meshtastic/apponly.rb +19 -7
  16. data/lib/meshtastic/atak.rb +295 -17
  17. data/lib/meshtastic/bluetooth/bluez.rb +20 -1
  18. data/lib/meshtastic/bluetooth.rb +70 -59
  19. data/lib/meshtastic/cannedmessages.rb +13 -6
  20. data/lib/meshtastic/channel.rb +3 -25
  21. data/lib/meshtastic/clientonly.rb +19 -7
  22. data/lib/meshtastic/config.rb +3 -24
  23. data/lib/meshtastic/connection_status.rb +12 -5
  24. data/lib/meshtastic/deviceonly.rb +19 -7
  25. data/lib/meshtastic/localonly.rb +19 -7
  26. data/lib/meshtastic/mesh_interface.rb +11 -0
  27. data/lib/meshtastic/module_config.rb +13 -7
  28. data/lib/meshtastic/mqtt.rb +72 -35
  29. data/lib/meshtastic/paxcount.rb +17 -6
  30. data/lib/meshtastic/portnums.rb +13 -6
  31. data/lib/meshtastic/position.rb +16 -6
  32. data/lib/meshtastic/remote_hardware.rb +27 -11
  33. data/lib/meshtastic/rtttl.rb +17 -8
  34. data/lib/meshtastic/serial.rb +81 -70
  35. data/lib/meshtastic/storeforward.rb +13 -6
  36. data/lib/meshtastic/stream_interface.rb +11 -0
  37. data/lib/meshtastic/tcp.rb +73 -22
  38. data/lib/meshtastic/telemetry.rb +15 -6
  39. data/lib/meshtastic/traceroute.rb +15 -6
  40. data/lib/meshtastic/util.rb +3 -1
  41. data/lib/meshtastic/version.rb +1 -1
  42. data/lib/meshtastic/xmodem.rb +12 -5
  43. data/lib/meshtastic.rb +22 -3
  44. data/spec/conventions_spec.rb +321 -0
  45. data/spec/lib/meshtastic/admin/channel_spec.rb +45 -0
  46. data/spec/lib/meshtastic/admin/config_spec.rb +48 -0
  47. data/spec/lib/meshtastic/admin/firmware_spec.rb +118 -0
  48. data/spec/lib/meshtastic/admin_spec.rb +40 -0
  49. data/spec/lib/meshtastic/apponly_spec.rb +1 -1
  50. data/spec/lib/meshtastic/atak_spec.rb +129 -4
  51. data/spec/lib/meshtastic/bluetooth_spec.rb +1 -1
  52. data/spec/lib/meshtastic/channel_spec.rb +2 -20
  53. data/spec/lib/meshtastic/config_spec.rb +2 -20
  54. data/spec/lib/meshtastic/connection_status_spec.rb +1 -1
  55. data/spec/lib/meshtastic/deviceonly_spec.rb +1 -1
  56. data/spec/lib/meshtastic/localonly_spec.rb +1 -1
  57. data/spec/lib/meshtastic/portnums_spec.rb +2 -2
  58. metadata +11 -1
@@ -1,37 +1,315 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'meshtastic/atak_pb'
4
+ require 'meshtastic/portnums_pb'
5
+ require 'zlib'
4
6
 
5
7
  module Meshtastic
6
8
  module ATAK
7
- def self.encode(opts = {})
8
- packet = Meshtastic::TAKPacket.new
9
- packet.is_compressed = opts.fetch(:is_compressed, false)
10
- if opts[:chat]
11
- packet.chat = opts[:chat]
12
- elsif opts[:message]
13
- packet.chat = Meshtastic::GeoChat.new(message: opts[:message])
14
- end
9
+ V1_PORT = Meshtastic::PortNum::ATAK_PLUGIN
10
+ V2_PORT = Meshtastic::PortNum::ATAK_PLUGIN_V2
11
+ FORWARDER_PORT = Meshtastic::PortNum::ATAK_FORWARDER
12
+ V2_UNCOMPRESSED = 0xFF
13
+ COORD_SCALE = 10_000_000
14
+ SKIP_KEYS = %i[
15
+ serial_obj bluetooth_obj tcp_obj mqtt_obj to from channel want_ack hop_limit
16
+ port_num data want_response via psks message extra_skip packet
17
+ ].freeze
18
+
19
+ public_class_method def self.encode(opts = {})
20
+ encode_v1(opts.merge({}))
21
+ end
22
+
23
+ public_class_method def self.encode_v1(opts = {})
24
+ packet = opts[:packet] || Meshtastic::TAKPacket.new
25
+ packet.is_compressed = opts.fetch(:is_compressed, packet.is_compressed)
26
+ packet.contact = contact_from(opts) if contact?(opts)
27
+ packet.group = group_from(opts) if group?(opts)
28
+ packet.status = status_from(opts) if status?(opts)
29
+ packet.pli = pli_from(opts) if pli?(opts)
30
+ packet.chat = chat_from(opts) if chat?(opts)
31
+ packet.detail = opts[:detail] if opts[:detail]
15
32
  packet
16
33
  end
17
34
 
18
- def self.send(opts = {})
19
- data = Meshtastic::Data.new(
20
- portnum: :ATAK_PLUGIN,
21
- payload: encode(opts).to_proto
22
- )
23
- Meshtastic.deliver_data(opts.merge(data: data, port_num: Meshtastic::PortNum::ATAK_PLUGIN))
35
+ public_class_method def self.build_v2(opts = {})
36
+ packet = opts[:packet] || Meshtastic::TAKPacketV2.new
37
+ assign_fields(opts.merge(message: packet, extra_skip: %i[packet lat lon altitude message to]))
38
+ packet.latitude_i = scale_coord(value: opts[:lat]) if opts[:lat]
39
+ packet.longitude_i = scale_coord(value: opts[:lon]) if opts[:lon]
40
+ packet.altitude = opts[:altitude].to_i if opts[:altitude]
41
+ packet.chat = chat_from(opts) if chat?(opts) && opts[:chat].nil?
42
+ packet
43
+ end
44
+
45
+ public_class_method def self.wrap_v2(opts = {})
46
+ packet = opts[:packet]
47
+ [V2_UNCOMPRESSED].pack('C') + packet.to_proto
48
+ end
49
+
50
+ public_class_method def self.encode_v2(opts = {})
51
+ wrap_v2(packet: build_v2(opts))
52
+ end
53
+
54
+ public_class_method def self.decode_v2(opts = {})
55
+ bytes = (opts[:wire] || opts[:payload]).to_s.b
56
+ raise ArgumentError, 'empty ATAK V2 payload' if bytes.empty?
57
+
58
+ flags = bytes.getbyte(0)
59
+ body = bytes.byteslice(1..)
60
+ if flags == V2_UNCOMPRESSED
61
+ Meshtastic::TAKPacketV2.decode(body)
62
+ else
63
+ raise ArgumentError,
64
+ "compressed ATAK V2 dictionary id #{flags & 0x3F} is not unpacked (flags=0x#{flags.to_s(16)})"
65
+ end
66
+ end
67
+
68
+ public_class_method def self.compress_cot(opts = {})
69
+ Zlib::Deflate.deflate(opts[:cot].to_s)
70
+ end
71
+
72
+ public_class_method def self.decompress_cot(opts = {})
73
+ bytes = opts[:payload].to_s.b
74
+ bytes = bytes.byteslice(1..) if bytes.getbyte(0).zero? && bytes.bytesize > 1
75
+ Zlib::Inflate.inflate(bytes)
76
+ end
77
+
78
+ public_class_method def self.decode(opts = {})
79
+ payload = opts[:payload]
80
+ portnum = normalize_port(portnum: opts[:portnum])
81
+ case portnum
82
+ when V1_PORT, :ATAK_PLUGIN
83
+ Meshtastic::TAKPacket.decode(payload.to_s.b)
84
+ when V2_PORT, :ATAK_PLUGIN_V2
85
+ decode_v2(payload: payload)
86
+ when FORWARDER_PORT, :ATAK_FORWARDER
87
+ decompress_cot(payload: payload)
88
+ else
89
+ raise ArgumentError, "unsupported TAK portnum: #{opts[:portnum].inspect}"
90
+ end
91
+ end
92
+
93
+ public_class_method def self.send(opts = {})
94
+ send_v1(opts.merge({}))
95
+ end
96
+
97
+ public_class_method def self.send_v1(opts = {})
98
+ deliver(opts.merge(port_num: V1_PORT, payload: encode_v1(opts).to_proto))
99
+ end
100
+
101
+ public_class_method def self.send_chat(opts = {})
102
+ send_v1(opts.merge({}))
103
+ end
104
+
105
+ public_class_method def self.send_pli(opts = {})
106
+ send_v1(opts.merge({}))
107
+ end
108
+
109
+ public_class_method def self.send_v2(opts = {})
110
+ deliver(opts.merge(port_num: V2_PORT, payload: encode_v2(opts)))
111
+ end
112
+
113
+ public_class_method def self.send_cot(opts = {})
114
+ payload = compress_cot(cot: opts[:cot])
115
+ max_len = Meshtastic::Constants::DATA_PAYLOAD_LEN
116
+ raise ArgumentError, "ERROR: CoT Length > #{max_len} Bytes after zlib" if payload.bytesize > max_len
117
+
118
+ deliver(opts.merge(port_num: FORWARDER_PORT, payload: payload))
24
119
  end
25
120
 
26
- def self.authors
121
+ public_class_method def self.authors
27
122
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
28
123
  end
29
124
 
30
- def self.help
125
+ public_class_method def self.help
31
126
  puts "USAGE:
32
- #{self}.send(serial_obj: serial_obj, message: 'ATAK chat')
127
+ # Encode a V1 TAKPacket (alias of encode_v1).
128
+ #{self}.encode(
129
+ message: 'optional - GeoChat text body to place on the packet',
130
+ packet: 'optional - existing Meshtastic::TAKPacket to fill instead of a new one'
131
+ )
132
+
133
+ # Build a V1 TAKPacket with GeoChat, PLI, contact, group, or status.
134
+ #{self}.encode_v1(
135
+ packet: 'optional - existing Meshtastic::TAKPacket to fill instead of a new one',
136
+ detail: 'optional - raw CoT detail bytes for the V1 detail field'
137
+ )
138
+
139
+ # Build a V2 TAKPacketV2 with scaled lat/lon and typed payloads.
140
+ #{self}.build_v2(
141
+ packet: 'optional - existing Meshtastic::TAKPacketV2 to fill instead of a new one',
142
+ chat: 'optional - Meshtastic::GeoChat object if not using message:',
143
+ lat: 'optional - latitude in decimal degrees (stored times 1e7)',
144
+ lon: 'optional - longitude in decimal degrees (stored times 1e7)',
145
+ altitude: 'optional - altitude in meters as an integer'
146
+ )
147
+
148
+ # Prefix a TAKPacketV2 protobuf with the uncompressed V2 flags byte.
149
+ #{self}.wrap_v2(
150
+ packet: 'required - Meshtastic::TAKPacketV2 to serialize onto the wire'
151
+ )
152
+
153
+ # Build and wrap an uncompressed V2 wire frame.
154
+ #{self}.encode_v2(
155
+ message: 'optional - GeoChat text for the V2 chat variant'
156
+ )
157
+
158
+ # Decode an uncompressed V2 wire frame (flags 0xFF plus protobuf).
159
+ #{self}.decode_v2(
160
+ wire: 'optional - full V2 wire bytes including the flags byte',
161
+ payload: 'optional - same as wire when wire is omitted'
162
+ )
163
+
164
+ # zlib-compress Cursor-on-Target XML for ATAK_FORWARDER.
165
+ #{self}.compress_cot(
166
+ cot: 'required - CoT XML string to deflate for the forwarder port'
167
+ )
168
+
169
+ # zlib-decompress ATAK_FORWARDER payload bytes back to CoT XML.
170
+ #{self}.decompress_cot(
171
+ payload: 'required - zlib bytes from an ATAK_FORWARDER Data payload'
172
+ )
173
+
174
+ # Dispatch decode by Meshtastic TAK portnum.
175
+ #{self}.decode(
176
+ payload: 'required - raw port payload bytes from a decoded mesh Data',
177
+ portnum: 'required - :ATAK_PLUGIN, :ATAK_PLUGIN_V2, or :ATAK_FORWARDER'
178
+ )
179
+
180
+ # Send a V1 TAKPacket (alias of send_v1).
181
+ #{self}.send(
182
+ serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
183
+ message: 'optional - GeoChat text to send on ATAK_PLUGIN'
184
+ )
185
+
186
+ # Send a V1 TAKPacket on ATAK_PLUGIN (port 72).
187
+ #{self}.send_v1(
188
+ serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
189
+ bluetooth_obj: 'optional - BLE handle from Meshtastic::Bluetooth.connect',
190
+ tcp_obj: 'optional - TCP handle from Meshtastic::TCP.connect'
191
+ )
192
+
193
+ # Send V1 GeoChat on ATAK_PLUGIN.
194
+ #{self}.send_chat(
195
+ serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
196
+ message: 'optional - GeoChat text body to transmit'
197
+ )
198
+
199
+ # Send V1 PLI on ATAK_PLUGIN.
200
+ #{self}.send_pli(
201
+ serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
202
+ lat: 'optional - latitude in decimal degrees (stored times 1e7)',
203
+ lon: 'optional - longitude in decimal degrees (stored times 1e7)'
204
+ )
205
+
206
+ # Send an uncompressed V2 frame on ATAK_PLUGIN_V2 (port 78).
207
+ #{self}.send_v2(
208
+ serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
209
+ message: 'optional - GeoChat text for the V2 chat variant'
210
+ )
211
+
212
+ # Send zlib-compressed CoT XML on ATAK_FORWARDER (port 257).
213
+ #{self}.send_cot(
214
+ serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
215
+ cot: 'required - CoT XML string small enough to fit after zlib'
216
+ )
217
+
218
+ # Print the AUTHOR(S) string for this module.
33
219
  #{self}.authors
34
220
  "
35
221
  end
222
+
223
+ private_class_method def self.deliver(opts = {})
224
+ data = Meshtastic::Data.new(portnum: opts[:port_num], payload: opts[:payload])
225
+ Meshtastic.deliver_data(opts.merge(data: data, port_num: opts[:port_num]))
226
+ end
227
+
228
+ private_class_method def self.scale_coord(opts = {})
229
+ (opts[:value].to_f * COORD_SCALE).round
230
+ end
231
+
232
+ private_class_method def self.normalize_port(opts = {})
233
+ portnum = opts[:portnum]
234
+ return portnum if portnum.is_a?(Integer)
235
+ return Meshtastic::PortNum.resolve(portnum) if portnum.is_a?(Symbol)
236
+
237
+ portnum
238
+ end
239
+
240
+ private_class_method def self.assign_fields(opts = {})
241
+ message = opts[:message]
242
+ skip = SKIP_KEYS + Array(opts[:extra_skip])
243
+ opts.each do |key, value|
244
+ next if skip.include?(key)
245
+ next unless message.respond_to?("#{key}=")
246
+
247
+ message.public_send("#{key}=", value)
248
+ end
249
+ message
250
+ end
251
+
252
+ private_class_method def self.contact?(opts = {})
253
+ opts[:contact] || opts[:callsign] || opts[:device_callsign]
254
+ end
255
+
256
+ private_class_method def self.group?(opts = {})
257
+ opts[:group] || opts[:team] || opts[:role]
258
+ end
259
+
260
+ private_class_method def self.status?(opts = {})
261
+ opts[:status] || opts[:battery]
262
+ end
263
+
264
+ private_class_method def self.pli?(opts = {})
265
+ opts[:pli] || opts[:lat] || opts[:lon]
266
+ end
267
+
268
+ private_class_method def self.chat?(opts = {})
269
+ opts[:chat] || opts[:message]
270
+ end
271
+
272
+ private_class_method def self.contact_from(opts = {})
273
+ return opts[:contact] if opts[:contact].is_a?(Meshtastic::Contact)
274
+
275
+ Meshtastic::Contact.new(
276
+ callsign: opts[:callsign].to_s,
277
+ device_callsign: opts[:device_callsign].to_s
278
+ )
279
+ end
280
+
281
+ private_class_method def self.group_from(opts = {})
282
+ return opts[:group] if opts[:group].is_a?(Meshtastic::Group)
283
+
284
+ Meshtastic::Group.new(team: opts[:team], role: opts[:role])
285
+ end
286
+
287
+ private_class_method def self.status_from(opts = {})
288
+ return opts[:status] if opts[:status].is_a?(Meshtastic::Status)
289
+
290
+ Meshtastic::Status.new(battery: opts[:battery].to_i)
291
+ end
292
+
293
+ private_class_method def self.pli_from(opts = {})
294
+ return opts[:pli] if opts[:pli].is_a?(Meshtastic::PLI)
295
+
296
+ pli = Meshtastic::PLI.new
297
+ pli.latitude_i = scale_coord(value: opts[:lat]) if opts[:lat]
298
+ pli.longitude_i = scale_coord(value: opts[:lon]) if opts[:lon]
299
+ pli.altitude = opts[:altitude].to_i if opts[:altitude]
300
+ pli.speed = opts[:speed].to_i if opts[:speed]
301
+ pli.course = opts[:course].to_i if opts[:course]
302
+ pli
303
+ end
304
+
305
+ private_class_method def self.chat_from(opts = {})
306
+ return opts[:chat] if opts[:chat].is_a?(Meshtastic::GeoChat)
307
+
308
+ Meshtastic::GeoChat.new(
309
+ message: opts[:message].to_s,
310
+ to: opts[:to].to_s,
311
+ to_callsign: opts[:to_callsign].to_s
312
+ )
313
+ end
36
314
  end
37
315
  end
@@ -12,7 +12,9 @@ module Meshtastic
12
12
  TORADIO_UUID = 'f75c76d2-129e-4dad-a1dd-7866124401e7'
13
13
  FROMRADIO_UUID = '2c55e69e-4993-11ed-b878-0242ac120002'
14
14
 
15
- def self.scan(adapter: 'hci0', timeout: 5)
15
+ public_class_method def self.scan(opts = {})
16
+ adapter = opts[:adapter] || 'hci0'
17
+ timeout = opts[:timeout] || 5
16
18
  new(address: '00:00:00:00:00:00', adapter: adapter, timeout: timeout).scan_adapter
17
19
  end
18
20
 
@@ -182,6 +184,23 @@ module Meshtastic
182
184
  @timeout = timeout
183
185
  @mutex = Mutex.new
184
186
  end
187
+
188
+ public_class_method def self.authors
189
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
190
+ end
191
+
192
+ public_class_method def self.help
193
+ puts "USAGE:
194
+ # Scan BlueZ for nearby Meshtastic BLE advertisements.
195
+ #{self}.scan(
196
+ adapter: 'optional - BlueZ adapter name such as hci0',
197
+ timeout: 'optional - discovery duration in seconds (default: 5)'
198
+ )
199
+
200
+ # Print the AUTHOR(S) string for this module.
201
+ #{self}.authors
202
+ "
203
+ end
185
204
  end
186
205
  end
187
206
  end
@@ -8,12 +8,12 @@ module Meshtastic
8
8
  module Bluetooth
9
9
  autoload :BlueZ, 'meshtastic/bluetooth/bluez'
10
10
 
11
- def self.scan(opts = {})
12
- BlueZ.scan(adapter: opts.fetch(:adapter, 'hci0'), timeout: opts.fetch(:timeout, 5))
11
+ public_class_method def self.scan(opts = {})
12
+ BlueZ.scan(opts.merge({}))
13
13
  end
14
14
 
15
15
  # Connect to an already paired BLE address (not a mesh node ID).
16
- def self.connect(opts = {})
16
+ public_class_method def self.connect(opts = {})
17
17
  connection = BlueZ.new(address: opts[:address], adapter: opts.fetch(:adapter, 'hci0'), timeout: opts.fetch(:timeout, 15))
18
18
  connection.connect
19
19
  bluetooth_obj = {
@@ -21,7 +21,7 @@ module Meshtastic
21
21
  rx_mutex: Mutex.new, from_radio_queue: Queue.new, config_queue: Queue.new,
22
22
  proto_data: [], console_data: []
23
23
  }
24
- bluetooth_obj[:rx_thread] = start_reader(bluetooth_obj)
24
+ bluetooth_obj[:rx_thread] = start_reader(handle: bluetooth_obj)
25
25
  if opts.fetch(:want_config, true)
26
26
  mesh = Meshtastic::MeshInterface.new
27
27
  bytes = mesh.start_config
@@ -35,7 +35,8 @@ module Meshtastic
35
35
  raise
36
36
  end
37
37
 
38
- private_class_method def self.start_reader(handle)
38
+ private_class_method def self.start_reader(opts = {})
39
+ handle = opts[:handle]
39
40
  Thread.new do
40
41
  until handle[:closing]
41
42
  bytes = handle[:bluetooth_conn].read
@@ -43,7 +44,7 @@ module Meshtastic
43
44
  sleep 0.1
44
45
  next
45
46
  end
46
- receive_bytes(handle, bytes)
47
+ receive_bytes(handle: handle, bytes: bytes)
47
48
  end
48
49
  rescue StandardError => e
49
50
  handle[:rx_error] = IOError.new("Bluetooth receive failed: #{e.message}") unless handle[:closing]
@@ -53,7 +54,9 @@ module Meshtastic
53
54
  end
54
55
  end
55
56
 
56
- private_class_method def self.receive_bytes(handle, bytes)
57
+ private_class_method def self.receive_bytes(opts = {})
58
+ handle = opts[:handle]
59
+ bytes = opts[:bytes]
57
60
  message = Meshtastic::FromRadio.decode(bytes)
58
61
  if message.my_info
59
62
  handle[:my_info] = message.my_info.to_h
@@ -73,14 +76,14 @@ module Meshtastic
73
76
  warn "Meshtastic::Bluetooth: failed to decode FromRadio (#{e.message})"
74
77
  end
75
78
 
76
- private_class_method def self.handle_for(opts)
79
+ private_class_method def self.handle_for(opts = {})
77
80
  handle = opts[:bluetooth_obj] || @last_bluetooth_obj
78
81
  raise ArgumentError, 'bluetooth_obj is required; call connect first' unless handle
79
82
 
80
83
  handle
81
84
  end
82
85
 
83
- def self.wait_for_config(opts = {})
86
+ public_class_method def self.wait_for_config(opts = {})
84
87
  handle = handle_for(opts)
85
88
  raise ArgumentError, 'connect with want_config: true first' unless handle[:config_id]
86
89
 
@@ -92,7 +95,7 @@ module Meshtastic
92
95
  handle
93
96
  end
94
97
 
95
- def self.recv_from_radio(opts = {})
98
+ public_class_method def self.recv_from_radio(opts = {})
96
99
  handle = handle_for(opts)
97
100
  timeout = opts.fetch(:timeout, 5)
98
101
  timeout = nil if timeout&.negative?
@@ -102,7 +105,7 @@ module Meshtastic
102
105
  message
103
106
  end
104
107
 
105
- def self.drain_from_radio(opts = {})
108
+ public_class_method def self.drain_from_radio(opts = {})
106
109
  handle = handle_for(opts)
107
110
  messages = []
108
111
  opts.fetch(:max, 256).times do
@@ -114,16 +117,21 @@ module Meshtastic
114
117
  messages
115
118
  end
116
119
 
117
- def self.dump_stdout_data(opts = {}, &)
118
- Meshtastic::Serial.dump_stdout_data(opts.merge(serial_obj: handle_for(opts)), &)
120
+ public_class_method def self.dump_stdout_data(opts = {})
121
+ merged = opts.merge(serial_obj: handle_for(opts))
122
+ if block_given?
123
+ Meshtastic::Serial.dump_stdout_data(merged) { |row| yield row } # rubocop:disable Style/ExplicitBlockArgument
124
+ else
125
+ Meshtastic::Serial.dump_stdout_data(merged)
126
+ end
119
127
  end
120
128
 
121
- def self.flush_data(opts = {})
129
+ public_class_method def self.flush_data(opts = {})
122
130
  Meshtastic::Serial.flush_data(opts.merge(serial_obj: handle_for(opts)))
123
131
  end
124
132
 
125
133
  # Yield the same enriched FromRadio hashes as Serial, without opening a UART.
126
- def self.subscribe(opts = {})
134
+ public_class_method def self.subscribe(opts = {})
127
135
  handle = handle_for(opts)
128
136
  psks = opts.fetch(:psks, { LongFast: 'AQ==' }).dup
129
137
  raise ArgumentError, 'psks must be a hash' unless psks.is_a?(Hash)
@@ -166,7 +174,7 @@ module Meshtastic
166
174
  end
167
175
 
168
176
  # Write one complete serialized ToRadio to GATT. BLE does not use UART headers.
169
- def self.send_to_radio(opts = {})
177
+ public_class_method def self.send_to_radio(opts = {})
170
178
  handle = opts[:bluetooth_obj]
171
179
  raise ArgumentError, 'bluetooth_obj is required' unless handle
172
180
  raise IOError, 'Bluetooth connection closed' if handle[:closing]
@@ -187,7 +195,7 @@ module Meshtastic
187
195
  end
188
196
  end
189
197
 
190
- def self.send_text(opts = {})
198
+ public_class_method def self.send_text(opts = {})
191
199
  handle = opts[:bluetooth_obj]
192
200
  raise ArgumentError, 'bluetooth_obj is required' unless handle
193
201
 
@@ -199,7 +207,7 @@ module Meshtastic
199
207
  send_to_radio(bluetooth_obj: handle, to_radio: Meshtastic::MeshInterface.new.send_text(args))
200
208
  end
201
209
 
202
- def self.send_data(opts = {})
210
+ public_class_method def self.send_data(opts = {})
203
211
  handle = opts[:bluetooth_obj]
204
212
  raise ArgumentError, 'bluetooth_obj is required' unless handle
205
213
  raise ArgumentError, 'data must be Meshtastic::Data' unless opts[:data].is_a?(Meshtastic::Data)
@@ -208,7 +216,7 @@ module Meshtastic
208
216
  send_to_radio(bluetooth_obj: handle, to_radio: Meshtastic::MeshInterface.new.send_data(args))
209
217
  end
210
218
 
211
- def self.disconnect(opts = {})
219
+ public_class_method def self.disconnect(opts = {})
212
220
  handle = opts[:bluetooth_obj]
213
221
  return unless handle
214
222
  return if handle[:closing]
@@ -231,68 +239,71 @@ module Meshtastic
231
239
  nil
232
240
  end
233
241
 
234
- def self.authors
242
+ public_class_method def self.authors
235
243
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
236
244
  end
237
245
 
238
- def self.help
239
- puts "Send and receive Meshtastic messages over Bluetooth Low Energy (Linux BlueZ).
240
- BLE writes unframed ToRadio protobufs (no UART START1/START2 header). Pair first with bluetoothctl.
246
+ public_class_method def self.help
247
+ puts " USAGE:
248
+ # Run the scan class method for this module.
249
+ #{self}.scan
241
250
 
242
- USAGE:
243
- devices = #{self}.scan(
244
- adapter: 'optional - BlueZ adapter (default: hci0)',
245
- timeout: 'optional - discovery seconds (default: 5)'
251
+ # Run the connect class method for this module.
252
+ #{self}.connect(
253
+ address: 'optional - value for address passed into connect'
246
254
  )
247
255
 
248
- bluetooth_obj = #{self}.connect(
249
- address: 'required - BLE address (AA:BB:CC:DD:EE:FF), not a mesh node ID',
250
- adapter: 'optional - BlueZ adapter (default: hci0)',
251
- timeout: 'optional - D-Bus/connect seconds (default: 15)',
252
- want_config: 'optional - request full node DB after connect (default: true)'
253
- )
256
+ # Run the wait_for_config class method for this module.
257
+ #{self}.wait_for_config
254
258
 
255
- #{self}.wait_for_config(
256
- bluetooth_obj: 'required - bluetooth_obj connected with want_config: true',
257
- timeout: 'optional - seconds to await configuration (default: 10)'
258
- )
259
+ # Run the recv_from_radio class method for this module.
260
+ #{self}.recv_from_radio
259
261
 
260
- #{self}.send_to_radio(
261
- bluetooth_obj: 'required - bluetooth_obj returned from #connect',
262
- to_radio: 'required - Meshtastic::ToRadio OR serialized String'
263
- )
262
+ # Run the drain_from_radio class method for this module.
263
+ #{self}.drain_from_radio
264
264
 
265
- from_radio = #{self}.recv_from_radio(
266
- bluetooth_obj: 'optional - bluetooth_obj (default: most recently opened connection)',
267
- timeout: 'optional - seconds (default: 5; 0 = poll; nil = block forever)'
268
- )
265
+ # Run the dump_stdout_data class method for this module.
266
+ #{self}.dump_stdout_data
269
267
 
270
- msgs = #{self}.drain_from_radio(bluetooth_obj: bluetooth_obj, max: 256)
268
+ # Run the flush_data class method for this module.
269
+ #{self}.flush_data
271
270
 
271
+ # Run the subscribe class method for this module.
272
272
  #{self}.subscribe(
273
- bluetooth_obj: 'required - bluetooth_obj returned from #connect',
274
- include: 'optional - comma-delimited string(s) to include',
275
- exclude: 'optional - comma-delimited string(s) to exclude'
273
+ include: 'optional - value for include passed into subscribe',
274
+ exclude: 'optional - value for exclude passed into subscribe',
275
+ timeout: 'optional - value for timeout passed into subscribe',
276
+ gps_metadata: 'optional - value for gps_metadata passed into subscribe',
277
+ include_raw: 'optional - value for include_raw passed into subscribe'
276
278
  )
277
279
 
280
+ # Run the send_to_radio class method for this module.
281
+ #{self}.send_to_radio(
282
+ bluetooth_obj: 'optional - value for bluetooth_obj passed into send_to_radio',
283
+ to_radio: 'optional - value for to_radio passed into send_to_radio'
284
+ )
285
+
286
+ # Run the send_text class method for this module.
278
287
  #{self}.send_text(
279
- bluetooth_obj: 'required - bluetooth_obj returned from #connect',
280
- to: 'optional - Destination ID (Default: \"!ffffffff\")',
281
- channel: 'optional - channel index (Default: 0)',
282
- text: 'optional - Text Message (Default: SYN)',
283
- want_ack: 'optional - Want Acknowledgement (Default: false)'
288
+ bluetooth_obj: 'optional - value for bluetooth_obj passed into send_text',
289
+ from: 'optional - value for from passed into send_text'
284
290
  )
285
291
 
292
+ # Run the send_data class method for this module.
286
293
  #{self}.send_data(
287
- bluetooth_obj: 'required - bluetooth_obj returned from #connect',
288
- data: 'required - Meshtastic::Data',
289
- to: 'optional - Destination ID (Default: \"!ffffffff\")',
290
- channel: 'optional - channel index (Default: 0)'
294
+ bluetooth_obj: 'optional - value for bluetooth_obj passed into send_data',
295
+ data: 'optional - value for data passed into send_data',
296
+ from: 'optional - value for from passed into send_data'
291
297
  )
292
298
 
293
- #{self}.disconnect(bluetooth_obj: bluetooth_obj)
299
+ # Run the disconnect class method for this module.
300
+ #{self}.disconnect(
301
+ bluetooth_obj: 'optional - value for bluetooth_obj passed into disconnect'
302
+ )
294
303
 
304
+ # Run the authors class method for this module.
295
305
  #{self}.authors
306
+
296
307
  "
297
308
  end
298
309
  end
@@ -4,13 +4,13 @@ require 'meshtastic/cannedmessages_pb'
4
4
 
5
5
  module Meshtastic
6
6
  module Cannedmessages
7
- def self.encode(opts = {})
7
+ public_class_method def self.encode(opts = {})
8
8
  config = Meshtastic::CannedMessageModuleConfig.new
9
9
  config.messages = opts.fetch(:messages, '')
10
10
  config
11
11
  end
12
12
 
13
- def self.send(opts = {})
13
+ public_class_method def self.send(opts = {})
14
14
  data = Meshtastic::Data.new(
15
15
  portnum: :TEXT_MESSAGE_APP,
16
16
  payload: encode(opts).messages.to_s.b
@@ -18,14 +18,21 @@ module Meshtastic
18
18
  Meshtastic.deliver_data(opts.merge(data: data, port_num: Meshtastic::PortNum::TEXT_MESSAGE_APP))
19
19
  end
20
20
 
21
- def self.authors
21
+ public_class_method def self.authors
22
22
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
23
23
  end
24
24
 
25
- def self.help
26
- puts "USAGE:
27
- #{self}.encode(messages: \"Yes\\nNo\\nMaybe\")
25
+ public_class_method def self.help
26
+ puts " USAGE:
27
+ # Run the encode class method for this module.
28
+ #{self}.encode
29
+
30
+ # Run the send class method for this module.
31
+ #{self}.send
32
+
33
+ # Run the authors class method for this module.
28
34
  #{self}.authors
35
+
29
36
  "
30
37
  end
31
38
  end