meshtastic 0.0.178 → 0.0.180
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/README.md +3 -175
- data/documentation/README.md +60 -0
- data/documentation/admin-channel.md +33 -0
- data/documentation/admin-config.md +28 -0
- data/documentation/admin-firmware.md +69 -0
- data/documentation/admin.md +55 -0
- data/documentation/apponly.md +22 -0
- data/documentation/atak.md +110 -0
- data/documentation/bluetooth-bluez.md +35 -0
- data/documentation/bluetooth.md +87 -0
- data/documentation/cannedmessages.md +25 -0
- data/documentation/channel.md +3 -0
- data/documentation/clientonly.md +21 -0
- data/documentation/config.md +3 -0
- data/documentation/connection-status.md +22 -0
- data/documentation/deviceonly.md +22 -0
- data/documentation/localonly.md +21 -0
- data/documentation/mesh-interface.md +52 -0
- data/documentation/meshtastic.md +45 -0
- data/documentation/module-config.md +26 -0
- data/documentation/mqtt.md +101 -0
- data/documentation/paxcount.md +25 -0
- data/documentation/portnums.md +67 -0
- data/documentation/position.md +34 -0
- data/documentation/protobufs.md +162 -0
- data/documentation/remote-hardware.md +33 -0
- data/documentation/rtttl.md +24 -0
- data/documentation/serial.md +78 -0
- data/documentation/storeforward.md +24 -0
- data/documentation/stream-interface.md +15 -0
- data/documentation/tcp.md +43 -0
- data/documentation/telemetry.md +32 -0
- data/documentation/traceroute.md +28 -0
- data/documentation/util.md +27 -0
- data/documentation/xmodem.md +22 -0
- data/lib/meshtastic/admin/channel.rb +90 -0
- data/lib/meshtastic/admin/config.rb +154 -0
- data/lib/meshtastic/admin/firmware.rb +173 -0
- data/lib/meshtastic/admin.rb +495 -7
- data/lib/meshtastic/apponly.rb +26 -8
- data/lib/meshtastic/atak.rb +298 -7
- data/lib/meshtastic/bluetooth/bluez.rb +20 -1
- data/lib/meshtastic/bluetooth.rb +70 -59
- data/lib/meshtastic/cannedmessages.rb +23 -8
- data/lib/meshtastic/channel.rb +3 -21
- data/lib/meshtastic/clientonly.rb +26 -8
- data/lib/meshtastic/config.rb +3 -21
- data/lib/meshtastic/connection_status.rb +13 -8
- data/lib/meshtastic/deviceonly.rb +23 -8
- data/lib/meshtastic/localonly.rb +23 -8
- data/lib/meshtastic/mesh_interface.rb +11 -0
- data/lib/meshtastic/module_config.rb +18 -9
- data/lib/meshtastic/mqtt.rb +72 -35
- data/lib/meshtastic/paxcount.rb +30 -9
- data/lib/meshtastic/portnums.rb +18 -8
- data/lib/meshtastic/position.rb +45 -0
- data/lib/meshtastic/remote_hardware.rb +50 -9
- data/lib/meshtastic/rtttl.rb +26 -8
- data/lib/meshtastic/serial.rb +81 -70
- data/lib/meshtastic/storeforward.rb +23 -8
- data/lib/meshtastic/stream_interface.rb +11 -0
- data/lib/meshtastic/tcp.rb +153 -0
- data/lib/meshtastic/telemetry.rb +27 -9
- data/lib/meshtastic/traceroute.rb +42 -0
- data/lib/meshtastic/util.rb +3 -1
- data/lib/meshtastic/version.rb +1 -1
- data/lib/meshtastic/xmodem.rb +17 -8
- data/lib/meshtastic.rb +46 -6
- data/spec/conventions_spec.rb +321 -0
- data/spec/lib/meshtastic/admin/channel_spec.rb +45 -0
- data/spec/lib/meshtastic/admin/config_spec.rb +48 -0
- data/spec/lib/meshtastic/admin/firmware_spec.rb +118 -0
- data/spec/lib/meshtastic/admin_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/admin_spec.rb +79 -0
- data/spec/lib/meshtastic/apponly_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/apponly_spec.rb +4 -0
- data/spec/lib/meshtastic/atak_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/atak_spec.rb +147 -0
- data/spec/lib/meshtastic/bluetooth_spec.rb +1 -1
- data/spec/lib/meshtastic/cannedmessages_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/cannedmessages_spec.rb +3 -0
- data/spec/lib/meshtastic/channel_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/channel_spec.rb +3 -0
- data/spec/lib/meshtastic/clientonly_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/clientonly_spec.rb +5 -0
- data/spec/lib/meshtastic/config_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/config_spec.rb +4 -1
- data/spec/lib/meshtastic/connection_status_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/connection_status_spec.rb +3 -0
- data/spec/lib/meshtastic/deviceonly_legacy_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/deviceonly_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/deviceonly_spec.rb +3 -0
- data/spec/lib/meshtastic/localonly_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/localonly_spec.rb +3 -0
- data/spec/lib/meshtastic/mesh_beacon_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/mesh_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/module_config_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/module_config_spec.rb +21 -0
- data/spec/lib/meshtastic/mqtt_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/paxcount_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/paxcount_spec.rb +23 -0
- data/spec/lib/meshtastic/portnums_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/portnums_spec.rb +4 -0
- data/spec/lib/meshtastic/position_spec.rb +33 -0
- data/spec/lib/meshtastic/remote_hardware_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/remote_hardware_spec.rb +24 -0
- data/spec/lib/meshtastic/rtttl_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/rtttl_spec.rb +21 -0
- data/spec/lib/meshtastic/storeforward_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/storeforward_spec.rb +21 -0
- data/spec/lib/meshtastic/tcp_spec.rb +48 -0
- data/spec/lib/meshtastic/telemetry_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/telemetry_spec.rb +21 -0
- data/spec/lib/meshtastic/traceroute_spec.rb +27 -0
- data/spec/lib/meshtastic/xmodem_pb_spec.rb +1 -1
- data/spec/lib/meshtastic/xmodem_spec.rb +6 -1
- data/spec/lib/meshtastic_spec.rb +3 -0
- metadata +48 -1
data/lib/meshtastic.rb
CHANGED
|
@@ -12,11 +12,13 @@ module Meshtastic
|
|
|
12
12
|
require 'meshtastic/clientonly_pb'
|
|
13
13
|
require 'meshtastic/config_pb'
|
|
14
14
|
require 'meshtastic/connection_status_pb'
|
|
15
|
+
require 'meshtastic/deviceonly_legacy_pb'
|
|
15
16
|
require 'meshtastic/deviceonly_pb'
|
|
16
17
|
require 'meshtastic/device_ui_pb'
|
|
17
18
|
require 'meshtastic/interdevice_pb'
|
|
18
19
|
require 'meshtastic/localonly_pb'
|
|
19
20
|
require 'meshtastic/lorawan_bridge_pb'
|
|
21
|
+
require 'meshtastic/mesh_beacon_pb'
|
|
20
22
|
require 'meshtastic/mesh_pb'
|
|
21
23
|
require 'meshtastic/module_config_pb'
|
|
22
24
|
require 'meshtastic/mqtt_pb'
|
|
@@ -25,6 +27,7 @@ module Meshtastic
|
|
|
25
27
|
require 'meshtastic/powermon_pb'
|
|
26
28
|
require 'meshtastic/remote_hardware_pb'
|
|
27
29
|
require 'meshtastic/rtttl_pb'
|
|
30
|
+
require 'meshtastic/serial_hal_pb'
|
|
28
31
|
require 'meshtastic/storeforward_pb'
|
|
29
32
|
require 'meshtastic/telemetry_pb'
|
|
30
33
|
require 'meshtastic/version'
|
|
@@ -38,23 +41,20 @@ module Meshtastic
|
|
|
38
41
|
autoload :ATAK, 'meshtastic/atak'
|
|
39
42
|
autoload :Bluetooth, 'meshtastic/bluetooth'
|
|
40
43
|
autoload :Cannedmessages, 'meshtastic/cannedmessages'
|
|
41
|
-
autoload :Channel, 'meshtastic/channel'
|
|
42
44
|
autoload :Clientonly, 'meshtastic/clientonly'
|
|
43
|
-
autoload :Config, 'meshtastic/config'
|
|
44
45
|
autoload :ConnectionStatus, 'meshtastic/connection_status'
|
|
45
46
|
autoload :Deviceonly, 'meshtastic/deviceonly'
|
|
46
47
|
autoload :Localonly, 'meshtastic/localonly'
|
|
47
48
|
autoload :MeshInterface, 'meshtastic/mesh_interface'
|
|
48
|
-
autoload :ModuleConfig, 'meshtastic/module_config'
|
|
49
49
|
autoload :MQTT, 'meshtastic/mqtt'
|
|
50
|
-
autoload :Paxcount, 'meshtastic/paxcount'
|
|
51
50
|
autoload :Portnums, 'meshtastic/portnums'
|
|
52
51
|
autoload :RemoteHardware, 'meshtastic/remote_hardware'
|
|
53
52
|
autoload :RTTTL, 'meshtastic/rtttl'
|
|
54
53
|
autoload :Serial, 'meshtastic/serial'
|
|
55
54
|
autoload :Storeforward, 'meshtastic/storeforward'
|
|
56
55
|
autoload :StreamInterface, 'meshtastic/stream_interface'
|
|
57
|
-
autoload :
|
|
56
|
+
autoload :TCP, 'meshtastic/tcp'
|
|
57
|
+
autoload :Traceroute, 'meshtastic/traceroute'
|
|
58
58
|
autoload :Util, 'meshtastic/util'
|
|
59
59
|
autoload :Xmodem, 'meshtastic/xmodem'
|
|
60
60
|
|
|
@@ -67,7 +67,47 @@ module Meshtastic
|
|
|
67
67
|
|
|
68
68
|
# Display a List of Every Meshtastic Module
|
|
69
69
|
|
|
70
|
+
public_class_method def self.authors
|
|
71
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
|
|
72
|
+
end
|
|
73
|
+
|
|
70
74
|
public_class_method def self.help
|
|
71
|
-
|
|
75
|
+
puts " USAGE:
|
|
76
|
+
# Run the deliver_data class method for this module.
|
|
77
|
+
#{self}.deliver_data(
|
|
78
|
+
data: 'optional - value for data passed into deliver_data',
|
|
79
|
+
serial_obj: 'optional - value for serial_obj passed into deliver_data',
|
|
80
|
+
bluetooth_obj: 'optional - value for bluetooth_obj passed into deliver_data',
|
|
81
|
+
tcp_obj: 'optional - value for tcp_obj passed into deliver_data',
|
|
82
|
+
mqtt_obj: 'optional - value for mqtt_obj passed into deliver_data'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Run the authors class method for this module.
|
|
86
|
+
#{self}.authors
|
|
87
|
+
|
|
88
|
+
"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
public_class_method def self.deliver_data(opts = {})
|
|
92
|
+
raise ArgumentError, 'data is required' unless opts[:data].is_a?(Meshtastic::Data)
|
|
93
|
+
|
|
94
|
+
if opts[:serial_obj]
|
|
95
|
+
Serial.send_data(opts)
|
|
96
|
+
elsif opts[:bluetooth_obj]
|
|
97
|
+
Bluetooth.send_data(opts)
|
|
98
|
+
elsif opts[:tcp_obj]
|
|
99
|
+
TCP.send_data(opts)
|
|
100
|
+
elsif opts[:mqtt_obj]
|
|
101
|
+
MQTT.send_data(opts)
|
|
102
|
+
else
|
|
103
|
+
raise ArgumentError, 'serial_obj, bluetooth_obj, tcp_obj, or mqtt_obj is required'
|
|
104
|
+
end
|
|
72
105
|
end
|
|
73
106
|
end
|
|
107
|
+
|
|
108
|
+
require 'meshtastic/channel'
|
|
109
|
+
require 'meshtastic/config'
|
|
110
|
+
require 'meshtastic/module_config'
|
|
111
|
+
require 'meshtastic/paxcount'
|
|
112
|
+
require 'meshtastic/position'
|
|
113
|
+
require 'meshtastic/telemetry'
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
# Global source-level conventions for every Ruby module under lib/meshtastic
|
|
6
|
+
# (same rules as /opt/pwn/spec/conventions_spec.rb).
|
|
7
|
+
#
|
|
8
|
+
# RULES
|
|
9
|
+
# 0. Every method on a Meshtastic namespace *module* MUST be referenced via
|
|
10
|
+
# `self.<method_name>` with public_class_method / private_class_method.
|
|
11
|
+
# Bare `def foo` at module scope fails. So does `module_function`.
|
|
12
|
+
# Nested `class` instance methods and class_eval / `class <<` are exempt.
|
|
13
|
+
# 1. Module methods MUST be declared with an explicit visibility decorator
|
|
14
|
+
# immediately preceding `def self.<name>` on the same line.
|
|
15
|
+
# 2. Methods that accept arguments MUST take exactly `(opts = {})`.
|
|
16
|
+
# 3. Methods declared `(opts = {})` MUST consume opts in the body.
|
|
17
|
+
# 4. Every leaf module file MUST define `def self.help`.
|
|
18
|
+
# 5. Every leaf module file MUST define `def self.authors`.
|
|
19
|
+
# 6. Every public class method except `help` MUST appear in that module's
|
|
20
|
+
# `help` method — including `authors`. Directly above each method, `help`
|
|
21
|
+
# MUST print a brief purpose line. Methods that take `(opts = {})` MUST
|
|
22
|
+
# document each `opts[:key]` they read as `required` or `optional` plus an
|
|
23
|
+
# operator-usable description — same shape as PWN::WWW::Google.help.
|
|
24
|
+
|
|
25
|
+
module MeshtasticConventions
|
|
26
|
+
ROOT = File.expand_path('..', __dir__)
|
|
27
|
+
LIB = File.join(ROOT, 'lib', 'meshtastic')
|
|
28
|
+
|
|
29
|
+
GLOBAL_ALLOWLIST = [
|
|
30
|
+
'lib/meshtastic/version.rb' # VERSION constant only
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
NAMESPACE_INDEX_FILES = [].freeze
|
|
34
|
+
|
|
35
|
+
RB_FILES = (
|
|
36
|
+
Dir[File.join(LIB, '**', '*.rb')] + [File.join(ROOT, 'lib', 'meshtastic.rb')]
|
|
37
|
+
).reject do |f|
|
|
38
|
+
rel = f.sub("#{ROOT}/", '')
|
|
39
|
+
GLOBAL_ALLOWLIST.include?(rel) || rel.end_with?('_pb.rb')
|
|
40
|
+
end.sort.freeze
|
|
41
|
+
|
|
42
|
+
MODULE_FILES = RB_FILES.select { |f| File.read(f).match?(/^\s*module\s+\w/) }.freeze
|
|
43
|
+
|
|
44
|
+
SELF_DEF_RE = /^([ \t]*)((?:public_class_method|private_class_method)\s+)?def self\.([a-z_][\w?!]*)(?:\(([^)]*)\))?/
|
|
45
|
+
|
|
46
|
+
BARE_DEF_RE = /^[ \t]*def ([a-z_][\w?!]*)(?:\(([^)]*)\))?/
|
|
47
|
+
|
|
48
|
+
MODULE_FUNCTION_RE = /^\s*module_function\b/
|
|
49
|
+
|
|
50
|
+
CLASS_LIKE_OPEN_RE = /
|
|
51
|
+
\Aclass\b
|
|
52
|
+
| \.class_eval\b
|
|
53
|
+
| \.module_eval\b
|
|
54
|
+
| \Aclass_eval\b
|
|
55
|
+
| \Amodule_eval\b
|
|
56
|
+
| \Aclass\s*<<
|
|
57
|
+
| \bClass\.new\b
|
|
58
|
+
| \bStruct\.new\b
|
|
59
|
+
| \bModule\.new\b
|
|
60
|
+
| \bData\.define\b
|
|
61
|
+
/x
|
|
62
|
+
|
|
63
|
+
MODULE_OPEN_RE = /\Amodule\s+[A-Z]/
|
|
64
|
+
|
|
65
|
+
module_function
|
|
66
|
+
|
|
67
|
+
def line_indent(line)
|
|
68
|
+
line[/^[ \t]*/].to_s.length
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def scan_methods(path)
|
|
72
|
+
src = File.read(path)
|
|
73
|
+
lines = src.lines
|
|
74
|
+
out = []
|
|
75
|
+
lines.each_with_index do |line, idx|
|
|
76
|
+
m = line.match(SELF_DEF_RE)
|
|
77
|
+
next unless m
|
|
78
|
+
|
|
79
|
+
out << {
|
|
80
|
+
file: path,
|
|
81
|
+
line: idx + 1,
|
|
82
|
+
decorator: m[2]&.strip,
|
|
83
|
+
name: m[3],
|
|
84
|
+
arglist: m[4],
|
|
85
|
+
body_excerpt: lines[(idx + 1)..(idx + 60)]&.join.to_s,
|
|
86
|
+
method_body: method_body_excerpt(lines, idx)
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
out
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def method_body_excerpt(lines, idx)
|
|
93
|
+
indent = lines[idx][/^[ \t]*/]
|
|
94
|
+
buf = []
|
|
95
|
+
((idx + 1)...lines.length).each do |i|
|
|
96
|
+
break if lines[i].match?(/^#{Regexp.escape(indent)}end\b/)
|
|
97
|
+
|
|
98
|
+
buf << lines[i]
|
|
99
|
+
break if buf.length >= 120
|
|
100
|
+
end
|
|
101
|
+
buf.join
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def scan_bare_defs(path)
|
|
105
|
+
stack = []
|
|
106
|
+
out = []
|
|
107
|
+
File.readlines(path).each_with_index do |line, idx|
|
|
108
|
+
stripped = line.strip
|
|
109
|
+
next if stripped.empty? || stripped.start_with?('#')
|
|
110
|
+
|
|
111
|
+
indent = line_indent(line)
|
|
112
|
+
|
|
113
|
+
if stripped == 'end' || stripped.match?(/\Aend\b/)
|
|
114
|
+
stack.pop while stack.any? && stack.last[:indent] >= indent
|
|
115
|
+
next
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if stripped.match?(MODULE_OPEN_RE) && !stripped.match?(/\bend\b/)
|
|
119
|
+
stack.pop while stack.any? && stack.last[:indent] >= indent
|
|
120
|
+
stack << { type: :module, indent: indent }
|
|
121
|
+
next
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if stripped.match?(CLASS_LIKE_OPEN_RE) && !stripped.match?(/\bend\b/)
|
|
125
|
+
stack.pop while stack.any? && stack.last[:indent] >= indent
|
|
126
|
+
stack << { type: :class, indent: indent }
|
|
127
|
+
next
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
next if line.match?(SELF_DEF_RE)
|
|
131
|
+
|
|
132
|
+
m = line.match(BARE_DEF_RE)
|
|
133
|
+
next unless m
|
|
134
|
+
|
|
135
|
+
container = stack.rfind { |frame| frame[:indent] < indent }
|
|
136
|
+
next if container.nil?
|
|
137
|
+
next if container[:type] == :class
|
|
138
|
+
|
|
139
|
+
out << {
|
|
140
|
+
file: path,
|
|
141
|
+
line: idx + 1,
|
|
142
|
+
name: m[1],
|
|
143
|
+
arglist: m[2]
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
out
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def scan_module_function(path)
|
|
150
|
+
out = []
|
|
151
|
+
File.readlines(path).each_with_index do |line, idx|
|
|
152
|
+
next if line.match?(/^\s*#/)
|
|
153
|
+
next unless line.match?(MODULE_FUNCTION_RE)
|
|
154
|
+
|
|
155
|
+
out << { file: path, line: idx + 1 }
|
|
156
|
+
end
|
|
157
|
+
out
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
ALL_METHODS = MODULE_FILES.flat_map { |f| scan_methods(f) }.freeze
|
|
161
|
+
ALL_BARE_DEFS = MODULE_FILES.flat_map { |f| scan_bare_defs(f) }.freeze
|
|
162
|
+
ALL_MODULE_FUNCTIONS = MODULE_FILES.flat_map { |f| scan_module_function(f) }.freeze
|
|
163
|
+
|
|
164
|
+
HELP_SKIP = %w[help].freeze
|
|
165
|
+
|
|
166
|
+
def help_text(path)
|
|
167
|
+
lines = File.readlines(path)
|
|
168
|
+
idx = lines.find_index { |l| l.match?(/def self\.help\b/) }
|
|
169
|
+
return '' unless idx
|
|
170
|
+
|
|
171
|
+
indent = lines[idx][/^[ \t]*/]
|
|
172
|
+
buf = []
|
|
173
|
+
((idx + 1)...lines.length).each do |i|
|
|
174
|
+
break if lines[i].match?(/^#{Regexp.escape(indent)}end\b/)
|
|
175
|
+
|
|
176
|
+
buf << lines[i]
|
|
177
|
+
end
|
|
178
|
+
buf.join
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def opts_keys(body)
|
|
182
|
+
body.to_s.scan(/opts\[\s*:([A-Za-z_]\w*)\s*\]/).flatten.uniq
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def thin_option_desc?(key, desc)
|
|
186
|
+
rest = desc.to_s.strip.sub(/\A(?:required|optional)\s*-\s*/i, '')
|
|
187
|
+
return true if rest.empty?
|
|
188
|
+
|
|
189
|
+
norm = rest.downcase.gsub(/[^a-z0-9]+/, ' ').strip
|
|
190
|
+
key_norm = key.to_s.downcase.tr('_', ' ')
|
|
191
|
+
return true if norm == key_norm
|
|
192
|
+
|
|
193
|
+
words = rest.split
|
|
194
|
+
words.length < 2 && rest.length < 18
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def purpose_above?(help, name)
|
|
198
|
+
lines = help.to_s.lines
|
|
199
|
+
idx = lines.find_index { |l| l.match?(/\#\{self\}(?:::[A-Z]\w*)*\.#{Regexp.escape(name)}(?!\w)/) }
|
|
200
|
+
return false unless idx
|
|
201
|
+
|
|
202
|
+
prev = nil
|
|
203
|
+
(idx - 1).downto(0) do |i|
|
|
204
|
+
next if lines[i].strip.empty?
|
|
205
|
+
|
|
206
|
+
prev = lines[i]
|
|
207
|
+
break
|
|
208
|
+
end
|
|
209
|
+
return false unless prev
|
|
210
|
+
|
|
211
|
+
text = prev.strip.sub(/\A#\s*/, '')
|
|
212
|
+
return false if text.match?(/\AUSAGE:/i)
|
|
213
|
+
return false if text.match?(/\.\w+\s*\(/) || text.match?(/\#\{self\}/)
|
|
214
|
+
return false if text.match?(/\A\w+\s*=\s*(?:PWN|Meshtastic)::/)
|
|
215
|
+
return false if text.downcase.gsub(/[^a-z0-9]+/, ' ').strip == name.downcase.tr('_', ' ')
|
|
216
|
+
|
|
217
|
+
words = text.split
|
|
218
|
+
words.length >= 3 || text.length >= 20
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def rel(path)
|
|
222
|
+
path.sub("#{ROOT}/", '')
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
describe 'Meshtastic module conventions' do
|
|
227
|
+
c = MeshtasticConventions
|
|
228
|
+
|
|
229
|
+
it '0) every Meshtastic module method is referenced via `self.<method_name>` (no bare module-level `def`, no module_function)' do
|
|
230
|
+
bare = c::ALL_BARE_DEFS
|
|
231
|
+
mfun = c::ALL_MODULE_FUNCTIONS
|
|
232
|
+
parts = []
|
|
233
|
+
unless bare.empty?
|
|
234
|
+
parts << 'bare module-level `def <name>` (use `public_class_method def self.<name>` / `private_class_method def self.<name>`):'
|
|
235
|
+
parts.concat(bare.map { |m| " #{c.rel(m[:file])}:#{m[:line]} def #{m[:name]}" })
|
|
236
|
+
end
|
|
237
|
+
unless mfun.empty?
|
|
238
|
+
parts << '`module_function` is forbidden (declare each method with def self.<name>):'
|
|
239
|
+
parts.concat(mfun.map { |m| " #{c.rel(m[:file])}:#{m[:line]} module_function" })
|
|
240
|
+
end
|
|
241
|
+
msg = parts.join("\n")
|
|
242
|
+
expect(bare + mfun).to be_empty, msg
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it '1) every `def self.<name>` is decorated with public_class_method or private_class_method' do
|
|
246
|
+
bare = c::ALL_METHODS.select { |m| m[:decorator].nil? }
|
|
247
|
+
msg = bare.map { |m| " #{c.rel(m[:file])}:#{m[:line]} def self.#{m[:name]}" }.join("\n")
|
|
248
|
+
expect(bare).to be_empty, "bare `def self.*` (add public_class_method / private_class_method):\n#{msg}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it '2) every argument-accepting module method takes exactly `(opts = {})`' do
|
|
252
|
+
bad = c::ALL_METHODS.reject do |m|
|
|
253
|
+
a = m[:arglist]
|
|
254
|
+
a.nil? || a.strip.empty? || a.strip == 'opts = {}'
|
|
255
|
+
end
|
|
256
|
+
msg = bad.map { |m| " #{c.rel(m[:file])}:#{m[:line]} def self.#{m[:name]}(#{m[:arglist]})" }.join("\n")
|
|
257
|
+
expect(bad).to be_empty, "non-conforming arglists (use `(opts = {})`):\n#{msg}"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it '3) methods declared `(opts = {})` actually consume opts in the body' do
|
|
261
|
+
unused = c::ALL_METHODS.select do |m|
|
|
262
|
+
m[:arglist]&.strip == 'opts = {}' &&
|
|
263
|
+
!m[:body_excerpt].match?(/\bopts\s*\[|\bopts\.(?:dig|fetch|key\?|keys|values|merge|each|delete|map|\[)/)
|
|
264
|
+
end
|
|
265
|
+
msg = unused.map { |m| " #{c.rel(m[:file])}:#{m[:line]} def self.#{m[:name]}(opts = {}) # opts never read" }.join("\n")
|
|
266
|
+
expect(unused).to be_empty, "declared `(opts = {})` but never read opts (unpack at top of method):\n#{msg}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it '4) every module file defines `def self.help`' do
|
|
270
|
+
missing = c::MODULE_FILES.reject do |f|
|
|
271
|
+
c::NAMESPACE_INDEX_FILES.include?(c.rel(f)) || File.read(f).match?(/def self\.help\b/)
|
|
272
|
+
end
|
|
273
|
+
msg = missing.map { |f| " #{c.rel(f)}" }.join("\n")
|
|
274
|
+
expect(missing).to be_empty, "modules missing `def self.help`:\n#{msg}"
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it '5) every module file defines `def self.authors`' do
|
|
278
|
+
missing = c::MODULE_FILES.reject do |f|
|
|
279
|
+
c::NAMESPACE_INDEX_FILES.include?(c.rel(f)) || File.read(f).match?(/def self\.authors\b/)
|
|
280
|
+
end
|
|
281
|
+
msg = missing.map { |f| " #{c.rel(f)}" }.join("\n")
|
|
282
|
+
expect(missing).to be_empty, "modules missing `def self.authors`:\n#{msg}"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it '6) every public class method is documented in help with required/optional options (Google.help shape)' do
|
|
286
|
+
misses = []
|
|
287
|
+
c::MODULE_FILES.each do |path|
|
|
288
|
+
next if c::NAMESPACE_INDEX_FILES.include?(c.rel(path))
|
|
289
|
+
|
|
290
|
+
help = c.help_text(path)
|
|
291
|
+
misses << "#{c.rel(path)} missing method authors in help" unless help.match?(/\.authors(?!\w)/)
|
|
292
|
+
pubs = c::ALL_METHODS.select do |m|
|
|
293
|
+
m[:file] == path &&
|
|
294
|
+
m[:decorator] == 'public_class_method' &&
|
|
295
|
+
!c::HELP_SKIP.include?(m[:name])
|
|
296
|
+
end
|
|
297
|
+
pubs.each do |meth|
|
|
298
|
+
name = meth[:name]
|
|
299
|
+
unless help.match?(/\.#{Regexp.escape(name)}(?!\w)/)
|
|
300
|
+
misses << "#{c.rel(path)} missing method #{name} in help"
|
|
301
|
+
next
|
|
302
|
+
end
|
|
303
|
+
misses << "#{c.rel(path)} #{name}: missing purpose line above the method in help" unless c.purpose_above?(help, name)
|
|
304
|
+
next unless meth[:arglist]&.strip == 'opts = {}'
|
|
305
|
+
|
|
306
|
+
c.opts_keys(meth[:method_body]).each do |key|
|
|
307
|
+
descs = help.scan(/#{Regexp.escape(key)}:\s*['"]((?:required|optional)\s*-\s*[^'"]+)/i).flatten
|
|
308
|
+
if descs.empty?
|
|
309
|
+
misses << "#{c.rel(path)} #{name} option #{key}: missing required/optional description"
|
|
310
|
+
next
|
|
311
|
+
end
|
|
312
|
+
next unless descs.any? { |d| c.thin_option_desc?(key, d) }
|
|
313
|
+
|
|
314
|
+
misses << "#{c.rel(path)} #{name} option #{key}: description too thin (say what to pass, not just the key name)"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
extra = misses.length > 80 ? "\n... #{misses.length} total" : ''
|
|
319
|
+
expect(misses).to be_empty, "#{misses.first(80).join("\n")}#{extra}"
|
|
320
|
+
end
|
|
321
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Meshtastic::Admin::Channel do
|
|
6
|
+
def fake_serial_obj
|
|
7
|
+
written = +''.b
|
|
8
|
+
serial_conn = Object.new
|
|
9
|
+
serial_conn.define_singleton_method(:write) do |b|
|
|
10
|
+
written << b
|
|
11
|
+
b.bytesize
|
|
12
|
+
end
|
|
13
|
+
serial_conn.define_singleton_method(:flush) { true }
|
|
14
|
+
serial_conn.define_singleton_method(:closed?) { false }
|
|
15
|
+
serial_conn.define_singleton_method(:close) { true }
|
|
16
|
+
{ serial_conn: serial_conn, written: written, my_node_num: 0xb0b }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def decode_admin(serial_obj)
|
|
20
|
+
frame = serial_obj[:written]
|
|
21
|
+
body = frame.byteslice(4, (frame.getbyte(2) << 8) + frame.getbyte(3))
|
|
22
|
+
Meshtastic::AdminMessage.decode(Meshtastic::ToRadio.decode(body).packet.decoded.payload)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'requests a channel by index via Admin' do
|
|
26
|
+
serial_obj = fake_serial_obj
|
|
27
|
+
described_class.get(serial_obj: serial_obj, index: 1)
|
|
28
|
+
expect(decode_admin(serial_obj).get_channel_request).to eq(1)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'sets a Channel protobuf including settings and role' do
|
|
32
|
+
serial_obj = fake_serial_obj
|
|
33
|
+
settings = described_class.build_settings(name: 'LongFast', uplink_enabled: true)
|
|
34
|
+
described_class.set(serial_obj: serial_obj, index: 0, role: :PRIMARY, settings: settings)
|
|
35
|
+
channel = decode_admin(serial_obj).set_channel
|
|
36
|
+
expect(channel.index).to eq(0)
|
|
37
|
+
expect(channel.role).to eq(:PRIMARY)
|
|
38
|
+
expect(channel.settings.name).to eq('LongFast')
|
|
39
|
+
expect(channel.settings.uplink_enabled).to be true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'prints usage without raising' do
|
|
43
|
+
expect { described_class.help }.to output(/USAGE/).to_stdout
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Meshtastic::Admin::Config do
|
|
6
|
+
def fake_serial_obj
|
|
7
|
+
written = +''.b
|
|
8
|
+
serial_conn = Object.new
|
|
9
|
+
serial_conn.define_singleton_method(:write) do |b|
|
|
10
|
+
written << b
|
|
11
|
+
b.bytesize
|
|
12
|
+
end
|
|
13
|
+
serial_conn.define_singleton_method(:flush) { true }
|
|
14
|
+
serial_conn.define_singleton_method(:closed?) { false }
|
|
15
|
+
serial_conn.define_singleton_method(:close) { true }
|
|
16
|
+
{ serial_conn: serial_conn, written: written, my_node_num: 0xb0b }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def decode_admin(serial_obj)
|
|
20
|
+
frame = serial_obj[:written]
|
|
21
|
+
body = frame.byteslice(4, (frame.getbyte(2) << 8) + frame.getbyte(3))
|
|
22
|
+
Meshtastic::AdminMessage.decode(Meshtastic::ToRadio.decode(body).packet.decoded.payload)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'requests LoRa config via Admin' do
|
|
26
|
+
serial_obj = fake_serial_obj
|
|
27
|
+
described_class.get(serial_obj: serial_obj, config_type: :LORA_CONFIG)
|
|
28
|
+
expect(decode_admin(serial_obj).get_config_request).to eq(:LORA_CONFIG)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'sets a Config protobuf' do
|
|
32
|
+
serial_obj = fake_serial_obj
|
|
33
|
+
config = Meshtastic::Config.new
|
|
34
|
+
config.device = Meshtastic::Config::DeviceConfig.new(role: :CLIENT)
|
|
35
|
+
described_class.set(serial_obj: serial_obj, config: config)
|
|
36
|
+
expect(decode_admin(serial_obj).set_config.device.role).to eq(:CLIENT)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'requests each ConfigType via named getters' do
|
|
40
|
+
serial_obj = fake_serial_obj
|
|
41
|
+
described_class.get_lora(serial_obj: serial_obj)
|
|
42
|
+
expect(decode_admin(serial_obj).get_config_request).to eq(:LORA_CONFIG)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'prints usage without raising' do
|
|
46
|
+
expect { described_class.help }.to output(/USAGE/).to_stdout
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require 'digest'
|
|
6
|
+
|
|
7
|
+
describe Meshtastic::Admin::Firmware do
|
|
8
|
+
def fake_serial_obj
|
|
9
|
+
written = +''.b
|
|
10
|
+
serial_conn = Object.new
|
|
11
|
+
serial_conn.define_singleton_method(:write) do |b|
|
|
12
|
+
written << b
|
|
13
|
+
b.bytesize
|
|
14
|
+
end
|
|
15
|
+
serial_conn.define_singleton_method(:flush) { true }
|
|
16
|
+
serial_conn.define_singleton_method(:closed?) { false }
|
|
17
|
+
serial_conn.define_singleton_method(:close) { true }
|
|
18
|
+
{ serial_conn: serial_conn, written: written, my_node_num: 0xb0b }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def decode_frames(serial_obj)
|
|
22
|
+
buf = serial_obj[:written].dup
|
|
23
|
+
frames = []
|
|
24
|
+
until buf.bytesize < 4
|
|
25
|
+
break unless buf.getbyte(0) == Meshtastic::START1 && buf.getbyte(1) == Meshtastic::START2
|
|
26
|
+
|
|
27
|
+
len = (buf.getbyte(2) << 8) + buf.getbyte(3)
|
|
28
|
+
break if buf.bytesize < 4 + len
|
|
29
|
+
|
|
30
|
+
frames << Meshtastic::ToRadio.decode(buf.byteslice(4, len))
|
|
31
|
+
buf = buf.byteslice((4 + len)..)
|
|
32
|
+
end
|
|
33
|
+
frames
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def firmware_file(bytes = "hello firmware\n")
|
|
37
|
+
file = Tempfile.new(['fw', '.bin'])
|
|
38
|
+
file.binmode
|
|
39
|
+
file.write(bytes)
|
|
40
|
+
file.flush
|
|
41
|
+
file
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'sends ota_request with SHA-256 and BLE mode' do
|
|
45
|
+
serial_obj = fake_serial_obj
|
|
46
|
+
file = firmware_file('abc')
|
|
47
|
+
described_class.request_ota(serial_obj: serial_obj, firmware: file.path, mode: :OTA_BLE)
|
|
48
|
+
packet = decode_frames(serial_obj).last.packet
|
|
49
|
+
admin = Meshtastic::AdminMessage.decode(packet.decoded.payload)
|
|
50
|
+
expect(packet.decoded.portnum).to eq(:ADMIN_APP)
|
|
51
|
+
expect(admin.ota_request.reboot_ota_mode).to eq(:OTA_BLE)
|
|
52
|
+
expect(admin.ota_request.ota_hash.bytesize).to eq(32)
|
|
53
|
+
expect(admin.ota_request.ota_hash).to eq(Digest::SHA256.digest('abc'))
|
|
54
|
+
ensure
|
|
55
|
+
file.close!
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'sends enter_dfu_mode_request' do
|
|
59
|
+
serial_obj = fake_serial_obj
|
|
60
|
+
described_class.enter_dfu(serial_obj: serial_obj)
|
|
61
|
+
admin = Meshtastic::AdminMessage.decode(decode_frames(serial_obj).last.packet.decoded.payload)
|
|
62
|
+
expect(admin.enter_dfu_mode_request).to be true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'builds XModem SOH blocks with CRC16 and an EOT' do
|
|
66
|
+
blocks = described_class.xmodem_blocks(bytes: 'A' * 130)
|
|
67
|
+
expect(blocks.first.control).to eq(:SOH)
|
|
68
|
+
expect(blocks.first.seq).to eq(1)
|
|
69
|
+
expect(blocks.first.buffer.bytesize).to eq(128)
|
|
70
|
+
expect(blocks[1].seq).to eq(2)
|
|
71
|
+
expect(blocks.last.control).to eq(:EOT)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'streams XModem ToRadio frames after ota_request on serial' do
|
|
75
|
+
serial_obj = fake_serial_obj
|
|
76
|
+
file = firmware_file('A' * 10)
|
|
77
|
+
described_class.install(serial_obj: serial_obj, firmware: file.path, mode: :OTA_BLE)
|
|
78
|
+
frames = decode_frames(serial_obj)
|
|
79
|
+
admin = Meshtastic::AdminMessage.decode(frames.first.packet.decoded.payload)
|
|
80
|
+
expect(admin.ota_request.ota_hash.bytesize).to eq(32)
|
|
81
|
+
xmodem = frames[1..]
|
|
82
|
+
expect(xmodem.first.xmodemPacket.control).to eq(:SOH)
|
|
83
|
+
expect(xmodem.last.xmodemPacket.control).to eq(:EOT)
|
|
84
|
+
ensure
|
|
85
|
+
file.close!
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'publishes ota_request over MQTT without XModem PhoneAPI frames' do
|
|
89
|
+
published = []
|
|
90
|
+
mqtt_obj = Object.new
|
|
91
|
+
mqtt_obj.define_singleton_method(:client_id) { '00000b0b' }
|
|
92
|
+
mqtt_obj.define_singleton_method(:publish) do |topic, payload|
|
|
93
|
+
published << { topic: topic, payload: payload }
|
|
94
|
+
payload.bytesize
|
|
95
|
+
end
|
|
96
|
+
file = firmware_file('xyz')
|
|
97
|
+
described_class.install(mqtt_obj: mqtt_obj, firmware: file.path, mode: :OTA_WIFI, to: '!aabbccdd')
|
|
98
|
+
expect(published.size).to eq(1)
|
|
99
|
+
envelope = Meshtastic::ServiceEnvelope.decode(published.first[:payload])
|
|
100
|
+
packet = envelope.packet
|
|
101
|
+
nonce = [packet.id].pack('V').ljust(8, "\x00") + [packet.from].pack('V').ljust(8, "\x00")
|
|
102
|
+
psk = Base64.strict_decode64('1PG7OiApB1nwvP+rz05pAQ==')
|
|
103
|
+
cipher = OpenSSL::Cipher.new('AES-128-CTR')
|
|
104
|
+
cipher.decrypt
|
|
105
|
+
cipher.key = psk
|
|
106
|
+
cipher.iv = nonce
|
|
107
|
+
data = Meshtastic::Data.decode(cipher.update(packet.encrypted) + cipher.final)
|
|
108
|
+
expect(data.portnum).to eq(:ADMIN_APP)
|
|
109
|
+
admin = Meshtastic::AdminMessage.decode(data.payload)
|
|
110
|
+
expect(admin.ota_request.reboot_ota_mode).to eq(:OTA_WIFI)
|
|
111
|
+
ensure
|
|
112
|
+
file.close!
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'prints usage without raising' do
|
|
116
|
+
expect { described_class.help }.to output(/firmware/).to_stdout
|
|
117
|
+
end
|
|
118
|
+
end
|