meshtastic 0.0.179 → 0.0.181
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +2 -2
- data/documentation/README.md +3 -2
- data/documentation/admin-channel.md +64 -0
- data/documentation/admin-config.md +59 -0
- data/documentation/admin-firmware-nordic.md +86 -0
- data/documentation/admin-firmware-serial.md +180 -0
- data/documentation/admin-firmware.md +126 -0
- data/documentation/admin.md +88 -35
- data/documentation/atak.md +93 -10
- data/documentation/channel.md +1 -28
- data/documentation/config.md +1 -27
- data/documentation/mesh-interface.md +7 -0
- data/documentation/meshtastic.md +2 -2
- data/lib/meshtastic/admin/channel.rb +164 -0
- data/lib/meshtastic/admin/config.rb +213 -0
- data/lib/meshtastic/admin/firmware/ble.rb +207 -0
- data/lib/meshtastic/admin/firmware/nordic_dfu.rb +218 -0
- data/lib/meshtastic/admin/firmware/serial_bootloader.rb +233 -0
- data/lib/meshtastic/admin/firmware.rb +288 -0
- data/lib/meshtastic/admin.rb +688 -34
- data/lib/meshtastic/apponly.rb +19 -7
- data/lib/meshtastic/atak.rb +295 -17
- data/lib/meshtastic/bluetooth/bluez.rb +20 -1
- data/lib/meshtastic/bluetooth.rb +70 -59
- data/lib/meshtastic/cannedmessages.rb +13 -6
- data/lib/meshtastic/channel.rb +3 -25
- data/lib/meshtastic/clientonly.rb +19 -7
- data/lib/meshtastic/config.rb +3 -24
- data/lib/meshtastic/config_pb.rb +2 -1
- data/lib/meshtastic/connection_status.rb +12 -5
- data/lib/meshtastic/deviceonly.rb +19 -7
- data/lib/meshtastic/localonly.rb +19 -7
- data/lib/meshtastic/mesh_interface.rb +19 -0
- data/lib/meshtastic/module_config.rb +13 -7
- data/lib/meshtastic/mqtt.rb +72 -35
- data/lib/meshtastic/paxcount.rb +17 -6
- data/lib/meshtastic/portnums.rb +13 -6
- data/lib/meshtastic/position.rb +16 -6
- data/lib/meshtastic/remote_hardware.rb +27 -11
- data/lib/meshtastic/rtttl.rb +17 -8
- data/lib/meshtastic/serial.rb +81 -70
- data/lib/meshtastic/storeforward.rb +13 -6
- data/lib/meshtastic/storeforward_pb.rb +1 -1
- data/lib/meshtastic/stream_interface.rb +11 -0
- data/lib/meshtastic/tcp.rb +73 -22
- data/lib/meshtastic/telemetry.rb +15 -6
- data/lib/meshtastic/traceroute.rb +15 -6
- data/lib/meshtastic/util.rb +3 -1
- data/lib/meshtastic/version.rb +1 -1
- data/lib/meshtastic/xmodem.rb +12 -5
- data/lib/meshtastic.rb +22 -3
- data/spec/conventions_spec.rb +321 -0
- data/spec/lib/meshtastic/admin/channel_spec.rb +202 -0
- data/spec/lib/meshtastic/admin/config_spec.rb +114 -0
- data/spec/lib/meshtastic/admin/firmware/ble_spec.rb +170 -0
- data/spec/lib/meshtastic/admin/firmware/nordic_dfu_spec.rb +248 -0
- data/spec/lib/meshtastic/admin/firmware/serial_bootloader_spec.rb +263 -0
- data/spec/lib/meshtastic/admin/firmware_spec.rb +315 -0
- data/spec/lib/meshtastic/admin_spec.rb +412 -1
- data/spec/lib/meshtastic/apponly_spec.rb +1 -1
- data/spec/lib/meshtastic/atak_spec.rb +129 -4
- data/spec/lib/meshtastic/bluetooth_spec.rb +1 -1
- data/spec/lib/meshtastic/channel_spec.rb +2 -20
- data/spec/lib/meshtastic/config_spec.rb +2 -20
- data/spec/lib/meshtastic/connection_status_spec.rb +1 -1
- data/spec/lib/meshtastic/deviceonly_spec.rb +1 -1
- data/spec/lib/meshtastic/localonly_spec.rb +1 -1
- data/spec/lib/meshtastic/mesh_interface_spec.rb +31 -0
- data/spec/lib/meshtastic/portnums_spec.rb +2 -2
- metadata +24 -6
data/lib/meshtastic/telemetry.rb
CHANGED
|
@@ -4,13 +4,13 @@ require 'meshtastic/telemetry_pb'
|
|
|
4
4
|
|
|
5
5
|
module Meshtastic
|
|
6
6
|
class Telemetry
|
|
7
|
-
def self.build(opts = {})
|
|
7
|
+
public_class_method def self.build(opts = {})
|
|
8
8
|
telemetry = new
|
|
9
9
|
telemetry.time = opts[:time].to_i if opts[:time]
|
|
10
10
|
telemetry
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def self.request(opts = {})
|
|
13
|
+
public_class_method def self.request(opts = {})
|
|
14
14
|
data = Meshtastic::Data.new(
|
|
15
15
|
portnum: :TELEMETRY_APP,
|
|
16
16
|
payload: build(opts).to_proto,
|
|
@@ -19,14 +19,23 @@ module Meshtastic
|
|
|
19
19
|
Meshtastic.deliver_data(opts.merge(data: data, port_num: Meshtastic::PortNum::TELEMETRY_APP, want_response: true))
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def self.authors
|
|
22
|
+
public_class_method def self.authors
|
|
23
23
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def self.help
|
|
27
|
-
puts "USAGE:
|
|
28
|
-
#
|
|
26
|
+
public_class_method def self.help
|
|
27
|
+
puts " USAGE:
|
|
28
|
+
# Run the build class method for this module.
|
|
29
|
+
#{self}.build(
|
|
30
|
+
time: 'optional - value for time passed into build'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Run the request class method for this module.
|
|
34
|
+
#{self}.request
|
|
35
|
+
|
|
36
|
+
# Run the authors class method for this module.
|
|
29
37
|
#{self}.authors
|
|
38
|
+
|
|
30
39
|
"
|
|
31
40
|
end
|
|
32
41
|
end
|
|
@@ -4,13 +4,13 @@ require 'meshtastic/mesh_pb'
|
|
|
4
4
|
|
|
5
5
|
module Meshtastic
|
|
6
6
|
module Traceroute
|
|
7
|
-
def self.encode(opts = {})
|
|
7
|
+
public_class_method def self.encode(opts = {})
|
|
8
8
|
discovery = Meshtastic::RouteDiscovery.new
|
|
9
9
|
Array(opts[:route]).each { |hop| discovery.route << hop }
|
|
10
10
|
discovery
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def self.send(opts = {})
|
|
13
|
+
public_class_method def self.send(opts = {})
|
|
14
14
|
data = Meshtastic::Data.new(
|
|
15
15
|
portnum: :TRACEROUTE_APP,
|
|
16
16
|
payload: encode(opts).to_proto,
|
|
@@ -19,14 +19,23 @@ module Meshtastic
|
|
|
19
19
|
Meshtastic.deliver_data(opts.merge(data: data, port_num: Meshtastic::PortNum::TRACEROUTE_APP, want_response: true))
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def self.authors
|
|
22
|
+
public_class_method def self.authors
|
|
23
23
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def self.help
|
|
27
|
-
puts "USAGE:
|
|
28
|
-
#
|
|
26
|
+
public_class_method def self.help
|
|
27
|
+
puts " USAGE:
|
|
28
|
+
# Run the encode class method for this module.
|
|
29
|
+
#{self}.encode(
|
|
30
|
+
route: 'optional - value for route passed into encode'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Run the send class method for this module.
|
|
34
|
+
#{self}.send
|
|
35
|
+
|
|
36
|
+
# Run the authors class method for this module.
|
|
29
37
|
#{self}.authors
|
|
38
|
+
|
|
30
39
|
"
|
|
31
40
|
end
|
|
32
41
|
end
|
data/lib/meshtastic/util.rb
CHANGED
data/lib/meshtastic/version.rb
CHANGED
data/lib/meshtastic/xmodem.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'meshtastic/xmodem_pb'
|
|
|
4
4
|
|
|
5
5
|
module Meshtastic
|
|
6
6
|
module Xmodem
|
|
7
|
-
def self.encode(opts = {})
|
|
7
|
+
public_class_method def self.encode(opts = {})
|
|
8
8
|
packet = Meshtastic::XModem.new
|
|
9
9
|
packet.control = opts.fetch(:control, :SOH)
|
|
10
10
|
packet.seq = opts[:seq].to_i if opts[:seq]
|
|
@@ -12,14 +12,21 @@ module Meshtastic
|
|
|
12
12
|
packet
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def self.authors
|
|
15
|
+
public_class_method def self.authors
|
|
16
16
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n "
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def self.help
|
|
20
|
-
puts "USAGE:
|
|
21
|
-
#
|
|
19
|
+
public_class_method def self.help
|
|
20
|
+
puts " USAGE:
|
|
21
|
+
# Run the encode class method for this module.
|
|
22
|
+
#{self}.encode(
|
|
23
|
+
seq: 'optional - value for seq passed into encode',
|
|
24
|
+
buffer: 'optional - value for buffer passed into encode'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Run the authors class method for this module.
|
|
22
28
|
#{self}.authors
|
|
29
|
+
|
|
23
30
|
"
|
|
24
31
|
end
|
|
25
32
|
end
|
data/lib/meshtastic.rb
CHANGED
|
@@ -67,11 +67,28 @@ 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
|
+
"
|
|
72
89
|
end
|
|
73
90
|
|
|
74
|
-
def self.deliver_data(opts = {})
|
|
91
|
+
public_class_method def self.deliver_data(opts = {})
|
|
75
92
|
raise ArgumentError, 'data is required' unless opts[:data].is_a?(Meshtastic::Data)
|
|
76
93
|
|
|
77
94
|
if opts[:serial_obj]
|
|
@@ -80,8 +97,10 @@ module Meshtastic
|
|
|
80
97
|
Bluetooth.send_data(opts)
|
|
81
98
|
elsif opts[:tcp_obj]
|
|
82
99
|
TCP.send_data(opts)
|
|
100
|
+
elsif opts[:mqtt_obj]
|
|
101
|
+
MQTT.send_data(opts)
|
|
83
102
|
else
|
|
84
|
-
raise ArgumentError, 'serial_obj, bluetooth_obj, or
|
|
103
|
+
raise ArgumentError, 'serial_obj, bluetooth_obj, tcp_obj, or mqtt_obj is required'
|
|
85
104
|
end
|
|
86
105
|
end
|
|
87
106
|
end
|
|
@@ -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
|