diff --git a/.github/assets/header.png b/.github/assets/header.png new file mode 100644 index 0000000..1214d61 Binary files /dev/null and b/.github/assets/header.png differ diff --git a/.github/assets/icons/ai-neural-net.png b/.github/assets/icons/ai-neural-net.png new file mode 100644 index 0000000..798af85 Binary files /dev/null and b/.github/assets/icons/ai-neural-net.png differ diff --git a/.github/assets/icons/code.png b/.github/assets/icons/code.png new file mode 100644 index 0000000..c26044c Binary files /dev/null and b/.github/assets/icons/code.png differ diff --git a/.github/assets/icons/embedded-chip.png b/.github/assets/icons/embedded-chip.png new file mode 100644 index 0000000..b53c0b0 Binary files /dev/null and b/.github/assets/icons/embedded-chip.png differ diff --git a/.github/assets/icons/mechanical-gear.png b/.github/assets/icons/mechanical-gear.png new file mode 100644 index 0000000..11a636d Binary files /dev/null and b/.github/assets/icons/mechanical-gear.png differ diff --git a/.github/assets/icons/milestone-flag.png b/.github/assets/icons/milestone-flag.png new file mode 100644 index 0000000..59eefab Binary files /dev/null and b/.github/assets/icons/milestone-flag.png differ diff --git a/.github/assets/icons/publication.png b/.github/assets/icons/publication.png new file mode 100644 index 0000000..a66a79b Binary files /dev/null and b/.github/assets/icons/publication.png differ diff --git a/.github/assets/icons/results-chart.png b/.github/assets/icons/results-chart.png new file mode 100644 index 0000000..bf338e5 Binary files /dev/null and b/.github/assets/icons/results-chart.png differ diff --git a/.github/assets/icons/ros-graph.png b/.github/assets/icons/ros-graph.png new file mode 100644 index 0000000..c668114 Binary files /dev/null and b/.github/assets/icons/ros-graph.png differ diff --git a/.github/assets/icons/workshop-idea.png b/.github/assets/icons/workshop-idea.png new file mode 100644 index 0000000..e715c00 Binary files /dev/null and b/.github/assets/icons/workshop-idea.png differ diff --git a/.github/assets/social-preview.png b/.github/assets/social-preview.png new file mode 100644 index 0000000..e114202 Binary files /dev/null and b/.github/assets/social-preview.png differ diff --git a/.github/workflows/configs.yml b/.github/workflows/configs.yml new file mode 100644 index 0000000..7160719 --- /dev/null +++ b/.github/workflows/configs.yml @@ -0,0 +1,53 @@ +name: configs + +# Proves every config in configs/ works: each formatter and linter must accept the samples in +# examples/ and reject the deliberately bad samples in examples//bad/. +on: + push: + branches: [master] + paths: + - "configs/**" + - "examples/**" + - "templates/**" + - "scripts/check-configs.sh" + - ".github/workflows/configs.yml" + pull_request: + paths: + - "configs/**" + - "examples/**" + - "templates/**" + - "scripts/check-configs.sh" + - ".github/workflows/configs.yml" + schedule: + - cron: "0 9 1 * *" # First of each month: catch breakage from new base images + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + name: ${{ matrix.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: EditorConfig and Markdown + languages: editorconfig markdown + # The Python job also runs the ROS 2 linters, which need the C++ job's image, + # so the two share a job. + - name: C, C++ and Python (ROS 2 Jazzy) + languages: c-cpp python + - name: JavaScript and TypeScript + languages: javascript-typescript + - name: Swift + languages: swift + - name: Kotlin + languages: kotlin + - name: C# and Unity + languages: csharp-unity + steps: + - uses: actions/checkout@v7 + - name: Run the checks + run: scripts/check-configs.sh ${{ matrix.languages }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..7f703e7 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,37 @@ +name: docs + +on: + push: + branches: [main, master] + pull_request: + schedule: + - cron: "0 9 * * 1" # Mondays: catch links that rot over time + workflow_dispatch: + +permissions: + contents: read + +jobs: + markdown: + name: Markdown lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: DavidAnson/markdownlint-cli2-action@v24 + with: + globs: "**/*.md" + + links: + name: Link check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + # Pull requests check only links to files in this repository (--offline), because links to other + # ATR-Lab repositories may point at pages that are merged later. Pushes and the weekly run check everything. + - uses: lycheeverse/lychee-action@v2 + with: + args: >- + --no-progress --max-concurrency 8 --accept 200,206,429 + ${{ github.event_name == 'pull_request' && '--offline' || '' }} + "**/*.md" + fail: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0631685 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# OS and editors +.DS_Store +Thumbs.db +*.swp +*~ +.idea/ +.vscode/ + +# Anything a local run of the configs or the examples might create +node_modules/ +.venv/ +__pycache__/ +*.pyc +.mypy_cache/ +.ruff_cache/ +.pytest_cache/ +build/ +install/ +log/ +bin/ +obj/ +.build/ +.gradle/ +dist/ diff --git a/.lycheeignore b/.lycheeignore new file mode 100644 index 0000000..4c41e9e --- /dev/null +++ b/.lycheeignore @@ -0,0 +1,16 @@ +# Sites that block automated link checkers (403/999 to bots). Check these by hand when you edit them. +^https://dl\.acm\.org/ +^https://doi\.org/10\.1145/ +^https://ieeexplore\.ieee\.org/ +^https://(www\.)?linkedin\.com/ +^https://(www\.)?researchgate\.net/ +^https://(www\.)?instagram\.com/ +^https://(www\.)?x\.com/ +^https://(www\.)?twitter\.com/ +^https://(www\.)?reddit\.com/ +^https://(www\.)?raspberrypi\.com/ +^https://forums\.raspberrypi\.com/ +^https://support\.unitree\.com/ +# Repository-specific +^https://journals\.ieeeauthorcenter\.ieee\.org/ +^https://raw\.githubusercontent\.com/ATR-Lab/dev-guidelines/ diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..5bc8cbb --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,20 @@ +// markdownlint rules for ATR Lab docs (https://github.com/DavidAnson/markdownlint) +{ + "default": true, + // Long lines are fine in prose; editors wrap them. + "MD013": false, + // READMEs open with an HTML header image instead of a "# Heading". + "MD041": false, + // HTML is allowed only for layout GitHub supports: centered headers, card grids, collapsible sections. + "MD033": { + "allowed_elements": ["p", "img", "a", "b", "br", "sub", "sup", "table", "tr", "td", "th", "thead", "tbody", "picture", "source", "details", "summary", "kbd", "div"] + }, + // Repeated headings are fine in different sections (e.g. "Safety" under each robot). + "MD024": { "siblings_only": true }, + // Numbered lists may use 1. 2. 3. or all 1. + "MD029": false, + // Table pipe spacing is cosmetic; "|---|" and "| --- |" are both fine. + "MD060": false, + // Emphasis used as a label ("**Read this when** ...") is not a heading. + "MD036": false +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3847f3e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,130 @@ +# AGENTS.md: dev-guidelines + +This repo is the Advanced Telerobotics Research Lab's development standard. It covers the Git and GitHub workflow, documentation and AI-agent rules, one style guide per language (C and C++, Python, JavaScript and TypeScript, Swift, Kotlin, C# and Unity) and conventions for ROS 2 packages and hardware. It also holds copy-ready formatter and linter configs that `scripts/check-configs.sh` tests against sample code. Lab members read it when they start or review code; coding agents read it before working in any lab repo. It is **not** a ROS 2 tutorial (that is `all-things-ros2`), not lab onboarding or safety rules (`getting-started-atr-lab`) and not a description of the lab (`about`). + +## Repo map + +| Path | What it is | +|---|---| +| `README.md` | Landing page: start-here cards, language matrix, quick start, repo map | +| `AGENTS.md` | This file: the agent's index and rules | +| `CLAUDE.md` | One line, `@AGENTS.md`, so Claude Code reads this file | +| `LICENSE` | MIT License, 2017-2026 | +| `.gitignore` | OS, editor, build and cache files kept out of Git | +| `.markdownlint.jsonc` | markdownlint rules for this repo's Markdown (shared across the lab's docs repos) | +| `.lycheeignore` | URL patterns the link checker skips: sites that block bots and template placeholders | +| `.github/workflows/docs.yml` | CI: markdownlint and lychee link check | +| `.github/workflows/configs.yml` | CI: runs `scripts/check-configs.sh`, one job per language | +| `.github/assets/header.png`, `social-preview.png` | README banner and GitHub social preview (do not regenerate) | +| `.github/assets/icons/*.png` | Navy icon badges used by the README card grid | +| `docs/README.md` | Index of all guides | +| `docs/git-and-github.md` | Branches, seven commit rules (credited to Chris Beams), Conventional Commits, PRs, reviews, issues, SemVer releases, `.gitignore`, Git LFS, protected branches, secrets | +| `docs/documentation.md` | README standard, docstrings per language, AGENTS.md and CLAUDE.md convention, diagrams, changelogs | +| `docs/ai-assisted-development.md` | Rules for Claude Code, Codex and other agents in lab repos | +| `docs/c-cpp.md` | ROS 2 C++ style, clang-format, clang-tidy, ament linters, CI | +| `docs/python.md` | PEP 8, Google docstrings, Ruff, mypy, pyright, uv, ROS 2 interplay | +| `docs/javascript-typescript.md` | ESLint flat config, typescript-eslint, Prettier, strict tsconfig | +| `docs/swift.md` | Swift API Design Guidelines, swift-format, SwiftLint | +| `docs/kotlin.md` | Kotlin conventions, Android style guide, ktlint (`android_studio`), detekt | +| `docs/csharp-unity.md` | Microsoft C# conventions, Unity style guide, `.editorconfig`, Unity 6 analyzers, `dotnet format` | +| `docs/ros2-packages.md` | REP 144 naming, package layouts, `ament_lint_auto`, REP 103, REP 105, launch, parameters | +| `docs/hardware.md` | CAD naming and versions, BOMs, PlatformIO firmware, wiring colors and labels, 3D prints | +| `docs/other-languages.md` | Official style guides for Arduino, Java, MATLAB, Solidity and Ada | +| `configs/README.md` | What each config folder holds, copy commands, tested versions table, how checks work | +| `configs/editorconfig/.editorconfig` | Shared EditorConfig for every language | +| `configs/c-cpp/.clang-format`, `.clang-tidy` | ROS 2 clang-format (identical to `ament_clang_format`) and a focused clang-tidy check set | +| `configs/python/ruff.toml`, `pyproject-snippet.toml` | Ruff lint and format; mypy, pyright, pytest and pinned dev dependencies | +| `configs/javascript-typescript/eslint.config.mjs`, `.prettierrc.json`, `tsconfig.base.json`, `package-snippet.json` | ESLint, Prettier, TypeScript and pinned devDependencies | +| `configs/swift/.swift-format`, `.swiftlint.yml` | swift-format and SwiftLint, set up not to conflict | +| `configs/kotlin/.editorconfig`, `detekt.yml` | ktlint settings and detekt overrides | +| `configs/csharp-unity/.editorconfig` | C# formatting, naming rules, analyzer severities, Unity notes | +| `configs/markdown/.markdownlint.jsonc` | markdownlint rules for project repos | +| `examples/README.md` | What each sample shows; the `bad/` convention | +| `examples/c-cpp/teleop_velocity_limiter/` | ROS 2 C++ component package (library, node, gtest, launch, config) | +| `examples/c-cpp/firmware/` | C ring buffer for microcontroller firmware | +| `examples/python/` | `src/teleop_episodes/` module and `tests/` | +| `examples/javascript-typescript/` | `src/` modules and `tsconfig.json` | +| `examples/swift/Sources/` | SwiftUI view, `@Observable` view model, watchOS model | +| `examples/kotlin/src/main/kotlin/edu/kent/cs/atr/` | Pepper speech queue and Wear OS Compose card | +| `examples/csharp-unity/Assets/Scripts/Teleoperation/` | Unity MonoBehaviour and command struct | +| `examples/csharp-unity/check/` | Check harness only: `.csproj` and UnityEngine stand-ins (never copy into Unity) | +| `examples/*/bad/` | Deliberately bad samples that every linter must reject; never copy | +| `scripts/check-configs.sh` | Runs every formatter and linter against `examples/` in Docker; `--local` uses installed tools | +| `templates/README.template.md` | README template for lab project repos | +| `templates/AGENTS.template.md` | AGENTS.md template for lab project repos | + +## Sources of truth + +- **Lab facts** (names, director, projects, platforms, contacts): only `references/brand-foundation.md` in the lab design skill (`atr-lab-design`). This repo needs very few: the lab's full name on first reference, then "the lab"; "Department of Computer Science, Kent State University". +- **Technical facts** (tool versions, rules, commands, dates): only official sources checked on the date in each page's footer: docs.ros.org, ros.org/reps, the ament_lint and ros2_documentation repos, clang.llvm.org, docs.astral.sh, mypy.readthedocs.io, typescript-eslint.io, eslint.org, prettier.io, swift.org, the swiftlang and realm/SwiftLint repos, kotlinlang.org, developer.android.com, ktlint.github.io, detekt.dev, learn.microsoft.com, docs.unity3d.com, unity.com, docs.github.com, git-scm.com, semver.org, conventionalcommits.org, docs.platformio.org, docs.arduino.cc and each tool's GitHub releases (plus npm and PyPI metadata for version numbers). Link the source next to the fact. +- **Tested versions** live in the table in `configs/README.md` and in `scripts/check-configs.sh`; the two must match. +- **Never invent names, rooms, results, versions or dates: use `[brackets]`** and list them under Open items. If a fact cannot be verified, say so in the text or leave it out. + +## Writing rules + +- Two readers: a new student who skims and a coding agent that opens one file cold. One topic per file, stable paths, relative links inside the repo, full `https://github.com/ATR-Lab//blob//` links across repos (`master` here and in `about` and `getting-started-atr-lab`; `main` in `all-things-ros2` and `.github`). +- Voice: precise, curious, hands-on, welcoming, grounded. "We" for the lab, "you" for the reader, active voice, short paragraphs, outcome first. Define jargon on first use. +- Kent State style: sentence-case headings, no Oxford comma in a simple series, "and" not "&" in prose, one through nine spelled out (numerals with units and versions), dates like "Sept. 30, 2026", no emoji, no exclamation marks. +- Words: ROS 2 (never "ROS2" in prose), teleoperation, open-source (adjective), real-world (adjective), leader-follower. Cut hype words (see the grep in the next section). +- Never write "ATR" alone or "KSU" in running copy; never print the lab handles or rooms from the brand foundation's "never print" list. +- Every page except README.md, AGENTS.md and CLAUDE.md: `# Title`, a `> **Read this when** ...` line, two to four sentences with the one thing to remember, `## Contents` when there are five or more sections, sections in the order what, why, how, reference, `## Related`, then the footer line `Last reviewed: ... · Sources checked on that date · [Suggest a change](...)`. +- Language pages keep the same section order: style we follow, naming and layout, what the configs do, install, editor setup, adopt the config, CI, common mistakes, related. +- Commands: fenced blocks tagged with the language, one command per line, no `$` prompts, a comment when a step is not obvious and the OS or version they are for. +- GitHub alerts (`> [!NOTE]`, `> [!WARNING]`, `> [!CAUTION]`) sparingly; safety goes in WARNING or CAUTION. +- Mermaid diagrams start with the lab theme line: + + ```text + %%{init: {'theme':'base','themeVariables':{'primaryColor':'#003976','primaryTextColor':'#FFFFFF','primaryBorderColor':'#00295F','secondaryColor':'#EFAB00','secondaryTextColor':'#1B2533','tertiaryColor':'#F3F6FA','tertiaryTextColor':'#1B2533','lineColor':'#2C8ECD','textColor':'#1B2533','edgeLabelBackground':'#F3F6FA','clusterBkg':'#F3F6FA','clusterBorder':'#D6DEE8','titleColor':'#003976'}}}%% + ``` + + When edges carry labels, add `linkStyle default color:#1B2533` at the end of the diagram so the labels stay readable. Don't set a font in the theme line: a custom `fontFamily` makes node text clip. + +- Third-party tools are facts, not endorsements. +- **Repo-specific:** a config change is not done until `scripts/check-configs.sh ` passes, including the bad-sample checks. Keep each config's header comment, `configs/README.md` and the script's version variables in sync. Samples in `examples/` stay small and realistic; bad samples carry a comment naming the rule each line breaks. + +## How to check your work + +Run from the repo root: + +```bash +# 1. Configs: every formatter and linter against examples/ (needs Docker) +scripts/check-configs.sh +# 2. Brand, facts and Kent State style (exit 0 = no errors); README files are named explicitly +PYTHONDONTWRITEBYTECODE=1 ~/.claude/skills/atr-lab-design/.venv/bin/python ~/.claude/skills/atr-lab-design/scripts/brand_check.py $(git ls-files --others --cached --exclude-standard '*.md') +# 3. Markdown structure +npx --yes markdownlint-cli2 "**/*.md" +# 4. Hype words and "ROS2" in prose (review each hit; slugs, URLs and code are fine) +grep -rnE -i "cutting-edge|state-of-the-art|revolutionary|groundbreaking|world-class|innovative|novel|leverag|utiliz|seamless|robust|holistic|transformative|pivotal|delve|furthermore|moreover|ROS2" --include='*.md' . +# 5. Links (what the docs workflow runs; --offline checks only links inside the repo) +docker run --rm -v "$PWD":/input -w /input lycheeverse/lychee --offline --no-progress "**/*.md" +``` + +Brand-check warnings for intentional `[placeholders]` are expected. Templates in `templates/` are full of placeholders by design. + +## Open items + +Placeholders only the lab can fill. Agents must not fill these by guessing. + +| File | Placeholder | Who can fill it | +|---|---|---| +| `docs/git-and-github.md` | `[agreed review time, for example two working days: lab to confirm]` | Director or lab manager | +| `docs/git-and-github.md` | `[lab data storage location: lab manager to confirm]` | Lab manager | +| `docs/git-and-github.md` | `[repo admin: lab manager to confirm]` | Lab manager (GitHub org owners) | +| `docs/git-and-github.md` | `[lab contact for security incidents: lab manager to confirm]` | Lab manager | +| `docs/git-and-github.md` | `[the lab's shared-machine account policy: lab manager to confirm]` | Lab manager | +| `docs/ai-assisted-development.md` | Which lab work may use AI tools outside Kent State's approved list (Claude Code, Codex); the page limits them to public or non-sensitive code until then | Director, with Kent State Information Security | +| `docs/hardware.md` | `[cloud CAD workspace: lab manager to confirm]` | Lab manager | +| `docs/hardware.md` | `[parts ordering process: lab manager to confirm]` | Lab manager | +| `docs/hardware.md` | `[printer booking process: lab manager to confirm]` | Lab manager | + +The BOM example in `docs/hardware.md` and both files in `templates/` contain placeholders on purpose (`[supplier]`, `[Project name]` and so on); they stay. + +Decisions the lab may want to revisit: the ktlint `android_studio` style, the `_camelCase` private-field convention for C#, Swift's four-space indentation and 120-column limit and Python's single quotes. Each page explains the choice. + +## Related repos + +- [ATR-Lab/.github](https://github.com/ATR-Lab/.github): the organization's GitHub profile page; which repo answers which question. +- [about](https://github.com/ATR-Lab/about): who the lab is, its research, achievements and press kit. +- [getting-started-atr-lab](https://github.com/ATR-Lab/getting-started-atr-lab): how a new member gets started, lab rules, safety, research standards and hardware. +- [dev-guidelines](https://github.com/ATR-Lab/dev-guidelines): this repo; how lab code is written, reviewed and checked. +- [all-things-ros2](https://github.com/ATR-Lab/all-things-ros2): how to learn, install and debug ROS 2 on lab robots. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a1085dd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-2026 Advanced Telerobotics Research Lab contributors, Kent State University + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 80876ec..8ef354d 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,131 @@ -# dev-guidelines -Software and hardware development guidelines - -## Table of Contents - -- [Software](#project-structure) - - [Styleguides](#styleguides) - - [Documentation](#documentation) - - [Dependency Management](#dependency-management) - - [Version Control](#version-control) - - [GitHub](#github) - - [Software Releases](#software-releases) - - [Tools](#tools) - - [IDEs](#ides) - - [Middleware and Frameworks](#middleware-and-frameworks) -- [Hardware](#hardware) - - - -## Software - -One's profession should not be measured by the actions that are taken, but rather by the end-goal and the means of achieving such goal. - -Anyone can program, not everyone can develop scalable, robust, and maintainable software. Let's aim for that... - -You may find a list of useful books and [resources here](https://github.com/kPatch/awesome-developer-resources/blob/master/README.md#software-engineering). - -### Styleguides - -- [Arduino](https://www.arduino.cc/en/Reference/StyleGuide) -- [Ada]() -- [C](http://www.maultech.com/chrislott/resources/cstyle/indhill-cstyle.pdf) -- [C++](https://google.github.io/styleguide/cppguide.html) -- [Java](https://google.github.io/styleguide/javaguide.html) -- [JavaScript](https://github.com/feross/standard) -- [MATLAB](https://sites.google.com/site/matlabstyleguidelines/) -- [Python](https://google.github.io/styleguide/pyguide.html) -- [Solidity](http://solidity.readthedocs.io/en/develop/style-guide.html) - -### Documentation - -Self describing, terse code should be the norm. But, often times the complexity .... - -- [Beginners Guide To Docs](http://www.writethedocs.org/guide/writing/beginners-guide-to-docs/) -- [RTFM? How to write a manual worth reading](https://opensource.com/business/15/5/write-better-docs) - -### Dependency Management - -- [Gradle](https://gradle.org) - -### Version Control - -We use [Git](https://git-scm.com/) as our version control system [(VCS)](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control) and [GitHub](https://github.com/ATR-Lab) as our open-source Git hosting service. -Below are some quick rules to follow when working with a version control system, specifically Git: - -1. Make sure the build succeeds before committing. -**Rationale**: Broken code should not be committed. - -2. Create feature branches. -**Rationale**: Changes to a branch don’t affect other developers on the team. This is a good thing because a feature under development can create instability. - -3. Pull down the latest changes before beginning to code. -**Rationale**: Keep you local version up to date. This is crucial if you’re working on feature branch, don’t let your branch diverge too far from the master branch. - -4. Use meaningful and succinct git messages. -**Rationale**: “A commit message shows whether a developer is a good collaborator”. **A project’s long-term success rests (among other things) on its maintainability** and being able to review others’ commits and pull requests with ease. -This drives efficiency. - -- [A Successful Git Branching Model](http://nvie.com/posts/a-successful-git-branching-model/) - -#### Git Messages - -When [committing](http://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html) code into a Git repository, you will have to write a message describing your code changes. -It is crucial that we make these messages concise and consistent. A well-crafted commit message can tell other developers right away why your code changes matter or why they took place. - -Chris Beams has a great [post](https://chris.beams.io/posts/git-commit/) that summarizes the importance of a well-crafted commit message, along with the rationale behind it. Below are the seven rules he writes in his post, check out his post for more information. - -**The seven rules of a great Git commit message** -1. Separate subject from body with a blank line -2. Limit the subject line to 50 characters -3. Capitalize the subject line -4. Do not end the subject line with a period -5. Use the imperative mood in the subject line -6. Wrap the body at 72 characters -7. Use the body to explain what and why vs. how - -- [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/) - -#### Gitignore - -[What is .gitignore?](http://stackoverflow.com/questions/27850222/what-is-gitignore-exactly/27850270) - -- A collection of useful [.gitignore templates](https://github.com/github/gitignore) -- The [Ignoring Files chapter](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#Ignoring-Files) of the Pro Git book. -- The [Ignoring Files article](https://help.github.com/articles/ignoring-files/) on the GitHub Help site. -- The [gitignore(5)](https://git-scm.com/docs/gitignore) manual page. - -### GitHub -- [Referencing / closing issues using keywords](https://help.github.com/en/articles/closing-issues-using-keywords) - -### Software Releases -- [GitHub: Creating Releases](https://help.github.com/en/articles/creating-releases) -- [Drupal: Release Types](https://www.drupal.org/node/467020) -- [Drupal: Release - rc, alpha, beta, dev](https://drupal.stackexchange.com/questions/99612/what-does-rc-stand-for-when-to-use-alpha-beta-and-dev-instead) -- [Drupal: Release Naming Conventions](https://www.drupal.org/node/1015226) -- [StackOverflow: Release Title is Same as Release Version](https://softwareengineering.stackexchange.com/questions/345006/why-popular-repositories-use-release-version-as-a-release-title-in-github) - -### Tools - -Know your tools, from debuggers, to profilers, to IDEs. - -### IDEs - -#### IntelliJ - -#### Eclipse - -### Middleware and Frameworks - -#### ROS -- Check what packages are available before development. http://www.ros.org/browse/list.php -- Packages vs Nodes - - https://answers.ros.org/question/9133/packages-vs-nodes/ - - https://answers.ros.org/question/11835/when-should-i-split-my-code-into-multiple-packages-and-whats-a-good-way-to-split-it/ -- Standard Units of Measure and Coordinate Conventions - - http://www.ros.org/reps/rep-0103.html - -## Hardware - -### Tools +

+ Dev guidelines: Style guides, lint configs and Git practices. Advanced Telerobotics Research Lab, Kent State University. +

+ +

+ Style guides, copy-ready lint configs and Git practices for every code repo of the Advanced Telerobotics Research Lab, written for lab members and the coding agents that work with them.
+ Advanced Telerobotics Research Lab · Department of Computer Science · Kent State University +

+ +

+ License: MIT + Advanced Telerobotics Research Lab, Kent State University + Last reviewed Sept. 2026 +

+ +## Start here + + + + + + + + + + + + + + + + + +
+
+ Language guides
+ C and C++, Python, TypeScript, Swift, Kotlin, C# and Unity: style, tools and CI. +
+
+ Copy-ready configs
+ Formatter and linter files for each language, with the versions we tested. +
+
+ Git and GitHub
+ Branches, commit messages, pull requests, releases, Git LFS and secrets. +
+
+ ROS 2 packages
+ Naming, layout, ament linters, units, frames, launch files and parameters. +
+
+ Hardware and firmware
+ CAD versions, BOMs, PlatformIO firmware, wiring colors and 3D prints. +
+
+ AI-assisted development
+ Rules for Claude Code, Codex and other coding agents in lab repos. +
+
+ Documentation
+ READMEs, docstrings, AGENTS.md, diagrams and changelogs. +
+
+ Templates
+ README and AGENTS.md templates for new lab project repos. +
+
+ Tested examples
+ Sample code that passes every config, and bad samples that must fail. +
+ +## Language matrix + +The six languages the lab uses most. Each row links to its guide, which has the exact check commands; the config folder holds files you can copy as they are. + +| Language | Style guide we follow | Formatter | Linter | Config folder | +|---|---|---|---|---| +| [C and C++](docs/c-cpp.md) | [ROS 2 code style](https://docs.ros.org/en/jazzy/The-ROS2-Project/Contributing/Code-Style-Language-Versions.html) (Google C++ with ROS 2 changes) | clang-format | clang-tidy; `ament_cpplint` in ROS 2 packages | [`configs/c-cpp/`](configs/c-cpp/) | +| [Python](docs/python.md) | [PEP 8](https://peps.python.org/pep-0008/), [Google docstrings](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings) | Ruff | Ruff, mypy | [`configs/python/`](configs/python/) | +| [JavaScript and TypeScript](docs/javascript-typescript.md) | typescript-eslint strict rules, Prettier defaults | Prettier | ESLint with typescript-eslint, `tsc` | [`configs/javascript-typescript/`](configs/javascript-typescript/) | +| [Swift](docs/swift.md) | [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) | swift-format | SwiftLint | [`configs/swift/`](configs/swift/) | +| [Kotlin](docs/kotlin.md) | [Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html), [Android Kotlin style guide](https://developer.android.com/kotlin/style-guide) | ktlint (`--format`) | ktlint, detekt | [`configs/kotlin/`](configs/kotlin/) | +| [C# and Unity](docs/csharp-unity.md) | [Microsoft C# conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions), [Unity C# style guide](https://unity.com/resources/c-sharp-style-guide-unity-6) | `dotnet format`, your IDE | Roslyn analyzers, Microsoft.Unity.Analyzers | [`configs/csharp-unity/`](configs/csharp-unity/) | + +Every repo also gets the shared [`.editorconfig`](configs/editorconfig/.editorconfig) and, for Markdown, [`.markdownlint.jsonc`](configs/markdown/.markdownlint.jsonc). Arduino, Java, MATLAB, Solidity and Ada: see [Other languages](docs/other-languages.md). + +## Quick start + +1. **New to the lab's code?** Read [Git and GitHub](docs/git-and-github.md), then the guide for your language. +2. **Setting up a repo?** Copy the configs for its languages (each guide has a copy command), add the CI snippet from the guide and start the README and `AGENTS.md` from the [templates](templates/). +3. **Changing a config here?** Run the checks, which need only Docker: + + ```bash + scripts/check-configs.sh # all languages + scripts/check-configs.sh python # one language + ``` + +## Repo map + +| Path | What it holds | Read it when | +|---|---|---| +| [`docs/`](docs/README.md) | One guide per topic: Git, documentation, AI agents, six languages, ROS 2 packages, hardware, other languages | You want the rules and the reasons | +| [`configs/`](configs/README.md) | Copy-ready formatter and linter configs (one folder per language) and the tested versions | You set up or update a repo | +| [`examples/`](examples/README.md) | Samples that pass each config; `bad/` samples that must fail | You want to see the style in real code, or you change a config | +| [`templates/`](templates/) | `README.template.md` and `AGENTS.template.md` for lab project repos | You start a new repo | +| [`scripts/check-configs.sh`](scripts/check-configs.sh) | Runs every tool against `examples/` in Docker | You change a config or a tool version | +| [`.github/workflows/`](.github/workflows/) | `configs.yml` runs the checks; `docs.yml` lints Markdown and checks links | You wonder what CI does | +| [`AGENTS.md`](AGENTS.md) | Instructions and repo map for coding agents (`CLAUDE.md` points to it) | You are, or you are directing, a coding agent | + +## Contributing + +Suggest a change by [opening an issue](https://github.com/ATR-Lab/dev-guidelines/issues/new) or a pull request. Change one topic per pull request, and for a config change, say why and run `scripts/check-configs.sh` for that language. Writing rules, sources and checks for this repo are in [`AGENTS.md`](AGENTS.md); people and agents follow the same ones. + +## License + +Text and code in this repository are released under the [MIT License](LICENSE). The names and logos of the Advanced Telerobotics Research Lab and Kent State University are trademarks and are not covered by that license. + +The seven commit message rules in [Git and GitHub](docs/git-and-github.md#commit-messages) are quoted from Chris Beams' post "[How to Write a Git Commit Message](https://cbea.ms/git-commit/)", with credit. The `.clang-format` rules come from the ROS 2 [ament_lint](https://github.com/ament/ament_lint) project (Apache License 2.0). + +--- + +

+ ATR Lab docs · + About the lab · + Getting started · + Dev guidelines · + All things ROS 2 · + All repositories
+ Advanced Telerobotics Research Lab · Department of Computer Science · Kent State University · + www.atr.cs.kent.edu +

diff --git a/configs/README.md b/configs/README.md new file mode 100644 index 0000000..c743885 --- /dev/null +++ b/configs/README.md @@ -0,0 +1,91 @@ +# Copy-ready configs + +> **Read this when** you want to add formatter and linter settings to a lab repo, or you are updating a tool version here. + +Each folder holds the config files for one language, ready to copy to the root of your repo. Every file is tested: `scripts/check-configs.sh` runs each tool against the samples in [`examples/`](../examples/), which must pass, and against a deliberately bad sample, which must fail. The one thing to remember: copy the files as they are, and change a rule only in a pull request that says why. + +## Contents + +- [What's here](#whats-here) +- [How to copy a config](#how-to-copy-a-config) +- [Tested versions](#tested-versions) +- [How the configs are checked](#how-the-configs-are-checked) +- [Updating a tool version](#updating-a-tool-version) +- [Related](#related) + +## What's here + +| Folder | Files | Language page | +|---|---|---| +| [`editorconfig/`](editorconfig/) | `.editorconfig`: indentation, line endings and line length for every language | [Documentation](../docs/documentation.md) | +| [`c-cpp/`](c-cpp/) | `.clang-format` (ROS 2 style, identical to `ament_clang_format`), `.clang-tidy` | [C and C++](../docs/c-cpp.md) | +| [`python/`](python/) | `ruff.toml`, `pyproject-snippet.toml` (mypy, pyright, pytest, dev dependencies) | [Python](../docs/python.md) | +| [`javascript-typescript/`](javascript-typescript/) | `eslint.config.mjs`, `.prettierrc.json`, `tsconfig.base.json`, `package-snippet.json` | [JavaScript and TypeScript](../docs/javascript-typescript.md) | +| [`swift/`](swift/) | `.swift-format`, `.swiftlint.yml` | [Swift](../docs/swift.md) | +| [`kotlin/`](kotlin/) | `.editorconfig` (ktlint settings), `detekt.yml` | [Kotlin](../docs/kotlin.md) | +| [`csharp-unity/`](csharp-unity/) | `.editorconfig` (formatting, naming rules, analyzer severities, Unity notes) | [C# and Unity](../docs/csharp-unity.md) | +| [`markdown/`](markdown/) | `.markdownlint.jsonc` (the same rules the lab's docs repos use) | [Documentation](../docs/documentation.md#writing-well) | + +A repo that mixes languages has one `.editorconfig` at its root: start from `editorconfig/.editorconfig` and paste in the `[*.{kt,kts}]` section from `kotlin/` or the `[*.cs]` section from `csharp-unity/` when you need them. + +## How to copy a config + +Each language page has the exact commands. The pattern is the same everywhere: + +```bash +# From the root of your repo; replace and +BASE=https://raw.githubusercontent.com/ATR-Lab/dev-guidelines/master/configs +curl -fsSLO "$BASE//" +``` + +Then run the formatter over the whole repo once, commit that on its own ("Apply lab formatter config") and add the CI snippet from the language page. + +## Tested versions + +Tested with these versions on Sept. 30, 2026. Newer patch releases should behave the same; re-run the checks before you move to a new minor or major version. + +| Language | Tool | Version | Where it ran | +|---|---|---|---| +| All | editorconfig-checker | 4.0.2 | `mstruebing/editorconfig-checker:4.0.2` | +| C, C++ | clang-format, clang-tidy | 18.1.3 | Ubuntu 24.04 packages in `ros:jazzy-ros-base` | +| C, C++ | clang-format (same output on our examples) | 14.0.0, 21.1.8, 23.1.2 | Ubuntu 22.04, Ubuntu 26.04, apt.llvm.org | +| C, C++, Python | ament_lint (`ament_clang_format`, `ament_cpplint`, `ament_flake8`, `ament_pep257` and others) | 0.17.5 | ROS 2 Jazzy | +| Python | Ruff | 0.16.9 | Python 3.12.14, uv 0.12.21 | +| Python | mypy, pyright, pytest | 2.3.1, 1.1.414, 9.1.1 | same | +| JavaScript, TypeScript | ESLint, `@eslint/js` | 10.11.0, 10.0.1 | Node.js 24.21.0 | +| JavaScript, TypeScript | typescript-eslint, TypeScript | 8.71.0, 6.0.3 | same | +| JavaScript, TypeScript | Prettier, eslint-config-prettier, globals | 3.9.9, 10.1.8, 17.12.0 | same | +| Swift | swift-format (bundled with the toolchain) | Swift 6.4 | `swift:6.4-noble`; Xcode 27.1 on macOS | +| Swift | SwiftLint | 0.65.1 (also 0.63.2) | `ghcr.io/realm/swiftlint:0.65.1`; Homebrew | +| Kotlin | ktlint | 1.8.0 | Eclipse Temurin Java 21 | +| Kotlin | detekt | 1.23.8 | same | +| C# | .NET SDK (`dotnet build`, `dotnet format`) | 10.0.401 | `mcr.microsoft.com/dotnet/sdk:10.0` | +| C# | Microsoft.Unity.Analyzers | 1.28.0 | same | +| Markdown | markdownlint-cli2 | 0.23.3 | `davidanson/markdownlint-cli2:v0.23.3` | + +The Unity scripts were checked with the .NET SDK against small stand-ins for the UnityEngine types (`examples/csharp-unity/check/`), not inside the Unity Editor. See [C# and Unity](../docs/csharp-unity.md#how-unity-6-uses-analyzers-and-editorconfig) for what Unity itself reads. + +## How the configs are checked + +```bash +scripts/check-configs.sh # every language (needs Docker) +scripts/check-configs.sh python kotlin # only some +scripts/check-configs.sh --local swift # use tools installed on this machine +``` + +For each language, the script copies the samples and the configs into a throwaway container, so nothing changes in your checkout. The good samples must pass every tool; each file in `examples//bad/` must be rejected, which proves the rules are switched on. The bad samples for EditorConfig and Markdown are generated by the script, because editors and the docs workflow would "fix" or reject committed ones. The same script runs in CI ([`.github/workflows/configs.yml`](../.github/workflows/configs.yml)) on every pull request that touches `configs/`, `examples/` or the script. + +## Updating a tool version + +1. Change the version in the config file's header comment, in `scripts/check-configs.sh` and in the table above. +2. For JavaScript and TypeScript, also update `package-snippet.json`; for Python, `pyproject-snippet.toml`. +3. Run `scripts/check-configs.sh` for that language and fix the samples or the config until it passes. +4. Mention the new version and anything that changed in the pull request, so repos that copied the config know to update. + +## Related + +- [All guides](../docs/README.md) +- [Examples](../examples/README.md): what each sample shows +- [Check script](../scripts/check-configs.sh) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/configs/c-cpp/.clang-format b/configs/c-cpp/.clang-format new file mode 100644 index 0000000..1314611 --- /dev/null +++ b/configs/c-cpp/.clang-format @@ -0,0 +1,38 @@ +# clang-format settings for C and C++ in ATR Lab repos (ROS 2 style). +# +# This is the ROS 2 configuration exactly as ament_clang_format ships it +# (github.com/ament/ament_lint, ament_clang_format/configuration/.clang-format; +# identical on the jazzy, kilted, lyrical and rolling branches, checked +# Sept. 30, 2026). Keeping it identical means format-on-save in your editor and +# `ament_clang_format` in `colcon test` always agree, with no extra settings. +# +# Do not edit the rules. If a repo truly needs a change, also pass the file to +# ament_clang_format with `--config`, and say why in the pull request. +# +# In ROS 2 packages, run ament_clang_format instead of ament_uncrustify: the two +# formatters disagree on lambdas, one-line functions and constructor +# initializer lists, so a file cannot satisfy both. See docs/c-cpp.md. +# +# clang-format applies the "Cpp" settings to C files too, so firmware written in +# C uses the same file. +# Options reference: https://clang.llvm.org/docs/ClangFormatStyleOptions.html +--- +Language: Cpp +BasedOnStyle: Google + +AccessModifierOffset: -2 +AlignAfterOpenBracket: AlwaysBreak +BraceWrapping: + AfterClass: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterEnum: true +BreakBeforeBraces: Custom +ColumnLimit: 100 +ConstructorInitializerIndentWidth: 0 +ContinuationIndentWidth: 2 +DerivePointerAlignment: false +PointerAlignment: Middle +ReflowComments: false +... diff --git a/configs/c-cpp/.clang-tidy b/configs/c-cpp/.clang-tidy new file mode 100644 index 0000000..bd2e24e --- /dev/null +++ b/configs/c-cpp/.clang-tidy @@ -0,0 +1,91 @@ +# clang-tidy settings for C and C++ in ATR Lab repos. +# +# Goal: catch real bugs and outdated C++ with few false alarms. We start from +# nothing ("-*") and turn on four families, then switch off the checks that are +# mostly noise in robot code: +# bugprone-easily-swappable-parameters fires on most functions with two doubles +# bugprone-narrowing-conversions fires on every float/double/int mix in sensor code +# bugprone-reserved-identifier ROS 2 header guards (PKG__FILE_HPP_) use "__" +# performance-avoid-endl style, not a real cost in node code +# modernize-avoid-bind the ROS 2 style allows std::bind; tutorials use it +# modernize-avoid-c-arrays vendor SDKs and C interop need C arrays +# modernize-use-nodiscard would ask for [[nodiscard]] on every getter +# modernize-use-trailing-return-type a style choice we do not make +# +# clang-tidy needs to compile each file, so give it a compilation database: +# colcon build --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +# clang-tidy -p build/ src/*.cpp +# Check list: https://clang.llvm.org/extra/clang-tidy/checks/list.html +--- +Checks: > + -*, + bugprone-*, + -bugprone-easily-swappable-parameters, + -bugprone-narrowing-conversions, + -bugprone-reserved-identifier, + clang-analyzer-*, + performance-*, + -performance-avoid-endl, + modernize-*, + -modernize-avoid-bind, + -modernize-avoid-c-arrays, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + readability-braces-around-statements, + readability-container-size-empty, + readability-delete-null-pointer, + readability-duplicate-include, + readability-else-after-return, + readability-identifier-naming, + readability-inconsistent-declaration-parameter-name, + readability-make-member-function-const, + readability-misleading-indentation, + readability-redundant-control-flow, + readability-redundant-declaration, + readability-redundant-smartptr-get, + readability-redundant-string-cstr, + readability-simplify-boolean-expr, + readability-static-accessed-through-instance, + readability-string-compare + +# Every enabled check fails the build in CI. Keep the list short enough that +# this stays true. +WarningsAsErrors: '*' + +# Report problems in our own headers (include/ and src/), not in /opt/ros or +# vendor SDK headers. +HeaderFilterRegex: '.*/(include|src)/.*' + +FormatStyle: file + +CheckOptions: + # Naming follows the ROS 2 code style: classes in CamelCase, functions and + # variables in snake_case, members with a trailing underscore, globals with + # a g_ prefix. ROS 2 allows several styles for constants, so we do not + # enforce one (aNy_CasE). + readability-identifier-naming.NamespaceCase: lower_case + readability-identifier-naming.ClassCase: CamelCase + readability-identifier-naming.StructCase: CamelCase + readability-identifier-naming.UnionCase: CamelCase + readability-identifier-naming.EnumCase: CamelCase + readability-identifier-naming.TemplateParameterCase: CamelCase + readability-identifier-naming.FunctionCase: lower_case + readability-identifier-naming.MethodCase: lower_case + readability-identifier-naming.ParameterCase: lower_case + readability-identifier-naming.VariableCase: lower_case + readability-identifier-naming.MemberCase: lower_case + readability-identifier-naming.PrivateMemberSuffix: '_' + readability-identifier-naming.ProtectedMemberSuffix: '_' + readability-identifier-naming.GlobalVariablePrefix: 'g_' + readability-identifier-naming.MacroDefinitionCase: UPPER_CASE + # Header guards in the cpplint/ROS 2 form: PKG_NAME__FILE_NAME_HPP_ + readability-identifier-naming.MacroDefinitionIgnoredRegexp: '^[A-Z0-9_]+_(H|HPP)_$' + readability-identifier-naming.ConstexprVariableCase: aNy_CasE + readability-identifier-naming.GlobalConstantCase: aNy_CasE + readability-identifier-naming.StaticConstantCase: aNy_CasE + readability-identifier-naming.LocalConstantCase: aNy_CasE + readability-identifier-naming.ConstantMemberCase: aNy_CasE + readability-identifier-naming.EnumConstantCase: aNy_CasE + # ROS 2 subscription callbacks often take the message's shared pointer by value. + performance-unnecessary-value-param.AllowedTypes: 'std::shared_ptr;SharedPtr;ConstSharedPtr' +... diff --git a/configs/csharp-unity/.editorconfig b/configs/csharp-unity/.editorconfig new file mode 100644 index 0000000..d86b70a --- /dev/null +++ b/configs/csharp-unity/.editorconfig @@ -0,0 +1,160 @@ +# EditorConfig for C# in ATR Lab repos: Unity 6 projects (VR/XR teleoperation) and plain .NET. +# +# Rider, Visual Studio and VS Code (C# Dev Kit, Unity extension) read this file for formatting, +# naming and analyzer severities. `dotnet format` and `dotnet build` read it for .NET projects. +# Tested with .NET SDK 10.0 and Microsoft.Unity.Analyzers 1.28.0 (Sept. 30, 2026). +# +# Unity notes (details in docs/csharp-unity.md): +# - Put this file at the repo root, next to Assets/. Unity leaves .editorconfig alone. +# - Unity 6 compiles C# 9.0, so file-scoped namespaces and other C# 10+ features are out. +# - The Unity manual documents setting analyzer severities in a root .editorconfig +# (dotnet_diagnostic..severity) or in Assets/Default.ruleset; check the Console to +# confirm which one your Unity version honors. +# - Naming (IDE1006) and formatting (IDE0055) are .NET code-style rules: your editor shows +# them while you type; CI checks them with `dotnet format`. +# +# Naming follows Microsoft's C# conventions (PascalCase members, _camelCase private fields, +# s_camelCase private static fields), which Unity's C# style guide lists as an option. +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +# Unity writes these files itself. Leave them exactly as Unity saves them. +[*.{unity,prefab,asset,meta,mat,anim,controller,overrideController,physicMaterial,mask,mixer,playable,signal,renderTexture,spriteatlas,shadervariants,lighting,terrainlayer,brush,flare,fontsettings,guiskin,giparams,cubemap}] +charset = unset +end_of_line = unset +indent_style = unset +insert_final_newline = unset +trim_trailing_whitespace = unset + +[*.{csproj,props,targets,xml,json,asmdef,asmref,inputactions}] +indent_size = 2 + +[*.cs] +indent_size = 4 +max_line_length = 120 + +#### Formatting (IDE0055): Allman braces, as in Microsoft's C# conventions #### +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +#### Code style #### +csharp_prefer_braces = true:warning +csharp_using_directive_placement = outside_namespace:warning +csharp_style_namespace_declarations = block_scoped:warning +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = true:suggestion + +#### Naming (IDE1006) #### +# Symbol groups +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter +dotnet_naming_symbols.types.applicable_kinds = class, struct, enum, delegate +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method, local_function +dotnet_naming_symbols.constants.applicable_kinds = field +dotnet_naming_symbols.constants.required_modifiers = const +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +# Styles +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_naming_style.camel_case.capitalization = camel_case +dotnet_naming_style.i_prefix.capitalization = pascal_case +dotnet_naming_style.i_prefix.required_prefix = I +dotnet_naming_style.t_prefix.capitalization = pascal_case +dotnet_naming_style.t_prefix.required_prefix = T +dotnet_naming_style.underscore_camel_case.capitalization = camel_case +dotnet_naming_style.underscore_camel_case.required_prefix = _ +dotnet_naming_style.s_camel_case.capitalization = camel_case +dotnet_naming_style.s_camel_case.required_prefix = s_ + +# Rules (the SDK orders them from most to least specific) +dotnet_naming_rule.interfaces_start_with_i.symbols = interfaces +dotnet_naming_rule.interfaces_start_with_i.style = i_prefix +dotnet_naming_rule.interfaces_start_with_i.severity = warning +dotnet_naming_rule.type_parameters_start_with_t.symbols = type_parameters +dotnet_naming_rule.type_parameters_start_with_t.style = t_prefix +dotnet_naming_rule.type_parameters_start_with_t.severity = warning +dotnet_naming_rule.types_are_pascal_case.symbols = types +dotnet_naming_rule.types_are_pascal_case.style = pascal_case +dotnet_naming_rule.types_are_pascal_case.severity = warning +dotnet_naming_rule.members_are_pascal_case.symbols = non_field_members +dotnet_naming_rule.members_are_pascal_case.style = pascal_case +dotnet_naming_rule.members_are_pascal_case.severity = warning +dotnet_naming_rule.constants_are_pascal_case.symbols = constants +dotnet_naming_rule.constants_are_pascal_case.style = pascal_case +dotnet_naming_rule.constants_are_pascal_case.severity = warning +dotnet_naming_rule.public_fields_are_pascal_case.symbols = public_fields +dotnet_naming_rule.public_fields_are_pascal_case.style = pascal_case +dotnet_naming_rule.public_fields_are_pascal_case.severity = warning +dotnet_naming_rule.private_static_fields_are_s_camel_case.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_are_s_camel_case.style = s_camel_case +dotnet_naming_rule.private_static_fields_are_s_camel_case.severity = warning +dotnet_naming_rule.private_fields_are_underscore_camel_case.symbols = private_fields +dotnet_naming_rule.private_fields_are_underscore_camel_case.style = underscore_camel_case +dotnet_naming_rule.private_fields_are_underscore_camel_case.severity = warning +dotnet_naming_rule.locals_and_parameters_are_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_and_parameters_are_camel_case.style = camel_case +dotnet_naming_rule.locals_and_parameters_are_camel_case.severity = warning + +#### Analyzer severities #### +# Unnecessary using directive +dotnet_diagnostic.IDE0005.severity = warning +# Add braces +dotnet_diagnostic.IDE0011.severity = warning +# Add accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning +# Formatting +dotnet_diagnostic.IDE0055.severity = warning +# Naming +dotnet_diagnostic.IDE1006.severity = warning + +# Microsoft.Unity.Analyzers (bundled with Visual Studio's Unity workload and the VS Code Unity +# extension; NuGet package for other setups). Their default severity is "info", which is easy to +# miss, so the rules that point at real bugs or per-frame allocations are raised to warnings. +# Rule list: https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/main/doc/index.md +# Empty Unity message (Update with no body still costs a call) +dotnet_diagnostic.UNT0001.severity = warning +# Inefficient tag comparison; use CompareTag +dotnet_diagnostic.UNT0002.severity = warning +# Incorrect Unity message signature +dotnet_diagnostic.UNT0006.severity = warning +# Null coalescing (??) on Unity objects +dotnet_diagnostic.UNT0007.severity = warning +# Null propagation (?.) on Unity objects +dotnet_diagnostic.UNT0008.severity = warning +# GetComponent always allocates; use TryGetComponent +dotnet_diagnostic.UNT0026.severity = warning +# Pattern matching with null (is null) on Unity objects +dotnet_diagnostic.UNT0029.severity = warning diff --git a/configs/editorconfig/.editorconfig b/configs/editorconfig/.editorconfig new file mode 100644 index 0000000..47fd176 --- /dev/null +++ b/configs/editorconfig/.editorconfig @@ -0,0 +1,77 @@ +# EditorConfig for ATR Lab repos: one file at the repo root sets the basics for every language, +# so any editor indents and saves files the same way. Formatters (clang-format, ruff, Prettier, +# swift-format, ktlint, dotnet format) still have the final say; these values match them. +# +# Kotlin and C# need extra settings (ktlint style, naming rules, analyzer severities): copy the +# [*.{kt,kts}] section of configs/kotlin/.editorconfig and the [*.cs] section of +# configs/csharp-unity/.editorconfig into this file when your repo uses those languages. +# +# EditorConfig: https://editorconfig.org (spec: https://spec.editorconfig.org). +# max_line_length is not in the core spec; some editors and editorconfig-checker read it. +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +# C and C++ (ROS 2 style) and Arduino sketches +[*.{c,cc,cpp,cxx,h,hh,hpp,hxx,ino}] +indent_size = 2 +max_line_length = 100 + +# Python (PEP 8 indentation; 99 columns, as ament_flake8 checks) +[*.py] +indent_size = 4 +max_line_length = 99 + +# Swift (Xcode's default indentation; same numbers as configs/swift/.swift-format) +[*.swift] +indent_size = 4 +max_line_length = 120 + +# Kotlin (Android Kotlin style guide) +[*.{kt,kts}] +indent_size = 4 +max_line_length = 100 + +# C# and Unity scripts +[*.cs] +indent_size = 4 +max_line_length = 120 + +# JavaScript, TypeScript and web files (Prettier printWidth) +[*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,css,scss,html}] +indent_size = 2 +max_line_length = 100 + +# Data, config and robot description files (package.xml, URDF, xacro, launch, YAML) +[*.{json,jsonc,yml,yaml,toml,xml,launch,urdf,xacro,sdf,srdf,world,rviz}] +indent_size = 2 + +[{CMakeLists.txt,*.cmake}] +indent_size = 2 + +# Prose wraps in the editor, so no line limit +[*.md] +max_line_length = off + +# Tabs are part of the syntax here +[{Makefile,*.mk}] +indent_style = tab + +# Windows scripts +[*.{bat,cmd,ps1}] +end_of_line = crlf + +# Files that tools write: leave them exactly as the tool saved them +[*.{unity,prefab,asset,meta,mat,anim,controller,svg,lock}] +charset = unset +end_of_line = unset +indent_style = unset +indent_size = unset +insert_final_newline = unset +trim_trailing_whitespace = unset diff --git a/configs/javascript-typescript/.prettierrc.json b/configs/javascript-typescript/.prettierrc.json new file mode 100644 index 0000000..6414c22 --- /dev/null +++ b/configs/javascript-typescript/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "printWidth": 100, + "singleQuote": true +} diff --git a/configs/javascript-typescript/eslint.config.mjs b/configs/javascript-typescript/eslint.config.mjs new file mode 100644 index 0000000..551754c --- /dev/null +++ b/configs/javascript-typescript/eslint.config.mjs @@ -0,0 +1,51 @@ +// @ts-check +// ESLint flat config for JavaScript and TypeScript in ATR Lab repos. +// +// Tested with eslint 10.11.0, typescript-eslint 8.71.0, typescript 6.0.3 and +// eslint-config-prettier 10.1.8 (Sept. 30, 2026). +// +// - ESLint's recommended rules plus typescript-eslint's "strict" and "stylistic" +// sets with type information, which catch floating promises, unsafe `any` +// and unnecessary conditions that plain lint rules miss. +// - Prettier owns formatting; eslint-config-prettier turns off every ESLint rule +// that would fight it, so it must stay last. +// Docs: https://typescript-eslint.io/getting-started/typed-linting +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier/flat'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default defineConfig( + globalIgnores(['dist/', 'build/', 'coverage/', '**/*.min.js']), + { + files: ['**/*.{js,mjs,cjs,ts,mts,cts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + // Dashboards run in the browser, servers in Node.js. Remove the one you + // do not use. + globals: { ...globals.browser, ...globals.node }, + }, + rules: { + eqeqeq: ['error', 'always'], + 'no-console': ['warn', { allow: ['warn', 'error'] }], + '@typescript-eslint/consistent-type-imports': 'error', + // Numbers in template strings (`${speed} m/s`) are fine. + '@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }], + }, + }, + { + // Plain JavaScript files (like this config) have no type information. + files: ['**/*.{js,mjs,cjs}'], + extends: [tseslint.configs.disableTypeChecked], + }, + prettier, +); diff --git a/configs/javascript-typescript/package-snippet.json b/configs/javascript-typescript/package-snippet.json new file mode 100644 index 0000000..69d5422 --- /dev/null +++ b/configs/javascript-typescript/package-snippet.json @@ -0,0 +1,25 @@ +{ + "//": "Merge into your package.json. Versions tested together on Sept. 30, 2026. typescript stays on 6.0.x because typescript-eslint 8.71 supports TypeScript below 6.1.", + "type": "module", + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier . --write", + "format:check": "prettier . --check", + "typecheck": "tsc --noEmit", + "check": "npm run format:check && npm run lint && npm run typecheck" + }, + "devDependencies": { + "@eslint/js": "10.0.1", + "@types/node": "24.19.0", + "eslint": "10.11.0", + "eslint-config-prettier": "10.1.8", + "globals": "17.12.0", + "prettier": "3.9.9", + "typescript": "6.0.3", + "typescript-eslint": "8.71.0" + }, + "engines": { + "node": ">=22.13" + } +} diff --git a/configs/javascript-typescript/tsconfig.base.json b/configs/javascript-typescript/tsconfig.base.json new file mode 100644 index 0000000..2a475a1 --- /dev/null +++ b/configs/javascript-typescript/tsconfig.base.json @@ -0,0 +1,33 @@ +// Strict base settings for ATR Lab TypeScript projects (tested with typescript 6.0.3, +// Sept. 30, 2026). Extend it from your tsconfig.json and set include, outDir and lib there: +// { "extends": "./tsconfig.base.json", "compilerOptions": { "outDir": "dist" }, "include": ["src"] } +// TypeScript 6 no longer loads every installed @types package: list the ones you use, for +// example "types": ["node"] for a Node.js server. +// Options reference: https://www.typescriptlang.org/tsconfig/ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + + // Type safety + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + + // Module hygiene: imports compile the way they are written + "verbatimModuleSyntax": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + + // Output + "declaration": true, + "sourceMap": true, + "skipLibCheck": true + } +} diff --git a/configs/kotlin/.editorconfig b/configs/kotlin/.editorconfig new file mode 100644 index 0000000..41ffa87 --- /dev/null +++ b/configs/kotlin/.editorconfig @@ -0,0 +1,31 @@ +# EditorConfig for Kotlin (Android, Wear OS) in ATR Lab repos; ktlint reads its settings here. +# Tested with ktlint 1.8.0 (Sept. 30, 2026). If your repo already has a root .editorconfig +# (configs/editorconfig/.editorconfig), copy only the [*.{kt,kts}] section into it. +# ktlint settings: https://ktlint.github.io/ktlint/1.8.0/rules/configuration-ktlint/ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{kt,kts}] +indent_size = 4 +# Android Kotlin style guide: "Code has a column limit of 100 characters." +max_line_length = 100 +# "android_studio" follows the Android Kotlin style guide and Android Studio's own +# formatter, so Code > Reformat Code and ktlint agree. +ktlint_code_style = android_studio +# Jetpack Compose functions (Wear OS UI) are named like types: HeartRateCard(). +ktlint_function_naming_ignore_when_annotated_with = Composable +# Android Kotlin style guide: "Wildcard imports (of any type) are not allowed." +# These three lines stop Android Studio from writing them (from ktlint's IntelliJ guide). +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 +ij_kotlin_packages_to_use_import_on_demand = unset +# Trailing commas on declarations, as the Kotlin coding conventions encourage. +# Android Studio reads the same property. +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = false diff --git a/configs/kotlin/detekt.yml b/configs/kotlin/detekt.yml new file mode 100644 index 0000000..49cd6d2 --- /dev/null +++ b/configs/kotlin/detekt.yml @@ -0,0 +1,41 @@ +# detekt settings for Kotlin in ATR Lab repos: only the changes from detekt's defaults. +# Run with --build-upon-default-config (Gradle: buildUponDefaultConfig = true) so +# every other rule keeps its default. ktlint handles formatting, so the +# detekt-formatting plugin is not used. +# Tested with detekt 1.23.8 (Sept. 30, 2026). Rules: https://detekt.dev/docs/1.23.8/rules/complexity + +config: + validation: true + +complexity: + LongParameterList: + # Compose functions take many parameters with defaults. + ignoreAnnotated: ['Composable'] + TooManyFunctions: + thresholdInFiles: 15 + thresholdInClasses: 15 + +naming: + FunctionNaming: + # Compose functions are named like types: HeartRateCard(). + ignoreAnnotated: ['Composable'] + TopLevelPropertyNaming: + constantPattern: '[A-Z][A-Z0-9_]*' + +style: + MaxLineLength: + # Same limit as ktlint (.editorconfig max_line_length). + maxLineLength: 100 + MagicNumber: + ignorePropertyDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotated: ['Composable', 'Preview'] + WildcardImport: + # Android Kotlin style guide: no wildcard imports of any kind (the default allows java.util.*). + excludeImports: [] + UnusedPrivateMember: + # @Preview functions are used by Android Studio, not by code. + ignoreAnnotated: ['Preview'] + ForbiddenComment: + # TODO and FIXME are fine; track the work in an issue. + comments: ['STOPSHIP'] diff --git a/configs/markdown/.markdownlint.jsonc b/configs/markdown/.markdownlint.jsonc new file mode 100644 index 0000000..0bc83f1 --- /dev/null +++ b/configs/markdown/.markdownlint.jsonc @@ -0,0 +1,22 @@ +// markdownlint rules for ATR Lab repos (https://github.com/DavidAnson/markdownlint). +// The same rules the lab's docs repos use. Copy this file to your repo root as .markdownlint.jsonc. +// Tested with markdownlint-cli2 0.23.3 (Sept. 30, 2026). +{ + "default": true, + // Long lines are fine in prose; editors wrap them. + "MD013": false, + // READMEs open with an HTML header image instead of a "# Heading". + "MD041": false, + // HTML is allowed only for layout GitHub supports: centered headers, card grids, collapsible sections. + "MD033": { + "allowed_elements": ["p", "img", "a", "b", "br", "sub", "sup", "table", "tr", "td", "th", "thead", "tbody", "picture", "source", "details", "summary", "kbd", "div"] + }, + // Repeated headings are fine in different sections (e.g. "Safety" under each robot). + "MD024": { "siblings_only": true }, + // Numbered lists may use 1. 2. 3. or all 1. + "MD029": false, + // Table pipe spacing is cosmetic; "|---|" and "| --- |" are both fine. + "MD060": false, + // Emphasis used as a label ("**Read this when** ...") is not a heading. + "MD036": false +} diff --git a/configs/python/pyproject-snippet.toml b/configs/python/pyproject-snippet.toml new file mode 100644 index 0000000..510d209 --- /dev/null +++ b/configs/python/pyproject-snippet.toml @@ -0,0 +1,38 @@ +# Paste these tables into your project's pyproject.toml, next to ruff.toml. +# Tested with ruff 0.16.9, mypy 2.3.1 and pyright 1.1.414 (Sept. 30, 2026). +# +# Add the dev tools with uv (versions below are the ones we tested): +# uv add --dev ruff==0.16.9 mypy==2.3.1 pytest + +[dependency-groups] +dev = [ + "mypy==2.3.1", + "pytest>=8", + "ruff==0.16.9", +] + +# mypy is the type checker that gates CI: `uv run mypy .` +# Docs: https://mypy.readthedocs.io/en/stable/config_file.html +[tool.mypy] +python_version = "3.10" +strict = true +warn_unreachable = true +# Keep caches and virtual environments out of the check. +exclude = ["^\\.venv/", "^build/", "^install/", "^log/"] + +# Libraries without type hints: list them here instead of silencing mypy +# everywhere. Replace the example names with the ones your code imports. +[[tool.mypy.overrides]] +module = ["example_untyped_package.*"] +ignore_missing_imports = true + +# pyright powers Pylance in VS Code. "standard" gives fast, low-noise feedback +# while you type; mypy --strict stays the gate in CI. +# Docs: https://microsoft.github.io/pyright/#/configuration +[tool.pyright] +pythonVersion = "3.10" +typeCheckingMode = "standard" +exclude = [".venv", "build", "install", "log"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/configs/python/ruff.toml b/configs/python/ruff.toml new file mode 100644 index 0000000..41073f4 --- /dev/null +++ b/configs/python/ruff.toml @@ -0,0 +1,57 @@ +# Ruff settings for Python in ATR Lab repos (lint and format). +# +# Tested with ruff 0.16.9 (Sept. 30, 2026). Docs: https://docs.astral.sh/ruff/ +# +# The line length and quote style match the ROS 2 Python checks (ament_flake8), +# so code that passes Ruff also passes `colcon test` in a ROS 2 package. Docstrings +# follow the Google style; in ROS 2 packages, run ament_pep257 with +# `--convention google` (see docs/python.md), because its default "ament" +# convention rejects Google-style "Args:" sections. + +# Oldest Python we support: 3.10 (Ubuntu 22.04 and ROS 2 Humble). Raise it when +# a repo only targets newer systems (Jazzy on Ubuntu 24.04 ships 3.12). +target-version = "py310" +line-length = 99 + +[format] +# ROS 2 style: "We pick single quotes over double quotes as long as no escaping +# is necessary." Docstrings keep triple double quotes (PEP 257). +quote-style = "single" +docstring-code-format = true + +[lint] +# Ruff 0.16 changed its default rule set, so we list every family we want. +select = [ + "E", # pycodestyle errors (what flake8 checks) + "W", # pycodestyle warnings + "F", # Pyflakes: unused imports, undefined names + "I", # isort: import order + "N", # pep8-naming + "D", # pydocstyle: docstrings (Google convention below) + "UP", # pyupgrade: modern syntax for target-version + "B", # flake8-bugbear: likely bugs + "A", # flake8-builtins (also run by ament_flake8) + "C4", # flake8-comprehensions (also run by ament_flake8) + "SIM", # flake8-simplify + "NPY", # NumPy-specific rules + "RUF", # Ruff's own rules +] +ignore = [ + "D100", # module docstring: optional, as in ament_pep257 + "D104", # package docstring: __init__.py files are often empty + "D105", # magic-method docstring + "D107", # __init__ docstring: document the class instead +] + +[lint.pydocstyle] +convention = "google" + +[lint.isort] +# Sort `import x` and `from x import y` together by module name, like the +# "google" import-order style that ament_flake8 checks. +force-sort-within-sections = true + +[lint.per-file-ignores] +"test/**" = ["D"] +"tests/**" = ["D"] +"setup.py" = ["D"] diff --git a/configs/swift/.swift-format b/configs/swift/.swift-format new file mode 100644 index 0000000..e067761 --- /dev/null +++ b/configs/swift/.swift-format @@ -0,0 +1,60 @@ +{ + "version": 1, + "lineLength": 120, + "indentation": { + "spaces": 4 + }, + "tabWidth": 4, + "maximumBlankLines": 1, + "respectsExistingLineBreaks": true, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeControlFlowKeywords": false, + "indentSwitchCaseLabels": false, + "multiElementCollectionTrailingCommas": true, + "spacesBeforeEndOfLineComments": 2, + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": true, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": false, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": false, + "UseExplicitNilCheckInConditions": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": false, + "ValidateDocumentationComments": false + } +} diff --git a/configs/swift/.swiftlint.yml b/configs/swift/.swiftlint.yml new file mode 100644 index 0000000..d767719 --- /dev/null +++ b/configs/swift/.swiftlint.yml @@ -0,0 +1,54 @@ +# SwiftLint settings for ATR Lab Swift projects (visionOS, watchOS, iOS, macOS). +# +# swift-format (the .swift-format file next to this one) owns layout; SwiftLint +# checks naming, safety and API use. The first three rules below are off because +# swift-format's output breaks them, so the two tools never disagree. +# Tested with SwiftLint 0.65.1 and the swift-format bundled with Swift 6.4 +# (Sept. 30, 2026). Rule reference: https://realm.github.io/SwiftLint/rule-directory.html + +disabled_rules: + - opening_brace # swift-format puts `{` on its own line after a multi-line condition + - trailing_comma # swift-format adds trailing commas to multi-line collections + - vertical_parameter_alignment # swift-format indents wrapped parameters instead of aligning them + - todo # TODO comments are fine; track the work in an issue + +opt_in_rules: + - accessibility_label_for_image # VoiceOver needs a label for every meaningful image + - contains_over_filter_count + - contains_over_first_not_nil + - discouraged_optional_boolean + - empty_count + - empty_string + - fatal_error_message + - first_where + - flatmap_over_map_reduce + - identical_operands + - last_where + - legacy_multiple + - private_swiftui_state # @State belongs to the view that owns it + - redundant_nil_coalescing + - sorted_first_last + - toggle_bool + - unowned_variable_capture + - weak_delegate + - yoda_condition + +excluded: + - .build + - DerivedData + - "**/Generated" + +line_length: + warning: 120 # same as lineLength in .swift-format + error: 160 + ignores_urls: true + +identifier_name: + excluded: # short names that are clear in robotics and math code + - x + - y + - z + - i + - j + - dt + - id diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e71fab5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,41 @@ +# Dev guidelines: all pages + +> **Read this when** you want the full list of guides in this repo, or you are not sure which page answers your question. + +Each page covers one topic and can be read on its own in about 10 minutes. Language pages all follow the same order: the style guide we follow, naming and layout, the tools and their config, editor setup, how to adopt the config, a CI snippet and common mistakes. + +## Working together + +| Page | What's inside | Read it when | +|---|---|---| +| [Git and GitHub](git-and-github.md) | Branches, the seven commit message rules, pull requests and reviews, issues, releases, `.gitignore`, Git LFS, secrets | You start a branch, write a commit or cut a release | +| [Documentation](documentation.md) | README standard, docstrings per language, `AGENTS.md` and `CLAUDE.md`, diagrams, changelogs | You start a repo or finish something others will use | +| [AI-assisted development](ai-assisted-development.md) | Rules for Claude Code, Codex and other coding agents in lab repos | You use an agent or review code one helped write | + +## Languages + +| Page | Formatter and linter | Read it when | +|---|---|---| +| [C and C++](c-cpp.md) | clang-format, clang-tidy, `ament_clang_format` | You write ROS 2 nodes, robot SDK code or C firmware | +| [Python](python.md) | Ruff, mypy, uv | You write ROS 2 nodes, ML code or tools | +| [JavaScript and TypeScript](javascript-typescript.md) | Prettier, ESLint with typescript-eslint, `tsc` | You build a dashboard or a Node.js server | +| [Swift](swift.md) | swift-format, SwiftLint | You build a visionOS, watchOS or iOS app | +| [Kotlin](kotlin.md) | ktlint, detekt | You build an Android or Wear OS app | +| [C# and Unity](csharp-unity.md) | `.editorconfig`, Roslyn analyzers, Microsoft.Unity.Analyzers, `dotnet format` | You build a Unity VR or XR app or a .NET tool | +| [Other languages](other-languages.md) | Official guides only | You write Arduino, Java, MATLAB, Solidity or Ada code | + +## Robots and hardware + +| Page | What's inside | Read it when | +|---|---|---| +| [ROS 2 packages](ros2-packages.md) | Package naming (REP 144), layout, `ament_lint_auto`, units (REP 103), frames (REP 105), launch files, parameters | You create or review a ROS 2 package | +| [Hardware and firmware](hardware.md) | CAD naming and versions, BOMs, PlatformIO firmware, wiring colors and labels, 3D print files | You design, wire, print or flash something | + +## Related + +- [Repo README](../README.md): the language matrix and quick start +- [Configs](../configs/README.md): the copy-ready config files and the versions we tested +- [Templates](../templates/): README and AGENTS templates for lab project repos +- [Getting started](https://github.com/ATR-Lab/getting-started-atr-lab): the lab's onboarding repo + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/ai-assisted-development.md b/docs/ai-assisted-development.md new file mode 100644 index 0000000..aedcaba --- /dev/null +++ b/docs/ai-assisted-development.md @@ -0,0 +1,87 @@ +# AI-assisted development + +> **Read this when** you use a coding agent such as Claude Code or Codex in a lab repo, or review a pull request that one helped write. + +Coding agents can read a repo, run commands and write code. Used well, they speed up the boring parts; used carelessly, they commit code nobody understood, leak data or move a robot nobody was watching. These rules apply to every lab repo. The one thing to remember: you are the author of every line you commit, whoever typed it. + +> [!NOTE] +> We name Claude Code and Codex because lab members use them. The rules apply to any AI coding tool, and naming a tool is not an endorsement by the lab or by Kent State University. + +## Contents + +- [The rules](#the-rules) +- [Set up a repo for agents](#set-up-a-repo-for-agents) +- [Working with an agent, step by step](#working-with-an-agent-step-by-step) +- [Data you must never share](#data-you-must-never-share) +- [Robots and hardware](#robots-and-hardware) +- [Licenses and third-party code](#licenses-and-third-party-code) +- [Papers and disclosure](#papers-and-disclosure) +- [Related](#related) + +## The rules + +1. **Review every diff before you commit it.** Read it as if a new lab mate wrote it. If you can't explain a line, don't commit it. +2. **Run the tests and linters yourself.** An agent saying "tests pass" is not evidence; the CI log is. +3. **Never paste secrets or restricted research data** into a prompt, a file the agent can read or a tool's settings (details below). +4. **Keep `AGENTS.md` current.** Agents follow it; an outdated one sends them the wrong way. +5. **Agents never push to the default branch and never force-push.** They work on a branch; a person opens or reviews the pull request and merges it. +6. **Agents never run code that moves hardware** unless a person is watching the robot with the emergency stop in reach. +7. **Check licenses** of code and dependencies an agent adds. +8. **Disclose AI assistance in papers** the way the venue requires. + +## Set up a repo for agents + +- Add `AGENTS.md` at the root (start from the [template](../templates/AGENTS.template.md)) and a `CLAUDE.md` containing only `@AGENTS.md`. Both Claude Code and Codex then read the same instructions ([Documentation](documentation.md#agentsmd-and-claudemd) explains why). +- In `AGENTS.md`, list the exact build, test and lint commands, the safety rules for the robot and what is off limits (for example, `config/limits.yaml` changes need a human reviewer). +- Use the tool's permission settings so it asks before running commands that change files outside the repo, install software or use the network. Don't give an agent credentials it doesn't need. +- Protect the default branch ([Git and GitHub](git-and-github.md#protected-default-branches)), so even a mistaken push cannot land there. + +## Working with an agent, step by step + +1. Create a branch: `git switch -c feature/short-description`. +2. Give the agent a small, clear task with the files and the goal ("add a unit test for `VelocityLimiter::limit` when `dt` is zero"). +3. Let it run the formatter, linter and tests from `AGENTS.md`. +4. Read the diff (`git diff`), run the tests yourself and fix what you don't like. +5. Commit with a message you wrote or checked (see [commit messages](git-and-github.md#commit-messages)). Keep any `Co-authored-by` line the tool adds, so the history shows how the code was made. +6. Open a pull request and say in the description that an agent helped, and with what. + +Good tasks: explaining unfamiliar code, writing tests, repetitive refactors, first drafts of docs, reviewing your own diff. Poor tasks: safety-critical control code without careful review, anything that produces research data or results. + +## Data you must never share + +Do not paste these into a prompt or leave them where an agent can read them: + +- passwords, API keys, tokens, private keys, Wi-Fi credentials, robot account logins; +- **identifiable research data**: anything from human-subjects studies (video, audio, photos, names, questionnaire responses) and any data your IRB protocol or data agreement restricts; +- unpublished data or text from collaborators without their permission. + +Check the tool's data-use terms before you use it for lab work, and follow Kent State's [AI Hub](https://www.kent.edu/ai): it lists the AI tools approved for university use and says tools outside that list (it names Claude and ChatGPT) "may be used for personal/non-sensitive work only, do not input university data." Until the director confirms with Kent State Information Security which lab work those tools may touch, use them only on public or non-sensitive code, such as the lab's open-source repositories. When in doubt, ask your advisor first. The lab's rules for research data are in the [research standards](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/research/standards.md). + +## Robots and hardware + +> [!CAUTION] +> Code an agent writes can move a robot in ways you did not expect. Test new motion code in simulation first. On hardware, a person watches the robot with the emergency stop in reach, and the speed limits in the parameter files stay in place. Never let an agent run a launch file, a driver or a firmware upload unattended. + +Follow the lab's [safety guide](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/lab-rules/safety.md) for every robot, whoever wrote the code. + +## Licenses and third-party code + +- Agents can reproduce code from their training data. If a block looks copied from a known project, find the source and check that its license allows our use, or rewrite it. +- Check the license of every dependency an agent adds (`package.xml`, `pyproject.toml`, `package.json`, Gradle files). Ask before adding GPL-licensed code to a repo under the MIT or Apache license. +- Keep vendor code in its own folder with its license file, and say where it came from in the README. + +## Papers and disclosure + +Publishers set their own rules for AI assistance. IEEE, for example, requires authors to disclose AI-generated content in the acknowledgments section and to name the system used ([IEEE author guidelines](https://journals.ieeeauthorcenter.ieee.org/become-an-ieee-journal-author/publishing-ethics/guidelines-and-policies/submission-and-peer-review-policies/)). Check the policy of each venue before you submit; the lab's [research standards](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/research/standards.md) list the common ones and how the lab handles authorship. + +Agents must never invent results, data or citations. Check that every reference an agent suggests exists and says what the text claims. + +## Related + +- [Documentation](documentation.md#agentsmd-and-claudemd): the `AGENTS.md` and `CLAUDE.md` convention +- [Git and GitHub](git-and-github.md): branches, reviews and protected branches +- [AGENTS template](../templates/AGENTS.template.md) +- [Research standards](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/research/standards.md) and [lab safety guide](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/lab-rules/safety.md) in Getting started +- Tool docs: [Claude Code memory and CLAUDE.md](https://code.claude.com/docs/en/memory), [Codex and AGENTS.md](https://learn.chatgpt.com/docs/agent-configuration/agents-md) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/c-cpp.md b/docs/c-cpp.md new file mode 100644 index 0000000..0258b05 --- /dev/null +++ b/docs/c-cpp.md @@ -0,0 +1,216 @@ +# C and C++ + +> **Read this when** you write C++ for a ROS 2 package or a robot SDK (Unitree Go2, Dynamixel), or C for firmware, and want your editor, your reviewer and CI to agree on style. + +We follow the ROS 2 code style everywhere, including code that never touches ROS, so one `.clang-format` and one `.clang-tidy` serve every lab repo. The one thing to remember: format with clang-format, and in ROS 2 packages run `ament_clang_format` instead of `ament_uncrustify`, because the two formatters cannot both be satisfied. + +## Contents + +- [The style we follow](#the-style-we-follow) +- [Language versions](#language-versions) +- [Naming and file layout](#naming-and-file-layout) +- [Install the tools](#install-the-tools) +- [Editor setup](#editor-setup) +- [Adopt the config in a repo](#adopt-the-config-in-a-repo) +- [Check in CI](#check-in-ci) +- [Common mistakes](#common-mistakes) +- [Related](#related) + +## The style we follow + +The [ROS 2 code style](https://docs.ros.org/en/jazzy/The-ROS2-Project/Contributing/Code-Style-Language-Versions.html) builds on the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) with a few changes. We use it because most of our C++ lives in ROS 2 packages, and matching the ecosystem means code moves between lab repos and upstream projects without a reformat. + +The changes from Google style that you will notice: + +| Rule | ROS 2 style | +|---|---| +| Line length | 100 characters | +| Braces | Open brace on its own line for function, class, struct and enum definitions; on the same line for `if`, `else`, `for`, `while` | +| Always braces | Even for one-line `if` and loop bodies | +| Pointers and references | `char * c;` and `const Limits & limits` (space on both sides) | +| Access specifiers | `public:` in the same column as `class` | +| Function names | `snake_case` is allowed and is what ROS 2 core packages use; we use it | +| Exceptions | Allowed; avoid them in APIs you plan to wrap in C | +| Boost | Avoid unless there is no alternative | + +For C, ROS 2 follows [PEP 7](https://peps.python.org/pep-0007/) (Python's C style) with `//` comments allowed. Our `.clang-format` formats C files with the same settings. + +`configs/c-cpp/.clang-format` is the configuration that ROS 2 ships in [ament_clang_format](https://github.com/ament/ament_lint/blob/jazzy/ament_clang_format/ament_clang_format/configuration/.clang-format), unchanged. We checked that clang-format 14 (Ubuntu 22.04), 18 (Ubuntu 24.04), 21 (Ubuntu 26.04) and 23 (the version Homebrew installs) all format our examples the same way; CI pins version 18. + +`configs/c-cpp/.clang-tidy` turns on four check families and then removes the checks that are mostly noise in robot code: + +| Family | What it catches | Turned off, and why | +|---|---|---| +| `bugprone-*` | Likely bugs: dangling handles, bad `memset`, suspicious string compares | `easily-swappable-parameters` (fires on every function with two `double`s), `narrowing-conversions` (every float/int mix in sensor code), `reserved-identifier` (ROS 2 header guards contain `__`) | +| `clang-analyzer-*` | Null dereferences, memory leaks, uses of uninitialized values (it follows each path through the code) | None | +| `performance-*` | Needless copies, `std::move` misuse | `avoid-endl` | +| `modernize-*` | `nullptr`, `using`, range `for`, `override` | `avoid-bind` (the ROS 2 style allows `std::bind`), `avoid-c-arrays` (vendor SDKs), `use-nodiscard`, `use-trailing-return-type` | +| `readability-*` (16 picked checks) | Missing braces, `size() == 0`, naming, redundant code | Everything else in the family | + +`readability-identifier-naming` enforces the naming table below. Every enabled check is an error (`WarningsAsErrors: '*'`), so keep the list short enough that this stays true. + +## Language versions + +The C and C++ standards depend on the ROS 2 distribution. Set them in `CMakeLists.txt` so every compiler agrees. + +| ROS 2 distribution | C++ | C | Source | +|---|---|---|---| +| Humble, Jazzy, Kilted | C++17 | C99 | [Jazzy code style page](https://docs.ros.org/en/jazzy/The-ROS2-Project/Contributing/Code-Style-Language-Versions.html) | +| Lyrical, Rolling | C++20 | C17 | [Rolling code style page](https://docs.ros.org/en/rolling/The-ROS2-Project/Contributing/Code-Style-Language-Versions.html) | + +Code that must build on more than one distribution targets the oldest one, so C++17 for anything that still runs on Humble or Jazzy. Firmware uses the standard its board's toolchain supports; check the platform's docs. + +## Naming and file layout + +| Thing | Style | Example | +|---|---|---| +| Namespaces, packages | `snake_case` | `teleop_velocity_limiter` | +| Classes, structs, enums, type aliases | `CamelCase` | `VelocityLimiter` | +| Functions and methods | `snake_case` | `limit()`, `on_command()` | +| Variables and parameters | `snake_case` | `dt_seconds` | +| Private and protected members | `snake_case_` with a trailing underscore | `last_command_` | +| Global variables | `g_` prefix | `g_shutdown_requested` | +| Macros | `UPPER_CASE` | `RING_BUFFER_CAPACITY` | +| Constants and enum values | Any consistent style in the package (ROS 2 mixes `kName`, `NAME` and `name`) | `kMaxSpeed` | +| Header guards | `PACKAGE__PATH__FILE_HPP_` (what `ament_cpplint` expects) | `TELEOP_VELOCITY_LIMITER__VELOCITY_LIMITER_HPP_` | +| Files | `snake_case.hpp` and `snake_case.cpp`; C uses `.h` and `.c` | `velocity_limiter_node.cpp` | + +A ROS 2 C++ package keeps public headers in `include//`, sources in `src/` and tests in `test/`; launch and parameter files go in `launch/` and `config/`. [ROS 2 packages](ros2-packages.md) has the full layout; `examples/c-cpp/teleop_velocity_limiter/` is a working example. + +## Install the tools + +macOS (Homebrew): + +```bash +brew install clang-format +brew install llvm # provides clang-tidy; Homebrew does not put it on PATH +echo 'export PATH="$(brew --prefix llvm)/bin:$PATH"' >> ~/.zshrc +``` + +Ubuntu 24.04 (clang-format and clang-tidy 18) or 22.04 (version 14): + +```bash +sudo apt update +sudo apt install clang-format clang-tidy +``` + +For ROS 2 packages, also install the ament wrapper (replace `jazzy` with your distribution): + +```bash +sudo apt install ros-jazzy-ament-cmake-clang-format +``` + +## Editor setup + +VS Code has two good options. Pick one; running both gives duplicate diagnostics. + +- **C/C++ extension** (`ms-vscode.cpptools`, Microsoft): formats with clang-format and reads `.clang-format` by default (`C_Cpp.clang_format_style` is `file`). Turn on clang-tidy with `"C_Cpp.codeAnalysis.clangTidy.enabled": true`. +- **clangd extension** (`llvm-vs-code-extensions.vscode-clangd`): reads `.clang-format` and [merges your `.clang-tidy`](https://clangd.llvm.org/config) into its live diagnostics. It looks for `compile_commands.json` in parent folders and in `build/`; in a colcon workspace, point it at the package's database with a `.clangd` file: + + ```yaml + CompileFlags: + CompilationDatabase: build/teleop_velocity_limiter + ``` + +Both need a compilation database for accurate results. In a colcon workspace: + +```bash +colcon build --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +``` + +Turn on format on save in `.vscode/settings.json`: + +```json +{ + "[cpp]": { "editor.formatOnSave": true }, + "[c]": { "editor.formatOnSave": true } +} +``` + +## Adopt the config in a repo + +Run this from the repo root (a single package, a colcon workspace repo or a firmware project): + +```bash +BASE=https://raw.githubusercontent.com/ATR-Lab/dev-guidelines/master/configs +curl -fsSLO "$BASE/c-cpp/.clang-format" +curl -fsSLO "$BASE/c-cpp/.clang-tidy" +# Reformat everything once, in its own commit, so later diffs stay small +find . -path ./build -prune -o \( -name '*.[ch]pp' -o -name '*.[ch]' \) -print0 | xargs -0 clang-format -i +git add .clang-format .clang-tidy +git commit -am "Format C++ with the lab clang-format config" +``` + +In each ROS 2 package that uses `ament_lint_auto`, swap uncrustify for clang-format. Add the test dependency to `package.xml`: + +```xml +ament_cmake_clang_format +``` + +Then exclude uncrustify in `CMakeLists.txt`, before `ament_lint_auto_find_test_dependencies()`: + +```cmake +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + # We format with clang-format; uncrustify disagrees with it on lambdas and one-line functions. + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify) + ament_lint_auto_find_test_dependencies() +endif() +``` + +`ament_clang_format` uses the same rules as our file, so it needs no extra settings. + +## Check in CI + +This GitHub Actions job builds and tests a ROS 2 repo on Jazzy and runs clang-format and clang-tidy. We ran these steps in a `ros:jazzy-ros-base` container against `examples/c-cpp/` before publishing them; `rosdep` installs `ament_cmake_clang_format` from your `package.xml`. + +```yaml +name: cpp +on: [push, pull_request] +jobs: + lint-build-test: + runs-on: ubuntu-latest + container: ros:jazzy-ros-base + steps: + - uses: actions/checkout@v7 + - name: Install tools and dependencies + run: | + apt-get update + apt-get install -y clang-format clang-tidy jq + rosdep update + rosdep install --from-paths . --ignore-src -y + - name: clang-format + run: find . \( -name '*.[ch]pp' -o -name '*.[ch]' \) -print0 | xargs -0 clang-format --dry-run --Werror + - name: Build, clang-tidy and test + shell: bash + run: | + source /opt/ros/jazzy/setup.bash + colcon build --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + # clang-tidy on our own sources only (not generated files or system libraries) + for db in build/*/compile_commands.json; do + files=$(jq -r '.[].file' "$db" | grep "^$PWD/" | grep -v "^$PWD/build/" | sort -u || true) + if [ -n "$files" ]; then clang-tidy -p "$(dirname "$db")" --quiet $files; fi + done + colcon test + colcon test-result --verbose +``` + +## Common mistakes + +- **Running two formatters.** `ament_uncrustify --reformat` and clang-format undo each other's changes. Pick clang-format and exclude uncrustify, as above. +- **clang-tidy without a compilation database.** It then guesses include paths and reports missing headers. Build with `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON` and pass `-p build/`. +- **`#pragma once` in ROS 2 packages.** `ament_cpplint` expects `#ifndef` guards named after the path. +- **Formatting vendor code.** Keep vendor SDKs (Unitree, Dynamixel) in their own folder and exclude it from `find`, so updates stay easy to diff. `HeaderFilterRegex` already keeps clang-tidy out of headers outside `include/` and `src/`. +- **A reformat mixed into a feature commit.** Reformat in a separate commit so reviewers can see the real change. +- **Committing `build/`, `install/` or `log/`.** Add them to `.gitignore` (see [Git and GitHub](git-and-github.md#gitignore)). + +## Related + +- [ROS 2 packages](ros2-packages.md): layout, naming and `ament_lint_auto` +- [Hardware and firmware](hardware.md): PlatformIO and Arduino projects +- [Python](python.md): the other half of most ROS 2 workspaces +- Config files: [`configs/c-cpp/`](../configs/c-cpp/) and the example package in [`examples/c-cpp/`](../examples/c-cpp/) +- [All things ROS 2](https://github.com/ATR-Lab/all-things-ros2): ROS 2 basics, distributions and installation +- clang-format [style options](https://clang.llvm.org/docs/ClangFormatStyleOptions.html) and clang-tidy [check list](https://clang.llvm.org/extra/clang-tidy/checks/list.html) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/csharp-unity.md b/docs/csharp-unity.md new file mode 100644 index 0000000..10abeac --- /dev/null +++ b/docs/csharp-unity.md @@ -0,0 +1,151 @@ +# C# and Unity + +> **Read this when** you write C# for a Unity 6 VR or XR teleoperation app (for example, on Meta Quest) or for a plain .NET tool, and want Rider, Visual Studio, VS Code and CI to apply the same rules. + +We follow Microsoft's C# coding conventions and pick, from the options Unity's C# style guide offers, the naming that matches Microsoft's. One `.editorconfig` carries the formatting rules, the naming rules and the analyzer severities, including the Unity-specific analyzers. The one thing to remember: your editor enforces these rules while you type; Unity's own compiler does not run the .NET code-style rules, so CI checks them with `dotnet format`. + +## Contents + +- [The style we follow](#the-style-we-follow) +- [Naming and file layout](#naming-and-file-layout) +- [What the config does](#what-the-config-does) +- [How Unity 6 uses analyzers and .editorconfig](#how-unity-6-uses-analyzers-and-editorconfig) +- [Install and editor setup](#install-and-editor-setup) +- [Adopt the config in a repo](#adopt-the-config-in-a-repo) +- [Check in CI](#check-in-ci) +- [Common mistakes](#common-mistakes) +- [Related](#related) + +## The style we follow + +- **Microsoft's [common C# code conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)** and [identifier naming rules](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names): Allman braces (each brace on its own line), four-space indentation, PascalCase for types and members. +- **Unity's C# style guide** ([Unity 6 edition](https://unity.com/resources/c-sharp-style-guide-unity-6)) for Unity-specific advice: serialized fields, MonoBehaviour messages, organizing scripts. Its [naming tips](https://unity.com/how-to/naming-and-code-style-tips-c-scripting-unity) say Unity's own guides prefix private fields with `m_`, constants with `k_` and statics with `s_`, and that others use an underscore instead. We use the underscore, because it is what Microsoft's conventions and the .NET libraries use, and the Inspector hides it (`_maxSpeed` shows as "Max Speed"). + +Unity 6 compiles **C# 9.0**, and its default API compatibility level is .NET Standard 2.1 ([C# compiler](https://docs.unity3d.com/6000.0/Documentation/Manual/csharp-compiler.html), [.NET profile support](https://docs.unity3d.com/6000.0/Documentation/Manual/dotnet-profile-support.html)). So no file-scoped namespaces, `record` types in serialized data or `init` setters in Unity code. + +## Naming and file layout + +| Thing | Style | Example | +|---|---|---| +| Namespaces | `PascalCase`, by feature | `Atr.Teleoperation` | +| Classes, structs, enums, delegates | `PascalCase` | `HandTargetFollower`, `TeleopCommand` | +| Interfaces | `I` + `PascalCase` | `IRobotLink` | +| Methods, properties, events | `PascalCase` | `Engage()`, `LastCommand` | +| Public fields | `PascalCase` (prefer a property or a `[SerializeField] private` field) | `MaxSpeed` | +| Private and internal fields, including `[SerializeField]` | `_camelCase` | `_maxSpeed` | +| Private static fields | `s_camelCase` | `s_instanceCount` | +| Constants | `PascalCase` | `DefaultSpeed` | +| Parameters and locals | `camelCase` | `angleStep` | +| Type parameters | `T` + `PascalCase` | `TMessage` | +| Files | One class per file, named after it (Unity requires this for MonoBehaviours) | `HandTargetFollower.cs` | + +Keep your scripts under `Assets/Scripts//` with an assembly definition (`.asmdef`) per feature, so a change recompiles less and analyzers can be scoped to your code. + +## What the config does + +`configs/csharp-unity/.editorconfig` has four parts: + +1. **Formatting** (`IDE0055`): Allman braces, four spaces, `using System` first. Unity's serialized files (`.unity`, `.prefab`, `.asset`, `.meta` and others) are left exactly as Unity writes them. +2. **Code style**: braces always (`IDE0011`), explicit access modifiers (`IDE0040`), no `this.`, `using` outside the namespace, block-scoped namespaces (C# 9). +3. **Naming** (`IDE1006`): the table above, as `dotnet_naming_rule` entries ([naming rules reference](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/naming-rules)). +4. **Analyzer severities**: the rules above as warnings, plus these [Microsoft.Unity.Analyzers](https://github.com/microsoft/Microsoft.Unity.Analyzers) rules raised from their default "info" level, which is easy to miss: + +| Rule | Catches | +|---|---| +| `UNT0001` | Empty Unity message, such as an empty `Update()` that still costs a call every frame | +| `UNT0002` | Tag comparison with `==`; use `CompareTag` | +| `UNT0006` | A Unity message with the wrong signature, which Unity will never call | +| `UNT0007`, `UNT0008`, `UNT0029` | `??`, `?.` and `is null` on Unity objects, which skip Unity's destroyed-object check | +| `UNT0026` | `GetComponent` that allocates; use `TryGetComponent` | + +## How Unity 6 uses analyzers and .editorconfig + +This part confuses most people, so here is what the [Unity 6 manual](https://docs.unity3d.com/6000.0/Documentation/Manual/roslyn-analyzers.html) documents: + +- **Your editor is where you see the rules.** Visual Studio, Rider and VS Code (with the Unity extension) read `.editorconfig` for formatting, naming and severities. Microsoft.Unity.Analyzers ships with Visual Studio's Unity workload and the VS Code Unity extension. +- **Analyzers inside the Unity Editor** are DLLs that you add to the project. Put the DLL under `Assets/`, turn off every platform in its import settings and give it the asset label `RoslynAnalyzer` (case-sensitive). It applies to the assembly definition it sits in and to assemblies that reference it ([install an analyzer](https://docs.unity3d.com/6000.0/Documentation/Manual/install-existing-analyzer.html)). Diagnostics then show in the Unity Console. +- **Severities for those analyzers** can come from `Assets/Default.ruleset` or, as the manual says, from `dotnet_diagnostic..severity` lines in a root `.editorconfig` ([analyzer scope and rule sets](https://docs.unity3d.com/6000.0/Documentation/Manual/analyzer-scope-and-diagnostics.html)). A Unity issue reported on 2022.3 says the Editor ignored `.editorconfig` there, so check the Console after you change a severity; if nothing changes, use the rule set file. +- **The .NET code-style rules (IDE0055, IDE1006 and friends)** come with the .NET SDK and your editor, not with Unity. That is why CI runs `dotnet format`. + +If you want Microsoft.Unity.Analyzers in the Unity Console too, its maintainers describe the rule-set route in [issue 242](https://github.com/microsoft/Microsoft.Unity.Analyzers/issues/242); their stated target is Visual Studio, so treat the Console as a bonus. + +## Install and editor setup + +Install Unity 6 through Unity Hub, and the .NET 10 SDK (the current long-term support release, supported until Nov. 14, 2028, per the [.NET support policy](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core)) for `dotnet format`: + +```bash +# macOS +brew install --cask dotnet-sdk +# Ubuntu 24.04 (in the built-in Ubuntu feed) +sudo apt-get update && sudo apt-get install -y dotnet-sdk-10.0 +``` + +Editors (pick one, and set it under Unity's **Preferences > External Tools**): + +- **Rider** reads `.editorconfig` for naming styles and inspection severities ([Rider docs](https://www.jetbrains.com/help/rider/Using_EditorConfig.html)). +- **Visual Studio** with the "Game development with Unity" workload: reads `.editorconfig` and includes Microsoft.Unity.Analyzers. +- **VS Code** with the **Unity** extension (`visualstudiotoolsforunity.vstuc`), which installs C# Dev Kit and needs the Visual Studio Editor package 2.0.20 or later in Unity ([VS Code Unity guide](https://code.visualstudio.com/docs/other/unity)). + +For a Meta Quest project, start with Meta's [Unity setup guide](https://developers.meta.com/vr/documentation/unity/unity-project-setup/). + +## Adopt the config in a repo + +Run from the repo root (the folder that holds `Assets/`): + +```bash +curl -fsSLO https://raw.githubusercontent.com/ATR-Lab/dev-guidelines/master/configs/csharp-unity/.editorconfig +# Fix whitespace in your scripts once, in its own commit (works without project files) +dotnet format whitespace Assets --folder +``` + +Then open a script in your editor and fix the naming warnings it shows, one feature at a time. For a plain .NET project, add these to the `.csproj` so the build enforces the style too: + +```xml + + true + true + +``` + +`EnforceCodeStyleInBuild` runs the `IDE` rules during `dotnet build`, which is off by default ([MSBuild reference](https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props)); `GenerateDocumentationFile` lets `IDE0005` (unused usings) run on build. + +## Check in CI + +Unity projects: CI cannot build without a Unity license, but it can check formatting from the source files alone. + +```yaml +name: csharp +on: [push, pull_request] +jobs: + format: + runs-on: ubuntu-latest + container: mcr.microsoft.com/dotnet/sdk:10.0 + steps: + - uses: actions/checkout@v7 + - run: dotnet format whitespace Assets --folder --verify-no-changes +``` + +Plain .NET projects: check everything, including naming and analyzers. + +```yaml + - run: dotnet format --verify-no-changes --severity warn + - run: dotnet build -warnaserror +``` + +## Common mistakes + +- **`?.`, `??` or `is null` on a `GameObject` or component.** Unity objects override `==` to report destroyed objects as null; the C# operators skip that check. Use `if (_hand == null)`. +- **Public fields just to see them in the Inspector.** Use `[SerializeField] private` so other scripts cannot change them. +- **`GetComponent` in `Update()`.** Cache the reference in `Awake()`, or use `TryGetComponent`. +- **Unity's `.meta` files missing from Git.** Commit every `.meta` file; Unity uses them to keep references. Use the Unity template from [github/gitignore](https://github.com/github/gitignore) so `Library/`, `Temp/` and `Logs/` stay out. +- **Coordinate frames.** Unity is left-handed with Y up; ROS 2 is right-handed with Z up ([REP 103](https://www.ros.org/reps/rep-0103.html)). Convert in one place (the bridge) and say so in a comment. +- **Large assets in plain Git.** Track textures, models and audio with Git LFS (see [Git and GitHub](git-and-github.md#large-files-git-lfs)). + +## Related + +- Config file: [`configs/csharp-unity/.editorconfig`](../configs/csharp-unity/.editorconfig); example scripts and the check harness in [`examples/csharp-unity/`](../examples/csharp-unity/) +- [TeleBot-4R-XR](https://github.com/ATR-Lab/TeleBot-4R-XR): the lab's public XR interface for TeleBot-4R +- [ROS 2 packages](ros2-packages.md): the robot side of a Unity teleoperation setup +- `dotnet format` [reference](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format), analyzer [configuration options](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-options), Microsoft.Unity.Analyzers [rule list](https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/main/doc/index.md) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/documentation.md b/docs/documentation.md new file mode 100644 index 0000000..8ad4758 --- /dev/null +++ b/docs/documentation.md @@ -0,0 +1,120 @@ +# Documentation + +> **Read this when** you start a repo, finish a feature someone else will use or set up a repo so coding agents can work in it. + +Good code still needs a README that gets a new person from clone to running robot, docstrings that say what a function promises and an `AGENTS.md` that gives coding agents the same map. This page sets the standard for all three, plus diagrams and changelogs. The one thing to remember: if you had to explain something to a lab mate out loud, write it down in the repo. + +## Contents + +- [README standard](#readme-standard) +- [Docstrings per language](#docstrings-per-language) +- [AGENTS.md and CLAUDE.md](#agentsmd-and-claudemd) +- [Diagrams](#diagrams) +- [Changelogs](#changelogs) +- [Writing well](#writing-well) +- [Related](#related) + +## README standard + +Every repo has a `README.md` at its root. Start from [`templates/README.template.md`](../templates/README.template.md), which has these sections in this order: + +| Section | What it answers | +|---|---| +| Title and one sentence | What is this, and who is it for? | +| Status | Active, maintained or archived; which robot and software versions it was last tested with | +| Quick start | The fewest commands from a clean machine to something running | +| Requirements | Hardware (which robot, which sensors), OS and ROS 2 distribution, accounts or licenses | +| Usage | Common tasks, launch files, parameters | +| Safety | What can go wrong with the hardware and how to stop it (a `> [!WARNING]` block) | +| Troubleshooting | The errors people actually hit, with fixes | +| Repo map | Folders and what they hold | +| Contributing | Branches, style and checks (link to this repo) | +| License and citation | License, how to cite the paper if there is one, funding acknowledgment | + +Rules: + +- **Commands are copyable.** One command per line in a fenced block tagged with its language, no `$` prompts and a comment when a step is not obvious. Say which OS and version they are for. +- **Everything the lab must supply stays a `[bracketed placeholder]`** until someone confirms it: robot names and locations, people, accounts, grant numbers. Never guess them. +- **Images have alt text.** Describe what the image shows; decorative images get `alt=""`. +- **Keep it current.** A README that describes last year's setup is worse than none; update it in the same PR as the change. + +## Docstrings per language + +Document every public class, function and message field: what it does, its units and what it returns or throws. Put examples in tests, not in long comments. + +| Language | Format | Minimal example | +|---|---|---| +| C, C++ | Doxygen, as the ROS 2 style asks: `///` or `/** */` for docs, `//` for notes ([Doxygen docs](https://www.doxygen.nl/manual/docblocks.html)) | `/// Returns the command to send, in m/s and rad/s.` | +| Python | Google style: summary line, then `Args:`, `Returns:`, `Raises:` ([Google guide 3.8](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings)) | `"""Load frames from a JSON file."""` | +| Swift | `///` with a one-line summary, then `- Parameters:`, `- Returns:`, `- Throws:` ([Apple docs](https://developer.apple.com/documentation/xcode/writing-symbol-documentation-in-your-source-files)) | `/// Stops the robot.` | +| Kotlin | KDoc: `/** */` with `@param`, `@return`, `@throws` ([KDoc](https://kotlinlang.org/docs/kotlin-doc.html)) | `/** Adds a phrase to the queue. */` | +| C# | XML comments: `/// `, ``, `` ([XML documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/)) | `/// Stops following the hand.` | +| TypeScript, JavaScript | TSDoc or JSDoc: `/** */` with `@param`, `@returns`, `@throws` ([TSDoc](https://tsdoc.org/)) | `/** Formats a speed for display. */` | + +Every file in [`examples/`](../examples/) shows the format for its language and passes the linters. + +## AGENTS.md and CLAUDE.md + +Coding agents read a file at the repo root before they work. We keep one canonical file and point the others at it: + +- **`AGENTS.md` is the source of truth.** [AGENTS.md](https://agents.md/) is an open format, "a README for agents"; OpenAI's Codex reads it before doing any work ([Codex docs](https://learn.chatgpt.com/docs/agent-configuration/agents-md)). +- **`CLAUDE.md` holds exactly one line: `@AGENTS.md`.** Claude Code reads `CLAUDE.md` and imports files written as `@path` ([Claude Code memory docs](https://code.claude.com/docs/en/memory)), so both tools read the same instructions and nothing drifts apart. + +Start from [`templates/AGENTS.template.md`](../templates/AGENTS.template.md). It has the same sections as the `AGENTS.md` files in the lab's docs repos, plus safety rules: + +1. What the repo is, who uses it and what it is not. +2. **Repo map**: every folder and important file, one line each. +3. **Sources of truth**: where facts come from (datasheets, lockfiles, papers) and the rule that agents never invent names, results, versions or dates. +4. **Writing rules**: which configs from this repo apply, the commit message style, units and frames. +5. **How to check your work**: the exact format, lint, build and test commands. +6. **Safety rules**: what an agent must never do, such as run code that moves the robot or change speed limits without review. +7. **Open items**: every `[placeholder]` and who can fill it. +8. **Related repos**. + +Update `AGENTS.md` in the same PR as any change to commands, layout or rules. An outdated map misleads agents faster than no map. [AI-assisted development](ai-assisted-development.md) has the rules for using agents in lab repos. + +## Diagrams + +- **Mermaid in Markdown** for architecture, ROS 2 graphs and flows. GitHub renders `mermaid` code blocks ([creating diagrams](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams)), and the source stays diffable. Start each diagram with the lab theme line from [`AGENTS.md`](../AGENTS.md#writing-rules) so it reads in light and dark mode. +- **GitHub also renders ASCII STL files** in the browser (same page), which is handy for printed parts; see [Hardware and firmware](hardware.md). +- **Other drawings**: commit the editable source next to the exported image (`robot-wiring.drawio` and `robot-wiring.svg`), so the next person can edit it. +- Every diagram gets a sentence of text that says what it shows, for readers who cannot see it. + +## Changelogs + +Repos that publish releases keep a `CHANGELOG.md` in the [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) format: + +```markdown +## [Unreleased] + +## [1.4.0] - 2026-09-30 + +### Added + +- Dead-man switch timeout parameter (`deadman_timeout_s`). + +### Fixed + +- The follower no longer coasts after the operator releases the switch. +``` + +- Group entries under Added, Changed, Deprecated, Removed, Fixed and Security. +- Write for users of the software, not for its developers; link the PR for details. +- The dates follow the format's `YYYY-MM-DD`, an exception to the lab's "Sept. 30, 2026" style for prose. + +## Writing well + +- Lead with what the reader can do, then how. Short paragraphs, active voice, "you" for the reader. +- Define jargon the first time: "teleoperation (controlling a robot from a distance)". +- For bigger docs, separate tutorials, how-to guides, reference and explanation, as [Diátaxis](https://diataxis.fr/) describes. +- Good starting points: Write the Docs' [beginner's guide to docs](https://www.writethedocs.org/guide/writing/beginners-guide-to-docs/) and [RTFM? How to write a manual worth reading](https://opensource.com/business/15/5/write-better-docs). +- Lint Markdown with [`configs/markdown/.markdownlint.jsonc`](../configs/markdown/.markdownlint.jsonc); the VS Code extension is **markdownlint** (`DavidAnson.vscode-markdownlint`). + +## Related + +- [README template](../templates/README.template.md) and [AGENTS template](../templates/AGENTS.template.md) +- [AI-assisted development](ai-assisted-development.md) +- [Git and GitHub](git-and-github.md#releases-and-tags): releases and tags +- [Research standards](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/research/standards.md) in Getting started: papers, citations and data + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/git-and-github.md b/docs/git-and-github.md new file mode 100644 index 0000000..86ecde8 --- /dev/null +++ b/docs/git-and-github.md @@ -0,0 +1,191 @@ +# Git and GitHub + +> **Read this when** you start a branch, write a commit message, open or review a pull request, cut a release or wonder whether a file belongs in Git at all. + +We use [Git](https://git-scm.com/) for version control and [GitHub](https://github.com/ATR-Lab) to host the lab's code. This page is the workflow every lab repo follows: short-lived branches, clear commits, reviewed pull requests and tagged releases. The one thing to remember: nothing reaches the default branch without a pull request that someone else has read. + +## Contents + +- [Four habits](#four-habits) +- [Branches](#branches) +- [Commit messages](#commit-messages) +- [Pull requests and reviews](#pull-requests-and-reviews) +- [Issues](#issues) +- [Releases and tags](#releases-and-tags) +- [.gitignore](#gitignore) +- [Large files: Git LFS](#large-files-git-lfs) +- [Protected default branches](#protected-default-branches) +- [Never commit secrets](#never-commit-secrets) +- [Related](#related) + +## Four habits + +1. **Make sure the build passes before you commit.** + *Why:* broken code should not be committed; the next person to pull inherits it. +2. **Work on a feature branch.** + *Why:* changes on your branch don't affect anyone else, and a feature under development can be unstable. +3. **Pull the latest changes before you start.** + *Why:* your branch stays close to the default branch, so merging stays easy. +4. **Write meaningful, short commit messages.** + *Why:* a project's long-term success depends on its maintainability, and clear history makes every commit and pull request easier to review. + +## Branches + +We use the simple flow GitHub describes as [GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow): branch from the default branch, commit, open a pull request, review, merge, delete the branch. + +- The **default branch** (`main` in newer repos, `master` in older ones) always builds and passes its tests. +- Name branches `/` in lowercase with hyphens: `feature/deadman-switch`, `fix/imu-frame-id`, `docs/setup-guide`, `refactor/driver-timeouts`. +- Keep branches short-lived (days, not months). Merge the default branch into yours, or rebase onto it, when it moves. +- Delete the branch after the merge. Long-lived `develop` branches go stale; if a repo still has one, merge anything useful and delete it. + +> [!NOTE] +> The well-known ["A successful Git branching model"](https://nvie.com/posts/a-successful-git-branching-model/) (git-flow, 2010) now opens with a note from its author recommending simpler workflows, like GitHub flow, for software that ships continuously. Use git-flow only if a project really maintains several released versions at once. + +## Commit messages + +A good commit message tells other developers why a change happened. We follow the seven rules from Chris Beams' post [How to Write a Git Commit Message](https://cbea.ms/git-commit/): + +1. Separate subject from body with a blank line +2. Limit the subject line to 50 characters +3. Capitalize the subject line +4. Do not end the subject line with a period +5. Use the imperative mood in the subject line +6. Wrap the body at 72 characters +7. Use the body to explain what and why vs. how + +For example: + +```text +Limit teleop speed when the dead-man switch is released + +The follower kept the last velocity command for up to 0.5 s after the +operator let go, because the timeout started only after the next +message. Stop immediately on release and reset the limiter, so the +robot never coasts. + +Fixes #42 +``` + +**Conventional Commits (optional).** Repos that generate changelogs and version numbers automatically can use [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/): `fix:` for a bug fix (a PATCH release), `feat:` for a new feature (a MINOR release) and `!` after the type or a `BREAKING CHANGE:` footer for incompatible changes (a MAJOR release). The type prefix replaces rule 3 in those repos: + +```text +feat(teleop): add dead-man switch timeout parameter +``` + +Pick one style per repo and write it in the repo's `AGENTS.md`, so people and coding agents follow it. + +## Pull requests and reviews + +Open a pull request (PR) for every change to the default branch, even small ones. Open it as a draft early if you want feedback. + +**Author checklist:** + +- [ ] The PR does one thing. Split refactors and reformatting into their own PRs. +- [ ] The description says what changed, why and how you tested it (in simulation, on the robot or both). +- [ ] Formatters and linters pass locally; CI is green. +- [ ] Docs and `AGENTS.md` are updated if behavior, setup or commands changed. +- [ ] Changes to speed, force or workspace limits are called out as safety changes. +- [ ] Linked issues use a closing keyword (see [Issues](#issues)). + +**Review etiquette:** + +- Review within [agreed review time, for example two working days: lab to confirm]. If you can't, say so and suggest another reviewer. +- Comment on the code, not the person. Ask questions ("What happens if the robot loses Wi-Fi here?") rather than give orders. +- Mark optional suggestions as optional ("nit:"), so the author knows what blocks the merge. +- Approve when the change is correct and safe, even if you would have written it differently. +- Authors: reply to every comment, push fixes as new commits during review and let the reviewer resolve their own threads. + +A `.github/pull_request_template.md` file pre-fills the description for every PR ([GitHub docs](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository)); a `CODEOWNERS` file requests reviews from the right people automatically ([about code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners)). + +## Issues + +Track bugs, tasks and ideas as GitHub issues, one problem per issue, with steps to reproduce for bugs (robot, distribution, commit, what you expected, what happened). + +A PR closes an issue when its description or a commit message uses one of GitHub's [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue): `close`, `closes`, `closed`, `fix`, `fixes`, `fixed`, `resolve`, `resolves` or `resolved`, followed by the issue number (`Fixes #42`). It works only when the PR targets the default branch. + +## Releases and tags + +Version anything other people install or depend on (a driver, a library, an app build) with [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html): `MAJOR.MINOR.PATCH`. Increase MAJOR for incompatible API changes, MINOR for new features that keep compatibility and PATCH for bug fixes. Use a pre-release suffix for candidates: `1.4.0-rc.1`. + +```bash +git switch master # or main: the repo's default branch +git pull +git tag -a v1.4.0 -m "Release 1.4.0" +git push origin v1.4.0 +``` + +Then create a GitHub release from the tag ([about releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases)). Use the version as the release title, and let GitHub [draft the release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) from merged PRs; edit them for humans. Keep a `CHANGELOG.md` in the [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) format if people upgrade between versions (see [Documentation](documentation.md#changelogs)). + +In ROS 2 packages, the version in `package.xml` must match the tag. + +## .gitignore + +Every repo has a `.gitignore` that keeps build output, editor files and secrets out. Start from GitHub's [templates](https://github.com/github/gitignore) for your language (`ROS.gitignore`, `Python.gitignore`, `Unity.gitignore`, `Swift.gitignore`, `Android.gitignore`, `Node.gitignore`) and add what your project generates: + +```gitignore +# colcon workspaces +build/ +install/ +log/ +# Python +.venv/ +__pycache__/ +# Editors and OS +.vscode/* +!.vscode/settings.json +!.vscode/extensions.json +.idea/ +.DS_Store +# Secrets and local settings +.env +*.pem +``` + +More: the [Ignoring files](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#_ignoring) section of the Pro Git book, GitHub's [ignoring files](https://docs.github.com/en/get-started/git-basics/ignoring-files) guide and the [gitignore(5)](https://git-scm.com/docs/gitignore) manual page. + +## Large files: Git LFS + +GitHub warns about files larger than 50 MiB and blocks files larger than 100 MiB ([about large files](https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github)). Robot projects hit that quickly. Track these with [Git LFS](https://git-lfs.com/) from the first commit: + +- meshes and CAD exports (`*.stl`, `*.dae`, `*.obj`, `*.glb`, `*.step`) +- Unity assets (`*.fbx`, `*.psd`, `*.png` textures, `*.wav`) +- short sample recordings used by tests (`*.mcap`, `*.db3`) +- model weights (`*.pt`, `*.onnx`, `*.safetensors`) + +```bash +# Once per machine: macOS `brew install git-lfs`, Ubuntu `sudo apt install git-lfs` +git lfs install +# Once per repo, then commit .gitattributes +git lfs track "*.stl" "*.dae" "*.mcap" +git add .gitattributes +``` + +Full ROS bag recordings and training datasets are usually too large even for LFS. Keep them in [lab data storage location: lab manager to confirm] and put a README next to your code that says where they are and how to get them. + +## Protected default branches + +Repo admins protect the default branch of every active repo with [branch protection](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches) or a [ruleset](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets): + +- require a pull request with at least one approving review; +- require CI status checks to pass; +- block force-pushes and branch deletion. + +Ask [repo admin: lab manager to confirm] to set this up for a new repo. + +## Never commit secrets + +API keys, access tokens, passwords, Wi-Fi credentials, robot account logins and private keys never go into Git, not even in a private repo or a commit you plan to undo. + +- Keep them in environment variables or an ignored `.env` file, and commit a `.env.example` with placeholder values. +- GitHub's [push protection](https://docs.github.com/en/code-security/concepts/secret-security/push-protection) blocks pushes that contain known secret formats, and [secret scanning](https://docs.github.com/en/code-security/concepts/secret-security/secret-scanning) finds them in history. Don't bypass a block without asking. +- **If a secret reaches GitHub, revoke or rotate it first**, then tell [lab contact for security incidents: lab manager to confirm]. Rewriting history comes second ([removing sensitive data](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository)), because clones and forks may already have it. +- On shared robot computers, don't leave your personal GitHub credentials behind; log out or use [the lab's shared-machine account policy: lab manager to confirm]. + +## Related + +- [Documentation](documentation.md): READMEs, `AGENTS.md` and changelogs +- [AI-assisted development](ai-assisted-development.md): rules for coding agents working in these repos +- [ROS 2 packages](ros2-packages.md): what goes into a package and its PR checklist +- [Hardware and firmware](hardware.md): versioning CAD files and BOMs + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/hardware.md b/docs/hardware.md new file mode 100644 index 0000000..f55d05c --- /dev/null +++ b/docs/hardware.md @@ -0,0 +1,121 @@ +# Hardware and firmware + +> **Read this when** you design a part, write microcontroller firmware, wire a robot or print something, and want the next person to find, understand and rebuild it. + +Hardware work needs the same care as code: files someone else can open, versions you can tell apart and wiring you can trace without taking the robot apart. This page sets the conventions for CAD files, bills of materials, firmware projects, wiring and 3D prints. The one thing to remember: every physical thing the lab builds has its source files, a BOM and a photo in a repo. + +> [!WARNING] +> Power off and disconnect batteries before you change wiring, and follow the [lab safety guide](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/lab-rules/safety.md) for batteries, power tools, printers and moving robots. The lab's equipment list is in [Getting started: hardware](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/hardware/README.md). + +## Contents + +- [Repo layout for a hardware project](#repo-layout-for-a-hardware-project) +- [CAD files: naming and versions](#cad-files-naming-and-versions) +- [Bills of materials](#bills-of-materials) +- [Firmware projects](#firmware-projects) +- [Wiring: colors and labels](#wiring-colors-and-labels) +- [3D print files](#3d-print-files) +- [Related](#related) + +## Repo layout for a hardware project + +```text +gripper-mount/ +├── README.md # what it is, photo, how to build it, which robot it fits +├── cad/ # native CAD source and STEP exports +├── print/ # STL or 3MF files and print settings +├── electronics/ # KiCad project, wiring diagram +├── firmware/ # PlatformIO project +├── bom.csv # bill of materials +└── photos/ # finished build, wiring close-ups +``` + +Track binary files (STEP, STL, 3MF, images) with Git LFS from the first commit; see [Git and GitHub](git-and-github.md#large-files-git-lfs). + +## CAD files: naming and versions + +- **Names:** lowercase kebab-case, `--rev-`, for example `gripper-mount-bracket-rev-b.step`. The same name, with a different extension, for every format of the same part. +- **Revisions:** a letter for each version that was built or printed (A, B, C). Record what changed and why in the README ("Rev B: widened the cable slot to 8 mm"). +- **Formats:** commit the native file you edit (a Fusion `.f3d` archive, a SolidWorks part, a FreeCAD `.FCStd` file) *and* a STEP export, so someone without that CAD tool can still open the part. +- **Cloud CAD** (Fusion, Onshape) keeps its own history. Mark built versions there too (Fusion calls them [milestones](https://help.autodesk.com/view/fusion360/ENU/?guid=TPD-DESIGN-MILESTONES)), and put the share link in the README: [cloud CAD workspace: lab manager to confirm]. +- **Units:** millimeters in CAD, meters in robot descriptions (URDF uses SI units, [REP 103](https://www.ros.org/reps/rep-0103.html)). Say which one in the file name or README when it is not obvious. + +## Bills of materials + +Keep the BOM as a CSV file in the repo, so changes show up in diffs: + +```csv +item,qty,part,manufacturer,mpn,supplier,supplier_part,unit_cost_usd,price_checked,notes +1,4,M3x8 socket head cap screw,,,[supplier],[supplier part number],,,stainless steel +2,1,Dynamixel servo,ROBOTIS,[model],[supplier],[supplier part number],[cost],[date],[supply voltage] +3,1,LiPo battery 3S,[manufacturer],[model],[supplier],[supplier part number],[cost],[date],see safety guide +``` + +- One row per distinct part; quantities for one complete build. +- Manufacturer part numbers (MPN) identify a part better than supplier links, which change. +- Record the date you checked a price (`price_checked`); prices change. +- Link datasheets for anything electrical. +- Order through [parts ordering process: lab manager to confirm]. + +## Firmware projects + +We use [PlatformIO](https://docs.platformio.org/en/latest/core/quickstart.html) for new firmware, because it pins the board platform and library versions in one file and builds from the command line and in CI. The Arduino IDE is fine for quick experiments. + +```text +firmware/ +├── platformio.ini # board, framework, pinned platform and library versions +├── src/ # main.cpp and sources +├── include/ # headers +├── lib/ # project-private libraries +└── test/ # unit tests for `pio test` +``` + +- **Pin versions** in `platformio.ini` (`platform = @`, `lib_deps = @`) so a build from last year still builds today. +- **Style:** the same [`.clang-format`](../configs/c-cpp/.clang-format) as the rest of our C and C++, which also formats `.ino` sketches. Libraries published for the Arduino ecosystem follow Arduino's own [library style guide](https://docs.arduino.cc/learn/contributions/arduino-library-style-guide/) (camelCase functions), so they look like other Arduino libraries. +- **Checks:** `pio run` builds, `pio test` runs unit tests and `pio check` runs static analysis (cppcheck by default; clang-tidy is also supported, see the [static analysis docs](https://docs.platformio.org/en/latest/advanced/static-code-analysis/index.html)). +- **Secrets:** Wi-Fi passwords and tokens go in an ignored `include/secrets.h`; commit `include/secrets.example.h` with placeholders. +- **Arduino IDE projects:** [Arduino CLI](https://arduino.github.io/arduino-cli/latest/) builds them from the command line, and [Arduino Lint](https://arduino.github.io/arduino-lint/latest/) checks libraries and sketches. + +Install PlatformIO with its VS Code extension (**PlatformIO IDE**, `platformio.platformio-ide`), or on the command line: + +```bash +# macOS +brew install platformio +# Ubuntu 24.04: the installer script recommended by the PlatformIO docs +curl -fsSL -o get-platformio.py https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py +python3 get-platformio.py +``` + +## Wiring: colors and labels + +These are the lab's conventions. When a robot vendor's harness uses other colors, keep the vendor's and write them in the README. + +| Wire | Color | +|---|---| +| Positive supply (+V) | Red | +| Ground (0 V) | Black | +| Second supply voltage on the same robot | Another color, written on the label ("5 V", "12 V") | +| Signals and buses (UART, I2C, CAN, RS-485) | Any other colors, one color per signal, listed in the wiring diagram's legend | + +- **Label both ends of every cable** with the same ID, matching the wiring diagram (`W07: battery to fuse box`). Heat-shrink labels survive longer than tape. +- **Label connectors with their voltage** where two connectors could be swapped. +- **Fuse the battery** close to its terminal, and write the fuse rating in the diagram. +- **Draw it**: a KiCad schematic or a diagram (commit the source and a PDF or SVG export), plus a photo of the finished wiring. KiCad files are plain text ([file formats](https://dev-docs.kicad.org/en/file-formats/sexpr-intro/)), so they diff well; start from GitHub's [KiCad `.gitignore`](https://github.com/github/gitignore/blob/main/KiCad.gitignore). + +## 3D print files + +- Commit the CAD source, the STEP export and the printable file (STL or 3MF) with the same name and revision. +- Write the print settings in the README or a `print/settings.md`: printer, material, layer height, infill, supports, orientation and any post-processing. +- Note which robot and which revision a part fits. +- 3MF can store settings with the model; still write them down, because slicers read them differently. +- Resin printers and their chemicals have their own safety rules; read the safety guide before you use one. Book printers through [printer booking process: lab manager to confirm]. + +## Related + +- [Getting started: hardware](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/hardware/README.md): the lab's robots and equipment +- [Lab safety guide](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/lab-rules/safety.md) +- [C and C++](c-cpp.md): style and linters for firmware code +- [ROS 2 packages](ros2-packages.md): drivers and robot descriptions +- [multi-servo-dynamixel-library](https://github.com/ATR-Lab/multi-servo-dynamixel-library): a public lab library for Dynamixel servos + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/javascript-typescript.md b/docs/javascript-typescript.md new file mode 100644 index 0000000..fca6229 --- /dev/null +++ b/docs/javascript-typescript.md @@ -0,0 +1,139 @@ +# JavaScript and TypeScript + +> **Read this when** you build a web dashboard or a Node.js server (for example, a bridge between a robot and a browser) and want type safety, one formatter and a linter that catches async bugs. + +New code is TypeScript with a strict `tsconfig`, formatted by Prettier and linted by ESLint with typescript-eslint's type-aware rules. The one thing to remember: install the exact versions in `package-snippet.json`, because `npm install typescript` now gets TypeScript 7, which typescript-eslint does not support yet. + +## Contents + +- [The style we follow](#the-style-we-follow) +- [Naming and file layout](#naming-and-file-layout) +- [What the configs do](#what-the-configs-do) +- [Install and editor setup](#install-and-editor-setup) +- [Adopt the config in a repo](#adopt-the-config-in-a-repo) +- [Check in CI](#check-in-ci) +- [Common mistakes](#common-mistakes) +- [Related](#related) + +## The style we follow + +There is no single official JavaScript style guide, so we let the tools define it: + +- **Prettier** decides layout. We change two defaults: 100-character lines (to match our other languages) and single quotes. +- **ESLint** with [typescript-eslint](https://typescript-eslint.io/)'s `strict-type-checked` and `stylistic-type-checked` sets decides everything else. These rules use the TypeScript compiler, so they catch problems plain linting cannot: a promise nobody awaits, a value typed `any` leaking into your code, a condition that is always true. +- **TypeScript** in strict mode, plus the extra checks in `tsconfig.base.json`. + +Write plain JavaScript only where TypeScript is not an option (a quick script, a config file). The same ESLint config lints it, without the type-aware rules. + +## Naming and file layout + +| Thing | Style | Example | +|---|---|---| +| Variables, functions, methods | `camelCase` | `parseStatus()`, `batteryPercent` | +| Classes, interfaces, types, enums | `PascalCase` | `RobotStatus`, `StatusParseError` | +| Module-level constants | `UPPER_CASE` | `STALE_AFTER_MS` | +| Files | `kebab-case.ts` (follow your framework where it differs, for example React components) | `robot-status.ts` | +| Units in names or comments | Always | `linearSpeed: number; // m/s` | + +Keep source in `src/`, build output in `dist/` (ignored by Git and ESLint) and tests next to the code (`robot-status.test.ts`) or in `test/`. + +## What the configs do + +| File | What it sets | +|---|---| +| `eslint.config.mjs` | ESLint's flat config: recommended rules, typescript-eslint strict and stylistic sets with type information (`projectService`), `eqeqeq`, `consistent-type-imports`, then `eslint-config-prettier` last so no ESLint rule fights Prettier | +| `.prettierrc.json` | `printWidth: 100`, `singleQuote: true`; everything else is Prettier's default | +| `tsconfig.base.json` | `strict`, `noUncheckedIndexedAccess` (array lookups may be `undefined`), `exactOptionalPropertyTypes`, `noImplicitOverride`, `verbatimModuleSyntax`, ES2022 target, NodeNext modules | +| `package-snippet.json` | The `devDependencies` we tested together and `lint`, `format` and `typecheck` scripts | + +TypeScript 6 changed a default you will notice: it no longer loads every installed `@types` package ([TypeScript 6.0 announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/)). A Node.js server needs `"types": ["node"]` in its `tsconfig.json`. + +## Install and editor setup + +Install Node.js 24 (the active LTS release until Oct. 20, 2026; see the [Node.js release schedule](https://github.com/nodejs/Release#release-schedule)). ESLint 10 needs Node.js 20.19, 22.13 or 24 and later. + +macOS: + +```bash +brew install node@24 +# Versioned formulas are not linked by default; put this one on PATH +echo 'export PATH="$(brew --prefix node@24)/bin:$PATH"' >> ~/.zshrc +``` + +Ubuntu 24.04, with nvm (one of the options on the [Node.js download page](https://nodejs.org/en/download)): + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash +. "$HOME/.nvm/nvm.sh" # or open a new terminal +nvm install 24 +``` + +VS Code: install **ESLint** (`dbaeumer.vscode-eslint`) and **Prettier** (`esbenp.prettier-vscode`), then add to `.vscode/settings.json`: + +```json +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" } +} +``` + +## Adopt the config in a repo + +```bash +BASE=https://raw.githubusercontent.com/ATR-Lab/dev-guidelines/master/configs/javascript-typescript +curl -fsSLO "$BASE/eslint.config.mjs" +curl -fsSLO "$BASE/.prettierrc.json" +curl -fsSLO "$BASE/tsconfig.base.json" +# Add the tested versions as devDependencies +npm install --save-dev --save-exact @eslint/js@10.0.1 eslint@10.11.0 eslint-config-prettier@10.1.8 \ + globals@17.12.0 prettier@3.9.9 typescript@6.0.3 typescript-eslint@8.71.0 +``` + +Then point your `tsconfig.json` at the base file and copy the `scripts` block from [`package-snippet.json`](../configs/javascript-typescript/package-snippet.json): + +```json +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { "outDir": "dist", "lib": ["ES2022", "DOM"] }, + "include": ["src"] +} +``` + +Run `npm run format` once and commit the result on its own before you turn on the CI check. + +## Check in CI + +```yaml +name: web +on: [push, pull_request] +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run check # format:check, lint and typecheck +``` + +## Common mistakes + +- **Upgrading to TypeScript 7 before typescript-eslint supports it.** typescript-eslint 8.71 declares support for TypeScript below 6.1. Keep `typescript` pinned until its release notes say otherwise. +- **Floating promises.** `fetchStatus(url);` without `await`, `.catch()` or `void` loses errors silently. `no-floating-promises` flags it. +- **`any` from `JSON.parse`.** Type the result as `unknown` and check its fields, as `parseStatus()` in the example does. +- **`==` instead of `===`.** Blocked by `eqeqeq`. +- **Formatting rules in ESLint.** Leave layout to Prettier; `eslint-config-prettier` must stay the last entry. +- **Committing `node_modules/` or skipping the lockfile.** Ignore the first, commit `package-lock.json` (or `pnpm-lock.yaml`) and use `npm ci` in CI. + +## Related + +- Config files: [`configs/javascript-typescript/`](../configs/javascript-typescript/) and the example in [`examples/javascript-typescript/`](../examples/javascript-typescript/) +- [Documentation](documentation.md#docstrings-per-language): TSDoc comments +- [atr-dashboard](https://github.com/ATR-Lab/atr-dashboard): one of the lab's public web projects +- typescript-eslint [shared configs](https://typescript-eslint.io/users/configs/), ESLint [configuration files](https://eslint.org/docs/latest/use/configure/configuration-files), Prettier [options](https://prettier.io/docs/options), TypeScript [tsconfig reference](https://www.typescriptlang.org/tsconfig/) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/kotlin.md b/docs/kotlin.md new file mode 100644 index 0000000..ace9550 --- /dev/null +++ b/docs/kotlin.md @@ -0,0 +1,162 @@ +# Kotlin + +> **Read this when** you build an Android app, such as an app for the Pepper robot or a Wear OS app, and want Android Studio, ktlint and detekt to agree. + +We follow the Kotlin coding conventions and the Android Kotlin style guide. ktlint, in its `android_studio` code style, checks and fixes layout; detekt finds code smells. The one thing to remember: ktlint reads its settings from `.editorconfig`, and so does Android Studio, which is why the `android_studio` style keeps the two in agreement. + +## Contents + +- [The style we follow](#the-style-we-follow) +- [Naming and file layout](#naming-and-file-layout) +- [What the configs do](#what-the-configs-do) +- [Install the tools](#install-the-tools) +- [Editor setup](#editor-setup) +- [Adopt the config in a repo](#adopt-the-config-in-a-repo) +- [Check in CI](#check-in-ci) +- [Common mistakes](#common-mistakes) +- [Related](#related) + +## The style we follow + +- **[Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html)** (JetBrains): four-space indentation, naming, trailing commas at the declaration site, idiomatic use of the language. +- **[Android Kotlin style guide](https://developer.android.com/kotlin/style-guide)** (Google): the same basics plus a 100-character column limit and no wildcard imports of any kind. + +The two guides agree on almost everything. Where they differ, we take the Android guide, because most of our Kotlin runs on Android. + +**Why ktlint's `android_studio` code style.** ktlint has three [code styles](https://ktlint.github.io/ktlint/1.8.0/rules/code-styles/): `ktlint_official` (the default, stricter than either guide), `intellij_idea` (based on the Kotlin coding conventions) and `android_studio` (based on the Android Kotlin style guide). We use `android_studio` because it matches both our guide and Android Studio's own formatter, so **Code > Reformat Code** and ktlint do not undo each other. Its default line limit is 100, as in the Android guide. + +## Naming and file layout + +| Thing | Style | Example | +|---|---|---| +| Packages | lowercase, the lab's web domain reversed | `edu.kent.cs.atr.pepper` (from `atr.cs.kent.edu`) | +| Classes, interfaces, objects | `PascalCase` | `SpeechQueue` | +| Functions, properties, variables | `camelCase` | `enqueue()`, `isSpeaking` | +| Constants (`const val`, top-level `val` of immutable data) | `UPPER_SNAKE_CASE` | `DEFAULT_CAPACITY` | +| Compose functions | `PascalCase` nouns, like types | `HeartRateCard()` | +| Backing properties | leading underscore | `_state` behind `state` | +| Files | Named after the single top-level class, or `PascalCase` for a group of top-level functions | `SpeechQueue.kt` | + +Android projects follow the Gradle layout (`app/src/main/kotlin/...`, `app/src/test/`, `app/src/androidTest/`). Put the package path in the folder path; detekt checks that they match. + +## What the configs do + +**`configs/kotlin/.editorconfig`** (read by ktlint and Android Studio): + +- `ktlint_code_style = android_studio`, `indent_size = 4`, `max_line_length = 100`. +- `ktlint_function_naming_ignore_when_annotated_with = Composable`, so Compose functions may start with a capital letter. +- The three `ij_kotlin_*import*` settings that stop Android Studio from writing wildcard imports, from ktlint's [IntelliJ IDEA guide](https://ktlint.github.io/ktlint/1.8.0/rules/configuration-intellij-idea/). +- `ij_kotlin_allow_trailing_comma = true`: trailing commas on declarations, as the Kotlin conventions encourage. + +**`configs/kotlin/detekt.yml`** lists only the changes from detekt's defaults, so run it with `--build-upon-default-config`: + +- `MaxLineLength` at 100, to match ktlint. +- `WildcardImport` with no exceptions (the default allows `java.util.*`; the Android guide does not). +- Compose-friendly settings: `FunctionNaming`, `LongParameterList` and `MagicNumber` ignore `@Composable` functions; `UnusedPrivateMember` ignores `@Preview` functions. +- `ForbiddenComment` allows `TODO` and `FIXME` and blocks only `STOPSHIP`. + +We do not use the `detekt-formatting` plugin: it wraps ktlint 0.50.0, an old release whose rules differ from ktlint 1.8. Run ktlint itself instead. + +> [!NOTE] +> detekt 1.23.8 is the current stable release (Feb. 21, 2025). Its [compatibility table](https://detekt.dev/docs/1.23.8/introduction/compatibility/) lists Kotlin 2.0.21, so code that uses newer language features may not parse. detekt 2.0 is in alpha; check its release notes before you switch. + +## Install the tools + +macOS: + +```bash +brew install ktlint detekt +``` + +Ubuntu 24.04 (both tools need Java; we tested with Java 21): + +```bash +sudo apt install openjdk-21-jre-headless +# ktlint: a self-contained launcher from the official GitHub release +curl -fsSLO https://github.com/ktlint/ktlint/releases/download/1.8.0/ktlint +chmod +x ktlint && sudo mv ktlint /usr/local/bin/ +# detekt: the command-line jar from the official GitHub release +curl -fsSLO https://github.com/detekt/detekt/releases/download/v1.23.8/detekt-cli-1.23.8-all.jar +java -jar detekt-cli-1.23.8-all.jar --version +``` + +## Editor setup + +**Android Studio.** + +1. Put `.editorconfig` at the repo root. Android Studio, like IntelliJ IDEA, reads it when it formats code, including the `ij_kotlin_*` settings. +2. Add `kotlin.code.style=official` to `gradle.properties` so every machine starts from the Kotlin conventions ([migration guide](https://kotlinlang.org/docs/code-style-migration-guide.html)). +3. If a project was created long ago, delete `.idea/codeStyles/`, as ktlint's IntelliJ IDEA guide advises, so old XML code-style settings cannot compete with `.editorconfig`. +4. Optional: the community [ktlint plugin](https://plugins.jetbrains.com/plugin/15057-ktlint) shows ktlint results while you type. + +**VS Code** is not a good fit for Android work; use Android Studio and run ktlint and detekt in the terminal. + +## Adopt the config in a repo + +Run from the repo root: + +```bash +BASE=https://raw.githubusercontent.com/ATR-Lab/dev-guidelines/master/configs/kotlin +# No .editorconfig yet? Download ours. Already have one? Copy our [*.{kt,kts}] section into it instead. +curl -fsSLO "$BASE/.editorconfig" +mkdir -p config/detekt && curl -fsSL -o config/detekt/detekt.yml "$BASE/detekt.yml" +ktlint --format "**/src/**/*.kt" "**/*.kts" # reformat once, in its own commit +detekt --input . --config config/detekt/detekt.yml --build-upon-default-config +``` + +To run detekt from Gradle instead, add its plugin to the app module ([detekt Gradle docs](https://detekt.dev/docs/1.23.8/gettingstarted/gradle/)): + +```kotlin +plugins { + id("io.gitlab.arturbosch.detekt") version "1.23.8" +} + +detekt { + config.setFrom("$rootDir/config/detekt/detekt.yml") + buildUponDefaultConfig = true +} +``` + +Then run `./gradlew detekt`. ktlint has only third-party Gradle plugins; the command line is enough for CI. + +## Check in CI + +```yaml +name: kotlin +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: 21 + - name: ktlint + run: | + curl -fsSLO https://github.com/ktlint/ktlint/releases/download/1.8.0/ktlint + chmod +x ktlint + ./ktlint "**/src/**/*.kt" "**/*.kts" + - name: detekt + run: | + curl -fsSLO https://github.com/detekt/detekt/releases/download/v1.23.8/detekt-cli-1.23.8-all.jar + java -jar detekt-cli-1.23.8-all.jar --input . --config config/detekt/detekt.yml --build-upon-default-config +``` + +## Common mistakes + +- **Running `ktlint --format` with a different code style than Android Studio.** Keep `ktlint_code_style = android_studio` in `.editorconfig`, or the two formatters take turns rewriting your files. +- **Wildcard imports from Android Studio's auto-import.** The `.editorconfig` settings above stop them; ktlint and detekt both flag any that slip through. +- **Catching `Exception` and doing nothing.** detekt flags `TooGenericExceptionCaught` and `SwallowedException`; on a robot, a swallowed error hides why the robot stopped. +- **Blocking the main thread** with robot SDK calls that wait for the robot. Run them in a coroutine on a background dispatcher. +- **Committing `local.properties`, `build/` or `.gradle/`.** Use the Android template from [github/gitignore](https://github.com/github/gitignore). + +## Related + +- Config files: [`configs/kotlin/`](../configs/kotlin/) and the example in [`examples/kotlin/`](../examples/kotlin/) +- [PepperLLMDemo](https://github.com/ATR-Lab/PepperLLMDemo): a public lab Android app written in Kotlin +- [Documentation](documentation.md#docstrings-per-language): KDoc comments +- ktlint [rules](https://ktlint.github.io/ktlint/1.8.0/rules/standard/), detekt [rule sets](https://detekt.dev/docs/1.23.8/rules/complexity/) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/other-languages.md b/docs/other-languages.md new file mode 100644 index 0000000..0614c33 --- /dev/null +++ b/docs/other-languages.md @@ -0,0 +1,33 @@ +# Other languages + +> **Read this when** you write code in a language the lab uses only now and then (Arduino sketches, Java, MATLAB, Solidity or Ada) and need the style guide to follow. + +We don't keep configs for these languages. Follow the official guide below, and fall back on the general rules in this repo: [Git and GitHub](git-and-github.md), [Documentation](documentation.md) and the shared [`.editorconfig`](../configs/editorconfig/.editorconfig). If a language becomes common in lab projects, open an issue so we can add a page and a tested config. + +## Style guides + +| Language | Guide to follow | Notes | +|---|---|---| +| Arduino | [Arduino Style Guide for Creating Libraries](https://docs.arduino.cc/learn/contributions/arduino-library-style-guide/) (APIs) and [Arduino Style Guide for Writing Content](https://docs.arduino.cc/learn/contributions/arduino-writing-style-guide/) (examples and sketches) | Check libraries and sketches with [Arduino Lint](https://arduino.github.io/arduino-lint/latest/). For firmware that is not an Arduino library, use our [C and C++](c-cpp.md) setup and [PlatformIO](hardware.md#firmware-projects) | +| Java | [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) | Oracle's [Code Conventions for the Java Programming Language](https://www.oracle.com/java/technologies/javase/codeconventions-contents.html) are archived (last revised in 1999). For Android, write new code in [Kotlin](kotlin.md) | +| MATLAB | MathWorks' [MATLAB Coding Guidelines](https://github.com/mathworks/MATLAB-Coding-Guidelines) (version 1.0, 2025) | For MATLAB R2025a and later. The repo's `codeAnalyzerConfiguration.json` lets the MATLAB Code Analyzer flag rule violations in the Editor and in its report; the guidelines link MathWorks' instructions for setting it up | +| Solidity | [Solidity style guide](https://docs.soliditylang.org/en/latest/style-guide.html) in the official Solidity docs | | +| Ada | The [Ada 95 Quality and Style Guide](https://www.adaic.org/resources/add_content/docs/95style/html/cover.html) (Ada Information Clearinghouse) | Format with `gnatpp`, the GNAT pretty printer ([GNAT user's guide](https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/gnat_utility_programs.html)); the language reference is the [Ada 2022 Reference Manual](http://www.ada-auth.org/standards/ada22.html) | + +## What changed from the old list + +The old version of this repo linked guides that have moved or no longer apply: + +- **C:** the Indian Hill C style guide is replaced by the ROS 2 C style (PEP 7 based), which our [C and C++](c-cpp.md) page and `.clang-format` cover. +- **C++:** the Google C++ Style Guide is still the base, with the ROS 2 changes described in [C and C++](c-cpp.md). +- **JavaScript:** JavaScript Standard Style is replaced by our [JavaScript and TypeScript](javascript-typescript.md) setup (ESLint, typescript-eslint and Prettier). +- **Python:** the Google Python Style Guide now applies to docstrings, with PEP 8 and Ruff for everything else; see [Python](python.md). +- **Arduino:** the old `arduino.cc/en/Reference/StyleGuide` link now opens the Arduino home page; the guides above replaced it. + +## Related + +- [Language matrix](../README.md#language-matrix): the six languages with tested configs +- [Hardware and firmware](hardware.md): microcontroller projects +- [Google style guides](https://google.github.io/styleguide/): the index of Google's guides for other languages + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/python.md b/docs/python.md new file mode 100644 index 0000000..1a6afd2 --- /dev/null +++ b/docs/python.md @@ -0,0 +1,208 @@ +# Python + +> **Read this when** you write Python for a ROS 2 node, a machine learning experiment (PyTorch, LeRobot) or a lab tool, and want one setup that passes both our checks and `colcon test`. + +We follow PEP 8, write Google-style docstrings and let Ruff do both formatting and linting, with mypy for types and uv for environments. The one thing to remember: our Ruff settings match the ROS 2 linters (99-character lines, single quotes), so the same code passes in a ROS 2 package, as long as `ament_pep257` is told to use the Google docstring convention. + +## Contents + +- [The style we follow](#the-style-we-follow) +- [Naming and file layout](#naming-and-file-layout) +- [Ruff: which rules and why](#ruff-which-rules-and-why) +- [Types: mypy and pyright](#types-mypy-and-pyright) +- [Environments with uv](#environments-with-uv) +- [Inside ROS 2 packages](#inside-ros-2-packages) +- [Install and editor setup](#install-and-editor-setup) +- [Adopt the config in a repo](#adopt-the-config-in-a-repo) +- [Check in CI](#check-in-ci) +- [Common mistakes](#common-mistakes) +- [Related](#related) + +## The style we follow + +- **[PEP 8](https://peps.python.org/pep-0008/)** for layout and naming. PEP 8 allows lines up to 99 characters when a team agrees, and we do, because that is what `ament_flake8` checks in ROS 2 packages. +- **Single quotes** for strings unless the string contains one. The [ROS 2 code style](https://docs.ros.org/en/jazzy/The-ROS2-Project/Contributing/Code-Style-Language-Versions.html) picks single quotes, and PEP 8 makes no recommendation. Docstrings keep triple double quotes, as [PEP 257](https://peps.python.org/pep-0257/) says. +- **[Google Python Style Guide, section 3.8](https://google.github.io/styleguide/pyguide.html#s3.8-comments-and-docstrings)** for docstrings: a one-line summary, a blank line, then `Args:`, `Returns:` and `Raises:` sections. + +```python +def summarize(frames: Sequence[Frame]) -> EpisodeSummary: + """Summarize one recorded episode. + + Args: + frames: Samples in time order. Every frame must have the same number of joints. + + Returns: + The frame count, the duration, the range of each joint and the fastest joint speed. + + Raises: + ValueError: If there are fewer than two frames or the joint counts differ. + """ +``` + +## Naming and file layout + +| Thing | Style | Example | +|---|---|---| +| Modules and packages | `snake_case`, short | `teleop_episodes/stats.py` | +| Classes and exceptions | `CamelCase` | `EpisodeSummary`, `StatusParseError` | +| Functions, methods, variables | `snake_case` | `load_frames()`, `max_speed` | +| Constants | `UPPER_CASE` | `DEFAULT_RATE_HZ` | +| Private names | leading underscore | `_clamp()` | + +Outside ROS 2, use the `src/` layout that `uv init --package` creates, with tests in `tests/`: + +```text +my_tool/ +├── pyproject.toml # dependencies, mypy and pyright settings +├── ruff.toml # from configs/python/ +├── uv.lock # commit it: it pins every dependency +├── src/my_tool/ +└── tests/ +``` + +ROS 2 Python packages use the `ament_python` layout instead; see [ROS 2 packages](ros2-packages.md#ament_python-layout). + +## Ruff: which rules and why + +[Ruff](https://docs.astral.sh/ruff/) replaces Black, isort, flake8 and pydocstyle with one fast tool. Ruff 0.16 changed its default rule set, so `configs/python/ruff.toml` lists every family it wants: + +| Rules | What they catch | Why we want them | +|---|---|---| +| `E`, `W` | pycodestyle errors and warnings | The same checks flake8 runs in ROS 2 packages | +| `F` | Unused imports, undefined names | Real bugs | +| `I` | Import order | Sorted like the "google" order `ament_flake8` checks | +| `N` | PEP 8 naming | Consistent names | +| `D` | Docstrings, Google convention | Public classes and functions need one (module and package docstrings are optional, as in ROS 2) | +| `UP` | Old syntax for the target version | `list[int]`, not `List[int]` | +| `B` | flake8-bugbear | Mutable default arguments, loop variable capture | +| `A`, `C4` | Shadowed built-ins, clumsy comprehensions | Also run by `ament_flake8` | +| `SIM` | Code that can be simpler | Fewer nested `if`s | +| `NPY` | Deprecated NumPy calls | Common in ML code | +| `RUF` | Ruff's own checks | For example, `itertools.pairwise` over `zip(x, x[1:])` | + +We leave out the quote (`Q`) and trailing-comma (`COM`) rules, because the formatter already handles them and the [Ruff docs](https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) list them as conflicting. For the same reason we cannot enforce the ROS 2 preference for one import per line: Ruff's `force-single-line` setting is incompatible with its formatter. + +## Types: mypy and pyright + +- **mypy** (`strict = true`) is the gate in CI. Add type hints to every function you write; `mypy --strict` rejects untyped definitions. +- **pyright** runs inside VS Code as Pylance. We set it to `standard` so the editor stays quiet enough to be useful; mypy catches the rest. +- For a library without type hints (common in robot SDKs), add it to the `[[tool.mypy.overrides]]` list in `pyproject.toml` rather than turning checks off for the whole project. + +## Environments with uv + +[uv](https://docs.astral.sh/uv/) creates the virtual environment, installs dependencies and writes `uv.lock`, so everyone in the project gets the same versions. + +```bash +uv init --package my_tool # new project with src/ layout +cd my_tool +uv add numpy # runtime dependency +uv add --dev ruff==0.16.9 mypy==2.3.1 pytest +uv run pytest # runs inside the project's environment +uvx ruff check . # runs a tool without adding it to the project +``` + +Commit `pyproject.toml` and `uv.lock`; never commit `.venv/`. For ML projects, follow the library's own install guide (PyTorch builds differ by CUDA version) and let uv record the result in the lockfile. + +## Inside ROS 2 packages + +ROS 2 packages are checked by `ament_flake8` and `ament_pep257` when you run `colcon test`. Our Ruff settings agree with `ament_flake8`'s configuration (99 columns, Google import order, single quotes), and we tested that the example code passes both. Two things need attention: + +1. **Docstrings.** `ament_pep257`'s default "ament" convention rejects Google-style `Args:` sections. Tell it to use the Google convention in `test/test_pep257.py` (the paths must come before the options): + + ```python + # test/test_pep257.py: the file ros2 pkg create generates, with the argv changed + from ament_pep257.main import main + import pytest + + # Docstrings stay optional, as in the default "ament" convention. + OPTIONAL_DOCSTRINGS = ['D100', 'D101', 'D102', 'D103', 'D104', 'D105', 'D106', 'D107'] + + + @pytest.mark.linter + @pytest.mark.pep257 + def test_pep257(): + rc = main(argv=['.', 'test', '--convention', 'google', '--add-ignore', *OPTIONAL_DOCSTRINGS]) + assert rc == 0, 'Found code style errors / warnings' + ``` + + Keeping docstrings optional means the test files that `ros2 pkg create` generates still pass. We ran this file through Ruff, `ament_flake8` and pytest on ROS 2 Jazzy. + +2. **Slices.** Ruff's formatter writes `ham[lower + offset : upper]` with spaces around the colon, and `ament_flake8` reports that as `E203`. Put the bound in a variable (`end = lower + offset`) or add `# noqa: E203`. + +**Virtual environments and ROS 2.** ROS 2 is built against the system Python, so do not point a ROS 2 workspace at a uv-managed interpreter. The ROS 2 guide on [using Python packages](https://docs.ros.org/en/jazzy/How-To-Guides/Using-Python-Packages.html) prefers rosdep keys; when a package is only on PyPI, it recommends a virtual environment made from the system interpreter, with a `COLCON_IGNORE` file inside so colcon skips it: + +```bash +# Ubuntu 24.04 with ROS 2 Jazzy; run from the workspace root +uv venv --python /usr/bin/python3 --system-site-packages venv # also sees packages rosdep installed with apt +touch venv/COLCON_IGNORE +source venv/bin/activate +uv pip install +``` + +## Install and editor setup + +macOS or Linux: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh # or, on macOS: brew install uv +uv tool install ruff@0.16.9 +uv tool install mypy@2.3.1 +``` + +VS Code: install **Python** (`ms-python.python`), **Pylance** (`ms-python.vscode-pylance`), **Ruff** (`charliermarsh.ruff`) and **Mypy Type Checker** (`ms-python.mypy-type-checker`). Then format on save with Ruff, as its [README](https://github.com/astral-sh/ruff-vscode) shows: + +```json +{ + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { "source.fixAll": "explicit", "source.organizeImports": "explicit" } + } +} +``` + +## Adopt the config in a repo + +```bash +BASE=https://raw.githubusercontent.com/ATR-Lab/dev-guidelines/master/configs +curl -fsSLO "$BASE/python/ruff.toml" +curl -fsSL "$BASE/python/pyproject-snippet.toml" # read it, then paste the tables into pyproject.toml +uvx ruff@0.16.9 format . # reformat once, in its own commit +uvx ruff@0.16.9 check --fix . +``` + +## Check in CI + +```yaml +name: python +on: [push, pull_request] +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: astral-sh/setup-uv@v10 + - run: uv sync --dev + - run: uv run ruff format --check . + - run: uv run ruff check . + - run: uv run mypy . + - run: uv run pytest +``` + +## Common mistakes + +- **Double quotes everywhere.** Ruff fixes them on format; `ament_flake8` fails on them (`Q000`) if you skip the formatter. +- **`pip install` into the system Python on Ubuntu 24.04.** Ubuntu marks its Python as externally managed ([PEP 668](https://peps.python.org/pep-0668/)), so pip refuses. Use uv for projects and rosdep for ROS 2 dependencies. +- **Mutable default arguments** (`def f(items=[])`). Ruff's `B006` catches them. +- **Notebooks as the only copy of the code.** Move code you reuse into a module with tests; keep notebooks for exploration. +- **Unpinned tools.** Pin Ruff and mypy in `pyproject.toml` so a new release does not fail CI overnight. + +## Related + +- [ROS 2 packages](ros2-packages.md): `ament_python` layout and linters +- [C and C++](c-cpp.md): the C++ side of a ROS 2 workspace +- [Documentation](documentation.md#docstrings-per-language): docstrings in every language +- Config files: [`configs/python/`](../configs/python/) and the example in [`examples/python/`](../examples/python/) +- Ruff [rules](https://docs.astral.sh/ruff/rules/), mypy [configuration](https://mypy.readthedocs.io/en/stable/config_file.html), uv [projects guide](https://docs.astral.sh/uv/guides/projects/) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/ros2-packages.md b/docs/ros2-packages.md new file mode 100644 index 0000000..afbc95c --- /dev/null +++ b/docs/ros2-packages.md @@ -0,0 +1,183 @@ +# ROS 2 packages + +> **Read this when** you create or review a ROS 2 package: its name, folder layout, linters, units, coordinate frames, launch files and parameters. + +This page covers how we structure ROS 2 packages so that any lab member can build, test and reuse them. For ROS 2 itself (what nodes and topics are, installing a distribution, the command line), read [All things ROS 2](https://github.com/ATR-Lab/all-things-ros2). The one thing to remember: SI units, REP 105 frames and `colcon test` passing before you open a pull request. + +## Contents + +- [Naming packages](#naming-packages) +- [Package layout](#package-layout) +- [Linters with ament_lint_auto](#linters-with-ament_lint_auto) +- [Units and coordinates (REP 103)](#units-and-coordinates-rep-103) +- [Coordinate frames (REP 105)](#coordinate-frames-rep-105) +- [Launch files](#launch-files) +- [Parameters](#parameters) +- [Topics, services and interfaces](#topics-services-and-interfaces) +- [Checklist before a pull request](#checklist-before-a-pull-request) +- [Related](#related) + +## Naming packages + +[REP 144](https://www.ros.org/reps/rep-0144.html) sets the rules. A package name: + +- uses only lowercase letters, digits and underscores, and starts with a letter; +- never has two underscores in a row, and has at least two characters; +- should not contain `ros` (every package is a ROS package); +- should have a prefix when only one organization uses it. + +REP 144 also lists common suffixes: `_msgs` (interfaces), `_driver`, `_description` (URDF and meshes), `_bringup` and `_launch`. So the packages for a robot arm might be `my_arm_driver`, `my_arm_description` (its URDF) and `my_arm_bringup` (the launch files that start it). + +## Package layout + +### ament_cmake layout + +For C++ nodes (and packages with only launch or config files). `examples/c-cpp/teleop_velocity_limiter/` follows it. + +```text +teleop_velocity_limiter/ +├── CMakeLists.txt +├── package.xml +├── include/teleop_velocity_limiter/ # public headers +├── src/ # node and library sources +├── test/ # gtest files +├── launch/ # *.launch.py, *.launch.xml, *.launch.yaml +└── config/ # parameter YAML files +``` + +### ament_python layout + +For Python nodes. `ros2 pkg create --build-type ament_python ` generates it, including three linter tests ([tutorial](https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Creating-Your-First-ROS2-Package.html)). + +```text +my_python_pkg/ +├── package.xml +├── setup.py # entry points for each node; install launch/ and config/ via data_files +├── setup.cfg +├── resource/my_python_pkg +├── my_python_pkg/ # the Python module, with __init__.py +├── test/ # test_copyright.py, test_flake8.py, test_pep257.py and your tests +├── launch/ +└── config/ +``` + +Workspace repos (like the lab's [go2_ws](https://github.com/ATR-Lab/go2_ws)) keep packages under `src/`. Repos with one package put it at the root, and users clone them into their workspace's `src/`. Either way, never commit `build/`, `install/` or `log/`. + +## Linters with ament_lint_auto + +`colcon test` runs the linters a package declares. The usual set comes from `ament_lint_common`: copyright, cppcheck, cpplint, flake8, lint_cmake, pep257, uncrustify and xmllint. Clang-format and clang-tidy are opt-in. Declare them in `package.xml`: + +```xml +ament_lint_auto +ament_lint_common +ament_cmake_clang_format +``` + +And in `CMakeLists.txt`, inside `if(BUILD_TESTING)` and before `ament_package()` ([ament_cmake guide](https://docs.ros.org/en/jazzy/How-To-Guides/Ament-CMake-Documentation.html)): + +```cmake +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + # The lab formats C++ with clang-format; uncrustify disagrees with it (see docs/c-cpp.md). + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify) + ament_lint_auto_find_test_dependencies() +endif() +``` + +Notes: + +- `ros2 pkg create` adds `set(ament_cmake_copyright_FOUND TRUE)` and `set(ament_cmake_cpplint_FOUND TRUE)` to new CMake packages, which skip those two linters until you add license headers. Remove the lines once your files have headers. +- For Python packages, run `ament_pep257` with the Google docstring convention; the change to `test/test_pep257.py` is in [Python](python.md#inside-ros-2-packages). +- Run everything before you push: + + ```bash + colcon build --packages-select my_pkg + colcon test --packages-select my_pkg + colcon test-result --verbose + ``` + +## Units and coordinates (REP 103) + +[REP 103](https://www.ros.org/reps/rep-0103.html) makes robot software composable. The rules we see broken most often: + +| Quantity | Unit | +|---|---| +| Length | meter | +| Angle | radian | +| Time | second | +| Mass, force | kilogram, newton | +| Linear and angular velocity | meters per second, radians per second | +| Temperature | degree Celsius | + +- All frames are **right-handed**. On a robot body, **x points forward, y left, z up**. +- Outdoors, map frames use **ENU** (east, north, up). +- Camera image frames get an `_optical` suffix (z forward, x right, y down) next to the body-style frame. + +Convert units at the edge of your code (the driver or the UI), never in the middle. When a vendor SDK uses degrees or millimeters, convert right where you call it and put the unit in the variable name until then (`angle_deg`). + +## Coordinate frames (REP 105) + +[REP 105](https://www.ros.org/reps/rep-0105.html) names the frames of a mobile robot. `map` is the parent of `odom`, and `odom` is the parent of `base_link`. + +```mermaid +%%{init: {'theme':'base','themeVariables':{'primaryColor':'#003976','primaryTextColor':'#FFFFFF','primaryBorderColor':'#00295F','secondaryColor':'#EFAB00','secondaryTextColor':'#1B2533','tertiaryColor':'#F3F6FA','tertiaryTextColor':'#1B2533','lineColor':'#2C8ECD','textColor':'#1B2533','edgeLabelBackground':'#F3F6FA','clusterBkg':'#F3F6FA','clusterBorder':'#D6DEE8','titleColor':'#003976'}}}%% +flowchart LR + earth["earth (optional, ECEF)"] --> map["map: world-fixed, no drift, may jump"] + map --> odom["odom: continuous, drifts over time"] + odom --> base["base_link: rigidly attached to the robot"] + base --> sensors["sensor frames: laser, camera_link, imu_link"] +``` + +- `odom` → `base_link` comes from odometry (wheel encoders, visual or leg odometry). It is smooth but drifts. +- `map` → `odom` comes from localization (AMCL, SLAM). It corrects the drift and may jump. +- Each frame has one parent. Publish static sensor mounts with `robot_state_publisher` from the URDF, not from code. + +## Launch files + +ROS 2 launch files can be [Python, XML or YAML](https://docs.ros.org/en/jazzy/How-To-Guides/Launch-file-different-formats.html). The ROS 2 docs suggest Python when you need flexibility that XML or YAML cannot give you. + +- Name them `.launch.py` (or `.launch.xml`, `.launch.yaml`) and put them in `launch/`. +- Install the folder: `install(DIRECTORY launch config DESTINATION share/${PROJECT_NAME})` in CMake, or `data_files` in `setup.py`. +- Keep robot-specific values (serial ports, IP addresses, limits) in parameter files and launch arguments, not in code. +- A bringup package (`_bringup`) holds the launch files that start a whole robot; driver packages hold only their own node's launch file. + +## Parameters + +- Declare every parameter with a default and a unit in its name or description (`max_linear_speed`, in meters per second). See the parameter tutorials for [C++](https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Using-Parameters-In-A-Class-CPP.html) and [Python](https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Using-Parameters-In-A-Class-Python.html). +- Keep defaults safe: the slowest speed, the smallest workspace. +- Parameter files are YAML, keyed by node name, then `ros__parameters`; `/**` matches every node: + + ```yaml + velocity_limiter: + ros__parameters: + max_linear_speed: 0.5 # m/s + max_angular_speed: 1.0 # rad/s + ``` + +> [!WARNING] +> Speed, force and workspace limits protect people near the robot. Changing a default limit is a safety change: say so in the pull request and have someone who knows the robot review it. Lab rules for operating robots are in the [lab safety guide](https://github.com/ATR-Lab/getting-started-atr-lab/blob/master/lab-rules/safety.md). + +## Topics, services and interfaces + +- Use relative names in code (`cmd_vel`, not `/cmd_vel`) so a namespace or a remap can move the node; set absolute names in launch files. +- Names are `snake_case`. +- Use standard messages (`geometry_msgs`, `sensor_msgs`, `std_srvs`) when they fit. Put custom messages, services and actions in their own `_msgs` or `_interfaces` package, so other packages can depend on the interfaces without the node. +- Stamp sensor data with the time it was measured and the frame it was measured in (`header.stamp`, `header.frame_id`). + +## Checklist before a pull request + +- [ ] Package name follows REP 144. +- [ ] `colcon build` and `colcon test` pass; `colcon test-result --verbose` shows no failures. +- [ ] Units are SI and frames follow REP 103 and REP 105. +- [ ] Parameters have safe defaults and appear in a YAML file under `config/`. +- [ ] Launch files are installed, and a README explains how to start the node (use the [README template](../templates/README.template.md)). +- [ ] `package.xml` lists every dependency, so `rosdep install --from-paths src --ignore-src -y` works on a clean machine. + +## Related + +- [All things ROS 2](https://github.com/ATR-Lab/all-things-ros2): ROS 2 basics, distributions and installation +- [C and C++](c-cpp.md) and [Python](python.md): style and linters for node code +- [Hardware and firmware](hardware.md): microcontrollers and wiring for robots +- ROS 2 [code style and language versions](https://docs.ros.org/en/jazzy/The-ROS2-Project/Contributing/Code-Style-Language-Versions.html), [ROS Index](https://index.ros.org/) (find an existing package before you write one) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/docs/swift.md b/docs/swift.md new file mode 100644 index 0000000..9f97557 --- /dev/null +++ b/docs/swift.md @@ -0,0 +1,144 @@ +# Swift + +> **Read this when** you build a visionOS, watchOS or iOS app (for example, a teleoperation interface on Apple Vision Pro or a study app on Apple Watch) and want consistent code that Xcode, your reviewer and CI agree on. + +We follow Apple's Swift API Design Guidelines, format with swift-format (it ships with the Swift toolchain) and lint with SwiftLint. The one thing to remember: swift-format owns layout and SwiftLint owns everything else; our SwiftLint config switches off the few rules that would fight swift-format. + +## Contents + +- [The style we follow](#the-style-we-follow) +- [Naming and file layout](#naming-and-file-layout) +- [What the configs do](#what-the-configs-do) +- [Install the tools](#install-the-tools) +- [Editor setup](#editor-setup) +- [Adopt the config in a repo](#adopt-the-config-in-a-repo) +- [Check in CI](#check-in-ci) +- [Common mistakes](#common-mistakes) +- [Related](#related) + +## The style we follow + +The [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) are the style guide for Swift. Their first rule, "Clarity at the point of use is your most important goal," decides most naming questions: read the call site out loud and check that it makes sense. + +For layout we use swift-format with two changes from its defaults: four-space indentation (Xcode's default, so files look the same in every editor) and 120-character lines (SwiftUI modifier chains nest deeply). + +## Naming and file layout + +| Thing | Style | Example | +|---|---|---| +| Types and protocols | `UpperCamelCase` | `TeleopViewModel`, `RobotLink` | +| Everything else: properties, functions, cases, parameters | `lowerCamelCase` | `joystickMoved(x:y:)`, `.teleoperated` | +| Booleans | Read as an assertion | `isConnected`, `showsDetails` | +| Factory methods | Start with `make` | `makeRobotLink()` | +| Protocols that describe a capability | End in `-able`, `-ible` or `-ing` | `Sendable`, `Equatable` | +| Units | In the name or a doc comment | `maxLinearSpeed` with `/// meters per second` | +| Files | One main type per file, named after it | `TeleopViewModel.swift` | + +Layout: + +- Swift packages: `Sources//` and `Tests/Tests/` (the Swift Package Manager default). +- Xcode app projects: group files by feature (`Teleoperation/`, `Status/`), not by kind (`Views/`, `Models/`), so a feature's view, view model and model sit together. +- SwiftUI: views end in `View`; UI state lives in an `@Observable` view model marked `@MainActor`; `@State` properties are `private`. + +`examples/swift/Sources/` has a view model, a SwiftUI view and a small watchOS model that pass both tools. + +## What the configs do + +**`.swift-format`** (JSON) sets `indentation` to four spaces and `lineLength` to 120, and turns on a few lint rules that are off by default: + +- `BeginDocumentationCommentWithOneLineSummary`: doc comments start with a one-sentence summary. +- `NeverUseForceTry`: no `try!`. + +The full rule list is in the swift-format [rule documentation](https://github.com/swiftlang/swift-format/blob/main/Documentation/RuleDocumentation.md). + +**`.swiftlint.yml`** keeps SwiftLint's default rules, with these changes: + +| Change | Rules | Why | +|---|---|---| +| Off | `opening_brace`, `trailing_comma`, `vertical_parameter_alignment` | swift-format's output breaks them. We found these by formatting sample code with swift-format and then running every SwiftLint rule on it | +| Off | `todo` | TODO comments are fine; track the work in an issue | +| On (opt-in) | `accessibility_label_for_image` | VoiceOver users need a label for every meaningful image, on visionOS and watchOS too | +| On (opt-in) | `private_swiftui_state` | `@State` belongs to the view that owns it | +| On (opt-in) | `empty_count`, `first_where`, `contains_over_first_not_nil`, `toggle_bool` and others | Clearer, faster code; each has an automatic fix | +| Tuned | `line_length` warns at 120 (same as swift-format), errors at 160 | Long string literals that swift-format cannot break | +| Tuned | `identifier_name` allows `x`, `y`, `z`, `i`, `j`, `dt`, `id` | Short names that are clear in math and robotics code | + +## Install the tools + +macOS: install Xcode 16 or later. Its toolchain includes swift-format, which you run as `swift format` ([swift-format README](https://github.com/swiftlang/swift-format)). Then: + +```bash +brew install swiftlint +swift format --version +swiftlint version +``` + +Ubuntu 24.04 (for CI or server-side Swift): install the toolchain with swiftly, as [swift.org](https://www.swift.org/install/linux/) describes, or use the official `swift:6.4-noble` Docker image. SwiftLint publishes a Docker image too: + +```bash +docker run --rm -v "$PWD":/work -w /work swift:6.4-noble swift format lint --strict --recursive . +docker run --rm -v "$PWD":/work -w /work ghcr.io/realm/swiftlint:0.65.1 swiftlint lint --strict +``` + +## Editor setup + +**Xcode.** Add SwiftLint as a build tool plugin so warnings appear inline after each build. In the project's Package Dependencies, add `https://github.com/SimplyDanny/SwiftLintPlugins` (the repo SwiftLint's README recommends for plugins), then add `SwiftLintBuildToolPlugin` under your target's Build Phases, in Run Build Tool Plug-ins. Format from Terminal before you commit: + +```bash +swift format --in-place --recursive . +``` + +**VS Code.** Install the **Swift** extension (`swiftlang.swift-vscode`) for language support, and run the same `swift format` and `swiftlint` commands in the integrated terminal. + +## Adopt the config in a repo + +Run from the folder that holds your `.xcodeproj` or `Package.swift`: + +```bash +BASE=https://raw.githubusercontent.com/ATR-Lab/dev-guidelines/master/configs/swift +curl -fsSLO "$BASE/.swift-format" +curl -fsSLO "$BASE/.swiftlint.yml" +swift format --in-place --recursive . # reformat once, in its own commit +swiftlint lint --fix # apply SwiftLint's automatic fixes +swiftlint lint --strict # what is left needs a person +``` + +## Check in CI + +Linux containers are enough for formatting and linting; you only need a macOS runner to build the app. + +```yaml +name: swift +on: [push, pull_request] +jobs: + swift-format: + runs-on: ubuntu-latest + container: swift:6.4-noble + steps: + - uses: actions/checkout@v7 + - run: swift format lint --strict --recursive --parallel . + swiftlint: + runs-on: ubuntu-latest + container: ghcr.io/realm/swiftlint:0.65.1 + steps: + - uses: actions/checkout@v7 + - run: swiftlint lint --strict +``` + +## Common mistakes + +- **Letting Xcode's re-indent and swift-format disagree.** Our four-space setting matches Xcode's default; if you changed Xcode's indentation, set it back or run `swift format` before every commit. +- **`try!` and `as!`** in app code. A crash on a headset or a watch in the middle of a session is worse than an error message. Use `do`/`catch` and `as?`. +- **Public `@State`.** Keep it `private`; pass data in through `let` properties or bindings. +- **Images without labels.** Give meaningful images an `accessibilityLabel`, and mark decorative ones with `.accessibilityHidden(true)`. +- **Ignoring Swift 6 concurrency warnings.** Mark value types that cross tasks `Sendable` and UI types `@MainActor`, as the example does, instead of silencing the checker. +- **Committing `xcuserdata/`, `DerivedData/` or `.build/`.** Use the Swift template from [github/gitignore](https://github.com/github/gitignore). + +## Related + +- Config files: [`configs/swift/`](../configs/swift/) and the example in [`examples/swift/`](../examples/swift/) +- [Documentation](documentation.md#docstrings-per-language): Swift doc comments +- [Git and GitHub](git-and-github.md): branches, commits and pull requests +- swift-format [configuration](https://github.com/swiftlang/swift-format/blob/main/Documentation/Configuration.md), SwiftLint [rule directory](https://realm.github.io/SwiftLint/rule-directory.html) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..e8e43e5 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,29 @@ +# Examples + +> **Read this when** you want to see code that passes the lab's configs, or you are changing a config and need to know what the check script runs against. + +Small, realistic samples for each language. Every file outside a `bad/` folder passes the formatter and linters in [`configs/`](../configs/); every file inside a `bad/` folder is written to fail them, and `scripts/check-configs.sh` checks both. The one thing to remember: never copy code from a `bad/` folder. + +## What each folder shows + +| Folder | Good sample | What it demonstrates | +|---|---|---| +| [`c-cpp/`](c-cpp/) | `teleop_velocity_limiter/`: a ROS 2 component node (C++17) with a library class, gtest, launch file and parameter YAML; `firmware/`: a C ring buffer | ROS 2 style, `ament_lint_auto` with `ament_clang_format` instead of uncrustify, clang-tidy with a compilation database | +| [`python/`](python/) | `src/teleop_episodes/stats.py`: episode statistics with dataclasses, type hints and Google docstrings, plus pytest tests | Ruff, `mypy --strict`, pyright and the same code passing `ament_flake8` and `ament_pep257` | +| [`javascript-typescript/`](javascript-typescript/) | `src/robot-status.ts`, `src/status-summary.ts`: parsing and summarizing robot status messages for a dashboard | Strict TypeScript, typed ESLint rules, `unknown` instead of `any` | +| [`swift/`](swift/) | `Sources/`: an `@Observable` teleoperation view model, a SwiftUI status view and a watchOS model | swift-format and SwiftLint together, Swift 6 concurrency annotations | +| [`kotlin/`](kotlin/) | `src/main/kotlin/edu/kent/cs/atr/`: a coroutine speech queue for a Pepper app and a Wear OS Compose card | ktlint `android_studio` style, detekt with Compose settings | +| [`csharp-unity/`](csharp-unity/) | `Assets/Scripts/Teleoperation/`: a MonoBehaviour that follows the operator's hand with a speed limit, and a command struct | Naming rules, Allman braces, Microsoft.Unity.Analyzers | + +The `csharp-unity/check/` folder is only for the check script: a project file and small stand-ins for the UnityEngine types, so the scripts compile with the .NET SDK without Unity. Never copy it into a Unity project. + +## Bad samples + +Each `bad/` file has a comment on each line that names the rule it breaks. The check script requires every tool to reject it; if a tool accepts it, the config is not doing its job and the check fails. The EditorConfig and Markdown bad samples are generated by the script instead of committed. + +## Related + +- [Configs](../configs/README.md): the files these samples are checked against, and the tested versions +- [All guides](../docs/README.md) + +Last reviewed: Sept. 30, 2026 · Sources checked on that date · [Suggest a change](https://github.com/ATR-Lab/dev-guidelines/issues/new) diff --git a/examples/c-cpp/bad/bad_limiter.cpp b/examples/c-cpp/bad/bad_limiter.cpp new file mode 100644 index 0000000..9edbca8 --- /dev/null +++ b/examples/c-cpp/bad/bad_limiter.cpp @@ -0,0 +1,27 @@ +// Deliberately bad C++: scripts/check-configs.sh expects clang-format AND clang-tidy to reject +// this file. Do not copy it. Each comment names the rule that should fire. +#include +#include + +typedef std::vector SpeedList; // modernize-use-using + +class speed_limiter { // readability-identifier-naming (class), clang-format (brace) +public: + double MaxSpeed; // readability-identifier-naming (member) + + double Clamp(double v) { // readability-identifier-naming (method), clang-format (brace) + if (v > MaxSpeed) return MaxSpeed; // readability-braces-around-statements + return v; + } + + bool has_name(const std::string name) { // performance-unnecessary-value-param + return name.size() == 0 ? false : true; // readability-container-size-empty + } +}; + +int main() { + int* count = NULL; // modernize-use-nullptr, clang-format (pointer alignment) + speed_limiter limiter; + limiter.MaxSpeed = 1.0; + return *count; // clang-analyzer-core.NullDereference +} diff --git a/examples/c-cpp/firmware/ring_buffer.c b/examples/c-cpp/firmware/ring_buffer.c new file mode 100644 index 0000000..293cb51 --- /dev/null +++ b/examples/c-cpp/firmware/ring_buffer.c @@ -0,0 +1,33 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +#include "ring_buffer.h" + +void ring_buffer_init(RingBuffer * buffer) +{ + buffer->head = 0U; + buffer->tail = 0U; + buffer->count = 0U; +} + +bool ring_buffer_push(RingBuffer * buffer, uint8_t value) +{ + if (buffer->count == RING_BUFFER_CAPACITY) { + return false; + } + buffer->data[buffer->head] = value; + buffer->head = (buffer->head + 1U) % RING_BUFFER_CAPACITY; + buffer->count++; + return true; +} + +bool ring_buffer_pop(RingBuffer * buffer, uint8_t * value) +{ + if (buffer->count == 0U) { + return false; + } + *value = buffer->data[buffer->tail]; + buffer->tail = (buffer->tail + 1U) % RING_BUFFER_CAPACITY; + buffer->count--; + return true; +} diff --git a/examples/c-cpp/firmware/ring_buffer.h b/examples/c-cpp/firmware/ring_buffer.h new file mode 100644 index 0000000..947b7e6 --- /dev/null +++ b/examples/c-cpp/firmware/ring_buffer.h @@ -0,0 +1,33 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +// A fixed-size byte queue for serial links (for example, servo bus packets on a +// microcontroller). No dynamic memory, safe to use from one producer and one consumer. + +#ifndef RING_BUFFER_H_ +#define RING_BUFFER_H_ + +#include +#include +#include + +#define RING_BUFFER_CAPACITY 64U + +typedef struct +{ + uint8_t data[RING_BUFFER_CAPACITY]; + size_t head; // Next index to write. + size_t tail; // Next index to read. + size_t count; // Bytes currently stored. +} RingBuffer; + +/// Empties the buffer. +void ring_buffer_init(RingBuffer * buffer); + +/// Appends one byte. Returns false, and drops the byte, when the buffer is full. +bool ring_buffer_push(RingBuffer * buffer, uint8_t value); + +/// Removes the oldest byte into *value. Returns false when the buffer is empty. +bool ring_buffer_pop(RingBuffer * buffer, uint8_t * value); + +#endif // RING_BUFFER_H_ diff --git a/examples/c-cpp/teleop_velocity_limiter/CMakeLists.txt b/examples/c-cpp/teleop_velocity_limiter/CMakeLists.txt new file mode 100644 index 0000000..18d2364 --- /dev/null +++ b/examples/c-cpp/teleop_velocity_limiter/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.20) +project(teleop_velocity_limiter) + +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +find_package(ament_cmake REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(rclcpp REQUIRED) +find_package(rclcpp_components REQUIRED) + +add_library(${PROJECT_NAME} SHARED + src/velocity_limiter.cpp + src/velocity_limiter_node.cpp +) +target_include_directories(${PROJECT_NAME} PUBLIC + $ + $ +) +target_link_libraries(${PROJECT_NAME} PUBLIC + ${geometry_msgs_TARGETS} + rclcpp::rclcpp + rclcpp_components::component +) +rclcpp_components_register_node(${PROJECT_NAME} + PLUGIN "teleop_velocity_limiter::VelocityLimiterNode" + EXECUTABLE velocity_limiter +) + +install(DIRECTORY include/ DESTINATION include/${PROJECT_NAME}) +install(TARGETS ${PROJECT_NAME} + EXPORT export_${PROJECT_NAME} + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) +install(DIRECTORY config launch DESTINATION share/${PROJECT_NAME}) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + # We format with clang-format, so check with ament_clang_format (a test_depend in + # package.xml) and skip ament_uncrustify: the two formatters disagree on lambdas + # and one-line functions. + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify) + # ament_copyright wants the full license text in every file; our files carry a + # short SPDX header and the repo has a LICENSE file, so skip that one linter. + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_copyright) + ament_lint_auto_find_test_dependencies() + + find_package(ament_cmake_gtest REQUIRED) + ament_add_gtest(test_velocity_limiter test/test_velocity_limiter.cpp) + target_link_libraries(test_velocity_limiter ${PROJECT_NAME}) +endif() + +ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET) +ament_export_dependencies(geometry_msgs rclcpp rclcpp_components) +ament_package() diff --git a/examples/c-cpp/teleop_velocity_limiter/config/limits.yaml b/examples/c-cpp/teleop_velocity_limiter/config/limits.yaml new file mode 100644 index 0000000..e31bd22 --- /dev/null +++ b/examples/c-cpp/teleop_velocity_limiter/config/limits.yaml @@ -0,0 +1,7 @@ +# Speed limits for teleoperation, in SI units (REP 103). +velocity_limiter: + ros__parameters: + max_linear_speed: 0.5 # m/s + max_angular_speed: 1.0 # rad/s + max_linear_accel: 1.0 # m/s^2 + max_angular_accel: 2.0 # rad/s^2 diff --git a/examples/c-cpp/teleop_velocity_limiter/include/teleop_velocity_limiter/velocity_limiter.hpp b/examples/c-cpp/teleop_velocity_limiter/include/teleop_velocity_limiter/velocity_limiter.hpp new file mode 100644 index 0000000..3be3bb6 --- /dev/null +++ b/examples/c-cpp/teleop_velocity_limiter/include/teleop_velocity_limiter/velocity_limiter.hpp @@ -0,0 +1,52 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +#ifndef TELEOP_VELOCITY_LIMITER__VELOCITY_LIMITER_HPP_ +#define TELEOP_VELOCITY_LIMITER__VELOCITY_LIMITER_HPP_ + +namespace teleop_velocity_limiter +{ + +/// Speed and acceleration limits for a mobile base, in SI units (REP 103). +struct Limits +{ + double max_linear_speed{0.5}; ///< Meters per second. + double max_angular_speed{1.0}; ///< Radians per second. + double max_linear_accel{1.0}; ///< Meters per second squared. + double max_angular_accel{2.0}; ///< Radians per second squared. +}; + +/// A planar velocity command: forward speed and yaw rate in the robot's base_link frame. +struct Velocity +{ + double linear{0.0}; ///< Meters per second, positive forward (x). + double angular{0.0}; ///< Radians per second, positive counterclockwise (z). +}; + +/** + * Clamps teleoperation commands so the robot never exceeds its speed and + * acceleration limits. + * + * The class holds no ROS types, so it can be unit tested without a running node. + */ +class VelocityLimiter +{ +public: + explicit VelocityLimiter(const Limits & limits); + + /// Returns the command to send, given the requested command and the time since the last one. + Velocity limit(const Velocity & requested, double dt_seconds); + + /// Forgets the previous command, for example after the operator releases the dead-man switch. + void reset(); + + const Limits & limits() const { return limits_; } + +private: + Limits limits_; + Velocity last_command_; +}; + +} // namespace teleop_velocity_limiter + +#endif // TELEOP_VELOCITY_LIMITER__VELOCITY_LIMITER_HPP_ diff --git a/examples/c-cpp/teleop_velocity_limiter/include/teleop_velocity_limiter/velocity_limiter_node.hpp b/examples/c-cpp/teleop_velocity_limiter/include/teleop_velocity_limiter/velocity_limiter_node.hpp new file mode 100644 index 0000000..3485242 --- /dev/null +++ b/examples/c-cpp/teleop_velocity_limiter/include/teleop_velocity_limiter/velocity_limiter_node.hpp @@ -0,0 +1,39 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +#ifndef TELEOP_VELOCITY_LIMITER__VELOCITY_LIMITER_NODE_HPP_ +#define TELEOP_VELOCITY_LIMITER__VELOCITY_LIMITER_NODE_HPP_ + +#include + +#include "geometry_msgs/msg/twist.hpp" +#include "rclcpp/rclcpp.hpp" +#include "teleop_velocity_limiter/velocity_limiter.hpp" + +namespace teleop_velocity_limiter +{ + +/** + * Subscribes to raw teleoperation commands on `cmd_vel_in`, limits them and + * publishes the result on `cmd_vel_out`. + * + * Parameters (all doubles, SI units): max_linear_speed, max_angular_speed, + * max_linear_accel, max_angular_accel. + */ +class VelocityLimiterNode : public rclcpp::Node +{ +public: + explicit VelocityLimiterNode(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()); + +private: + void on_command(const geometry_msgs::msg::Twist & msg); + + VelocityLimiter limiter_; + rclcpp::Time last_stamp_; + rclcpp::Subscription::SharedPtr subscription_; + rclcpp::Publisher::SharedPtr publisher_; +}; + +} // namespace teleop_velocity_limiter + +#endif // TELEOP_VELOCITY_LIMITER__VELOCITY_LIMITER_NODE_HPP_ diff --git a/examples/c-cpp/teleop_velocity_limiter/launch/velocity_limiter.launch.py b/examples/c-cpp/teleop_velocity_limiter/launch/velocity_limiter.launch.py new file mode 100644 index 0000000..471ffa2 --- /dev/null +++ b/examples/c-cpp/teleop_velocity_limiter/launch/velocity_limiter.launch.py @@ -0,0 +1,32 @@ +# Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +# SPDX-License-Identifier: MIT + +"""Start the velocity limiter between a teleoperation source and the robot.""" + +from pathlib import Path + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node + + +def generate_launch_description() -> LaunchDescription: + """Return the launch description for the velocity limiter node.""" + default_params = Path(get_package_share_directory('teleop_velocity_limiter')) / 'config' + params_file = LaunchConfiguration('params_file') + + return LaunchDescription([ + DeclareLaunchArgument( + 'params_file', + default_value=str(default_params / 'limits.yaml'), + description='YAML file with the speed and acceleration limits.', + ), + Node( + package='teleop_velocity_limiter', + executable='velocity_limiter', + parameters=[params_file], + remappings=[('cmd_vel_in', 'teleop/cmd_vel'), ('cmd_vel_out', 'cmd_vel')], + ), + ]) diff --git a/examples/c-cpp/teleop_velocity_limiter/package.xml b/examples/c-cpp/teleop_velocity_limiter/package.xml new file mode 100644 index 0000000..949b609 --- /dev/null +++ b/examples/c-cpp/teleop_velocity_limiter/package.xml @@ -0,0 +1,24 @@ + + + + teleop_velocity_limiter + 0.1.0 + Limits speed and acceleration of teleoperation velocity commands. + Example Maintainer + MIT + + ament_cmake + + geometry_msgs + rclcpp + rclcpp_components + + ament_cmake_clang_format + ament_cmake_gtest + ament_lint_auto + ament_lint_common + + + ament_cmake + + diff --git a/examples/c-cpp/teleop_velocity_limiter/src/velocity_limiter.cpp b/examples/c-cpp/teleop_velocity_limiter/src/velocity_limiter.cpp new file mode 100644 index 0000000..b6fbe5b --- /dev/null +++ b/examples/c-cpp/teleop_velocity_limiter/src/velocity_limiter.cpp @@ -0,0 +1,43 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +#include "teleop_velocity_limiter/velocity_limiter.hpp" + +#include + +namespace teleop_velocity_limiter +{ +namespace +{ + +double clamp_step(double requested, double previous, double max_value, double max_step) +{ + const double bounded = std::clamp(requested, -max_value, max_value); + return std::clamp(bounded, previous - max_step, previous + max_step); +} + +} // namespace + +VelocityLimiter::VelocityLimiter(const Limits & limits) : limits_(limits) {} + +Velocity VelocityLimiter::limit(const Velocity & requested, double dt_seconds) +{ + if (dt_seconds <= 0.0) { + return last_command_; + } + + Velocity command; + command.linear = clamp_step( + requested.linear, last_command_.linear, limits_.max_linear_speed, + limits_.max_linear_accel * dt_seconds); + command.angular = clamp_step( + requested.angular, last_command_.angular, limits_.max_angular_speed, + limits_.max_angular_accel * dt_seconds); + + last_command_ = command; + return command; +} + +void VelocityLimiter::reset() { last_command_ = Velocity{}; } + +} // namespace teleop_velocity_limiter diff --git a/examples/c-cpp/teleop_velocity_limiter/src/velocity_limiter_node.cpp b/examples/c-cpp/teleop_velocity_limiter/src/velocity_limiter_node.cpp new file mode 100644 index 0000000..b2c0922 --- /dev/null +++ b/examples/c-cpp/teleop_velocity_limiter/src/velocity_limiter_node.cpp @@ -0,0 +1,54 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +#include "teleop_velocity_limiter/velocity_limiter_node.hpp" + +#include "rclcpp_components/register_node_macro.hpp" + +namespace teleop_velocity_limiter +{ +namespace +{ + +Limits declare_limits(rclcpp::Node & node) +{ + Limits limits; + limits.max_linear_speed = node.declare_parameter("max_linear_speed", limits.max_linear_speed); + limits.max_angular_speed = node.declare_parameter("max_angular_speed", limits.max_angular_speed); + limits.max_linear_accel = node.declare_parameter("max_linear_accel", limits.max_linear_accel); + limits.max_angular_accel = node.declare_parameter("max_angular_accel", limits.max_angular_accel); + return limits; +} + +} // namespace + +VelocityLimiterNode::VelocityLimiterNode(const rclcpp::NodeOptions & options) +: rclcpp::Node("velocity_limiter", options), limiter_(declare_limits(*this)), last_stamp_(now()) +{ + publisher_ = create_publisher("cmd_vel_out", rclcpp::QoS(10)); + subscription_ = create_subscription( + "cmd_vel_in", rclcpp::QoS(10), + [this](const geometry_msgs::msg::Twist & msg) { on_command(msg); }); + + RCLCPP_INFO( + get_logger(), "Limiting to %.2f m/s and %.2f rad/s", limiter_.limits().max_linear_speed, + limiter_.limits().max_angular_speed); +} + +void VelocityLimiterNode::on_command(const geometry_msgs::msg::Twist & msg) +{ + const rclcpp::Time stamp = now(); + const double dt_seconds = (stamp - last_stamp_).seconds(); + last_stamp_ = stamp; + + const Velocity command = limiter_.limit(Velocity{msg.linear.x, msg.angular.z}, dt_seconds); + + geometry_msgs::msg::Twist out; + out.linear.x = command.linear; + out.angular.z = command.angular; + publisher_->publish(out); +} + +} // namespace teleop_velocity_limiter + +RCLCPP_COMPONENTS_REGISTER_NODE(teleop_velocity_limiter::VelocityLimiterNode) diff --git a/examples/c-cpp/teleop_velocity_limiter/test/test_velocity_limiter.cpp b/examples/c-cpp/teleop_velocity_limiter/test/test_velocity_limiter.cpp new file mode 100644 index 0000000..6cf5377 --- /dev/null +++ b/examples/c-cpp/teleop_velocity_limiter/test/test_velocity_limiter.cpp @@ -0,0 +1,32 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +#include + +#include "teleop_velocity_limiter/velocity_limiter.hpp" + +using teleop_velocity_limiter::Limits; +using teleop_velocity_limiter::Velocity; +using teleop_velocity_limiter::VelocityLimiter; + +TEST(VelocityLimiter, ClampsToMaxSpeed) +{ + VelocityLimiter limiter(Limits{0.5, 1.0, 100.0, 100.0}); + const Velocity out = limiter.limit(Velocity{3.0, -4.0}, 0.1); + EXPECT_DOUBLE_EQ(out.linear, 0.5); + EXPECT_DOUBLE_EQ(out.angular, -1.0); +} + +TEST(VelocityLimiter, LimitsAcceleration) +{ + VelocityLimiter limiter(Limits{2.0, 2.0, 1.0, 1.0}); + const Velocity out = limiter.limit(Velocity{2.0, 0.0}, 0.1); + EXPECT_NEAR(out.linear, 0.1, 1e-9); +} + +TEST(VelocityLimiter, IgnoresNonPositiveTimeStep) +{ + VelocityLimiter limiter(Limits{}); + const Velocity out = limiter.limit(Velocity{0.3, 0.0}, 0.0); + EXPECT_DOUBLE_EQ(out.linear, 0.0); +} diff --git a/examples/csharp-unity/Assets/Scripts/Teleoperation/HandTargetFollower.cs b/examples/csharp-unity/Assets/Scripts/Teleoperation/HandTargetFollower.cs new file mode 100644 index 0000000..082c758 --- /dev/null +++ b/examples/csharp-unity/Assets/Scripts/Teleoperation/HandTargetFollower.cs @@ -0,0 +1,60 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +using UnityEngine; + +namespace Atr.Teleoperation +{ + /// + /// Moves the robot's end-effector target toward the operator's tracked hand, with a speed + /// limit so a tracking glitch cannot make the arm jump. + /// + public sealed class HandTargetFollower : MonoBehaviour + { + [SerializeField] + [Tooltip("The tracked hand or controller that the operator moves.")] + private Transform _hand; + + [SerializeField] + [Tooltip("Maximum target speed in meters per second.")] + [Min(0f)] + private float _maxSpeed = 0.25f; + + [SerializeField] + [Tooltip("Maximum target rotation speed in degrees per second.")] + [Min(0f)] + private float _maxAngularSpeed = 90f; + + private bool _isEngaged; + + /// Gets the last command sent to the robot bridge. + public TeleopCommand LastCommand { get; private set; } + + /// Starts following the hand, for example when the operator holds the grip button. + public void Engage() + { + _isEngaged = true; + } + + /// Stops following the hand and holds the current target. + public void Disengage() + { + _isEngaged = false; + } + + private void Update() + { + if (!_isEngaged || _hand == null) + { + return; + } + + float step = _maxSpeed * Time.deltaTime; + float angleStep = _maxAngularSpeed * Time.deltaTime; + transform.position = Vector3.MoveTowards(transform.position, _hand.position, step); + transform.rotation = Quaternion.RotateTowards(transform.rotation, _hand.rotation, angleStep); + + LastCommand = new TeleopCommand(transform.position, transform.rotation); + } + } +} diff --git a/examples/csharp-unity/Assets/Scripts/Teleoperation/TeleopCommand.cs b/examples/csharp-unity/Assets/Scripts/Teleoperation/TeleopCommand.cs new file mode 100644 index 0000000..be455dc --- /dev/null +++ b/examples/csharp-unity/Assets/Scripts/Teleoperation/TeleopCommand.cs @@ -0,0 +1,48 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +using System; +using UnityEngine; + +namespace Atr.Teleoperation +{ + /// + /// A pose target for the robot arm, in Unity's left-handed, Y-up world frame. The ROS 2 bridge + /// converts it to the robot's right-handed, Z-up frame (REP 103) before sending it. + /// + public readonly struct TeleopCommand : IEquatable + { + /// Initializes a new instance of the struct. + /// Target position in meters. + /// Target orientation. + public TeleopCommand(Vector3 position, Quaternion rotation) + { + Position = position; + Rotation = rotation; + } + + /// Gets the target position in meters. + public Vector3 Position { get; } + + /// Gets the target orientation. + public Quaternion Rotation { get; } + + /// + public bool Equals(TeleopCommand other) + { + return Position == other.Position && Rotation == other.Rotation; + } + + /// + public override bool Equals(object obj) + { + return obj is TeleopCommand other && Equals(other); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Position, Rotation); + } + } +} diff --git a/examples/csharp-unity/bad/BadFollower.cs b/examples/csharp-unity/bad/BadFollower.cs new file mode 100644 index 0000000..4ec99b7 --- /dev/null +++ b/examples/csharp-unity/bad/BadFollower.cs @@ -0,0 +1,22 @@ +// Deliberately bad C#: scripts/check-configs.sh expects dotnet build (with the lab .editorconfig and +// Microsoft.Unity.Analyzers) and dotnet format to reject this file. Do not copy it. +using System.Collections.Generic; // IDE0005 unnecessary using +using UnityEngine; + +namespace Atr.Bad +{ + public class bad_follower : MonoBehaviour { // IDE1006 naming, IDE0055 brace on same line + public Transform target; // IDE1006: public fields are PascalCase + private float speed = 1f; // IDE1006: private fields are _camelCase + + void Update() // IDE0040: missing accessibility modifier + { + } // UNT0001: empty Unity message + + private void LateUpdate() + { + if (target?.position == null) return; // UNT0008: null propagation, IDE0011: braces + if (target.gameObject is null) { } // UNT0029: pattern matching with null + } + } +} diff --git a/examples/csharp-unity/check/CheckHarness.csproj b/examples/csharp-unity/check/CheckHarness.csproj new file mode 100644 index 0000000..946cede --- /dev/null +++ b/examples/csharp-unity/check/CheckHarness.csproj @@ -0,0 +1,34 @@ + + + + + netstandard2.1 + + 9.0 + disable + false + + true + + true + true + false + + + + + + + + + + + + + + diff --git a/examples/csharp-unity/check/UnityEngineStubs.cs b/examples/csharp-unity/check/UnityEngineStubs.cs new file mode 100644 index 0000000..309dc3b --- /dev/null +++ b/examples/csharp-unity/check/UnityEngineStubs.cs @@ -0,0 +1,121 @@ +// +// Minimal stand-ins for the few UnityEngine types the examples use, so scripts/check-configs.sh can +// compile and analyze the Unity scripts with the .NET SDK, without installing Unity. Not a Unity +// API: never copy this file into a Unity project (the real UnityEngine assembly already exists). +// The auto-generated marker above tells the analyzers and dotnet format to skip this file. +#pragma warning disable + +namespace UnityEngine +{ + public class Object + { + public static bool operator ==(Object a, Object b) => ReferenceEquals(a, b); + + public static bool operator !=(Object a, Object b) => !ReferenceEquals(a, b); + + public override bool Equals(object other) => ReferenceEquals(this, other); + + public override int GetHashCode() => 0; + } + + public class Component : Object + { + public Transform transform => null; + + public GameObject gameObject => null; + } + + public class Behaviour : Component + { + } + + public class MonoBehaviour : Behaviour + { + } + + public class Transform : Component + { + public Vector3 position { get; set; } + + public Quaternion rotation { get; set; } + } + + public struct Vector3 : System.IEquatable + { + public float x; + public float y; + public float z; + + public static Vector3 MoveTowards(Vector3 current, Vector3 target, float maxDistanceDelta) => target; + + public static bool operator ==(Vector3 a, Vector3 b) => a.Equals(b); + + public static bool operator !=(Vector3 a, Vector3 b) => !a.Equals(b); + + public bool Equals(Vector3 other) => x == other.x && y == other.y && z == other.z; + + public override bool Equals(object other) => other is Vector3 v && Equals(v); + + public override int GetHashCode() => System.HashCode.Combine(x, y, z); + } + + public struct Quaternion : System.IEquatable + { + public float x; + public float y; + public float z; + public float w; + + public static Quaternion RotateTowards(Quaternion from, Quaternion to, float maxDegreesDelta) => to; + + public static bool operator ==(Quaternion a, Quaternion b) => a.Equals(b); + + public static bool operator !=(Quaternion a, Quaternion b) => !a.Equals(b); + + public bool Equals(Quaternion other) => x == other.x && y == other.y && z == other.z && w == other.w; + + public override bool Equals(object other) => other is Quaternion q && Equals(q); + + public override int GetHashCode() => System.HashCode.Combine(x, y, z, w); + } + + public static class Time + { + public static float deltaTime => 0.02f; + } + + public static class Debug + { + public static void Log(object message) + { + } + } + + public class GameObject : Object + { + public string tag { get; set; } + + public T GetComponent() => default; + } + + [System.AttributeUsage(System.AttributeTargets.Field)] + public sealed class SerializeField : System.Attribute + { + } + + [System.AttributeUsage(System.AttributeTargets.Field)] + public sealed class TooltipAttribute : System.Attribute + { + public TooltipAttribute(string tooltip) + { + } + } + + [System.AttributeUsage(System.AttributeTargets.Field)] + public sealed class MinAttribute : System.Attribute + { + public MinAttribute(float min) + { + } + } +} diff --git a/examples/javascript-typescript/bad/bad-status.ts b/examples/javascript-typescript/bad/bad-status.ts new file mode 100644 index 0000000..6fabc94 --- /dev/null +++ b/examples/javascript-typescript/bad/bad-status.ts @@ -0,0 +1,20 @@ +// Deliberately bad TypeScript: scripts/check-configs.sh expects ESLint, Prettier and tsc to +// reject this file. Do not copy it. Each comment names the rule that should fire. +import { parseStatus } from "../src/robot-status.js"; // prettier (double quotes), no-unused-vars + +export function batteryLabel(status: any) { // no-explicit-any + if (status.battery == null) { // eqeqeq + return 'unknown' + } + return status.battery + '%'; // restrict-plus-operands (any + string) +} + +export async function fetchStatus(url: string): Promise { + return fetch(url).then((r) => r.json()); +} + +export function refresh(url: string): void { + fetchStatus(url); // no-floating-promises + const count: number = 'three'; // tsc: string is not assignable to number + console.log(count); // no-console +} diff --git a/examples/javascript-typescript/bad/tsconfig.json b/examples/javascript-typescript/bad/tsconfig.json new file mode 100644 index 0000000..b621939 --- /dev/null +++ b/examples/javascript-typescript/bad/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "lib": ["ES2022", "DOM"] + }, + "include": ["."] +} diff --git a/examples/javascript-typescript/src/robot-status.ts b/examples/javascript-typescript/src/robot-status.ts new file mode 100644 index 0000000..5140e1c --- /dev/null +++ b/examples/javascript-typescript/src/robot-status.ts @@ -0,0 +1,59 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +/** Battery and motion state that a robot publishes to the dashboard. */ +export interface RobotStatus { + readonly robotId: string; + readonly batteryPercent: number; + readonly linearSpeed: number; // m/s + readonly mode: RobotMode; + readonly receivedAt: Date; +} + +export type RobotMode = 'idle' | 'teleoperated' | 'autonomous' | 'fault'; + +const ROBOT_MODES: readonly RobotMode[] = ['idle', 'teleoperated', 'autonomous', 'fault']; + +/** Thrown when a message from the robot does not have the fields we expect. */ +export class StatusParseError extends Error { + override readonly name = 'StatusParseError'; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function isRobotMode(value: unknown): value is RobotMode { + return typeof value === 'string' && (ROBOT_MODES as readonly string[]).includes(value); +} + +/** + * Parses one JSON status message, for example from a rosbridge WebSocket. + * + * @param raw - The message text. + * @param now - Clock to stamp the message with; pass a fixed date in tests. + * @returns The validated status. + * @throws {StatusParseError} If a field is missing or has the wrong type. + */ +export function parseStatus(raw: string, now: () => Date = () => new Date()): RobotStatus { + const data: unknown = JSON.parse(raw); + if (!isRecord(data)) { + throw new StatusParseError('Status message must be a JSON object.'); + } + + const { robot_id: robotId, battery, speed, mode } = data; + if (typeof robotId !== 'string' || robotId.length === 0) { + throw new StatusParseError('robot_id must be a non-empty string.'); + } + if (typeof battery !== 'number' || battery < 0 || battery > 100) { + throw new StatusParseError('battery must be a number from 0 to 100.'); + } + if (typeof speed !== 'number' || !Number.isFinite(speed)) { + throw new StatusParseError('speed must be a finite number.'); + } + if (!isRobotMode(mode)) { + throw new StatusParseError(`mode must be one of: ${ROBOT_MODES.join(', ')}.`); + } + + return { robotId, batteryPercent: battery, linearSpeed: speed, mode, receivedAt: now() }; +} diff --git a/examples/javascript-typescript/src/status-summary.ts b/examples/javascript-typescript/src/status-summary.ts new file mode 100644 index 0000000..bca7777 --- /dev/null +++ b/examples/javascript-typescript/src/status-summary.ts @@ -0,0 +1,40 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +import type { RobotStatus } from './robot-status.js'; + +const LOW_BATTERY_PERCENT = 20; +const STALE_AFTER_MS = 5_000; + +export interface StatusSummary { + readonly online: number; + readonly lowBattery: readonly string[]; + readonly stale: readonly string[]; +} + +/** + * Groups the latest status of each robot for the dashboard header. + * + * @param statuses - Latest status per robot. + * @param now - Current time, used to find robots that stopped reporting. + */ +export function summarize(statuses: readonly RobotStatus[], now: Date): StatusSummary { + const lowBattery: string[] = []; + const stale: string[] = []; + + for (const status of statuses) { + if (status.batteryPercent < LOW_BATTERY_PERCENT) { + lowBattery.push(status.robotId); + } + if (now.getTime() - status.receivedAt.getTime() > STALE_AFTER_MS) { + stale.push(status.robotId); + } + } + + return { online: statuses.length - stale.length, lowBattery, stale }; +} + +/** Formats a speed for display, for example "0.45 m/s". */ +export function formatSpeed(metersPerSecond: number): string { + return `${metersPerSecond.toFixed(2)} m/s`; +} diff --git a/examples/javascript-typescript/tsconfig.json b/examples/javascript-typescript/tsconfig.json new file mode 100644 index 0000000..3b5e842 --- /dev/null +++ b/examples/javascript-typescript/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "lib": ["ES2022", "DOM"] + }, + "include": ["src"] +} diff --git a/examples/kotlin/bad/BadRobot.kt b/examples/kotlin/bad/BadRobot.kt new file mode 100644 index 0000000..fbb723d --- /dev/null +++ b/examples/kotlin/bad/BadRobot.kt @@ -0,0 +1,18 @@ +// Deliberately bad Kotlin: scripts/check-configs.sh expects ktlint AND detekt to reject this +// file. Do not copy it. Each comment names the rule that should fire. +package edu.kent.cs.atr.bad + +import kotlin.math.* // ktlint no-wildcard-imports, detekt WildcardImport + +class bad_robot { // ktlint class-naming, detekt ClassNaming + var Speed = 0.0 // detekt VariableNaming, ktlint property-naming + + fun Drive(distance: Double) : Double { // ktlint function-naming, colon spacing + if (distance > 42.0) return 42.0 // detekt MagicNumber + try { + return distance / Speed + } catch (e: Exception) { // detekt TooGenericExceptionCaught, SwallowedException + return 0.0 + } + } +} diff --git a/examples/kotlin/src/main/kotlin/edu/kent/cs/atr/pepper/SpeechQueue.kt b/examples/kotlin/src/main/kotlin/edu/kent/cs/atr/pepper/SpeechQueue.kt new file mode 100644 index 0000000..b8d649a --- /dev/null +++ b/examples/kotlin/src/main/kotlin/edu/kent/cs/atr/pepper/SpeechQueue.kt @@ -0,0 +1,56 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +package edu.kent.cs.atr.pepper + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Something the robot can say aloud. The app wraps the robot SDK's text-to-speech call. */ +fun interface Speaker { + suspend fun say(text: String) +} + +/** + * Queues phrases so the robot says them one at a time, in order. + * + * Callers on the UI thread call [enqueue]; one coroutine calls [run] and drains the queue. + */ +class SpeechQueue(private val speaker: Speaker, private val maxQueued: Int = DEFAULT_CAPACITY) { + private val phrases = Channel(capacity = maxQueued) + private val speaking = MutableStateFlow(false) + + /** True while the robot is speaking. */ + val isSpeaking: StateFlow = speaking.asStateFlow() + + /** Adds a phrase. Returns false, and drops it, when the queue is full or the text is blank. */ + fun enqueue(text: String): Boolean { + if (text.isBlank()) { + return false + } + return phrases.trySend(text.trim()).isSuccess + } + + /** Says queued phrases until the queue is closed. */ + suspend fun run() { + for (phrase in phrases) { + speaking.value = true + try { + speaker.say(phrase) + } finally { + speaking.value = false + } + } + } + + /** Stops accepting phrases; [run] returns after the last queued phrase. */ + fun close() { + phrases.close() + } + + private companion object { + const val DEFAULT_CAPACITY = 8 + } +} diff --git a/examples/kotlin/src/main/kotlin/edu/kent/cs/atr/wear/HeartRateCard.kt b/examples/kotlin/src/main/kotlin/edu/kent/cs/atr/wear/HeartRateCard.kt new file mode 100644 index 0000000..402addc --- /dev/null +++ b/examples/kotlin/src/main/kotlin/edu/kent/cs/atr/wear/HeartRateCard.kt @@ -0,0 +1,20 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +package edu.kent.cs.atr.wear + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.wear.compose.material3.Text + +/** Shows the latest heart rate, or a dash when the sensor has not reported recently. */ +@Composable +fun HeartRateCard(reading: HeartRateReading?, modifier: Modifier = Modifier) { + val label = reading?.takeIf { it.isFresh }?.let { "${it.beatsPerMinute} bpm" } ?: "--" + Column(modifier = modifier.semantics { contentDescription = "Heart rate $label" }) { + Text(text = label) + } +} diff --git a/examples/kotlin/src/main/kotlin/edu/kent/cs/atr/wear/HeartRateReading.kt b/examples/kotlin/src/main/kotlin/edu/kent/cs/atr/wear/HeartRateReading.kt new file mode 100644 index 0000000..2c015b6 --- /dev/null +++ b/examples/kotlin/src/main/kotlin/edu/kent/cs/atr/wear/HeartRateReading.kt @@ -0,0 +1,7 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +package edu.kent.cs.atr.wear + +/** Heart rate reading shown on the watch face during a session. */ +data class HeartRateReading(val beatsPerMinute: Int, val isFresh: Boolean) diff --git a/examples/python/bad/bad_module.py b/examples/python/bad/bad_module.py new file mode 100644 index 0000000..2142ccd --- /dev/null +++ b/examples/python/bad/bad_module.py @@ -0,0 +1,16 @@ +# Deliberately bad Python: scripts/check-configs.sh expects ruff (lint and format) and mypy to +# reject this file. Do not copy it. Each comment names the rule that should fire. +import os, sys # E401 (two imports on one line), F401 (unused) +from typing import List # UP035 (use list), F401 + + +def ComputeSpeed(distance, time): # N802 (function name), D103 (no docstring), untyped (mypy) + if time == 0: return None # E701 (statement on same line), format + speed = distance / time + l = [x for x in [speed]] # E741 (ambiguous name), C416 (unnecessary comprehension) + return l[0] + + +def total(values: list[int]) -> int: + """Add the values.""" + return sum(values) + "0" # mypy: int + str diff --git a/examples/python/src/teleop_episodes/__init__.py b/examples/python/src/teleop_episodes/__init__.py new file mode 100644 index 0000000..7ff7ebc --- /dev/null +++ b/examples/python/src/teleop_episodes/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +# SPDX-License-Identifier: MIT + +from teleop_episodes.stats import EpisodeSummary, Frame, is_within_limits, load_frames, summarize + +__all__ = ['EpisodeSummary', 'Frame', 'is_within_limits', 'load_frames', 'summarize'] diff --git a/examples/python/src/teleop_episodes/stats.py b/examples/python/src/teleop_episodes/stats.py new file mode 100644 index 0000000..9113ee1 --- /dev/null +++ b/examples/python/src/teleop_episodes/stats.py @@ -0,0 +1,94 @@ +# Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +# SPDX-License-Identifier: MIT + +"""Summary statistics for recorded teleoperation episodes.""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +import itertools +import json +import math +from pathlib import Path + +DEFAULT_RATE_HZ = 30.0 + + +@dataclass(frozen=True) +class Frame: + """One sample of joint positions, in radians, at a time in seconds.""" + + stamp: float + positions: tuple[float, ...] + + +@dataclass(frozen=True) +class EpisodeSummary: + """Numbers we report for each episode before training on it.""" + + frame_count: int + duration_s: float + joint_ranges: tuple[float, ...] + max_joint_speed: float + + +def summarize(frames: Sequence[Frame]) -> EpisodeSummary: + """Summarize one recorded episode. + + Args: + frames: Samples in time order. Every frame must have the same number of joints. + + Returns: + The frame count, the duration, the range of each joint and the fastest joint speed + seen between two consecutive frames. + + Raises: + ValueError: If there are fewer than two frames or the joint counts differ. + """ + if len(frames) < 2: + raise ValueError('An episode needs at least two frames.') + joint_count = len(frames[0].positions) + if any(len(frame.positions) != joint_count for frame in frames): + raise ValueError('Every frame must have the same number of joints.') + + joint_ranges = tuple( + max(frame.positions[j] for frame in frames) - min(frame.positions[j] for frame in frames) + for j in range(joint_count) + ) + max_speed = 0.0 + for previous, current in itertools.pairwise(frames): + dt = current.stamp - previous.stamp + if dt <= 0.0: + continue + for a, b in zip(previous.positions, current.positions, strict=True): + max_speed = max(max_speed, abs(b - a) / dt) + + return EpisodeSummary( + frame_count=len(frames), + duration_s=frames[-1].stamp - frames[0].stamp, + joint_ranges=joint_ranges, + max_joint_speed=max_speed, + ) + + +def load_frames(path: Path, rate_hz: float = DEFAULT_RATE_HZ) -> list[Frame]: + """Load frames from a JSON file that holds a list of joint-position lists. + + Args: + path: File written by the recorder, one inner list per sample. + rate_hz: Recording rate, used to rebuild the time stamps. + + Returns: + The frames, stamped from zero. + """ + rows = json.loads(path.read_text(encoding='utf-8')) + return [ + Frame(stamp=index / rate_hz, positions=tuple(float(value) for value in row)) + for index, row in enumerate(rows) + ] + + +def is_within_limits(summary: EpisodeSummary, max_speed: float = math.pi) -> bool: + """Return True when no joint moved faster than max_speed (rad/s).""" + return summary.max_joint_speed <= max_speed diff --git a/examples/python/tests/test_stats.py b/examples/python/tests/test_stats.py new file mode 100644 index 0000000..08a4691 --- /dev/null +++ b/examples/python/tests/test_stats.py @@ -0,0 +1,25 @@ +# Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +# SPDX-License-Identifier: MIT + +import pytest + +from teleop_episodes import Frame, is_within_limits, summarize + + +def test_summary_reports_ranges_and_speed() -> None: + frames = [ + Frame(stamp=0.0, positions=(0.0, 1.0)), + Frame(stamp=0.5, positions=(0.5, 1.0)), + Frame(stamp=1.0, positions=(0.25, 0.0)), + ] + summary = summarize(frames) + assert summary.frame_count == 3 + assert summary.duration_s == pytest.approx(1.0) + assert summary.joint_ranges == pytest.approx((0.5, 1.0)) + assert summary.max_joint_speed == pytest.approx(2.0) + assert is_within_limits(summary) + + +def test_summary_needs_two_frames() -> None: + with pytest.raises(ValueError, match='at least two frames'): + summarize([Frame(stamp=0.0, positions=(0.0,))]) diff --git a/examples/swift/Sources/HeartRateZone.swift b/examples/swift/Sources/HeartRateZone.swift new file mode 100644 index 0000000..b2a9e00 --- /dev/null +++ b/examples/swift/Sources/HeartRateZone.swift @@ -0,0 +1,31 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +/// Heart-rate zones that a watchOS app can show during a study session. +/// +/// The thresholds are fractions of the participant's maximum heart rate and come from the +/// study protocol, not from this code. +enum HeartRateZone: String, CaseIterable, Sendable { + case resting + case light + case moderate + case vigorous + + /// Returns the zone for a heart rate, given the upper bound of each zone as a fraction of + /// the maximum heart rate. + static func zone( + forBeatsPerMinute bpm: Double, + maximumBeatsPerMinute maxBPM: Double, + upperBounds: [Double] = [0.5, 0.64, 0.77] + ) -> Self { + guard maxBPM > 0 else { + return .resting + } + let fraction = bpm / maxBPM + let zones = Self.allCases + for (index, bound) in upperBounds.enumerated() where fraction < bound { + return zones[index] + } + return .vigorous + } +} diff --git a/examples/swift/Sources/RobotStatusView.swift b/examples/swift/Sources/RobotStatusView.swift new file mode 100644 index 0000000..0132329 --- /dev/null +++ b/examples/swift/Sources/RobotStatusView.swift @@ -0,0 +1,50 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +import SwiftUI + +/// Shows whether the robot is connected and what it was last told to do. +struct RobotStatusView: View { + let viewModel: TeleopViewModel + + @State private var showsDetails = false + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Label(statusText, systemImage: statusSymbol) + .font(.headline) + .accessibilityLabel("Robot connection: \(statusText)") + + if showsDetails { + Text( + String( + format: "%.2f m/s, %.2f rad/s", + viewModel.lastCommand.linear, + viewModel.lastCommand.angular + ) + ) + .font(.caption.monospacedDigit()) + } + + Button(showsDetails ? "Hide details" : "Show details") { + showsDetails.toggle() + } + } + .padding() + } + + private var statusText: String { + switch viewModel.connectionState { + case .disconnected: + "Disconnected" + case .connected: + "Connected" + case .failed(let message): + "Error: \(message)" + } + } + + private var statusSymbol: String { + viewModel.connectionState == .connected ? "wifi" : "wifi.slash" + } +} diff --git a/examples/swift/Sources/TeleopViewModel.swift b/examples/swift/Sources/TeleopViewModel.swift new file mode 100644 index 0000000..645473d --- /dev/null +++ b/examples/swift/Sources/TeleopViewModel.swift @@ -0,0 +1,74 @@ +// Copyright 2026 Advanced Telerobotics Research Lab contributors, Kent State University +// SPDX-License-Identifier: MIT + +import Foundation +import Observation + +/// A velocity command for a mobile robot, in SI units (meters per second, radians per second). +struct VelocityCommand: Equatable, Sendable { + var linear: Double + var angular: Double + + static let stop = Self(linear: 0, angular: 0) +} + +/// Sends commands to the robot. +/// +/// The app uses a WebSocket client; tests use a fake. +protocol RobotLink: Sendable { + func send(_ command: VelocityCommand) async throws +} + +/// Drives the teleoperation screen: turns joystick input into limited velocity commands. +@MainActor +@Observable +final class TeleopViewModel { + enum ConnectionState: Equatable { + case disconnected + case connected + case failed(message: String) + } + + private(set) var connectionState: ConnectionState = .disconnected + private(set) var lastCommand: VelocityCommand = .stop + + private let link: any RobotLink + private let maxLinearSpeed: Double + private let maxAngularSpeed: Double + + init(link: any RobotLink, maxLinearSpeed: Double = 0.5, maxAngularSpeed: Double = 1.0) { + self.link = link + self.maxLinearSpeed = maxLinearSpeed + self.maxAngularSpeed = maxAngularSpeed + } + + /// Maps a joystick position (each axis from -1 to 1) to a command and sends it. + func joystickMoved(x: Double, y: Double) async { + let command = VelocityCommand( + linear: clamp(y, limit: 1) * maxLinearSpeed, + angular: -clamp(x, limit: 1) * maxAngularSpeed + ) + await send(command) + } + + /// Stops the robot. + /// + /// Called when the operator lets go of the joystick. + func joystickReleased() async { + await send(.stop) + } + + private func send(_ command: VelocityCommand) async { + do { + try await link.send(command) + lastCommand = command + connectionState = .connected + } catch { + connectionState = .failed(message: error.localizedDescription) + } + } + + private func clamp(_ value: Double, limit: Double) -> Double { + min(max(value, -limit), limit) + } +} diff --git a/examples/swift/bad/BadViewModel.swift b/examples/swift/bad/BadViewModel.swift new file mode 100644 index 0000000..b06757d --- /dev/null +++ b/examples/swift/bad/BadViewModel.swift @@ -0,0 +1,23 @@ +// Deliberately bad Swift: scripts/check-configs.sh expects swift-format (lint) and SwiftLint to +// reject this file. Do not copy it. Each comment names the rule that should fire. +import SwiftUI +import Foundation // swift-format OrderedImports + +class robot_view_model { // SwiftLint type_name, swift-format TypeNamesShouldBeCapitalized + var Speeds: [Double] = [] // SwiftLint identifier_name, swift-format AlwaysUseLowerCamelCase + + func load(path: String) -> [Double] { + let data = try! Data(contentsOf: URL(fileURLWithPath: path)) // force_try, NeverUseForceTry + let values = try! JSONDecoder().decode([Double].self, from: data); // DoNotUseSemicolons + if values.count == 0 { return [] } // SwiftLint empty_count + return values + } +} + +struct BadImageView: View { + @State var isOn = false // SwiftLint private_swiftui_state + + var body: some View { + Image("robot") // SwiftLint accessibility_label_for_image + } +} diff --git a/scripts/check-configs.sh b/scripts/check-configs.sh new file mode 100755 index 0000000..81c247f --- /dev/null +++ b/scripts/check-configs.sh @@ -0,0 +1,386 @@ +#!/usr/bin/env bash +# Runs every formatter and linter in configs/ against the samples in examples/. +# +# For each language, the good samples must pass and every file in examples//bad/ must be +# rejected, which proves the config is both correct and actually switched on. +# +# Usage: +# scripts/check-configs.sh # all languages +# scripts/check-configs.sh python swift # only these +# scripts/check-configs.sh --local swift # use tools installed on this machine, not Docker +# +# Languages: editorconfig markdown c-cpp python javascript-typescript swift kotlin csharp-unity +# +# Needs only Docker. Each check copies the samples and configs into a throwaway container, so +# nothing is written to your checkout. Without Docker (or with --local), the script uses local +# tools where it finds them (swift format, swiftlint, uvx, npm) and skips the rest. +# Exit status: 0 when every check that ran passed, 1 otherwise. +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ALL_LANGUAGES=(editorconfig markdown c-cpp python javascript-typescript swift kotlin csharp-unity) + +# Tool versions tested on Sept. 30, 2026. Change them together with configs/README.md. +ROS_IMAGE="ros:jazzy-ros-base" # Ubuntu 24.04: clang-format and clang-tidy 18 +UV_IMAGE="ghcr.io/astral-sh/uv:0.12.21-python3.12-trixie-slim" +RUFF_VERSION="0.16.9" +MYPY_VERSION="2.3.1" +PYRIGHT_VERSION="1.1.414" +NODE_IMAGE="node:24-bookworm-slim" +SWIFT_IMAGE="swift:6.4-noble" # swift-format ships in the toolchain +SWIFTLINT_IMAGE="ghcr.io/realm/swiftlint:0.65.1" +JAVA_IMAGE="eclipse-temurin:21-jre" +KTLINT_VERSION="1.8.0" +KTLINT_SHA256="a3fd620207d5c40da6ca789b95e7f823c54e854b7fade7f613e91096a3706d75" +DETEKT_VERSION="1.23.8" +DETEKT_SHA256="2ce2ff952e150baf28a29cda70a363b0340b3e81a55f43e51ec5edffc3d066c1" +DOTNET_IMAGE="mcr.microsoft.com/dotnet/sdk:10.0" +MARKDOWNLINT_IMAGE="davidanson/markdownlint-cli2:v0.23.3" +EDITORCONFIG_IMAGE="mstruebing/editorconfig-checker:4.0.2" +CPP_CHECK_IMAGE="atr-dev-guidelines-cpp:jazzy" # built locally from ROS_IMAGE (see check_c_cpp) +CACHE_VOLUME="atr-dev-guidelines-cache" # Docker volume for downloads (npm, NuGet, jars) + +USE_DOCKER=1 +declare -a RESULTS=() +FAILED=0 + +log() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } + +record() { # record + RESULTS+=("$1 $2") + if [[ "$1" == "FAIL" ]]; then + FAILED=1 + fi +} + +# expect_pass : the command must exit 0. +expect_pass() { + local name="$1" + shift + log "$name (must pass)" + if "$@"; then record PASS "$name"; else record FAIL "$name"; fi +} + +# expect_fail : the command must exit non-zero (the linter caught the bad file). +expect_fail() { + local name="$1" + shift + log "$name (must be rejected)" + if "$@"; then + echo "!! The linter accepted a deliberately bad file: its rules are not active." + record FAIL "$name" + else + echo "-- Rejected, as expected." + record PASS "$name" + fi +} + +# in_docker : runs the script in a container with the repo mounted read-only +# at /repo and a cache volume at /cache. +in_docker() { + local image="$1" script="$2" + docker run --rm -v "$REPO_ROOT:/repo:ro" -v "$CACHE_VOLUME:/cache" --entrypoint sh "$image" \ + -c "set -e; $script" +} + +have() { command -v "$1" >/dev/null 2>&1; } + +# -------------------------------------------------------------------------------------------- +# EditorConfig: every sample must follow configs/editorconfig/.editorconfig. +# The bad sample (tabs and trailing spaces) is generated here, because editors would "fix" a +# committed one. +check_editorconfig() { + local setup='mkdir -p /tmp/w && cp -r /repo/examples /tmp/w/ && rm -rf /tmp/w/examples/*/bad + cp /repo/configs/editorconfig/.editorconfig /tmp/w/ && cd /tmp/w' + if [[ $USE_DOCKER == 1 ]]; then + expect_pass "editorconfig: examples" in_docker "$EDITORCONFIG_IMAGE" \ + "$setup && /usr/bin/editorconfig-checker" + expect_fail "editorconfig: generated bad file" in_docker "$EDITORCONFIG_IMAGE" \ + "$setup && printf 'def f():\n\treturn 1 \n' > examples/bad.py && /usr/bin/editorconfig-checker" + else + record SKIP "editorconfig (needs Docker)" + fi +} + +# -------------------------------------------------------------------------------------------- +# Markdown: the README and AGENTS templates must pass configs/markdown/.markdownlint.jsonc. +check_markdown() { + local setup='mkdir -p /tmp/w && cp /repo/templates/*.md /tmp/w/ + cp /repo/configs/markdown/.markdownlint.jsonc /tmp/w/ && cd /tmp/w' + if [[ $USE_DOCKER == 1 ]]; then + expect_pass "markdown: templates" in_docker "$MARKDOWNLINT_IMAGE" \ + "$setup && markdownlint-cli2 '*.md'" + expect_fail "markdown: generated bad file" in_docker "$MARKDOWNLINT_IMAGE" \ + "$setup && rm ./*.md && printf '# Title\n## Skipped a blank line\nText with trailing spaces \n* mixed\n- markers\n' > bad.md && markdownlint-cli2 bad.md" + elif have npx; then + local work + work="$(mktemp -d)" + cp "$REPO_ROOT"/templates/*.md "$REPO_ROOT/configs/markdown/.markdownlint.jsonc" "$work/" + expect_pass "markdown: templates (local)" bash -c "cd '$work' && npx --yes markdownlint-cli2 '*.md'" + rm -rf "$work" + else + record SKIP "markdown (needs Docker or npx)" + fi +} + +# -------------------------------------------------------------------------------------------- +# C and C++: a ROS 2 package (teleop_velocity_limiter) and a C firmware module. +# clang-format and clang-tidy come from Ubuntu 24.04; the package is built and tested with +# colcon, so ament_clang_format, ament_cpplint, ament_lint_cmake, ament_xmllint, ament_flake8 +# and ament_pep257 all run against the same code. +check_c_cpp() { + if [[ $USE_DOCKER == 0 ]]; then + record SKIP "c-cpp (needs Docker: ROS 2 Jazzy image)" + return + fi + log "Building $CPP_CHECK_IMAGE from $ROS_IMAGE (cached after the first run)" + if ! docker build -q -t "$CPP_CHECK_IMAGE" - </dev/null \ + && colcon test-result --verbose" + expect_fail "c-cpp: clang-format rejects bad/" in_docker "$CPP_CHECK_IMAGE" \ + "$setup && clang-format --dry-run --Werror bad/*.cpp" + expect_fail "c-cpp: clang-tidy rejects bad/" in_docker "$CPP_CHECK_IMAGE" \ + "$setup && clang-tidy --quiet bad/*.cpp -- -std=c++17" +} + +# -------------------------------------------------------------------------------------------- +# Python: ruff (lint and format), mypy --strict, pyright and pytest; then the ROS 2 linters +# (ament_flake8, ament_pep257 with the Google convention) on the same files. +check_python() { + local copy='mkdir -p /tmp/w && cp -r /repo/examples/python/. /tmp/w/ + cp /repo/configs/python/ruff.toml /tmp/w/ && cp /repo/configs/python/pyproject-snippet.toml /tmp/w/pyproject.toml + cd /tmp/w' + # pyright's PyPI wrapper downloads Node.js, which needs libatomic on slim Debian images. + local prep="$copy && export UV_CACHE_DIR=/cache/uv MYPYPATH=src PYTHONPATH=src" + local ruff="uvx ruff@$RUFF_VERSION" mypy="uvx --with pytest mypy@$MYPY_VERSION" + local pyright="uvx --with pytest pyright@$PYRIGHT_VERSION" pytest="uvx pytest" + local libatomic="apt-get update -qq && apt-get install -y -qq libatomic1 >/dev/null" + if [[ $USE_DOCKER == 1 ]]; then + expect_pass "python: ruff check, ruff format, mypy, pyright, pytest" in_docker "$UV_IMAGE" \ + "$libatomic && $prep && $ruff --version && $ruff check --exclude bad . \ + && $ruff format --check --exclude bad . && $mypy src tests && $pyright src tests && $pytest -q" + expect_fail "python: ruff check rejects bad/" in_docker "$UV_IMAGE" "$prep && $ruff check bad" + expect_fail "python: ruff format rejects bad/" in_docker "$UV_IMAGE" "$prep && $ruff format --check bad" + expect_fail "python: mypy rejects bad/" in_docker "$UV_IMAGE" "$prep && $mypy bad" + if docker image inspect "$CPP_CHECK_IMAGE" >/dev/null 2>&1; then + expect_pass "python: ament_flake8 and ament_pep257 (ROS 2) accept the same code" \ + in_docker "$CPP_CHECK_IMAGE" ". /opt/ros/jazzy/setup.sh && $copy && ament_flake8 src tests \ + && ament_pep257 src tests --convention google \ + --add-ignore D100 D101 D102 D103 D104 D105 D106 D107" + else + record SKIP "python: ROS 2 linters (run the c-cpp check first to build its image)" + fi + elif have uvx; then + local work + work="$(mktemp -d)" + cp -r "$REPO_ROOT/examples/python/." "$work/" + cp "$REPO_ROOT/configs/python/ruff.toml" "$work/" + cp "$REPO_ROOT/configs/python/pyproject-snippet.toml" "$work/pyproject.toml" + local env="cd '$work' && export MYPYPATH=src PYTHONPATH=src" + expect_pass "python: ruff, mypy, pyright, pytest (local)" bash -c "$env \ + && $ruff check --exclude bad . && $ruff format --check --exclude bad . && $mypy src tests \ + && $pyright src tests && $pytest -q" + expect_fail "python: ruff check rejects bad/ (local)" bash -c "$env && $ruff check bad" + expect_fail "python: mypy rejects bad/ (local)" bash -c "$env && $mypy bad" + rm -rf "$work" + else + record SKIP "python (needs Docker or uv)" + fi +} + +# -------------------------------------------------------------------------------------------- +# JavaScript and TypeScript: Prettier, ESLint (typed rules) and tsc with the versions pinned in +# configs/javascript-typescript/package-snippet.json. +check_javascript_typescript() { + local prep='mkdir -p /tmp/w && cp -r /repo/examples/javascript-typescript/. /tmp/w/ + cd /repo/configs/javascript-typescript && cp eslint.config.mjs .prettierrc.json tsconfig.base.json /tmp/w/ + cd /tmp/w && node -e " + const s = require(\"/repo/configs/javascript-typescript/package-snippet.json\"); + delete s[\"//\"]; s.name = \"config-check\"; s.private = true; + require(\"fs\").writeFileSync(\"package.json\", JSON.stringify(s, null, 2));" + npm install --no-audit --no-fund --loglevel=error --cache /cache/npm + npx eslint --version && npx prettier --version && npx tsc --version' + if [[ $USE_DOCKER == 1 ]]; then + expect_pass "javascript-typescript: prettier, eslint, tsc" in_docker "$NODE_IMAGE" \ + "$prep && npx prettier --check src eslint.config.mjs tsconfig.json tsconfig.base.json \ + && npx eslint src eslint.config.mjs && npx tsc --noEmit -p tsconfig.json" + expect_fail "javascript-typescript: prettier rejects bad/" in_docker "$NODE_IMAGE" \ + "$prep >/dev/null && npx prettier --check bad/*.ts" + expect_fail "javascript-typescript: eslint rejects bad/" in_docker "$NODE_IMAGE" \ + "$prep >/dev/null && npx eslint bad" + expect_fail "javascript-typescript: tsc rejects bad/" in_docker "$NODE_IMAGE" \ + "$prep >/dev/null && npx tsc --noEmit -p bad/tsconfig.json" + elif have npm && have node; then + local work + work="$(mktemp -d)" + cp -r "$REPO_ROOT/examples/javascript-typescript/." "$work/" + cp "$REPO_ROOT"/configs/javascript-typescript/{eslint.config.mjs,.prettierrc.json,tsconfig.base.json} "$work/" + node -e " + const s = require('$REPO_ROOT/configs/javascript-typescript/package-snippet.json'); + delete s['//']; s.name = 'config-check'; s.private = true; + require('fs').writeFileSync('$work/package.json', JSON.stringify(s, null, 2));" + local env="cd '$work' && npm install --no-audit --no-fund --loglevel=error" + expect_pass "javascript-typescript: prettier, eslint, tsc (local)" bash -c "$env \ + && npx prettier --check src eslint.config.mjs tsconfig.json tsconfig.base.json \ + && npx eslint src eslint.config.mjs && npx tsc --noEmit -p tsconfig.json" + expect_fail "javascript-typescript: eslint rejects bad/ (local)" bash -c "cd '$work' && npx eslint bad" + expect_fail "javascript-typescript: tsc rejects bad/ (local)" bash -c \ + "cd '$work' && npx tsc --noEmit -p bad/tsconfig.json" + rm -rf "$work" + else + record SKIP "javascript-typescript (needs Docker or Node.js)" + fi +} + +# -------------------------------------------------------------------------------------------- +# Swift: swift-format (bundled with the Swift 6 toolchain) and SwiftLint. +check_swift() { + local prep='mkdir -p /tmp/w && cp -r /repo/examples/swift/. /tmp/w/ + cp /repo/configs/swift/.swift-format /repo/configs/swift/.swiftlint.yml /tmp/w/ && cd /tmp/w' + if [[ $USE_DOCKER == 1 ]]; then + expect_pass "swift: swift format lint" in_docker "$SWIFT_IMAGE" \ + "$prep && swift --version && swift format lint --strict --recursive Sources" + expect_pass "swift: swiftlint" in_docker "$SWIFTLINT_IMAGE" \ + "$prep && swiftlint version && swiftlint lint --strict --quiet Sources" + expect_fail "swift: swift format rejects bad/" in_docker "$SWIFT_IMAGE" \ + "$prep && swift format lint --strict --recursive bad" + expect_fail "swift: swiftlint rejects bad/" in_docker "$SWIFTLINT_IMAGE" \ + "$prep && swiftlint lint --strict --quiet bad" + elif have swift && have swiftlint; then + local work + work="$(mktemp -d)" + cp -r "$REPO_ROOT/examples/swift/." "$work/" + cp "$REPO_ROOT/configs/swift/.swift-format" "$REPO_ROOT/configs/swift/.swiftlint.yml" "$work/" + expect_pass "swift: swift format lint (local)" bash -c \ + "cd '$work' && swift format lint --strict --recursive Sources" + expect_pass "swift: swiftlint $(swiftlint version) (local)" bash -c \ + "cd '$work' && swiftlint lint --strict --quiet Sources" + expect_fail "swift: swift format rejects bad/ (local)" bash -c \ + "cd '$work' && swift format lint --strict --recursive bad" + expect_fail "swift: swiftlint rejects bad/ (local)" bash -c \ + "cd '$work' && swiftlint lint --strict --quiet bad" + rm -rf "$work" + else + record SKIP "swift (needs Docker, or swift and swiftlint)" + fi +} + +# -------------------------------------------------------------------------------------------- +# Kotlin: ktlint (reads .editorconfig) and detekt, from their official GitHub releases, +# checked against pinned SHA-256 sums. +check_kotlin() { + if [[ $USE_DOCKER == 0 ]]; then + record SKIP "kotlin (needs Docker)" + return + fi + local prep="set -eu + mkdir -p /cache/kotlin && cd /cache/kotlin + if [ ! -f ktlint-$KTLINT_VERSION ]; then + curl -fsSL -o ktlint-$KTLINT_VERSION \ + https://github.com/ktlint/ktlint/releases/download/$KTLINT_VERSION/ktlint + fi + if [ ! -f detekt-cli-$DETEKT_VERSION-all.jar ]; then + curl -fsSL -o detekt-cli-$DETEKT_VERSION-all.jar \ + https://github.com/detekt/detekt/releases/download/v$DETEKT_VERSION/detekt-cli-$DETEKT_VERSION-all.jar + fi + echo '$KTLINT_SHA256 ktlint-$KTLINT_VERSION' | sha256sum -c - + echo '$DETEKT_SHA256 detekt-cli-$DETEKT_VERSION-all.jar' | sha256sum -c - + chmod +x ktlint-$KTLINT_VERSION + mkdir -p /tmp/w && cp -r /repo/examples/kotlin/. /tmp/w/ + cp /repo/configs/kotlin/.editorconfig /repo/configs/kotlin/detekt.yml /tmp/w/ && cd /tmp/w" + local ktlint="/cache/kotlin/ktlint-$KTLINT_VERSION" + local detekt="java -jar /cache/kotlin/detekt-cli-$DETEKT_VERSION-all.jar --config detekt.yml --build-upon-default-config" + expect_pass "kotlin: ktlint, detekt" in_docker "$JAVA_IMAGE" \ + "$prep && $ktlint --version && $ktlint 'src/**/*.kt' && $detekt --input src" + expect_fail "kotlin: ktlint rejects bad/" in_docker "$JAVA_IMAGE" "$prep && $ktlint 'bad/**/*.kt'" + expect_fail "kotlin: detekt rejects bad/" in_docker "$JAVA_IMAGE" "$prep && $detekt --input bad" +} + +# -------------------------------------------------------------------------------------------- +# C# and Unity: the check/ harness compiles the Unity scripts against small UnityEngine stubs +# with the lab .editorconfig and Microsoft.Unity.Analyzers, then runs dotnet format. +check_csharp_unity() { + if [[ $USE_DOCKER == 0 ]]; then + record SKIP "csharp-unity (needs Docker)" + return + fi + local prep='export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1 NUGET_PACKAGES=/cache/nuget + mkdir -p /tmp/w && cp -r /repo/examples/csharp-unity/. /tmp/w/ + cp /repo/configs/csharp-unity/.editorconfig /tmp/w/ && cd /tmp/w && dotnet --version' + expect_pass "csharp-unity: dotnet build (analyzers), dotnet format" in_docker "$DOTNET_IMAGE" \ + "$prep && dotnet build check/CheckHarness.csproj -nologo -warnaserror \ + && dotnet format check/CheckHarness.csproj --verify-no-changes --severity warn \ + && dotnet format whitespace Assets --folder --verify-no-changes && echo 'dotnet format: clean'" + expect_fail "csharp-unity: dotnet build rejects bad/" in_docker "$DOTNET_IMAGE" \ + "$prep && dotnet build check/CheckHarness.csproj -nologo -p:CheckBad=true" + expect_fail "csharp-unity: dotnet format rejects bad/" in_docker "$DOTNET_IMAGE" \ + "$prep && dotnet format whitespace bad --folder --verify-no-changes" +} + +# -------------------------------------------------------------------------------------------- +main() { + local languages=() + for arg in "$@"; do + case "$arg" in + --local) USE_DOCKER=0 ;; + -h | --help) sed -n '2,19p' "${BASH_SOURCE[0]}"; exit 0 ;; + *) languages+=("$arg") ;; + esac + done + if [[ ${#languages[@]} -eq 0 ]]; then + languages=("${ALL_LANGUAGES[@]}") + fi + if [[ $USE_DOCKER == 1 ]] && ! docker info >/dev/null 2>&1; then + echo "Docker is not running; falling back to local tools (--local)." + USE_DOCKER=0 + fi + + for language in "${languages[@]}"; do + case "$language" in + editorconfig) check_editorconfig ;; + markdown) check_markdown ;; + c-cpp) check_c_cpp ;; + python) check_python ;; + javascript-typescript) check_javascript_typescript ;; + swift) check_swift ;; + kotlin) check_kotlin ;; + csharp-unity) check_csharp_unity ;; + *) echo "Unknown language: $language (choose from: ${ALL_LANGUAGES[*]})"; exit 2 ;; + esac + done + + log "Summary" + printf '%s\n' "${RESULTS[@]}" + if [[ $FAILED == 1 ]]; then + echo "Some checks failed." + exit 1 + fi + echo "All checks that ran passed." +} + +main "$@" diff --git a/templates/AGENTS.template.md b/templates/AGENTS.template.md new file mode 100644 index 0000000..ceb41cf --- /dev/null +++ b/templates/AGENTS.template.md @@ -0,0 +1,67 @@ +# AGENTS.md: [repo-name] + + + +[One paragraph: what this repo is (for example, "ROS 2 packages that drive the [robot model] and teleoperate it from a Meta Quest headset"), who uses it (lab members running [experiment or course]) and what it is not (for example, "not the robot's firmware, which lives in [repo]").] + +## Repo map + +Every folder and important file, one line each. + +| Path | What it holds | +|---|---| +| `README.md` | Human quick start | +| `AGENTS.md` | This file | +| `[folder]/` | [description] | + +## Sources of truth + +- Robot and hardware facts (joint limits, voltages, ports): [datasheet or vendor docs URL] and `[path to config]`. +- Software versions: `package.xml`, `pyproject.toml`, `package.json` or `[other lockfile]`; do not assume newer versions. +- Lab facts (names, rooms, people, results): only what is written in this repo or given by a person. **Never invent names, rooms, results, versions or dates: use a `[bracketed placeholder]` and add it to Open items.** + +## Writing rules + +- Code style: the lab configs from [dev-guidelines](https://github.com/ATR-Lab/dev-guidelines/tree/master/configs), copied into this repo: [list the config files, for example `.clang-format`, `.clang-tidy`, `ruff.toml`]. +- Commit messages: [the seven rules / Conventional Commits], subject line under 50 characters, imperative mood. +- Branches: `/`; never commit to the default branch; never force-push. +- Units: SI (meters, radians, seconds); frames follow REP 103 and REP 105. +- Docs: sentence-case headings; commands in fenced blocks, one per line, no `$` prompts. + +## How to check your work + +Run these before you say a task is done, and report their output. + +```bash +[format check command] +[lint command] +[build command] +[test command] +``` + +## Safety rules + +- Never run code that moves the robot ([launch files or scripts that command motion]) unless a person confirms they are watching it with the emergency stop in reach. +- Never change speed, force or workspace limits (`[path to limits file]`) without flagging it in the pull request as a safety change. +- Never flash firmware or change network settings on the robot. +- Never read or copy study data from `[path, if any]`; it may contain identifiable information. + +## Open items + +Placeholders that only a person can fill. Agents must not fill these by guessing. + +| File | Placeholder | Who can fill it | +|---|---|---| +| `[file]` | `[placeholder]` | [maintainer / advisor / lab manager] | + +## Related repos + +- [dev-guidelines](https://github.com/ATR-Lab/dev-guidelines): code style, Git workflow and configs +- [getting-started-atr-lab](https://github.com/ATR-Lab/getting-started-atr-lab): lab onboarding, safety and hardware +- [all-things-ros2](https://github.com/ATR-Lab/all-things-ros2): ROS 2 basics and installation +- [other lab repos this one depends on] diff --git a/templates/README.template.md b/templates/README.template.md new file mode 100644 index 0000000..7e38649 --- /dev/null +++ b/templates/README.template.md @@ -0,0 +1,90 @@ +# [Project name] + + + +[One sentence: what this is, and who it is for. Example: "ROS 2 driver and teleoperation launch files for the [robot model], for lab members running experiments on it."] + +> [!NOTE] +> **Status:** [active / maintained / archived]. Last tested on [date] with [robot model and firmware version], [Ubuntu version] and [ROS 2 distribution]. + +## Quick start + +The fewest steps from a clean machine to something running. For [Ubuntu version] with [ROS 2 distribution]: + +```bash +# 1. Get the code +git clone [repo URL] +cd [repo folder] +# 2. Install dependencies +rosdep install --from-paths src --ignore-src -y +# 3. Build +colcon build +# 4. Run +source install/setup.bash +ros2 launch [package] [file].launch.py +``` + +## Requirements + +| What | Version or detail | +|---|---| +| Robot or hardware | [robot model, sensors, cables] | +| Operating system | [Ubuntu version / macOS version] | +| ROS 2 distribution | [distribution] | +| Other software | [SDK, Unity version, Xcode version] | +| Accounts or licenses | [what is needed and who to ask] | + +## Usage + +[Common tasks, one short subsection each: how to start the robot, how to record data, how to change a parameter. Link launch files and parameter files by path.] + +| Parameter | Default | Unit | What it does | +|---|---|---|---| +| `[name]` | `[value]` | [unit] | [description] | + +## Safety + +> [!WARNING] +> [What can go wrong with the hardware (unexpected motion, pinch points, battery hazards) and how to stop it (emergency stop location, software stop command). Follow the lab safety guide.] + +## Troubleshooting + +| Problem | Fix | +|---|---| +| [Error message or symptom] | [What to do] | + +## Repo map + +| Path | What it holds | +|---|---| +| `[folder]/` | [description] | + +## Contributing + +Branches, commits, code style and checks follow the lab's [dev guidelines](https://github.com/ATR-Lab/dev-guidelines). Before a pull request, run: + +```bash +[format, lint and test commands] +``` + +Coding agents: read `AGENTS.md` first. + +## License and citation + +[License name]; see `LICENSE`. + +If you use this work in a publication, please cite: + +```bibtex +[BibTeX entry, or delete this block] +``` + +[Funding acknowledgment exactly as the award requires, or delete this line: "This material is based upon work supported by [Sponsor] under award No. [Grant No.]."] + +--- + +Advanced Telerobotics Research Lab · Department of Computer Science · Kent State University