meshtastic 0.0.42 → 0.0.44
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/lib/meshtastic/mqtt.rb +94 -106
- data/lib/meshtastic/version.rb +1 -1
- data/lib/meshtastic.rb +187 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5dec0934f3c9c639e3af0cf5f8b88447dd83a96c5ed8082ff954ce13c5b758dd
|
4
|
+
data.tar.gz: c4a3e692fdc5f92a8e1ddf997ae9d5f7c6e33915dd239546d52f3adcee690a44
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c20d4c63ffdf13b5903f2e5605d5296268613b7bd7cc3380f48e4405e7b4aabda8216775f6664b42e5886dffdac42cfcb66b13bc1a48068e19f3152e1fa9598
|
7
|
+
data.tar.gz: a2ff5b9076561cb0b43b798d4bc6471f7ee2a4387be99bd53b98b389b4344954fbf451ce41be341c6e3a945b12adc02ba531883466fbb46cde30d8f7e0651c02
|
data/lib/meshtastic/mqtt.rb
CHANGED
@@ -64,120 +64,108 @@ module Meshtastic
|
|
64
64
|
end
|
65
65
|
|
66
66
|
# Supported Method Parameters::
|
67
|
-
# Meshtastic::MQQT.
|
68
|
-
#
|
67
|
+
# Meshtastic::MQQT.decode_payload(
|
68
|
+
# payload: 'required - payload to recursively decode',
|
69
69
|
# msg_type: 'required - message type (e.g. :TEXT_MESSAGE_APP)',
|
70
|
-
# gps_metadata: 'optional - include GPS metadata in output (default: false)'
|
70
|
+
# gps_metadata: 'optional - include GPS metadata in output (default: false)',
|
71
71
|
# )
|
72
72
|
|
73
|
-
|
74
|
-
|
73
|
+
public_class_method def self.decode_payload(opts = {})
|
74
|
+
payload = opts[:payload]
|
75
75
|
msg_type = opts[:msg_type]
|
76
76
|
gps_metadata = opts[:gps_metadata]
|
77
77
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
#
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
# decoder = Meshtastic::Reply
|
132
|
-
when :ROUTING_APP
|
133
|
-
decoder = Meshtastic::Routing
|
134
|
-
when :SERIAL_APP
|
135
|
-
decoder = Meshtastic::SerialConnectionStatus
|
136
|
-
when :SIMULATOR_APP
|
137
|
-
decoder = Meshtastic::Compressed
|
138
|
-
when :STORE_FORWARD_APP
|
139
|
-
decoder = Meshtastic::StoreAndForward
|
140
|
-
when :TEXT_MESSAGE_APP
|
141
|
-
# Unsure if this is the correct protobuf object
|
142
|
-
# decoder = Meshtastic::MqttClientProxyMessage
|
143
|
-
decoder = Meshtastic::Data
|
144
|
-
when :TELEMETRY_APP
|
145
|
-
decoder = Meshtastic::Telemetry
|
146
|
-
when :TRACEROUTE_APP
|
147
|
-
decoder = Meshtastic::RouteDiscovery
|
148
|
-
when :UNKNOWN_APP
|
149
|
-
decoder = Meshtastic::Data.decode
|
150
|
-
when :WAYPOINT_APP
|
151
|
-
decoder = Meshtastic::Waypoint
|
152
|
-
# when :ZPS_APP
|
153
|
-
# decoder = Meshtastic::Zps
|
154
|
-
else
|
155
|
-
puts "WARNING: Unknown message type: #{msg_type}"
|
156
|
-
decoder = Meshtastic::Data.decode
|
157
|
-
end
|
78
|
+
case msg_type
|
79
|
+
when :ADMIN_APP
|
80
|
+
decoder = Meshtastic::AdminMessage
|
81
|
+
when :ATAK_FORWARDER, :ATAK_PLUGIN
|
82
|
+
decoder = Meshtastic::TAKPacket
|
83
|
+
# when :AUDIO_APP
|
84
|
+
# decoder = Meshtastic::Audio
|
85
|
+
when :DETECTION_SENSOR_APP
|
86
|
+
decoder = Meshtastic::DeviceState
|
87
|
+
# when :IP_TUNNEL_APP
|
88
|
+
# decoder = Meshtastic::IpTunnel
|
89
|
+
when :MAP_REPORT_APP
|
90
|
+
decoder = Meshtastic::MapReport
|
91
|
+
# when :MAX
|
92
|
+
# decoder = Meshtastic::Max
|
93
|
+
when :NEIGHBORINFO_APP
|
94
|
+
decoder = Meshtastic::NeighborInfo
|
95
|
+
when :NODEINFO_APP
|
96
|
+
decoder = Meshtastic::User
|
97
|
+
when :PAXCOUNTER_APP
|
98
|
+
decoder = Meshtastic::Paxcount
|
99
|
+
when :POSITION_APP
|
100
|
+
decoder = Meshtastic::Position
|
101
|
+
# when :PRIVATE_APP
|
102
|
+
# decoder = Meshtastic::Private
|
103
|
+
when :RANGE_TEST_APP
|
104
|
+
# Unsure if this is the correct protobuf object
|
105
|
+
decoder = Meshtastic::FromRadio
|
106
|
+
when :REMOTE_HARDWARE_APP
|
107
|
+
decoder = Meshtastic::HardwareMessage
|
108
|
+
# when :REPLY_APP
|
109
|
+
# decoder = Meshtastic::Reply
|
110
|
+
when :ROUTING_APP
|
111
|
+
decoder = Meshtastic::Routing
|
112
|
+
when :SERIAL_APP
|
113
|
+
decoder = Meshtastic::SerialConnectionStatus
|
114
|
+
when :SIMULATOR_APP
|
115
|
+
decoder = Meshtastic::Compressed
|
116
|
+
when :STORE_FORWARD_APP
|
117
|
+
decoder = Meshtastic::StoreAndForward
|
118
|
+
when :TEXT_MESSAGE_APP
|
119
|
+
# Unsure if this is the correct protobuf object
|
120
|
+
# decoder = Meshtastic::MqttClientProxyMessage
|
121
|
+
decoder = Meshtastic::Data
|
122
|
+
when :TELEMETRY_APP
|
123
|
+
decoder = Meshtastic::Telemetry
|
124
|
+
when :TRACEROUTE_APP
|
125
|
+
decoder = Meshtastic::RouteDiscovery
|
126
|
+
when :WAYPOINT_APP
|
127
|
+
decoder = Meshtastic::Waypoint
|
128
|
+
# when :ZPS_APP
|
129
|
+
# decoder = Meshtastic::Zps
|
130
|
+
end
|
158
131
|
|
159
|
-
|
160
|
-
# # If the value is base64 encoded, base64 decode and then decode
|
161
|
-
# base64_decoded_value = Base64.strict_decode64(value)
|
162
|
-
# object[key] = decoder.decode(base64_decoded_value).to_h
|
163
|
-
# rescue ArgumentError => e
|
164
|
-
# # Otherwise, just decode the value
|
165
|
-
object[key] = decoder.decode(value).to_h
|
166
|
-
# end
|
132
|
+
payload = decoder.decode(payload).to_h
|
167
133
|
|
168
|
-
|
134
|
+
if payload.keys.include?(:latitude_i)
|
135
|
+
lat = payload[:latitude_i] * 0.0000001
|
136
|
+
payload[:latitude] = lat
|
137
|
+
end
|
138
|
+
|
139
|
+
if payload.keys.include?(:longitude_i)
|
140
|
+
lon = payload[:longitude_i] * 0.0000001
|
141
|
+
payload[:longitude] = lon
|
142
|
+
end
|
169
143
|
|
170
|
-
|
144
|
+
if payload.keys.include?(:macaddr)
|
145
|
+
mac_raw = payload[:macaddr]
|
146
|
+
mac_hex_arr = mac_raw.bytes.map { |byte| byte.to_s(16).rjust(2, '0') }
|
147
|
+
mac_hex_str = mac_hex_arr.join(':')
|
148
|
+
payload[:macaddr_hex] = mac_hex_str
|
149
|
+
end
|
171
150
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
151
|
+
if payload.keys.include?(:time)
|
152
|
+
time_int = payload[:time]
|
153
|
+
time_utc = Time.at(time_int).utc if time_int.is_a?(Integer)
|
154
|
+
payload[:time_utc] = time_utc
|
155
|
+
end
|
156
|
+
|
157
|
+
if gps_metadata && payload[:latitude] && payload[:longitude]
|
158
|
+
lat = payload[:latitude]
|
159
|
+
lon = payload[:longitude]
|
160
|
+
unless lat.zero? && lon.zero?
|
161
|
+
gps_search_resp = gps_search(lat: lat, lon: lon)
|
162
|
+
payload[:gps_metadata] = gps_search_resp
|
163
|
+
end
|
176
164
|
end
|
177
165
|
|
178
|
-
|
166
|
+
payload
|
179
167
|
rescue Google::Protobuf::ParseError
|
180
|
-
|
168
|
+
payload
|
181
169
|
rescue StandardError => e
|
182
170
|
raise e
|
183
171
|
end
|
@@ -256,8 +244,8 @@ module Meshtastic
|
|
256
244
|
nonce = "#{nonce_packet_id}#{nonce_from_node}".b
|
257
245
|
|
258
246
|
psk = psks[:LongFast]
|
259
|
-
|
260
|
-
psk = psks[
|
247
|
+
target_channel = decoded_payload_hash[:channel_id].to_s.to_sym
|
248
|
+
psk = psks[target_channel] if psks.keys.include?(target_channel)
|
261
249
|
dec_psk = Base64.strict_decode64(psk)
|
262
250
|
|
263
251
|
cipher = OpenSSL::Cipher.new('AES-128-CTR')
|
@@ -275,8 +263,8 @@ module Meshtastic
|
|
275
263
|
# payload = Meshtastic::Data.decode(message[:decoded][:payload]).to_h
|
276
264
|
payload = message[:decoded][:payload]
|
277
265
|
msg_type = message[:decoded][:portnum]
|
278
|
-
message[:decoded][:payload] =
|
279
|
-
|
266
|
+
message[:decoded][:payload] = decode_payload(
|
267
|
+
payload: payload,
|
280
268
|
msg_type: msg_type,
|
281
269
|
gps_metadata: gps_metadata
|
282
270
|
)
|
@@ -347,7 +335,7 @@ module Meshtastic
|
|
347
335
|
|
348
336
|
gps_arr = [lat.to_f, lon.to_f]
|
349
337
|
|
350
|
-
Geocoder.search(gps_arr)
|
338
|
+
Geocoder.search(gps_arr).first.data
|
351
339
|
rescue StandardError => e
|
352
340
|
raise e
|
353
341
|
end
|
data/lib/meshtastic/version.rb
CHANGED
data/lib/meshtastic.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
# Plugin used to interact with Meshtastic nodes
|
4
4
|
module Meshtastic
|
5
|
+
require 'base64'
|
5
6
|
# Protocol Buffers for Meshtastic
|
6
7
|
require 'meshtastic/admin_pb'
|
7
8
|
require 'nanopb_pb'
|
@@ -25,6 +26,7 @@ module Meshtastic
|
|
25
26
|
require 'meshtastic/telemetry_pb'
|
26
27
|
require 'meshtastic/version'
|
27
28
|
require 'meshtastic/xmodem_pb'
|
29
|
+
require 'openssl'
|
28
30
|
|
29
31
|
autoload :Admin, 'meshtastic/admin'
|
30
32
|
autoload :Apponly, 'meshtastic/apponly'
|
@@ -47,6 +49,191 @@ module Meshtastic
|
|
47
49
|
autoload :Telemetry, 'meshtastic/telemetry'
|
48
50
|
autoload :Xmodem, 'meshtastic/xmodem'
|
49
51
|
|
52
|
+
# Supported Method Parameters::
|
53
|
+
# Meshtastic.send_text(
|
54
|
+
# from_id: 'optional - Source ID (Default: 0)',
|
55
|
+
# dest_id: 'optional - Destination ID (Default: 0xFFFFFFFF)',
|
56
|
+
# text: 'optional - Text Message (Default: SYN)',
|
57
|
+
# want_ack: 'optional - Want Acknowledgement (Default: false)',
|
58
|
+
# want_response: 'optional - Want Response (Default: false)',
|
59
|
+
# hop_limit: 'optional - Hop Limit (Default: 3)',
|
60
|
+
# on_response: 'optional - Callback on Response',
|
61
|
+
# psk: 'optional - Pre-Shared Key if Encrypted (Default: nil)'
|
62
|
+
# )
|
63
|
+
public_class_method def self.send_text(opts = {})
|
64
|
+
# Send a text message to a node
|
65
|
+
from_id = opts[:from_id].to_i
|
66
|
+
dest_id = opts[:dest_id] ||= 0xFFFFFFFF
|
67
|
+
text = opts[:text] ||= 'SYN'
|
68
|
+
want_ack = opts[:want_ack] ||= false
|
69
|
+
want_response = opts[:want_response] ||= false
|
70
|
+
hop_limit = opts[:hop_limit] ||= 3
|
71
|
+
on_response = opts[:on_response]
|
72
|
+
psk = opts[:psk]
|
73
|
+
|
74
|
+
# TODO: verify text length validity
|
75
|
+
max_txt_len = Meshtastic::Constants::DATA_PAYLOAD_LEN
|
76
|
+
raise "ERROR: Text Length > #{max_txt_len} Bytes" if text.length > max_txt_len
|
77
|
+
|
78
|
+
port_num = Meshtastic::PortNum::TEXT_MESSAGE_APP
|
79
|
+
|
80
|
+
data = Meshtastic::Data.new
|
81
|
+
data.payload = text
|
82
|
+
data.portnum = port_num
|
83
|
+
data.want_response = want_response
|
84
|
+
puts data.to_h
|
85
|
+
|
86
|
+
send_data(
|
87
|
+
from_id: from_id,
|
88
|
+
dest_id: dest_id,
|
89
|
+
data: data,
|
90
|
+
want_ack: want_ack,
|
91
|
+
want_response: want_response,
|
92
|
+
hop_limit: hop_limit,
|
93
|
+
port_num: port_num,
|
94
|
+
on_response: on_response,
|
95
|
+
psk: psk
|
96
|
+
)
|
97
|
+
rescue StandardError => e
|
98
|
+
raise e
|
99
|
+
end
|
100
|
+
|
101
|
+
# Supported Method Parameters::
|
102
|
+
# Meshtastic.send_data(
|
103
|
+
# from_id: 'optional - Source ID (Default: 0)',
|
104
|
+
# dest_id: 'optional - Destination ID (Default: 0xFFFFFFFF)',
|
105
|
+
# data: 'required - Data to Send',
|
106
|
+
# want_ack: 'optional - Want Acknowledgement (Default: false)',
|
107
|
+
# hop_limit: 'optional - Hop Limit (Default: 3)',
|
108
|
+
# port_num: 'optional - (Default: Meshtastic::PortNum::PRIVATE_APP)',
|
109
|
+
# psk: 'optional - Pre-Shared Key if Encrypted (Default: nil)'
|
110
|
+
# )
|
111
|
+
public_class_method def self.send_data(opts = {})
|
112
|
+
# Send a text message to a node
|
113
|
+
from_id = opts[:from_id].to_i
|
114
|
+
dest_id = opts[:dest_id] ||= 0xFFFFFFFF
|
115
|
+
data = opts[:data]
|
116
|
+
want_ack = opts[:want_ack] ||= false
|
117
|
+
hop_limit = opts[:hop_limit] ||= 3
|
118
|
+
port_num = opts[:port_num] ||= Meshtastic::PortNum::PRIVATE_APP
|
119
|
+
max_port_num = Meshtastic::PortNum::MAX
|
120
|
+
raise "ERROR: Invalid port_num" unless port_num.positive? && port_num < max_port_num
|
121
|
+
|
122
|
+
psk = opts[:psk]
|
123
|
+
|
124
|
+
data_len = data.payload.length
|
125
|
+
max_len = Meshtastic::Constants::DATA_PAYLOAD_LEN
|
126
|
+
raise "ERROR: Data Length > #{max_len} Bytes" if data_len > max_len
|
127
|
+
|
128
|
+
mesh_packet = Meshtastic::MeshPacket.new
|
129
|
+
mesh_packet.decoded = data
|
130
|
+
|
131
|
+
send_packet(
|
132
|
+
mesh_packet: mesh_packet,
|
133
|
+
from_id: from_id,
|
134
|
+
dest_id: dest_id,
|
135
|
+
want_ack: want_ack,
|
136
|
+
hop_limit: hop_limit,
|
137
|
+
psk: psk
|
138
|
+
)
|
139
|
+
rescue StandardError => e
|
140
|
+
raise e
|
141
|
+
end
|
142
|
+
|
143
|
+
# Supported Method Parameters::
|
144
|
+
# Meshtastic.send_packet(
|
145
|
+
# mesh_packet: 'required - Mesh Packet to Send',
|
146
|
+
# from_id: 'optional - Source ID (Default: 0)',
|
147
|
+
# dest_id: 'optional - Destination ID (Default: 0xFFFFFFFF)',
|
148
|
+
# want_ack: 'optional - Want Acknowledgement (Default: false)',
|
149
|
+
# hop_limit: 'optional - Hop Limit (Default: 3)',
|
150
|
+
# psk: 'optional - Pre-Shared Key if Encrypted (Default: nil)'
|
151
|
+
# )
|
152
|
+
public_class_method def self.send_packet(opts = {})
|
153
|
+
mesh_packet = opts[:mesh_packet]
|
154
|
+
from_id = opts[:from_id] ||= 0
|
155
|
+
dest_id = opts[:dest_id] ||= 0xFFFFFFFF
|
156
|
+
want_ack = opts[:want_ack] ||= false
|
157
|
+
hop_limit = opts[:hop_limit] ||= 3
|
158
|
+
psk = opts[:psk]
|
159
|
+
|
160
|
+
# my_info = Meshtastic::FromRadio.my_info
|
161
|
+
# wait_connected if dest_id != my_info.my_node_num && my_info.is_a(Meshtastic::Deviceonly::MyInfo)
|
162
|
+
|
163
|
+
node_num = dest_id
|
164
|
+
node_num_hex = dest_id.bytes.map { |b| b.to_s(16).rjust(2, '0') }.join if dest_id.is_a?(String)
|
165
|
+
node_num = node_num_hex.to_i(16) if node_num_hex
|
166
|
+
|
167
|
+
mesh_packet.from = from_id
|
168
|
+
mesh_packet.to = node_num
|
169
|
+
mesh_packet.want_ack = want_ack
|
170
|
+
mesh_packet.hop_limit = hop_limit
|
171
|
+
|
172
|
+
mesh_packet.id = generate_packet_id if mesh_packet.id.zero?
|
173
|
+
|
174
|
+
if psk
|
175
|
+
nonce_packet_id = [mesh_packet.id].pack('V').ljust(8, "\x00")
|
176
|
+
nonce_from_node = [from_id].pack('V').ljust(8, "\x00")
|
177
|
+
nonce = "#{nonce_packet_id}#{nonce_from_node}".b
|
178
|
+
|
179
|
+
dec_psk = Base64.strict_decode64(psk)
|
180
|
+
cipher = OpenSSL::Cipher.new('AES-128-CTR')
|
181
|
+
cipher = OpenSSL::Cipher.new('AES-256-CTR') if dec_psk.length == 32
|
182
|
+
cipher.encrypt
|
183
|
+
cipher.key = dec_psk
|
184
|
+
cipher.iv = nonce
|
185
|
+
|
186
|
+
decrypted_payload = mesh_packet.decoded.to_s
|
187
|
+
encrypted_payload = cipher.update(decrypted_payload) + cipher.final
|
188
|
+
|
189
|
+
mesh_packet.encrypted = encrypted_payload
|
190
|
+
end
|
191
|
+
# puts mesh_packet.to_h
|
192
|
+
|
193
|
+
# to_radio = Meshtastic::ToRadio.new
|
194
|
+
# to_radio.packet = mesh_packet
|
195
|
+
# send_to_radio(to_radio: to_radio)
|
196
|
+
|
197
|
+
mesh_packet
|
198
|
+
rescue StandardError => e
|
199
|
+
raise e
|
200
|
+
end
|
201
|
+
|
202
|
+
# Supported Method Parameters::
|
203
|
+
# packet_id = Meshtastic.generate_packet_id(
|
204
|
+
# last_packet_id: 'optional - Last Packet ID (Default: 0)'
|
205
|
+
# )
|
206
|
+
public_class_method def self.generate_packet_id(opts = {})
|
207
|
+
last_packet_id = opts[:last_packet_id] ||= 0
|
208
|
+
|
209
|
+
packet_id = last_packet_id + 1 if last_packet_id.positive?
|
210
|
+
packet_id = rand(2**32) if last_packet_id.zero?
|
211
|
+
|
212
|
+
packet_id
|
213
|
+
end
|
214
|
+
|
215
|
+
# Supported Method Parameters::
|
216
|
+
# Meshtastic.send_to_radio(
|
217
|
+
# to_radio: 'required - ToRadio Message to Send'
|
218
|
+
# )
|
219
|
+
public_class_method def self.send_to_radio(opts = {})
|
220
|
+
to_radio = opts[:to_radio]
|
221
|
+
|
222
|
+
raise 'ERROR: Invalid ToRadio Message' unless to_radio.is_a?(Meshtastic::ToRadio)
|
223
|
+
|
224
|
+
to_radio.to_proto
|
225
|
+
rescue StandardError => e
|
226
|
+
raise e
|
227
|
+
end
|
228
|
+
|
229
|
+
# Author(s):: 0day Inc. <support@0dayinc.com>
|
230
|
+
|
231
|
+
public_class_method def self.authors
|
232
|
+
"AUTHOR(S):
|
233
|
+
0day Inc. <support@0dayinc.com>
|
234
|
+
"
|
235
|
+
end
|
236
|
+
|
50
237
|
# Display a List of Every Meshtastic Module
|
51
238
|
|
52
239
|
public_class_method def self.help
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: meshtastic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.44
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 0day Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-05-
|
11
|
+
date: 2024-05-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -348,7 +348,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
348
348
|
- !ruby/object:Gem::Version
|
349
349
|
version: '0'
|
350
350
|
requirements: []
|
351
|
-
rubygems_version: 3.5.
|
351
|
+
rubygems_version: 3.5.10
|
352
352
|
signing_key:
|
353
353
|
specification_version: 4
|
354
354
|
summary: Ruby gem for Meshtastic
|