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.
- checksums.yaml +4 -4
- data/documentation/README.md +1 -0
- data/documentation/admin-backup.md +369 -0
- data/documentation/admin.md +4 -0
- data/lib/meshtastic/admin/backup.rb +560 -0
- data/lib/meshtastic/admin.rb +2 -0
- data/lib/meshtastic/config_pb.rb +1 -1
- data/lib/meshtastic/mesh_pb.rb +1 -1
- data/lib/meshtastic/module_config_pb.rb +2 -1
- data/lib/meshtastic/version.rb +1 -1
- data/spec/lib/meshtastic/admin/backup_spec.rb +704 -0
- metadata +4 -1
|
@@ -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
|
data/lib/meshtastic/admin.rb
CHANGED
data/lib/meshtastic/config_pb.rb
CHANGED
|
@@ -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)
|