meshtastic 0.0.179 → 0.0.181
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/Gemfile +2 -2
- data/documentation/README.md +3 -2
- data/documentation/admin-channel.md +64 -0
- data/documentation/admin-config.md +59 -0
- data/documentation/admin-firmware-nordic.md +86 -0
- data/documentation/admin-firmware-serial.md +180 -0
- data/documentation/admin-firmware.md +126 -0
- data/documentation/admin.md +88 -35
- data/documentation/atak.md +93 -10
- data/documentation/channel.md +1 -28
- data/documentation/config.md +1 -27
- data/documentation/mesh-interface.md +7 -0
- data/documentation/meshtastic.md +2 -2
- data/lib/meshtastic/admin/channel.rb +164 -0
- data/lib/meshtastic/admin/config.rb +213 -0
- data/lib/meshtastic/admin/firmware/ble.rb +207 -0
- data/lib/meshtastic/admin/firmware/nordic_dfu.rb +218 -0
- data/lib/meshtastic/admin/firmware/serial_bootloader.rb +233 -0
- data/lib/meshtastic/admin/firmware.rb +288 -0
- data/lib/meshtastic/admin.rb +688 -34
- 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/config_pb.rb +2 -1
- 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 +19 -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/storeforward_pb.rb +1 -1
- 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 +202 -0
- data/spec/lib/meshtastic/admin/config_spec.rb +114 -0
- data/spec/lib/meshtastic/admin/firmware/ble_spec.rb +170 -0
- data/spec/lib/meshtastic/admin/firmware/nordic_dfu_spec.rb +248 -0
- data/spec/lib/meshtastic/admin/firmware/serial_bootloader_spec.rb +263 -0
- data/spec/lib/meshtastic/admin/firmware_spec.rb +315 -0
- data/spec/lib/meshtastic/admin_spec.rb +412 -1
- 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/mesh_interface_spec.rb +31 -0
- data/spec/lib/meshtastic/portnums_spec.rb +2 -2
- metadata +24 -6
data/lib/meshtastic/admin.rb
CHANGED
|
@@ -1,82 +1,736 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'timeout'
|
|
4
|
+
require 'monitor'
|
|
3
5
|
require 'meshtastic/admin_pb'
|
|
4
6
|
|
|
5
7
|
module Meshtastic
|
|
6
8
|
module Admin
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
opts.each do |key, value|
|
|
10
|
-
next if %i[message serial_obj bluetooth_obj tcp_obj to from channel want_ack hop_limit].include?(key)
|
|
11
|
-
next unless message.respond_to?("#{key}=")
|
|
9
|
+
class RoutingError < StandardError
|
|
10
|
+
attr_reader :reason, :request_id, :from
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
def initialize(reason, request_id, from)
|
|
13
|
+
@reason = reason
|
|
14
|
+
@request_id = request_id
|
|
15
|
+
@from = from
|
|
16
|
+
super("Admin routing failure: #{reason} (request #{request_id})")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
SKIP = %i[
|
|
21
|
+
message serial_obj bluetooth_obj tcp_obj mqtt_obj to from channel want_ack hop_limit
|
|
22
|
+
want_response port_num data via psks seconds owner long_name short_name index config_type
|
|
23
|
+
module_config_type path node_num lat lon altitude time channel_settings channel_pb
|
|
24
|
+
config module_config messages ringtone scale location event event_code kb_char touch_x
|
|
25
|
+
touch_y ham call_sign tx_power frequency position ui_config contact value preserve_favorites
|
|
26
|
+
last_packet_id pki_encrypted public_key topic qos retained timeout auto_session refresh_session
|
|
27
|
+
].freeze
|
|
28
|
+
PAYLOAD_FIELDS = Meshtastic::AdminMessage.descriptor.lookup_oneof('payload_variant').map { |field| field.name.to_sym }.freeze
|
|
29
|
+
|
|
30
|
+
public_class_method def self.encode(opts = {})
|
|
31
|
+
original = opts[:message] || Meshtastic::AdminMessage.new
|
|
32
|
+
raise ArgumentError, 'message must be an AdminMessage' unless original.is_a?(Meshtastic::AdminMessage)
|
|
33
|
+
|
|
34
|
+
fields = opts.keys & PAYLOAD_FIELDS
|
|
35
|
+
variants = (fields + [original.payload_variant]).compact.uniq
|
|
36
|
+
raise ArgumentError, 'exactly one payload is required' unless variants.length == 1
|
|
37
|
+
|
|
38
|
+
unknown = opts.keys - SKIP - PAYLOAD_FIELDS - [:session_passkey]
|
|
39
|
+
raise ArgumentError, "unknown admin options: #{unknown.join(', ')}" unless unknown.empty?
|
|
40
|
+
|
|
41
|
+
message = Meshtastic::AdminMessage.decode(original.to_proto)
|
|
42
|
+
fields.each do |key|
|
|
43
|
+
raise ArgumentError, "#{key} cannot be nil" if opts[key].nil?
|
|
44
|
+
|
|
45
|
+
message.public_send("#{key}=", opts[key])
|
|
46
|
+
end
|
|
47
|
+
if opts.key?(:session_passkey)
|
|
48
|
+
passkey = opts[:session_passkey]
|
|
49
|
+
raise ArgumentError, 'session_passkey must contain exactly eight bytes' unless passkey.is_a?(String) && passkey.bytesize == 8
|
|
50
|
+
|
|
51
|
+
message.session_passkey = passkey
|
|
14
52
|
end
|
|
15
53
|
message
|
|
16
54
|
end
|
|
17
55
|
|
|
18
|
-
def self.send(opts = {})
|
|
56
|
+
public_class_method def self.send(opts = {})
|
|
19
57
|
message = encode(opts)
|
|
58
|
+
connection = opts[:serial_obj] || opts[:bluetooth_obj] || opts[:tcp_obj]
|
|
59
|
+
destination = opts[:to] || connection&.dig(:my_node_num)
|
|
60
|
+
destination = destination.delete_prefix('!').to_i(16) if destination.is_a?(String) && destination.match?(/\A![0-9a-fA-F]{8}\z/)
|
|
61
|
+
raise ArgumentError, 'an explicit unicast destination or connected my_node_num is required' unless destination.is_a?(Integer) && destination.between?(1, 0xfffffffe)
|
|
62
|
+
|
|
63
|
+
if opts.fetch(:auto_session, true) && message.session_passkey.empty? && destination != connection&.dig(:my_node_num) &&
|
|
64
|
+
!message.payload_variant.to_s.match?(/\Aget_.*_(request|response)\z/)
|
|
65
|
+
message.session_passkey = acquire_session(opts.merge(connection: connection, target: destination))
|
|
66
|
+
end
|
|
67
|
+
want_response = opts.fetch(:want_response) { message.payload_variant.to_s.match?(/\Aget_.*_request\z/) }
|
|
20
68
|
data = Meshtastic::Data.new(
|
|
21
69
|
portnum: :ADMIN_APP,
|
|
22
70
|
payload: message.to_proto,
|
|
23
|
-
want_response:
|
|
71
|
+
want_response: want_response
|
|
24
72
|
)
|
|
25
|
-
|
|
73
|
+
delivery = opts.merge(data: data, port_num: Meshtastic::PortNum::ADMIN_APP, to: destination)
|
|
74
|
+
delivery[:from] = 0 if connection && !opts.key?(:from)
|
|
75
|
+
Meshtastic.deliver_data(delivery)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
public_class_method def self.request(opts = {})
|
|
79
|
+
request_id = opts.fetch(:request_id) { Random.rand(2..0xffffffff) }
|
|
80
|
+
raise ArgumentError, 'request_id must be an Integer from 2 through 0xffffffff' unless request_id.is_a?(Integer) && request_id.between?(2, 0xffffffff)
|
|
81
|
+
|
|
82
|
+
options = opts.except(:request_id, :wait)
|
|
83
|
+
return { request_id: request_id, result: send(options.merge(last_packet_id: request_id - 1)) } unless opts.fetch(:wait, true)
|
|
84
|
+
|
|
85
|
+
connection = options[:serial_obj] || options[:bluetooth_obj] || options[:tcp_obj]
|
|
86
|
+
queue = connection && connection[:from_radio_queue]
|
|
87
|
+
raise ArgumentError, 'synchronous Admin requires a connected radio receive queue' unless queue
|
|
88
|
+
|
|
89
|
+
timeout = opts.fetch(:timeout, 10)
|
|
90
|
+
raise ArgumentError, 'timeout must be a positive finite number' unless timeout.is_a?(Numeric) && timeout.positive? && timeout.finite?
|
|
91
|
+
|
|
92
|
+
lock = connection[:admin_request_lock] ||= Monitor.new
|
|
93
|
+
acquired = lock.try_enter
|
|
94
|
+
raise IOError, 'a synchronous Admin request is already active on this handle' unless acquired
|
|
95
|
+
|
|
96
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
97
|
+
message = encode(options)
|
|
98
|
+
target = options.fetch(:to, connection[:my_node_num])
|
|
99
|
+
target = target.delete_prefix('!').to_i(16) if target.is_a?(String) && target.match?(/\A![0-9a-fA-F]{8}\z/)
|
|
100
|
+
result = send(options.merge(last_packet_id: request_id - 1, want_response: true, want_ack: true, timeout: timeout))
|
|
101
|
+
variant = message.payload_variant.to_s
|
|
102
|
+
expected = variant.match?(/\Aget_.*_request\z/) ? variant.sub(/_request\z/, '_response').to_sym : :routing
|
|
103
|
+
reply = await_response(connection: connection, queue: queue, deadline: deadline, request_id: request_id, target: target,
|
|
104
|
+
variant: expected)
|
|
105
|
+
if reply[:session_passkey]&.bytesize == 8
|
|
106
|
+
sessions = connection[:admin_sessions] ||= {}
|
|
107
|
+
sessions[target] = { key: reply[:session_passkey], expires_at: deadline - timeout + 150 }
|
|
108
|
+
end
|
|
109
|
+
reply.merge(result: result)
|
|
110
|
+
rescue RoutingError => e
|
|
111
|
+
connection[:admin_sessions]&.delete(target) if connection && e.reason == :ADMIN_BAD_SESSION_KEY
|
|
112
|
+
raise
|
|
113
|
+
ensure
|
|
114
|
+
lock.exit if acquired
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private_class_method def self.acquire_session(opts = {})
|
|
118
|
+
connection = opts[:connection]
|
|
119
|
+
raise ArgumentError, 'automatic Admin sessions require a radio receive queue; supply session_passkey for MQTT' unless connection && connection[:from_radio_queue]
|
|
120
|
+
|
|
121
|
+
sessions = connection[:admin_sessions] ||= {}
|
|
122
|
+
cached = sessions[opts[:target]]
|
|
123
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
124
|
+
return cached[:key] if cached && now < cached[:expires_at] && !opts[:refresh_session]
|
|
125
|
+
|
|
126
|
+
sessions.delete(opts[:target])
|
|
127
|
+
options = opts.except(*(PAYLOAD_FIELDS + %i[message connection target session_passkey refresh_session last_packet_id]))
|
|
128
|
+
reply = request(options.merge(get_config_request: :SESSIONKEY_CONFIG, auto_session: false))
|
|
129
|
+
key = reply[:session_passkey]
|
|
130
|
+
raise ArgumentError, 'Admin session response did not contain an eight-byte passkey' unless key.bytesize == 8
|
|
131
|
+
|
|
132
|
+
sessions[opts[:target]] = { key: key, expires_at: now + 150 }
|
|
133
|
+
key
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private_class_method def self.await_response(opts = {})
|
|
137
|
+
queue = opts[:queue]
|
|
138
|
+
deferred = []
|
|
139
|
+
loop do
|
|
140
|
+
remaining = opts[:deadline] - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
141
|
+
raise Timeout::Error, 'Admin response timed out' unless remaining.positive?
|
|
142
|
+
|
|
143
|
+
incoming = queue.pop(timeout: remaining)
|
|
144
|
+
raise Timeout::Error, 'Admin response timed out or transport closed' unless incoming
|
|
145
|
+
|
|
146
|
+
packet = incoming.is_a?(Meshtastic::FromRadio) ? incoming.packet : incoming
|
|
147
|
+
if packet.is_a?(Meshtastic::MeshPacket) && packet.decoded&.portnum == :ROUTING_APP && packet.decoded.request_id == opts[:request_id] &&
|
|
148
|
+
[opts[:target], opts[:connection][:my_node_num]].include?(packet.from)
|
|
149
|
+
reason = Meshtastic::Routing.decode(packet.decoded.payload).error_reason
|
|
150
|
+
raise RoutingError.new(reason, opts[:request_id], packet.from) if reason != :NONE && [opts[:target], opts[:connection][:my_node_num]].include?(packet.from)
|
|
151
|
+
return { variant: :routing, value: reason, request_id: opts[:request_id], from: packet.from } if reason == :NONE && packet.from == opts[:target] && opts[:variant] == :routing
|
|
152
|
+
end
|
|
153
|
+
reply = response(packet: incoming, request_id: opts[:request_id], from: opts[:target])
|
|
154
|
+
return reply if reply && reply[:variant] == opts[:variant]
|
|
155
|
+
|
|
156
|
+
deferred << incoming
|
|
157
|
+
end
|
|
158
|
+
ensure
|
|
159
|
+
begin
|
|
160
|
+
until deferred.nil? || deferred.empty?
|
|
161
|
+
queue << deferred.first
|
|
162
|
+
deferred.shift
|
|
163
|
+
end
|
|
164
|
+
rescue ClosedQueueError
|
|
165
|
+
replacement = Queue.new
|
|
166
|
+
deferred.each { |incoming| replacement << incoming }
|
|
167
|
+
while (incoming = queue.pop)
|
|
168
|
+
replacement << incoming
|
|
169
|
+
end
|
|
170
|
+
replacement.close
|
|
171
|
+
opts[:connection][:from_radio_queue] = replacement
|
|
172
|
+
deferred.clear
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
public_class_method def self.decode(opts = {})
|
|
177
|
+
packet = opts[:packet]
|
|
178
|
+
packet = packet.packet if packet.is_a?(Meshtastic::FromRadio)
|
|
179
|
+
data = packet.is_a?(Meshtastic::MeshPacket) ? packet.decoded : packet
|
|
180
|
+
raise ArgumentError, 'packet must contain decoded ADMIN_APP data' if opts.key?(:packet) && (!data.is_a?(Meshtastic::Data) || data.portnum != :ADMIN_APP)
|
|
181
|
+
|
|
182
|
+
payload = data ? data.payload : opts[:payload]
|
|
183
|
+
raise ArgumentError, 'payload bytes or a decoded packet are required' unless payload.is_a?(String)
|
|
184
|
+
|
|
185
|
+
Meshtastic::AdminMessage.decode(payload)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
public_class_method def self.response(opts = {})
|
|
189
|
+
packet = opts[:packet]
|
|
190
|
+
packet = packet.packet if packet.is_a?(Meshtastic::FromRadio)
|
|
191
|
+
return nil unless packet.is_a?(Meshtastic::MeshPacket) && packet.decoded&.portnum == :ADMIN_APP
|
|
192
|
+
return nil if opts.key?(:request_id) && packet.decoded.request_id != opts[:request_id]
|
|
193
|
+
return nil if opts.key?(:from) && packet.from != opts[:from]
|
|
194
|
+
|
|
195
|
+
message = decode(packet: packet)
|
|
196
|
+
variant = message.payload_variant
|
|
197
|
+
return nil unless variant.to_s.end_with?('_response')
|
|
198
|
+
|
|
199
|
+
{
|
|
200
|
+
message: message, variant: variant, value: message.public_send(variant),
|
|
201
|
+
session_passkey: message.session_passkey, request_id: packet.decoded.request_id, from: packet.from
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
public_class_method def self.reboot(opts = {})
|
|
206
|
+
send(opts.merge(reboot_seconds: opts[:seconds] || 5))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
public_class_method def self.shutdown(opts = {})
|
|
210
|
+
send(opts.merge(shutdown_seconds: opts[:seconds] || 5))
|
|
26
211
|
end
|
|
27
212
|
|
|
28
|
-
def self.
|
|
29
|
-
send(opts.merge(
|
|
213
|
+
public_class_method def self.reboot_ota(opts = {})
|
|
214
|
+
send(opts.merge(reboot_ota_seconds: opts[:seconds] || 5))
|
|
30
215
|
end
|
|
31
216
|
|
|
32
|
-
def self.
|
|
33
|
-
send(opts.merge(
|
|
217
|
+
public_class_method def self.ota_request(opts = {})
|
|
218
|
+
send(opts.merge(ota_request: opts[:event]))
|
|
34
219
|
end
|
|
35
220
|
|
|
36
|
-
def self.set_owner(opts = {})
|
|
221
|
+
public_class_method def self.set_owner(opts = {})
|
|
37
222
|
user = opts[:owner] || Meshtastic::User.new(long_name: opts[:long_name], short_name: opts[:short_name])
|
|
38
223
|
send(opts.merge(set_owner: user))
|
|
39
224
|
end
|
|
40
225
|
|
|
41
|
-
def self.get_owner(opts = {})
|
|
226
|
+
public_class_method def self.get_owner(opts = {})
|
|
42
227
|
send(opts.merge(get_owner_request: true))
|
|
43
228
|
end
|
|
44
229
|
|
|
45
|
-
def self.set_channel(opts = {})
|
|
46
|
-
send(opts.merge(set_channel: opts
|
|
230
|
+
public_class_method def self.set_channel(opts = {})
|
|
231
|
+
send(opts.merge(set_channel: opts[:channel_settings] || opts[:channel_pb]))
|
|
47
232
|
end
|
|
48
233
|
|
|
49
|
-
def self.get_channel(opts = {})
|
|
50
|
-
|
|
234
|
+
public_class_method def self.get_channel(opts = {})
|
|
235
|
+
index = opts[:index]
|
|
236
|
+
index = 0 if index.nil?
|
|
237
|
+
raise ArgumentError, 'index must be an Integer from 0 through 7' unless index.is_a?(Integer) && index.between?(0, 7)
|
|
238
|
+
|
|
239
|
+
send(opts.merge(get_channel_request: index + 1))
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
public_class_method def self.get_config(opts = {})
|
|
243
|
+
send(opts.merge(get_config_request: opts[:config_type] || :DEVICE_CONFIG))
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
public_class_method def self.set_config(opts = {})
|
|
247
|
+
send(opts.merge(set_config: opts[:config]))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
public_class_method def self.get_module_config(opts = {})
|
|
251
|
+
send(opts.merge(get_module_config_request: opts[:module_config_type] || :MQTT_CONFIG))
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
public_class_method def self.set_module_config(opts = {})
|
|
255
|
+
send(opts.merge(set_module_config: opts[:module_config]))
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
public_class_method def self.get_canned_messages(opts = {})
|
|
259
|
+
send(opts.merge(get_canned_message_module_messages_request: true))
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
public_class_method def self.set_canned_messages(opts = {})
|
|
263
|
+
send(opts.merge(set_canned_message_module_messages: opts[:messages]))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
public_class_method def self.get_device_metadata(opts = {})
|
|
267
|
+
send(opts.merge(get_device_metadata_request: true))
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
public_class_method def self.get_ringtone(opts = {})
|
|
271
|
+
send(opts.merge(get_ringtone_request: true))
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
public_class_method def self.set_ringtone(opts = {})
|
|
275
|
+
send(opts.merge(set_ringtone_message: opts[:ringtone]))
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
public_class_method def self.get_device_connection_status(opts = {})
|
|
279
|
+
send(opts.merge(get_device_connection_status_request: true))
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
public_class_method def self.get_node_remote_hardware_pins(opts = {})
|
|
283
|
+
send(opts.merge(get_node_remote_hardware_pins_request: true))
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
public_class_method def self.enter_dfu(opts = {})
|
|
287
|
+
send(opts.merge(enter_dfu_mode_request: true))
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
public_class_method def self.delete_file(opts = {})
|
|
291
|
+
send(opts.merge(delete_file_request: opts[:path]))
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
public_class_method def self.set_scale(opts = {})
|
|
295
|
+
send(opts.merge(set_scale: opts[:scale]))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
public_class_method def self.backup_preferences(opts = {})
|
|
299
|
+
send(opts.merge(backup_preferences: opts[:location] || :FLASH))
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
public_class_method def self.restore_preferences(opts = {})
|
|
303
|
+
send(opts.merge(restore_preferences: opts[:location] || :FLASH))
|
|
51
304
|
end
|
|
52
305
|
|
|
53
|
-
def self.
|
|
54
|
-
send(opts.merge(
|
|
306
|
+
public_class_method def self.remove_backup_preferences(opts = {})
|
|
307
|
+
send(opts.merge(remove_backup_preferences: opts[:location] || :FLASH))
|
|
55
308
|
end
|
|
56
309
|
|
|
57
|
-
def self.
|
|
58
|
-
|
|
310
|
+
public_class_method def self.send_input_event(opts = {})
|
|
311
|
+
event = opts[:event] || Meshtastic::AdminMessage::InputEvent.new(
|
|
312
|
+
event_code: opts[:event_code].to_i,
|
|
313
|
+
kb_char: opts[:kb_char].to_i,
|
|
314
|
+
touch_x: opts[:touch_x].to_i,
|
|
315
|
+
touch_y: opts[:touch_y].to_i
|
|
316
|
+
)
|
|
317
|
+
send(opts.merge(send_input_event: event))
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
public_class_method def self.set_ham_mode(opts = {})
|
|
321
|
+
ham = opts[:ham] || Meshtastic::HamParameters.new(
|
|
322
|
+
call_sign: opts[:call_sign].to_s,
|
|
323
|
+
tx_power: opts[:tx_power].to_i,
|
|
324
|
+
frequency: opts[:frequency].to_f,
|
|
325
|
+
short_name: opts[:short_name].to_s,
|
|
326
|
+
long_name: opts[:long_name].to_s
|
|
327
|
+
)
|
|
328
|
+
send(opts.merge(set_ham_mode: ham))
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
public_class_method def self.remove_by_nodenum(opts = {})
|
|
332
|
+
send(opts.merge(remove_by_nodenum: opts[:node_num]))
|
|
59
333
|
end
|
|
60
334
|
|
|
61
|
-
def self.
|
|
62
|
-
send(opts.merge(
|
|
335
|
+
public_class_method def self.set_favorite_node(opts = {})
|
|
336
|
+
send(opts.merge(set_favorite_node: opts[:node_num]))
|
|
63
337
|
end
|
|
64
338
|
|
|
65
|
-
def self.
|
|
339
|
+
public_class_method def self.remove_favorite_node(opts = {})
|
|
340
|
+
send(opts.merge(remove_favorite_node: opts[:node_num]))
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
public_class_method def self.set_fixed_position(opts = {})
|
|
344
|
+
position = opts[:position] || Meshtastic::Position.build(lat: opts[:lat], lon: opts[:lon], altitude: opts[:altitude])
|
|
345
|
+
send(opts.merge(set_fixed_position: position))
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
public_class_method def self.remove_fixed_position(opts = {})
|
|
349
|
+
send(opts.merge(remove_fixed_position: true))
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
public_class_method def self.set_time(opts = {})
|
|
353
|
+
send(opts.merge(set_time_only: opts[:time]))
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
public_class_method def self.get_ui_config(opts = {})
|
|
357
|
+
send(opts.merge(get_ui_config_request: true))
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
public_class_method def self.store_ui_config(opts = {})
|
|
361
|
+
send(opts.merge(store_ui_config: opts[:ui_config]))
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
public_class_method def self.set_ignored_node(opts = {})
|
|
365
|
+
send(opts.merge(set_ignored_node: opts[:node_num]))
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
public_class_method def self.remove_ignored_node(opts = {})
|
|
369
|
+
send(opts.merge(remove_ignored_node: opts[:node_num]))
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
public_class_method def self.toggle_muted_node(opts = {})
|
|
373
|
+
send(opts.merge(toggle_muted_node: opts[:node_num]))
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
public_class_method def self.begin_edit(opts = {})
|
|
377
|
+
send(opts.merge(begin_edit_settings: true))
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
public_class_method def self.commit_edit(opts = {})
|
|
381
|
+
send(opts.merge(commit_edit_settings: true))
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
public_class_method def self.add_contact(opts = {})
|
|
385
|
+
send(opts.merge(add_contact: opts[:contact]))
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
public_class_method def self.key_verification(opts = {})
|
|
389
|
+
send(opts.merge(key_verification: opts[:key_verification]))
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
public_class_method def self.factory_reset_device(opts = {})
|
|
393
|
+
send(opts.merge(factory_reset_device: opts[:value] || 1))
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
public_class_method def self.factory_reset_config(opts = {})
|
|
397
|
+
send(opts.merge(factory_reset_config: opts[:value] || 1))
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
public_class_method def self.nodedb_reset(opts = {})
|
|
401
|
+
send(opts.merge(nodedb_reset: opts.fetch(:preserve_favorites, true)))
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
public_class_method def self.exit_simulator(opts = {})
|
|
405
|
+
send(opts.merge(exit_simulator: true))
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
public_class_method def self.sensor_config(opts = {})
|
|
409
|
+
send(opts.merge(sensor_config: opts[:sensor_config]))
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
public_class_method def self.lockdown_auth(opts = {})
|
|
413
|
+
send(opts.merge(lockdown_auth: opts[:lockdown_auth]))
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
public_class_method def self.authors
|
|
66
417
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
|
|
67
418
|
end
|
|
68
419
|
|
|
69
|
-
def self.help
|
|
420
|
+
public_class_method def self.help
|
|
70
421
|
puts "USAGE:
|
|
71
|
-
#
|
|
72
|
-
#{self}.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
#
|
|
422
|
+
# Encode exactly one AdminMessage payload without modifying the caller.
|
|
423
|
+
#{self}.encode(
|
|
424
|
+
message: 'optional - existing AdminMessage to copy instead of a new one',
|
|
425
|
+
session_passkey: 'optional - bytes from a prior get_*_response to authorize sets'
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Send an AdminMessage on ADMIN_APP over a connected transport.
|
|
429
|
+
#{self}.send(
|
|
430
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
|
|
431
|
+
bluetooth_obj: 'optional - BLE handle from Meshtastic::Bluetooth.connect',
|
|
432
|
+
tcp_obj: 'optional - TCP handle from Meshtastic::TCP.connect',
|
|
433
|
+
mqtt_obj: 'optional - MQTT client from Meshtastic::MQTT.connect',
|
|
434
|
+
to: 'optional - unicast node integer or !eighthex; defaults to connected my_node_num; required for MQTT',
|
|
435
|
+
from: 'optional - sender number; radio default zero denotes the local PhoneAPI client',
|
|
436
|
+
channel: 'optional - routing channel index (default zero)',
|
|
437
|
+
want_ack: 'optional - request routing acknowledgment, distinct from an admin reply',
|
|
438
|
+
hop_limit: 'optional - maximum radio hop count',
|
|
439
|
+
psks: 'optional - MQTT channel key map; not public-key admin authorization',
|
|
440
|
+
pki_encrypted: 'optional - enable device-owned PKI on serial BLE or TCP; unsupported over MQTT',
|
|
441
|
+
public_key: 'optional - recipient public-key bytes for radio PKI',
|
|
442
|
+
last_packet_id: 'optional - previous packet number used by transport packet generation',
|
|
443
|
+
want_response: 'optional - defaults true for get requests and false for state changes',
|
|
444
|
+
auto_session: 'optional - acquire a session for remote writes unless a passkey is supplied (default true)',
|
|
445
|
+
refresh_session: 'optional - discard cached session for this automatic acquisition (default false)',
|
|
446
|
+
timeout: 'optional - positive finite receive deadline in seconds for session acquisition (default ten)'
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Send admin data and synchronously await correlated readback.
|
|
450
|
+
#{self}.request(
|
|
451
|
+
request_id: 'optional - packet ID from 2 through 0xffffffff; generated when omitted; accepts send options',
|
|
452
|
+
wait: 'optional - false returns only request_id and submission result; true waits for reply (default true)',
|
|
453
|
+
timeout: 'optional - positive finite total response budget in seconds including session acquisition (default ten)'
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Decode protobuf bytes or an incoming admin packet.
|
|
457
|
+
#{self}.decode(
|
|
458
|
+
payload: 'optional - serialized AdminMessage bytes when packet is omitted',
|
|
459
|
+
packet: 'optional - FromRadio MeshPacket or Data containing decoded ADMIN_APP bytes'
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Extract a matching response and its session passkey.
|
|
463
|
+
#{self}.response(
|
|
464
|
+
packet: 'required - incoming FromRadio or MeshPacket protobuf',
|
|
465
|
+
request_id: 'optional - require this Data.request_id to match the outgoing packet ID',
|
|
466
|
+
from: 'optional - require this numeric responding node; mismatches return nil'
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Request legacy ESP32 OTA reboot; deprecated firmware operation.
|
|
470
|
+
#{self}.reboot_ota(
|
|
471
|
+
seconds: 'optional - legacy reboot delay, negative cancels (default five); use ota_request on current firmware'
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Send the current OTA loader request protobuf.
|
|
475
|
+
#{self}.ota_request(
|
|
476
|
+
event: 'required - AdminMessage::OTAEvent containing mode and firmware hash; see Admin::Firmware'
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Reboot the node after a delay.
|
|
480
|
+
#{self}.reboot(
|
|
481
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
|
|
482
|
+
seconds: 'optional - delay before reboot in seconds (default: 5)'
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Shut down the node after a delay.
|
|
486
|
+
#{self}.shutdown(
|
|
487
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
|
|
488
|
+
seconds: 'optional - delay before shutdown in seconds (default: 5)'
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Set the node owner User protobuf.
|
|
492
|
+
#{self}.set_owner(
|
|
493
|
+
owner: 'optional - Meshtastic::User protobuf to send as set_owner',
|
|
494
|
+
long_name: 'optional - owner long name when owner is omitted',
|
|
495
|
+
short_name: 'optional - owner short name when owner is omitted'
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Request the node owner User protobuf.
|
|
499
|
+
#{self}.get_owner(
|
|
500
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Write a Channel protobuf to the node.
|
|
504
|
+
#{self}.set_channel(
|
|
505
|
+
channel_settings: 'optional - Meshtastic::Channel protobuf to write',
|
|
506
|
+
channel_pb: 'optional - Channel protobuf alias used as set_channel'
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Request a channel by index.
|
|
510
|
+
#{self}.get_channel(
|
|
511
|
+
index: 'optional - zero-based channel index 0 through 7; wire conversion happens here (default zero)'
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Request a radio Config section.
|
|
515
|
+
#{self}.get_config(
|
|
516
|
+
config_type: 'optional - AdminMessage::ConfigType such as :LORA_CONFIG'
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Write a Config protobuf to the node.
|
|
520
|
+
#{self}.set_config(
|
|
521
|
+
config: 'required - Meshtastic::Config protobuf to write'
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Request a ModuleConfig section.
|
|
525
|
+
#{self}.get_module_config(
|
|
526
|
+
module_config_type: 'optional - ModuleConfigType such as :MQTT_CONFIG'
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Write a ModuleConfig protobuf to the node.
|
|
530
|
+
#{self}.set_module_config(
|
|
531
|
+
module_config: 'required - Meshtastic::ModuleConfig protobuf to write'
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Request canned-message module strings.
|
|
535
|
+
#{self}.get_canned_messages(
|
|
536
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Set canned-message module strings.
|
|
540
|
+
#{self}.set_canned_messages(
|
|
541
|
+
messages: 'required - pipe-separated canned message text; empty string clears it'
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
# Request DeviceMetadata (firmware version and hardware).
|
|
545
|
+
#{self}.get_device_metadata(
|
|
546
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# Request the RTTTL ringtone string.
|
|
550
|
+
#{self}.get_ringtone(
|
|
551
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Set the RTTTL ringtone string.
|
|
555
|
+
#{self}.set_ringtone(
|
|
556
|
+
ringtone: 'required - RTTTL ringtone text to store on the node'
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Request DeviceConnectionStatus.
|
|
560
|
+
#{self}.get_device_connection_status(
|
|
561
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Request remote-hardware pin definitions.
|
|
565
|
+
#{self}.get_node_remote_hardware_pins(
|
|
566
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Ask the node to enter DFU / UF2 mode.
|
|
570
|
+
#{self}.enter_dfu(
|
|
571
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# Delete a file on the node filesystem.
|
|
575
|
+
#{self}.delete_file(
|
|
576
|
+
path: 'required - filesystem path on the node to delete'
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Set the NAU7802 scale calibration value.
|
|
580
|
+
#{self}.set_scale(
|
|
581
|
+
scale: 'required - integer scale factor for the NAU7802 module'
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Backup preferences to flash or SD.
|
|
585
|
+
#{self}.backup_preferences(
|
|
586
|
+
location: 'optional - :FLASH or :SD backup location (default: :FLASH)'
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Restore preferences from flash or SD.
|
|
590
|
+
#{self}.restore_preferences(
|
|
591
|
+
location: 'optional - :FLASH or :SD backup location (default: :FLASH)'
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# Remove a preferences backup from flash or SD.
|
|
595
|
+
#{self}.remove_backup_preferences(
|
|
596
|
+
location: 'optional - :FLASH or :SD backup location (default: :FLASH)'
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Inject a device-UI input event.
|
|
600
|
+
#{self}.send_input_event(
|
|
601
|
+
event: 'optional - AdminMessage::InputEvent protobuf to send',
|
|
602
|
+
event_code: 'optional - input event code when event is omitted',
|
|
603
|
+
kb_char: 'optional - keyboard character code when event is omitted',
|
|
604
|
+
touch_x: 'optional - touch X coordinate when event is omitted',
|
|
605
|
+
touch_y: 'optional - touch Y coordinate when event is omitted'
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# Enable ham-radio identity parameters.
|
|
609
|
+
#{self}.set_ham_mode(
|
|
610
|
+
ham: 'optional - HamParameters protobuf to send',
|
|
611
|
+
call_sign: 'optional - amateur callsign when ham is omitted',
|
|
612
|
+
tx_power: 'optional - transmit power when ham is omitted',
|
|
613
|
+
frequency: 'optional - frequency in MHz when ham is omitted',
|
|
614
|
+
short_name: 'optional - short name when ham is omitted',
|
|
615
|
+
long_name: 'optional - long name when ham is omitted'
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# Remove a node from the local nodedb by number.
|
|
619
|
+
#{self}.remove_by_nodenum(
|
|
620
|
+
node_num: 'required - node number to remove from the nodedb'
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Mark a node as a favorite.
|
|
624
|
+
#{self}.set_favorite_node(
|
|
625
|
+
node_num: 'required - node number to mark as favorite'
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Clear favorite status for a node.
|
|
629
|
+
#{self}.remove_favorite_node(
|
|
630
|
+
node_num: 'required - node number to unfavorite'
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Set a fixed GPS position on the node.
|
|
634
|
+
#{self}.set_fixed_position(
|
|
635
|
+
position: 'optional - Meshtastic::Position protobuf to store',
|
|
636
|
+
lat: 'optional - latitude in decimal degrees when position is omitted',
|
|
637
|
+
lon: 'optional - longitude in decimal degrees when position is omitted',
|
|
638
|
+
altitude: 'optional - altitude in meters when position is omitted'
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Clear the fixed GPS position on the node.
|
|
642
|
+
#{self}.remove_fixed_position(
|
|
643
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# Set node wall-clock time.
|
|
647
|
+
#{self}.set_time(
|
|
648
|
+
time: 'required - unix timestamp to set on the node'
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Request DeviceUIConfig.
|
|
652
|
+
#{self}.get_ui_config(
|
|
653
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Store DeviceUIConfig on the node.
|
|
657
|
+
#{self}.store_ui_config(
|
|
658
|
+
ui_config: 'required - Meshtastic::DeviceUIConfig protobuf to store'
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
# Ignore a node number.
|
|
662
|
+
#{self}.set_ignored_node(
|
|
663
|
+
node_num: 'required - node number to ignore'
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Stop ignoring a node number.
|
|
667
|
+
#{self}.remove_ignored_node(
|
|
668
|
+
node_num: 'required - node number to stop ignoring'
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# Toggle mute for a node number.
|
|
672
|
+
#{self}.toggle_muted_node(
|
|
673
|
+
node_num: 'required - node number whose mute flag should toggle'
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# Open a settings edit transaction.
|
|
677
|
+
#{self}.begin_edit(
|
|
678
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Commit a settings edit transaction.
|
|
682
|
+
#{self}.commit_edit(
|
|
683
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
# Add a SharedContact to the node.
|
|
687
|
+
#{self}.add_contact(
|
|
688
|
+
contact: 'required - Meshtastic::SharedContact protobuf to add'
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
# Send a KeyVerificationAdmin protobuf.
|
|
692
|
+
#{self}.key_verification(
|
|
693
|
+
key_verification: 'required - KeyVerificationAdmin protobuf to send'
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Factory-reset device state including BLE bonds.
|
|
697
|
+
#{self}.factory_reset_device(
|
|
698
|
+
value: 'optional - int32 factory_reset_device field (default: 1)'
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# Factory-reset configuration only.
|
|
702
|
+
#{self}.factory_reset_config(
|
|
703
|
+
value: 'optional - int32 factory_reset_config field (default: 1)'
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# Clear the node database.
|
|
707
|
+
#{self}.nodedb_reset(
|
|
708
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect',
|
|
709
|
+
preserve_favorites: 'optional - retain favorite nodes (default true); some firmware roles always preserve favorites'
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Exit the firmware simulator if running.
|
|
713
|
+
#{self}.exit_simulator(
|
|
714
|
+
serial_obj: 'optional - serial handle from Meshtastic::Serial.connect'
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
# Write a SensorConfig protobuf.
|
|
718
|
+
#{self}.sensor_config(
|
|
719
|
+
sensor_config: 'required - Meshtastic::SensorConfig protobuf to write'
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
# Send lockdown authentication material.
|
|
723
|
+
#{self}.lockdown_auth(
|
|
724
|
+
lockdown_auth: 'required - Meshtastic::LockdownAuth protobuf to send'
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
# Print the AUTHOR(S) string for this module.
|
|
78
728
|
#{self}.authors
|
|
79
729
|
"
|
|
80
730
|
end
|
|
81
731
|
end
|
|
82
732
|
end
|
|
733
|
+
|
|
734
|
+
require 'meshtastic/admin/firmware'
|
|
735
|
+
require 'meshtastic/admin/channel'
|
|
736
|
+
require 'meshtastic/admin/config'
|