Selaa lähdekoodia

Automate VKFly dialect regeneration and staleness check

LiuYang 1 viikko sitten
vanhempi
sitoutus
ea0930498d
7 muutettua tiedostoa jossa 292 lisäystä ja 6 poistoa
  1. 39 3
      README.md
  2. 81 2
      motian_bridge/dialects/v20/VKFly.py
  3. 1 1
      msg_definitions
  4. 34 0
      tests/test_dialect_sync.py
  5. 1 0
      tools/__init__.py
  6. 23 0
      tools/check_dialect.py
  7. 113 0
      tools/gen_dialect.py

+ 39 - 3
README.md

@@ -189,14 +189,50 @@ mosquitto_sub -h 127.0.0.1 -p 1883 \
 .venv/bin/python -m unittest discover -s tests -v
 ```
 
-方言源文件由 `msg_definitions` Git 子模块提供,实际 XML 位于 `msg_definitions/msg_definitions/`。若子模块中的 XML 改动,需要用项目环境中的 `pymavlink` 重新生成提交的 Python 文件(原文件不要手工修改):
+方言源文件由 `msg_definitions` Git 子模块提供,实际 XML 位于 `msg_definitions/msg_definitions/`。`motian_bridge/dialects/v20/VKFly.py` 是**生成文件,不要手工修改**。
+
+更新方言并重新生成:
+
+```bash
+# 1. 拉取最新方言
+git -C msg_definitions pull origin main
+
+# 2. 重新生成(脚本内部仍是调用 pymavlink 的 mavgen)
+.venv/bin/python tools/gen_dialect.py
+
+# 3. 验证并跑测试
+.venv/bin/python tools/check_dialect.py
+.venv/bin/python -m unittest discover -s tests -v
+
+# 4. 一起提交子模块指针与生成文件
+git add msg_definitions motian_bridge/dialects/v20/VKFly.py
+git commit -m 'Regenerate VKFly dialect from submodule'
+```
+
+`tools/gen_dialect.py` 会先检查 XML 是否齐全(未初始化子模块时给出明确提示),再在临时目录生成、与现有文件比对,只有内容变化时才覆盖,因此可以安全地反复执行。常用选项:
+
+| 命令 | 作用 |
+| --- | --- |
+| `tools/gen_dialect.py` | 按需重新生成 |
+| `tools/gen_dialect.py --dry-run` | 只报告是否需要重新生成,不写文件 |
+| `tools/gen_dialect.py --check` | 校验已提交文件是否与 XML 一致,不一致时退出码非 0 |
+| `tools/check_dialect.py` | 同上,供 pre-commit / CI 调用 |
+| `--no-validate` | 跳过 mavgen 的 XML 校验,可忽略上游 bitmask 告警 |
+
+也可以直接调用 mavgen(注意 `--output` 要写完整文件名,写目录名会生成 `v20.py` 从而破坏导入):
 
 ```bash
 .venv/bin/python -m pymavlink.tools.mavgen --lang Python --wire-protocol 2.0 \
     --output motian_bridge/dialects/v20/VKFly.py msg_definitions/msg_definitions/VKFly.xml
-.venv/bin/python -m unittest discover -s tests -v
 ```
 
-当前生成文件使用 pymavlink **2.4.50**;生成时会显示 XML bitmask 枚举的已有警告,但会完成生成。重新生成后应复测真实 MAVLink 帧,以确认设备 XML 版本和校验码兼容。
+建议把 CI 接入 `--check`,并在本地安装 pre-commit 钩子:
+
+```bash
+.venv/bin/python tools/check_dialect.py || \
+  echo 'VKFly.py 已过期,请运行 .venv/bin/python tools/gen_dialect.py'
+```
+
+`tests/test_dialect_sync.py` 也会在单元测试里做同样的过期检查,所以提交前跑一次 `unittest` 就能发现忘记重新生成的情况。当前生成文件使用 pymavlink **2.4.50**;生成时会显示 XML bitmask 枚举的已有警告,但会完成生成;换 pymavlink 版本后生成的字节内容可能不同,应一并提交并复测真实 MAVLink 帧。
 
 协议仅定义 `/PUBLISH/无人机id` 为“透传包”,没有提供上行载荷示例。本实现按 **Base JSON + DataType 526 + HEX Data** 约定,正式联调前需平台确认。`TimeStamp` 为本机时区 ISO-8601 时间;应确保设备时间正确。MQTT QoS 0 降低重放风险,但不是严格端到端交付保证。此服务不做飞行安全仲裁,正式部署应限制 Broker 访问权限。

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 81 - 2
motian_bridge/dialects/v20/VKFly.py


+ 1 - 1
msg_definitions

@@ -1 +1 @@
-Subproject commit fce609a7d8063a59f7ca203b48f396ea517de5f2
+Subproject commit 6738b07dcf819b945efed00e88b4ba500ccf38d0

+ 34 - 0
tests/test_dialect_sync.py

@@ -0,0 +1,34 @@
+"""Guard the committed generated dialect against submodule drift."""
+
+import unittest
+
+from tools import gen_dialect
+from tools.gen_dialect import INCLUDES, SOURCE
+
+
+class DialectSyncTests(unittest.TestCase):
+    def test_xml_sources_are_present(self):
+        missing = [
+            str(path)
+            for path in (SOURCE, *(SOURCE.parent / name for name in INCLUDES))
+            if not path.is_file()
+        ]
+        self.assertEqual(
+            missing,
+            [],
+            "run `git submodule update --init --recursive` before testing",
+        )
+
+    def test_generated_dialect_is_up_to_date(self):
+        if not gen_dialect.OUTPUT.is_file():
+            self.fail(f"{gen_dialect.OUTPUT} is missing; run: python tools/gen_dialect.py")
+        updated = gen_dialect.generate(dry_run=True, no_validate=True)
+        self.assertFalse(
+            updated,
+            "motian_bridge/dialects/v20/VKFly.py is stale; regenerate with "
+            "`.venv/bin/python tools/gen_dialect.py`",
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()

+ 1 - 0
tools/__init__.py

@@ -0,0 +1 @@
+"""Development helper scripts (not part of the installed package)."""

+ 23 - 0
tools/check_dialect.py

@@ -0,0 +1,23 @@
+"""Verify the committed generated dialect matches the dialect submodule XML.
+
+Intended for pre-commit hooks and CI. Exits nonzero when regeneration is needed.
+"""
+
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+from tools import gen_dialect
+
+
+def main() -> int:
+    try:
+        gen_dialect.generate(check=True, no_validate=True)
+    except SystemExit as exc:
+        print(exc, file=sys.stderr)
+        return 1
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 113 - 0
tools/gen_dialect.py

@@ -0,0 +1,113 @@
+"""Regenerate motian_bridge/dialects/v20/VKFly.py from the dialect submodule.
+
+The generated module must never be hand-edited. Run this script after updating
+the `msg_definitions` submodule, then commit both the submodule pointer and the
+regenerated file.
+"""
+
+import argparse
+import hashlib
+import logging
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+if __package__ in (None, ""):
+    # Allow `python tools/gen_dialect.py` as well as `from tools import ...`.
+    sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+ROOT = Path(__file__).resolve().parent.parent
+OUTPUT = ROOT / "motian_bridge" / "dialects" / "v20" / "VKFly.py"
+SOURCE = ROOT / "msg_definitions" / "msg_definitions" / "VKFly.xml"
+INCLUDES = ("common.xml", "standard.xml", "minimal.xml")
+
+LOG = logging.getLogger("gen_dialect")
+
+
+def _sha256(path: Path) -> str:
+    digest = hashlib.sha256()
+    with open(path, "rb") as stream:
+        for block in iter(lambda: stream.read(1 << 20), b""):
+            digest.update(block)
+    return digest.hexdigest()
+
+
+def check_sources() -> None:
+    """Fail early with an actionable message instead of a mavgen traceback."""
+    missing = [path for path in (SOURCE, *(SOURCE.parent / name for name in INCLUDES))
+               if not path.is_file()]
+    if missing:
+        listing = "\n".join(f"  - {path}" for path in missing)
+        raise SystemExit(
+            "MAVLink XML sources are missing:\n"
+            f"{listing}\n"
+            "Initialize the dialect submodule first:\n"
+            "  git submodule update --init --recursive"
+        )
+
+
+def mavgen(extra_args: list[str], output: Path) -> None:
+    command = [
+        sys.executable, "-m", "pymavlink.tools.mavgen",
+        "--lang", "Python",
+        "--wire-protocol", "2.0",
+        "--output", str(output),
+        *extra_args,
+        str(SOURCE),
+    ]
+    LOG.debug("running: %s", " ".join(command))
+    subprocess.run(command, cwd=ROOT, check=True)
+
+
+def generate(dry_run: bool = False, check: bool = False, no_validate: bool = False) -> bool:
+    """Generate the dialect. Returns True when the committed file was updated."""
+    check_sources()
+    previous = _sha256(OUTPUT) if OUTPUT.is_file() else None
+    with tempfile.TemporaryDirectory(prefix="mavgen-") as directory:
+        candidate = Path(directory) / "VKFly.py"
+        mavgen(["--no-validate"] if no_validate else [], candidate)
+        generated = candidate.read_bytes()
+    if check:
+        if previous is None:
+            raise SystemExit(f"{OUTPUT} does not exist; run: python tools/gen_dialect.py")
+        if _sha256(OUTPUT) != hashlib.sha256(generated).hexdigest():
+            raise SystemExit(
+                f"{OUTPUT} is stale. Regenerate with: python tools/gen_dialect.py"
+            )
+        LOG.info("generated dialect matches the submodule XML")
+        return False
+    if previous == hashlib.sha256(generated).hexdigest():
+        LOG.info("generated dialect already up to date")
+        return False
+    if dry_run:
+        LOG.info("dry run: %s would be regenerated", OUTPUT)
+        return True
+    OUTPUT.parent.mkdir(parents=True, exist_ok=True)
+    OUTPUT.write_bytes(generated)
+    LOG.info("wrote %s (%d bytes)", OUTPUT, len(generated))
+    return True
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("--check", action="store_true",
+                        help="verify the committed file matches the XML; do not write")
+    parser.add_argument("--dry-run", action="store_true",
+                        help="report whether regeneration is needed; do not write")
+    parser.add_argument("--no-validate", action="store_true",
+                        help="pass --no-validate to mavgen (skips upstream enum warnings)")
+    parser.add_argument("--quiet", action="store_true")
+    args = parser.parse_args()
+    logging.basicConfig(
+        level=logging.WARNING if args.quiet else logging.INFO,
+        format="%(levelname)s %(name)s: %(message)s",
+    )
+    try:
+        generate(dry_run=args.dry_run, check=args.check, no_validate=args.no_validate)
+    except subprocess.CalledProcessError as exc:
+        raise SystemExit(f"mavgen failed with exit code {exc.returncode}")
+
+
+if __name__ == "__main__":
+    main()

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä