From e05073e697cf1b134cd563102ac9443eeb0249e5 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 21 May 2026 17:25:20 +0900 Subject: [PATCH] Prepare project * Add missing license information * Add CoC * Add CI * Add linter * Add release scripts * Prepare issues * Remove a needless file --- .editorconfig | 23 +++ .github/ISSUE_TEMPLATE/1-bug-report.yml | 25 +++ .github/ISSUE_TEMPLATE/2-feature-request.yml | 25 +++ .github/ISSUE_TEMPLATE/config.yml | 22 ++ .github/copilot-instructions.md | 4 + .github/dependabot.yml | 20 ++ main.py => .github/release.yml | 10 +- .github/workflows/lint.yaml | 43 ++++ .github/workflows/package.yaml | 112 ++++++++++ .gitignore | 14 ++ .pre-commit-config.yaml | 26 +++ CODE_OF_CONDUCT.md | 133 ++++++++++++ CONTRIBUTING.md | 19 ++ LICENSE.txt | 202 +++++++++++++++++++ README.md | 18 +- dev/README.md | 9 + dev/release.sh | 69 +++++++ pyproject.toml | 21 +- src/openarm_control/__init__.py | 8 +- src/openarm_control/config.py | 11 +- src/openarm_control/kinematics.py | 119 ++++++++--- src/openarm_control/poses.py | 2 +- 22 files changed, 890 insertions(+), 45 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/1-bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/2-feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/copilot-instructions.md create mode 100644 .github/dependabot.yml rename main.py => .github/release.yml (86%) create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/workflows/package.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 dev/README.md create mode 100755 dev/release.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..52dd625 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +root = true + +[*.sh] + +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml new file mode 100644 index 0000000..640996c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -0,0 +1,25 @@ +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Bug Report +description: File a bug report. +type: Bug +body: + - type: textarea + id: description + attributes: + label: Describe the problem you got + description: It's better that you provide information as much as possible. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.yml b/.github/ISSUE_TEMPLATE/2-feature-request.yml new file mode 100644 index 0000000..437fab0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature-request.yml @@ -0,0 +1,25 @@ +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Feature Request +description: Request a feature. +type: Feature +body: + - type: textarea + id: description + attributes: + label: Describe the feature you want + description: It's better that you also provide your use case. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..1ca4473 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,22 @@ +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +blank_issues_enabled: false +contact_links: + - name: Discord + url: https://discord.gg/FsZaZ4z3We + about: Please ask and answer questions here. + - name: GitHub Discussions + url: https://github.com/enactic/openarm_control/discussions + about: If you prefer GitHub Discussions to Discord, you can use GitHub Discussions too. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..6bcb9e5 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,4 @@ +# GitHub Copilot instructions + +- In pull request descriptions, the first line must be `Fix GH-${ISSUE_NUMBER}` with `${ISSUE_NUMBER}` replaced by the corresponding issue number (for example, `Fix GH-123`). +- Do not add a `Changes` section to pull request descriptions. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..40cb670 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/main.py b/.github/release.yml similarity index 86% rename from main.py rename to .github/release.yml index 6c4645b..b81175b 100644 --- a/main.py +++ b/.github/release.yml @@ -12,9 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -def main(): - print("Hello from openarm-control!") - - -if __name__ == "__main__": - main() +changelog: + exclude: + authors: + - dependabot[bot] diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..728e624 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,43 @@ +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Lint +on: + push: + branches-ignore: + - 'copilot/**' + - 'dependabot/**' + pull_request: +concurrency: + group: ${{ github.head_ref || github.sha }}-${{ github.workflow }} + cancel-in-progress: true +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - name: Install pre-commit + run: | + python -m pip install pre-commit + - uses: actions/cache@v5 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Run pre-commit + run: | + pre-commit run --show-diff-on-failure --color=always --all-files diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml new file mode 100644 index 0000000..69ec21d --- /dev/null +++ b/.github/workflows/package.yaml @@ -0,0 +1,112 @@ +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Package +on: + push: + branches-ignore: + - 'copilot/**' + - 'dependabot/**' + tags: + - '**' + pull_request: +concurrency: + group: ${{ github.head_ref || github.sha }}-${{ github.workflow }} + cancel-in-progress: true +jobs: + source: + name: Source + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - name: Prepare environment variables + run: | + PACKAGE_NAME=$(yq --unwrapScalar .project.name pyproject.toml) + echo "PACKAGE_NAME=${PACKAGE_NAME}" >> "${GITHUB_ENV}" + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + echo "VERSION=${GITHUB_REF_NAME}" >> "${GITHUB_ENV}" + else + VERSION=$(yq --unwrapScalar .project.version pyproject.toml) + echo "VERSION=${VERSION}" >> "${GITHUB_ENV}" + fi + - uses: actions/setup-python@v6 + with: + python-version: 3 + - name: Install dependencies + run: | + pip install build + - name: Create source archive + run: | + python -m build --sdist + cd dist + sha256sum \ + ${PACKAGE_NAME}-${VERSION}.tar.gz > \ + ${PACKAGE_NAME}-${VERSION}.tar.gz.sha256 + sha512sum \ + ${PACKAGE_NAME}-${VERSION}.tar.gz > \ + ${PACKAGE_NAME}-${VERSION}.tar.gz.sha512 + - uses: actions/upload-artifact@v7 + with: + name: source + path: | + dist/${{ env.PACKAGE_NAME }}-${{ env.VERSION }}.tar.gz + dist/${{ env.PACKAGE_NAME }}-${{ env.VERSION }}.tar.gz.sha256 + dist/${{ env.PACKAGE_NAME }}-${{ env.VERSION }}.tar.gz.sha512 + release: + name: Release + if: github.ref_type == 'tag' + needs: + - source + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: release + permissions: + contents: write + discussions: write + steps: + - uses: actions/download-artifact@v8 + with: + name: source + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + version=${GITHUB_REF_NAME} + gh release create ${version} \ + --discussion-category Announcements \ + --generate-notes \ + --repo ${GITHUB_REPOSITORY} \ + --title "OpenArm Control ${version}" \ + --verify-tag \ + *.tar.gz* + pypi: + name: PyPI + if: github.ref_type == 'tag' + needs: + - source + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v8 + with: + name: source + - name: Prepare directory structure + run: | + mkdir -p dist + mv *.tar.gz dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 8e42a72..ed10b02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..29d9209 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.11 + hooks: + - id: ruff-check + args: + - --fix + - id: ruff-format + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.12.0-2 + hooks: + - id: shfmt diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..bee83e9 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +hi_public@reazon.jp. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0969010 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# How to contribute + +## Did you find a bug? + +Please report it to [GitHub Issues](https://github.com/enactic/openarm_control/issues/new?template=1-bug-report.yml)! + +## Did you have a feature request? + +Please share it to [GitHub Issues](https://github.com/enactic/openarm_control/issues/new?template=2-feature-request.yml)! + +## Did you write a patch? + +Please open a pull request with it! + +Please make sure to review [our license](https://github.com/enactic/openarm_control/blob/main/LICENSE.txt) before you open a pull request. + +## Others? + +Please share it on [Discord](https://discord.gg/FsZaZ4z3We) or [GitHub Discussions](https://github.com/enactic/openarm_control/discussions)! diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 2c1b1eb..ac4b0cf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# openarm_control +# OpenArm Control Reusable kinematics and control utilities for OpenArm, backed by MuJoCo and [mink](https://github.com/kevinzakka/mink). @@ -12,7 +12,6 @@ uv sync ### `Kinematics` - ```python from openarm_control import Kinematics, IKParams, ArmSetup @@ -45,3 +44,18 @@ Solver configuration passed to `Kinematics`. All fields have defaults. | `max_iters` | `5` | IK iterations per solve | Build from CLI args with `register_ik_args` + `ik_params_from_args`: + +## Related links + +- 💬 Join the community on [Discord](https://discord.gg/FsZaZ4z3We) +- 📬 Contact us through + +## License + +Licensed under the Apache License 2.0. See [LICENSE.txt](LICENSE.txt) for details. + +Copyright 2026 Enactic, Inc. + +## Code of Conduct + +All participation in the OpenArm project is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000..c37c642 --- /dev/null +++ b/dev/README.md @@ -0,0 +1,9 @@ +# Development + +## How to release + +```bash +git clone git@github.com:enactic/openarm_control.git +cd openarm_control +dev/release.sh ${VERSION} # e.g. dev/release.sh 1.0.0 +``` diff --git a/dev/release.sh b/dev/release.sh new file mode 100755 index 0000000..76702ae --- /dev/null +++ b/dev/release.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# +# Copyright 2026 Enactic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eu + +if [ $# -ne 1 ]; then + echo "Usage: $0 version" + echo " e.g.: $0 1.0.0" + exit 0 +fi + +version="$1" + +base_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.." + +cd "${base_dir}" + +repository_name=$( + python3 <=64", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "openarm-control" +name = "openarm_control" version = "0.1.0" description = "Kinematics and control utilities for OpenArm" +authors = [{name = "Enactic, Inc."}] +license = {text = "Apache-2.0"} readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.10" + dependencies = [ "daqp==0.7.2", "mink>=1.1.0", @@ -30,11 +33,25 @@ dependencies = [ "openarm-mujoco", ] +[project.urls] +Changelog = "https://github.com/enactic/openarm_control/releases" +Issues = "https://github.com/enactic/openarm_control/issues" +Repository = "https://github.com/enactic/openarm_control.git" + [tool.setuptools] package-dir = {"" = "src"} [tool.setuptools.packages.find] where = ["src"] +[tool.ruff.lint] +extend-select = [ + "D", # pydocstyle + "UP", +] + +[tool.ruff.lint.per-file-ignores] +"!src/**.py" = ["D"] + [tool.uv.sources] openarm-mujoco = { git = "https://github.com/enactic/openarm_mujoco.git", subdirectory = "v2", rev = "cba9759f7245ae3925f91c6600bdccc130cbf4e3" } diff --git a/src/openarm_control/__init__.py b/src/openarm_control/__init__.py index 3e7ac6f..8659d23 100644 --- a/src/openarm_control/__init__.py +++ b/src/openarm_control/__init__.py @@ -12,14 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from openarm_control.config import ArmSetup, register_common_args, setup_from_args -from openarm_control.kinematics import ( +"""Kinematics and control utilities for OpenArm.""" + +from .config import ArmSetup, register_common_args, setup_from_args +from .kinematics import ( IKParams, Kinematics, register_ik_args, ik_params_from_args, ) -from openarm_control.poses import read_ee_pose, pose_to_se3, se3_to_pose +from .poses import read_ee_pose, pose_to_se3, se3_to_pose __all__ = [ # context diff --git a/src/openarm_control/config.py b/src/openarm_control/config.py index 90d28eb..3f44b03 100644 --- a/src/openarm_control/config.py +++ b/src/openarm_control/config.py @@ -19,6 +19,7 @@ import argparse import mujoco +import numpy as np import openarm_mujoco_v2 as openarm_mujoco from openarm_mujoco_v2 import JointResolver @@ -66,11 +67,12 @@ def __init__( frame_ids: dict[str, int], frame_types: dict[str, str], ) -> None: + """Initialize.""" self.model = model self.data = data self.joint_resolver = joint_resolver self.sides = sides - self.frame_ids = frame_ids # side → MuJoCo object ID + self.frame_ids = frame_ids # side → MuJoCo object ID self.frame_types = frame_types # side → "body" | "site" | "geom" @classmethod @@ -84,6 +86,7 @@ def from_args( frame_type_left: str, keyframe: str | None = "home", ) -> ArmSetup: + """Build from XML.""" model = mujoco.MjModel.from_xml_path(xml) data = mujoco.MjData(model) @@ -119,9 +122,10 @@ def from_args( frame_types=frame_types, ) - def read_ee_pose(self, side: str) -> "np.ndarray": + def read_ee_pose(self, side: str) -> np.ndarray: """Return float32[7] = [px, py, pz, qw, qx, qy, qz] for the given arm.""" from openarm_control.poses import read_ee_pose + return read_ee_pose(self.data, self.frame_ids[side], self.frame_types[side]) @@ -143,7 +147,8 @@ def register_common_args(parser: argparse.ArgumentParser) -> None: help=f"MJCF scene file (default: {_DEFAULT_XML})", ) parser.add_argument( - "--keyframe", "-k", + "--keyframe", + "-k", default="home", help="Initial keyframe name (default: home)", ) diff --git a/src/openarm_control/kinematics.py b/src/openarm_control/kinematics.py index 2f37c66..e8cc0df 100644 --- a/src/openarm_control/kinematics.py +++ b/src/openarm_control/kinematics.py @@ -65,6 +65,7 @@ class Kinematics: """ def __init__(self, setup: ArmSetup, ik_params: IKParams | None = None) -> None: + """Initialize.""" self.setup = setup self._ik: _IKSolver | None = ( _IKSolver(setup, ik_params) if ik_params is not None else None @@ -98,7 +99,7 @@ def sync(self, values16: np.ndarray) -> None: self._require_ik().sync(values16) def ready(self) -> bool: - """True once all active arms have received at least one target this cycle.""" + """Return True once all active arms have received at least one target this cycle.""" return self._require_ik().ready() def solve(self) -> np.ndarray | None: @@ -118,6 +119,7 @@ def _require_ik(self) -> _IKSolver: # ── internal IK implementation ──────────────────────────────────────────────── + class _IKSolver: """mink QP-based differential IK. Managed by Kinematics; not public API.""" @@ -147,10 +149,9 @@ def __init__(self, setup: ArmSetup, params: IKParams) -> None: for side in setup.sides } - active_qpos: set[int] = ( - set(setup.joint_resolver._right.arm_qpos.tolist()) - | set(setup.joint_resolver._left.arm_qpos.tolist()) - ) + active_qpos: set[int] = set( + setup.joint_resolver._right.arm_qpos.tolist() + ) | set(setup.joint_resolver._left.arm_qpos.tolist()) freeze_dofs = [ int(setup.model.jnt_dofadr[j]) for j in range(setup.model.njnt) @@ -160,7 +161,8 @@ def __init__(self, setup: ArmSetup, params: IKParams) -> None: print(freeze_dofs) self._freeze_task: mink.DofFreezingTask | None = ( mink.DofFreezingTask(model=setup.model, dof_indices=freeze_dofs) - if freeze_dofs else None + if freeze_dofs + else None ) self._limits = [mink.ConfigurationLimit(setup.model)] @@ -202,19 +204,31 @@ def solve(self) -> np.ndarray | None: for _ in range(self._max_iters): try: vel = mink.solve_ik( - self._config, tasks, self._dt, self._solver_name, - limits=self._limits, constraints=constraints, - safety_break=False, **self._solver_params, + self._config, + tasks, + self._dt, + self._solver_name, + limits=self._limits, + constraints=constraints, + safety_break=False, + **self._solver_params, ) except mink.exceptions.NoSolutionFound: try: vel = mink.solve_ik( - self._config, tasks, self._dt, self._solver_name, - limits=[], constraints=constraints, - safety_break=False, **self._solver_params, + self._config, + tasks, + self._dt, + self._solver_name, + limits=[], + constraints=constraints, + safety_break=False, + **self._solver_params, ) except mink.exceptions.NoSolutionFound: - print("Warning: IK solver failed (constrained and unconstrained). Skipping step.") + print( + "Warning: IK solver failed (constrained and unconstrained). Skipping step." + ) return None self._config.integrate_inplace(vel, self._dt) @@ -223,10 +237,12 @@ def solve(self) -> np.ndarray | None: qpos = self._config.data.qpos right_joints, _ = self._joint_resolver.get_driver(qpos, "right") left_joints, _ = self._joint_resolver.get_driver(qpos, "left") - return np.concatenate([ - np.append(right_joints, self._gripper[0]), - np.append(left_joints, self._gripper[1]), - ]).astype(np.float32) + return np.concatenate( + [ + np.append(right_joints, self._gripper[0]), + np.append(left_joints, self._gripper[1]), + ] + ).astype(np.float32) def _frame_name(setup: ArmSetup, side: str) -> str: @@ -239,6 +255,7 @@ def _frame_name(setup: ArmSetup, side: str) -> str: }[ftype] return mujoco.mj_id2name(setup.model, obj, fid) + def _convert_velocity( rad_per_sec: float, dt: float, @@ -249,21 +266,67 @@ def _convert_velocity( raise ValueError("max_iters, dt, and tick_hz must all be positive.") return rad_per_sec / (max_iters * dt * tick_hz) + # ── CLI helpers ─────────────────────────────────────────────────────────────── + def register_ik_args(parser: argparse.ArgumentParser) -> None: """Register IK-specific CLI flags. Call after register_common_args.""" - parser.add_argument("--pos-cost", type=float, default=1.0, help="Position task cost (default: 1.0)") - parser.add_argument("--ori-cost", type=float, default=1.0, help="Orientation task cost (default: 1.0)") - parser.add_argument("--lm-damping", type=float, default=0.01, help="Per-task LM damping (default: 0.01)") - parser.add_argument("--damping", type=float, default=0.25, help="Global Tikhonov regularization (default: 0.25)") - parser.add_argument("--solver", default="daqp", help="QP backend (default: daqp)") - parser.add_argument("--max-iters", type=int, default=5, help="IK iterations per event (default: 5)") - parser.add_argument("--dt", type=float, default=0.1, help="Integration timestep per iteration (default: 0.1)") - parser.add_argument("--posture-cost", type=float, default=0.01, help="Posture task weight, 0=disabled (default: 0.01)") - parser.add_argument("--diag-reg", type=float, default=0.0, help="QP diagonal regularization (default: 0.0)") - parser.add_argument("--vel-scale", type=float, default=None, help="Scale velocity limit safety. 1=90deg/s for shoulder. Unset = VelocityLimit disabled.") - parser.add_argument("--tick-hz", type=float, default=500.0, help="Dora tick rate in Hz; used only for --vel-scale unit conversion") + parser.add_argument( + "--pos-cost", type=float, default=1.0, help="Position task cost (default: 1.0)" + ) + parser.add_argument( + "--ori-cost", + type=float, + default=1.0, + help="Orientation task cost (default: 1.0)", + ) + parser.add_argument( + "--lm-damping", + type=float, + default=0.01, + help="Per-task LM damping (default: 0.01)", + ) + parser.add_argument( + "--damping", + type=float, + default=0.25, + help="Global Tikhonov regularization (default: 0.25)", + ) + parser.add_argument("--solver", default="daqp", help="QP backend (default: daqp)") + parser.add_argument( + "--max-iters", type=int, default=5, help="IK iterations per event (default: 5)" + ) + parser.add_argument( + "--dt", + type=float, + default=0.1, + help="Integration timestep per iteration (default: 0.1)", + ) + parser.add_argument( + "--posture-cost", + type=float, + default=0.01, + help="Posture task weight, 0=disabled (default: 0.01)", + ) + parser.add_argument( + "--diag-reg", + type=float, + default=0.0, + help="QP diagonal regularization (default: 0.0)", + ) + parser.add_argument( + "--vel-scale", + type=float, + default=None, + help="Scale velocity limit safety. 1=90deg/s for shoulder. Unset = VelocityLimit disabled.", + ) + parser.add_argument( + "--tick-hz", + type=float, + default=500.0, + help="Dora tick rate in Hz; used only for --vel-scale unit conversion", + ) def ik_params_from_args(args: argparse.Namespace) -> IKParams: diff --git a/src/openarm_control/poses.py b/src/openarm_control/poses.py index 4916d91..25b027f 100644 --- a/src/openarm_control/poses.py +++ b/src/openarm_control/poses.py @@ -51,7 +51,7 @@ def pose_to_se3(pose: np.ndarray): # -> mink.SE3 wxyz_xyz = np.empty(7, dtype=np.float64) wxyz_xyz[:4] = pose[3:7] # quat: wxyz - wxyz_xyz[4:] = pose[:3] # translation: xyz + wxyz_xyz[4:] = pose[:3] # translation: xyz return mink.SE3(wxyz_xyz=wxyz_xyz)