meshtastic 0.0.169 → 0.0.171
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/README.md +115 -5
- data/README.md.bak.20260709105815 +152 -0
- data/lib/meshtastic/mesh_interface.rb +30 -11
- data/lib/meshtastic/serial_interface.rb +496 -279
- data/lib/meshtastic/version.rb +1 -1
- data/spec/lib/meshtastic/serial_interface_spec.rb +72 -0
- metadata +2 -1
|
@@ -6,81 +6,222 @@ require 'io/wait'
|
|
|
6
6
|
require 'json'
|
|
7
7
|
require 'openssl'
|
|
8
8
|
require 'securerandom'
|
|
9
|
+
require 'timeout'
|
|
9
10
|
require 'uart'
|
|
10
11
|
|
|
11
|
-
# Plugin used to interact with Meshtastic nodes
|
|
12
|
+
# Plugin used to interact with Meshtastic nodes over a serial (UART) link.
|
|
13
|
+
# Wire protocol matches the official Python client:
|
|
14
|
+
# [START1=0x94][START2=0xC3][len_hi][len_lo] + protobuf(ToRadio|FromRadio)
|
|
12
15
|
module Meshtastic
|
|
13
|
-
module SerialInterface
|
|
16
|
+
module SerialInterface # rubocop:disable Metrics/ModuleLength
|
|
14
17
|
@console_data = []
|
|
15
18
|
@proto_data = []
|
|
19
|
+
@from_radio_queue = nil
|
|
20
|
+
@rx_mutex = Mutex.new
|
|
21
|
+
@want_exit = false
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# ---- low-level IO helpers ------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def self.clear_hupcl(block_dev)
|
|
28
|
+
# Prevent device reboot on open by clearing HUPCL (same as Python pyserial path).
|
|
29
|
+
return unless defined?(Termios)
|
|
30
|
+
|
|
31
|
+
File.open(block_dev, File::RDWR | Fcntl::O_NOCTTY | Fcntl::O_NDELAY) do |f|
|
|
32
|
+
attrs = Termios.tcgetattr(f)
|
|
33
|
+
attrs.cflag &= ~Termios::HUPCL if defined?(Termios::HUPCL)
|
|
34
|
+
Termios.tcsetattr(f, Termios::TCSAFLUSH, attrs)
|
|
35
|
+
end
|
|
36
|
+
sleep 0.1
|
|
37
|
+
rescue StandardError
|
|
38
|
+
# Best-effort — uart.open will still work without this.
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
private_class_method :clear_hupcl
|
|
16
42
|
|
|
17
43
|
# Supported Method Parameters::
|
|
18
|
-
# proto_thread =
|
|
19
|
-
# serial_conn: 'required -
|
|
20
|
-
#
|
|
44
|
+
# proto_thread = init_rx_thread(
|
|
45
|
+
# serial_conn: 'required - File returned from UART.open',
|
|
46
|
+
# serial_obj: 'required - serial_obj hash being built'
|
|
21
47
|
# )
|
|
22
|
-
|
|
23
|
-
private_class_method def self.init_stdout_thread(opts = {})
|
|
48
|
+
private_class_method def self.init_rx_thread(opts = {})
|
|
24
49
|
serial_conn = opts[:serial_conn]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
serial_obj = opts[:serial_obj]
|
|
51
|
+
debug_out = opts[:debug_out]
|
|
52
|
+
|
|
53
|
+
@want_exit = false
|
|
54
|
+
@from_radio_queue = Queue.new
|
|
55
|
+
@console_data = []
|
|
56
|
+
@proto_data = []
|
|
28
57
|
|
|
29
|
-
# Spin up a serial_obj console_thread
|
|
30
58
|
Thread.new do
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
Thread.current.abort_on_exception = false
|
|
60
|
+
rx_buf = +''.b
|
|
61
|
+
empty = +''.b
|
|
62
|
+
|
|
63
|
+
until @want_exit
|
|
64
|
+
begin
|
|
65
|
+
chunk = serial_conn.read(1)
|
|
66
|
+
if chunk.nil? || chunk.empty?
|
|
67
|
+
sleep 0.01
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
c = chunk.getbyte(0)
|
|
72
|
+
rx_buf << chunk
|
|
73
|
+
ptr = rx_buf.bytesize - 1
|
|
74
|
+
|
|
75
|
+
if ptr.zero?
|
|
76
|
+
# looking for START1
|
|
77
|
+
unless c == Meshtastic::START1
|
|
78
|
+
rx_buf = empty.dup
|
|
79
|
+
append_console_byte(chunk, debug_out)
|
|
80
|
+
end
|
|
81
|
+
elsif ptr == 1
|
|
82
|
+
# looking for START2
|
|
83
|
+
rx_buf = empty.dup unless c == Meshtastic::START2
|
|
84
|
+
elsif ptr >= (Meshtastic::HEADER_LEN - 1)
|
|
85
|
+
packet_len = (rx_buf.getbyte(2) << 8) + rx_buf.getbyte(3)
|
|
86
|
+
|
|
87
|
+
if ptr == (Meshtastic::HEADER_LEN - 1) && packet_len > Meshtastic::MAX_TO_FROM_RADIO_SIZE
|
|
88
|
+
rx_buf = empty.dup
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if rx_buf.bytesize >= (packet_len + Meshtastic::HEADER_LEN)
|
|
93
|
+
payload = rx_buf.byteslice(Meshtastic::HEADER_LEN, packet_len)
|
|
94
|
+
rx_buf = empty.dup
|
|
95
|
+
handle_from_radio_bytes(payload: payload, serial_obj: serial_obj)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
rescue IOError, Errno::EBADF, Errno::EIO
|
|
99
|
+
break if @want_exit
|
|
100
|
+
|
|
101
|
+
sleep 0.05
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
warn "Meshtastic::SerialInterface RX error: #{e.class}: #{e.message}" unless @want_exit
|
|
104
|
+
sleep 0.05
|
|
105
|
+
end
|
|
42
106
|
end
|
|
43
107
|
end
|
|
44
|
-
rescue StandardError => e
|
|
45
|
-
raise e
|
|
46
108
|
end
|
|
47
109
|
|
|
110
|
+
|
|
111
|
+
private_class_method def self.append_console_byte(chunk, debug_out)
|
|
112
|
+
if debug_out
|
|
113
|
+
begin
|
|
114
|
+
debug_out.write(chunk.force_encoding('UTF-8'))
|
|
115
|
+
rescue StandardError
|
|
116
|
+
debug_out.write('?')
|
|
117
|
+
end
|
|
118
|
+
else
|
|
119
|
+
@rx_mutex.synchronize { @console_data << chunk.force_encoding('UTF-8') }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private_class_method def self.handle_from_radio_bytes(opts = {})
|
|
124
|
+
payload = opts[:payload]
|
|
125
|
+
serial_obj = opts[:serial_obj]
|
|
126
|
+
return if payload.nil? || payload.empty?
|
|
127
|
+
|
|
128
|
+
from_radio = Meshtastic::FromRadio.decode(payload)
|
|
129
|
+
hash = from_radio.to_h
|
|
130
|
+
|
|
131
|
+
@rx_mutex.synchronize { @proto_data << hash }
|
|
132
|
+
@from_radio_queue << from_radio if @from_radio_queue
|
|
133
|
+
|
|
134
|
+
# Cache useful device identity on the serial_obj handle.
|
|
135
|
+
if serial_obj && from_radio.my_info
|
|
136
|
+
serial_obj[:my_info] = from_radio.my_info.to_h
|
|
137
|
+
serial_obj[:my_node_num] = from_radio.my_info.my_node_num
|
|
138
|
+
end
|
|
139
|
+
serial_obj[:metadata] = from_radio.metadata.to_h if serial_obj && from_radio.metadata
|
|
140
|
+
|
|
141
|
+
if from_radio.log_record
|
|
142
|
+
msg = from_radio.log_record.message.to_s
|
|
143
|
+
@rx_mutex.synchronize { @console_data << "#{msg}\n" } unless msg.empty?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
from_radio
|
|
147
|
+
rescue Google::Protobuf::ParseError => e
|
|
148
|
+
warn "Meshtastic::SerialInterface: failed to decode FromRadio (#{e.message})"
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# ---- public API ----------------------------------------------------------
|
|
153
|
+
|
|
48
154
|
# Supported Method Parameters::
|
|
49
155
|
# Meshtastic::SerialInterface.request(
|
|
50
156
|
# serial_obj: 'required serial_obj returned from #connect method',
|
|
51
|
-
# payload: 'required - array of bytes OR string to write to serial device
|
|
157
|
+
# payload: 'required - array of bytes OR string to write to serial device'
|
|
52
158
|
# )
|
|
53
|
-
|
|
54
159
|
public_class_method def self.request(opts = {})
|
|
55
160
|
serial_obj = opts[:serial_obj]
|
|
56
161
|
serial_conn = serial_obj[:serial_conn]
|
|
57
162
|
payload = opts[:payload]
|
|
58
163
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
end
|
|
164
|
+
bytes =
|
|
165
|
+
case payload
|
|
166
|
+
when String then payload.b
|
|
167
|
+
when Array then payload.pack('C*')
|
|
168
|
+
else
|
|
169
|
+
raise "ERROR: Invalid payload type: #{payload.class}"
|
|
170
|
+
end
|
|
67
171
|
|
|
68
|
-
|
|
172
|
+
serial_conn.write(bytes)
|
|
69
173
|
serial_conn.flush
|
|
174
|
+
sleep 0.05
|
|
175
|
+
bytes.bytesize
|
|
70
176
|
rescue StandardError => e
|
|
71
177
|
disconnect(serial_obj: serial_obj) unless serial_obj.nil?
|
|
72
178
|
raise e
|
|
73
179
|
end
|
|
74
180
|
|
|
181
|
+
# Supported Method Parameters::
|
|
182
|
+
# Meshtastic::SerialInterface.send_to_radio(
|
|
183
|
+
# serial_obj: 'required - serial_obj returned from #connect method',
|
|
184
|
+
# to_radio: 'required - Meshtastic::ToRadio OR already-serialized String'
|
|
185
|
+
# )
|
|
186
|
+
public_class_method def self.send_to_radio(opts = {})
|
|
187
|
+
serial_obj = opts[:serial_obj]
|
|
188
|
+
raise 'ERROR: serial_obj is required' unless serial_obj
|
|
189
|
+
|
|
190
|
+
to_radio = opts[:to_radio]
|
|
191
|
+
raise 'ERROR: to_radio is required' if to_radio.nil?
|
|
192
|
+
|
|
193
|
+
body =
|
|
194
|
+
case to_radio
|
|
195
|
+
when String
|
|
196
|
+
to_radio.b
|
|
197
|
+
when Meshtastic::ToRadio
|
|
198
|
+
to_radio.to_proto
|
|
199
|
+
else
|
|
200
|
+
raise "ERROR: to_radio must be Meshtastic::ToRadio or String, got #{to_radio.class}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
raise "ERROR: ToRadio payload too large (#{body.bytesize} > #{Meshtastic::MAX_TO_FROM_RADIO_SIZE})" if body.bytesize > Meshtastic::MAX_TO_FROM_RADIO_SIZE
|
|
204
|
+
|
|
205
|
+
header = [
|
|
206
|
+
Meshtastic::START1,
|
|
207
|
+
Meshtastic::START2,
|
|
208
|
+
(body.bytesize >> 8) & 0xFF,
|
|
209
|
+
body.bytesize & 0xFF
|
|
210
|
+
].pack('C*')
|
|
211
|
+
|
|
212
|
+
request(serial_obj: serial_obj, payload: header + body)
|
|
213
|
+
end
|
|
214
|
+
|
|
75
215
|
# Supported Method Parameters::
|
|
76
216
|
# serial_obj = Meshtastic::SerialInterface.connect(
|
|
77
217
|
# block_dev: 'optional - serial block device path (defaults to /dev/ttyUSB0)',
|
|
78
218
|
# baud: 'optional - (defaults to 115200)',
|
|
79
219
|
# data_bits: 'optional - (defaults to 8)',
|
|
80
220
|
# stop_bits: 'optional - (defaults to 1)',
|
|
81
|
-
# parity: 'optional - :even|:
|
|
221
|
+
# parity: 'optional - :even|:odd|:none (defaults to :none)',
|
|
222
|
+
# debug_out: 'optional - IO to receive non-protobuf debug console bytes',
|
|
223
|
+
# want_config: 'optional - request full node DB after connect (default: true)'
|
|
82
224
|
# )
|
|
83
|
-
|
|
84
225
|
public_class_method def self.connect(opts = {})
|
|
85
226
|
block_dev = opts[:block_dev] ||= '/dev/ttyUSB0'
|
|
86
227
|
raise "Invalid block device: #{block_dev}" unless File.exist?(block_dev)
|
|
@@ -89,56 +230,64 @@ module Meshtastic
|
|
|
89
230
|
data_bits = opts[:data_bits] ||= 8
|
|
90
231
|
stop_bits = opts[:stop_bits] ||= 1
|
|
91
232
|
parity = opts[:parity] ||= :none
|
|
233
|
+
debug_out = opts[:debug_out]
|
|
234
|
+
want_config = opts.fetch(:want_config, true)
|
|
235
|
+
|
|
236
|
+
parity_char =
|
|
237
|
+
case parity.to_s.to_sym
|
|
238
|
+
when :even then 'E'
|
|
239
|
+
when :odd then 'O'
|
|
240
|
+
when :none then 'N'
|
|
241
|
+
else
|
|
242
|
+
raise "Invalid parity: #{opts[:parity]}"
|
|
243
|
+
end
|
|
92
244
|
|
|
93
|
-
|
|
94
|
-
when :even
|
|
95
|
-
parity = 'E'
|
|
96
|
-
when :odd
|
|
97
|
-
parity = 'O'
|
|
98
|
-
when :none
|
|
99
|
-
parity = 'N'
|
|
100
|
-
end
|
|
101
|
-
raise "Invalid parity: #{opts[:parity]}" if parity.nil?
|
|
245
|
+
mode = "#{data_bits}#{parity_char}#{stop_bits}"
|
|
102
246
|
|
|
103
|
-
|
|
247
|
+
clear_hupcl(block_dev)
|
|
104
248
|
|
|
105
|
-
serial_conn = UART.open(
|
|
106
|
-
block_dev,
|
|
107
|
-
baud,
|
|
108
|
-
mode
|
|
109
|
-
)
|
|
249
|
+
serial_conn = UART.open(block_dev, baud, mode)
|
|
110
250
|
|
|
111
|
-
serial_obj = {
|
|
112
|
-
serial_obj[:serial_conn] = serial_conn
|
|
113
|
-
serial_obj[:console_thread] = init_stdout_thread(
|
|
251
|
+
serial_obj = {
|
|
114
252
|
serial_conn: serial_conn,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
253
|
+
block_dev: block_dev,
|
|
254
|
+
baud: baud,
|
|
255
|
+
my_info: nil,
|
|
256
|
+
my_node_num: nil,
|
|
257
|
+
metadata: nil
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
serial_obj[:rx_thread] = init_rx_thread(
|
|
118
261
|
serial_conn: serial_conn,
|
|
119
|
-
|
|
262
|
+
serial_obj: serial_obj,
|
|
263
|
+
debug_out: debug_out
|
|
120
264
|
)
|
|
121
265
|
|
|
266
|
+
# Wake / resync the device's framing state-machine.
|
|
122
267
|
wake_up_device(serial_obj: serial_obj)
|
|
123
268
|
|
|
124
|
-
|
|
125
|
-
|
|
269
|
+
if want_config
|
|
270
|
+
mui = Meshtastic::MeshInterface.new
|
|
271
|
+
to_radio_bytes = mui.start_config
|
|
272
|
+
send_to_radio(serial_obj: serial_obj, to_radio: to_radio_bytes)
|
|
273
|
+
end
|
|
126
274
|
|
|
127
275
|
serial_obj
|
|
128
276
|
rescue StandardError => e
|
|
129
277
|
disconnect(serial_obj: serial_obj) unless serial_obj.nil?
|
|
130
278
|
raise e
|
|
131
279
|
end
|
|
132
|
-
|
|
280
|
+
|
|
133
281
|
# Supported Method Parameters::
|
|
134
282
|
# wake_up_device(
|
|
135
283
|
# serial_obj: 'required - serial_obj returned from #connect method'
|
|
136
284
|
# )
|
|
137
285
|
public_class_method def self.wake_up_device(opts = {})
|
|
138
286
|
serial_obj = opts[:serial_obj]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
request(serial_obj: serial_obj, payload:
|
|
287
|
+
# START2 * 32 — does not look like a valid header, forces RX state machine resync
|
|
288
|
+
start2_bytes = ([Meshtastic::START2] * 32).pack('C*')
|
|
289
|
+
request(serial_obj: serial_obj, payload: start2_bytes)
|
|
290
|
+
sleep 0.1
|
|
142
291
|
rescue StandardError => e
|
|
143
292
|
disconnect(serial_obj: serial_obj) unless serial_obj.nil?
|
|
144
293
|
raise e
|
|
@@ -148,48 +297,79 @@ module Meshtastic
|
|
|
148
297
|
# stdout_data = Meshtastic::SerialInterface.dump_stdout_data(
|
|
149
298
|
# type: 'required - :proto or :console'
|
|
150
299
|
# )
|
|
151
|
-
|
|
152
300
|
public_class_method def self.dump_stdout_data(opts = {})
|
|
153
301
|
type = opts[:type]
|
|
154
302
|
valid_types = %i[proto console]
|
|
155
303
|
raise "ERROR: Invalid type: #{type}. Supported types are :proto or :console" unless valid_types.include?(type)
|
|
156
304
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
305
|
+
@rx_mutex.synchronize do
|
|
306
|
+
if block_given?
|
|
307
|
+
if type == :proto
|
|
308
|
+
@proto_data.each { |proto_hash| yield proto_hash }
|
|
309
|
+
else
|
|
310
|
+
@console_data.join.split("\n").each { |line| yield line.force_encoding('UTF-8') }
|
|
311
|
+
end
|
|
312
|
+
nil
|
|
313
|
+
else
|
|
314
|
+
type == :proto ? @proto_data.dup : @console_data.join
|
|
315
|
+
end
|
|
165
316
|
end
|
|
166
|
-
rescue StandardError => e
|
|
167
|
-
raise e
|
|
168
317
|
end
|
|
169
318
|
|
|
170
319
|
# Supported Method Parameters::
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
320
|
+
# Meshtastic::SerialInterface.flush_data(
|
|
321
|
+
# type: 'required - :proto or :console'
|
|
322
|
+
# )
|
|
323
|
+
public_class_method def self.flush_data(opts = {}) # rubocop:disable Naming/PredicateMethod
|
|
174
324
|
type = opts[:type]
|
|
175
325
|
valid_types = %i[proto console]
|
|
176
326
|
raise "ERROR: Invalid type: #{type}. Supported types are :proto or :console" unless valid_types.include?(type)
|
|
177
327
|
|
|
178
|
-
@
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
328
|
+
@rx_mutex.synchronize do
|
|
329
|
+
@console_data.clear if type == :console
|
|
330
|
+
@proto_data.clear if type == :proto
|
|
331
|
+
end
|
|
332
|
+
true
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Drain the FromRadio queue without blocking (returns Array of FromRadio msgs).
|
|
336
|
+
public_class_method def self.drain_from_radio(opts = {})
|
|
337
|
+
max = opts[:max] ||= 256
|
|
338
|
+
msgs = []
|
|
339
|
+
return msgs unless @from_radio_queue
|
|
340
|
+
|
|
341
|
+
max.times do
|
|
342
|
+
msgs << @from_radio_queue.pop(true)
|
|
343
|
+
rescue ThreadError
|
|
344
|
+
break
|
|
345
|
+
end
|
|
346
|
+
msgs
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Block until a FromRadio arrives or timeout (seconds). Returns FromRadio or nil.
|
|
350
|
+
public_class_method def self.recv_from_radio(opts = {})
|
|
351
|
+
timeout = opts[:timeout] ||= 5
|
|
352
|
+
raise 'ERROR: RX queue not initialised — call connect first' unless @from_radio_queue
|
|
353
|
+
|
|
354
|
+
if timeout.nil? || timeout.negative?
|
|
355
|
+
@from_radio_queue.pop
|
|
356
|
+
else
|
|
357
|
+
begin
|
|
358
|
+
Timeout.timeout(timeout) { @from_radio_queue.pop }
|
|
359
|
+
rescue Timeout::Error
|
|
360
|
+
nil
|
|
361
|
+
end
|
|
362
|
+
end
|
|
182
363
|
end
|
|
183
364
|
|
|
184
365
|
# Supported Method Parameters::
|
|
185
|
-
#
|
|
366
|
+
# Meshtastic::SerialInterface.monitor_stdout(
|
|
186
367
|
# serial_obj: 'required - serial_obj returned from #connect method',
|
|
187
368
|
# type: 'required - :proto or :console',
|
|
188
369
|
# refresh: 'optional - refresh interval (default: 3)',
|
|
189
370
|
# include: 'optional - comma-delimited string(s) to include in message (default: nil)',
|
|
190
371
|
# exclude: 'optional - comma-delimited string(s) to exclude in message (default: nil)'
|
|
191
372
|
# )
|
|
192
|
-
|
|
193
373
|
public_class_method def self.monitor_stdout(opts = {})
|
|
194
374
|
serial_obj = opts[:serial_obj]
|
|
195
375
|
type = opts[:type]
|
|
@@ -205,14 +385,14 @@ module Meshtastic
|
|
|
205
385
|
include_arr = include.to_s.split(',').map(&:strip)
|
|
206
386
|
|
|
207
387
|
dump_stdout_data(type: type) do |data|
|
|
208
|
-
|
|
209
|
-
disp =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
puts
|
|
214
|
-
flush_data(type: type)
|
|
388
|
+
data_s = data.is_a?(Hash) ? data.inspect : data.to_s
|
|
389
|
+
disp = !exclude_arr.intersect?(data_s) && (
|
|
390
|
+
include_arr.empty? ||
|
|
391
|
+
include_arr.all? { |inc| data_s.include?(inc) }
|
|
392
|
+
)
|
|
393
|
+
puts data_s if disp
|
|
215
394
|
end
|
|
395
|
+
flush_data(type: type)
|
|
216
396
|
sleep refresh
|
|
217
397
|
end
|
|
218
398
|
rescue Interrupt
|
|
@@ -223,26 +403,73 @@ module Meshtastic
|
|
|
223
403
|
raise e
|
|
224
404
|
end
|
|
225
405
|
|
|
406
|
+
# Enrich / optionally decrypt a MeshPacket hash (shared by MQTT + serial).
|
|
407
|
+
private_class_method def self.enrich_packet(opts = {})
|
|
408
|
+
message = opts[:message]
|
|
409
|
+
psks = opts[:psks] || {}
|
|
410
|
+
gps_metadata = opts[:gps_metadata] || false
|
|
411
|
+
include_raw = opts[:include_raw] || false
|
|
412
|
+
raw_packet = opts[:raw_packet]
|
|
413
|
+
|
|
414
|
+
message[:node_id_from] = "!#{message[:from].to_i.to_s(16)}"
|
|
415
|
+
message[:node_id_to] = "!#{message[:to].to_i.to_s(16)}"
|
|
416
|
+
|
|
417
|
+
message[:rx_time_utc] = Time.at(message[:rx_time]).utc.to_s if message[:rx_time].is_a?(Integer)
|
|
418
|
+
|
|
419
|
+
message[:public_key] = Base64.strict_encode64(message[:public_key]) if message[:public_key].to_s.length.positive? && !(message[:public_key].ascii_only? && message[:public_key] =~ %r{\A[A-Za-z0-9+/]+=*\z})
|
|
420
|
+
|
|
421
|
+
encrypted_message = message[:encrypted]
|
|
422
|
+
if encrypted_message.to_s.length.positive? && psks.any?
|
|
423
|
+
packet_id = message[:id]
|
|
424
|
+
packet_from = message[:from]
|
|
425
|
+
nonce_packet_id = [packet_id].pack('V').ljust(8, "\x00")
|
|
426
|
+
nonce_from_node = [packet_from].pack('V').ljust(8, "\x00")
|
|
427
|
+
nonce = "#{nonce_packet_id}#{nonce_from_node}"
|
|
428
|
+
|
|
429
|
+
psk = psks[:LongFast] || psks[psks.keys.first]
|
|
430
|
+
dec_psk = Base64.strict_decode64(psk)
|
|
431
|
+
|
|
432
|
+
cipher = OpenSSL::Cipher.new(dec_psk.length == 32 ? 'AES-256-CTR' : 'AES-128-CTR')
|
|
433
|
+
cipher.decrypt
|
|
434
|
+
cipher.key = dec_psk
|
|
435
|
+
cipher.iv = nonce
|
|
436
|
+
decrypted = cipher.update(encrypted_message) + cipher.final
|
|
437
|
+
message[:decoded] = Meshtastic::Data.decode(decrypted).to_h
|
|
438
|
+
message[:encrypted] = :decrypted
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
if message[:decoded]
|
|
442
|
+
payload = message[:decoded][:payload]
|
|
443
|
+
msg_type = message[:decoded][:portnum]
|
|
444
|
+
mui = Meshtastic::MeshInterface.new
|
|
445
|
+
message[:decoded][:payload] = mui.decode_payload(
|
|
446
|
+
payload: payload,
|
|
447
|
+
msg_type: msg_type,
|
|
448
|
+
gps_metadata: gps_metadata
|
|
449
|
+
)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
message[:raw_packet] = raw_packet if include_raw
|
|
453
|
+
message
|
|
454
|
+
rescue OpenSSL::Cipher::CipherError, ArgumentError, Google::Protobuf::ParseError => e
|
|
455
|
+
message[:decrypted] = e.message
|
|
456
|
+
message
|
|
457
|
+
end
|
|
458
|
+
|
|
226
459
|
# Supported Method Parameters::
|
|
227
460
|
# Meshtastic::SerialInterface.subscribe(
|
|
228
|
-
# serial_obj: 'required - serial_obj returned from #connect method'
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
# include: 'optional - comma-delimited string(s) to include on in message (default: nil)',
|
|
236
|
-
# gps_metadata: 'optional - include GPS metadata in output (default: false)',
|
|
237
|
-
# include_raw: 'optional - include raw packet data in output (default: false)'
|
|
461
|
+
# serial_obj: 'required - serial_obj returned from #connect method',
|
|
462
|
+
# psks: 'optional - hash of :channel_id => psk (default: { LongFast: "AQ==" })',
|
|
463
|
+
# exclude: 'optional - comma-delimited substrings to hide',
|
|
464
|
+
# include: 'optional - comma-delimited substrings required to display',
|
|
465
|
+
# gps_metadata: 'optional - reverse-geocode POSITION payloads (default: false)',
|
|
466
|
+
# include_raw: 'optional - include raw protobuf bytes (default: false)',
|
|
467
|
+
# timeout: 'optional - seconds to block on empty queue per iteration (default: nil = forever)'
|
|
238
468
|
# )
|
|
239
|
-
|
|
469
|
+
# Yields each decoded FromRadio hash. Without a block, pretty-prints packets.
|
|
240
470
|
public_class_method def self.subscribe(opts = {})
|
|
241
471
|
serial_obj = opts[:serial_obj]
|
|
242
|
-
|
|
243
|
-
region = opts[:region] ||= 'US'
|
|
244
|
-
channel_topic = opts[:channel_topic] ||= '2/e/LongFast/#'
|
|
245
|
-
# TODO: Support Array of PSKs and attempt each until decrypted
|
|
472
|
+
raise 'ERROR: serial_obj is required' unless serial_obj
|
|
246
473
|
|
|
247
474
|
public_psk = '1PG7OiApB1nwvP+rz05pAQ=='
|
|
248
475
|
psks = opts[:psks] ||= { LongFast: public_psk }
|
|
@@ -252,212 +479,186 @@ module Meshtastic
|
|
|
252
479
|
mui = Meshtastic::MeshInterface.new
|
|
253
480
|
psks = mui.get_cipher_keys(psks: psks)
|
|
254
481
|
|
|
255
|
-
qos = opts[:qos] ||= 0
|
|
256
|
-
json = opts[:json] ||= false
|
|
257
482
|
exclude = opts[:exclude]
|
|
258
483
|
include = opts[:include]
|
|
259
484
|
gps_metadata = opts[:gps_metadata] ||= false
|
|
260
485
|
include_raw = opts[:include_raw] ||= false
|
|
261
|
-
|
|
262
|
-
# NOTE: Use MQTT Explorer for topic discovery
|
|
263
|
-
full_topic = "#{root_topic}/#{region}/#{channel_topic}"
|
|
264
|
-
full_topic = "#{root_topic}/#{region}" if region == '#'
|
|
265
|
-
puts "Subscribing to: #{full_topic}"
|
|
266
|
-
serial_obj.subscribe(full_topic, qos)
|
|
267
|
-
|
|
268
|
-
# MQTT::ProtocolException: No Ping Response received for 23 seconds (MQTT::ProtocolException)
|
|
486
|
+
timeout = opts[:timeout]
|
|
269
487
|
|
|
270
488
|
include_arr = include.to_s.split(',').map(&:strip)
|
|
271
489
|
exclude_arr = exclude.to_s.split(',').map(&:strip)
|
|
272
|
-
serial_obj.get_packet do |packet_bytes|
|
|
273
|
-
raw_packet = packet_bytes.to_s if include_raw
|
|
274
|
-
raw_topic = packet_bytes.topic ||= ''
|
|
275
|
-
raw_payload = packet_bytes.payload ||= ''
|
|
276
490
|
|
|
277
|
-
|
|
278
|
-
disp = false
|
|
279
|
-
decoded_payload_hash = {}
|
|
280
|
-
message = {}
|
|
281
|
-
stdout_message = ''
|
|
491
|
+
puts 'Subscribing to serial FromRadio stream...'
|
|
282
492
|
|
|
283
|
-
|
|
284
|
-
|
|
493
|
+
loop do
|
|
494
|
+
from_radio =
|
|
495
|
+
if timeout
|
|
496
|
+
recv_from_radio(timeout: timeout)
|
|
285
497
|
else
|
|
286
|
-
|
|
287
|
-
decoded_payload = Meshtastic::FromRadio.decode(raw_payload)
|
|
288
|
-
decoded_payload_hash = decoded_payload.to_h
|
|
498
|
+
@from_radio_queue.pop
|
|
289
499
|
end
|
|
500
|
+
next if from_radio.nil?
|
|
290
501
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
message[:topic] = raw_topic
|
|
295
|
-
message[:node_id_from] = "!#{message[:from].to_i.to_s(16)}"
|
|
296
|
-
message[:node_id_to] = "!#{message[:to].to_i.to_s(16)}"
|
|
297
|
-
if message.keys.include?(:rx_time)
|
|
298
|
-
rx_time_int = message[:rx_time]
|
|
299
|
-
if rx_time_int.is_a?(Integer)
|
|
300
|
-
rx_time_utc = Time.at(rx_time_int).utc.to_s
|
|
301
|
-
message[:rx_time_utc] = rx_time_utc
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
if message.keys.include?(:public_key)
|
|
306
|
-
raw_public_key = message[:public_key]
|
|
307
|
-
message[:public_key] = Base64.strict_encode64(raw_public_key)
|
|
308
|
-
end
|
|
502
|
+
begin
|
|
503
|
+
decoded_payload_hash = from_radio.to_h
|
|
504
|
+
raw_packet = from_radio.to_proto if include_raw
|
|
309
505
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
encrypted_message = message[:encrypted]
|
|
313
|
-
if encrypted_message.to_s.length.positive? &&
|
|
314
|
-
message[:topic]
|
|
315
|
-
|
|
316
|
-
# if message[:pki_encrypted]
|
|
317
|
-
# # TODO: Display Decrypted PKI Message
|
|
318
|
-
# public_key = message[:public_key]
|
|
319
|
-
# dec_public_key = Base64.strict_decode64(public_key)
|
|
320
|
-
# else
|
|
321
|
-
packet_id = message[:id]
|
|
322
|
-
packet_from = message[:from]
|
|
323
|
-
|
|
324
|
-
nonce_packet_id = [packet_id].pack('V').ljust(8, "\x00")
|
|
325
|
-
nonce_from_node = [packet_from].pack('V').ljust(8, "\x00")
|
|
326
|
-
nonce = "#{nonce_packet_id}#{nonce_from_node}"
|
|
327
|
-
|
|
328
|
-
psk = psks[:LongFast]
|
|
329
|
-
target_channel = message[:topic].split('/')[-2].to_sym
|
|
330
|
-
psk = psks[target_channel] if psks.keys.include?(target_channel)
|
|
331
|
-
dec_psk = Base64.strict_decode64(psk)
|
|
332
|
-
|
|
333
|
-
cipher = OpenSSL::Cipher.new('AES-128-CTR')
|
|
334
|
-
cipher = OpenSSL::Cipher.new('AES-256-CTR') if dec_psk.length == 32
|
|
335
|
-
cipher.decrypt
|
|
336
|
-
cipher.key = dec_psk
|
|
337
|
-
cipher.iv = nonce
|
|
338
|
-
|
|
339
|
-
decrypted = cipher.update(encrypted_message) + cipher.final
|
|
340
|
-
# end
|
|
341
|
-
message[:decoded] = Meshtastic::Data.decode(decrypted).to_h
|
|
342
|
-
message[:encrypted] = :decrypted
|
|
343
|
-
end
|
|
506
|
+
message = {}
|
|
507
|
+
stdout_message = ''
|
|
344
508
|
|
|
345
|
-
if
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
msg_type: msg_type,
|
|
353
|
-
gps_metadata: gps_metadata
|
|
509
|
+
if decoded_payload_hash[:packet].is_a?(Hash)
|
|
510
|
+
message = enrich_packet(
|
|
511
|
+
message: decoded_payload_hash[:packet],
|
|
512
|
+
psks: psks,
|
|
513
|
+
gps_metadata: gps_metadata,
|
|
514
|
+
include_raw: include_raw,
|
|
515
|
+
raw_packet: raw_packet
|
|
354
516
|
)
|
|
517
|
+
decoded_payload_hash[:packet] = message
|
|
355
518
|
end
|
|
356
519
|
|
|
357
|
-
message[:raw_packet] = raw_packet if include_raw
|
|
358
|
-
decoded_payload_hash[:packet] = message
|
|
359
520
|
unless block_given?
|
|
360
|
-
message[:stdout] = 'pretty'
|
|
521
|
+
message[:stdout] = 'pretty' if message.is_a?(Hash)
|
|
361
522
|
stdout_message = JSON.pretty_generate(decoded_payload_hash)
|
|
362
523
|
end
|
|
363
524
|
rescue Encoding::CompatibilityError,
|
|
364
525
|
Google::Protobuf::ParseError,
|
|
365
526
|
JSON::GeneratorError,
|
|
366
527
|
ArgumentError => e
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
message[:
|
|
371
|
-
|
|
372
|
-
unless block_given?
|
|
373
|
-
puts "WARNING: #{e.inspect} - MSG IS >>>"
|
|
374
|
-
# puts e.backtrace
|
|
375
|
-
message[:stdout] = 'inspect'
|
|
376
|
-
stdout_message = decoded_payload_hash.inspect
|
|
377
|
-
end
|
|
528
|
+
message[:decrypted] = e.message if message.is_a?(Hash)
|
|
529
|
+
decoded_payload_hash[:packet] = message if message.is_a?(Hash)
|
|
530
|
+
unless block_given?
|
|
531
|
+
message[:stdout] = 'inspect' if message.is_a?(Hash)
|
|
532
|
+
stdout_message = decoded_payload_hash.inspect
|
|
378
533
|
end
|
|
379
|
-
|
|
380
|
-
next
|
|
381
534
|
ensure
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
puts '-' * 80
|
|
400
|
-
puts "\n\n\n"
|
|
401
|
-
end
|
|
402
|
-
# else
|
|
403
|
-
# print '.'
|
|
535
|
+
flat_source = decoded_payload_hash.is_a?(Hash) ? decoded_payload_hash : {}
|
|
536
|
+
flat_message = flat_source.values.join(' ')
|
|
537
|
+
flat_message = "#{flat_message} #{message.values.join(' ')}" if message.is_a?(Hash)
|
|
538
|
+
|
|
539
|
+
disp = !exclude_arr.intersect?(flat_message) &&
|
|
540
|
+
include_arr.all? { |inc| flat_message.include?(inc) }
|
|
541
|
+
|
|
542
|
+
if disp
|
|
543
|
+
if block_given?
|
|
544
|
+
yield decoded_payload_hash
|
|
545
|
+
else
|
|
546
|
+
puts "\n"
|
|
547
|
+
puts '-' * 80
|
|
548
|
+
puts 'MSG:'
|
|
549
|
+
puts stdout_message
|
|
550
|
+
puts '-' * 80
|
|
551
|
+
puts "\n\n\n"
|
|
404
552
|
end
|
|
405
553
|
end
|
|
406
554
|
end
|
|
407
555
|
end
|
|
408
556
|
rescue Interrupt
|
|
409
557
|
puts "\nCTRL+C detected. Exiting..."
|
|
410
|
-
|
|
558
|
+
disconnect(serial_obj: serial_obj) unless serial_obj.nil?
|
|
411
559
|
rescue StandardError => e
|
|
412
|
-
|
|
560
|
+
disconnect(serial_obj: serial_obj) unless serial_obj.nil?
|
|
413
561
|
raise e
|
|
414
562
|
end
|
|
415
563
|
|
|
416
564
|
# Supported Method Parameters::
|
|
417
565
|
# Meshtastic::SerialInterface.send_text(
|
|
418
566
|
# serial_obj: 'required - serial_obj returned from #connect method',
|
|
419
|
-
# from: '
|
|
567
|
+
# from: 'optional - From ID (Default: local my_node_num or "!00000b0b")',
|
|
420
568
|
# to: 'optional - Destination ID (Default: "!ffffffff")',
|
|
421
|
-
#
|
|
422
|
-
# channel: 'optional - channel (Default: 6)',
|
|
569
|
+
# channel: 'optional - channel index (Default: 0)',
|
|
423
570
|
# text: 'optional - Text Message (Default: SYN)',
|
|
424
571
|
# want_ack: 'optional - Want Acknowledgement (Default: false)',
|
|
425
572
|
# want_response: 'optional - Want Response (Default: false)',
|
|
426
573
|
# hop_limit: 'optional - Hop Limit (Default: 3)',
|
|
427
|
-
#
|
|
428
|
-
# psks: 'optional - hash of :channel_id => psk key value pairs (default: { LongFast: "AQ==" })'
|
|
574
|
+
# psks: 'optional - ignored for serial (device owns channel crypto)'
|
|
429
575
|
# )
|
|
430
576
|
public_class_method def self.send_text(opts = {})
|
|
431
|
-
|
|
432
|
-
|
|
577
|
+
serial_obj = opts[:serial_obj]
|
|
578
|
+
raise 'ERROR: serial_obj is required' unless serial_obj
|
|
579
|
+
|
|
580
|
+
opts = opts.dup
|
|
433
581
|
opts[:via] = :radio
|
|
582
|
+
opts[:channel] ||= 0
|
|
583
|
+
|
|
584
|
+
if opts[:from].nil?
|
|
585
|
+
opts[:from] =
|
|
586
|
+
if serial_obj[:my_node_num]
|
|
587
|
+
"!#{serial_obj[:my_node_num].to_s(16)}"
|
|
588
|
+
else
|
|
589
|
+
'!00000b0b'
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Device performs channel encryption for serial ToRadio packets.
|
|
594
|
+
# Pass empty psks so MeshInterface leaves the payload in :decoded form.
|
|
595
|
+
opts[:psks] = nil
|
|
434
596
|
|
|
435
|
-
# TODO: Implement chunked message to deal with large messages
|
|
436
597
|
mui = Meshtastic::MeshInterface.new
|
|
437
|
-
mui.send_text(opts)
|
|
598
|
+
protobuf = mui.send_text(opts)
|
|
599
|
+
send_to_radio(serial_obj: serial_obj, to_radio: protobuf)
|
|
600
|
+
rescue StandardError => e
|
|
601
|
+
disconnect(serial_obj: serial_obj) unless serial_obj.nil?
|
|
602
|
+
raise e
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Supported Method Parameters::
|
|
606
|
+
# Meshtastic::SerialInterface.send_data(
|
|
607
|
+
# serial_obj: 'required - serial_obj returned from #connect method',
|
|
608
|
+
# ...same kwargs as MeshInterface#send_data (via forced to :radio)
|
|
609
|
+
# )
|
|
610
|
+
public_class_method def self.send_data(opts = {})
|
|
611
|
+
serial_obj = opts[:serial_obj]
|
|
612
|
+
raise 'ERROR: serial_obj is required' unless serial_obj
|
|
613
|
+
|
|
614
|
+
opts = opts.dup
|
|
615
|
+
opts[:via] = :radio
|
|
616
|
+
opts[:channel] ||= 0
|
|
617
|
+
opts[:psks] = nil
|
|
618
|
+
opts[:from] = "!#{serial_obj[:my_node_num].to_s(16)}" if opts[:from].nil? && serial_obj[:my_node_num]
|
|
438
619
|
|
|
439
|
-
|
|
440
|
-
|
|
620
|
+
mui = Meshtastic::MeshInterface.new
|
|
621
|
+
protobuf = mui.send_data(opts)
|
|
622
|
+
send_to_radio(serial_obj: serial_obj, to_radio: protobuf)
|
|
441
623
|
rescue StandardError => e
|
|
442
|
-
|
|
624
|
+
disconnect(serial_obj: serial_obj) unless serial_obj.nil?
|
|
443
625
|
raise e
|
|
444
626
|
end
|
|
445
627
|
|
|
446
628
|
# Supported Method Parameters::
|
|
447
|
-
# serial_obj = Meshtastic.disconnect(
|
|
629
|
+
# serial_obj = Meshtastic::SerialInterface.disconnect(
|
|
448
630
|
# serial_obj: 'required - serial_obj returned from #connect method'
|
|
449
631
|
# )
|
|
450
632
|
public_class_method def self.disconnect(opts = {})
|
|
451
633
|
serial_obj = opts[:serial_obj]
|
|
634
|
+
return nil unless serial_obj
|
|
452
635
|
|
|
453
|
-
|
|
454
|
-
console_thread = serial_obj[:console_thread]
|
|
455
|
-
proto_thread = serial_obj[:proto_thread]
|
|
456
|
-
serial_conn = serial_obj[:serial_conn]
|
|
636
|
+
@want_exit = true
|
|
457
637
|
|
|
458
|
-
|
|
459
|
-
|
|
638
|
+
# Ask device to release the link (best-effort).
|
|
639
|
+
begin
|
|
640
|
+
if serial_obj[:serial_conn] && !serial_obj[:serial_conn].closed?
|
|
641
|
+
to_radio = Meshtastic::ToRadio.new
|
|
642
|
+
to_radio.disconnect = true
|
|
643
|
+
send_to_radio(serial_obj: serial_obj, to_radio: to_radio)
|
|
644
|
+
sleep 0.05
|
|
645
|
+
end
|
|
646
|
+
rescue StandardError
|
|
647
|
+
# ignore during teardown
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
rx_thread = serial_obj[:rx_thread]
|
|
651
|
+
serial_conn = serial_obj[:serial_conn]
|
|
652
|
+
|
|
653
|
+
begin
|
|
460
654
|
serial_conn&.close
|
|
655
|
+
rescue StandardError
|
|
656
|
+
nil
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
if rx_thread&.alive? && rx_thread != Thread.current
|
|
660
|
+
rx_thread.join(1)
|
|
661
|
+
rx_thread.kill if rx_thread.alive?
|
|
461
662
|
end
|
|
462
663
|
|
|
463
664
|
nil
|
|
@@ -478,14 +679,13 @@ module Meshtastic
|
|
|
478
679
|
public_class_method def self.help
|
|
479
680
|
puts "USAGE:
|
|
480
681
|
serial_obj = #{self}.connect(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
ack_timeout: 'optional - acknowledgement timeout (default: 30)'
|
|
682
|
+
block_dev: 'optional - serial block device path (defaults to /dev/ttyUSB0)',
|
|
683
|
+
baud: 'optional - (defaults to 115200)',
|
|
684
|
+
data_bits: 'optional - (defaults to 8)',
|
|
685
|
+
stop_bits: 'optional - (defaults to 1)',
|
|
686
|
+
parity: 'optional - :even|:odd|:none (defaults to :none)',
|
|
687
|
+
debug_out: 'optional - IO receiving non-protobuf debug console bytes',
|
|
688
|
+
want_config: 'optional - request full node DB after connect (default: true)'
|
|
489
689
|
)
|
|
490
690
|
|
|
491
691
|
#{self}.wake_up_device(
|
|
@@ -494,53 +694,70 @@ module Meshtastic
|
|
|
494
694
|
|
|
495
695
|
#{self}.request(
|
|
496
696
|
serial_obj: 'required serial_obj returned from #connect method',
|
|
497
|
-
payload: 'required - array of bytes OR string to write to serial device
|
|
697
|
+
payload: 'required - array of bytes OR string to write to serial device'
|
|
498
698
|
)
|
|
499
699
|
|
|
700
|
+
#{self}.send_to_radio(
|
|
701
|
+
serial_obj: 'required - serial_obj returned from #connect method',
|
|
702
|
+
to_radio: 'required - Meshtastic::ToRadio OR serialized String'
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
from_radio = #{self}.recv_from_radio(
|
|
706
|
+
timeout: 'optional - seconds (default: 5; nil = block forever)'
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
msgs = #{self}.drain_from_radio(max: 256)
|
|
710
|
+
|
|
500
711
|
stdout_data = #{self}.dump_stdout_data(
|
|
501
712
|
type: 'required - :proto or :console'
|
|
502
713
|
)
|
|
503
714
|
|
|
504
715
|
#{self}.flush_data(
|
|
505
|
-
type: '
|
|
716
|
+
type: 'required - :console or :proto'
|
|
506
717
|
)
|
|
507
718
|
|
|
508
719
|
#{self}.monitor_stdout(
|
|
720
|
+
serial_obj: 'required - serial_obj returned from #connect method',
|
|
509
721
|
type: 'required - :proto or :console',
|
|
510
722
|
refresh: 'optional - refresh interval (default: 3)',
|
|
511
|
-
include: 'optional - comma-delimited string(s) to include in message
|
|
512
|
-
exclude: 'optional - comma-delimited string(s) to exclude in message
|
|
723
|
+
include: 'optional - comma-delimited string(s) to include in message',
|
|
724
|
+
exclude: 'optional - comma-delimited string(s) to exclude in message'
|
|
513
725
|
)
|
|
514
726
|
|
|
515
727
|
#{self}.subscribe(
|
|
516
|
-
serial_obj: 'required - serial_obj
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
exclude: 'optional - comma-delimited string(s) to exclude in message (default: nil)',
|
|
524
|
-
include: 'optional - comma-delimited string(s) to include on in message (default: nil)',
|
|
525
|
-
gps_metadata: 'optional - include GPS metadata in output (default: false)'
|
|
728
|
+
serial_obj: 'required - serial_obj returned from #connect method',
|
|
729
|
+
psks: 'optional - hash of :channel_id => psk (default: { LongFast: \"AQ==\" })',
|
|
730
|
+
exclude: 'optional - comma-delimited string(s) to exclude',
|
|
731
|
+
include: 'optional - comma-delimited string(s) to include',
|
|
732
|
+
gps_metadata: 'optional - include GPS metadata (default: false)',
|
|
733
|
+
include_raw: 'optional - include raw packet bytes (default: false)',
|
|
734
|
+
timeout: 'optional - seconds per pop (default: nil = forever)'
|
|
526
735
|
)
|
|
527
736
|
|
|
528
737
|
#{self}.send_text(
|
|
529
738
|
serial_obj: 'required - serial_obj returned from #connect method',
|
|
530
|
-
from: '
|
|
739
|
+
from: 'optional - From ID (Default: local my_node_num or \"!00000b0b\")',
|
|
531
740
|
to: 'optional - Destination ID (Default: \"!ffffffff\")',
|
|
532
|
-
|
|
533
|
-
channel: 'optional - channel (Default: 6)',
|
|
741
|
+
channel: 'optional - channel index (Default: 0)',
|
|
534
742
|
text: 'optional - Text Message (Default: SYN)',
|
|
535
743
|
want_ack: 'optional - Want Acknowledgement (Default: false)',
|
|
536
744
|
want_response: 'optional - Want Response (Default: false)',
|
|
745
|
+
hop_limit: 'optional - Hop Limit (Default: 3)'
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
#{self}.send_data(
|
|
749
|
+
serial_obj: 'required - serial_obj returned from #connect method',
|
|
750
|
+
from: 'optional - From ID',
|
|
751
|
+
to: 'optional - Destination ID (Default: \"!ffffffff\")',
|
|
752
|
+
channel: 'optional - channel index (Default: 0)',
|
|
753
|
+
data: 'required - Meshtastic::Data',
|
|
754
|
+
want_ack: 'optional - Want Acknowledgement (Default: false)',
|
|
537
755
|
hop_limit: 'optional - Hop Limit (Default: 3)',
|
|
538
|
-
|
|
539
|
-
psks: 'optional - hash of :channel => psk key value pairs (default: { LongFast: 'AQ==' })'
|
|
756
|
+
port_num: 'optional - PortNum (Default: PRIVATE_APP)'
|
|
540
757
|
)
|
|
541
758
|
|
|
542
759
|
serial_obj = #{self}.disconnect(
|
|
543
|
-
serial_obj: 'required - serial_obj
|
|
760
|
+
serial_obj: 'required - serial_obj returned from #connect method'
|
|
544
761
|
)
|
|
545
762
|
|
|
546
763
|
#{self}.authors
|