Update navigation.c
[inav.git] / src / utils / update_cli_docs.py
blob6d68a249bf5c42afa38fd870b680684b0266002a
1 #!/usr/bin/env python3
3 import optparse
4 import os
5 import re
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 = [
13 'baro_hardware',
14 'dterm_lpf_type',
15 'dterm_lpf2_type',
16 'failsafe_procedure',
17 'flaperon_throw_offset',
18 'heading_hold_rate_limit',
19 'mag_hardware',
20 'pitot_hardware',
21 'rx_min_usec',
22 'serialrx_provider',
25 MIN_MAX_REPLACEMENTS = {
26 'INT16_MIN': -32768,
27 'INT16_MAX': 32767,
28 'INT32_MIN': -2147483648,
29 'INT32_MAX': 2147483647,
30 'UINT8_MAX': 255,
31 'UINT16_MAX': 65535,
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"""
43 params = {}
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
54 if key in member:
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"
66 member["min"] = "OFF"
67 member["max"] = "ON"
68 # Replace zero placeholder with actual zero
69 elif member[key] == ":zero":
70 member[key] = 0
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
90 output_lines = []
91 for param in sorted(params.items()):
92 output_lines.extend([
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",
97 "---\n\n"
100 # Return the assembled doc body
101 return output_lines
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):
111 for f in files:
112 with open(f, 'r') as _f:
113 for _, line in enumerate(_f.readlines()):
114 matches = regex.search(line)
115 if matches:
116 yield matches
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)
129 defaults = []
130 for matches in regex_search(regex, files_to_search_in):
131 defaults.append(matches.group(1))
132 return defaults
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):
138 retval = True
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:
145 continue
147 default_from_code = find_default(member['name'], headers)
148 if len(default_from_code) == 0: # No default value found (no direct code mapping)
149 continue
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")
153 continue
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]:
170 continue
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}'")
173 retval = False
174 return retval
176 if __name__ == "__main__":
177 global options, args
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()
185 if options.defaults:
186 defaults_match = check_defaults(settings_yaml)
187 quit(0 if defaults_match else 1)
189 output_lines = generate_md_from_yaml(settings_yaml)
190 output_lines = [
191 "# CLI Variable Reference\n\n",
192 "> Note: this document is autogenerated. Do not edit it manually.\n\n"
193 ] + output_lines
194 write_settings_md(output_lines)