diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..af33c56e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,47 @@ +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: sudo apt-get install -y doxygen + - 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: sudo apt-get install -y doxygen + - 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 4a611b08..93b88332 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ crash course/data/MNIST/raw/* node_modules *venv +.claude +site/ 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" 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/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/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/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/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], 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/assets/pennair-preview-150x150.png b/docs/assets/pennair-preview-150x150.png new file mode 100644 index 00000000..56c1f9d5 Binary files /dev/null and b/docs/assets/pennair-preview-150x150.png differ 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/doc_utils.py b/docs/doc_utils.py new file mode 100644 index 00000000..2c373fff --- /dev/null +++ b/docs/doc_utils.py @@ -0,0 +1,76 @@ +"""Shared utilities for mkdocs-gen-files scripts.""" + +from pathlib import Path +import xml.etree.ElementTree as ET + +REPO_ROOT = Path(__file__).resolve().parent.parent +GITMODULES = REPO_ROOT / ".gitmodules" + + +def submodule_dirs() -> 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/docs-setup.md b/docs/docs-setup.md new file mode 100644 index 00000000..b52a4d59 --- /dev/null +++ b/docs/docs-setup.md @@ -0,0 +1,73 @@ +# 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 + +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 +``` + +## 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 new file mode 100644 index 00000000..38fb46df --- /dev/null +++ b/docs/gen_pkg_pages.py @@ -0,0 +1,91 @@ +"""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" + + +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() + +# --- 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" + + 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.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 new file mode 100644 index 00000000..6313dc9b --- /dev/null +++ b/docs/gen_ref_pages.py @@ -0,0 +1,84 @@ +""" +Generate the code reference pages. +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" + +# 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 = { + 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() +} + +# ----------------------------- +# 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")): + # restict to only listed python packages + 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 + + # 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 + + 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..5813c4ad --- /dev/null +++ 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 new file mode 100644 index 00000000..e36dbfc8 --- /dev/null +++ b/properdocs.yml @@ -0,0 +1,60 @@ +site_name: PennAir Monorepo +repo_url: https://github.com/pennaerial/monorepo + +theme: + name: material + logo: assets/pennair-preview-150x150.png + favicon: assets/pennair-preview-150x150.png + icon: + repo: fontawesome/solid/trash + features: + - navigation.tabs + + +nav: + - Home: index.md + - Getting Started: getting-started.md + - 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 + - gen-files: + scripts: + - docs/gen_ref_pages.py + - docs/gen_pkg_pages.py + - mkdocstrings: + handlers: + python: + paths: + - controls/sae_2025_ws/src + - 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 + 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