6 import yaml
# pyyaml / python-yaml
8 SETTINGS_MD_PATH
= "docs/Settings.md"
9 SETTINGS_YAML_PATH
= "src/main/fc/settings.yaml"
10 CODE_DEFAULTS_PATH
= "src/main"
12 DEFAULTS_BLACKLIST
= [
17 'flaperon_throw_offset',
18 'heading_hold_rate_limit',
25 MIN_MAX_REPLACEMENTS
= {
28 'INT32_MIN': -2147483648,
29 'INT32_MAX': 2147483647,
32 'UINT32_MAX': 4294967295,
35 def parse_settings_yaml():
36 """Parse the YAML settings specs"""
38 with
open(SETTINGS_YAML_PATH
, "r") as settings_yaml
:
39 return yaml
.load(settings_yaml
, Loader
=yaml
.Loader
)
41 def generate_md_from_yaml(settings_yaml
):
42 """Generate a sorted markdown table with description & default value for each setting"""
45 # Extract description, default/min/max values of each setting from the YAML specs (if present)
46 for group
in settings_yaml
['groups']:
47 for member
in group
['members']:
48 if not any(key
in member
for key
in ["description", "default_value", "min", "max"]) and not options
.quiet
:
49 print("Setting \"{}\" has an incomplete specification".format(member
['name']))
51 # Handle default/min/max fields for each setting
52 for key
in ["default_value", "min", "max"]:
53 # Basing on the check above, not all fields may be present
55 ### Fetch actual values from the `constants` block if present
56 if ('constants' in settings_yaml
) and (member
[key
] in settings_yaml
['constants']):
57 member
[key
] = settings_yaml
['constants'][member
[key
]]
58 ### Fetch actual values from hardcoded min/max replacements
59 elif member
[key
] in MIN_MAX_REPLACEMENTS
:
60 member
[key
] = MIN_MAX_REPLACEMENTS
[member
[key
]]
62 ### Handle edge cases of YAML autogeneration and prettify some values
63 # Replace booleans with "ON"/"OFF"
64 if type(member
[key
]) == bool:
65 member
[key
] = "ON" if member
[key
] else "OFF"
68 # Replace zero placeholder with actual zero
69 elif member
[key
] == ":zero":
71 # Replace target-default placeholder with extended definition
72 elif member
[key
] == ":target":
73 member
[key
] = "_target default_"
74 # Replace empty strings with more evident marker
75 elif member
[key
] == "":
76 member
[key
] = "_empty_"
77 # Reformat direct code references
78 elif str(member
[key
])[0] == ":":
79 member
[key
] = f
'`{member[key][1:]}`'
82 params
[member
['name']] = {
83 "description": member
["description"] if "description" in member
else "",
84 "default": member
["default_value"] if "default_value" in member
else "",
85 "min": member
["min"] if "min" in member
else "",
86 "max": member
["max"] if "max" in member
else ""
89 # Sort the settings by name and build the doc
91 for param
in sorted(params
.items()):
93 f
"### {param[0]}\n\n",
94 f
"{param[1]['description'] if param[1]['description'] else '_// TODO_'}\n\n",
95 "| Default | Min | Max |\n| --- | --- | --- |\n",
96 f
"| {param[1]['default']} | {param[1]['min']} | {param[1]['max']} |\n\n",
100 # Return the assembled doc body
103 def write_settings_md(lines
):
104 """Write the contents of the CLI settings docs"""
106 with
open(SETTINGS_MD_PATH
, "w") as settings_md
:
107 settings_md
.writelines(lines
)
109 # Return all matches of a compiled regex in a list of files
110 def regex_search(regex
, files
):
112 with
open(f
, 'r') as _f
:
113 for _
, line
in enumerate(_f
.readlines()):
114 matches
= regex
.search(line
)
118 # Return plausible default values for a given setting found by scraping the relative source files
119 def find_default(setting_name
, headers
):
120 regex
= re
.compile(rf
'^\s*\.{setting_name}\s=\s([A-Za-z0-9_\-]+)(?:,)?(?:\s+//.+$)?')
121 files_to_search_in
= []
122 for header
in headers
:
123 header
= f
'{CODE_DEFAULTS_PATH}/{header}'
124 files_to_search_in
.append(header
)
125 if header
.endswith('.h'):
126 header_c
= re
.sub(r
'\.h$', '.c', header
)
127 if os
.path
.isfile(header_c
):
128 files_to_search_in
.append(header_c
)
130 for matches
in regex_search(regex
, files_to_search_in
):
131 defaults
.append(matches
.group(1))
134 # Try to map default values in the YAML spec back to the actual C code and check for mismatches (defaults updated in the code but not in the YAML)
135 # Done by scraping the source files, prone to false negatives. Settings in `DEFAULTS_BLACKLIST` are ignored for this
136 # reason (values that refer to other source files are too complex to parse this way).
137 def check_defaults(settings_yaml
):
139 for group
in settings_yaml
['groups']:
140 if 'headers' in group
:
141 headers
= group
['headers']
142 for member
in group
['members']:
143 # Ignore blacklisted settings
144 if member
['name'] in DEFAULTS_BLACKLIST
:
147 default_from_code
= find_default(member
['name'], headers
)
148 if len(default_from_code
) == 0: # No default value found (no direct code mapping)
150 elif len(default_from_code
) > 1: # Duplicate default values found (regexes are a quick but poor solution)
151 if not options
.quiet
:
152 print(f
"Duplicate default values found for {member['name']}: {default_from_code}, consider adding to blacklist")
155 # Extract the only matched value, guarded by the previous checks
156 default_from_code
= default_from_code
[0]
157 # Map C values to their equivalents used in the YAML spec
158 code_values_map
= { 'true': 'ON', 'false': 'OFF' }
159 if default_from_code
in code_values_map
:
160 default_from_code
= code_values_map
[default_from_code
]
162 default_from_yaml
= member
["default_value"] if "default_value" in member
else ""
163 # Remove eventual Markdown formatting
164 default_from_yaml
= default_from_yaml
.replace('`', '').replace('*', '').replace('__', '')
165 # Allow specific C-YAML matches that coudln't be replaced in the previous steps
166 extra_allowed_matches
= { '1': 'ON', '0': 'OFF' }
168 if default_from_yaml
not in default_from_code
: # Equal or substring
169 if default_from_code
in extra_allowed_matches
and default_from_yaml
in extra_allowed_matches
[default_from_code
]:
171 if not options
.quiet
:
172 print(f
"{member['name']} has mismatched default values. Code reports '{default_from_code}' and YAML reports '{default_from_yaml}'")
176 if __name__
== "__main__":
178 parser
= optparse
.OptionParser()
179 parser
.add_option('-q', '--quiet', action
="store_true", default
=False, help="do not write anything to stdout")
180 parser
.add_option('-d', '--defaults', action
="store_true", default
=False, help="check for mismatched default values")
181 options
, args
= parser
.parse_args()
183 settings_yaml
= parse_settings_yaml()
186 defaults_match
= check_defaults(settings_yaml
)
187 quit(0 if defaults_match
else 1)
189 output_lines
= generate_md_from_yaml(settings_yaml
)
191 "# CLI Variable Reference\n\n",
192 "> Note: this document is autogenerated. Do not edit it manually.\n\n"
194 write_settings_md(output_lines
)