From 201e357ba5b291298f8995f3c909e66545aeb261 Mon Sep 17 00:00:00 2001 From: yuzhiliu8 Date: Mon, 27 Apr 2026 21:28:26 -0400 Subject: [PATCH 1/9] half working docs --- .github/workflows/docs.yml | 16 +++++++++++ .gitignore | 1 + docs/gen_pages.py | 56 ++++++++++++++++++++++++++++++++++++++ properdocs.yml | 31 +++++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/gen_pages.py create mode 100644 properdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..d7c45c74 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,16 @@ +name: Deploy Docs + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - run: pip install "mkdocs<2.0" mkdocs-material mkdocstrings[python] mkdocs-gen-files mkdocs-literate-nav + - run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index 4a611b08..ea478ae1 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ crash course/data/MNIST/raw/* node_modules *venv +.claude diff --git a/docs/gen_pages.py b/docs/gen_pages.py new file mode 100644 index 00000000..f381f422 --- /dev/null +++ b/docs/gen_pages.py @@ -0,0 +1,56 @@ +from pathlib import Path +import mkdocs_gen_files + +SRC_ROOT = Path("controls/sae_2025_ws/src") + +INCLUDED_PACKAGES = { + "integration", + "uav", + "sim", + "tools", +} + +SKIP_FILES = { + "__init__.py", + "setup.py", +} + +# Map of virtual doc path -> source README path (relative to repo root) +README_PAGES = { + "index.md": "README.md", + "controls/index.md": "controls/sae_2025_ws/README.md", + "controls/uav.md": "controls/sae_2025_ws/src/uav/README.md", + "controls/payload.md": "controls/sae_2025_ws/src/payload/README.md", + "controls/integration.md": "controls/sae_2025_ws/src/integration/README.md", + "controls/sim.md": "controls/sae_2025_ws/src/sim/README.md", + "controls/tools.md": "controls/sae_2025_ws/src/tools/README.md", + "controls/custom_sim_bridge.md": "controls/sae_2025_ws/src/custom_sim_bridge/README.md", + "controls/custom_gz_plugins.md": "controls/sae_2025_ws/src/custom_gz_plugins/README.md", + "sim/gazebo_harmonic.md": "sim/sae aero/gazebo harmonic/README.md", +} + +for doc_path, readme in README_PAGES.items(): + src = Path(readme) + if src.exists(): + with mkdocs_gen_files.open(doc_path, "w") as fd: + fd.write(src.read_text()) + mkdocs_gen_files.set_edit_path(doc_path, src) + +for package in INCLUDED_PACKAGES: + for path in sorted((SRC_ROOT / package).rglob("*.py")): + if path.name in SKIP_FILES: + continue + + if not (path.parent / "__init__.py").exists(): + continue + + module_name = ".".join( + path.relative_to(SRC_ROOT).with_suffix("").parts + ) + + doc_path = Path("API", *module_name.split(".")).with_suffix(".md") + + with mkdocs_gen_files.open(doc_path, "w") as fd: + print(f"::: {module_name}", file=fd) + + mkdocs_gen_files.set_edit_path(doc_path, path) diff --git a/properdocs.yml b/properdocs.yml new file mode 100644 index 00000000..dccc1bd4 --- /dev/null +++ b/properdocs.yml @@ -0,0 +1,31 @@ +site_name: PennAir Monorepo +repo_url: https://github.com/pennaerial/monorepo + +theme: + name: material + + + +nav: + - Home: index.md + - Controls: + - Overview: controls/index.md + - UAV: controls/uav.md + - Payload: controls/payload.md + - Integration: controls/integration.md + - Simulation: controls/sim.md + - Tools: controls/tools.md + - Custom Sim Bridge: controls/custom_sim_bridge.md + - Custom Gz Plugins: controls/custom_gz_plugins.md + +plugins: + - search + - mkdocstrings: + handlers: + python: + paths: + - controls/sae_2025_ws/src + - gen-files: + scripts: + - docs/gen_pages.py + - literate-nav: From 4c8fdc613e22d4a8106efb0bc30e0419d6668e72 Mon Sep 17 00:00:00 2001 From: yuzhiliu8 Date: Tue, 28 Apr 2026 21:43:25 -0400 Subject: [PATCH 2/9] docs python, c++ api reference --- .../src/payload/include/payload/motor.hpp | 17 +++- docs/gen_pages.py | 56 ------------- docs/gen_ref_pages.py | 82 +++++++++++++++++++ docs/getting-started.md | 3 + docs/index.md | 0 properdocs.yml | 43 +++++++--- 6 files changed, 130 insertions(+), 71 deletions(-) delete mode 100644 docs/gen_pages.py create mode 100644 docs/gen_ref_pages.py create mode 100644 docs/getting-started.md create mode 100644 docs/index.md diff --git a/controls/sae_2025_ws/src/payload/include/payload/motor.hpp b/controls/sae_2025_ws/src/payload/include/payload/motor.hpp index 59d805d5..9c0e9b75 100644 --- a/controls/sae_2025_ws/src/payload/include/payload/motor.hpp +++ b/controls/sae_2025_ws/src/payload/include/payload/motor.hpp @@ -5,23 +5,36 @@ #include #include "payload/gpio.hpp" +/// Which side of the vehicle this motor drives. enum class MotorType { - LEFT, - RIGHT + LEFT, ///< Left-side motor + RIGHT ///< Right-side motor }; +/// Abstract base class for all motor drivers. class Motor { public: virtual ~Motor() = default; + /// Set motor speed in [-1, 1] (negative = reverse). virtual void set_speed(float speed) = 0; + /// Drive forward at the given PWM duty cycle [0, 1]. virtual void forward(float duty) = 0; + /// Drive in reverse at the given PWM duty cycle [0, 1]. virtual void reverse(float duty) = 0; + /// Let the motor coast (no braking force). virtual void coast() = 0; + /// Apply electronic braking immediately. virtual void hard_brake() = 0; }; +/// Motor driver for the DRV8833 H-bridge IC. class DRVMotor : public Motor { public: + /// @param pi pigpio instance handle + /// @param in1 GPIO pin for IN1 + /// @param in2 GPIO pin for IN2 + /// @param frequency PWM frequency in Hz + /// @param motor_type Which side this motor drives DRVMotor(int pi, int in1, int in2, int frequency, MotorType motor_type); void set_speed(float speed) override; void forward(float duty) override; diff --git a/docs/gen_pages.py b/docs/gen_pages.py deleted file mode 100644 index f381f422..00000000 --- a/docs/gen_pages.py +++ /dev/null @@ -1,56 +0,0 @@ -from pathlib import Path -import mkdocs_gen_files - -SRC_ROOT = Path("controls/sae_2025_ws/src") - -INCLUDED_PACKAGES = { - "integration", - "uav", - "sim", - "tools", -} - -SKIP_FILES = { - "__init__.py", - "setup.py", -} - -# Map of virtual doc path -> source README path (relative to repo root) -README_PAGES = { - "index.md": "README.md", - "controls/index.md": "controls/sae_2025_ws/README.md", - "controls/uav.md": "controls/sae_2025_ws/src/uav/README.md", - "controls/payload.md": "controls/sae_2025_ws/src/payload/README.md", - "controls/integration.md": "controls/sae_2025_ws/src/integration/README.md", - "controls/sim.md": "controls/sae_2025_ws/src/sim/README.md", - "controls/tools.md": "controls/sae_2025_ws/src/tools/README.md", - "controls/custom_sim_bridge.md": "controls/sae_2025_ws/src/custom_sim_bridge/README.md", - "controls/custom_gz_plugins.md": "controls/sae_2025_ws/src/custom_gz_plugins/README.md", - "sim/gazebo_harmonic.md": "sim/sae aero/gazebo harmonic/README.md", -} - -for doc_path, readme in README_PAGES.items(): - src = Path(readme) - if src.exists(): - with mkdocs_gen_files.open(doc_path, "w") as fd: - fd.write(src.read_text()) - mkdocs_gen_files.set_edit_path(doc_path, src) - -for package in INCLUDED_PACKAGES: - for path in sorted((SRC_ROOT / package).rglob("*.py")): - if path.name in SKIP_FILES: - continue - - if not (path.parent / "__init__.py").exists(): - continue - - module_name = ".".join( - path.relative_to(SRC_ROOT).with_suffix("").parts - ) - - doc_path = Path("API", *module_name.split(".")).with_suffix(".md") - - with mkdocs_gen_files.open(doc_path, "w") as fd: - print(f"::: {module_name}", file=fd) - - mkdocs_gen_files.set_edit_path(doc_path, path) diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py new file mode 100644 index 00000000..edb4e4c2 --- /dev/null +++ b/docs/gen_ref_pages.py @@ -0,0 +1,82 @@ +"""Generate the code reference pages.""" + +from pathlib import Path +import mkdocs_gen_files + +root = Path(__file__).resolve().parent.parent +src = root / "controls" / "sae_2025_ws" / "src" + +# ----------------------------- +# Python package roots (REAL import roots) +# ----------------------------- +PYTHON_PACKAGE_MODULE_ROOTS = { + src / "sim" / "sim", + src / "uav" / "uav", + src / "udp_bridge" / "udp_bridge", + src / "tools", +} + +# ----------------------------- +# Skip ROS/non-Python API dirs +# ----------------------------- +PY_PKG_SKIP_DIRS = { + "launch", + "config", + "urdf", + "meshes", + "worlds", + "models" +} + +SKIP_FILES = { + "setup", +} + +nav = mkdocs_gen_files.Nav() + +for path in sorted(src.rglob("*.py")): + + valid_module = False + for pkg_module in PYTHON_PACKAGE_MODULE_ROOTS: + if str(path).startswith(str(pkg_module)): + valid_module = True + + if not valid_module: + continue + + # ----------------------------- + # Convert to module path relative to Python package root + # ----------------------------- + module_path = path.relative_to(src).with_suffix("") + parts = list(module_path.parts) + + if not parts: + continue + + if any(part in PY_PKG_SKIP_DIRS for part in parts): + continue + + if parts[-1] in SKIP_FILES: + continue + + doc_path = module_path.with_suffix(".md") + full_doc_path = Path("reference", doc_path) + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1] == "__main__": + continue + + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + identifier = ".".join(parts) + print(f"::: {identifier}", file=fd) + + # print(full_doc_path) + mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) + +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..d5eca4a7 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,3 @@ +# Getting Started + +TODO diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..e69de29b diff --git a/properdocs.yml b/properdocs.yml index dccc1bd4..00210a54 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -3,29 +3,46 @@ repo_url: https://github.com/pennaerial/monorepo theme: name: material - + logo: https://www.pennaerial.com/wp-content/uploads/2023/08/b-removebg-preview-150x150.png + icon: + repo: fontawesome/solid/trash + features: + - navigation.tabs nav: - Home: index.md - - Controls: - - Overview: controls/index.md - - UAV: controls/uav.md - - Payload: controls/payload.md - - Integration: controls/integration.md - - Simulation: controls/sim.md - - Tools: controls/tools.md - - Custom Sim Bridge: controls/custom_sim_bridge.md - - Custom Gz Plugins: controls/custom_gz_plugins.md + - Getting Started: getting-started.md + - API Reference: + - Python: reference/ + - C++: + - payload: payload/annotated.md + - custom_sim_bridge: custom_sim_bridge/annotated.md plugins: - search + - gen-files: + scripts: + - docs/gen_ref_pages.py - mkdocstrings: handlers: python: paths: - controls/sae_2025_ws/src - - gen-files: - scripts: - - docs/gen_pages.py - literate-nav: + nav_file: SUMMARY.md + - section-index + - mkdoxy: + projects: + payload: + src-dirs: controls/sae_2025_ws/src/payload/include controls/sae_2025_ws/src/payload/src + full-doc: true + doxy-cfg: + OUTPUT_LANGUAGE: English + EXTRACT_ALL: YES + custom_sim_bridge: + src-dirs: controls/sae_2025_ws/src/custom_sim_bridge/include controls/sae_2025_ws/src/custom_sim_bridge/src + full-doc: true + doxy-cfg: + OUTPUT_LANGUAGE: English + EXTRACT_ALL: YES From e6e50f6e45842c93439bb6858ffc24c1bc777c13 Mon Sep 17 00:00:00 2001 From: yuzhiliu8 Date: Tue, 28 Apr 2026 23:00:19 -0400 Subject: [PATCH 3/9] general structure, github action --- .github/workflows/docs.yml | 35 ++++++++++++++++++++++++++++++++--- .gitignore | 1 + docs/concepts.md | 4 ++++ docs/gen_ref_pages.py | 13 +++++++++---- properdocs.yml | 1 + 5 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 docs/concepts.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d7c45c74..0fe620b2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,16 +1,45 @@ -name: Deploy Docs +name: Docs on: push: branches: [main] + pull_request: jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - run: | + pip install \ + properdocs \ + mkdocs-material \ + mkdocstrings[python] \ + mkdocs-gen-files \ + mkdocs-literate-nav \ + mkdocs-section-index \ + mkdoxy + - run: properdocs build + deploy: + if: github.ref == 'refs/heads/main' + needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.10" - - run: pip install "mkdocs<2.0" mkdocs-material mkdocstrings[python] mkdocs-gen-files mkdocs-literate-nav - - run: mkdocs gh-deploy --force + - run: | + pip install \ + properdocs \ + mkdocs-material \ + mkdocstrings[python] \ + mkdocs-gen-files \ + mkdocs-literate-nav \ + mkdocs-section-index \ + mkdoxy + - run: properdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index ea478ae1..93b88332 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ crash course/data/MNIST/raw/* node_modules *venv .claude +site/ diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 00000000..43ce2c89 --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,4 @@ +# Concepts + + +TODO diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py index edb4e4c2..c8421b0b 100644 --- a/docs/gen_ref_pages.py +++ b/docs/gen_ref_pages.py @@ -1,4 +1,7 @@ -"""Generate the code reference pages.""" +""" +Generate the code reference pages. +Referenced this guide: https://mkdocstrings.github.io/recipes/ +""" from pathlib import Path import mkdocs_gen_files @@ -36,6 +39,7 @@ for path in sorted(src.rglob("*.py")): + # restict to only listed python packages valid_module = False for pkg_module in PYTHON_PACKAGE_MODULE_ROOTS: if str(path).startswith(str(pkg_module)): @@ -44,15 +48,16 @@ if not valid_module: continue - # ----------------------------- - # Convert to module path relative to Python package root - # ----------------------------- + # ex: uav/uav/modes/Mode module_path = path.relative_to(src).with_suffix("") parts = list(module_path.parts) if not parts: continue + # skip irrelevant directories within package. + # only relevant code in ros packages are the inner dir, like uav/uav or sim/sim + # launch files can be hand documented if any(part in PY_PKG_SKIP_DIRS for part in parts): continue diff --git a/properdocs.yml b/properdocs.yml index 00210a54..930994c6 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -13,6 +13,7 @@ theme: nav: - Home: index.md - Getting Started: getting-started.md + - Concepts: concepts.md - API Reference: - Python: reference/ - C++: From 6fceb67425a4bb7eada995a16ee7101a2c8efb98 Mon Sep 17 00:00:00 2001 From: yuzhiliu8 Date: Thu, 30 Apr 2026 23:35:11 -0400 Subject: [PATCH 4/9] compiling ros package READMEs and additional documentation in gen_pkg_pages.py --- docs/assets/pennair-preview-150x150.png | Bin 0 -> 11320 bytes docs/doc_utils.py | 81 ++++++++++++++++++++++++ docs/gen_pkg_pages.py | 52 +++++++++++++++ docs/gen_ref_pages.py | 19 ++++-- properdocs.yml | 12 +++- 5 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 docs/assets/pennair-preview-150x150.png create mode 100644 docs/doc_utils.py create mode 100644 docs/gen_pkg_pages.py diff --git a/docs/assets/pennair-preview-150x150.png b/docs/assets/pennair-preview-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..56c1f9d579f5fed6ea421562515e0a25211d33f8 GIT binary patch literal 11320 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 nycYnb6hr1-fl set[Path]: + """Return absolute paths of all submodules declared in the repo's .gitmodules.""" + if not GITMODULES.exists(): + return set() + paths: set[Path] = set() + for line in GITMODULES.read_text().splitlines(): + line = line.strip() + if line.startswith("path ="): + rel = line.split("=", 1)[1].strip() + paths.add(REPO_ROOT / rel) + return paths + + +def find_ros_packages( + src: Path, ament_type="all", exclude_submodules=True +) -> set[Path]: + """Return the set of ROS package directories under *src*. + + A ROS package is any directory that contains a ``package.xml`` file. + Only direct children of *src* are searched so that vendored/nested + package trees (e.g. ``ros_gz/ros_gz_sim/``) are excluded. + + Args: + src: The source directory to search in. + ament_type: Filter by build type — ``"all"``, ``"python"`` + (``ament_python``), or ``"cmake"`` (``ament_cmake``). + exclude_submodules: When ``True`` (default), directories that are + git submodules are excluded. + """ + if ament_type not in ("all", "python", "cmake"): + raise ValueError("ament_type must be 'all', 'python', or 'cmake'") + + src = src.resolve() + pkg_dirs = {pkg_xml.parent for pkg_xml in src.glob("*/package.xml")} + + if exclude_submodules: + pkg_dirs -= submodule_dirs() + + if ament_type == "all": + return pkg_dirs + + target_build_type = "ament_python" if ament_type == "python" else "ament_cmake" + + return { + pkg_dir + for pkg_dir in pkg_dirs + if ET.parse(pkg_dir / "package.xml").getroot().findtext("export/build_type") + == target_build_type + } + + +if __name__ == "__main__": + print("submodules:") + for smd in submodule_dirs(): + print(smd) + + print("\nALL:") + for pkg in find_ros_packages(Path("controls/sae_2025_ws/src")): + print(pkg) + + print("\nPYTHON:") + for pkg in find_ros_packages(Path("controls/sae_2025_ws/src"), ament_type="python"): + print(pkg) + + print("\nCMAKE:") + for pkg in find_ros_packages(Path("controls/sae_2025_ws/src"), ament_type="cmake"): + print(pkg) + + + + + diff --git a/docs/gen_pkg_pages.py b/docs/gen_pkg_pages.py new file mode 100644 index 00000000..6cd7ef7f --- /dev/null +++ b/docs/gen_pkg_pages.py @@ -0,0 +1,52 @@ +"""Generate one documentation page per ROS package.""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import mkdocs_gen_files +from doc_utils import find_ros_packages, REPO_ROOT + +src = REPO_ROOT / "controls" / "sae_2025_ws" / "src" + +nav = mkdocs_gen_files.Nav() + +for pkg_dir in sorted(find_ros_packages(src, ament_type="all"), key=lambda p: p.name): + pkg_name = pkg_dir.name + readme = pkg_dir / "README.md" + docs_dir = pkg_dir / "docs" + + supplementary = sorted( + f for f in docs_dir.iterdir() if f.is_file() and f.suffix == ".md" + ) if docs_dir.is_dir() else [] + + if readme.exists(): + dest = Path("packages", pkg_name, "README.md") + with mkdocs_gen_files.open(dest, "w") as fd: + fd.write(readme.read_text()) + mkdocs_gen_files.set_edit_path(dest, readme.relative_to(REPO_ROOT)) + nav[(pkg_name,)] = Path(pkg_name, "README.md").as_posix() + elif supplementary: + # first supplementary file acts as the section index if no README + first = supplementary.pop(0) + dest = Path("packages", pkg_name, "docs", first.name) + with mkdocs_gen_files.open(dest, "w") as fd: + fd.write(first.read_text()) + mkdocs_gen_files.set_edit_path(dest, first.relative_to(REPO_ROOT)) + nav[(pkg_name,)] = Path(pkg_name, "docs", first.name).as_posix() + else: + dest = Path("packages", pkg_name, "README.md") + with mkdocs_gen_files.open(dest, "w") as fd: + fd.write(f"# `{pkg_name}`\n\n*No documentation yet.*\n") + nav[(pkg_name,)] = Path(pkg_name, "README.md").as_posix() + + for doc_file in supplementary: + dest = Path("packages", pkg_name, "docs", doc_file.name) + with mkdocs_gen_files.open(dest, "w") as fd: + fd.write(doc_file.read_text()) + mkdocs_gen_files.set_edit_path(dest, doc_file.relative_to(REPO_ROOT)) + nav[(pkg_name, doc_file.stem)] = Path(pkg_name, "docs", doc_file.name).as_posix() + +with mkdocs_gen_files.open("packages/SUMMARY.md", "w") as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py index c8421b0b..905046a7 100644 --- a/docs/gen_ref_pages.py +++ b/docs/gen_ref_pages.py @@ -3,20 +3,25 @@ Referenced this guide: https://mkdocstrings.github.io/recipes/ """ +import sys from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + import mkdocs_gen_files +from doc_utils import find_ros_packages root = Path(__file__).resolve().parent.parent src = root / "controls" / "sae_2025_ws" / "src" -# ----------------------------- -# Python package roots (REAL import roots) -# ----------------------------- +# Auto-detect Python module roots. +# A root is any dir with __init__.py whose parent is a direct-child ROS package +# of src. PYTHON_PACKAGE_MODULE_ROOTS = { - src / "sim" / "sim", - src / "uav" / "uav", - src / "udp_bridge" / "udp_bridge", - src / "tools", + child + for pkg_dir in find_ros_packages(src, ament_type="python") + for child in pkg_dir.iterdir() + if child.is_dir() and (child / "__init__.py").exists() } # ----------------------------- diff --git a/properdocs.yml b/properdocs.yml index 930994c6..8faa71f8 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -3,7 +3,8 @@ repo_url: https://github.com/pennaerial/monorepo theme: name: material - logo: https://www.pennaerial.com/wp-content/uploads/2023/08/b-removebg-preview-150x150.png + logo: assets/pennair-preview-150x150.png + favicon: assets/pennair-preview-150x150.png icon: repo: fontawesome/solid/trash features: @@ -13,18 +14,21 @@ theme: nav: - Home: index.md - Getting Started: getting-started.md + - Packages: packages/ - Concepts: concepts.md - API Reference: - Python: reference/ - C++: - payload: payload/annotated.md - custom_sim_bridge: custom_sim_bridge/annotated.md + - custom_gz_plugins: custom_gz_plugins/annotated.md plugins: - search - gen-files: scripts: - docs/gen_ref_pages.py + - docs/gen_pkg_pages.py - mkdocstrings: handlers: python: @@ -47,3 +51,9 @@ plugins: doxy-cfg: OUTPUT_LANGUAGE: English EXTRACT_ALL: YES + custom_gz_plugins: + src-dirs: controls/sae_2025_ws/src/custom_gz_plugins/src + full-doc: true + doxy-cfg: + OUTPUT_LANGUAGE: English + EXTRACT_ALL: YES From 4e0e7b95b5b6878ab7b060ce2f937bb9acce5070 Mon Sep 17 00:00:00 2001 From: yuzhiliu8 Date: Thu, 30 Apr 2026 23:50:22 -0400 Subject: [PATCH 5/9] wrote homepage, updated some readmes --- .../sae_2025_ws/src/custom_gz_msgs/README.md | 2 +- .../src/custom_gz_plugins/README.md | 2 +- .../src/custom_sim_bridge/README.md | 2 +- controls/sae_2025_ws/src/sim/README.md | 2 + controls/sae_2025_ws/src/tools/README.md | 2 +- docs/docs-setup.md | 64 +++++++++++++++++++ docs/gen_pkg_pages.py | 39 ++++++++++- docs/index.md | 11 ++++ properdocs.yml | 3 +- 9 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 docs/docs-setup.md diff --git a/controls/sae_2025_ws/src/custom_gz_msgs/README.md b/controls/sae_2025_ws/src/custom_gz_msgs/README.md index fc20eb5f..469aa3ba 100644 --- a/controls/sae_2025_ws/src/custom_gz_msgs/README.md +++ b/controls/sae_2025_ws/src/custom_gz_msgs/README.md @@ -1,6 +1,6 @@ # custom_gz_msgs -Custom Gazebo Harmonic protobuf message definitions for the SAE 2025 simulation environment. +Custom Gazebo Harmonic protobuf message definitions for the simulation environment. For general information on creating Gazebo messages, see the [official guide](https://gazebosim.org/api/msgs/10/index.html). diff --git a/controls/sae_2025_ws/src/custom_gz_plugins/README.md b/controls/sae_2025_ws/src/custom_gz_plugins/README.md index 40782cf5..746eeda0 100644 --- a/controls/sae_2025_ws/src/custom_gz_plugins/README.md +++ b/controls/sae_2025_ws/src/custom_gz_plugins/README.md @@ -1,6 +1,6 @@ # custom_gz_plugins -Custom Gazebo Harmonic system plugins for the SAE 2025 simulation environment. +Custom Gazebo Harmonic system plugins for simulation. For general information on creating Gazebo system plugins, see the [official guide](https://gazebosim.org/api/sim/8/createsystemplugins.html). diff --git a/controls/sae_2025_ws/src/custom_sim_bridge/README.md b/controls/sae_2025_ws/src/custom_sim_bridge/README.md index bcdc5375..243ea9fb 100644 --- a/controls/sae_2025_ws/src/custom_sim_bridge/README.md +++ b/controls/sae_2025_ws/src/custom_sim_bridge/README.md @@ -1,6 +1,6 @@ # custom_sim_bridge -Pluginlib-based ROS 2 bridge plugins that translate between ROS 2 services and Gazebo Harmonic transport for the SAE 2025 simulation environment. +Pluginlib-based ROS 2 bridge plugins that translate between ROS 2 services and Gazebo Harmonic transport for simulation. For general information on pluginlib, see the [official guide](https://docs.ros.org/en/rolling/Tutorials/Beginner-Client-Libraries/Pluginlib.html). diff --git a/controls/sae_2025_ws/src/sim/README.md b/controls/sae_2025_ws/src/sim/README.md index 63f124d0..e9708d2a 100644 --- a/controls/sae_2025_ws/src/sim/README.md +++ b/controls/sae_2025_ws/src/sim/README.md @@ -1,5 +1,7 @@ # `sim` Package +Gazebo Harmonic simulation backend — world generation, multi-vehicle spawning, hoop-course scoring, and stage configuration. + ## Extra Dependency ```bash sudo apt install ros-humble-tf-transformations diff --git a/controls/sae_2025_ws/src/tools/README.md b/controls/sae_2025_ws/src/tools/README.md index bcf283c4..a77fc31e 100644 --- a/controls/sae_2025_ws/src/tools/README.md +++ b/controls/sae_2025_ws/src/tools/README.md @@ -1,6 +1,6 @@ # tools -Developer tools for the SAE 2025 workspace. +Various developer tools --- diff --git a/docs/docs-setup.md b/docs/docs-setup.md new file mode 100644 index 00000000..94185f03 --- /dev/null +++ b/docs/docs-setup.md @@ -0,0 +1,64 @@ +# Documentation Setup + +Docs are built with [MkDocs](https://www.mkdocs.org/) via the `properdocs` wrapper. The config is `properdocs.yml` at the repo root. + +## Serving locally + +```bash +properdocs serve +``` + +## Plugins + +| Plugin | Role | +|---|---| +| `mkdocs-gen-files` | Runs Python scripts at build time to generate virtual doc pages | +| `mkdocstrings` | Renders Python API reference from docstrings | +| `mkdoxy` | Renders C++ API reference via Doxygen | +| `literate-nav` | Reads `SUMMARY.md` files to build nav trees for generated sections | +| `section-index` | Makes section headers in the nav clickable (links to their `index.md` / `README.md`) | + +## Auto-generated sections + +Two scripts in `docs/` run at build time via `mkdocs-gen-files`: + +### `docs/gen_ref_pages.py` — Python API reference + +Finds every ament_python ROS package under `controls/sae_2025_ws/src/`, then for each one finds direct child directories with an `__init__.py` (the importable module roots). Walks each module and generates a `reference//.md` page with a `:::` mkdocstrings directive. Output nav is written to `reference/SUMMARY.md`. + +### `docs/gen_pkg_pages.py` — Package docs + +Finds every ROS package (all build types, submodules excluded). For each: + +- Copies `README.md` from the package root → `packages//README.md` (section index) +- Copies `docs/*.md` → `packages//docs/*.md` (supplementary pages, preserving the `docs/` prefix so relative links in the README stay valid) +- Generates a stub if neither exists + +Also generates `packages/index.md` as a landing page with a one-line description pulled from the first paragraph of each package's README. Output nav is written to `packages/SUMMARY.md`. + +### `docs/doc_utils.py` — Shared helpers + +`find_ros_packages(src, ament_type, exclude_submodules)` — detects ROS packages by finding `package.xml` files one level deep under `src/`. Git submodules are excluded by parsing `.gitmodules`. The `ament_type` parameter filters to `"python"`, `"cmake"`, or `"all"`. + +## Adding docs to a package + +1. Add a `README.md` at the package root for the overview (this becomes the section landing page). +2. Add supplementary pages to `/docs/*.md` — link to them from the README as `docs/quickstart.md` etc. +3. No config changes needed — packages are auto-detected. + +## Adding C++ API docs (Doxygen) + +Add a new project under `mkdoxy.projects` in `properdocs.yml`: + +```yaml +- mkdoxy: + projects: + my_package: + src-dirs: controls/sae_2025_ws/src/my_package/include controls/sae_2025_ws/src/my_package/src + full-doc: true + doxy-cfg: + OUTPUT_LANGUAGE: English + EXTRACT_ALL: YES +``` + +Then add a nav entry under `API Reference > C++`. diff --git a/docs/gen_pkg_pages.py b/docs/gen_pkg_pages.py index 6cd7ef7f..10e3c2c2 100644 --- a/docs/gen_pkg_pages.py +++ b/docs/gen_pkg_pages.py @@ -10,9 +10,45 @@ src = REPO_ROOT / "controls" / "sae_2025_ws" / "src" + +def _first_paragraph(readme: Path) -> str: + """Return the first non-heading paragraph from a README, or empty string.""" + lines = readme.read_text().splitlines() + paragraph: list[str] = [] + in_paragraph = False + for line in lines: + if line.startswith("#"): + if in_paragraph: + break + continue + if line.strip() == "": + if in_paragraph: + break + continue + paragraph.append(line.strip()) + in_paragraph = True + return " ".join(paragraph) + + nav = mkdocs_gen_files.Nav() -for pkg_dir in sorted(find_ros_packages(src, ament_type="all"), key=lambda p: p.name): +# --- landing page --- +packages = sorted(find_ros_packages(src, ament_type="all"), key=lambda p: p.name) + +with mkdocs_gen_files.open("packages/index.md", "w") as fd: + fd.write("# Packages\n\n") + fd.write("| Package | Description |\n") + fd.write("|---|---|\n") + for pkg_dir in packages: + pkg_name = pkg_dir.name + readme = pkg_dir / "README.md" + desc = _first_paragraph(readme) if readme.exists() else "" + fd.write(f"| [`{pkg_name}`]({pkg_name}/README.md) | {desc} |\n") + +nav[()] = "index.md" + +# --- per-package pages --- +for pkg_dir in packages: pkg_name = pkg_dir.name readme = pkg_dir / "README.md" docs_dir = pkg_dir / "docs" @@ -28,7 +64,6 @@ mkdocs_gen_files.set_edit_path(dest, readme.relative_to(REPO_ROOT)) nav[(pkg_name,)] = Path(pkg_name, "README.md").as_posix() elif supplementary: - # first supplementary file acts as the section index if no README first = supplementary.pop(0) dest = Path("packages", pkg_name, "docs", first.name) with mkdocs_gen_files.open(dest, "w") as fd: diff --git a/docs/index.md b/docs/index.md index e69de29b..5813c4ad 100644 --- a/docs/index.md +++ b/docs/index.md @@ -0,0 +1,11 @@ +# PennAir Monorepo + +ROS 2 / Gazebo Harmonic autonomy stack for Penn Aerial Robotics multi-UAV missions — mission runtime, hardware control, simulation, and ground-station tooling. + +## Quick Links + +- [Getting Started](getting-started.md) +- [Packages](packages/) +- [UAV Quickstart](packages/uav/docs/quickstart.md) +- [API Reference — Python](reference/) +- [API Reference — C++](payload/annotated.md) diff --git a/properdocs.yml b/properdocs.yml index 8faa71f8..e36dbfc8 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -14,14 +14,15 @@ theme: nav: - Home: index.md - Getting Started: getting-started.md - - Packages: packages/ - Concepts: concepts.md + - Packages: packages/ - API Reference: - Python: reference/ - C++: - payload: payload/annotated.md - custom_sim_bridge: custom_sim_bridge/annotated.md - custom_gz_plugins: custom_gz_plugins/annotated.md + - Docs Setup: docs-setup.md plugins: - search From 2a13d233a7a4bbc592c643ca0771691480fc29da Mon Sep 17 00:00:00 2001 From: yuzhiliu8 Date: Thu, 30 Apr 2026 23:51:23 -0400 Subject: [PATCH 6/9] ruff --- .../payload/PayloadWaitForDriveOutMode.py | 24 ++++++++++++++----- docs/doc_utils.py | 5 ---- docs/gen_pkg_pages.py | 12 ++++++---- docs/gen_ref_pages.py | 10 +------- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/controls/sae_2025_ws/src/uav/uav/modes/payload/PayloadWaitForDriveOutMode.py b/controls/sae_2025_ws/src/uav/uav/modes/payload/PayloadWaitForDriveOutMode.py index 53f7ef12..70d1bc09 100644 --- a/controls/sae_2025_ws/src/uav/uav/modes/payload/PayloadWaitForDriveOutMode.py +++ b/controls/sae_2025_ws/src/uav/uav/modes/payload/PayloadWaitForDriveOutMode.py @@ -210,17 +210,23 @@ def _publish_debug( now = self._now() if orange_found: - elapsed = (now - self._clear_since) if self._clear_since is not None else 0.0 + elapsed = ( + (now - self._clear_since) if self._clear_since is not None else 0.0 + ) state_label = f"DLZ {elapsed:.1f}/{self.dlz_color_wait_seconds:.1f}s" label_color = (0, 255, 0) elif is_still: - state_label = f"OBSTRUCTED {self._obstruction_count}/{self.obstruction_frames}" + state_label = ( + f"OBSTRUCTED {self._obstruction_count}/{self.obstruction_frames}" + ) label_color = (0, 140, 255) else: state_label = "IN-FLIGHT" label_color = (128, 128, 128) - cv2.putText(vis, state_label, (8, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, label_color, 2) + cv2.putText( + vis, state_label, (8, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, label_color, 2 + ) cv2.putText( vis, f"dlz={coverage:.2f} thresh={self.dlz_color_pixel_threshold:.2f}", @@ -279,9 +285,13 @@ def on_update(self, time_delta: float) -> None: self._obstruction_count = 0 if self._clear_since is None: self._clear_since = now - self.log(f"DLZ in view — starting {self.dlz_color_wait_seconds}s timer") + self.log( + f"DLZ in view — starting {self.dlz_color_wait_seconds}s timer" + ) elapsed = now - self._clear_since - self.log(f"DLZ confirmed for {elapsed:.1f}/{self.dlz_color_wait_seconds:.1f}s") + self.log( + f"DLZ confirmed for {elapsed:.1f}/{self.dlz_color_wait_seconds:.1f}s" + ) self._publish_debug(bgr, True, coverage, orange_mask, None) if elapsed >= self.dlz_color_wait_seconds: self.log("landing confirmed — moving to reverse") @@ -289,7 +299,9 @@ def on_update(self, time_delta: float) -> None: else: # Orange absent — reset DLZ timer, reset optical flow so readings are fresh if self._clear_since is not None: - self.log("orange lost — resetting DLZ timer, resetting optical flow") + self.log( + "orange lost — resetting DLZ timer, resetting optical flow" + ) self._prev_gray = None self._flow_deltas.clear() self._clear_since = None diff --git a/docs/doc_utils.py b/docs/doc_utils.py index d5097af2..2c373fff 100644 --- a/docs/doc_utils.py +++ b/docs/doc_utils.py @@ -74,8 +74,3 @@ def find_ros_packages( print("\nCMAKE:") for pkg in find_ros_packages(Path("controls/sae_2025_ws/src"), ament_type="cmake"): print(pkg) - - - - - diff --git a/docs/gen_pkg_pages.py b/docs/gen_pkg_pages.py index 10e3c2c2..38fb46df 100644 --- a/docs/gen_pkg_pages.py +++ b/docs/gen_pkg_pages.py @@ -53,9 +53,11 @@ def _first_paragraph(readme: Path) -> str: readme = pkg_dir / "README.md" docs_dir = pkg_dir / "docs" - supplementary = sorted( - f for f in docs_dir.iterdir() if f.is_file() and f.suffix == ".md" - ) if docs_dir.is_dir() else [] + supplementary = ( + sorted(f for f in docs_dir.iterdir() if f.is_file() and f.suffix == ".md") + if docs_dir.is_dir() + else [] + ) if readme.exists(): dest = Path("packages", pkg_name, "README.md") @@ -81,7 +83,9 @@ def _first_paragraph(readme: Path) -> str: with mkdocs_gen_files.open(dest, "w") as fd: fd.write(doc_file.read_text()) mkdocs_gen_files.set_edit_path(dest, doc_file.relative_to(REPO_ROOT)) - nav[(pkg_name, doc_file.stem)] = Path(pkg_name, "docs", doc_file.name).as_posix() + nav[(pkg_name, doc_file.stem)] = Path( + pkg_name, "docs", doc_file.name + ).as_posix() with mkdocs_gen_files.open("packages/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py index 905046a7..6313dc9b 100644 --- a/docs/gen_ref_pages.py +++ b/docs/gen_ref_pages.py @@ -27,14 +27,7 @@ # ----------------------------- # Skip ROS/non-Python API dirs # ----------------------------- -PY_PKG_SKIP_DIRS = { - "launch", - "config", - "urdf", - "meshes", - "worlds", - "models" -} +PY_PKG_SKIP_DIRS = {"launch", "config", "urdf", "meshes", "worlds", "models"} SKIP_FILES = { "setup", @@ -43,7 +36,6 @@ nav = mkdocs_gen_files.Nav() for path in sorted(src.rglob("*.py")): - # restict to only listed python packages valid_module = False for pkg_module in PYTHON_PACKAGE_MODULE_ROOTS: From 95ad74b6ad820ca3d16e3f7b0917c8f1d7841e6f Mon Sep 17 00:00:00 2001 From: yuzhiliu8 Date: Thu, 30 Apr 2026 23:54:49 -0400 Subject: [PATCH 7/9] doxygen requirement, update dependencies q --- .github/workflows/docs.yml | 2 ++ docs/docs-setup.md | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0fe620b2..af33c56e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,6 +13,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.10" + - run: sudo apt-get install -y doxygen - run: | pip install \ properdocs \ @@ -33,6 +34,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.10" + - run: sudo apt-get install -y doxygen - run: | pip install \ properdocs \ diff --git a/docs/docs-setup.md b/docs/docs-setup.md index 94185f03..b52a4d59 100644 --- a/docs/docs-setup.md +++ b/docs/docs-setup.md @@ -4,6 +4,15 @@ Docs are built with [MkDocs](https://www.mkdocs.org/) via the `properdocs` wrapp ## Serving locally +Install dependencies: + +```bash +sudo apt-get install -y doxygen +pip install properdocs mkdocs-material "mkdocstrings[python]" mkdocs-gen-files mkdocs-literate-nav mkdocs-section-index mkdoxy +``` + +Then serve: + ```bash properdocs serve ``` From a9f73119682c87ce5a26e13cc7905ebca9af7765 Mon Sep 17 00:00:00 2001 From: yuzhiliu8 Date: Fri, 1 May 2026 00:24:05 -0400 Subject: [PATCH 8/9] fixed tests hopefully --- .../sae_2025_ws/src/integration/backend/mission_compat.py | 2 +- controls/sae_2025_ws/src/uav/test/test_auto_launch.py | 1 + controls/sae_2025_ws/src/uav/test/test_launch_helpers.py | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/controls/sae_2025_ws/src/integration/backend/mission_compat.py b/controls/sae_2025_ws/src/integration/backend/mission_compat.py index e1b46712..28bbdb80 100644 --- a/controls/sae_2025_ws/src/integration/backend/mission_compat.py +++ b/controls/sae_2025_ws/src/integration/backend/mission_compat.py @@ -245,7 +245,7 @@ def load_mission_spec_compat(path: str | Path | dict[str, Any]) -> MissionSpecCo unknown_targets = sorted( next_mode for next_mode in mode_spec.transitions.values() - if next_mode not in modes + if next_mode not in modes and next_mode != "terminate" ) if unknown_targets: raise ValueError( diff --git a/controls/sae_2025_ws/src/uav/test/test_auto_launch.py b/controls/sae_2025_ws/src/uav/test/test_auto_launch.py index e22cbefa..fecdfec8 100644 --- a/controls/sae_2025_ws/src/uav/test/test_auto_launch.py +++ b/controls/sae_2025_ws/src/uav/test/test_auto_launch.py @@ -700,6 +700,7 @@ def test_payload_bootstrap_forwards_auto_launch(monkeypatch, auto_launch): "vehicle_name": "payload_0", "peer_heartbeat_hz": 12.0, "peer_stale_timeout_s": 0.75, + "vision_debug": False, } bootstrap.get_parameter = lambda name: _FakeParameter(parameters[name]) diff --git a/controls/sae_2025_ws/src/uav/test/test_launch_helpers.py b/controls/sae_2025_ws/src/uav/test/test_launch_helpers.py index 3323a15f..6384fa50 100644 --- a/controls/sae_2025_ws/src/uav/test/test_launch_helpers.py +++ b/controls/sae_2025_ws/src/uav/test/test_launch_helpers.py @@ -664,6 +664,7 @@ def test_build_runtime_parameters_for_uav(stack_launch_module): vehicle_name="uav_3", auto_launch=True, debug=1, + vision_debug=False, servo_only=0, vehicle_class_name="MULTICOPTER", camera_mount_offsets=[0.1, 0.2, 0.3], @@ -687,6 +688,7 @@ def test_build_runtime_parameters_for_payload(stack_launch_module): vehicle_name="payload_0", auto_launch=False, debug=True, + vision_debug=False, servo_only=True, vehicle_class_name=None, camera_mount_offsets=[], @@ -696,6 +698,7 @@ def test_build_runtime_parameters_for_payload(stack_launch_module): "mode_map": "/tmp/payload.yaml", "auto_launch": False, "vehicle_name": "payload_0", + "vision_debug": False, } @@ -707,6 +710,7 @@ def test_build_runtime_parameters_rejects_missing_vehicle_class(stack_launch_mod vehicle_name="uav", auto_launch=True, debug=False, + vision_debug=False, servo_only=False, vehicle_class_name=None, camera_mount_offsets=[0.0, 0.0, 0.0], @@ -723,6 +727,7 @@ def test_build_runtime_parameters_rejects_bad_camera_offsets(stack_launch_module vehicle_name="uav", auto_launch=True, debug=False, + vision_debug=False, servo_only=False, vehicle_class_name="MULTICOPTER", camera_mount_offsets=[0.0, 0.0], From 64fecf7a881ab575bdcd3779d306b2d5f3f2a4ea Mon Sep 17 00:00:00 2001 From: yuzhiliu8 Date: Fri, 1 May 2026 00:36:08 -0400 Subject: [PATCH 9/9] added udp bridge --- controls/sae_2025_ws/scripts/ci/validate_ros_workspace.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controls/sae_2025_ws/scripts/ci/validate_ros_workspace.sh b/controls/sae_2025_ws/scripts/ci/validate_ros_workspace.sh index 716874ca..a1e7bafa 100755 --- a/controls/sae_2025_ws/scripts/ci/validate_ros_workspace.sh +++ b/controls/sae_2025_ws/scripts/ci/validate_ros_workspace.sh @@ -31,7 +31,7 @@ rosdep install -r -i -y --rosdistro "$ROS_DISTRO" \ ci_log "Building shared hardware dependencies" colcon build \ - --packages-select payload_interfaces px4_msgs uav_interfaces actuator_msgs + --packages-select payload_interfaces px4_msgs uav_interfaces actuator_msgs udp_bridge ci_log "Building hardware payload package" ci_source_workspace "$WORKSPACE_ROOT"