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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/documentation/README.md +3 -2
  4. data/documentation/admin-channel.md +64 -0
  5. data/documentation/admin-config.md +59 -0
  6. data/documentation/admin-firmware-nordic.md +86 -0
  7. data/documentation/admin-firmware-serial.md +180 -0
  8. data/documentation/admin-firmware.md +126 -0
  9. data/documentation/admin.md +88 -35
  10. data/documentation/atak.md +93 -10
  11. data/documentation/channel.md +1 -28
  12. data/documentation/config.md +1 -27
  13. data/documentation/mesh-interface.md +7 -0
  14. data/documentation/meshtastic.md +2 -2
  15. data/lib/meshtastic/admin/channel.rb +164 -0
  16. data/lib/meshtastic/admin/config.rb +213 -0
  17. data/lib/meshtastic/admin/firmware/ble.rb +207 -0
  18. data/lib/meshtastic/admin/firmware/nordic_dfu.rb +218 -0
  19. data/lib/meshtastic/admin/firmware/serial_bootloader.rb +233 -0
  20. data/lib/meshtastic/admin/firmware.rb +288 -0
  21. data/lib/meshtastic/admin.rb +688 -34
  22. data/lib/meshtastic/apponly.rb +19 -7
  23. data/lib/meshtastic/atak.rb +295 -17
  24. data/lib/meshtastic/bluetooth/bluez.rb +20 -1
  25. data/lib/meshtastic/bluetooth.rb +70 -59
  26. data/lib/meshtastic/cannedmessages.rb +13 -6
  27. data/lib/meshtastic/channel.rb +3 -25
  28. data/lib/meshtastic/clientonly.rb +19 -7
  29. data/lib/meshtastic/config.rb +3 -24
  30. data/lib/meshtastic/config_pb.rb +2 -1
  31. data/lib/meshtastic/connection_status.rb +12 -5
  32. data/lib/meshtastic/deviceonly.rb +19 -7
  33. data/lib/meshtastic/localonly.rb +19 -7
  34. data/lib/meshtastic/mesh_interface.rb +19 -0
  35. data/lib/meshtastic/module_config.rb +13 -7
  36. data/lib/meshtastic/mqtt.rb +72 -35
  37. data/lib/meshtastic/paxcount.rb +17 -6
  38. data/lib/meshtastic/portnums.rb +13 -6
  39. data/lib/meshtastic/position.rb +16 -6
  40. data/lib/meshtastic/remote_hardware.rb +27 -11
  41. data/lib/meshtastic/rtttl.rb +17 -8
  42. data/lib/meshtastic/serial.rb +81 -70
  43. data/lib/meshtastic/storeforward.rb +13 -6
  44. data/lib/meshtastic/storeforward_pb.rb +1 -1
  45. data/lib/meshtastic/stream_interface.rb +11 -0
  46. data/lib/meshtastic/tcp.rb +73 -22
  47. data/lib/meshtastic/telemetry.rb +15 -6
  48. data/lib/meshtastic/traceroute.rb +15 -6
  49. data/lib/meshtastic/util.rb +3 -1
  50. data/lib/meshtastic/version.rb +1 -1
  51. data/lib/meshtastic/xmodem.rb +12 -5
  52. data/lib/meshtastic.rb +22 -3
  53. data/spec/conventions_spec.rb +321 -0
  54. data/spec/lib/meshtastic/admin/channel_spec.rb +202 -0
  55. data/spec/lib/meshtastic/admin/config_spec.rb +114 -0
  56. data/spec/lib/meshtastic/admin/firmware/ble_spec.rb +170 -0
  57. data/spec/lib/meshtastic/admin/firmware/nordic_dfu_spec.rb +248 -0
  58. data/spec/lib/meshtastic/admin/firmware/serial_bootloader_spec.rb +263 -0
  59. data/spec/lib/meshtastic/admin/firmware_spec.rb +315 -0
  60. data/spec/lib/meshtastic/admin_spec.rb +412 -1
  61. data/spec/lib/meshtastic/apponly_spec.rb +1 -1
  62. data/spec/lib/meshtastic/atak_spec.rb +129 -4
  63. data/spec/lib/meshtastic/bluetooth_spec.rb +1 -1
  64. data/spec/lib/meshtastic/channel_spec.rb +2 -20
  65. data/spec/lib/meshtastic/config_spec.rb +2 -20
  66. data/spec/lib/meshtastic/connection_status_spec.rb +1 -1
  67. data/spec/lib/meshtastic/deviceonly_spec.rb +1 -1
  68. data/spec/lib/meshtastic/localonly_spec.rb +1 -1
  69. data/spec/lib/meshtastic/mesh_interface_spec.rb +31 -0
  70. data/spec/lib/meshtastic/portnums_spec.rb +2 -2
  71. metadata +24 -6
@@ -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
- def self.encode(opts = {})
8
- message = opts[:message] || Meshtastic::AdminMessage.new
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
- message.public_send("#{key}=", value)
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: opts.fetch(:want_response, true)
71
+ want_response: want_response
24
72
  )
25
- Meshtastic.deliver_data(opts.merge(data: data, port_num: Meshtastic::PortNum::ADMIN_APP))
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.reboot(opts = {})
29
- send(opts.merge(reboot_seconds: opts.fetch(:seconds, 5)))
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.shutdown(opts = {})
33
- send(opts.merge(shutdown_seconds: opts.fetch(:seconds, 5)))
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.fetch(:channel_settings)))
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
- send(opts.merge(get_channel_request: opts.fetch(:index, 0)))
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.get_config(opts = {})
54
- send(opts.merge(get_config_request: opts.fetch(:config_type, :DEVICE_CONFIG)))
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.set_config(opts = {})
58
- send(opts.merge(set_config: opts.fetch(:config)))
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.nodedb_reset(opts = {})
62
- send(opts.merge(nodedb_reset: true))
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.authors
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
- #{self}.reboot(serial_obj: serial_obj, seconds: 5)
72
- #{self}.shutdown(serial_obj: serial_obj, seconds: 5)
73
- #{self}.get_owner(serial_obj: serial_obj)
74
- #{self}.set_owner(serial_obj: serial_obj, long_name: 'Node', short_name: 'N1')
75
- #{self}.get_channel(serial_obj: serial_obj, index: 0)
76
- #{self}.get_config(serial_obj: serial_obj, config_type: :LORA_CONFIG)
77
- #{self}.nodedb_reset(serial_obj: serial_obj)
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'