meshtastic 0.0.183 → 0.0.184

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.
@@ -0,0 +1,560 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'base64'
5
+ require 'uri'
6
+ require 'stringio'
7
+ require 'meshtastic/clientonly_pb'
8
+ require 'meshtastic/admin/channel'
9
+ require 'meshtastic/admin'
10
+
11
+ module Meshtastic
12
+ module Admin
13
+ # Host-side snapshots, not firmware filesystem backup commands.
14
+ module Backup
15
+ FORMAT = 'meshtastic-admin-backup'
16
+ WARNING = 'Contains secrets returned by firmware. Firmware may redact secrets; absent/default values are not proof of completeness. Owner identity and hardware metadata are excluded. ACKs do not prove persistence.'
17
+ CONFIG_TYPES = %i[DEVICE_CONFIG POSITION_CONFIG POWER_CONFIG NETWORK_CONFIG DISPLAY_CONFIG LORA_CONFIG BLUETOOTH_CONFIG SECURITY_CONFIG].freeze
18
+ MODULE_TYPES = %i[MQTT_CONFIG SERIAL_CONFIG EXTNOTIF_CONFIG STOREFORWARD_CONFIG RANGETEST_CONFIG TELEMETRY_CONFIG CANNEDMSG_CONFIG AUDIO_CONFIG REMOTEHARDWARE_CONFIG NEIGHBORINFO_CONFIG AMBIENTLIGHTING_CONFIG DETECTIONSENSOR_CONFIG PAXCOUNTER_CONFIG STATUSMESSAGE_CONFIG TRAFFICMANAGEMENT_CONFIG TAK_CONFIG MESHBEACON_CONFIG].freeze
19
+ OWNER_FIELDS = %w[long_name short_name is_licensed].freeze
20
+
21
+ # JSON 2.x otherwise silently keeps the last duplicate key.
22
+ class StrictObject < Hash
23
+ def []=(key, value)
24
+ raise ArgumentError, 'duplicate JSON key' if key?(key)
25
+
26
+ super
27
+ end
28
+ end
29
+ private_constant :StrictObject
30
+
31
+ public_class_method def self.export(opts = {})
32
+ validate_options(options: opts, operation: :export)
33
+ connection = connection_options(opts)
34
+ return export_profile(opts.merge(connection: connection)) if opts.fetch(:format, :json) == :device_profile
35
+
36
+ records = []
37
+ selection(opts).each do |section, slot|
38
+ value = Admin.request(connection.merge(read_command(section: section, slot: slot)))[:value]
39
+ value = Meshtastic::User.new(long_name: value.long_name, short_name: value.short_name, is_licensed: value.is_licensed) if section == 'owner'
40
+ json = JSON.parse(value.class.encode_json(value, emit_defaults: true, preserve_proto_fieldnames: true))
41
+ json.select! { |key, _| OWNER_FIELDS.include?(key) } if section == 'owner'
42
+ records << { 'section' => section, 'slot' => slot, 'value' => json }
43
+ end
44
+ document = { 'format' => FORMAT, 'version' => 1, 'warning' => WARNING, 'records' => records }
45
+ validate_document(document: document)
46
+ if opts[:path]
47
+ File.open(opts[:path], File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
48
+ file.chmod(0o600)
49
+ file.write(JSON.pretty_generate(document))
50
+ file.flush
51
+ file.fsync
52
+ end
53
+ end
54
+ { status: :exported, count: records.length, backup: document, warnings: [WARNING] }
55
+ end
56
+
57
+ private_class_method def self.export_profile(opts = {})
58
+ selected = selection(opts)
59
+ raise ArgumentError, 'DeviceProfile cannot represent UI configuration' if opts.fetch(:include_ui, false)
60
+
61
+ indexes = opts.fetch(:channel_indexes, (0..7).to_a)
62
+ raise ArgumentError, 'DeviceProfile channels must be an ordered contiguous prefix starting at zero' unless indexes == (0...indexes.length).to_a
63
+ raise ArgumentError, 'DeviceProfile selection is empty' if selected.empty? && !opts.key?(:fixed_position)
64
+
65
+ profile = Meshtastic::DeviceProfile.new
66
+ profile.fixed_position = profile_position(value: opts[:fixed_position]) if opts.key?(:fixed_position)
67
+ channels = []
68
+ selected.each do |section, slot|
69
+ value = Admin.request(opts[:connection].merge(read_command(section: section, slot: slot)))[:value]
70
+ case section
71
+ when 'owner'
72
+ OWNER_FIELDS.each { |name| profile[name] = value[name] }
73
+ profile.is_unmessagable = value.is_unmessagable if value.has_is_unmessagable?
74
+ when 'config', 'module_config'
75
+ klass, types, container = section == 'config' ? [Meshtastic::Config, CONFIG_TYPES, Meshtastic::LocalConfig] : [Meshtastic::ModuleConfig, MODULE_TYPES, Meshtastic::LocalModuleConfig]
76
+ field = klass.descriptor.to_a[types.index(slot.to_sym)].name
77
+ raise ArgumentError, 'protobuf section does not match slot' unless value.payload_variant.to_s == field
78
+
79
+ profile[section] ||= container.new
80
+ profile[section][field] = value[field]
81
+ when 'channel'
82
+ raise ArgumentError, 'channel index or role cannot round-trip in DeviceProfile' unless value.index == slot && value.role == (slot.zero? ? :PRIMARY : :SECONDARY) && value.settings
83
+
84
+ channels << value
85
+ when 'ringtone', 'canned_messages'
86
+ profile[section] = value
87
+ end
88
+ end
89
+ profile.channel_url = Admin::Channel.export_url(channels: channels, lora_config: profile.config&.lora) unless channels.empty?
90
+ bytes = profile.to_proto.b
91
+ count = profile_plan(bytes: bytes).length
92
+ if opts[:path]
93
+ File.open(opts[:path], File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
94
+ file.binmode
95
+ file.chmod(0o600)
96
+ file.write(bytes)
97
+ file.flush
98
+ file.fsync
99
+ end
100
+ end
101
+ { status: :exported, format: :device_profile, count: count, backup: bytes, warnings: [WARNING] }
102
+ end
103
+
104
+ private_class_method def self.profile_position(opts = {})
105
+ value = opts[:value]
106
+ raise ArgumentError unless value.is_a?(Hash) || value.is_a?(Meshtastic::Position)
107
+
108
+ value = Meshtastic::Position.new(value) if value.is_a?(Hash)
109
+ validate_wire(bytes: value.to_proto, descriptor: Meshtastic::Position.descriptor)
110
+ Meshtastic::Position.decode(value.to_proto)
111
+ rescue ArgumentError, TypeError, RangeError, Google::Protobuf::Error
112
+ raise ArgumentError, 'fixed_position must be a valid Position protobuf or field Hash', cause: nil
113
+ end
114
+
115
+ public_class_method def self.import(opts = {})
116
+ validate_options(options: opts, operation: :import)
117
+ connection = connection_options(opts)
118
+ raise ArgumentError, 'supply exactly one of path or backup' unless opts.key?(:path) ^ opts.key?(:backup)
119
+
120
+ document = opts[:backup]
121
+ if opts.key?(:path)
122
+ document = File.open(opts[:path], File::RDONLY | File::NOFOLLOW) do |file|
123
+ raise ArgumentError, 'backup path must be a regular file' unless file.stat.file?
124
+
125
+ file.binmode
126
+ file.read
127
+ end
128
+ end
129
+ entries = import_plan(document: document, format: opts.fetch(:format, :auto), path: opts[:path])
130
+ plan = entries.each_with_index.sort_by do |entry, index|
131
+ priority = if entry[:section] == 'config'
132
+ { 'LORA_CONFIG' => 20, 'BLUETOOTH_CONFIG' => 21, 'NETWORK_CONFIG' => 22, 'SECURITY_CONFIG' => 23 }.fetch(entry[:slot], 0)
133
+ else
134
+ entry[:section] == 'channel' ? 10 : 0
135
+ end
136
+ [priority, index]
137
+ end.map(&:first)
138
+ report = { status: :dry_run, planned: plan.length, attempted: 0, acknowledged: 0, readback_confirmed: 0, persistence_verified: false,
139
+ transaction: :not_requested, records: [], plan: plan.map { |entry| entry.slice(:section, :slot) }, warnings: [WARNING] }
140
+ return report if opts.fetch(:dry_run, false)
141
+
142
+ failure = nil
143
+ if opts.fetch(:edit_transaction, false) && !plan.empty?
144
+ failure = { operation: :begin_edit_settings }
145
+ report[:transaction] = :begin_uncertain
146
+ Admin.request(connection.merge(begin_edit_settings: true))
147
+ report[:transaction] = :open
148
+ end
149
+ plan.each do |entry|
150
+ field = { 'owner' => :set_owner, 'config' => :set_config, 'module_config' => :set_module_config,
151
+ 'channel' => :set_channel, 'ui' => :store_ui_config, 'fixed_position' => :set_fixed_position,
152
+ 'ringtone' => :set_ringtone_message, 'canned_messages' => :set_canned_message_module_messages }.fetch(entry[:section])
153
+ failure = entry.slice(:section, :slot).merge(operation: field)
154
+ report[:attempted] += 1
155
+ status = apply_entry(connection: connection, field: field, entry: entry, failure: failure)
156
+ report[status] += 1
157
+ report[:records] << entry.slice(:section, :slot).merge(status: status)
158
+ end
159
+ if report[:transaction] == :open
160
+ failure = { operation: :commit_edit_settings }
161
+ report[:transaction] = :commit_uncertain
162
+ Admin.request(connection.merge(commit_edit_settings: true))
163
+ report[:transaction] = :commit_acknowledged
164
+ end
165
+ report[:status] = report[:readback_confirmed].positive? ? :applied : :acknowledged
166
+ verify_readback(connection: connection, plan: plan, report: report) if opts.fetch(:verify, false)
167
+ report
168
+ rescue StandardError => e
169
+ raise unless failure
170
+
171
+ report[:status] = :partial_failure
172
+ report[:failure] = failure.merge(error: e.class.name)
173
+ report[:failure][:reason] = e.reason if e.is_a?(Admin::RoutingError)
174
+ report
175
+ end
176
+
177
+ # A timed-out mutation is never resent; only a fresh getter can resolve it.
178
+ private_class_method def self.apply_entry(opts = {})
179
+ entry = opts[:entry]
180
+ Admin.request(opts[:connection].merge(opts[:field] => entry[:message]))
181
+ :acknowledged
182
+ rescue Timeout::Error
183
+ command = read_command(entry)
184
+ opts[:failure][:readback] = :unsupported
185
+ raise unless command
186
+
187
+ opts[:failure][:readback] = :failed
188
+ actual = Admin.request(opts[:connection].merge(command))[:value]
189
+ opts[:failure][:readback] = :mismatch
190
+ raise unless readback_matches?(entry: entry, actual: actual)
191
+
192
+ :readback_confirmed
193
+ end
194
+
195
+ private_class_method def self.import_plan(opts = {})
196
+ document = opts[:document]
197
+ format = opts[:format]
198
+ if format == :auto
199
+ format = :json if document.is_a?(Hash) || File.extname(opts[:path].to_s).downcase == '.json'
200
+ format = :device_profile if File.extname(opts[:path].to_s).downcase == '.cfg'
201
+ end
202
+ return profile_plan(bytes: document) if format == :device_profile
203
+
204
+ if format == :auto
205
+ begin
206
+ return profile_plan(bytes: document)
207
+ rescue ArgumentError
208
+ raise unless document.is_a?(String) && document.b.lstrip.start_with?('{', '[')
209
+ end
210
+ end
211
+ document = JSON.parse(document, object_class: StrictObject, allow_duplicate_key: false) if document.is_a?(String)
212
+ validate_document(document: document)
213
+ rescue JSON::ParserError
214
+ raise ArgumentError, 'invalid backup JSON', cause: nil
215
+ end
216
+
217
+ # Protobuf decoding alone silently accepts unknown tags and last-wins duplicates.
218
+ private_class_method def self.validate_wire(opts = {})
219
+ bytes = opts[:bytes]
220
+ descriptor = opts[:descriptor]
221
+ depth = opts.fetch(:depth, 0)
222
+ raise ArgumentError, 'invalid protobuf size or nesting' unless bytes.is_a?(String) && bytes.bytesize <= 1_048_576 && depth <= 32
223
+
224
+ stream = StringIO.new(bytes.b)
225
+ fields = descriptor.to_h { |field| [field.number, field] }
226
+ oneofs = {}
227
+ descriptor.each_oneof { |oneof| oneof.each { |field| oneofs[field.number] = oneof.name } }
228
+ seen = []
229
+ until stream.eof?
230
+ tag = wire_varint(stream: stream)
231
+ field = fields[tag >> 3]
232
+ raise ArgumentError, 'unknown protobuf field' unless field
233
+
234
+ identity = oneofs.fetch(field.number, field.number)
235
+ raise ArgumentError, 'duplicate protobuf field or conflicting oneof' if field.label != :repeated && seen.include?(identity)
236
+
237
+ seen << identity
238
+ wire = tag & 7
239
+ expected = case field.type
240
+ when :double, :fixed64, :sfixed64 then 1
241
+ when :string, :bytes, :message then 2
242
+ when :float, :fixed32, :sfixed32 then 5
243
+ else 0
244
+ end
245
+ packed = field.label == :repeated && wire == 2 && expected != 2
246
+ raise ArgumentError, 'invalid protobuf wire type' unless wire == expected || packed
247
+
248
+ if wire == 2
249
+ length = wire_varint(stream: stream)
250
+ raise ArgumentError, 'truncated protobuf field' if length > stream.size - stream.pos
251
+
252
+ payload = stream.read(length)
253
+ if field.type == :message
254
+ validate_wire(bytes: payload, descriptor: field.subtype, depth: depth + 1)
255
+ elsif packed
256
+ packed_stream = StringIO.new(payload)
257
+ wire_scalar(stream: packed_stream, field: field, wire: expected) until packed_stream.eof?
258
+ end
259
+ else
260
+ wire_scalar(stream: stream, field: field, wire: wire)
261
+ end
262
+ end
263
+ end
264
+
265
+ private_class_method def self.wire_varint(opts = {})
266
+ value = 0
267
+ 10.times do |index|
268
+ byte = opts[:stream].getbyte
269
+ raise ArgumentError, 'invalid protobuf varint' if byte.nil? || (index == 9 && byte > 1)
270
+
271
+ value |= (byte & 127) << (index * 7)
272
+ next unless byte < 128
273
+
274
+ raise ArgumentError, 'noncanonical protobuf varint' if index.positive? && byte.zero?
275
+
276
+ return value
277
+ end
278
+ raise ArgumentError, 'invalid protobuf varint'
279
+ end
280
+
281
+ private_class_method def self.wire_scalar(opts = {})
282
+ stream = opts[:stream]
283
+ field = opts[:field]
284
+ if opts[:wire].zero?
285
+ value = wire_varint(stream: stream)
286
+ invalid = field.type == :bool && value > 1
287
+ invalid ||= %i[uint32 sint32].include?(field.type) && value > 0xffffffff
288
+ invalid ||= %i[int32 enum].include?(field.type) && value > 0x7fffffff && value < 0xffffffff80000000
289
+ if field.type == :enum
290
+ signed = value >= 0x8000000000000000 ? value - 0x10000000000000000 : value
291
+ invalid ||= field.subtype.lookup_value(signed).nil?
292
+ end
293
+ raise ArgumentError, 'invalid protobuf scalar or unknown enum' if invalid
294
+ else
295
+ length = opts[:wire] == 1 ? 8 : 4
296
+ bytes = stream.read(length)
297
+ raise ArgumentError, 'truncated protobuf scalar' unless bytes && bytes.bytesize == length
298
+ raise ArgumentError, 'nonfinite protobuf float' if %i[float double].include?(field.type) && !bytes.unpack1(length == 4 ? 'e' : 'E').finite?
299
+ end
300
+ end
301
+
302
+ private_class_method def self.profile_plan(opts = {})
303
+ validate_wire(bytes: opts[:bytes], descriptor: Meshtastic::DeviceProfile.descriptor)
304
+ profile = Meshtastic::DeviceProfile.decode(opts[:bytes])
305
+ entries = []
306
+ owner = (OWNER_FIELDS + ['is_unmessagable']).select { |name| profile.public_send("has_#{name}?") }
307
+ unless owner.empty?
308
+ values = owner.to_h { |name| [name.to_sym, profile[name]] }
309
+ entries << { section: 'owner', slot: nil, message: Meshtastic::User.new(values), owner_fields: owner }
310
+ end
311
+ %w[fixed_position ringtone canned_messages].each do |name|
312
+ entries << { section: name, slot: nil, message: profile[name] } if profile.public_send("has_#{name}?")
313
+ end
314
+ [[profile.config, Meshtastic::Config, CONFIG_TYPES, 'config'],
315
+ [profile.module_config, Meshtastic::ModuleConfig, MODULE_TYPES, 'module_config']].each do |local, klass, types, section|
316
+ next unless local
317
+
318
+ klass.descriptor.to_a.take(types.length).each_with_index do |field, index|
319
+ value = local[field.name]
320
+ next unless value
321
+
322
+ entries << { section: section, slot: types[index].to_s, message: klass.new(field.name.to_sym => value) }
323
+ end
324
+ end
325
+ if profile.has_channel_url?
326
+ uri = URI.parse(profile.channel_url)
327
+ raise ArgumentError, 'channel URL queries are unsupported' if uri.query
328
+
329
+ channel_set = Admin::Channel.import_url(url: profile.channel_url)
330
+ payload = Base64.urlsafe_decode64(uri.fragment)
331
+ raise ArgumentError, 'noncanonical channel URL base64' unless Base64.urlsafe_encode64(payload, padding: false) == uri.fragment.delete_suffix('==').delete_suffix('=')
332
+
333
+ validate_wire(bytes: payload, descriptor: Meshtastic::ChannelSet.descriptor)
334
+ channel_set.settings.each_with_index do |settings, index|
335
+ channel = Admin::Channel.build(index: index, role: index.zero? ? :PRIMARY : :SECONDARY, settings: settings)
336
+ entries << { section: 'channel', slot: index, message: channel }
337
+ end
338
+ if channel_set.lora_config
339
+ lora = entries.find { |entry| entry[:section] == 'config' && entry[:slot] == 'LORA_CONFIG' }
340
+ message = Meshtastic::Config.new(lora: channel_set.lora_config)
341
+ raise ArgumentError, 'conflicting profile and channel URL LoRa configuration' if lora && lora[:message] != message
342
+
343
+ entries << { section: 'config', slot: 'LORA_CONFIG', message: message } unless lora
344
+ end
345
+ end
346
+ raise ArgumentError, 'DeviceProfile contains no restorable fields' if entries.empty?
347
+
348
+ entries
349
+ rescue Google::Protobuf::ParseError, TypeError, RangeError, URI::InvalidURIError
350
+ raise ArgumentError, 'invalid DeviceProfile protobuf or channel URL', cause: nil
351
+ end
352
+
353
+ private_class_method def self.verify_readback(opts = {})
354
+ report = opts[:report]
355
+ report[:readback_matched] = 0
356
+ opts[:plan].each_with_index do |entry, index|
357
+ command = read_command(entry)
358
+ unless command
359
+ report[:records][index][:readback] = :unsupported
360
+ next
361
+ end
362
+ actual = Admin.request(opts[:connection].merge(command))[:value]
363
+ matched = readback_matches?(entry: entry, actual: actual)
364
+ report[:records][index][:readback] = matched ? :matched : :mismatch
365
+ report[:readback_matched] += 1 if matched
366
+ rescue StandardError => e
367
+ report[:records][index][:readback] = :failed
368
+ report[:records][index][:readback_error] = e.class.name
369
+ end
370
+ report[:status] = report[:readback_matched] == opts[:plan].length ? :readback_matched : :readback_incomplete
371
+ end
372
+
373
+ private_class_method def self.readback_matches?(opts = {})
374
+ entry = opts[:entry]
375
+ actual = opts[:actual]
376
+ if entry[:section] == 'owner'
377
+ fields = entry.fetch(:owner_fields, OWNER_FIELDS).select do |name|
378
+ !Meshtastic::User.descriptor.lookup(name).has_presence? || actual.public_send("has_#{name}?")
379
+ end
380
+ actual = Meshtastic::User.new(fields.to_h { |name| [name.to_sym, actual[name]] })
381
+ end
382
+ actual == entry[:message]
383
+ end
384
+
385
+ private_class_method def self.validate_document(opts = {})
386
+ document = opts[:document]
387
+ raise ArgumentError, 'invalid backup document or version' unless document.is_a?(Hash) && document.keys.sort == %w[format records version warning] && document['format'] == FORMAT && document['version'].is_a?(Integer) && document['version'] == 1 && document['warning'].is_a?(String) && document['records'].is_a?(Array)
388
+
389
+ seen = []
390
+ document['records'].map do |record|
391
+ raise ArgumentError, 'invalid backup record' unless record.is_a?(Hash) && record.keys.sort == %w[section slot value]
392
+
393
+ section = record['section']
394
+ slot = record['slot']
395
+ identity = [section, slot]
396
+ raise ArgumentError, 'duplicate backup slot' if seen.include?(identity)
397
+
398
+ seen << identity
399
+ klass = case section
400
+ when 'owner'
401
+ raise ArgumentError, 'invalid owner slot or identity fields' unless slot.nil? && record['value'].is_a?(Hash) && (record['value'].keys - OWNER_FIELDS).empty?
402
+
403
+ Meshtastic::User
404
+ when 'config'
405
+ raise ArgumentError, 'invalid config slot' unless CONFIG_TYPES.map(&:to_s).include?(slot)
406
+
407
+ Meshtastic::Config
408
+ when 'module_config'
409
+ raise ArgumentError, 'invalid module slot' unless MODULE_TYPES.map(&:to_s).include?(slot)
410
+
411
+ Meshtastic::ModuleConfig
412
+ when 'channel'
413
+ raise ArgumentError, 'invalid channel slot' unless slot.is_a?(Integer) && slot.between?(0, 7)
414
+
415
+ Meshtastic::Channel
416
+ when 'ui'
417
+ raise ArgumentError, 'invalid UI slot' unless slot.nil?
418
+
419
+ Meshtastic::DeviceUIConfig
420
+ else
421
+ raise ArgumentError, 'unknown backup section'
422
+ end
423
+ validate_proto_json(value: record['value'], descriptor: klass.descriptor)
424
+ message = klass.decode_json(JSON.generate(record['value']), ignore_unknown_fields: false)
425
+ if %w[config module_config].include?(section)
426
+ types = section == 'config' ? CONFIG_TYPES : MODULE_TYPES
427
+ fields = klass.descriptor.map(&:name)
428
+ expected = fields[types.index(slot.to_sym)]
429
+ raise ArgumentError, 'protobuf section does not match slot' unless message.payload_variant.to_s == expected
430
+ end
431
+ raise ArgumentError, 'channel index does not match slot' if section == 'channel' && message.index != slot
432
+
433
+ { section: section, slot: slot, message: message }
434
+ end
435
+ rescue Google::Protobuf::ParseError, TypeError, RangeError
436
+ raise ArgumentError, 'invalid protobuf JSON in backup'
437
+ end
438
+
439
+ private_class_method def self.validate_proto_json(opts = {})
440
+ value = opts[:value]
441
+ descriptor = opts[:descriptor]
442
+ raise ArgumentError, 'protobuf value must be an object' unless value.is_a?(Hash)
443
+
444
+ value.each do |name, item|
445
+ field = descriptor.lookup(name) if name.is_a?(String)
446
+ raise ArgumentError, 'unknown protobuf field' unless field
447
+ raise ArgumentError, 'null protobuf values are not supported' if item.nil?
448
+
449
+ items = field.label == :repeated ? item : [item]
450
+ raise ArgumentError, 'repeated protobuf field must be an array' unless items.is_a?(Array)
451
+
452
+ items.each do |entry|
453
+ validate_proto_json(value: entry, descriptor: field.subtype) if field.type == :message
454
+ next unless field.type == :bytes
455
+
456
+ raise ArgumentError, 'bytes must use canonical base64' unless entry.is_a?(String) && Base64.strict_encode64(Base64.strict_decode64(entry)) == entry
457
+ end
458
+ end
459
+ end
460
+
461
+ private_class_method def self.validate_options(opts = {})
462
+ options = opts[:options]
463
+ allowed = %i[transport_obj to timeout channel hop_limit path]
464
+ allowed += opts[:operation] == :export ? %i[format config_types module_config_types channel_indexes include_owner include_ui] : %i[backup format dry_run edit_transaction verify]
465
+ allowed += %i[fixed_position include_ringtone include_canned_messages] if opts[:operation] == :export && options[:format] == :device_profile
466
+ raise ArgumentError, 'unknown backup options' unless (options.keys - allowed).empty?
467
+
468
+ formats = opts[:operation] == :export ? %i[json device_profile] : %i[auto json device_profile]
469
+ raise ArgumentError, "format must be one of #{formats.join(', ')}" if options.key?(:format) && !formats.include?(options[:format])
470
+
471
+ %i[include_owner include_ui include_ringtone include_canned_messages dry_run edit_transaction verify].each do |key|
472
+ raise ArgumentError, 'backup flags must be boolean' if options.key?(key) && ![true, false].include?(options[key])
473
+ end
474
+ timeout = options.fetch(:timeout, 10)
475
+ raise ArgumentError, 'timeout must be positive and finite' unless timeout.is_a?(Numeric) && timeout.positive? && timeout.finite?
476
+ end
477
+
478
+ private_class_method def self.connection_options(opts = {})
479
+ raise ArgumentError, 'MQTT synchronous backup is unsupported' if Admin.transport_type(opts) == :mqtt
480
+
481
+ opts.slice(:transport_obj, :to, :timeout, :channel, :hop_limit)
482
+ end
483
+
484
+ private_class_method def self.selection(opts = {})
485
+ configs = opts.fetch(:config_types, CONFIG_TYPES)
486
+ modules = opts.fetch(:module_config_types, [])
487
+ channels = opts.fetch(:channel_indexes, (0..7).to_a)
488
+ raise ArgumentError, 'invalid config_types' unless configs.is_a?(Array) && (configs - CONFIG_TYPES).empty? && configs.uniq == configs
489
+ raise ArgumentError, 'invalid module_config_types' unless modules.is_a?(Array) && (modules - MODULE_TYPES).empty? && modules.uniq == modules
490
+ raise ArgumentError, 'invalid channel_indexes' unless channels.is_a?(Array) && channels.all? { |i| i.is_a?(Integer) && i.between?(0, 7) } && channels.uniq == channels
491
+
492
+ records = []
493
+ records << ['owner', nil] if opts.fetch(:include_owner, true)
494
+ configs.each { |type| records << ['config', type.to_s] }
495
+ modules.each { |type| records << ['module_config', type.to_s] }
496
+ channels.each { |index| records << ['channel', index] }
497
+ records << ['ui', nil] if opts.fetch(:include_ui, false)
498
+ records << ['ringtone', nil] if opts.fetch(:include_ringtone, false)
499
+ records << ['canned_messages', nil] if opts.fetch(:include_canned_messages, false)
500
+ records
501
+ end
502
+
503
+ private_class_method def self.read_command(opts = {})
504
+ case opts[:section]
505
+ when 'owner' then { get_owner_request: true }
506
+ when 'config' then { get_config_request: opts[:slot].to_sym }
507
+ when 'module_config' then { get_module_config_request: opts[:slot].to_sym }
508
+ when 'channel' then { get_channel_request: opts[:slot] + 1 }
509
+ when 'ui' then { get_ui_config_request: true }
510
+ when 'ringtone' then { get_ringtone_request: true }
511
+ when 'canned_messages' then { get_canned_message_module_messages_request: true }
512
+ end
513
+ end
514
+
515
+ public_class_method def self.authors
516
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
517
+ end
518
+
519
+ public_class_method def self.help
520
+ puts "USAGE:
521
+ # Export fresh selected configuration sections.
522
+ #{self}.export(
523
+ transport_obj: 'required - connected Serial, Bluetooth or TCP handle',
524
+ format: 'optional - :json (default version-1 Hash) or :device_profile (binary String in result[:backup]); result also has status, count and warnings',
525
+ path: 'optional - new secret JSON or binary .cfg file matching format; never overwritten; mode 0600',
526
+ to: 'optional - explicit unicast target; defaults to local node',
527
+ timeout: 'optional - positive finite per-request seconds; default 10',
528
+ config_types: 'optional - ConfigType symbols; default eight writable core sections',
529
+ module_config_types: 'optional - ModuleConfigType symbols; default empty',
530
+ channel_indexes: 'optional - unique zero-based indexes 0 through 7; default all eight; binary requires contiguous prefix from 0 with PRIMARY then SECONDARY roles, no disabled slots',
531
+ include_owner: 'optional - portable owner names and license flag; binary also preserves present is_unmessagable; default true',
532
+ include_ui: 'optional - dedicated UI configuration; default false; true is rejected for binary before requests',
533
+ fixed_position: 'optional - binary only: explicit Position protobuf or field Hash; no getter exists; omitted by default',
534
+ include_ringtone: 'optional - binary only: fresh ringtone including empty string; boolean default false',
535
+ include_canned_messages: 'optional - binary only: fresh canned messages including empty string; boolean default false',
536
+ channel: 'optional - mesh transport channel index; default Admin behavior',
537
+ hop_limit: 'optional - mesh transport hop limit; default Admin behavior'
538
+ )
539
+ # Restore with bounded ACKs or exact timeout readback; never replay writes.
540
+ #{self}.import(
541
+ transport_obj: 'required - connected Serial, Bluetooth or TCP handle; no MQTT',
542
+ path: 'optional - existing JSON or binary DeviceProfile .cfg file; mutually exclusive with backup',
543
+ backup: 'optional - versioned JSON Hash, JSON String or binary DeviceProfile String; mutually exclusive with path',
544
+ format: 'optional - :auto (default), :json or :device_profile; cfg paths select binary, other content is validated',
545
+ dry_run: 'optional - validate and plan without any radio requests; default false',
546
+ edit_transaction: 'optional - begin/commit on firmware known to support edits; default false',
547
+ verify: 'optional - additional fresh comparison after completed writes/commit; timeout recovery reads occur regardless; not durability proof; default false',
548
+ timeout: 'optional - positive finite seconds per request; default 10',
549
+ to: 'optional - explicit unicast target; defaults to connected local node',
550
+ channel: 'optional - mesh transport channel index; default Admin behavior',
551
+ hop_limit: 'optional - mesh transport hop limit; default Admin behavior'
552
+ )
553
+ # Contains secrets; firmware may redact values. Never logs document contents.
554
+ # Display the module authors.
555
+ #{self}.authors
556
+ "
557
+ end
558
+ end
559
+ end
560
+ end
@@ -769,3 +769,5 @@ end
769
769
  require 'meshtastic/admin/firmware'
770
770
  require 'meshtastic/admin/channel'
771
771
  require 'meshtastic/admin/config'
772
+
773
+ Meshtastic::Admin.autoload :Backup, 'meshtastic/admin/backup'
@@ -8,7 +8,7 @@ require 'meshtastic/device_ui_pb'
8
8
  require 'meshtastic/field_metadata_pb'
9
9
 
10
10
 
11
- descriptor_data = "\n\x17meshtastic/config.proto\x12\nmeshtastic\x1a\x1ameshtastic/device_ui.proto\x1a\x1fmeshtastic/field_metadata.proto\"\x80\x31\n\x06\x43onfig\x12\x31\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x1f.meshtastic.Config.DeviceConfigH\x00\x12\x35\n\x08position\x18\x02 \x01(\x0b\x32!.meshtastic.Config.PositionConfigH\x00\x12/\n\x05power\x18\x03 \x01(\x0b\x32\x1e.meshtastic.Config.PowerConfigH\x00\x12\x33\n\x07network\x18\x04 \x01(\x0b\x32 .meshtastic.Config.NetworkConfigH\x00\x12\x33\n\x07\x64isplay\x18\x05 \x01(\x0b\x32 .meshtastic.Config.DisplayConfigH\x00\x12-\n\x04lora\x18\x06 \x01(\x0b\x32\x1d.meshtastic.Config.LoRaConfigH\x00\x12\x37\n\tbluetooth\x18\x07 \x01(\x0b\x32\".meshtastic.Config.BluetoothConfigH\x00\x12\x35\n\x08security\x18\x08 \x01(\x0b\x32!.meshtastic.Config.SecurityConfigH\x00\x12\x39\n\nsessionkey\x18\t \x01(\x0b\x32#.meshtastic.Config.SessionkeyConfigH\x00\x12/\n\tdevice_ui\x18\n \x01(\x0b\x32\x1a.meshtastic.DeviceUIConfigH\x00\x1a\xf6\x06\n\x0c\x44\x65viceConfig\x12\x32\n\x04role\x18\x01 \x01(\x0e\x32$.meshtastic.Config.DeviceConfig.Role\x12\x1a\n\x0eserial_enabled\x18\x02 \x01(\x08\x42\x02\x18\x01\x12\x13\n\x0b\x62utton_gpio\x18\x04 \x01(\r\x12\x13\n\x0b\x62uzzer_gpio\x18\x05 \x01(\r\x12I\n\x10rebroadcast_mode\x18\x06 \x01(\x0e\x32/.meshtastic.Config.DeviceConfig.RebroadcastMode\x12 \n\x18node_info_broadcast_secs\x18\x07 \x01(\r\x12\"\n\x1a\x64ouble_tap_as_button_press\x18\x08 \x01(\x08\x12\x16\n\nis_managed\x18\t \x01(\x08\x42\x02\x18\x01\x12\x1c\n\x14\x64isable_triple_click\x18\n \x01(\x08\x12\r\n\x05tzdef\x18\x0b \x01(\t\x12\x1e\n\x16led_heartbeat_disabled\x18\x0c \x01(\x08\x12?\n\x0b\x62uzzer_mode\x18\r \x01(\x0e\x32*.meshtastic.Config.DeviceConfig.BuzzerMode\"\xd4\x01\n\x04Role\x12\n\n\x06\x43LIENT\x10\x00\x12\x0f\n\x0b\x43LIENT_MUTE\x10\x01\x12\n\n\x06ROUTER\x10\x02\x12\x15\n\rROUTER_CLIENT\x10\x03\x1a\x02\x08\x01\x12\x10\n\x08REPEATER\x10\x04\x1a\x02\x08\x01\x12\x0b\n\x07TRACKER\x10\x05\x12\n\n\x06SENSOR\x10\x06\x12\x07\n\x03TAK\x10\x07\x12\x11\n\rCLIENT_HIDDEN\x10\x08\x12\x12\n\x0eLOST_AND_FOUND\x10\t\x12\x0f\n\x0bTAK_TRACKER\x10\n\x12\x0f\n\x0bROUTER_LATE\x10\x0b\x12\x0f\n\x0b\x43LIENT_BASE\x10\x0c\"s\n\x0fRebroadcastMode\x12\x07\n\x03\x41LL\x10\x00\x12\x15\n\x11\x41LL_SKIP_DECODING\x10\x01\x12\x0e\n\nLOCAL_ONLY\x10\x02\x12\x0e\n\nKNOWN_ONLY\x10\x03\x12\x08\n\x04NONE\x10\x04\x12\x16\n\x12\x43ORE_PORTNUMS_ONLY\x10\x05\"i\n\nBuzzerMode\x12\x0f\n\x0b\x41LL_ENABLED\x10\x00\x12\x0c\n\x08\x44ISABLED\x10\x01\x12\x16\n\x12NOTIFICATIONS_ONLY\x10\x02\x12\x0f\n\x0bSYSTEM_ONLY\x10\x03\x12\x13\n\x0f\x44IRECT_MSG_ONLY\x10\x04\x1a\xf8\x05\n\x0ePositionConfig\x12\x1f\n\x17position_broadcast_secs\x18\x01 \x01(\r\x12(\n position_broadcast_smart_enabled\x18\x02 \x01(\x08\x12\x16\n\x0e\x66ixed_position\x18\x03 \x01(\x08\x12\x17\n\x0bgps_enabled\x18\x04 \x01(\x08\x42\x02\x18\x01\x12\x1b\n\x13gps_update_interval\x18\x05 \x01(\r\x12\x1c\n\x10gps_attempt_time\x18\x06 \x01(\rB\x02\x18\x01\x12\x16\n\x0eposition_flags\x18\x07 \x01(\r\x12\x17\n\x07rx_gpio\x18\x08 \x01(\rB\x06\xca\xf3\x18\x02\x08\x01\x12\x17\n\x07tx_gpio\x18\t \x01(\rB\x06\xca\xf3\x18\x02\x08\x01\x12(\n broadcast_smart_minimum_distance\x18\n \x01(\r\x12-\n%broadcast_smart_minimum_interval_secs\x18\x0b \x01(\r\x12\x13\n\x0bgps_en_gpio\x18\x0c \x01(\r\x12;\n\x08gps_mode\x18\r \x01(\x0e\x32).meshtastic.Config.PositionConfig.GpsMode\"\x82\x02\n\rPositionFlags\x12\t\n\x05UNSET\x10\x00\x12\x63\n\x08\x41LTITUDE\x10\x01\x1aU\xca\xf3\x18Q:\x08\x41ltitudeBEInclude an altitude value in position reports, when one is available.\x12\x10\n\x0c\x41LTITUDE_MSL\x10\x02\x12\x16\n\x12GEOIDAL_SEPARATION\x10\x04\x12\x07\n\x03\x44OP\x10\x08\x12\t\n\x05HVDOP\x10\x10\x12\r\n\tSATINVIEW\x10 \x12\n\n\x06SEQ_NO\x10@\x12\x0e\n\tTIMESTAMP\x10\x80\x01\x12\x0c\n\x07HEADING\x10\x80\x02\x12\n\n\x05SPEED\x10\x80\x04\"5\n\x07GpsMode\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07\x45NABLED\x10\x01\x12\x0f\n\x0bNOT_PRESENT\x10\x02\x1a\x84\x02\n\x0bPowerConfig\x12\x17\n\x0fis_power_saving\x18\x01 \x01(\x08\x12&\n\x1eon_battery_shutdown_after_secs\x18\x02 \x01(\r\x12\x1f\n\x17\x61\x64\x63_multiplier_override\x18\x03 \x01(\x02\x12\x1b\n\x13wait_bluetooth_secs\x18\x04 \x01(\r\x12\x10\n\x08sds_secs\x18\x06 \x01(\r\x12\x0f\n\x07ls_secs\x18\x07 \x01(\r\x12\x15\n\rmin_wake_secs\x18\x08 \x01(\r\x12\"\n\x1a\x64\x65vice_battery_ina_address\x18\t \x01(\r\x12\x18\n\x10powermon_enables\x18 \x01(\x04\x1a\xe5\x03\n\rNetworkConfig\x12\x14\n\x0cwifi_enabled\x18\x01 \x01(\x08\x12\x11\n\twifi_ssid\x18\x03 \x01(\t\x12\x10\n\x08wifi_psk\x18\x04 \x01(\t\x12\x12\n\nntp_server\x18\x05 \x01(\t\x12\x13\n\x0b\x65th_enabled\x18\x06 \x01(\x08\x12\x42\n\x0c\x61\x64\x64ress_mode\x18\x07 \x01(\x0e\x32,.meshtastic.Config.NetworkConfig.AddressMode\x12@\n\x0bipv4_config\x18\x08 \x01(\x0b\x32+.meshtastic.Config.NetworkConfig.IpV4Config\x12\x16\n\x0ersyslog_server\x18\t \x01(\t\x12\x19\n\x11\x65nabled_protocols\x18\n \x01(\r\x12\x14\n\x0cipv6_enabled\x18\x0b \x01(\x08\x1a\x46\n\nIpV4Config\x12\n\n\x02ip\x18\x01 \x01(\x07\x12\x0f\n\x07gateway\x18\x02 \x01(\x07\x12\x0e\n\x06subnet\x18\x03 \x01(\x07\x12\x0b\n\x03\x64ns\x18\x04 \x01(\x07\"#\n\x0b\x41\x64\x64ressMode\x12\x08\n\x04\x44HCP\x10\x00\x12\n\n\x06STATIC\x10\x01\"4\n\rProtocolFlags\x12\x10\n\x0cNO_BROADCAST\x10\x00\x12\x11\n\rUDP_BROADCAST\x10\x01\x1a\xc2\x08\n\rDisplayConfig\x12\x16\n\x0escreen_on_secs\x18\x01 \x01(\r\x12V\n\ngps_format\x18\x02 \x01(\x0e\x32>.meshtastic.Config.DisplayConfig.DeprecatedGpsCoordinateFormatB\x02\x18\x01\x12!\n\x19\x61uto_screen_carousel_secs\x18\x03 \x01(\r\x12\x1d\n\x11\x63ompass_north_top\x18\x04 \x01(\x08\x42\x02\x18\x01\x12\x13\n\x0b\x66lip_screen\x18\x05 \x01(\x08\x12<\n\x05units\x18\x06 \x01(\x0e\x32-.meshtastic.Config.DisplayConfig.DisplayUnits\x12\x37\n\x04oled\x18\x07 \x01(\x0e\x32).meshtastic.Config.DisplayConfig.OledType\x12\x41\n\x0b\x64isplaymode\x18\x08 \x01(\x0e\x32,.meshtastic.Config.DisplayConfig.DisplayMode\x12\x14\n\x0cheading_bold\x18\t \x01(\x08\x12\x1d\n\x15wake_on_tap_or_motion\x18\n \x01(\x08\x12P\n\x13\x63ompass_orientation\x18\x0b \x01(\x0e\x32\x33.meshtastic.Config.DisplayConfig.CompassOrientation\x12\x15\n\ruse_12h_clock\x18\x0c \x01(\x08\x12\x1a\n\x12use_long_node_name\x18\r \x01(\x08\x12\x1e\n\x16\x65nable_message_bubbles\x18\x0e \x01(\x08\"+\n\x1d\x44\x65precatedGpsCoordinateFormat\x12\n\n\x06UNUSED\x10\x00\"(\n\x0c\x44isplayUnits\x12\n\n\x06METRIC\x10\x00\x12\x0c\n\x08IMPERIAL\x10\x01\"\x7f\n\x08OledType\x12\r\n\tOLED_AUTO\x10\x00\x12\x10\n\x0cOLED_SSD1306\x10\x01\x12\x0f\n\x0bOLED_SH1106\x10\x02\x12\x0f\n\x0bOLED_SH1107\x10\x03\x12\x17\n\x13OLED_SH1107_128_128\x10\x04\x12\x17\n\x13OLED_SH1107_ROTATED\x10\x05\"A\n\x0b\x44isplayMode\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0c\n\x08TWOCOLOR\x10\x01\x12\x0c\n\x08INVERTED\x10\x02\x12\t\n\x05\x43OLOR\x10\x03\"\xba\x01\n\x12\x43ompassOrientation\x12\r\n\tDEGREES_0\x10\x00\x12\x0e\n\nDEGREES_90\x10\x01\x12\x0f\n\x0b\x44\x45GREES_180\x10\x02\x12\x0f\n\x0b\x44\x45GREES_270\x10\x03\x12\x16\n\x12\x44\x45GREES_0_INVERTED\x10\x04\x12\x17\n\x13\x44\x45GREES_90_INVERTED\x10\x05\x12\x18\n\x14\x44\x45GREES_180_INVERTED\x10\x06\x12\x18\n\x14\x44\x45GREES_270_INVERTED\x10\x07\x1a\xc7\x0c\n\nLoRaConfig\x12\x12\n\nuse_preset\x18\x01 \x01(\x08\x12?\n\x0cmodem_preset\x18\x02 \x01(\x0e\x32).meshtastic.Config.LoRaConfig.ModemPreset\x12\x11\n\tbandwidth\x18\x03 \x01(\r\x12\x15\n\rspread_factor\x18\x04 \x01(\r\x12\x13\n\x0b\x63oding_rate\x18\x05 \x01(\r\x12\x18\n\x10\x66requency_offset\x18\x06 \x01(\x02\x12\x38\n\x06region\x18\x07 \x01(\x0e\x32(.meshtastic.Config.LoRaConfig.RegionCode\x12\x9d\x01\n\thop_limit\x18\x08 \x01(\rB\x89\x01\xca\xf3\x18\x84\x01\x19\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x00\x00\x00\x1c@:\tHop LimitBIHow many times a message may be repeated before it stops being forwarded.J\x1ahops|ttl|range|rebroadcast\x12\x12\n\ntx_enabled\x18\t \x01(\x08\x12\x10\n\x08tx_power\x18\n \x01(\x05\x12\x13\n\x0b\x63hannel_num\x18\x0b \x01(\r\x12\x1b\n\x13override_duty_cycle\x18\x0c \x01(\x08\x12\x1e\n\x16sx126x_rx_boosted_gain\x18\r \x01(\x08\x12\x1a\n\x12override_frequency\x18\x0e \x01(\x02\x12\x17\n\x0fpa_fan_disabled\x18\x0f \x01(\x08\x12\x17\n\x0fignore_incoming\x18g \x03(\r\x12\x13\n\x0bignore_mqtt\x18h \x01(\x08\x12\x19\n\x11\x63onfig_ok_to_mqtt\x18i \x01(\x08\x12@\n\x0c\x66\x65m_lna_mode\x18j \x01(\x0e\x32*.meshtastic.Config.LoRaConfig.FEM_LNA_Mode\x12\x17\n\x0fserial_hal_only\x18k \x01(\x08\"\xc8\x03\n\nRegionCode\x12\t\n\x05UNSET\x10\x00\x12\x06\n\x02US\x10\x01\x12\n\n\x06\x45U_433\x10\x02\x12\n\n\x06\x45U_868\x10\x03\x12\x06\n\x02\x43N\x10\x04\x12\x06\n\x02JP\x10\x05\x12\x07\n\x03\x41NZ\x10\x06\x12\x06\n\x02KR\x10\x07\x12\x06\n\x02TW\x10\x08\x12\x06\n\x02RU\x10\t\x12\x06\n\x02IN\x10\n\x12\n\n\x06NZ_865\x10\x0b\x12\x06\n\x02TH\x10\x0c\x12\x0b\n\x07LORA_24\x10\r\x12\n\n\x06UA_433\x10\x0e\x12\x0e\n\x06UA_868\x10\x0f\x1a\x02\x08\x01\x12\n\n\x06MY_433\x10\x10\x12\n\n\x06MY_919\x10\x11\x12\n\n\x06SG_923\x10\x12\x12\n\n\x06PH_433\x10\x13\x12\n\n\x06PH_868\x10\x14\x12\n\n\x06PH_915\x10\x15\x12\x0b\n\x07\x41NZ_433\x10\x16\x12\n\n\x06KZ_433\x10\x17\x12\n\n\x06KZ_863\x10\x18\x12\n\n\x06NP_865\x10\x19\x12\n\n\x06\x42R_902\x10\x1a\x12\x0b\n\x07ITU1_2M\x10\x1b\x12\x0b\n\x07ITU2_2M\x10\x1c\x12\n\n\x06\x45U_866\x10\x1d\x12\n\n\x06\x45U_874\x10\x1e\x12\n\n\x06\x45U_917\x10\x1f\x12\x0c\n\x08\x45U_N_868\x10 \x12\x0b\n\x07ITU3_2M\x10!\x12\r\n\tITU1_70CM\x10\"\x12\r\n\tITU2_70CM\x10#\x12\r\n\tITU3_70CM\x10$\x12\x0e\n\nITU2_125CM\x10%\"\xd8\x02\n\x0bModemPreset\x12\x38\n\tLONG_FAST\x10\x00\x1a)\xca\xf3\x18%:\x11Long Range - FastJ\x10longfast|default\x12\x11\n\tLONG_SLOW\x10\x01\x1a\x02\x08\x01\x12\x16\n\x0eVERY_LONG_SLOW\x10\x02\x1a\x02\x08\x01\x12\x0f\n\x0bMEDIUM_SLOW\x10\x03\x12\x0f\n\x0bMEDIUM_FAST\x10\x04\x12\x0e\n\nSHORT_SLOW\x10\x05\x12\x0e\n\nSHORT_FAST\x10\x06\x12\x11\n\rLONG_MODERATE\x10\x07\x12\x0f\n\x0bSHORT_TURBO\x10\x08\x12\x0e\n\nLONG_TURBO\x10\t\x12\r\n\tLITE_FAST\x10\n\x12\r\n\tLITE_SLOW\x10\x0b\x12\x0f\n\x0bNARROW_FAST\x10\x0c\x12\x0f\n\x0bNARROW_SLOW\x10\r\x12\r\n\tTINY_FAST\x10\x0e\x12\r\n\tTINY_SLOW\x10\x0f\x12\x10\n\x0cMEDIUM_TURBO\x10\x10\":\n\x0c\x46\x45M_LNA_Mode\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07\x45NABLED\x10\x01\x12\x0f\n\x0bNOT_PRESENT\x10\x02\x1a\xad\x01\n\x0f\x42luetoothConfig\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12<\n\x04mode\x18\x02 \x01(\x0e\x32..meshtastic.Config.BluetoothConfig.PairingMode\x12\x11\n\tfixed_pin\x18\x03 \x01(\r\"8\n\x0bPairingMode\x12\x0e\n\nRANDOM_PIN\x10\x00\x12\r\n\tFIXED_PIN\x10\x01\x12\n\n\x06NO_PIN\x10\x02\x1a\x9c\x03\n\x0eSecurityConfig\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x13\n\x0bprivate_key\x18\x02 \x01(\x0c\x12\x11\n\tadmin_key\x18\x03 \x03(\x0c\x12\x12\n\nis_managed\x18\x04 \x01(\x08\x12\x16\n\x0eserial_enabled\x18\x05 \x01(\x08\x12\x1d\n\x15\x64\x65\x62ug_log_api_enabled\x18\x06 \x01(\x08\x12\x1d\n\x15\x61\x64min_channel_enabled\x18\x08 \x01(\x08\x12X\n\x17packet_signature_policy\x18\t \x01(\x0e\x32\x37.meshtastic.Config.SecurityConfig.PacketSignaturePolicy\"\x89\x01\n\x15PacketSignaturePolicy\x12&\n\"PACKET_SIGNATURE_POLICY_COMPATIBLE\x10\x00\x12$\n PACKET_SIGNATURE_POLICY_BALANCED\x10\x01\x12\"\n\x1ePACKET_SIGNATURE_POLICY_STRICT\x10\x02\x1a\x12\n\x10SessionkeyConfigB\x11\n\x0fpayload_variantBb\n\x14org.meshtastic.protoB\x0c\x43onfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3"
11
+ descriptor_data = "\n\x17meshtastic/config.proto\x12\nmeshtastic\x1a\x1ameshtastic/device_ui.proto\x1a\x1fmeshtastic/field_metadata.proto\"\xe2g\n\x06\x43onfig\x12\x31\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x1f.meshtastic.Config.DeviceConfigH\x00\x12\x35\n\x08position\x18\x02 \x01(\x0b\x32!.meshtastic.Config.PositionConfigH\x00\x12/\n\x05power\x18\x03 \x01(\x0b\x32\x1e.meshtastic.Config.PowerConfigH\x00\x12\x33\n\x07network\x18\x04 \x01(\x0b\x32 .meshtastic.Config.NetworkConfigH\x00\x12\x33\n\x07\x64isplay\x18\x05 \x01(\x0b\x32 .meshtastic.Config.DisplayConfigH\x00\x12-\n\x04lora\x18\x06 \x01(\x0b\x32\x1d.meshtastic.Config.LoRaConfigH\x00\x12\x37\n\tbluetooth\x18\x07 \x01(\x0b\x32\".meshtastic.Config.BluetoothConfigH\x00\x12\x35\n\x08security\x18\x08 \x01(\x0b\x32!.meshtastic.Config.SecurityConfigH\x00\x12\x39\n\nsessionkey\x18\t \x01(\x0b\x32#.meshtastic.Config.SessionkeyConfigH\x00\x12/\n\tdevice_ui\x18\n \x01(\x0b\x32\x1a.meshtastic.DeviceUIConfigH\x00\x1a\xa2\x1c\n\x0c\x44\x65viceConfig\x12\x45\n\x04role\x18\x01 \x01(\x0e\x32$.meshtastic.Config.DeviceConfig.RoleB\x11\xca\xf3\x18\r:\x0b\x44\x65vice Role\x12\x1a\n\x0eserial_enabled\x18\x02 \x01(\x08\x42\x02\x18\x01\x12&\n\x0b\x62utton_gpio\x18\x04 \x01(\rB\x11\xca\xf3\x18\r:\x0b\x42utton GPIO\x12&\n\x0b\x62uzzer_gpio\x18\x05 \x01(\rB\x11\xca\xf3\x18\r:\x0b\x42uzzer GPIO\x12\x61\n\x10rebroadcast_mode\x18\x06 \x01(\x0e\x32/.meshtastic.Config.DeviceConfig.RebroadcastModeB\x16\xca\xf3\x18\x12:\x10Rebroadcast Mode\x12G\n\x18node_info_broadcast_secs\x18\x07 \x01(\rB%\xca\xf3\x18!*\x01s:\x1cNode Info Broadcast Interval\x12\x84\x01\n\x1a\x64ouble_tap_as_button_press\x18\x08 \x01(\x08\x42`\xca\xf3\x18\\:\x14\x44ouble Tap as ButtonBDTreat double tap on supported accelerometers as a user button press.\x12\x16\n\nis_managed\x18\t \x01(\x08\x42\x02\x18\x01\x12i\n\x14\x64isable_triple_click\x18\n \x01(\x08\x42K\xca\xf3\x18G:\x14\x44isable Triple ClickB/Disables the user button triple-press shortcut.\x12\x1e\n\x05tzdef\x18\x0b \x01(\tB\x0f\xca\xf3\x18\x0b:\tTime Zone\x12\xcc\x01\n\x16led_heartbeat_disabled\x18\x0c \x01(\x08\x42\xab\x01\xca\xf3\x18\xa6\x01:\rLED HeartbeatB\x94\x01\x43ontrols the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable.\x12?\n\x0b\x62uzzer_mode\x18\r \x01(\x0e\x32*.meshtastic.Config.DeviceConfig.BuzzerMode\"\xc1\x0b\n\x04Role\x12H\n\x06\x43LIENT\x10\x00\x1a<\xca\xf3\x18\x38:\x06\x43lientB.App connected or stand alone messaging device.\x12\\\n\x0b\x43LIENT_MUTE\x10\x01\x1aK\xca\xf3\x18G:\x0b\x43lient MuteB8Device that does not forward packets from other devices.\x12\xb0\x01\n\x06ROUTER\x10\x02\x1a\xa3\x01\xca\xf3\x18\x9e\x01:\x06RouterB\x93\x01Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Needs exceptional coverage. Visible in Nodes list.\x12\x15\n\rROUTER_CLIENT\x10\x03\x1a\x02\x08\x01\x12\xb3\x01\n\x08REPEATER\x10\x04\x1a\xa4\x01\x08\x01\xca\xf3\x18\x9d\x01:\x08RepeaterB\x90\x01\x44\x65precated infrastructure role that creates gaps in the mesh rebroadcast chain. Switch this node to a Router-based role (Router or Router Late).\x12H\n\x07TRACKER\x10\x05\x1a;\xca\xf3\x18\x37:\x07TrackerB,Broadcasts GPS position packets as priority.\x12\x43\n\x06SENSOR\x10\x06\x1a\x37\xca\xf3\x18\x33:\x06SensorB)Broadcasts telemetry packets as priority.\x12X\n\x03TAK\x10\x07\x1aO\xca\xf3\x18K:\x03TAKBDOptimized for ATAK system communication, reduces routine broadcasts.\x12k\n\rCLIENT_HIDDEN\x10\x08\x1aX\xca\xf3\x18T:\rClient HiddenBCDevice that only broadcasts as needed for stealth or power savings.\x12\x89\x01\n\x0eLOST_AND_FOUND\x10\t\x1au\xca\xf3\x18q:\x0eLost and FoundB_Broadcasts location as message to default channel regularly for to assist with device recovery.\x12h\n\x0bTAK_TRACKER\x10\n\x1aW\xca\xf3\x18S:\x0bTAK TrackerBDEnables automatic TAK PLI broadcasts and reduces routine broadcasts.\x12\xbc\x01\n\x0bROUTER_LATE\x10\x0b\x1a\xaa\x01\xca\xf3\x18\xa5\x01:\x0bRouter LateB\x95\x01Infrastructure node that always rebroadcasts packets once but only after all other modes. Visible in Nodes list. Not a good choice for rooftop nodes.\x12\x85\x01\n\x0b\x43LIENT_BASE\x10\x0c\x1at\xca\xf3\x18p:\x0b\x43lient BaseBaUsed for rooftop nodes to distribute messages more widely from multiple nearby client mute nodes.\"\xc9\x08\n\x0fRebroadcastMode\x12\x8a\x01\n\x03\x41LL\x10\x00\x1a\x80\x01\xca\xf3\x18|:\x03\x41llBuRebroadcast any observed message, if it was on our private channel or from another channel with the same lora params.\x12\xe0\x01\n\x11\x41LL_SKIP_DECODING\x10\x01\x1a\xc8\x01\xca\xf3\x18\xc3\x01:\x11\x41ll Skip DecodingB\xad\x01Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior.\x12\xcd\x01\n\nLOCAL_ONLY\x10\x02\x1a\xbc\x01\xca\xf3\x18\xb7\x01:\nLocal OnlyB\xa8\x01Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels.\x12\xc8\x01\n\nKNOWN_ONLY\x10\x03\x1a\xb7\x01\xca\xf3\x18\xb2\x01:\nKnown OnlyB\xa3\x01Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node\'s known list.\x12\x92\x01\n\x04NONE\x10\x04\x1a\x87\x01\xca\xf3\x18\x82\x01:\x04NoneBzOnly permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.\x12\x95\x01\n\x12\x43ORE_PORTNUMS_ONLY\x10\x05\x1a}\xca\xf3\x18y:\x12\x43ore Portnums OnlyBcOnly rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing.\"i\n\nBuzzerMode\x12\x0f\n\x0b\x41LL_ENABLED\x10\x00\x12\x0c\n\x08\x44ISABLED\x10\x01\x12\x16\n\x12NOTIFICATIONS_ONLY\x10\x02\x12\x0f\n\x0bSYSTEM_ONLY\x10\x03\x12\x13\n\x0f\x44IRECT_MSG_ONLY\x10\x04\x1a\x82\t\n\x0ePositionConfig\x12<\n\x17position_broadcast_secs\x18\x01 \x01(\rB\x1b\xca\xf3\x18\x17*\x01s:\x12\x42roadcast Interval\x12>\n position_broadcast_smart_enabled\x18\x02 \x01(\x08\x42\x14\xca\xf3\x18\x10:\x0eSmart Position\x12\xb2\x01\n\x0e\x66ixed_position\x18\x03 \x01(\x08\x42\x99\x01\xca\xf3\x18\x94\x01:\x0e\x46ixed PositionB\x81\x01The last known latitude, longitude and altitude are broadcast over the mesh on the position interval, rather than a live GPS fix.\x12\x17\n\x0bgps_enabled\x18\x04 \x01(\x08\x42\x02\x18\x01\x12\x35\n\x13gps_update_interval\x18\x05 \x01(\rB\x18\xca\xf3\x18\x14*\x01s:\x0fUpdate Interval\x12\x1c\n\x10gps_attempt_time\x18\x06 \x01(\rB\x02\x18\x01\x12\x16\n\x0eposition_flags\x18\x07 \x01(\r\x12)\n\x07rx_gpio\x18\x08 \x01(\rB\x18\xca\xf3\x18\x14\x08\x01:\x10GPS Receive GPIO\x12*\n\x07tx_gpio\x18\t \x01(\rB\x19\xca\xf3\x18\x15\x08\x01:\x11GPS Transmit GPIO\x12\x43\n broadcast_smart_minimum_distance\x18\n \x01(\rB\x19\xca\xf3\x18\x15*\x01m:\x10Minimum Distance\x12H\n%broadcast_smart_minimum_interval_secs\x18\x0b \x01(\rB\x19\xca\xf3\x18\x15*\x01s:\x10Minimum Interval\x12&\n\x0bgps_en_gpio\x18\x0c \x01(\rB\x11\xca\xf3\x18\r:\x0bGPS EN GPIO\x12;\n\x08gps_mode\x18\r \x01(\x0e\x32).meshtastic.Config.PositionConfig.GpsMode\"\x82\x02\n\rPositionFlags\x12\t\n\x05UNSET\x10\x00\x12\x63\n\x08\x41LTITUDE\x10\x01\x1aU\xca\xf3\x18Q:\x08\x41ltitudeBEInclude an altitude value in position reports, when one is available.\x12\x10\n\x0c\x41LTITUDE_MSL\x10\x02\x12\x16\n\x12GEOIDAL_SEPARATION\x10\x04\x12\x07\n\x03\x44OP\x10\x08\x12\t\n\x05HVDOP\x10\x10\x12\r\n\tSATINVIEW\x10 \x12\n\n\x06SEQ_NO\x10@\x12\x0e\n\tTIMESTAMP\x10\x80\x01\x12\x0c\n\x07HEADING\x10\x80\x02\x12\n\n\x05SPEED\x10\x80\x04\"g\n\x07GpsMode\x12\x1c\n\x08\x44ISABLED\x10\x00\x1a\x0e\xca\xf3\x18\n:\x08\x44isabled\x12\x1a\n\x07\x45NABLED\x10\x01\x1a\r\xca\xf3\x18\t:\x07\x45nabled\x12\"\n\x0bNOT_PRESENT\x10\x02\x1a\x11\xca\xf3\x18\r:\x0bNot Present\x1a\xbb\x04\n\x0bPowerConfig\x12\x98\x02\n\x0fis_power_saving\x18\x01 \x01(\x08\x42\xfe\x01\xca\xf3\x18\xf9\x01:\x0cPower SavingB\xe8\x01Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don\'t use this setting if you want to use your device with the phone apps or are using a device without a user button.\x12G\n\x1eon_battery_shutdown_after_secs\x18\x02 \x01(\rB\x1f\xca\xf3\x18\x1b*\x01s:\x16Shutdown on Power Loss\x12\x33\n\x17\x61\x64\x63_multiplier_override\x18\x03 \x01(\x02\x42\x12\xca\xf3\x18\x0e:\x0c\x41\x44\x43 Override\x12\x1b\n\x13wait_bluetooth_secs\x18\x04 \x01(\r\x12\x10\n\x08sds_secs\x18\x06 \x01(\r\x12\x0f\n\x07ls_secs\x18\x07 \x01(\r\x12\x15\n\rmin_wake_secs\x18\x08 \x01(\r\x12\"\n\x1a\x64\x65vice_battery_ina_address\x18\t \x01(\r\x12\x18\n\x10powermon_enables\x18 \x01(\x04\x1a\xc5\x06\n\rNetworkConfig\x12i\n\x0cwifi_enabled\x18\x01 \x01(\x08\x42S\xca\xf3\x18O:\x0cWiFi EnabledB?Enabling WiFi will disable the bluetooth connection to the app.\x12\x1d\n\twifi_ssid\x18\x03 \x01(\tB\n\xca\xf3\x18\x06:\x04SSID\x12 \n\x08wifi_psk\x18\x04 \x01(\tB\x0e\xca\xf3\x18\n:\x08Password\x12$\n\nntp_server\x18\x05 \x01(\tB\x10\xca\xf3\x18\x0c:\nNTP Server\x12p\n\x0b\x65th_enabled\x18\x06 \x01(\x08\x42[\xca\xf3\x18W:\x10\x45thernet EnabledBCEnabling Ethernet will disable the bluetooth connection to the app.\x12V\n\x0c\x61\x64\x64ress_mode\x18\x07 \x01(\x0e\x32,.meshtastic.Config.NetworkConfig.AddressModeB\x12\xca\xf3\x18\x0e:\x0c\x41\x64\x64ress Mode\x12@\n\x0bipv4_config\x18\x08 \x01(\x0b\x32+.meshtastic.Config.NetworkConfig.IpV4Config\x12,\n\x0ersyslog_server\x18\t \x01(\tB\x14\xca\xf3\x18\x10:\x0eRsyslog Server\x12o\n\x11\x65nabled_protocols\x18\n \x01(\rBT\xca\xf3\x18P:\x11\x45nabled ProtocolsB;Enable broadcasting packets via UDP over the local network.\x12\x14\n\x0cipv6_enabled\x18\x0b \x01(\x08\x1a\x46\n\nIpV4Config\x12\n\n\x02ip\x18\x01 \x01(\x07\x12\x0f\n\x07gateway\x18\x02 \x01(\x07\x12\x0e\n\x06subnet\x18\x03 \x01(\x07\x12\x0b\n\x03\x64ns\x18\x04 \x01(\x07\"#\n\x0b\x41\x64\x64ressMode\x12\x08\n\x04\x44HCP\x10\x00\x12\n\n\x06STATIC\x10\x01\"4\n\rProtocolFlags\x12\x10\n\x0cNO_BROADCAST\x10\x00\x12\x11\n\rUDP_BROADCAST\x10\x01\x1a\xbc\x0f\n\rDisplayConfig\x12.\n\x0escreen_on_secs\x18\x01 \x01(\rB\x16\xca\xf3\x18\x12*\x01s:\rScreen on for\x12V\n\ngps_format\x18\x02 \x01(\x0e\x32>.meshtastic.Config.DisplayConfig.DeprecatedGpsCoordinateFormatB\x02\x18\x01\x12=\n\x19\x61uto_screen_carousel_secs\x18\x03 \x01(\rB\x1a\xca\xf3\x18\x16*\x01s:\x11\x43\x61rousel Interval\x12\x87\x01\n\x11\x63ompass_north_top\x18\x04 \x01(\x08\x42l\x18\x01\xca\xf3\x18\x66:\x12\x41lways point northBPThe compass heading on the screen outside of the circle will always point north.\x12>\n\x0b\x66lip_screen\x18\x05 \x01(\x08\x42)\xca\xf3\x18%:\x0b\x46lip ScreenB\x16\x46lip screen vertically\x12Q\n\x05units\x18\x06 \x01(\x0e\x32-.meshtastic.Config.DisplayConfig.DisplayUnitsB\x13\xca\xf3\x18\x0f:\rDisplay Units\x12H\n\x04oled\x18\x07 \x01(\x0e\x32).meshtastic.Config.DisplayConfig.OledTypeB\x0f\xca\xf3\x18\x0b:\tOLED Type\x12U\n\x0b\x64isplaymode\x18\x08 \x01(\x0e\x32,.meshtastic.Config.DisplayConfig.DisplayModeB\x12\xca\xf3\x18\x0e:\x0c\x44isplay Mode\x12N\n\x0cheading_bold\x18\t \x01(\x08\x42\x38\xca\xf3\x18\x34:\x0c\x42old HeadingB$Bold the heading text on the screen.\x12z\n\x15wake_on_tap_or_motion\x18\n \x01(\x08\x42[\xca\xf3\x18W:\x1cWake Screen on tap or motionB7Requires that there be an accelerometer on your device.\x12k\n\x13\x63ompass_orientation\x18\x0b \x01(\x0e\x32\x33.meshtastic.Config.DisplayConfig.CompassOrientationB\x19\xca\xf3\x18\x15:\x13\x43ompass Orientation\x12T\n\ruse_12h_clock\x18\x0c \x01(\x08\x42=\xca\xf3\x18\x39:\r12 Hour ClockB(Sets the screen clock format to 12-hour.\x12\x1a\n\x12use_long_node_name\x18\r \x01(\x08\x12\x1e\n\x16\x65nable_message_bubbles\x18\x0e \x01(\x08\"+\n\x1d\x44\x65precatedGpsCoordinateFormat\x12\n\n\x06UNUSED\x10\x00\"F\n\x0c\x44isplayUnits\x12\x18\n\x06METRIC\x10\x00\x1a\x0c\xca\xf3\x18\x08:\x06Metric\x12\x1c\n\x08IMPERIAL\x10\x01\x1a\x0e\xca\xf3\x18\n:\x08Imperial\"\xc9\x01\n\x08OledType\x12)\n\tOLED_AUTO\x10\x00\x1a\x1a\xca\xf3\x18\x16:\x14\x44\x65tect Automatically\x12 \n\x0cOLED_SSD1306\x10\x01\x1a\x0e\xca\xf3\x18\n:\x08SSD 1306\x12\x1e\n\x0bOLED_SH1106\x10\x02\x1a\r\xca\xf3\x18\t:\x07SH 1106\x12\x1e\n\x0bOLED_SH1107\x10\x03\x1a\r\xca\xf3\x18\t:\x07SH 1107\x12\x17\n\x13OLED_SH1107_128_128\x10\x04\x12\x17\n\x13OLED_SH1107_ROTATED\x10\x05\"\xd6\x01\n\x0b\x44isplayMode\x12/\n\x07\x44\x45\x46\x41ULT\x10\x00\x1a\"\xca\xf3\x18\x1e:\x1c\x44\x65\x66\x61ult 128x64 screen layout\x12\x32\n\x08TWOCOLOR\x10\x01\x1a$\xca\xf3\x18 :\x1eOptimized for 2 color displays\x12\x38\n\x08INVERTED\x10\x02\x1a*\xca\xf3\x18&:$Inverted top bar for 2 Color display\x12(\n\x05\x43OLOR\x10\x03\x1a\x1d\xca\xf3\x18\x19:\x17TFT Full Color Displays\"\xc0\x02\n\x12\x43ompassOrientation\x12\x18\n\tDEGREES_0\x10\x00\x1a\t\xca\xf3\x18\x05:\x03\x30\xc2\xb0\x12\x1a\n\nDEGREES_90\x10\x01\x1a\n\xca\xf3\x18\x06:\x04\x39\x30\xc2\xb0\x12\x1c\n\x0b\x44\x45GREES_180\x10\x02\x1a\x0b\xca\xf3\x18\x07:\x05\x31\x38\x30\xc2\xb0\x12\x1c\n\x0b\x44\x45GREES_270\x10\x03\x1a\x0b\xca\xf3\x18\x07:\x05\x32\x37\x30\xc2\xb0\x12*\n\x12\x44\x45GREES_0_INVERTED\x10\x04\x1a\x12\xca\xf3\x18\x0e:\x0c\x30\xc2\xb0 Inverted\x12,\n\x13\x44\x45GREES_90_INVERTED\x10\x05\x1a\x13\xca\xf3\x18\x0f:\r90\xc2\xb0 Inverted\x12.\n\x14\x44\x45GREES_180_INVERTED\x10\x06\x1a\x14\xca\xf3\x18\x10:\x0e\x31\x38\x30\xc2\xb0 Inverted\x12.\n\x14\x44\x45GREES_270_INVERTED\x10\x07\x1a\x14\xca\xf3\x18\x10:\x0e\x32\x37\x30\xc2\xb0 Inverted\x1a\xb6\x1b\n\nLoRaConfig\x12$\n\nuse_preset\x18\x01 \x01(\x08\x42\x10\xca\xf3\x18\x0c:\nUse Preset\x12N\n\x0cmodem_preset\x18\x02 \x01(\x0e\x32).meshtastic.Config.LoRaConfig.ModemPresetB\r\xca\xf3\x18\t:\x07Presets\x12\'\n\tbandwidth\x18\x03 \x01(\rB\x14\xca\xf3\x18\x10*\x03kHz:\tBandwidth\x12*\n\rspread_factor\x18\x04 \x01(\rB\x13\xca\xf3\x18\x0f:\rSpread Factor\x12&\n\x0b\x63oding_rate\x18\x05 \x01(\rB\x11\xca\xf3\x18\r:\x0b\x43oding Rate\x12\x18\n\x10\x66requency_offset\x18\x06 \x01(\x02\x12\x46\n\x06region\x18\x07 \x01(\x0e\x32(.meshtastic.Config.LoRaConfig.RegionCodeB\x0c\xca\xf3\x18\x08:\x06Region\x12\x9d\x01\n\thop_limit\x18\x08 \x01(\rB\x89\x01\xca\xf3\x18\x84\x01\x19\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x00\x00\x00\x1c@:\tHop LimitBIHow many times a message may be repeated before it stops being forwarded.J\x1ahops|ttl|range|rebroadcast\x12*\n\ntx_enabled\x18\t \x01(\x08\x42\x16\xca\xf3\x18\x12:\x10Transmit Enabled\x12\xd3\x01\n\x08tx_power\x18\n \x01(\x05\x42\xc0\x01\xca\xf3\x18\xbb\x01\x19\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x00\x00\x00>@*\x03\x64\x42m:\x0eTransmit PowerBxRadio transmit power. Leave at zero to use the highest level legal for the region, which is what most radios should use.J\x18tx|power|dbm|output|gain\x12\xe0\x01\n\x0b\x63hannel_num\x18\x0b \x01(\rB\xca\x01\xca\xf3\x18\xc5\x01:\x0e\x46requency SlotB\xb2\x01Your node\xe2\x80\x99s operating frequency is calculated based on the region, modem preset, and this field. When 0, the slot is automatically calculated based on the primary channel name.\x12\x1b\n\x13override_duty_cycle\x18\x0c \x01(\x08\x12\x35\n\x16sx126x_rx_boosted_gain\x18\r \x01(\x08\x42\x15\xca\xf3\x18\x11:\x0fRX Boosted Gain\x12\x34\n\x12override_frequency\x18\x0e \x01(\x02\x42\x18\xca\xf3\x18\x14:\x12\x46requency Override\x12\x17\n\x0fpa_fan_disabled\x18\x0f \x01(\x08\x12\x17\n\x0fignore_incoming\x18g \x03(\r\x12&\n\x0bignore_mqtt\x18h \x01(\x08\x42\x11\xca\xf3\x18\r:\x0bIgnore MQTT\x12+\n\x11\x63onfig_ok_to_mqtt\x18i \x01(\x08\x42\x10\xca\xf3\x18\x0c:\nOk to MQTT\x12@\n\x0c\x66\x65m_lna_mode\x18j \x01(\x0e\x32*.meshtastic.Config.LoRaConfig.FEM_LNA_Mode\x12\x17\n\x0fserial_hal_only\x18k \x01(\x08\"\xe5\n\n\nRegionCode\x12$\n\x05UNSET\x10\x00\x1a\x19\xca\xf3\x18\x15:\x13Please set a region\x12\x1b\n\x02US\x10\x01\x1a\x13\xca\xf3\x18\x0f:\rUnited States\x12\'\n\x06\x45U_433\x10\x02\x1a\x1b\xca\xf3\x18\x17:\x15\x45uropean Union 433MHz\x12\'\n\x06\x45U_868\x10\x03\x1a\x1b\xca\xf3\x18\x17:\x15\x45uropean Union 868MHz\x12\x13\n\x02\x43N\x10\x04\x1a\x0b\xca\xf3\x18\x07:\x05\x43hina\x12\x13\n\x02JP\x10\x05\x1a\x0b\xca\xf3\x18\x07:\x05Japan\x12&\n\x03\x41NZ\x10\x06\x1a\x1d\xca\xf3\x18\x19:\x17\x41ustralia / New Zealand\x12\x13\n\x02KR\x10\x07\x1a\x0b\xca\xf3\x18\x07:\x05Korea\x12\x14\n\x02TW\x10\x08\x1a\x0c\xca\xf3\x18\x08:\x06Taiwan\x12\x14\n\x02RU\x10\t\x1a\x0c\xca\xf3\x18\x08:\x06Russia\x12\x06\n\x02IN\x10\n\x12$\n\x06NZ_865\x10\x0b\x1a\x18\xca\xf3\x18\x14:\x12New Zealand 865MHz\x12\x16\n\x02TH\x10\x0c\x1a\x0e\xca\xf3\x18\n:\x08Thailand\x12\x1a\n\x07LORA_24\x10\r\x1a\r\xca\xf3\x18\t:\x07\x32.4 Ghz\x12 \n\x06UA_433\x10\x0e\x1a\x14\xca\xf3\x18\x10:\x0eUkraine 433MHz\x12\x0e\n\x06UA_868\x10\x0f\x1a\x02\x08\x01\x12!\n\x06MY_433\x10\x10\x1a\x15\xca\xf3\x18\x11:\x0fMalaysia 433MHz\x12!\n\x06MY_919\x10\x11\x1a\x15\xca\xf3\x18\x11:\x0fMalaysia 919MHz\x12\"\n\x06SG_923\x10\x12\x1a\x16\xca\xf3\x18\x12:\x10Singapore 923MHz\x12$\n\x06PH_433\x10\x13\x1a\x18\xca\xf3\x18\x14:\x12Philippines 433MHz\x12$\n\x06PH_868\x10\x14\x1a\x18\xca\xf3\x18\x14:\x12Philippines 868MHz\x12$\n\x06PH_915\x10\x15\x1a\x18\xca\xf3\x18\x14:\x12Philippines 915MHz\x12\x31\n\x07\x41NZ_433\x10\x16\x1a$\xca\xf3\x18 :\x1e\x41ustralia / New Zealand 433MHz\x12#\n\x06KZ_433\x10\x17\x1a\x17\xca\xf3\x18\x13:\x11Kazakhstan 433MHz\x12#\n\x06KZ_863\x10\x18\x1a\x17\xca\xf3\x18\x13:\x11Kazakhstan 863MHz\x12\x1e\n\x06NP_865\x10\x19\x1a\x12\xca\xf3\x18\x0e:\x0cNepal 865MHz\x12\x1f\n\x06\x42R_902\x10\x1a\x1a\x13\xca\xf3\x18\x0f:\rBrazil 902MHz\x12,\n\x07ITU1_2M\x10\x1b\x1a\x1f\xca\xf3\x18\x1b:\x19ITU Region 1 / Amateur 2m\x12,\n\x07ITU2_2M\x10\x1c\x1a\x1f\xca\xf3\x18\x1b:\x19ITU Region 2 / Amateur 2m\x12\'\n\x06\x45U_866\x10\x1d\x1a\x1b\xca\xf3\x18\x17:\x15\x45uropean Union 866MHz\x12\'\n\x06\x45U_874\x10\x1e\x1a\x1b\xca\xf3\x18\x17:\x15\x45uropean Union 874MHz\x12\'\n\x06\x45U_917\x10\x1f\x1a\x1b\xca\xf3\x18\x17:\x15\x45uropean Union 917MHz\x12\x32\n\x08\x45U_N_868\x10 \x1a$\xca\xf3\x18 :\x1e\x45uropean Union 868MHz (Narrow)\x12,\n\x07ITU3_2M\x10!\x1a\x1f\xca\xf3\x18\x1b:\x19ITU Region 3 / Amateur 2m\x12\x30\n\tITU1_70CM\x10\"\x1a!\xca\xf3\x18\x1d:\x1bITU Region 1 / Amateur 70cm\x12\x30\n\tITU2_70CM\x10#\x1a!\xca\xf3\x18\x1d:\x1bITU Region 2 / Amateur 70cm\x12\x30\n\tITU3_70CM\x10$\x1a!\xca\xf3\x18\x1d:\x1bITU Region 3 / Amateur 70cm\x12\x32\n\nITU2_125CM\x10%\x1a\"\xca\xf3\x18\x1e:\x1cITU Region 2 / Amateur 1.25m\"\xbd\x05\n\x0bModemPreset\x12\x38\n\tLONG_FAST\x10\x00\x1a)\xca\xf3\x18%:\x11Long Range - FastJ\x10longfast|default\x12(\n\tLONG_SLOW\x10\x01\x1a\x19\x08\x01\xca\xf3\x18\x13:\x11Long Range - Slow\x12\x16\n\x0eVERY_LONG_SLOW\x10\x02\x1a\x02\x08\x01\x12*\n\x0bMEDIUM_SLOW\x10\x03\x1a\x19\xca\xf3\x18\x15:\x13Medium Range - Slow\x12*\n\x0bMEDIUM_FAST\x10\x04\x1a\x19\xca\xf3\x18\x15:\x13Medium Range - Fast\x12(\n\nSHORT_SLOW\x10\x05\x1a\x18\xca\xf3\x18\x14:\x12Short Range - Slow\x12(\n\nSHORT_FAST\x10\x06\x1a\x18\xca\xf3\x18\x14:\x12Short Range - Fast\x12.\n\rLONG_MODERATE\x10\x07\x1a\x1b\xca\xf3\x18\x17:\x15Long Range - Moderate\x12*\n\x0bSHORT_TURBO\x10\x08\x1a\x19\xca\xf3\x18\x15:\x13Short Range - Turbo\x12(\n\nLONG_TURBO\x10\t\x1a\x18\xca\xf3\x18\x14:\x12Long Range - Turbo\x12 \n\tLITE_FAST\x10\n\x1a\x11\xca\xf3\x18\r:\x0bLite - Fast\x12 \n\tLITE_SLOW\x10\x0b\x1a\x11\xca\xf3\x18\r:\x0bLite - Slow\x12$\n\x0bNARROW_FAST\x10\x0c\x1a\x13\xca\xf3\x18\x0f:\rNarrow - Fast\x12$\n\x0bNARROW_SLOW\x10\r\x1a\x13\xca\xf3\x18\x0f:\rNarrow - Slow\x12 \n\tTINY_FAST\x10\x0e\x1a\x11\xca\xf3\x18\r:\x0bTiny - Fast\x12 \n\tTINY_SLOW\x10\x0f\x1a\x11\xca\xf3\x18\r:\x0bTiny - Slow\x12,\n\x0cMEDIUM_TURBO\x10\x10\x1a\x1a\xca\xf3\x18\x16:\x14Medium Range - Turbo\":\n\x0c\x46\x45M_LNA_Mode\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07\x45NABLED\x10\x01\x12\x0f\n\x0bNOT_PRESENT\x10\x02\x1a\xa9\x02\n\x0f\x42luetoothConfig\x12(\n\x07\x65nabled\x18\x01 \x01(\x08\x42\x17\xca\xf3\x18\x13:\x11\x42luetooth Enabled\x12P\n\x04mode\x18\x02 \x01(\x0e\x32..meshtastic.Config.BluetoothConfig.PairingModeB\x12\xca\xf3\x18\x0e:\x0cPairing Mode\x12\"\n\tfixed_pin\x18\x03 \x01(\rB\x0f\xca\xf3\x18\x0b:\tFixed Pin\"v\n\x0bPairingMode\x12 \n\nRANDOM_PIN\x10\x00\x1a\x10\xca\xf3\x18\x0c:\nRandom Pin\x12\x1e\n\tFIXED_PIN\x10\x01\x1a\x0f\xca\xf3\x18\x0b:\tFixed Pin\x12%\n\x06NO_PIN\x10\x02\x1a\x19\xca\xf3\x18\x15:\x13No PIN (Just Works)\x1a\xcc\x05\n\x0eSecurityConfig\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x13\n\x0bprivate_key\x18\x02 \x01(\x0c\x12\x11\n\tadmin_key\x18\x03 \x03(\x0c\x12\x8d\x01\n\nis_managed\x18\x04 \x01(\x08\x42y\xca\xf3\x18u:\x0eManaged DeviceBcDevice is managed by a mesh administrator, the user is unable to access any of the device settings.\x12Q\n\x0eserial_enabled\x18\x05 \x01(\x08\x42\x39\xca\xf3\x18\x35:\x0eSerial ConsoleB#Serial Console over the Stream API.\x12\x95\x01\n\x15\x64\x65\x62ug_log_api_enabled\x18\x06 \x01(\x08\x42v\xca\xf3\x18r:\nDebug LogsBdOutput live debug logging over serial, view and export position-redacted device logs over Bluetooth.\x12\x1d\n\x15\x61\x64min_channel_enabled\x18\x08 \x01(\x08\x12X\n\x17packet_signature_policy\x18\t \x01(\x0e\x32\x37.meshtastic.Config.SecurityConfig.PacketSignaturePolicy\"\x89\x01\n\x15PacketSignaturePolicy\x12&\n\"PACKET_SIGNATURE_POLICY_COMPATIBLE\x10\x00\x12$\n PACKET_SIGNATURE_POLICY_BALANCED\x10\x01\x12\"\n\x1ePACKET_SIGNATURE_POLICY_STRICT\x10\x02\x1a\x12\n\x10SessionkeyConfigB\x11\n\x0fpayload_variantBb\n\x14org.meshtastic.protoB\x0c\x43onfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3"
12
12
 
13
13
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
14
14
  pool.add_serialized_file(descriptor_data)