Sync inav to Gitea
Make sure docs are updated / settings_md (push) Canceled after 0s
Build firmware / test (push) Canceled after 0s
Build firmware / build-SITL-Windows (push) Canceled after 0s
Build firmware / build-SITL-Mac (push) Canceled after 0s
Build firmware / build-SITL-Linux (push) Canceled after 0s
Build firmware / build-SITL-Linux-arm64 (push) Canceled after 0s
Build firmware / upload-artifacts (push) Canceled after 0s
Build firmware / build-single-target (push) Canceled after 0s
Build firmware / build (9) (push) Canceled after 0s
Build firmware / build (8) (push) Canceled after 0s
Build firmware / build (7) (push) Canceled after 0s
Build firmware / build (6) (push) Canceled after 0s
Build firmware / build (5) (push) Canceled after 0s
Build firmware / build (4) (push) Canceled after 0s
Build firmware / build (3) (push) Canceled after 0s
Build firmware / build (2) (push) Canceled after 0s
Build firmware / build (14) (push) Canceled after 0s
Build firmware / build (13) (push) Canceled after 0s
Build firmware / build (12) (push) Canceled after 0s
Build firmware / build (11) (push) Canceled after 0s
Build firmware / build (10) (push) Canceled after 0s
Build firmware / build (1) (push) Canceled after 0s
Build firmware / build (0) (push) Canceled after 0s
Build firmware / detect (push) Canceled after 0s
Build pre-release / build (push) Canceled after 0s
Build pre-release / Release (push) Canceled after 0s

This commit is contained in:
2026-08-03 16:37:40 +08:00
commit dac37fd077
14095 changed files with 5603119 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
# INAV MSP Messages reference
**This page is auto-generated from the [master INAV MSP definitions file](https://github.com/iNavFlight/inav/blob/master/docs/development/msp/msp_messages.json)**
For details on the structure of MSP, see [The wiki page](https://github.com/iNavFlight/inav/wiki/MSP-V2)
For list of enums, see [Enum documentation page](https://github.com/iNavFlight/inav/wiki/Enums-reference)
**JSON file rev: <file_rev>**
**Warning: Verification needed, exercise caution until completely verified for accuracy and cleared, especially for integer signs. Source-based generation/validation is forthcoming. Refer to source for absolute certainty**
**If you find an error, it must be corrected in the JSON spec, not this markdown.**
**Guide:**
* **MSP Versions:**
* **MSPv1:** The original protocol. Uses command IDs from 0 to 254.
* **MSPv2:** An extended version. Uses command IDs from 0x1000 onwards.
* **Request Payload:** The request payload sent to the destination (usually the flight controller). May be empty or hold data for setting or requesting data from the FC.
* **Reply Payload:** The reply sent from the FC to the sender. May be empty or hold data.
* **Notes:** Pay attention to message notes and description.
<format>
---
+132
View File
@@ -0,0 +1,132 @@
# Format:
## JSON format example:
```
"MSP_API_VERSION": {
"code": 1,
"mspv": 1,
"request": null,
"reply": {
"payload": [
{
"name": "mspProtocolVersion",
"ctype": "uint8_t",
"units": "",
"desc": "MSP Protocol version (`MSP_PROTOCOL_VERSION`, typically 0)."
},
{
"name": "apiVersionMajor",
"ctype": "uint8_t",
"units": "",
"desc": "INAV API Major version (`API_VERSION_MAJOR`)."
},
{
"name": "apiVersionMinor",
"ctype": "uint8_t",
"units": "",
"desc": "INAV API Minor version (`API_VERSION_MINOR`)."
}
],
},
"notes": "Used by configurators to check compatibility.",
"description": "Provides the MSP protocol version and the INAV API version."
},
```
## Message fields:
**name**: MSP message name\
**code**: Integer message code\
**description**: String with description of message\
**request**: null or dict of data sent\
**reply**: null or dict of data received\
**variable_len**: Optional boolean, if true, message does not have a predefined fixed length and needs appropriate handling\
**variants**: Optional special case, message has different cases of reply/request. Key/description is not a strict expression or code; just a readable condition\
**not_implemented**: Optional special case, message is not implemented (never or deprecated)\
**replaced_by**: Optional array of MSP message names that replace this command. Present when a command is deprecated and scheduled for removal. Empty array if no replacement is needed\
**notes**: String with details of message
## Data dict fields:
**payload**: Array of payload fields\
**repeating**: Optional Special Case, integer or string of how many times the *entire* payload is repeated
## Payload fields:
### Fields:
**name**: field name from code\
**ctype**: Base C type of the value. Arrays list their element type here as well\
**desc**: Optional string with description and details of field\
**units**: Optional defined units\
**enum**: Optional string of enum struct if value is an enum
**array**: Optional boolean to denote field is array of more values\
**array_size**: If array, integer count of elements. Use `0` when the length is indeterminate/variable\
**array_size_define**: Optional string naming the source `#define` that provides the size (informational only)\
**repeating**: Optional Special case, contains array of more payload fields that are added Times * Key\
**payload**: If repeating, contains more payload fields\
**polymorph**: Optional boolean special case, field does not have a defined C type and could be anything
**Simple value**
```
{
"name": "mspProtocolVersion",
"ctype": "uint8_t",
"units": "",
"desc": "MSP Protocol version (`MSP_PROTOCOL_VERSION`, typically 0)."
},
```
**Fixed length array**
```
{
"name": "fcVariantIdentifier",
"ctype": "char",
"desc": "4-character identifier string (e.g., \"INAV\"). Defined by `flightControllerIdentifier",
"array": true,
"array_size": 4,
"units": ""
}
```
**Array sized via define**
```
{
"name": "buildDate",
"ctype": "char",
"desc": "Build date string (e.g., \"Dec 31 2023\").",
"array": true,
"array_size": 11,
"array_size_define": "BUILD_DATE_LENGTH",
"units": ""
}
```
**Undefined length array**
```
{
"name": "firmwareChunk",
"ctype": "uint8_t",
"desc": "Chunk of firmware data",
"array": true,
"array_size": 0,
}
```
**As of yet unknown length array**
```
{
"name": "elementText",
"ctype": "char",
"desc": "Static text bytes, not NUL-terminated and not yet sized.",
"array": true,
"array_size": 0
}
```
**Nested array with struct**
```
{
"repeating": "maxVehicles",
"payload": [
{
"name": "adsbVehicle",
"ctype": "adsbVehicle_t",
"desc": "Array of `adsbVehicle_t` Repeated `maxVehicles` times",
"repeating": "maxVehicles",
"array": true,
"array_size": 0,
"units": ""
}
]
}
```
+27
View File
@@ -0,0 +1,27 @@
INAV_MAIN_PATH="../../../src/main"
echo "###########"
echo get_all_inav_enums_h.py
python get_all_inav_enums_h.py --inav-root "$INAV_MAIN_PATH"
echo "###########"
echo "msp_messages.json checksum"
actual="$(md5sum msp_messages.json | awk '{print $1}')"
expected="$(awk '{print $1}' msp_messages.checksum)"
echo "Hash:" $actual
if [[ "$actual" != "$expected" ]]; then
n="$(cat rev)"
printf '%d' "$((n + 1))" > rev
echo "File changed, incrementing revision"
echo $actual > msp_messages.checksum
fi
echo "###########"
echo gen_msp_md.py
python gen_msp_md.py
echo "###########"
echo gen_enum_md.py
python gen_enum_md.py
rm all_enums.h
read -n 1 -s -r -p "Press any key to continue"
+297
View File
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
enumdoc.py — Generate Markdown documentation from C enums (no expression eval).
Rules:
- One Value column only.
* If explicit assignment is a plain int literal (dec/hex/bin/oct) -> show that number.
* If explicit assignment is anything else -> show the raw expression text.
* If no assignment -> auto-increment.
- If auto-increment occurs inside an active preprocessor condition, wrap the number
in parentheses to indicate conditional numbering: e.g., 3, 4, #ifdef, (5), (6).
- Tracks nested #if/#ifdef/#ifndef/#elif/#else/#endif and shows Condition text.
- Handles multiline enumerators (split at the first top-level comma).
"""
import sys
import re
from pathlib import Path
from typing import List, Optional
import json
# ---------- Helpers ----------
BLOCK_COMMENT_RE = re.compile(r'/\*.*?\*/', re.DOTALL)
def strip_comments(s: str) -> str:
s = BLOCK_COMMENT_RE.sub('', s)
s = re.sub(r'//.*', '', s)
return s
def find_top_level_comma(s: str) -> int:
depth = 0
for i, ch in enumerate(s):
if ch == '(':
depth += 1
elif ch == ')':
depth = max(0, depth - 1)
elif ch == ',' and depth == 0:
return i
return -1
def is_plain_int_literal(expr: str) -> Optional[int]:
"""
Return int value if expr is a plain integer literal (dec/hex/bin/oct),
otherwise None. Whitespace ok; no unary ops/casts/suffixes.
"""
t = expr.strip()
if not t:
return None
if re.fullmatch(r'0[xX][0-9A-Fa-f]+', t) or \
re.fullmatch(r'0[bB][01]+', t) or \
re.fullmatch(r'0[0-7]*', t) or \
re.fullmatch(r'[1-9][0-9]*', t) or \
t == '0':
try:
return int(t, 0)
except Exception:
return None
return None
# ---------- Parsing regexes ----------
RE_ENUM_START = re.compile(r'^\s*typedef\s+enum(?:\s+[A-Za-z_]\w*)?\s*\{')
RE_ENUM_END = re.compile(r'^\s*\}\s*([A-Za-z_]\w*)\s*;')
RE_LINE_COMMENT = re.compile(r'^\s*//\s*(.+?)\s*$')
RE_IFDEF = re.compile(r'^\s*#\s*ifdef\s+(\w+)')
RE_IFNDEF = re.compile(r'^\s*#\s*ifndef\s+(\w+)')
RE_IF = re.compile(r'^\s*#\s*if\s+(.+)$')
RE_ELIF = re.compile(r'^\s*#\s*elif\s+(.+)$')
RE_ELSE = re.compile(r'^\s*#\s*else\s*$')
RE_ENDIF = re.compile(r'^\s*#\s*endif\b')
def normalize_condition_text(text: str) -> str:
t = text.strip()
t = re.sub(r'\bdefined\s*\(\s*(\w+)\s*\)', r'\1', t)
t = re.sub(r'\s+', ' ', t)
return t
class ConditionStack:
def __init__(self):
self.stack: List[str] = []
def push_ifdef(self, sym: str): self.stack.append(sym)
def push_ifndef(self, sym: str): self.stack.append(f'!{sym}')
def push_if(self, expr: str): self.stack.append(normalize_condition_text(expr))
def elif_(self, expr: str):
if self.stack: self.stack.pop()
self.stack.append(normalize_condition_text(expr))
def else_(self):
if not self.stack: return
top = self.stack.pop()
if top.startswith('!'): self.stack.append(top[1:])
elif top and all(ch.isalnum() or ch == '_' for ch in top): self.stack.append(f'!{top}')
else: self.stack.append(f'NOT({top})')
def endif(self):
if self.stack: self.stack.pop()
def current(self) -> str:
return " AND ".join(self.stack) if self.stack else ""
def has_active(self) -> bool:
return bool(self.stack)
# ---------- Model ----------
class EnumItem:
def __init__(self, name: str, value_display: str, cond: str):
self.name = name
self.value_display = value_display # number, (number), or raw expr string
self.cond = cond
class EnumDef:
def __init__(self, name: str, source_note: str):
self.name = name
self.source_note = source_note
self.items: List[EnumItem] = []
# ---------- Core parsing ----------
def parse_files(paths: List[Path]) -> List[EnumDef]:
enums: List[EnumDef] = []
outer_cond = ConditionStack()
for path in paths:
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
i = 0
recent_comment: Optional[str] = None
while i < len(lines):
line = lines[i]
# Track outer preproc
if m := RE_IFDEF.match(line): outer_cond.push_ifdef(m.group(1)); i += 1; continue
if m := RE_IFNDEF.match(line): outer_cond.push_ifndef(m.group(1)); i += 1; continue
if m := RE_IF.match(line): outer_cond.push_if(m.group(1)); i += 1; continue
if m := RE_ELIF.match(line): outer_cond.elif_(m.group(1)); i += 1; continue
if RE_ELSE.match(line): outer_cond.else_(); i += 1; continue
if RE_ENDIF.match(line): outer_cond.endif(); i += 1; continue
# Source comment directly above typedef
mcom = RE_LINE_COMMENT.match(line)
if mcom:
recent_comment = mcom.group(1)
if RE_ENUM_START.match(line):
source_note = recent_comment or str(path)
recent_comment = None
body_lines: List[str] = []
i += 1
local_i = i
while local_i < len(lines):
ln = lines[local_i]
if RE_ENUM_END.match(ln):
enum_name = RE_ENUM_END.match(ln).group(1)
enum = EnumDef(enum_name, source_note)
# second pass: parse enumerators
inner = ConditionStack()
current_numeric: Optional[int] = -1 # known numeric head; None means unknown
idx = 0
while idx < len(body_lines):
bl = body_lines[idx]
# inner preproc
if m := RE_IFDEF.match(bl): inner.push_ifdef(m.group(1)); idx += 1; continue
if m := RE_IFNDEF.match(bl): inner.push_ifndef(m.group(1)); idx += 1; continue
if m := RE_IF.match(bl): inner.push_if(m.group(1)); idx += 1; continue
if m := RE_ELIF.match(bl): inner.elif_(m.group(1)); idx += 1; continue
if RE_ELSE.match(bl): inner.else_(); idx += 1; continue
if RE_ENDIF.match(bl): inner.endif(); idx += 1; continue
# accumulate one item across lines
buf = [bl]
while True:
combined = strip_comments(" ".join(buf)).strip()
if not combined:
break
comma_pos = find_top_level_comma(combined)
if comma_pos != -1:
item_text = combined[:comma_pos].strip()
break
if idx + 1 >= len(body_lines):
item_text = combined
break
nxt = body_lines[idx + 1]
if RE_ENUM_END.match(nxt):
item_text = combined
break
idx += 1
buf.append(body_lines[idx])
if not combined:
idx += 1
continue
# NAME or NAME = expr
mitem = re.match(r'^\s*([A-Za-z_]\w*)\s*(?:=\s*(.*))?$', item_text)
if not mitem:
idx += 1
continue
name = mitem.group(1)
expr = (mitem.group(2) or "").strip()
# active condition text
cond_parts = [p for p in (outer_cond.current(), inner.current()) if p]
cond_text = " AND ".join(cond_parts)
# determine display value
if expr:
lit = is_plain_int_literal(expr)
if lit is not None:
# explicit numeric literal
value_display = str(lit)
current_numeric = lit
else:
# show raw expression; numeric chain becomes unknown
value_display = expr
current_numeric = None
else:
# auto-increment if we know a numeric head; else unknown
if current_numeric is None:
value_display = ""
else:
current_numeric += 1
if inner.has_active():
value_display = f"({current_numeric})"
else:
value_display = str(current_numeric)
enum.items.append(EnumItem(name=name, value_display=value_display, cond=cond_text))
idx += 1
enums.append(enum)
i = local_i + 1
break
else:
body_lines.append(lines[local_i])
local_i += 1
else:
i = local_i
continue
else:
i += 1
return enums
# ---------- Markdown rendering ----------
def render_markdown(enums: List[EnumDef]) -> str:
jsonfile = {}
out = []
out.append("# Enumerations\n")
out.append("**Auto-generated reference for MSP, refer to source for development, not this file, due to variations with #ifdefs which needs verification.**\n")
out.append("## Table of contents\n")
for e in sorted(enums, key=lambda x: x.name.lower()):
out.append(f"- [{e.name}](#enum-{e.name.lower()})")
out.append("")
for e in sorted(enums, key=lambda x: x.name.lower()):
jsonfile[e.name] = {}
out.append("---")
out.append(f"## <a id=\"enum-{e.name.lower()}\"></a>`{e.name}`\n")
if e.source_note:
out.append(f"> Source: {e.source_note}\n")
jsonfile[e.name]['_source'] = e.source_note
out.append("| Enumerator | Value | Condition |")
out.append("|---|---:|---|")
for it in e.items:
name_md = f"`{it.name}`"
val = it.value_display
cond = it.cond
out.append(f"| {name_md} | {val} | {cond} |")
jsonfile[e.name][name_md.strip('`')] = [val, cond] if len(cond)>0 else val
# normalize source to a stable inav/src/... path
if '_source' in jsonfile[e.name]:
jsonfile[e.name]['_source'] = jsonfile[e.name]['_source'].replace('../../../src', 'inav/src')
out.append("")
# While we're at it, chuck this all into a JSON file
Path("inav_enums.json").write_text(json.dumps(jsonfile,indent=4), encoding="utf-8")
return "\n".join(out)
# ---------- Main ----------
def main() -> int:
path = Path("all_enums.h")
if not path.exists():
print(f"Error: {path} not found", file=sys.stderr)
return 1
enums = parse_files([path])
md = render_markdown(enums)
Path("inav_enums_ref.md").write_text(md, encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+441
View File
@@ -0,0 +1,441 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Generate Markdown documentation from an MSP message definitions JSON.
Strict + Index:
- STRICT: If a code exists in one (MSPCodes vs JSON) but not the other, crash with details.
- Index items link to headings via GitHub-style auto-anchors.
- Tight layout; identical Request/Reply tables; skip complex=true with a stub.
- Default input: msp_messages.json ; default output: MSP_Doc.md
"""
import sys
import json
import re
import unicodedata
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Type
import enum
def build_msp_codes_enum(defs: Dict[str, Any]) -> Type[enum.IntEnum]:
members: Dict[str, int] = {}
for name, body in defs.items():
try:
code = int(body.get("code", -1))
except (TypeError, ValueError):
continue
members[name] = code
return enum.IntEnum("MSPCodes", members)
# ---- C type size helpers ----------------------------------------------------
BASE_SIZES = {
"uint8_t": 1, "int8_t": 1, "char": 1,
"uint16_t": 2, "int16_t": 2,
"uint32_t": 4, "int32_t": 4,
"uint64_t": 8, "int64_t": 8,
"float": 4, "double": 8,
}
array_brackets_re = re.compile(r"^(?P<base>[A-Za-z_0-9]+)\[(?P<size>.*)\]$")
def parse_ctype(ctype: str) -> Tuple[str, Optional[str]]:
m = array_brackets_re.match(ctype.strip())
if not m:
return ctype.strip(), None
return m.group("base").strip(), m.group("size").strip()
def format_ctype(field: Dict[str, Any]) -> str:
raw = (field.get("ctype") or "").strip()
if not raw:
return "-"
base, bracket = parse_ctype(raw)
has_array_meta = bool(field.get("array", False))
is_array = has_array_meta or (bracket is not None)
if not is_array:
return raw
size_define = (field.get("array_size_define") or "").strip()
array_size = field.get("array_size")
size_expr = ""
if size_define:
size_expr = size_define
else:
if isinstance(array_size, int):
if array_size > 0:
size_expr = str(array_size)
elif isinstance(array_size, str):
cleaned = array_size.strip()
if cleaned and cleaned != "0":
size_expr = cleaned
if not size_expr and bracket is not None:
size_expr = bracket.strip()
if size_expr == "0":
size_expr = ""
base_part = base or raw
return f"{base_part}[{size_expr}]"
def describe_array_bytes(array_size_meta: Any, base_bytes: Optional[int], base_name: str) -> str:
"""
Returns a printable byte-count (or symbolic string) for an array entry.
"""
if isinstance(array_size_meta, int):
if array_size_meta <= 0:
return "array"
if base_bytes is None:
return str(array_size_meta)
return str(array_size_meta * base_bytes)
if isinstance(array_size_meta, str):
expr = array_size_meta.strip()
if not expr:
return "array"
if base_bytes is None or base_name == "char":
return expr
return f"{expr} * {base_bytes}"
return "array"
def sizeof_entry(field: Dict[str, Any]) -> str:
ctype = field.get("ctype", "").strip()
base, bracket = parse_ctype(ctype)
is_array = bool(field.get("array", False))
array_size_meta = field.get("array_size", None)
array_size_define = (field.get("array_size_define") or "").strip()
if is_array or bracket is not None:
base_for_size = base if (base and is_array) else (base or ctype)
base_bytes = BASE_SIZES.get(base_for_size, None)
if is_array:
size_str = describe_array_bytes(array_size_meta, base_bytes, base_for_size)
if array_size_define:
if size_str in {"array", "-"}:
size_str = array_size_define
else:
size_str = f"{size_str} ({array_size_define})"
return size_str
if bracket is not None:
if bracket == "":
return "array"
if bracket.isdigit():
n = int(bracket)
return str(n * base_bytes) if base_bytes is not None else str(n)
if base_bytes is None or base == "char":
return bracket
return f"{bracket} * {base_bytes}"
return "array"
base_bytes = BASE_SIZES.get(base, None)
return str(base_bytes) if base_bytes is not None else "-"
# ---- Markdown rendering -----------------------------------------------------
#inav_wiki_url = "https://github.com/xznhj8129/msp_documentation/blob/master/docs/inav_enums_ref.md"
inav_wiki_url = "https://github.com/iNavFlight/inav/wiki/Enums-reference"
def units_cell(field: Dict[str, Any]) -> str:
if "enum" in field:
if field["enum"]=="?_e":
return "[ENUM_NAME](LINK_TO_ENUM)"
else:
return f"[{field['enum']}]({inav_wiki_url}#enum-{field['enum'].lower()})"
u = (field.get("units") or "").strip()
return u if u else "-"
def has_fields(section: Any) -> bool:
if not isinstance(section, dict):
return False
payload = section.get("payload")
return isinstance(payload, list) and len(payload) > 0
def get_fields(section: Any) -> List[Dict[str, Any]]:
if not isinstance(section, dict):
return []
payload = section.get("payload")
return payload if isinstance(payload, list) else []
def flatten_fields_with_repeats(fields: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Flattens one level of partially repeating payload blocks:
Items with {"repeating": "SOME_SYMBOL", "payload": [...]} are expanded so each child
field gets a symbolic multiplier in the size column.
"""
out: List[Dict[str, Any]] = []
for f in fields:
if isinstance(f, dict) and "repeating" in f and isinstance(f.get("payload"), list):
repeat_sym = str(f["repeating"])
for child in f["payload"]:
if isinstance(child, dict):
c = dict(child)
# Mark repeat multiplier for the size column
c["_repeat_multiplier"] = repeat_sym
out.append(c)
else:
out.append(f)
return out
def table_with_units(fields: List[Dict[str, Any]], label: str) -> str:
flat_fields = flatten_fields_with_repeats(fields)
has_repeats = any(isinstance(f, dict) and f.get("_repeat_multiplier") for f in flat_fields)
has_units = any(
isinstance(f, dict) and (
((f.get("units") or "").strip()) or ("enum" in f)
)
for f in flat_fields
)
# Build dynamic header
cols = ["Field", "C Type"]
if has_repeats:
cols.append("Repeats")
cols.append("Size (Bytes)")
if has_units:
cols.append("Units")
cols.append("Description")
header = " \n**{label}:**\n".format(label=label)
header += "|" + "|".join(cols) + "|\n"
header += "|" + "|".join(["---"] * len(cols)) + "|\n"
# Rows
rows: List[str] = []
for f in flat_fields:
name = f.get("name", "")
size = sizeof_entry(f)
if size == "0":
size = "-"
row_cells = [f"`{name}`", f"`{format_ctype(f)}`"]
if has_repeats:
repeats = f.get("_repeat_multiplier") or "-"
row_cells.append(repeats)
row_cells.append(size)
if has_units:
units = units_cell(f)
row_cells.append(units)
desc = (f.get("desc") or "").strip()
row_cells.append(desc)
rows.append("| " + " | ".join(row_cells) + " |")
return header + "\n".join(rows) + "\n"
def render_variant(parent_name: str, variant_name: str, variant_def: Dict[str, Any]) -> str:
"""
Renders a single variant block (subsection header, description, request/reply tables).
"""
out: List[str] = []
vdesc = (variant_def.get("description") or "").strip()
# GitHub auto-anchors will work off this header text
out.append(f"#### Variant: `{variant_name}`\n\n")
if vdesc:
out.append(f"**Description:** {vdesc} \n")
req = variant_def.get("request", None)
rep = variant_def.get("reply", None)
if has_fields(req):
out.append(table_with_units(get_fields(req), "Request Payload"))
else:
out.append("\n**Request Payload:** **None** \n")
if has_fields(rep):
out.append(table_with_units(get_fields(rep), "Reply Payload"))
else:
out.append("\n**Reply Payload:** **None** \n")
out.append("\n")
return "".join(out)
def render_message(name: str, msg: Dict[str, Any]) -> Tuple[str, str]:
"""
Returns (section_markdown, heading_text_for_anchor)
"""
code = msg.get("code", 0)
hex_str = msg.get("hex", hex(code))
description = (msg.get("description") or "").strip()
notes = (msg.get("notes") or "").strip()
complex_flag = bool(msg.get("complex", False))
heading = f'## <a id="{name.lower()}"></a>`{name} ({code} / {hex_str})`'
out = [heading + "\n"]
if description:
out.append(f"**Description:** {description} \n")
#if complex_flag:
# out.append("**Special case, skipped for now**\n\n")
# return "".join(out), heading
# NEW: variant-aware rendering
variants = msg.get("variants")
if isinstance(variants, dict) and variants:
# For variant messages, render a compact per-variant table set
for vname, vdef in variants.items():
out.append(render_variant(name, vname, vdef))
else:
# Fallback: single request/reply like before
req = msg.get("request", None)
rep = msg.get("reply", None)
if has_fields(req):
out.append(table_with_units(get_fields(req), "Request Payload"))
else:
out.append("\n**Request Payload:** **None** \n")
if has_fields(rep):
out.append(table_with_units(get_fields(rep), "Reply Payload"))
else:
out.append("\n**Reply Payload:** **None** \n")
if notes:
out.append(f"\n**Notes:** {notes}\n")
out.append("\n")
return "".join(out), heading
# ---- Index + strict consistency --------------------------------------------
def build_maps(defs: Dict[str, Any], codes_cls: Type[enum.IntEnum]) -> Tuple[Dict[int, str], Dict[int, str]]:
"""
Returns:
json_by_code: {code -> message_name_from_json}
mw_by_code: {code -> enum_name_from codes_cls}
Only for codes in the enforced ranges (v1 and v2).
"""
v1_range = range(0, 255)
v2_range = range(4096, 20001)
# JSON: build by code (restrict to ranges)
json_by_code: Dict[int, str] = {}
for name, body in defs.items():
code = int(body.get("code", -1))
if code in v1_range or code in v2_range:
json_by_code[code] = name
# MSPCodes: probe the same ranges
mw_by_code: Dict[int, str] = {}
def try_get(code: int) -> Optional[str]:
try:
e = codes_cls(code)
return e.name
except Exception:
return None
for code in list(v1_range) + list(v2_range):
ename = try_get(code)
if ename is not None:
mw_by_code[code] = ename
return json_by_code, mw_by_code
def enforce_strict_match(json_by_code: Dict[int, str], mw_by_code: Dict[int, str]) -> None:
json_codes = set(json_by_code.keys())
mw_codes = set(mw_by_code.keys())
only_in_json = sorted(json_codes - mw_codes)
only_in_mw = sorted(mw_codes - json_codes)
if only_in_json or only_in_mw:
lines = ["MSP code mismatch detected:"]
if only_in_json:
lines.append(" Present in JSON but missing in MSPCodes:")
for c in only_in_json:
lines.append(f" {c}\t{json_by_code[c]}")
if only_in_mw:
lines.append(" Present in MSPCodes but missing in JSON:")
for c in only_in_mw:
lines.append(f" {c}\t{mw_by_code[c]}")
raise SystemExit("\n".join(lines))
def build_index(json_by_code: Dict[int, str]) -> str:
"""
Build a compact index linking to each heading.
"""
v1 = []
v2 = []
for code, name in sorted(json_by_code.items()):
hex_str = hex(code)
item = f"[{code} - {name}](#{name.lower()}) "
if 0 <= code <= 255:
v1.append(item)
elif 4096 <= code <= 20000:
v2.append(item)
parts = ["## Index", "### MSPv1"]
parts.extend(v1)
parts.append("\n### MSPv2")
parts.extend(v2)
parts.append("") # trailing newline
return "\n".join(parts)
# ---- Orchestration ----------------------------------------------------------
def generate_markdown(defs: Dict[str, Any]) -> str:
# Strict maps & check
codes_enum = build_msp_codes_enum(defs)
json_by_code, mw_by_code = build_maps(defs, codes_enum)
enforce_strict_match(json_by_code, mw_by_code)
# Build sections, remembering headings for slugging (already handled in index)
items = sorted(((int(body.get("code", 0)), name, body) for name, body in defs.items()),
key=lambda t: t[0])
sections = []
for _, name, body in items:
sec, _heading = render_message(name, body)
sections.append(sec)
with open("docs_v2_header.md", "r", encoding="utf-8") as f:
header = f.read()
with open("format.md", "r", encoding="utf-8") as f:
fmt = f.read()
with open("msp_messages.checksum", "r", encoding="utf-8") as f:
chksum = f.read().split(' ')[0]
with open("rev", "r", encoding="utf-8") as f:
rev = f.read()
header = header.replace('<format>',fmt)
header = header.replace('<file_rev>',rev)
header = header.replace('<file_hash>',chksum)
index_md = build_index(json_by_code)
return header + "\n" + index_md + "\n" + "".join(sections)
def main():
in_path = Path(sys.argv[1]) if len(sys.argv) >= 2 else Path("msp_messages.json")
out_path = Path(sys.argv[2]) if len(sys.argv) >= 3 else Path("README.md")
with in_path.open("r", encoding="utf-8") as f:
defs = json.load(f)
md = generate_markdown(defs)
out_path.write_text(md, encoding="utf-8")
print(f"Wrote {out_path}")
if __name__ == "__main__":
main()
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
import argparse
import datetime
import re
from pathlib import Path
SUBDIRS = [
'common',
'blackbox',
'navigation',
'sensors',
'programming',
'rx',
'telemetry',
'io',
'flight',
'fc',
'drivers',
]
def strip_comments(text: str) -> str:
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) # block comments
text = re.sub(r'//.*', '', text) # line comments
return text
def extract_enums(fn: str, text: str):
src = strip_comments(text)
out = []
# typedef enum { ... } Alias;
i = 0
while True:
m = re.search(r'\btypedef\s+enum\b', src[i:])
if not m: break
start = i + m.start()
lb = src.find('{', i + m.end())
if lb == -1: break
depth = 0
k = lb
while k < len(src):
if src[k] == '{': depth += 1
elif src[k] == '}':
depth -= 1
if depth == 0:
semi = src.find(';', k)
if semi == -1: break
tail = src[k+1:semi]
alias = re.findall(r'\b([A-Za-z_]\w*)\b', tail)
if alias:
block = src[start:semi+1].strip()
out += [f'// {fn}\n', block + '\n\n']
i = semi + 1
break
k += 1
else:
break
# enum Tag { ... }; → also emit typedef enum Tag Tag;
i = 0
while True:
m = re.search(r'\benum\s+([A-Za-z_]\w*)\s*{', src[i:])
if not m: break
start = i + m.start()
tag = m.group(1)
lb = src.find('{', i + m.end() - 1)
if lb == -1: break
depth = 0
k = lb
while k < len(src):
if src[k] == '{': depth += 1
elif src[k] == '}':
depth -= 1
if depth == 0:
semi = src.find(';', k)
if semi == -1: break
block = src[start:semi+1].strip()
out += [
f'// {fn}\n',
block + '\n',
f'typedef enum {tag} {tag};\n\n'
]
i = semi + 1
break
k += 1
else:
break
return out
all_enums = []
def parse_args():
parser = argparse.ArgumentParser(description='Collect all enums from INAV sources.')
parser.add_argument(
'--inav-root',
default='../inav/src/main',
help="Path to the INAV 'src/main' directory (default: %(default)s)",
)
return parser.parse_args()
args = parse_args()
base_dir = Path(args.inav_root).expanduser()
for sd in SUBDIRS:
root = base_dir / sd
if not root.is_dir():
continue
for fn in root.rglob('*'):
print(fn)
if fn.suffix in ('.c', '.h'):
txt = fn.read_text(errors='ignore')
ret = extract_enums(fn, txt)
if ret: print(fn)
all_enums.extend(ret)
with open('all_enums.h', 'w') as out:
out.write(f"// Consolidated enums — generated on {datetime.datetime.now()}\n\n")
out.writelines(all_enums)
print(f"Found {len(all_enums)} enums. Wrote all_enums.h.")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
c9458e9a712b7a4f3bc9333aa7bc3dcb
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
5