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.
- checksums.yaml +4 -4
- data/documentation/README.md +3 -2
- data/documentation/admin-channel.md +33 -0
- data/documentation/admin-config.md +28 -0
- data/documentation/admin-firmware.md +69 -0
- data/documentation/admin.md +2 -1
- data/documentation/atak.md +93 -10
- data/documentation/channel.md +1 -28
- data/documentation/config.md +1 -27
- data/documentation/meshtastic.md +2 -2
- data/lib/meshtastic/admin/channel.rb +90 -0
- data/lib/meshtastic/admin/config.rb +154 -0
- data/lib/meshtastic/admin/firmware.rb +173 -0
- data/lib/meshtastic/admin.rb +458 -28
- 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/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 +11 -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/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 +45 -0
- data/spec/lib/meshtastic/admin/config_spec.rb +48 -0
- data/spec/lib/meshtastic/admin/firmware_spec.rb +118 -0
- data/spec/lib/meshtastic/admin_spec.rb +40 -0
- 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/portnums_spec.rb +2 -2
- metadata +11 -1
data/lib/meshtastic/atak.rb
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
|
|
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
|
-
#
|
|
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(
|
|
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
|
data/lib/meshtastic/bluetooth.rb
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 "
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
#
|
|
256
|
-
|
|
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
|
-
#
|
|
261
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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: '
|
|
280
|
-
|
|
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: '
|
|
288
|
-
data: '
|
|
289
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|