├── packages ├── debian │ ├── source │ │ └── format │ ├── changelog │ ├── openarm-can-utils.install │ ├── libopenarm-can1.install │ ├── python3-openarm-can.install │ ├── libopenarm-can-dev.install │ ├── upstream │ │ └── metadata │ ├── rules │ ├── copyright │ └── control.in ├── apt │ ├── ubuntu-jammy-arm64 │ │ └── from │ ├── ubuntu-noble-arm64 │ │ └── from │ ├── ubuntu-jammy │ │ └── Dockerfile │ ├── ubuntu-noble │ │ └── Dockerfile │ └── test.sh ├── fedora │ └── openarm-can.spec └── Rakefile ├── python ├── README.md ├── requirements.txt ├── examples │ ├── test_posvel.py │ └── example.py ├── pyproject.toml ├── openarm_can │ └── __init__.py ├── setup.py ├── build.sh └── CMakeLists.txt ├── .clang-format ├── CONTRIBUTING.md ├── .cmake-format.py ├── OpenArmCANConfig.cmake.in ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── 1-bug-report.yml │ ├── 2-feature-request.yml │ └── config.yml └── workflows │ ├── test.yaml │ └── package.yaml ├── dev └── README.md ├── openarm-can.pc.in ├── .packit.yaml ├── .editorconfig ├── .gitignore ├── package.xml ├── include └── openarm │ ├── can │ └── socket │ │ ├── arm_component.hpp │ │ ├── gripper_component.hpp │ │ └── openarm.hpp │ ├── canbus │ ├── can_device_collection.hpp │ ├── can_device.hpp │ └── can_socket.hpp │ └── damiao_motor │ ├── dm_motor_device.hpp │ ├── dm_motor.hpp │ ├── dm_motor_device_collection.hpp │ ├── dm_motor_constants.hpp │ └── dm_motor_control.hpp ├── src └── openarm │ ├── can │ └── socket │ │ ├── arm_component.cpp │ │ ├── gripper_component.cpp │ │ └── openarm.cpp │ ├── canbus │ ├── can_device_collection.cpp │ └── can_socket.cpp │ └── damiao_motor │ ├── dm_motor.cpp │ ├── dm_motor_device.cpp │ ├── dm_motor_device_collection.cpp │ └── dm_motor_control.cpp ├── .pre-commit-config.yaml ├── helper.rb ├── setup ├── openarm-can-configure-socketcan ├── openarm-can-configure-socketcan-4-arms ├── openarm-can-set-zero ├── openarm_can_diagnosis.cpp ├── openarm-can-change-baudrate └── motor_check.cpp ├── README.md ├── examples └── demo.cpp ├── Rakefile ├── CMakeLists.txt ├── CODE_OF_CONDUCT.md └── LICENSE.txt /packages/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # OpenArm CAN Python bindings 2 | 3 | This is the Python bindings of OpenArm CAN library. 4 | 5 | > [!WARNING] 6 | > 7 | > ⚠️ **WARNING: UNSTABLE API** ⚠️ 8 | > Python bindings are currently a direct low level **temporary port**, and will change **DRASTICALLY**. 9 | > The interface is may break between versions.Use at your own risk! Discussions on the interface are welcomed. 10 | -------------------------------------------------------------------------------- /packages/debian/changelog: -------------------------------------------------------------------------------- 1 | openarm-can (1.2.1-1) unstable; urgency=low 2 | 3 | * New upstream release. 4 | 5 | -- Sutou Kouhei Tue, 23 Dec 2025 08:30:45 -0000 6 | 7 | openarm-can (1.2.0-1) unstable; urgency=low 8 | 9 | * New upstream release. 10 | 11 | -- Sutou Kouhei Tue, 23 Dec 2025 07:16:34 -0000 12 | 13 | openarm-can (1.1.0-1) unstable; urgency=low 14 | 15 | * New upstream release. 16 | 17 | -- Sutou Kouhei Fri, 10 Oct 2025 06:49:53 -0000 18 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /packages/debian/openarm-can-utils.install: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | usr/bin/ 16 | -------------------------------------------------------------------------------- /packages/debian/libopenarm-can1.install: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | usr/lib/*/lib*.so.* 16 | -------------------------------------------------------------------------------- /packages/apt/ubuntu-jammy-arm64/from: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | --platform=linux/arm64 arm64v8/ubuntu:jammy 16 | -------------------------------------------------------------------------------- /packages/apt/ubuntu-noble-arm64/from: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | --platform=linux/arm64 arm64v8/ubuntu:noble 16 | -------------------------------------------------------------------------------- /packages/debian/python3-openarm-can.install: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | usr/lib/python3*/dist-packages/openarm_can* 16 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | --- 16 | AccessModifierOffset: -4 17 | BasedOnStyle: Google 18 | ColumnLimit: 100 19 | IndentWidth: 4 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Did you find a bug? 4 | 5 | Please report it to [GitHub Issues](https://github.com/enactic/openarm_can/issues/new?template=1-bug-report.yml)! 6 | 7 | ## Did you have a feature request? 8 | 9 | Please share it to [GitHub Issues](https://github.com/enactic/openarm_can/issues/new?template=2-feature-request.yml)! 10 | 11 | ## Did you write a patch? 12 | 13 | Please open a pull request with it! 14 | 15 | Please make sure to review [our license](https://github.com/enactic/openarm_can/blob/main/LICENSE.txt) before you open a pull request. 16 | 17 | ## Others? 18 | 19 | Please share it on [Discord](https://discord.gg/FsZaZ4z3We) or [GitHub Discussions](https://github.com/enactic/openarm_can/discussions)! 20 | -------------------------------------------------------------------------------- /.cmake-format.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | with section("format"): 16 | command_case = "lower" 17 | 18 | with section("markup"): 19 | enable_markup = False 20 | -------------------------------------------------------------------------------- /packages/debian/libopenarm-can-dev.install: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | usr/include/ 16 | usr/lib/*/lib*.so 17 | usr/lib/*/pkgconfig/ 18 | usr/lib/*/cmake/OpenArmCAN/ 19 | -------------------------------------------------------------------------------- /OpenArmCANConfig.cmake.in: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | @PACKAGE_INIT@ 16 | 17 | include("${CMAKE_CURRENT_LIST_DIR}/OpenArmCANTargets.cmake") 18 | 19 | check_required_components(OpenArmCAN) 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | version: 2 16 | updates: 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## How to build C++ Library 4 | 5 | ### Prerequisites 6 | 7 | - CMake 3.22+ 8 | - C++17 compiler 9 | 10 | ### Build 11 | 12 | ```bash 13 | cd openarm_can 14 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release 15 | cmake --build build 16 | sudo cmake --install build 17 | ``` 18 | 19 | ## How to release 20 | 21 | ```bash 22 | export LAUNCHPAD_UPLOADER_PGP_KEY=YOUR_PGP_KEY # e.g. export LAUNCHPAD_UPLOADER_PGP_KEY=08D3564B7C6A9CAFBFF6A66791D18FCF079F8007 23 | git clone https://github.com/apache/arrow.git 24 | export APACHE_ARROW_REPOSITORY=${PWD}/arrow 25 | git clone https://github.com/groonga/groonga.git 26 | export GROONGA_REPOSITORY=${PWD}/groonga 27 | git clone git@github.com:enactic/openarm_can.git 28 | cd openarm_can 29 | rake release NEW_VERSION=X.Y.Z # e.g. rake release 1.0.0 30 | ``` 31 | -------------------------------------------------------------------------------- /openarm-can.pc.in: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | prefix=@CMAKE_INSTALL_PREFIX@ 16 | includedir=@PKG_CONFIG_INCLUDEDIR@ 17 | libdir=@PKG_CONFIG_LIBDIR@ 18 | 19 | Name: OpenArm CAN 20 | Description: OpenArm CAN library 21 | Version: @PROJECT_VERSION@ 22 | Libs: -L${libdir} -lopenarm_can 23 | Cflags: -I${includedir} 24 | -------------------------------------------------------------------------------- /.packit.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # https://packit.dev/docs/configuration/ 16 | 17 | specfile_path: packages/fedora/openarm-can.spec 18 | 19 | notifications: 20 | pull_request: 21 | successful_build: true 22 | 23 | jobs: 24 | - job: copr_build 25 | trigger: pull_request 26 | targets: 27 | - epel-all 28 | - fedora-all 29 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # https://editorconfig.org/ 16 | 17 | root = true 18 | 19 | [*] 20 | charset = utf-8 21 | insert_final_newline = true 22 | spelling_language = en 23 | trim_trailing_whitespace = true 24 | 25 | [*.sh] 26 | indent_size = 4 27 | indent_style = space 28 | 29 | [setup/*] 30 | indent_size = 4 31 | indent_style = space 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Bug Report 16 | description: File a bug report. 17 | type: Bug 18 | body: 19 | - type: textarea 20 | id: description 21 | attributes: 22 | label: Describe the problem you got 23 | description: It's better that you provide information as much as possible. 24 | validations: 25 | required: true 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Feature Request 16 | description: Request a feature. 17 | type: Feature 18 | body: 19 | - type: textarea 20 | id: description 21 | attributes: 22 | label: Describe the feature you want 23 | description: It's better that you also provide your use case. 24 | validations: 25 | required: true 26 | -------------------------------------------------------------------------------- /packages/debian/upstream/metadata: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | Bug-Database: https://github.com/enactic/openarm_can/issues 14 | Bug-Submit: https://github.com/enactic/openarm_can/issues/new 15 | Changelog: https://github.com/enactic/openarm_can/blob/main/packages/debian/changelog 16 | Documentation: https://docs.openarm.dev/software/can/ 17 | Repository-Browse: https://github.com/enactic/openarm_can 18 | Repository: https://github.com/enactic/openarm_can.git 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | blank_issues_enabled: false 16 | contact_links: 17 | - name: Discord 18 | url: https://discord.gg/FsZaZ4z3We 19 | about: Please ask and answer questions here. 20 | - name: GitHub Discussions 21 | url: https://github.com/enactic/openarm_can/discussions 22 | about: If you prefer GitHub Discussions to Discord, you can use GitHub Discussions too. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | /.cache/ 16 | /.vscode 17 | /build 18 | /openarm-can-*.tar.gz 19 | /packages/apt/build.sh 20 | /packages/apt/build/ 21 | /packages/apt/env.sh 22 | /packages/apt/repositories/ 23 | /packages/apt/tmp/ 24 | /packages/openarm-can-*.tar.gz 25 | /packages/yum/build.sh 26 | /packages/yum/build/ 27 | /packages/yum/env.sh 28 | /packages/yum/repositories/ 29 | /packages/yum/tmp/ 30 | /python/dist/ 31 | /python/vendor/ 32 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | openarm_can 17 | 1.2.1 18 | Low-level SocketCAN library and tools for OpenArm manipulators. 19 | Enactic, Inc. 20 | Apache-2.0 21 | 22 | ament_cmake 23 | 24 | 25 | cmake 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/apt/ubuntu-jammy/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM ubuntu:jammy 16 | 17 | RUN \ 18 | echo "debconf debconf/frontend select Noninteractive" | \ 19 | debconf-set-selections 20 | 21 | ARG DEBUG 22 | 23 | RUN \ 24 | quiet=$([ "${DEBUG}" = "yes" ] || echo "-qq") && \ 25 | apt update ${quiet} && \ 26 | apt install -y -V ${quiet} \ 27 | build-essential \ 28 | ccache \ 29 | cmake \ 30 | debhelper \ 31 | devscripts \ 32 | dh-python \ 33 | ninja-build \ 34 | python3-all-dev \ 35 | python3-setuptools && \ 36 | apt clean 37 | -------------------------------------------------------------------------------- /packages/apt/ubuntu-noble/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM ubuntu:noble 16 | 17 | RUN \ 18 | echo "debconf debconf/frontend select Noninteractive" | \ 19 | debconf-set-selections 20 | 21 | ARG DEBUG 22 | 23 | RUN \ 24 | quiet=$([ "${DEBUG}" = "yes" ] || echo "-qq") && \ 25 | apt update ${quiet} && \ 26 | apt install -y -V ${quiet} \ 27 | build-essential \ 28 | ccache \ 29 | cmake \ 30 | debhelper \ 31 | devscripts \ 32 | dh-python \ 33 | nanobind-dev \ 34 | ninja-build \ 35 | pybuild-plugin-pyproject \ 36 | python3-all-dev \ 37 | python3-pathspec \ 38 | python3-pyproject-metadata \ 39 | python3-scikit-build-core && \ 40 | apt clean 41 | -------------------------------------------------------------------------------- /packages/apt/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2025 Enactic, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -exu 18 | 19 | apt update 20 | apt install -V -y curl lsb-release 21 | 22 | distribution=$(lsb_release --short --id | tr '[:upper:]' '[:lower:]') 23 | code_name=$(lsb_release --codename --short) 24 | architecture=$(dpkg --print-architecture) 25 | 26 | repositories_dir=/host/packages/apt/repositories 27 | apt install -V -y \ 28 | "${repositories_dir}"/"${distribution}"/pool/"${code_name}"/*/*/*/*_"${architecture}".deb 29 | 30 | openarm-can-change-baudrate -h 31 | openarm-can-configure-socketcan -h 32 | openarm-can-configure-socketcan-4-arms -h 33 | openarm-can-diagnosis -h 34 | openarm-can-motor-check -h 35 | openarm-can-set-zero -h 36 | 37 | python3 -c "import openarm_can" 38 | -------------------------------------------------------------------------------- /packages/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # 3 | # Copyright 2025 Enactic, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | export DEB_BUILD_MAINT_OPTIONS = hardening=+all 18 | 19 | %: 20 | dh $@ --with python3 --buildsystem=cmake+ninja 21 | 22 | override_dh_auto_configure: 23 | dh_auto_configure \ 24 | -- \ 25 | -DBUILD_SHARED_LIBS=ON \ 26 | -DCMAKE_BUILD_TYPE=RelWithDebInfo 27 | dh_auto_configure \ 28 | --buildsystem=pybuild \ 29 | --sourcedirectory=python 30 | 31 | override_dh_auto_install: 32 | dh_auto_install 33 | # The Python bindings depend on the C++ implementation. 34 | OpenArmCAN_ROOT=$$(pwd)/debian/tmp/usr \ 35 | dh_auto_build \ 36 | --buildsystem=pybuild \ 37 | --sourcedirectory=python 38 | dh_auto_install \ 39 | --buildsystem=pybuild \ 40 | --sourcedirectory=python 41 | -------------------------------------------------------------------------------- /include/openarm/can/socket/arm_component.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include 18 | 19 | #include "../../canbus/can_socket.hpp" 20 | #include "../../damiao_motor/dm_motor.hpp" 21 | #include "../../damiao_motor/dm_motor_device_collection.hpp" 22 | 23 | namespace openarm::can::socket { 24 | 25 | class ArmComponent : public damiao_motor::DMDeviceCollection { 26 | public: 27 | ArmComponent(canbus::CANSocket& can_socket); 28 | ~ArmComponent() = default; 29 | 30 | void init_motor_devices(const std::vector& motor_types, 31 | const std::vector& send_can_ids, 32 | const std::vector& recv_can_ids, bool use_fd); 33 | 34 | private: 35 | std::vector motors_; 36 | }; 37 | 38 | } // namespace openarm::can::socket 39 | -------------------------------------------------------------------------------- /python/examples/test_posvel.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import openarm_can as oa 16 | import time 17 | # Create OpenArm instance 18 | 19 | arm = oa.OpenArm("can0", True) 20 | 21 | # Initialize arm motors 22 | motor_types = [oa.MotorType.DM4310] 23 | send_ids = [0x0A] 24 | recv_ids = [0x1A] 25 | arm.init_arm_motors(motor_types, send_ids, recv_ids) 26 | 27 | # Use high-level operations 28 | arm.enable_all() 29 | arm.recv_all() 30 | 31 | # return to zero position 32 | arm.set_callback_mode_all(oa.CallbackMode.STATE) 33 | arm.get_arm().posvel_control_all([oa.PosVelParam(3.14 * 4, 20)]) 34 | 35 | arm.recv_all() 36 | 37 | # read motor position 38 | while True: 39 | arm.refresh_all() 40 | arm.recv_all() 41 | for motor in arm.get_arm().get_motors(): 42 | print(motor.get_position()) 43 | for motor in arm.get_gripper().get_motors(): 44 | print(motor.get_position()) 45 | -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [build-system] 16 | build-backend = "scikit_build_core.build" 17 | requires = ["scikit-build-core", "nanobind"] 18 | 19 | [project] 20 | authors = [{name = "Enactic, Inc."}] 21 | # pyproject-metadta 0.7.1 on Ubuntu 24.04 doesn't accept PEP 639 yet. 22 | # license = "Apache-2.0" 23 | # license-files = ["LICENSE.txt"] 24 | license = {text = "Apache-2.0"} 25 | maintainers = [{name = "Enactic, Inc."}] 26 | name = "openarm_can" 27 | readme = "README.md" 28 | requires-python = ">= 3.10" 29 | version = "1.2.1" 30 | 31 | [project.urls] 32 | changelog = "https://github.com/enactic/openarm_can/releases" 33 | documentation = "https://docs.openarm.dev/software/can" 34 | homepage = "https://docs.openarm.dev/software/can" 35 | issues = "https://github.com/enactic/openarm_can/issues" 36 | repository = "https://github.com/enactic/openarm_can.git" 37 | 38 | [tool.autopep8] 39 | -------------------------------------------------------------------------------- /include/openarm/canbus/can_device_collection.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "can_device.hpp" 23 | #include "can_socket.hpp" 24 | 25 | namespace openarm::canbus { 26 | class CANDeviceCollection { 27 | public: 28 | CANDeviceCollection(canbus::CANSocket& can_socket); 29 | ~CANDeviceCollection(); 30 | 31 | void add_device(const std::shared_ptr& device); 32 | void remove_device(const std::shared_ptr& device); 33 | void dispatch_frame_callback(can_frame& frame); 34 | void dispatch_frame_callback(canfd_frame& frame); 35 | const std::map>& get_devices() const { return devices_; } 36 | canbus::CANSocket& get_can_socket() const { return can_socket_; } 37 | int get_socket_fd() const { return can_socket_.get_socket_fd(); } 38 | 39 | private: 40 | canbus::CANSocket& can_socket_; 41 | std::map> devices_; 42 | }; 43 | } // namespace openarm::canbus 44 | -------------------------------------------------------------------------------- /python/openarm_can/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | OpenArm CAN Python bindings for motor control via SocketCAN. 18 | 19 | This package provides Python bindings for the OpenArm motor control system, 20 | allowing you to control DAMIAO motors through SocketCAN. 21 | """ 22 | 23 | from .openarm_can import * 24 | 25 | __version__ = "1.2.1" 26 | __author__ = "Enactic, Inc." 27 | 28 | # Direct export of C++ classes - no wrappers 29 | __all__ = [ 30 | # Enums 31 | "MotorType", 32 | "MotorVariable", 33 | "CallbackMode", 34 | 35 | # Data structures 36 | "LimitParam", 37 | "ParamResult", 38 | "MotorStateResult", 39 | "CanFrame", 40 | "CanFdFrame", 41 | "MITParam", 42 | 43 | # Main C++ classes (1:1 mapping) 44 | "Motor", 45 | "MotorControl", 46 | "CANSocket", # Low-level socket with file descriptor access 47 | "CANDevice", # Base CAN device class 48 | "MotorDeviceCan", # Motor device management 49 | "CANDeviceCollection", # Device collection management 50 | 51 | # Exceptions 52 | "CANSocketException", 53 | ] 54 | -------------------------------------------------------------------------------- /include/openarm/canbus/can_device.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | 23 | namespace openarm::canbus { 24 | // Abstract base class for CAN devices 25 | class CANDevice { 26 | public: 27 | explicit CANDevice(canid_t send_can_id, canid_t recv_can_id, canid_t recv_can_mask, 28 | bool is_fd_enabled = false) 29 | : send_can_id_(send_can_id), 30 | recv_can_id_(recv_can_id), 31 | recv_can_mask_(recv_can_mask), 32 | is_fd_enabled_(is_fd_enabled) {} 33 | virtual ~CANDevice() = default; 34 | 35 | virtual void callback(const can_frame& frame) = 0; 36 | virtual void callback(const canfd_frame& frame) = 0; 37 | 38 | canid_t get_send_can_id() const { return send_can_id_; } 39 | canid_t get_recv_can_id() const { return recv_can_id_; } 40 | canid_t get_recv_can_mask() const { return recv_can_mask_; } 41 | bool is_fd_enabled() const { return is_fd_enabled_; } 42 | 43 | protected: 44 | canid_t send_can_id_; 45 | canid_t recv_can_id_; 46 | // mask for receiving 47 | canid_t recv_can_mask_ = CAN_SFF_MASK; 48 | bool is_fd_enabled_ = false; 49 | }; 50 | } // namespace openarm::canbus 51 | -------------------------------------------------------------------------------- /python/examples/example.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import openarm_can as oa 16 | import time 17 | # Create OpenArm instance 18 | 19 | arm = oa.OpenArm("can0", True) 20 | 21 | # Initialize arm motors 22 | motor_types = [oa.MotorType.DM4310, oa.MotorType.DM4310] 23 | send_ids = [0x01, 0x02] 24 | recv_ids = [0x11, 0x12] 25 | arm.init_arm_motors(motor_types, send_ids, recv_ids) 26 | 27 | # Initialize gripper 28 | arm.init_gripper_motor(oa.MotorType.DM4310, 0x7, 0x17) 29 | arm.set_callback_mode_all(oa.CallbackMode.IGNORE) 30 | # Use high-level operations 31 | arm.enable_all() 32 | arm.recv_all() 33 | 34 | 35 | # return to zero position 36 | arm.set_callback_mode_all(oa.CallbackMode.STATE) 37 | arm.get_arm().mit_control_all([oa.MITParam(2, 0.5, 0, 0, 0), 38 | oa.MITParam(2, 0.5, 0, 0, 0)]) 39 | 40 | arm.recv_all() 41 | 42 | # torque control test 43 | 44 | arm.get_gripper().mit_control_all([oa.MITParam(0, 0, 0, 0, 0.15)]) 45 | arm.get_arm().mit_control_all( 46 | [oa.MITParam(0, 0, 0, 0, 0.15), oa.MITParam(0, 0, 0, 0, 0.15)]) 47 | arm.recv_all() 48 | 49 | # read motor position 50 | while True: 51 | arm.refresh_all() 52 | arm.recv_all() 53 | for motor in arm.get_arm().get_motors(): 54 | print(motor.get_position()) 55 | for motor in arm.get_gripper().get_motors(): 56 | print(motor.get_position()) 57 | -------------------------------------------------------------------------------- /src/openarm/can/socket/arm_component.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | 21 | namespace openarm::can::socket { 22 | 23 | ArmComponent::ArmComponent(canbus::CANSocket& can_socket) 24 | : damiao_motor::DMDeviceCollection(can_socket) {} 25 | 26 | void ArmComponent::init_motor_devices(const std::vector& motor_types, 27 | const std::vector& send_can_ids, 28 | const std::vector& recv_can_ids, bool use_fd) { 29 | // Reserve space to prevent vector reallocation that would invalidate motor 30 | // references 31 | motors_.reserve(motor_types.size()); 32 | 33 | for (size_t i = 0; i < motor_types.size(); i++) { 34 | // First, create and store the motor in the vector 35 | motors_.emplace_back(motor_types[i], send_can_ids[i], recv_can_ids[i]); 36 | // Then create the device with a reference to the stored motor 37 | auto motor_device = 38 | std::make_shared(motors_.back(), CAN_SFF_MASK, use_fd); 39 | get_device_collection().add_device(motor_device); 40 | } 41 | } 42 | 43 | } // namespace openarm::can::socket 44 | -------------------------------------------------------------------------------- /packages/debian/copyright: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 16 | Source: https://github.com/enactic/openarm_can/releases 17 | Upstream-Name: OpenArm CAN 18 | Upstream-Contact: "Enactic, Inc." 19 | 20 | Files: 21 | * 22 | Copyright: 23 | 2025 Enactic, Inc. 24 | License: Apache-2.0 25 | Licensed to the Apache Software Foundation (ASF) under one or more 26 | contributor license agreements. See the NOTICE file distributed with 27 | this work for additional information regarding copyright ownership. 28 | The ASF licenses this file to You under the Apache License, Version 2.0 29 | (the "License"); you may not use this file except in compliance with 30 | the License. You may obtain a copy of the License at 31 | . 32 | http://www.apache.org/licenses/LICENSE-2.0 33 | . 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. 39 | . 40 | On Debian systems, the full text of the Apache Software License version 2 can 41 | be found in the file `/usr/share/common-licenses/Apache-2.0'. 42 | -------------------------------------------------------------------------------- /include/openarm/damiao_motor/dm_motor_device.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include "../canbus/can_device.hpp" 18 | #include "../canbus/can_socket.hpp" 19 | #include "dm_motor.hpp" 20 | #include "dm_motor_control.hpp" 21 | 22 | namespace openarm::damiao_motor { 23 | enum CallbackMode { 24 | STATE, 25 | PARAM, 26 | // discard 27 | IGNORE 28 | }; 29 | 30 | class DMCANDevice : public canbus::CANDevice { 31 | public: 32 | explicit DMCANDevice(Motor& motor, canid_t recv_can_mask, bool use_fd); 33 | void callback(const can_frame& frame); 34 | void callback(const canfd_frame& frame); 35 | 36 | // Create frame from data array 37 | can_frame create_can_frame(canid_t send_can_id, std::vector data); 38 | canfd_frame create_canfd_frame(canid_t send_can_id, std::vector data); 39 | // Getter method to access motor state 40 | Motor& get_motor() { return motor_; } 41 | void set_callback_mode(CallbackMode callback_mode) { callback_mode_ = callback_mode; } 42 | 43 | private: 44 | std::vector get_data_from_frame(const can_frame& frame); 45 | std::vector get_data_from_frame(const canfd_frame& frame); 46 | Motor& motor_; 47 | CallbackMode callback_mode_; 48 | bool use_fd_; // Track if using CAN-FD 49 | }; 50 | } // namespace openarm::damiao_motor 51 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | repos: 16 | - repo: https://github.com/pre-commit/mirrors-clang-format 17 | rev: v20.1.7 18 | hooks: 19 | - id: clang-format 20 | alias: cpp 21 | - repo: https://github.com/cheshirekow/cmake-format-precommit 22 | rev: v0.6.13 23 | hooks: 24 | - id: cmake-format 25 | alias: cpp 26 | - repo: https://github.com/hhatto/autopep8 27 | rev: v2.3.2 28 | hooks: 29 | - id: autopep8 30 | alias: python 31 | name: Python Format 32 | args: 33 | - "--global-config" 34 | - "python/pyproject.toml" 35 | - "--ignore-local-config" 36 | - "--in-place" 37 | - repo: https://github.com/koalaman/shellcheck-precommit 38 | rev: v0.10.0 39 | hooks: 40 | - id: shellcheck 41 | alias: shell 42 | - repo: https://github.com/scop/pre-commit-shfmt 43 | # v3.11.0-1 or later requires pre-commit 3.2.0 or later but Ubuntu 44 | # 22.04 ships pre-commit 2.17.0. We can update this rev after 45 | # Ubuntu 22.04 reached EOL (June 2027). 46 | rev: v3.10.0-1 47 | hooks: 48 | - id: shfmt 49 | alias: shell 50 | args: 51 | # The default args is "--write --simplify" but we don't use 52 | # "--simplify". Because it's conflicted will ShellCheck. 53 | - "--write" 54 | -------------------------------------------------------------------------------- /src/openarm/can/socket/gripper_component.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | 21 | namespace openarm::can::socket { 22 | 23 | GripperComponent::GripperComponent(canbus::CANSocket& can_socket) 24 | : DMDeviceCollection(can_socket) {} 25 | 26 | void GripperComponent::init_motor_device(damiao_motor::MotorType motor_type, uint32_t send_can_id, 27 | uint32_t recv_can_id, bool use_fd) { 28 | // Create the motor 29 | motor_ = std::make_unique(motor_type, send_can_id, recv_can_id); 30 | // Create the device with a reference to the motor 31 | motor_device_ = std::make_shared(*motor_, CAN_SFF_MASK, use_fd); 32 | get_device_collection().add_device(motor_device_); 33 | } 34 | 35 | void GripperComponent::open(double kp, double kd) { set_position(gripper_open_position_, kp, kd); } 36 | 37 | void GripperComponent::close(double kp, double kd) { 38 | set_position(gripper_closed_position_, kp, kd); 39 | } 40 | 41 | void GripperComponent::set_position(double gripper_position, double kp, double kd) { 42 | if (!motor_device_) return; 43 | 44 | // MIT control to desired position (zero velocity and torque) 45 | mit_control_one( 46 | 0, damiao_motor::MITParam{kp, kd, gripper_to_motor_position(gripper_position), 0.0, 0.0}); 47 | } 48 | } // namespace openarm::can::socket 49 | -------------------------------------------------------------------------------- /packages/fedora/openarm-can.spec: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | Name: openarm-can 16 | Version: 1.2.1 17 | Release: %{autorelease} 18 | Summary: OpenArm CAN control library 19 | 20 | License: Apache-2.0 21 | URL: https://docs.openarm.dev/software/can 22 | Source: https://github.com/enactic/openarm_can/archive/%{version}/openarm_can-%{version}.tar.gz 23 | 24 | BuildRequires: cmake 25 | BuildRequires: gcc-c++ 26 | 27 | %description 28 | A C++ library for CAN communication with OpenArm robotic hardware, 29 | supporting Damiao motors over CAN/CAN-FD interfaces. This library 30 | is a part of OpenArm. 31 | 32 | %package devel 33 | Summary: Development files for OpenARM CAN control library 34 | Requires: %{name}%{?_isa} = %{version}-%{release} 35 | 36 | %description devel 37 | Header files and development libraries for OpenARM CAN control library. 38 | 39 | %package utils 40 | Summary: Setup and configuration utility scripts 41 | 42 | %description utils 43 | Setup and configuration utility scripts. 44 | 45 | %prep 46 | %autosetup 47 | 48 | 49 | %build 50 | %cmake 51 | %cmake_build 52 | 53 | 54 | %install 55 | %cmake_install 56 | 57 | 58 | %files 59 | %license LICENSE.txt 60 | %doc README.md 61 | %{_libdir}/libopenarm_can.so.1* 62 | 63 | %files devel 64 | %{_includedir}/openarm/ 65 | %{_libdir}/cmake/OpenArmCAN/ 66 | %{_libdir}/libopenarm_can.so 67 | %{_libdir}/pkgconfig/openarm-can.pc 68 | 69 | %files utils 70 | %license LICENSE.txt 71 | %{_bindir}/* 72 | 73 | %changelog 74 | %autochangelog 75 | -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # If you can use build, you should use pyproject.toml intead of 16 | # setup.py. setup.py is only for old environment such as deb packages 17 | # for Ubuntu 22.04. 18 | 19 | import pathlib 20 | 21 | from setuptools import setup, Extension 22 | from setuptools.command.build_ext import build_ext 23 | 24 | 25 | class cmake_build_ext(build_ext): 26 | def build_extensions(self): 27 | self.extensions = [ 28 | ext for ext in self.extensions if ext.name != '__dummy__'] 29 | super().run() 30 | 31 | def run(self): 32 | self._run_cmake() 33 | super().run() 34 | 35 | def _run_cmake(self): 36 | source = pathlib.Path().absolute() 37 | build_lib = pathlib.Path(self.build_lib).absolute() 38 | build_temp = pathlib.Path(self.build_temp).absolute() 39 | build_temp.mkdir(parents=True, exist_ok=True) 40 | self.spawn([ 41 | "cmake", 42 | "-S", str(source), 43 | "-B", str(build_temp), 44 | "-DCMAKE_BUILD_TYPE=Release", 45 | f"-DCMAKE_INSTALL_PREFIX={build_lib}", 46 | ]) 47 | self.spawn(["cmake", "--build", str(build_temp)]) 48 | self.spawn(["cmake", "--install", str(build_temp)]) 49 | 50 | 51 | setup( 52 | name="openarm_can", 53 | version="1.2.1", 54 | license="Apache-2.0", 55 | license_files=["LICENSE.txt"], 56 | ext_modules=[Extension('__dummy__', [])], 57 | cmdclass={ 58 | "build_ext": cmake_build_ext, 59 | } 60 | ) 61 | -------------------------------------------------------------------------------- /python/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2025 Enactic, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Build script for OpenArm Python bindings 18 | 19 | set -eu 20 | 21 | # Get script directory 22 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 23 | PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" 24 | 25 | echo "Building OpenArm Python bindings..." 26 | echo "Script directory: $SCRIPT_DIR" 27 | echo "Project root: $PROJECT_ROOT" 28 | 29 | # Check if we're in a virtual environment 30 | if [[ -n "${VIRTUAL_ENV:-}" ]]; then 31 | echo "Using virtual environment (venv): $VIRTUAL_ENV" 32 | elif [[ -n "${CONDA_DEFAULT_ENV:-}" ]]; then 33 | echo "Using conda environment: $CONDA_DEFAULT_ENV" 34 | else 35 | echo "Warning: Not in a virtual environment. Consider using:" 36 | echo " python -m venv venv && source venv/bin/activate" 37 | echo " # or" 38 | echo " conda create -n myenv python=3.x && conda activate myenv" 39 | fi 40 | 41 | # Build the C++ library first if needed 42 | if [ ! -d "$PROJECT_ROOT/build" ]; then 43 | echo "Building C++ library..." 44 | cmake \ 45 | -S "$PROJECT_ROOT" \ 46 | -B "$PROJECT_ROOT/build" \ 47 | -DCMAKE_BUILD_TYPE=Debug \ 48 | -GNinja 49 | cmake --build "$PROJECT_ROOT/build" 50 | cmake --install "$PROJECT_ROOT/build" 51 | fi 52 | 53 | # Install in development mode 54 | echo "Installing in development mode..." 55 | pip install . 56 | 57 | echo "Build completed successfully!" 58 | echo "" 59 | echo "To test the installation, run:" 60 | echo " python -c 'import openarm; print(openarm.__version__)'" 61 | echo "" 62 | echo "See examples/ directory for usage examples." 63 | -------------------------------------------------------------------------------- /src/openarm/canbus/can_device_collection.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | namespace openarm::canbus { 20 | 21 | CANDeviceCollection::CANDeviceCollection(CANSocket& can_socket) : can_socket_(can_socket) {} 22 | 23 | CANDeviceCollection::~CANDeviceCollection() {} 24 | 25 | void CANDeviceCollection::add_device(const std::shared_ptr& device) { 26 | if (!device) return; 27 | 28 | // Add device to our collection 29 | canid_t device_id = device->get_recv_can_id(); 30 | devices_[device_id] = device; 31 | } 32 | 33 | void CANDeviceCollection::remove_device(const std::shared_ptr& device) { 34 | if (!device) return; 35 | 36 | canid_t device_id = device->get_recv_can_id(); 37 | auto it = devices_.find(device_id); 38 | if (it != devices_.end()) { 39 | // Remove from our collection 40 | devices_.erase(it); 41 | } 42 | } 43 | 44 | void CANDeviceCollection::dispatch_frame_callback(can_frame& frame) { 45 | auto it = devices_.find(frame.can_id); 46 | if (it != devices_.end()) { 47 | it->second->callback(frame); 48 | } 49 | // Note: Silently ignore frames for unknown devices (this is normal in CAN 50 | // networks) 51 | } 52 | 53 | void CANDeviceCollection::dispatch_frame_callback(canfd_frame& frame) { 54 | auto it = devices_.find(frame.can_id); 55 | if (it != devices_.end()) { 56 | it->second->callback(frame); 57 | } 58 | // Note: Silently ignore frames for unknown devices (this is normal in CAN 59 | // networks) 60 | } 61 | 62 | } // namespace openarm::canbus 63 | -------------------------------------------------------------------------------- /src/openarm/damiao_motor/dm_motor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace openarm::damiao_motor { 21 | 22 | // Constructor 23 | Motor::Motor(MotorType motor_type, uint32_t send_can_id, uint32_t recv_can_id) 24 | : send_can_id_(send_can_id), 25 | recv_can_id_(recv_can_id), 26 | motor_type_(motor_type), 27 | enabled_(false), 28 | state_q_(0.0), 29 | state_dq_(0.0), 30 | state_tau_(0.0), 31 | state_tmos_(0), 32 | state_trotor_(0) {} 33 | 34 | // Enable methods 35 | void Motor::set_enabled(bool enable) { this->enabled_ = enable; } 36 | 37 | // Parameter methods 38 | // TODO: storing temp params in motor object might not be a good idea 39 | // also -1 is not a good default value, consider using a different value 40 | double Motor::get_param(int RID) const { 41 | auto it = temp_param_dict_.find(RID); 42 | return (it != temp_param_dict_.end()) ? it->second : -1; 43 | } 44 | 45 | void Motor::set_temp_param(int RID, int val) { temp_param_dict_[RID] = val; } 46 | 47 | // State update methods 48 | void Motor::update_state(double q, double dq, double tau, int tmos, int trotor) { 49 | state_q_ = q; 50 | state_dq_ = dq; 51 | state_tau_ = tau; 52 | state_tmos_ = tmos; 53 | state_trotor_ = trotor; 54 | } 55 | 56 | void Motor::set_state_tmos(int tmos) { state_tmos_ = tmos; } 57 | 58 | void Motor::set_state_trotor(int trotor) { state_trotor_ = trotor; } 59 | 60 | // Static methods 61 | LimitParam Motor::get_limit_param(MotorType motor_type) { 62 | size_t index = static_cast(motor_type); 63 | if (index >= MOTOR_LIMIT_PARAMS.size()) { 64 | throw std::invalid_argument("Invalid motor type: " + 65 | std::to_string(static_cast(motor_type))); 66 | } 67 | return MOTOR_LIMIT_PARAMS[index]; 68 | } 69 | 70 | } // namespace openarm::damiao_motor 71 | -------------------------------------------------------------------------------- /include/openarm/canbus/can_socket.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | 23 | namespace openarm::canbus { 24 | 25 | // Exception classes for socket operations 26 | class CANSocketException : public std::runtime_error { 27 | public: 28 | explicit CANSocketException(const std::string& message) 29 | : std::runtime_error("Socket error: " + message) {} 30 | }; 31 | 32 | // Base socket management class 33 | class CANSocket { 34 | public: 35 | explicit CANSocket(const std::string& interface, bool enable_fd = false); 36 | ~CANSocket(); 37 | 38 | // Disable copy, enable move 39 | CANSocket(const CANSocket&) = delete; 40 | CANSocket& operator=(const CANSocket&) = delete; 41 | CANSocket(CANSocket&&) = default; 42 | CANSocket& operator=(CANSocket&&) = default; 43 | 44 | // File descriptor access for Python bindings 45 | int get_socket_fd() const { return socket_fd_; } 46 | const std::string& get_interface() const { return interface_; } 47 | bool is_canfd_enabled() const { return fd_enabled_; } 48 | bool is_initialized() const { return socket_fd_ >= 0; } 49 | 50 | // Direct frame operations for Python bindings 51 | ssize_t read_raw_frame(void* buffer, size_t buffer_size); 52 | ssize_t write_raw_frame(const void* buffer, size_t frame_size); 53 | 54 | // write can_frame or canfd_frame 55 | bool write_can_frame(const can_frame& frame); 56 | bool write_canfd_frame(const canfd_frame& frame); 57 | 58 | // read can_frame or canfd_frame 59 | bool read_can_frame(can_frame& frame); 60 | bool read_canfd_frame(canfd_frame& frame); 61 | 62 | // check if data is available for reading (non-blocking) 63 | bool is_data_available(int timeout_us = 100); 64 | 65 | protected: 66 | bool initialize_socket(const std::string& interface); 67 | void cleanup(); 68 | 69 | int socket_fd_; 70 | std::string interface_; 71 | bool fd_enabled_; 72 | }; 73 | 74 | } // namespace openarm::canbus 75 | -------------------------------------------------------------------------------- /include/openarm/damiao_motor/dm_motor.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "dm_motor_constants.hpp" 22 | 23 | namespace openarm::damiao_motor { 24 | class Motor { 25 | friend class DMCANDevice; // Allow MotorDeviceCan to access protected 26 | // members 27 | friend class DMControl; 28 | 29 | public: 30 | // Constructor 31 | Motor(MotorType motor_type, uint32_t send_can_id, uint32_t recv_can_id); 32 | 33 | // State getters 34 | double get_position() const { return state_q_; } 35 | double get_velocity() const { return state_dq_; } 36 | double get_torque() const { return state_tau_; } 37 | int get_state_tmos() const { return state_tmos_; } 38 | int get_state_trotor() const { return state_trotor_; } 39 | 40 | // Motor property getters 41 | uint32_t get_send_can_id() const { return send_can_id_; } 42 | uint32_t get_recv_can_id() const { return recv_can_id_; } 43 | MotorType get_motor_type() const { return motor_type_; } 44 | 45 | // Enable status getters 46 | bool is_enabled() const { return enabled_; } 47 | 48 | // Parameter methods 49 | double get_param(int RID) const; 50 | 51 | // Static methods for motor properties 52 | static LimitParam get_limit_param(MotorType motor_type); 53 | 54 | protected: 55 | // State update methods 56 | void update_state(double q, double dq, double tau, int tmos, int trotor); 57 | void set_state_tmos(int tmos); 58 | void set_state_trotor(int trotor); 59 | void set_enabled(bool enabled); 60 | void set_temp_param(int RID, int val); 61 | 62 | // Motor identifiers 63 | uint32_t send_can_id_; 64 | uint32_t recv_can_id_; 65 | MotorType motor_type_; 66 | 67 | // Enable status 68 | bool enabled_; 69 | 70 | // Current state 71 | double state_q_, state_dq_, state_tau_; 72 | int state_tmos_, state_trotor_; 73 | 74 | // Parameter storage 75 | std::map temp_param_dict_; 76 | }; 77 | } // namespace openarm::damiao_motor 78 | -------------------------------------------------------------------------------- /include/openarm/damiao_motor/dm_motor_device_collection.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include 18 | #include 19 | 20 | #include "../canbus/can_device_collection.hpp" 21 | #include "dm_motor_constants.hpp" 22 | #include "dm_motor_control.hpp" 23 | #include "dm_motor_device.hpp" 24 | 25 | namespace openarm::damiao_motor { 26 | 27 | class DMDeviceCollection { 28 | public: 29 | DMDeviceCollection(canbus::CANSocket& can_socket); 30 | virtual ~DMDeviceCollection() = default; 31 | 32 | // Common motor operations 33 | void enable_all(); 34 | void disable_all(); 35 | void set_callback_mode_all(CallbackMode callback_mode); 36 | 37 | // Flash new zero position 38 | void set_zero(int i); 39 | void set_zero_all(); 40 | 41 | // Refresh operations (for individual motors) 42 | void refresh_one(int i); 43 | void refresh_all(); 44 | 45 | // Query parameter operations 46 | void query_param_one(int i, int RID); 47 | void query_param_all(int RID); 48 | 49 | // MIT control operations 50 | void mit_control_one(int i, const MITParam& mit_param); 51 | void mit_control_all(const std::vector& mit_params); 52 | 53 | // PosVel control operation 54 | void posvel_control_one(int i, const PosVelParam& posvel_param); 55 | void posvel_control_all(const std::vector& posvel_params); 56 | 57 | // Device collection access 58 | std::vector get_motors() const; 59 | Motor get_motor(int i) const; 60 | canbus::CANDeviceCollection& get_device_collection() { return *device_collection_; } 61 | 62 | protected: 63 | canbus::CANSocket& can_socket_; 64 | std::unique_ptr can_packet_encoder_; 65 | std::unique_ptr can_packet_decoder_; 66 | std::unique_ptr device_collection_; 67 | 68 | // Helper methods for subclasses 69 | void send_command_to_device(std::shared_ptr dm_device, const CANPacket& packet); 70 | std::vector> get_dm_devices() const; 71 | }; 72 | } // namespace openarm::damiao_motor 73 | -------------------------------------------------------------------------------- /helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "cgi/escape" 16 | require "json" 17 | require "open-uri" 18 | 19 | module Helper 20 | module_function 21 | def cmake_lists_txt 22 | File.join(__dir__, "CMakeLists.txt") 23 | end 24 | 25 | def detect_version 26 | File.read(cmake_lists_txt)[/^project\(.+ VERSION (.+?)\)/, 1] 27 | end 28 | 29 | def update_content(path) 30 | content = File.read(path) 31 | content = yield(content) 32 | File.write(path, content) 33 | end 34 | 35 | def update_cmake_lists_txt_version(new_version) 36 | update_content(cmake_lists_txt) do |content| 37 | content.sub(/^(project\(.* VERSION )(?:.*?)(\))/) do 38 | "#{$1}#{new_version}#{$2}" 39 | end 40 | end 41 | end 42 | 43 | def github_repository 44 | ENV["GITHUB_REPOSITORY"] || "enactic/openarm_can" 45 | end 46 | 47 | def call_github_api(path, **parameters) 48 | url = "https://api.github.com/#{path}" 49 | unless parameters.empty? 50 | encoded_parameters = parameters.collect do |key, value| 51 | "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" 52 | end 53 | url += "?#{encoded_parameters.join("&")}" 54 | end 55 | URI.open(url) do |response| 56 | JSON.parse(response.read) 57 | end 58 | end 59 | 60 | def wait_github_actions_workflow(branch, workflow_file) 61 | run_id = nil 62 | 63 | 3.times do 64 | response = call_github_api("repos/#{github_repository}/actions/runs", 65 | branch: branch) 66 | run = response["workflow_runs"].find do |workflow_run| 67 | workflow_run["path"] == ".github/workflows/#{workflow_file}" 68 | end 69 | if run 70 | run_id = run["id"] 71 | break 72 | end 73 | sleep(60) 74 | end 75 | raise "Couldn't find target workflow" if run_id.nil? 76 | 77 | run_request_path = "repos/#{github_repository}/actions/runs/#{run_id}" 78 | loop do 79 | response = call_github_api(run_request_path) 80 | status = response["status"] 81 | return if response["status"] == "completed" 82 | puts("Waiting...: #{status}") 83 | sleep(60) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /include/openarm/can/socket/gripper_component.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #define _USE_MATH_DEFINES 18 | #include 19 | #include 20 | 21 | #include "../../damiao_motor/dm_motor.hpp" 22 | #include "../../damiao_motor/dm_motor_device_collection.hpp" 23 | 24 | namespace openarm::can::socket { 25 | 26 | class GripperComponent : public damiao_motor::DMDeviceCollection { 27 | public: 28 | GripperComponent(canbus::CANSocket& can_socket); 29 | ~GripperComponent() = default; 30 | 31 | void init_motor_device(damiao_motor::MotorType motor_type, uint32_t send_can_id, 32 | uint32_t recv_can_id, bool use_fd); 33 | 34 | // Gripper-specific controls 35 | void open(double kp = 50.0, double kd = 1.0); 36 | void close(double kp = 50.0, double kd = 1.0); 37 | damiao_motor::Motor* get_motor() const { return motor_.get(); } 38 | 39 | private: 40 | std::unique_ptr motor_; 41 | std::shared_ptr motor_device_; 42 | 43 | void set_position(double position, double kp = 50.0, double kd = 1.0); 44 | 45 | // The actual physical gripper uses a slider cranker-like mechanism, this mapping is an 46 | // approximation. 47 | double gripper_to_motor_position(double gripper_position) { 48 | // Map gripper position (0.0=closed, 1.0=open) to motor position 49 | return (gripper_position - gripper_open_position_) / 50 | (gripper_closed_position_ - gripper_open_position_) * 51 | (motor_closed_position_ - motor_open_position_) + 52 | motor_open_position_; 53 | } 54 | 55 | double motor_to_gripper_position(double motor_position) { 56 | // Map motor position back to gripper position (0.0=closed, 1.0=open) 57 | return (motor_position - motor_open_position_) / 58 | (motor_closed_position_ - motor_open_position_) * 59 | (gripper_closed_position_ - gripper_open_position_) + 60 | gripper_open_position_; 61 | } 62 | 63 | // Gripper configuration 64 | double gripper_open_position_ = 1.0; 65 | double gripper_closed_position_ = 0.0; 66 | double motor_open_position_ = -1.0472; // 60 degrees 67 | double motor_closed_position_ = 0.0; 68 | }; 69 | 70 | } // namespace openarm::can::socket 71 | -------------------------------------------------------------------------------- /include/openarm/can/socket/openarm.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include 18 | #include 19 | 20 | #include "../../canbus/can_device_collection.hpp" 21 | #include "../../canbus/can_socket.hpp" 22 | #include "arm_component.hpp" 23 | #include "gripper_component.hpp" 24 | 25 | namespace openarm::can::socket { 26 | class OpenArm { 27 | public: 28 | OpenArm(const std::string& can_interface, bool enable_fd = false); 29 | ~OpenArm() = default; 30 | 31 | std::string can_interface() const noexcept { return can_interface_; } 32 | bool can_fd_enabled() const noexcept { return enable_fd_; } 33 | 34 | // Component initialization 35 | void init_arm_motors(const std::vector& motor_types, 36 | const std::vector& send_can_ids, 37 | const std::vector& recv_can_ids); 38 | 39 | void init_gripper_motor(damiao_motor::MotorType motor_type, uint32_t send_can_id, 40 | uint32_t recv_can_id); 41 | 42 | // Component access 43 | ArmComponent& get_arm() { return *arm_; } 44 | GripperComponent& get_gripper() { return *gripper_; } 45 | canbus::CANDeviceCollection& get_master_can_device_collection() { 46 | return *master_can_device_collection_; 47 | } 48 | 49 | // Damiao Motor operations (works only on sub_dm_device_collections_) 50 | void enable_all(); 51 | void disable_all(); 52 | void set_zero_all(); 53 | void refresh_all(); 54 | 55 | void refresh_one(int i); 56 | // The timeout for reading from socket, set to timeout_us. 57 | // Tuning this value may improve the performance but should be done with caution. 58 | void recv_all(int timeout_us = 500); 59 | void set_callback_mode_all(damiao_motor::CallbackMode callback_mode); 60 | void query_param_all(int RID); 61 | 62 | private: 63 | std::string can_interface_; 64 | bool enable_fd_; 65 | std::unique_ptr can_socket_; 66 | std::unique_ptr arm_; 67 | std::unique_ptr gripper_; 68 | std::unique_ptr master_can_device_collection_; 69 | std::vector sub_dm_device_collections_; 70 | void register_dm_device_collection(damiao_motor::DMDeviceCollection& device_collection); 71 | }; 72 | 73 | } // namespace openarm::can::socket 74 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Test 16 | on: 17 | push: 18 | branches: 19 | - '**' 20 | - '!dependabot/**' 21 | tags: 22 | - '**' 23 | pull_request: 24 | concurrency: 25 | group: ${{ github.head_ref || github.sha }}-${{ github.workflow }} 26 | cancel-in-progress: true 27 | jobs: 28 | lint: 29 | name: Lint 30 | runs-on: ubuntu-latest 31 | timeout-minutes: 5 32 | steps: 33 | - uses: actions/checkout@v6 34 | with: 35 | submodules: recursive 36 | - name: Install pre-commit 37 | run: | 38 | python -m pip install pre-commit 39 | - uses: actions/cache@v5 40 | with: 41 | path: ~/.cache/pre-commit 42 | key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 43 | - name: Run pre-commit 44 | run: | 45 | pre-commit run --show-diff-on-failure --color=always --all-files 46 | 47 | build: 48 | name: Build 49 | runs-on: ${{ matrix.runs-on }} 50 | timeout-minutes: 5 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | runs-on: 55 | - ubuntu-22.04 56 | - ubuntu-24.04 57 | steps: 58 | - uses: actions/checkout@v6 59 | with: 60 | submodules: recursive 61 | - name: "C++: Install dependencies" 62 | run: | 63 | sudo apt update 64 | sudo apt install -y -V \ 65 | cmake \ 66 | ninja-build 67 | - name: "C++: CMake" 68 | run: | 69 | cmake \ 70 | -B ../build \ 71 | -S . \ 72 | -GNinja \ 73 | -DCMAKE_INSTALL_PREFIX=$PWD/install \ 74 | -DCMAKE_BUILD_TYPE=Debug 75 | - name: "C++: Build" 76 | run: | 77 | ninja -C ../build 78 | - name: Install 79 | run: | 80 | ninja -C ../build install 81 | - uses: actions/setup-python@v6 82 | with: 83 | python-version: 3 84 | - name: "Python: Install dependencies" 85 | run: | 86 | python3 -m pip install -r python/requirements.txt 87 | if apt show nanobind-dev; then 88 | sudo apt install -y -V nanobind-dev 89 | fi 90 | - name: "Python: Build" 91 | run: | 92 | python3 -m pip install \ 93 | -Ccmake.define.CMAKE_PREFIX_PATH=$PWD/install \ 94 | --verbose \ 95 | ./python 96 | - name: "Python: Test" 97 | run: | 98 | python3 -c "import openarm_can" 99 | -------------------------------------------------------------------------------- /include/openarm/damiao_motor/dm_motor_constants.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | namespace openarm::damiao_motor { 22 | enum class MotorType : uint8_t { 23 | DM3507 = 0, 24 | DM4310 = 1, 25 | DM4310_48V = 2, 26 | DM4340 = 3, 27 | DM4340_48V = 4, 28 | DM6006 = 5, 29 | DM8006 = 6, 30 | DM8009 = 7, 31 | DM10010L = 8, 32 | DM10010 = 9, 33 | DMH3510 = 10, 34 | DMH6215 = 11, 35 | DMG6220 = 12, 36 | COUNT = 13 37 | }; 38 | 39 | enum class ControlMode : uint8_t { MIT = 1, POS_VEL = 2, VEL = 3, TORQUE_POS = 4 }; 40 | 41 | enum class RID : uint8_t { 42 | UV_Value = 0, 43 | KT_Value = 1, 44 | OT_Value = 2, 45 | OC_Value = 3, 46 | ACC = 4, 47 | DEC = 5, 48 | MAX_SPD = 6, 49 | MST_ID = 7, 50 | ESC_ID = 8, 51 | TIMEOUT = 9, 52 | CTRL_MODE = 10, 53 | Damp = 11, 54 | Inertia = 12, 55 | hw_ver = 13, 56 | sw_ver = 14, 57 | SN = 15, 58 | NPP = 16, 59 | Rs = 17, 60 | LS = 18, 61 | Flux = 19, 62 | Gr = 20, 63 | PMAX = 21, 64 | VMAX = 22, 65 | TMAX = 23, 66 | I_BW = 24, 67 | KP_ASR = 25, 68 | KI_ASR = 26, 69 | KP_APR = 27, 70 | KI_APR = 28, 71 | OV_Value = 29, 72 | GREF = 30, 73 | Deta = 31, 74 | V_BW = 32, 75 | IQ_c1 = 33, 76 | VL_c1 = 34, 77 | can_br = 35, 78 | sub_ver = 36, 79 | u_off = 50, 80 | v_off = 51, 81 | k1 = 52, 82 | k2 = 53, 83 | m_off = 54, 84 | dir = 55, 85 | p_m = 80, 86 | xout = 81, 87 | COUNT = 82 88 | }; 89 | 90 | // Limit parameters structure for different motor types 91 | struct LimitParam { 92 | double pMax; // Position limit (rad) 93 | double vMax; // Velocity limit (rad/s) 94 | double tMax; // Torque limit (Nm) 95 | }; 96 | // Limit parameters for each motor type [pMax, vMax, tMax] 97 | inline constexpr std::array(MotorType::COUNT)> 98 | MOTOR_LIMIT_PARAMS = {{ 99 | {12.5, 50, 5}, // DM3507 100 | {12.5, 30, 10}, // DM4310 101 | {12.5, 50, 10}, // DM4310_48V 102 | {12.5, 8, 28}, // DM4340 103 | {12.5, 10, 28}, // DM4340_48V 104 | {12.5, 45, 20}, // DM6006 105 | {12.5, 45, 40}, // DM8006 106 | {12.5, 45, 54}, // DM8009 107 | {12.5, 25, 200}, // DM10010L 108 | {12.5, 20, 200}, // DM10010 109 | {12.5, 280, 1}, // DMH3510 110 | {12.5, 45, 10}, // DMH6215 111 | {12.5, 45, 10} // DMG6220 112 | }}; 113 | } // namespace openarm::damiao_motor 114 | -------------------------------------------------------------------------------- /packages/debian/control.in: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | Source: openarm-can 16 | Section: libs 17 | Priority: optional 18 | Maintainer: "Enactic, Inc." 19 | Rules-Requires-Root: no 20 | Build-Depends: 21 | cmake, 22 | debhelper-compat (= 13), 23 | dh-sequence-python3, 24 | @HAVE_NANOBIND@ nanobind-dev, 25 | ninja-build, 26 | @USE_PYPROJECT@ pybuild-plugin-pyproject, 27 | python3-all-dev, 28 | @USE_PYPROJECT@ python3-pathspec, 29 | @USE_PYPROJECT@ python3-pyproject-metadata, 30 | @USE_PYPROJECT@ python3-scikit-build-core, 31 | @NOT_USE_PYPROJECT@ python3-setuptools, 32 | Standards-Version: 4.6.0 33 | Homepage: https://docs.openarm.dev/ 34 | Vcs-Browser: https://github.com/enactic/openarm_can 35 | Vcs-Git: https://github.com/enactic/openarm_can.git 36 | 37 | Package: libopenarm-can1 38 | Architecture: any 39 | Multi-Arch: same 40 | Depends: 41 | ${misc:Depends}, 42 | ${shlibs:Depends}, 43 | Description: CAN communication library for OpenArm robotic hardware 44 | OpenArm CAN Library is a communication bridge between high-level 45 | OpenArm control applications and low-level motor protocols. 46 | It abstracts CAN bus communication via SocketCAN interface. 47 | 48 | Package: libopenarm-can-dev 49 | Section: libdevel 50 | Architecture: any 51 | Multi-Arch: same 52 | Depends: 53 | ${misc:Depends}, 54 | libopenarm-can1 (= ${binary:Version}), 55 | Description: Development files to use OpenArm CAN as a library 56 | OpenArm CAN Library is a communication bridge between high-level 57 | OpenArm control applications and low-level motor protocols. 58 | It abstracts CAN bus communication via SocketCAN interface. 59 | . 60 | This package provides header files to use OpenArm CAN as a library. 61 | 62 | Package: python3-openarm-can 63 | Architecture: any 64 | Depends: 65 | ${misc:Depends}, 66 | ${python3:Depends}, 67 | ${shlibs:Depends}, 68 | Description: OpenArm CAN Python bindings 69 | OpenArm CAN Library is a communication bridge between high-level 70 | OpenArm control applications and low-level motor protocols. 71 | It abstracts CAN bus communication via Linux SocketCAN interface. 72 | . 73 | This package provides the Python bindings. 74 | 75 | Package: openarm-can-utils 76 | Architecture: any 77 | Depends: 78 | ${misc:Depends}, 79 | ${shlibs:Depends}, 80 | can-utils, 81 | iproute2, 82 | python3, 83 | python3-can, 84 | python3-openarm-can (= ${binary:Version}), 85 | Description: Utility tools for OpenArm CAN configuration 86 | OpenArm CAN Library is a communication bridge between high-level 87 | OpenArm control applications and low-level motor protocols. 88 | It abstracts CAN bus communication via Linux SocketCAN interface. 89 | . 90 | This package provides utility scripts for CAN interface setup, 91 | motor diagnostics, and configuration tools. 92 | -------------------------------------------------------------------------------- /setup/openarm-can-configure-socketcan: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2025 Enactic, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http:#www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eu 18 | 19 | # Simple CAN Interface Setup Script 20 | 21 | # Default values 22 | BITRATE=1000000 23 | DBITRATE=5000000 24 | FD_MODE=false 25 | 26 | # Show usage 27 | usage() { 28 | echo "Usage: $0 [options]" 29 | echo "" 30 | echo "Options:" 31 | echo " -fd Enable CAN FD mode (default: CAN 2.0)" 32 | echo " -b Set bitrate (default: 1000000)" 33 | echo " -d Set CAN FD data bitrate (default: 5000000)" 34 | echo " -h Show help" 35 | echo "" 36 | echo "Examples:" 37 | echo " $0 can0 # CAN 2.0 at 1Mbps" 38 | echo " $0 can0 -fd # CAN FD at 1Mbps/5Mbps" 39 | echo " $0 can0 -b 500000 # CAN 2.0 at 500kbps" 40 | echo " $0 can0 -fd -d 8000000 # CAN FD with 8Mbps data rate" 41 | } 42 | 43 | # Check if interface provided 44 | if [ -z "$1" ]; then 45 | echo "Error: Specify CAN interface (e.g., can0)" 46 | usage 47 | exit 1 48 | fi 49 | 50 | if [ "$1" = "-h" ]; then 51 | usage 52 | exit 0 53 | fi 54 | 55 | CAN_IF="$1" 56 | shift 57 | 58 | # Parse options 59 | while [[ $# -gt 0 ]]; do 60 | case "$1" in 61 | -fd) 62 | FD_MODE=true 63 | shift 64 | ;; 65 | -b) 66 | BITRATE="$2" 67 | shift 2 68 | ;; 69 | -d) 70 | DBITRATE="$2" 71 | shift 2 72 | ;; 73 | -h) 74 | usage 75 | exit 0 76 | ;; 77 | *) 78 | echo "Error: Unknown option '$1'" 79 | usage 80 | exit 1 81 | ;; 82 | esac 83 | done 84 | 85 | # Check if interface exists 86 | if ! ip link show "$CAN_IF" &>/dev/null; then 87 | echo "Error: CAN interface '$CAN_IF' not found" 88 | echo "Available interfaces:" 89 | ip link show | grep -E "can[0-9]" | cut -d: -f2 | tr -d ' ' || echo " No CAN interfaces found" 90 | exit 1 91 | fi 92 | 93 | # Configure CAN interface 94 | echo "Configuring $CAN_IF..." 95 | 96 | if ! sudo ip link set "$CAN_IF" down; then 97 | echo "Error: Failed to bring down $CAN_IF" 98 | exit 1 99 | fi 100 | 101 | if [ "$FD_MODE" = true ]; then 102 | if ! sudo ip link set "$CAN_IF" type can bitrate "$BITRATE" dbitrate "$DBITRATE" fd on; then 103 | echo "Error: Failed to configure CAN FD mode" 104 | exit 1 105 | fi 106 | echo "$CAN_IF is now set to CAN FD mode (${BITRATE} bps / ${DBITRATE} bps)" 107 | else 108 | if ! sudo ip link set "$CAN_IF" type can bitrate "$BITRATE"; then 109 | echo "Error: Failed to configure CAN 2.0 mode" 110 | exit 1 111 | fi 112 | echo "$CAN_IF is now set to CAN 2.0 mode (${BITRATE} bps)" 113 | fi 114 | 115 | if ! sudo ip link set "$CAN_IF" up; then 116 | echo "Error: Failed to bring up $CAN_IF" 117 | exit 1 118 | fi 119 | 120 | echo "✓ $CAN_IF is active" 121 | -------------------------------------------------------------------------------- /setup/openarm-can-configure-socketcan-4-arms: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2025 Enactic, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http:#www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eu 18 | 19 | # Simple CAN Interface Setup Script 20 | 21 | # Default values 22 | BITRATE=1000000 23 | DBITRATE=5000000 24 | FD_MODE=false 25 | 26 | # Show usage 27 | usage() { 28 | echo "Usage: $0 [options]" 29 | echo "" 30 | echo "Options:" 31 | echo " -fd Enable CAN FD mode (default: CAN 2.0)" 32 | echo " -b Set bitrate (default: 1000000)" 33 | echo " -d Set CAN FD data bitrate (default: 5000000)" 34 | echo " -h Show help" 35 | echo "" 36 | echo "Examples:" 37 | echo " $0 can0 # CAN 2.0 at 1Mbps" 38 | echo " $0 can0 -fd # CAN FD at 1Mbps/5Mbps" 39 | echo " $0 can0 -b 500000 # CAN 2.0 at 500kbps" 40 | echo " $0 can0 -fd -d 8000000 # CAN FD with 8Mbps data rate" 41 | } 42 | 43 | # Parse options 44 | while [[ $# -gt 0 ]]; do 45 | case $1 in 46 | -fd) 47 | FD_MODE=true 48 | shift 49 | ;; 50 | -b) 51 | BITRATE="$2" 52 | shift 2 53 | ;; 54 | -d) 55 | DBITRATE="$2" 56 | shift 2 57 | ;; 58 | -h) 59 | usage 60 | exit 0 61 | ;; 62 | *) 63 | echo "Error: Unknown option '$1'" 64 | usage 65 | exit 1 66 | ;; 67 | esac 68 | done 69 | 70 | CAN_DEVICES=("can0" "can1" "can2" "can3") 71 | # for each device, check if it exists and configure it 72 | for CAN_IF in "${CAN_DEVICES[@]}"; do 73 | echo "Configuring $CAN_IF..." 74 | 75 | # Check if interface exists 76 | if ! ip link show "$CAN_IF" &>/dev/null; then 77 | echo "Error: CAN interface '$CAN_IF' not found" 78 | echo "Available interfaces:" 79 | ip link show | grep -E "can[0-9]" | cut -d: -f2 | tr -d ' ' || echo " No CAN interfaces found" 80 | exit 1 81 | fi 82 | 83 | # Configure CAN interface 84 | echo "Configuring $CAN_IF..." 85 | 86 | if ! sudo ip link set "$CAN_IF" down; then 87 | echo "Error: Failed to bring down $CAN_IF" 88 | exit 1 89 | fi 90 | 91 | if [ "$FD_MODE" = true ]; then 92 | if ! sudo ip link set "$CAN_IF" type can bitrate "$BITRATE" dbitrate "$DBITRATE" fd on; then 93 | echo "Error: Failed to configure CAN FD mode" 94 | exit 1 95 | fi 96 | echo "$CAN_IF is now set to CAN FD mode (${BITRATE} bps / ${DBITRATE} bps)" 97 | else 98 | if ! sudo ip link set "$CAN_IF" type can bitrate "$BITRATE"; then 99 | echo "Error: Failed to configure CAN 2.0 mode" 100 | exit 1 101 | fi 102 | echo "$CAN_IF is now set to CAN 2.0 mode (${BITRATE} bps)" 103 | fi 104 | 105 | if ! sudo ip link set "$CAN_IF" up; then 106 | echo "Error: Failed to bring up $CAN_IF" 107 | exit 1 108 | fi 109 | 110 | echo "✓ $CAN_IF is active" 111 | 112 | done 113 | -------------------------------------------------------------------------------- /include/openarm/damiao_motor/dm_motor_control.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #pragma once 16 | 17 | #include 18 | 19 | #include 20 | #include // for memcpy 21 | #include 22 | #include 23 | #include 24 | 25 | #include "dm_motor.hpp" 26 | #include "dm_motor_constants.hpp" 27 | 28 | namespace openarm::damiao_motor { 29 | // Forward declarations 30 | class Motor; 31 | 32 | struct ParamResult { 33 | int rid; 34 | double value; 35 | bool valid; 36 | }; 37 | 38 | struct StateResult { 39 | double position; 40 | double velocity; 41 | double torque; 42 | int t_mos; 43 | int t_rotor; 44 | bool valid; 45 | }; 46 | 47 | struct CANPacket { 48 | uint32_t send_can_id; 49 | std::vector data; 50 | }; 51 | 52 | struct MITParam { 53 | double kp; 54 | double kd; 55 | double q; 56 | double dq; 57 | double tau; 58 | }; 59 | 60 | struct PosVelParam { 61 | double q; 62 | double dq; 63 | }; 64 | 65 | class CanPacketEncoder { 66 | public: 67 | static CANPacket create_enable_command(const Motor& motor); 68 | static CANPacket create_disable_command(const Motor& motor); 69 | static CANPacket create_set_zero_command(const Motor& motor); 70 | static CANPacket create_mit_control_command(const Motor& motor, const MITParam& mit_param); 71 | static CANPacket create_posvel_control_command(const Motor& motor, 72 | const PosVelParam& posvel_param); 73 | static CANPacket create_query_param_command(const Motor& motor, int RID); 74 | static CANPacket create_refresh_command(const Motor& motor); 75 | 76 | private: 77 | static std::vector pack_mit_control_data(MotorType motor_type, 78 | const MITParam& mit_param); 79 | static std::vector pack_posvel_control_data(MotorType motor_type, 80 | const PosVelParam& posvel_param); 81 | 82 | static std::vector pack_query_param_data(uint32_t send_can_id, int RID); 83 | static std::vector pack_command_data(uint8_t cmd); 84 | 85 | static double limit_min_max(double x, double min, double max); 86 | static uint16_t double_to_uint(double x, double x_min, double x_max, int bits); 87 | static std::array float_to_uint8s(float value); 88 | }; 89 | 90 | class CanPacketDecoder { 91 | public: 92 | static StateResult parse_motor_state_data(const Motor& motor, const std::vector& data); 93 | static ParamResult parse_motor_param_data(const std::vector& data); 94 | 95 | private: 96 | static double uint_to_double(uint16_t x, double min, double max, int bits); 97 | static float uint8s_to_float(const std::array& bytes); 98 | static uint32_t uint8s_to_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4); 99 | static bool is_in_ranges(int number); 100 | }; 101 | 102 | } // namespace openarm::damiao_motor 103 | -------------------------------------------------------------------------------- /packages/Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "time" 16 | 17 | apache_arrow_repository = ENV["APACHE_ARROW_REPOSITORY"] 18 | if apache_arrow_repository.nil? 19 | raise "Specify APACHE_ARROW_REPOSITORY environment variable" 20 | end 21 | require "#{apache_arrow_repository}/dev/tasks/linux-packages/package-task" 22 | 23 | groonga_repository = ENV["GROONGA_REPOSITORY"] 24 | if groonga_repository.nil? 25 | puts("You need to specify GROONGA_REPOSITORY environment variable " + 26 | "to push packages to Launchpad") 27 | else 28 | require "#{groonga_repository}/packages/launchpad-helper" 29 | end 30 | 31 | require_relative "../helper" 32 | 33 | class OpenArmCANPackageTask < PackageTask 34 | include LaunchpadHelper if Object.const_defined?(:LaunchpadHelper) 35 | 36 | def initialize 37 | super("openarm-can", detect_version, detect_release_time) 38 | end 39 | 40 | def define 41 | super 42 | define_ubuntu_tasks if respond_to?(:define_ubuntu_tasks, true) 43 | end 44 | 45 | private 46 | def detect_version 47 | Helper.detect_version 48 | end 49 | 50 | def detect_release_time 51 | release_time_env = ENV["RELEASE_TIME"] 52 | if release_time_env 53 | Time.parse(release_time_env).utc 54 | else 55 | Time.now.utc 56 | end 57 | end 58 | 59 | def apt_prepare_debian_control_pyproject(control, target) 60 | use_pyproject = (not target.start_with?("ubuntu-jammy")) 61 | if use_pyproject 62 | control 63 | .gsub(/@USE_PYPROJECT@/, "") 64 | .gsub(/@NOT_USE_PYPROJECT@/, "#") 65 | else 66 | control 67 | .gsub(/@USE_PYPROJECT@/, "#") 68 | .gsub(/@NOT_USE_PYPROJECT@/, "") 69 | end 70 | end 71 | 72 | def apt_prepare_debian_control_nanobind(control, target) 73 | have_nanobind = (not target.start_with?("ubuntu-jammy")) 74 | if have_nanobind 75 | control.gsub(/@HAVE_NANOBIND@/, "") 76 | else 77 | control.gsub(/@HAVE_NANOBIND@/, "#") 78 | end 79 | end 80 | 81 | def apt_prepare_debian_control(control_in, target) 82 | control = control_in.dup 83 | control = apt_prepare_debian_control_pyproject(control, target) 84 | control = apt_prepare_debian_control_nanobind(control, target) 85 | control 86 | end 87 | 88 | def enable_yum? 89 | false 90 | end 91 | 92 | def update_spec 93 | update_content("fedora/openarm-can.spec") do |content| 94 | content.gsub!(/^(Version:\s+)[\d.]+$/) do 95 | "#{$1}#{@version}" 96 | end 97 | end 98 | end 99 | 100 | def define_archive_task 101 | file @archive_name do 102 | if File.exist?("../#{@archive_name}") 103 | ln_s("../#{@archive_name}", 104 | @archive_name) 105 | else 106 | download("https://github.com/#{github_repository}/" + 107 | "releases/download/#{@version}/#{@archive_name}", 108 | @archive_name) 109 | end 110 | end 111 | end 112 | 113 | def github_repository 114 | Helper.github_repository 115 | end 116 | 117 | def docker_image(os, architecture) 118 | "ghcr.io/#{github_repository.gsub("_", "-")}-package:#{super}" 119 | end 120 | 121 | def dput_configuration_name 122 | "openarm-ppa" 123 | end 124 | 125 | def dput_incoming 126 | "~openarm/main/ubuntu/" 127 | end 128 | end 129 | 130 | task = OpenArmCANPackageTask.new 131 | task.define 132 | -------------------------------------------------------------------------------- /python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | cmake_minimum_required(VERSION 3.22) 16 | project( 17 | openarm_can_python 18 | VERSION 1.2.1 19 | LANGUAGES CXX) 20 | 21 | set(CMAKE_CXX_STANDARD 17) 22 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 23 | 24 | find_package( 25 | Python 26 | COMPONENTS Interpreter Development.Module 27 | REQUIRED) 28 | 29 | include(FetchContent) 30 | set(OCP_FETCH_CONTENT_COMMON_OPTIONS) 31 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28) 32 | list(APPEND OCP_FETCH_CONTENT_COMMON_OPTIONS EXCLUDE_FROM_ALL TRUE) 33 | endif() 34 | macro(ocp_prepare_fetchcontent) 35 | set(BUILD_SHARED_LIBS OFF) 36 | set(BUILD_TESTING OFF) 37 | set(CMAKE_COMPILE_WARNING_AS_ERROR FALSE) 38 | set(CMAKE_EXPORT_NO_PACKAGE_REGISTRY TRUE) 39 | set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) 40 | endmacro() 41 | 42 | execute_process( 43 | COMMAND Python::Interpreter -m nanobind --cmake_dir 44 | OUTPUT_STRIP_TRAILING_WHITESPACE 45 | OUTPUT_VARIABLE nanobind_ROOT) 46 | function(ocp_ensure_nanobind) 47 | # We can use FIND_PACKAGE_ARGS in fetchcontent_declare() instead of 48 | # explicit find_package() when we require CMake 3.24 or later. 49 | find_package(nanobind) 50 | if(nanobind_FOUND) 51 | return() 52 | endif() 53 | 54 | set(NANOBIND_BUNDLED_VERSION "2.10.2") 55 | set(NANOBIND_SOURCE_BASE_NAME "nanobind-${NANOBIND_BUNDLED_VERSION}.tar.gz") 56 | set(NANOBIND_SOURCE_LOCAL_PATH 57 | "${CMAKE_CURRENT_SOURCE_DIR}/vendor/${NANOBIND_SOURCE_BASE_NAME}") 58 | if(EXISTS ${NANOBIND_SOURCE_LOCAL_PATH}) 59 | set(NANOBIND_SOURCE_ARGS URL ${NANOBIND_SOURCE_LOCAL_PATH}) 60 | else() 61 | set(NANOBIND_SOURCE_ARGS 62 | GIT_REPOSITORY "https://github.com/wjakob/nanobind.git" GIT_TAG 63 | "v${NANOBIND_BUNDLED_VERSION}") 64 | endif() 65 | fetchcontent_declare( 66 | nanobind 67 | ${OCP_FETCH_CONTENT_COMMON_OPTIONS} 68 | ${NANOBIND_SOURCE_ARGS} 69 | # We can use FIND_PACKAGE_ARGS when we require CMake 3.24 or later. 70 | # 71 | # FIND_PACKAGE_ARGS 72 | ) 73 | ocp_prepare_fetchcontent() 74 | fetchcontent_makeavailable(nanobind) 75 | if(nanobind_SOURCE_DIR) 76 | if(CMAKE_VERSION VERSION_LESS 3.28) 77 | set_property(DIRECTORY "${nanobind_SOURCE_DIR}" PROPERTY EXCLUDE_FROM_ALL 78 | TRUE) 79 | endif() 80 | endif() 81 | endfunction() 82 | ocp_ensure_nanobind() 83 | 84 | function(ocp_ensure_openarm_can) 85 | # We can use FIND_PACKAGE_ARGS in fetchcontent_declare() instead of 86 | # explicit find_package() when we require CMake 3.24 or later. 87 | find_package(OpenArmCAN ${openarm_can_python_VERSION}) 88 | if(OpenArmCAN_FOUND) 89 | return() 90 | endif() 91 | 92 | fetchcontent_declare( 93 | openarm_can 94 | ${OCP_FETCH_CONTENT_COMMON_OPTIONS} 95 | GIT_REPOSITORY "https://github.com/enactic/openarm_can.git" 96 | GIT_TAG "${openarm_can_python_VERSION}" 97 | # We can use FIND_PACKAGE_ARGS when we require CMake 3.24 or later. 98 | # 99 | # FIND_PACKAGE_ARGS NAMES OpenArmCAN 100 | ) 101 | ocp_prepare_fetchcontent() 102 | fetchcontent_makeavailable(openarm_can) 103 | if(openarm_can_SOURCE_DIR) 104 | if(CMAKE_VERSION VERSION_LESS 3.28) 105 | set_property(DIRECTORY "${openarm_can_SOURCE_DIR}" 106 | PROPERTY EXCLUDE_FROM_ALL TRUE) 107 | endif() 108 | add_library(OpenArmCAN::openarm_can ALIAS openarm_can) 109 | endif() 110 | endfunction() 111 | ocp_ensure_openarm_can() 112 | 113 | nanobind_add_module(openarm_can_python src/openarm_can.cpp) 114 | set_target_properties(openarm_can_python PROPERTIES OUTPUT_NAME "openarm_can") 115 | target_link_libraries(openarm_can_python PRIVATE OpenArmCAN::openarm_can) 116 | install(TARGETS openarm_can_python LIBRARY DESTINATION "openarm_can") 117 | install(FILES openarm_can/__init__.py DESTINATION "openarm_can") 118 | -------------------------------------------------------------------------------- /src/openarm/canbus/can_socket.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | 27 | namespace openarm::canbus { 28 | 29 | CANSocket::CANSocket(const std::string& interface, bool enable_fd) 30 | : socket_fd_(-1), interface_(interface), fd_enabled_(enable_fd) { 31 | if (!initialize_socket(interface)) { 32 | throw CANSocketException("Failed to initialize socket for interface: " + interface); 33 | } 34 | } 35 | 36 | CANSocket::~CANSocket() { cleanup(); } 37 | 38 | bool CANSocket::initialize_socket(const std::string& interface) { 39 | // Create socket 40 | socket_fd_ = socket(PF_CAN, SOCK_RAW, CAN_RAW); 41 | if (socket_fd_ < 0) { 42 | return false; 43 | } 44 | 45 | struct ifreq ifr; 46 | struct sockaddr_can addr; 47 | 48 | strncpy(ifr.ifr_name, interface.c_str(), IFNAMSIZ - 1); 49 | ifr.ifr_name[IFNAMSIZ - 1] = '\0'; 50 | 51 | if (ioctl(socket_fd_, SIOCGIFINDEX, &ifr) < 0) { 52 | cleanup(); 53 | return false; 54 | } 55 | 56 | memset(&addr, 0, sizeof(addr)); 57 | addr.can_family = AF_CAN; 58 | addr.can_ifindex = ifr.ifr_ifindex; 59 | 60 | if (fd_enabled_) { 61 | int enable_canfd = 1; 62 | if (setsockopt(socket_fd_, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &enable_canfd, 63 | sizeof(enable_canfd)) < 0) { 64 | cleanup(); 65 | return false; 66 | } 67 | } 68 | 69 | if (bind(socket_fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) { 70 | cleanup(); 71 | return false; 72 | } 73 | 74 | struct timeval timeout; 75 | timeout.tv_sec = 0; 76 | timeout.tv_usec = 100; 77 | if (setsockopt(socket_fd_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { 78 | cleanup(); 79 | return false; 80 | } 81 | 82 | return true; 83 | } 84 | 85 | void CANSocket::cleanup() { 86 | if (socket_fd_ >= 0) { 87 | close(socket_fd_); 88 | socket_fd_ = -1; 89 | } 90 | } 91 | 92 | ssize_t CANSocket::read_raw_frame(void* buffer, size_t buffer_size) { 93 | if (!is_initialized()) return -1; 94 | return read(socket_fd_, buffer, buffer_size); 95 | } 96 | 97 | ssize_t CANSocket::write_raw_frame(const void* buffer, size_t frame_size) { 98 | if (!is_initialized()) return -1; 99 | return write(socket_fd_, buffer, frame_size); 100 | } 101 | 102 | bool CANSocket::write_can_frame(const can_frame& frame) { 103 | return write(socket_fd_, &frame, sizeof(frame)) == sizeof(frame); 104 | } 105 | 106 | bool CANSocket::write_canfd_frame(const canfd_frame& frame) { 107 | return write(socket_fd_, &frame, sizeof(frame)) == sizeof(frame); 108 | } 109 | 110 | bool CANSocket::read_can_frame(can_frame& frame) { 111 | if (!is_initialized()) return false; 112 | ssize_t bytes_read = read(socket_fd_, &frame, sizeof(frame)); 113 | return bytes_read == sizeof(frame); 114 | } 115 | 116 | bool CANSocket::read_canfd_frame(canfd_frame& frame) { 117 | if (!is_initialized()) return false; 118 | ssize_t bytes_read = read(socket_fd_, &frame, sizeof(frame)); 119 | return bytes_read == sizeof(frame); 120 | } 121 | 122 | bool CANSocket::is_data_available(int timeout_us) { 123 | if (!is_initialized()) return false; 124 | 125 | fd_set read_fds; 126 | struct timeval timeout; 127 | 128 | FD_ZERO(&read_fds); 129 | FD_SET(socket_fd_, &read_fds); 130 | 131 | timeout.tv_sec = timeout_us / 1000000; 132 | timeout.tv_usec = (timeout_us % 1000000); 133 | 134 | int result = select(socket_fd_ + 1, &read_fds, nullptr, nullptr, &timeout); 135 | 136 | return (result > 0 && FD_ISSET(socket_fd_, &read_fds)); 137 | } 138 | 139 | } // namespace openarm::canbus 140 | -------------------------------------------------------------------------------- /src/openarm/damiao_motor/dm_motor_device.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace openarm::damiao_motor { 23 | 24 | DMCANDevice::DMCANDevice(Motor& motor, canid_t recv_can_mask, bool use_fd) 25 | : canbus::CANDevice(motor.get_send_can_id(), motor.get_recv_can_id(), recv_can_mask, use_fd), 26 | motor_(motor), 27 | callback_mode_(CallbackMode::STATE), 28 | use_fd_(use_fd) {} 29 | 30 | std::vector DMCANDevice::get_data_from_frame(const can_frame& frame) { 31 | return std::vector(frame.data, frame.data + frame.can_dlc); 32 | } 33 | 34 | std::vector DMCANDevice::get_data_from_frame(const canfd_frame& frame) { 35 | return std::vector(frame.data, frame.data + frame.len); 36 | } 37 | void DMCANDevice::callback(const can_frame& frame) { 38 | if (use_fd_) { 39 | std::cerr << "WARNING: WRONG CALLBACK FUNCTION" << std::endl; 40 | return; 41 | } 42 | 43 | std::vector data = get_data_from_frame(frame); 44 | 45 | switch (callback_mode_) { 46 | case STATE: 47 | if (frame.can_dlc >= 8) { 48 | // Convert frame data to vector and let Motor handle parsing 49 | StateResult result = CanPacketDecoder::parse_motor_state_data(motor_, data); 50 | if (frame.can_id == motor_.get_recv_can_id() && result.valid) { 51 | motor_.update_state(result.position, result.velocity, result.torque, 52 | result.t_mos, result.t_rotor); 53 | } 54 | } 55 | break; 56 | case PARAM: { 57 | ParamResult result = CanPacketDecoder::parse_motor_param_data(data); 58 | if (result.valid) { 59 | motor_.set_temp_param(result.rid, result.value); 60 | } 61 | break; 62 | } 63 | case IGNORE: 64 | return; 65 | default: 66 | break; 67 | } 68 | } 69 | 70 | void DMCANDevice::callback(const canfd_frame& frame) { 71 | if (not use_fd_) { 72 | std::cerr << "WARNING: CANFD MODE NOT ENABLED" << std::endl; 73 | return; 74 | } 75 | 76 | if (frame.can_id != motor_.get_recv_can_id()) { 77 | std::cerr << "WARNING: CANFD FRAME ID DOES NOT MATCH MOTOR ID" << std::endl; 78 | return; 79 | } 80 | 81 | std::vector data = get_data_from_frame(frame); 82 | if (callback_mode_ == STATE) { 83 | StateResult result = CanPacketDecoder::parse_motor_state_data(motor_, data); 84 | if (result.valid) { 85 | motor_.update_state(result.position, result.velocity, result.torque, result.t_mos, 86 | result.t_rotor); 87 | } 88 | } else if (callback_mode_ == PARAM) { 89 | ParamResult result = CanPacketDecoder::parse_motor_param_data(data); 90 | if (result.valid) { 91 | motor_.set_temp_param(result.rid, result.value); 92 | } 93 | } else if (callback_mode_ == IGNORE) { 94 | return; 95 | } 96 | } 97 | 98 | can_frame DMCANDevice::create_can_frame(canid_t send_can_id, std::vector data) { 99 | can_frame frame; 100 | std::memset(&frame, 0, sizeof(frame)); 101 | frame.can_id = send_can_id; 102 | frame.can_dlc = data.size(); 103 | std::copy(data.begin(), data.end(), frame.data); 104 | return frame; 105 | } 106 | 107 | canfd_frame DMCANDevice::create_canfd_frame(canid_t send_can_id, std::vector data) { 108 | canfd_frame frame; 109 | frame.can_id = send_can_id; 110 | frame.len = data.size(); 111 | frame.flags = CANFD_BRS; 112 | std::copy(data.begin(), data.end(), frame.data); 113 | return frame; 114 | } 115 | 116 | } // namespace openarm::damiao_motor 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenArm CAN Library 2 | 3 | A C++ library for CAN communication with OpenArm robotic hardware, supporting Damiao motors over CAN/CAN-FD interfaces. 4 | This library is a part of [OpenArm](https://github.com/enactic/openarm/). See detailed setup guide and docs [here](https://docs.openarm.dev/software/can). 5 | 6 | 7 | ## Quick Start 8 | 9 | ### Prerequisites 10 | 11 | - Linux with SocketCAN support 12 | - CAN interface hardware 13 | 14 | ### 1. Install 15 | 16 | #### Ubuntu 17 | 18 | * 22.04 Jammy Jellyfish 19 | * 24.04 Noble Numbat 20 | 21 | ```bash 22 | sudo apt install -y software-properties-common 23 | sudo add-apt-repository -y ppa:openarm/main 24 | sudo apt update 25 | sudo apt install -y \ 26 | libopenarm-can-dev \ 27 | openarm-can-utils 28 | ``` 29 | 30 | #### AlmaLinux, CentOS, Fedora, RHEL, and Rocky Linux 31 | 32 | 1. Enable [EPEL](https://docs.fedoraproject.org/en-US/epel/). (Not required for [Fedora](https://fedoraproject.org/)) 33 | * AlmaLinux 8 / Rocky Linux 8 34 | ```bash 35 | sudo dnf install -y epel-release 36 | sudo dnf config-manager --set-enabled powertools 37 | ``` 38 | * AlmaLinux 9 & 10 / Rocky Linux 9 & 10 39 | ```bash 40 | sudo dnf install -y epel-release 41 | sudo crb enable 42 | ``` 43 | * CentOS Stream 9 44 | ```bash 45 | sudo dnf config-manager --set-enabled crb 46 | sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel{,-next}-release-latest-9.noarch.rpm 47 | ``` 48 | * CentOS Stream 10 49 | ```bash 50 | sudo dnf config-manager --set-enabled crb 51 | sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm 52 | ``` 53 | * RHEL 8 & 9 & 10 54 | ```bash 55 | releasever="$(. /etc/os-release && echo $VERSION_ID | grep -oE '^[0-9]+')" 56 | sudo subscription-manager repos --enable codeready-builder-for-rhel-$releasever-$(arch)-rpms 57 | sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-$releasever.noarch.rpm 58 | ``` 59 | 2. Install the package. 60 | ```bash 61 | sudo dnf update 62 | sudo dnf install -y \ 63 | openarm-can-devel \ 64 | openarm-can-utils 65 | ``` 66 | 67 | ### 2. Setup CAN Interface 68 | 69 | Configure your CAN interface using the provided script: 70 | 71 | ```bash 72 | # CAN 2.0 (default) 73 | openarm-can-configure-socketcan can0 74 | 75 | # CAN-FD with 5Mbps data rate 76 | openarm-can-configure-socketcan can0 -fd 77 | ``` 78 | 79 | ### 3. C++ Library 80 | 81 | ```cpp 82 | #include 83 | #include 84 | 85 | openarm::can::socket::OpenArm arm("can0", true); // CAN-FD enabled 86 | std::vector motor_types = { 87 | openarm::damiao_motor::MotorType::DM4310, openarm::damiao_motor::MotorType::DM4310}; 88 | std::vector send_can_ids = {0x01, 0x02}; 89 | std::vector recv_can_ids = {0x11, 0x12}; 90 | 91 | openarm.init_arm_motors(motor_types, send_can_ids, recv_can_ids); 92 | openarm.enable_all(); 93 | ``` 94 | 95 | See [dev/README.md](dev/README.md) for how to build. 96 | 97 | ### 4. Python (🚧 EXPERIMENTAL - TEMPORARY 🚧) 98 | 99 | > [!WARNING] 100 | > 101 | > ⚠️ **WARNING: UNSTABLE API** ⚠️ 102 | > Python bindings are currently a direct low level **temporary port**, and will change **DRASTICALLY**. 103 | > The interface is may break between versions.Use at your own risk! Discussions on the interface are welcomed. 104 | 105 | **Build & Install:** 106 | 107 | Please ensure that you install the C++ library first, as `1. Install` or [dev/README.md](dev/README.md). 108 | 109 | ```bash 110 | cd python 111 | 112 | # Create and activate virtual environment (recommended) 113 | python -m venv venv 114 | source venv/bin/activate 115 | 116 | pip install . 117 | ``` 118 | 119 | **Usage:** 120 | 121 | ```python 122 | # WARNING: This API is unstable and will change! 123 | import openarm_can as oa 124 | 125 | arm = oa.OpenArm("can0", True) # CAN-FD enabled 126 | arm.init_arm_motors([oa.MotorType.DM4310], [0x01], [0x11]) 127 | arm.enable_all() 128 | ``` 129 | 130 | ### Examples 131 | 132 | - **C++**: `examples/demo.cpp` - Complete arm control demo 133 | - **Python**: `python/examples/example.py` - Basic Python usage 134 | 135 | ## For developers 136 | 137 | See [dev/README.md](dev/README.md). 138 | 139 | ## Related links 140 | 141 | - 📚 Read the [documentation](https://docs.openarm.dev/software/can/) 142 | - 💬 Join the community on [Discord](https://discord.gg/FsZaZ4z3We) 143 | - 📬 Contact us through 144 | 145 | ## License 146 | 147 | Licensed under the Apache License 2.0. See `LICENSE.txt` for details. 148 | 149 | Copyright 2025 Enactic, Inc. 150 | 151 | ## Code of Conduct 152 | 153 | All participation in the OpenArm project is governed by our [Code of Conduct](CODE_OF_CONDUCT.md). 154 | -------------------------------------------------------------------------------- /setup/openarm-can-set-zero: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2025 Enactic, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http:#www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eu 18 | 19 | # CAN Interface Script 20 | 21 | # Function to display usage 22 | usage() { 23 | echo "Usage: $0 [CAN_ID] [--all]" 24 | echo " CAN_IF: CAN interface name (e.g., can0)" 25 | echo " CAN_ID: CAN ID in hex format (e.g., 00x) - not needed with --all" 26 | echo " --all: Send to all IDs from 001 to 008" 27 | echo "" 28 | echo "Examples:" 29 | echo " $0 can0 001" 30 | echo " $0 can0 --all" 31 | } 32 | 33 | # Function to check if CAN interface is up and get baudrate 34 | check_can_interface() { 35 | local interface=$1 36 | 37 | # Check if interface exists and is up 38 | if ! ip link show "$interface" &>/dev/null; then 39 | echo "Error: CAN interface $interface does not exist" 40 | return 1 41 | fi 42 | 43 | # Check if interface is up 44 | local state 45 | state=$(ip link show "$interface" | grep -o "state [A-Z]*" | cut -d' ' -f2) 46 | if [ "$state" != "UP" ]; then 47 | echo "Error: CAN interface $interface is not UP (current state: $state)" 48 | return 1 49 | fi 50 | 51 | echo "CAN interface $interface is UP" 52 | 53 | # Try to get baudrate information 54 | if command -v ethtool &>/dev/null; then 55 | local baudrate 56 | baudrate=$(ethtool "$interface" 2>/dev/null | grep -i speed | cut -d: -f2 | tr -d ' ') 57 | if [ -n "$baudrate" ]; then 58 | echo "Baudrate: $baudrate" 59 | fi 60 | fi 61 | 62 | # Alternative method using ip command 63 | local bitrate 64 | bitrate=$(ip -details link show "$interface" 2>/dev/null | grep -o "bitrate [0-9]*" | cut -d' ' -f2) 65 | if [ -n "$bitrate" ]; then 66 | echo "Bitrate: ${bitrate} bps" 67 | fi 68 | 69 | return 0 70 | } 71 | 72 | # Function to send CAN messages for a single ID 73 | send_can_messages() { 74 | local CAN_ID=$1 75 | local CAN_IF=$2 76 | 77 | echo "Sending CAN messages for ID: $CAN_ID on interface: $CAN_IF" 78 | 79 | # Send first disablemessage 80 | echo "Sending: cansend $CAN_IF ${CAN_ID}#FFFFFFFFFFFFFFFD" 81 | cansend "$CAN_IF" "${CAN_ID}#FFFFFFFFFFFFFFFD" 82 | 83 | sleep 0.1 84 | 85 | # Send second set zero message 86 | echo "Sending: cansend $CAN_IF ${CAN_ID}#FFFFFFFFFFFFFFFE" 87 | cansend "$CAN_IF" "${CAN_ID}#FFFFFFFFFFFFFFFE" 88 | 89 | sleep 0.1 90 | 91 | # Send third disable message 92 | echo "Sending: cansend $CAN_IF ${CAN_ID}#FFFFFFFFFFFFFFFD" 93 | cansend "$CAN_IF" "${CAN_ID}#FFFFFFFFFFFFFFFD" 94 | 95 | sleep 0.1 96 | 97 | echo "Messages sent for ID: $CAN_ID" 98 | echo "" 99 | } 100 | 101 | # Main script logic 102 | main() { 103 | # Check for minimum arguments 104 | if [ $# -lt 1 ]; then 105 | usage 106 | exit 1 107 | fi 108 | 109 | if [ "$1" = "-h" ]; then 110 | usage 111 | exit 0 112 | fi 113 | 114 | local CAN_IF=$1 115 | local CAN_ID="" 116 | local all_flag=false 117 | 118 | # Check for --all flag 119 | if [ "$2" = "--all" ]; then 120 | all_flag=true 121 | else 122 | CAN_ID=$2 123 | fi 124 | 125 | # Validate CAN_IF 126 | if [ -z "$CAN_IF" ]; then 127 | usage 128 | exit 1 129 | fi 130 | 131 | # Validate CAN_ID only if --all flag is not set 132 | if [ "$all_flag" = false ] && [ -z "$CAN_ID" ]; then 133 | echo "Error: CAN_ID is required when -all flag is not used" 134 | usage 135 | exit 1 136 | fi 137 | 138 | # Check if cansend command is available 139 | if ! command -v cansend &>/dev/null; then 140 | echo "Error: cansend command not found. Please install can-utils package." 141 | exit 1 142 | fi 143 | 144 | # Check CAN interface status 145 | if ! check_can_interface "$CAN_IF"; then 146 | exit 1 147 | fi 148 | 149 | echo "" 150 | 151 | # Execute based on flags 152 | if [ "$all_flag" = true ]; then 153 | echo "Sending set zero messages to all motor with CAN IDs from 001 to 008" 154 | echo "==========================================" 155 | for i in {1..8}; do 156 | local padded_id 157 | padded_id=$(printf "%03d" "$i") 158 | send_can_messages "$padded_id" "$CAN_IF" 159 | done 160 | else 161 | send_can_messages "$CAN_ID" "$CAN_IF" 162 | fi 163 | 164 | echo "Set zero completed." 165 | } 166 | 167 | # Run main function with all arguments 168 | main "$@" 169 | -------------------------------------------------------------------------------- /src/openarm/can/socket/openarm.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | 21 | namespace openarm::can::socket { 22 | 23 | OpenArm::OpenArm(const std::string& can_interface, bool enable_fd) 24 | : can_interface_(can_interface), enable_fd_(enable_fd) { 25 | can_socket_ = std::make_unique(can_interface_, enable_fd_); 26 | master_can_device_collection_ = std::make_unique(*can_socket_); 27 | arm_ = std::make_unique(*can_socket_); 28 | gripper_ = std::make_unique(*can_socket_); 29 | } 30 | 31 | void OpenArm::init_arm_motors(const std::vector& motor_types, 32 | const std::vector& send_can_ids, 33 | const std::vector& recv_can_ids) { 34 | if (motor_types.size() != send_can_ids.size() || motor_types.size() != recv_can_ids.size()) { 35 | throw std::invalid_argument( 36 | "Motor types, send CAN IDs, and receive CAN IDs vectors must have the same size, " 37 | "currently: " + 38 | std::to_string(motor_types.size()) + ", " + std::to_string(send_can_ids.size()) + ", " + 39 | std::to_string(recv_can_ids.size())); 40 | } 41 | arm_->init_motor_devices(motor_types, send_can_ids, recv_can_ids, enable_fd_); 42 | register_dm_device_collection(*arm_); 43 | } 44 | 45 | void OpenArm::init_gripper_motor(damiao_motor::MotorType motor_type, uint32_t send_can_id, 46 | uint32_t recv_can_id) { 47 | gripper_->init_motor_device(motor_type, send_can_id, recv_can_id, enable_fd_); 48 | register_dm_device_collection(*gripper_); 49 | } 50 | 51 | void OpenArm::register_dm_device_collection(damiao_motor::DMDeviceCollection& device_collection) { 52 | for (const auto& [id, device] : device_collection.get_device_collection().get_devices()) { 53 | master_can_device_collection_->add_device(device); 54 | } 55 | sub_dm_device_collections_.push_back(&device_collection); 56 | } 57 | 58 | void OpenArm::enable_all() { 59 | for (damiao_motor::DMDeviceCollection* device_collection : sub_dm_device_collections_) { 60 | device_collection->enable_all(); 61 | } 62 | } 63 | 64 | void OpenArm::set_zero_all() { 65 | for (damiao_motor::DMDeviceCollection* device_collection : sub_dm_device_collections_) { 66 | device_collection->set_zero_all(); 67 | } 68 | } 69 | 70 | void OpenArm::refresh_all() { 71 | for (damiao_motor::DMDeviceCollection* device_collection : sub_dm_device_collections_) { 72 | device_collection->refresh_all(); 73 | } 74 | } 75 | 76 | void OpenArm::refresh_one(int i) { 77 | for (damiao_motor::DMDeviceCollection* device_collection : sub_dm_device_collections_) { 78 | device_collection->refresh_one(i); 79 | } 80 | } 81 | 82 | void OpenArm::disable_all() { 83 | for (damiao_motor::DMDeviceCollection* device_collection : sub_dm_device_collections_) { 84 | device_collection->disable_all(); 85 | } 86 | } 87 | 88 | void OpenArm::recv_all(int timeout_us) { 89 | // The timeout for select() is set to timeout_us (default: 500 us). 90 | // Tuning this value may improve the performance but should be done with caution. 91 | 92 | // CAN FD 93 | if (enable_fd_) { 94 | canfd_frame response_frame; 95 | while (can_socket_->is_data_available(timeout_us) && 96 | can_socket_->read_canfd_frame(response_frame)) { 97 | master_can_device_collection_->dispatch_frame_callback(response_frame); 98 | } 99 | } 100 | // CAN 2.0 101 | else { 102 | can_frame response_frame; 103 | while (can_socket_->is_data_available(timeout_us) && 104 | can_socket_->read_can_frame(response_frame)) { 105 | master_can_device_collection_->dispatch_frame_callback(response_frame); 106 | } 107 | } 108 | // } 109 | } 110 | 111 | void OpenArm::query_param_all(int RID) { 112 | for (damiao_motor::DMDeviceCollection* device_collection : sub_dm_device_collections_) { 113 | device_collection->query_param_all(RID); 114 | } 115 | } 116 | 117 | void OpenArm::set_callback_mode_all(damiao_motor::CallbackMode callback_mode) { 118 | for (damiao_motor::DMDeviceCollection* device_collection : sub_dm_device_collections_) { 119 | device_collection->set_callback_mode_all(callback_mode); 120 | } 121 | } 122 | 123 | } // namespace openarm::can::socket 124 | -------------------------------------------------------------------------------- /examples/demo.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | int main() { 24 | try { 25 | std::cout << "=== OpenArm CAN Example ===" << std::endl; 26 | std::cout << "This example demonstrates the OpenArm API functionality" << std::endl; 27 | 28 | // Initialize OpenArm with CAN interface and enable CAN-FD 29 | std::cout << "Initializing OpenArm CAN..." << std::endl; 30 | openarm::can::socket::OpenArm openarm("can0", true); // Use CAN-FD on can0 interface 31 | 32 | // Initialize arm motors 33 | std::vector motor_types = { 34 | openarm::damiao_motor::MotorType::DM4310, openarm::damiao_motor::MotorType::DM4310}; 35 | std::vector send_can_ids = {0x01, 0x02}; 36 | std::vector recv_can_ids = {0x11, 0x12}; 37 | openarm.init_arm_motors(motor_types, send_can_ids, recv_can_ids); 38 | 39 | // Initialize gripper 40 | std::cout << "Initializing gripper..." << std::endl; 41 | openarm.init_gripper_motor(openarm::damiao_motor::MotorType::DM4310, 0x08, 0x18); 42 | 43 | // Set callback mode to ignore and enable all motors 44 | openarm.set_callback_mode_all(openarm::damiao_motor::CallbackMode::IGNORE); 45 | 46 | // Enable all motors 47 | std::cout << "\n=== Enabling Motors ===" << std::endl; 48 | openarm.enable_all(); 49 | // Allow time (2ms) for the motors to respond for slow operations like enabling 50 | openarm.recv_all(2000); 51 | 52 | // Set device mode to param and query motor id 53 | std::cout << "\n=== Querying Motor Recv IDs ===" << std::endl; 54 | openarm.set_callback_mode_all(openarm::damiao_motor::CallbackMode::PARAM); 55 | openarm.query_param_all(static_cast(openarm::damiao_motor::RID::MST_ID)); 56 | // Allow time (2ms) for the motors to respond for slow operations like querying 57 | // parameter from register 58 | openarm.recv_all(2000); 59 | 60 | // Access motors through components 61 | for (const auto& motor : openarm.get_arm().get_motors()) { 62 | std::cout << "Arm Motor: " << motor.get_send_can_id() << " ID: " 63 | << motor.get_param(static_cast(openarm::damiao_motor::RID::MST_ID)) 64 | << std::endl; 65 | } 66 | for (const auto& motor : openarm.get_gripper().get_motors()) { 67 | std::cout << "Gripper Motor: " << motor.get_send_can_id() << " ID: " 68 | << motor.get_param(static_cast(openarm::damiao_motor::RID::MST_ID)) 69 | << std::endl; 70 | } 71 | 72 | // Set device mode to state and control motor 73 | std::cout << "\n=== Controlling Motors ===" << std::endl; 74 | openarm.set_callback_mode_all(openarm::damiao_motor::CallbackMode::STATE); 75 | 76 | // Control arm motors with position control 77 | openarm.get_arm().mit_control_all({openarm::damiao_motor::MITParam{2, 1, 0, 0, 0}, 78 | openarm::damiao_motor::MITParam{2, 1, 0, 0, 0}}); 79 | openarm.recv_all(500); 80 | 81 | // Control arm motors with torque control 82 | openarm.get_arm().mit_control_all({openarm::damiao_motor::MITParam{0, 0, 0, 0, 0.1}, 83 | openarm::damiao_motor::MITParam{0, 0, 0, 0, 0.1}}); 84 | openarm.recv_all(500); 85 | 86 | // Control gripper 87 | std::cout << "Closing gripper..." << std::endl; 88 | openarm.get_gripper().close(); 89 | openarm.recv_all(1000); 90 | 91 | for (int i = 0; i < 10; i++) { 92 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 93 | 94 | openarm.refresh_all(); 95 | openarm.recv_all(300); 96 | 97 | // Display arm motor states 98 | for (const auto& motor : openarm.get_arm().get_motors()) { 99 | std::cout << "Arm Motor: " << motor.get_send_can_id() 100 | << " position: " << motor.get_position() << std::endl; 101 | } 102 | // Display gripper state 103 | for (const auto& motor : openarm.get_gripper().get_motors()) { 104 | std::cout << "Gripper Motor: " << motor.get_send_can_id() 105 | << " position: " << motor.get_position() << std::endl; 106 | } 107 | } 108 | 109 | openarm.disable_all(); 110 | openarm.recv_all(1000); 111 | 112 | } catch (const std::exception& e) { 113 | std::cerr << "Error: " << e.what() << std::endl; 114 | return -1; 115 | } 116 | 117 | return 0; 118 | } 119 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require "date" 16 | 17 | require_relative "helper" 18 | 19 | version = ENV["VERSION"] || Helper.detect_version 20 | 21 | def git_clone_archive(url, tag, archive_path) 22 | mkdir_p(File.dirname(archive_path)) 23 | clone_dir = File.basename(archive_path, ".tar.gz") 24 | rm_rf(clone_dir) 25 | sh("git", "clone", url, "--branch", tag, "--recursive", clone_dir) 26 | yield(clone_dir) if block_given? 27 | sh("tar", 28 | "--exclude-vcs", 29 | "--exclude-vcs-ignores", 30 | "-czf", archive_path, 31 | clone_dir) 32 | rm_rf(clone_dir) 33 | end 34 | 35 | cmakelists = File.read(File.join("python", "CMakeLists.txt")) 36 | nanobind_version = cmakelists[/set\(NANOBIND_BUNDLED_VERSION \"(.+)"\)/, 1] 37 | nanobind_tar_gz = File.join("python", "vendor", "nanobind-#{nanobind_version}.tar.gz") 38 | file nanobind_tar_gz do 39 | git_clone_archive("https://github.com/wjakob/nanobind.git", 40 | "v#{version}", 41 | nanobind_tar_gz) do |clone_dir| 42 | rm_rf(File.join(clone_dir, "docs")) # 1.1MB 43 | end 44 | end 45 | 46 | namespace :vendor do 47 | desc "Download vendored dependencies" 48 | task download: [ 49 | nanobind_tar_gz, 50 | ] 51 | end 52 | 53 | archive_base_name = "openarm-can-#{version}" 54 | archive_tar_gz = "#{archive_base_name}.tar.gz" 55 | file archive_tar_gz => ["vendor:download"] do 56 | sh("git", "archive", "HEAD", 57 | # This --prefix is for --add-file 58 | "--prefix", "#{archive_base_name}/python/vendor/", 59 | "--add-file", nanobind_tar_gz, 60 | "--prefix", "#{archive_base_name}/", 61 | "--output", archive_tar_gz) 62 | end 63 | 64 | desc "Create #{archive_tar_gz}" 65 | task :dist => archive_tar_gz 66 | 67 | namespace :release do 68 | namespace :version do 69 | desc "Update versions for a new release" 70 | task :update do 71 | new_version = ENV["NEW_VERSION"] 72 | if new_version.nil? 73 | raise "You must specify NEW_VERSION=..." 74 | end 75 | new_release_date = ENV["NEW_RELEASE_DATE"] || Date.today.iso8601 76 | Helper.update_cmake_lists_txt_version(new_version) 77 | Helper.update_content("package.xml") do |content| 78 | content.sub(/().*?(<\/version>)/) do 79 | "#{$1}#{new_version}#{$2}" 80 | end 81 | end 82 | Helper.update_content("python/CMakeLists.txt") do |content| 83 | content.sub(/^( VERSION ).*?$/) do 84 | "#{$1}#{new_version}" 85 | end 86 | end 87 | Helper.update_content("python/openarm_can/__init__.py") do |content| 88 | content.sub(/^(__version__ = ").*?(")$/) do 89 | "#{$1}#{new_version}#{$2}" 90 | end 91 | end 92 | Helper.update_content("python/pyproject.toml") do |content| 93 | content.sub(/^(version = ").*?(")$/) do 94 | "#{$1}#{new_version}#{$2}" 95 | end 96 | end 97 | Helper.update_content("python/setup.py") do |content| 98 | content.sub(/^( version=").*?(",)$/) do 99 | "#{$1}#{new_version}#{$2}" 100 | end 101 | end 102 | ruby("-C", 103 | "packages", 104 | "-S", 105 | "rake", 106 | "version:update", 107 | "RELEASE_DATE=#{new_release_date}") 108 | sh("git", 109 | "add", 110 | "CMakeLists.txt", 111 | "package.xml", 112 | "packages/debian/changelog", 113 | "packages/fedora/openarm-can.spec", 114 | "python/CMakeLists.txt", 115 | "python/openarm_can/__init__.py", 116 | "python/pyproject.toml", 117 | "python/setup.py") 118 | sh("git", 119 | "commit", 120 | "-m", 121 | "Update version info to #{new_version} (#{new_release_date})") 122 | sh("git", "push") 123 | end 124 | end 125 | 126 | desc "Tag" 127 | task :tag do 128 | current_version = Helper.detect_version 129 | 130 | changelog = "packages/debian/changelog" 131 | case File.readlines(changelog)[0] 132 | when /\((.+)-1\)/ 133 | package_version = $1 134 | unless package_version == current_version 135 | raise "package version isn't updated: #{package_version}" 136 | end 137 | else 138 | raise "failed to detect deb package version: #{changelog}" 139 | end 140 | 141 | sh("git", 142 | "tag", 143 | current_version, 144 | "-a", 145 | "-m", 146 | "OpenArm CAN #{current_version}!!!") 147 | sh("git", "push", "origin", current_version) 148 | end 149 | 150 | desc "Release packages for Ubuntu" 151 | task :ubuntu do 152 | current_version = Helper.detect_version 153 | Helper.wait_github_actions_workflow(current_version, "package.yaml") 154 | ruby("-C", 155 | "packages", 156 | "-S", 157 | "rake", 158 | "ubuntu") 159 | end 160 | end 161 | 162 | desc "Release" 163 | task release: [ 164 | "release:version:update", 165 | "release:tag", 166 | "release:ubuntu", 167 | ] 168 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | cmake_minimum_required(VERSION 3.22) 16 | project(openarm_can VERSION 1.2.1) 17 | 18 | # Set C++ standard 19 | set(CMAKE_CXX_STANDARD 17) 20 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 21 | 22 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 23 | add_compile_options(-Wall -Wextra -Wpedantic) 24 | endif() 25 | 26 | # Testing support 27 | include(CTest) 28 | 29 | include(GNUInstallDirs) 30 | 31 | # Create the main library 32 | add_library( 33 | openarm_can 34 | src/openarm/can/socket/arm_component.cpp 35 | src/openarm/can/socket/gripper_component.cpp 36 | src/openarm/can/socket/openarm.cpp 37 | src/openarm/canbus/can_device_collection.cpp 38 | src/openarm/canbus/can_socket.cpp 39 | src/openarm/damiao_motor/dm_motor.cpp 40 | src/openarm/damiao_motor/dm_motor_control.cpp 41 | src/openarm/damiao_motor/dm_motor_device.cpp 42 | src/openarm/damiao_motor/dm_motor_device_collection.cpp) 43 | set_target_properties( 44 | openarm_can 45 | PROPERTIES POSITION_INDEPENDENT_CODE ON 46 | VERSION ${PROJECT_VERSION} 47 | SOVERSION ${PROJECT_VERSION_MAJOR}) 48 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.23) 49 | target_sources( 50 | openarm_can 51 | PUBLIC FILE_SET 52 | HEADERS 53 | TYPE 54 | HEADERS 55 | BASE_DIRS 56 | include 57 | FILES 58 | include/openarm/can/socket/arm_component.hpp 59 | include/openarm/can/socket/gripper_component.hpp 60 | include/openarm/can/socket/openarm.hpp 61 | include/openarm/canbus/can_device.hpp 62 | include/openarm/canbus/can_device_collection.hpp 63 | include/openarm/canbus/can_socket.hpp 64 | include/openarm/damiao_motor/dm_motor.hpp 65 | include/openarm/damiao_motor/dm_motor_constants.hpp 66 | include/openarm/damiao_motor/dm_motor_control.hpp 67 | include/openarm/damiao_motor/dm_motor_device.hpp 68 | include/openarm/damiao_motor/dm_motor_device_collection.hpp) 69 | install( 70 | TARGETS openarm_can 71 | EXPORT openarm_can_export 72 | FILE_SET HEADERS) 73 | else() 74 | target_include_directories( 75 | openarm_can PUBLIC $ 76 | $) 77 | install(TARGETS openarm_can EXPORT openarm_can_export) 78 | install(DIRECTORY include/openarm TYPE INCLUDE) 79 | endif() 80 | 81 | # CMake package 82 | set(INSTALL_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/OpenArmCAN) 83 | install( 84 | EXPORT openarm_can_export 85 | DESTINATION ${INSTALL_CMAKE_DIR} 86 | NAMESPACE OpenArmCAN:: 87 | FILE OpenArmCANTargets.cmake) 88 | include(CMakePackageConfigHelpers) 89 | configure_package_config_file( 90 | OpenArmCANConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/OpenArmCANConfig.cmake 91 | INSTALL_DESTINATION ${INSTALL_CMAKE_DIR}) 92 | write_basic_package_version_file( 93 | ${CMAKE_CURRENT_BINARY_DIR}/OpenArmCANConfigVersion.cmake 94 | VERSION ${PROJECT_VERSION} 95 | COMPATIBILITY SameMajorVersion) 96 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/OpenArmCANConfig.cmake 97 | ${CMAKE_CURRENT_BINARY_DIR}/OpenArmCANConfigVersion.cmake 98 | DESTINATION ${INSTALL_CMAKE_DIR}) 99 | 100 | # pkg-config package 101 | if(IS_ABSOLUTE "${CMAKE_INSTALL_INCLUDEDIR}") 102 | set(PKG_CONFIG_INCLUDEDIR "${CMAKE_INSTALL_INCLUDEDIR}") 103 | else() 104 | set(PKG_CONFIG_INCLUDEDIR "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") 105 | endif() 106 | if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") 107 | set(PKG_CONFIG_LIBDIR "${CMAKE_INSTALL_LIBDIR}") 108 | else() 109 | set(PKG_CONFIG_LIBDIR "\${prefix}/${CMAKE_INSTALL_LIBDIR}") 110 | endif() 111 | configure_file(openarm-can.pc.in ${CMAKE_CURRENT_BINARY_DIR}/openarm-can.pc 112 | @ONLY) 113 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/openarm-can.pc 114 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) 115 | 116 | add_executable(openarm-can-motor-check setup/motor_check.cpp) 117 | target_link_libraries(openarm-can-motor-check openarm_can) 118 | install(TARGETS openarm-can-motor-check DESTINATION ${CMAKE_INSTALL_BINDIR}) 119 | 120 | add_executable(openarm-can-diagnosis setup/openarm_can_diagnosis.cpp) 121 | target_link_libraries(openarm-can-diagnosis openarm_can) 122 | install(TARGETS openarm-can-diagnosis DESTINATION ${CMAKE_INSTALL_BINDIR}) 123 | 124 | # Add motor control example executable 125 | add_executable(openarm-can-demo examples/demo.cpp) 126 | target_link_libraries(openarm-can-demo openarm_can) 127 | install(TARGETS openarm-can-demo DESTINATION ${CMAKE_INSTALL_BINDIR}) 128 | 129 | # Install scripts 130 | install( 131 | PROGRAMS setup/openarm-can-change-baudrate 132 | setup/openarm-can-configure-socketcan 133 | setup/openarm-can-configure-socketcan-4-arms 134 | setup/openarm-can-set-zero 135 | setup/openarm-can-zero-position-calibration 136 | DESTINATION ${CMAKE_INSTALL_BINDIR}) 137 | 138 | # Add tests 139 | if(BUILD_TESTING) 140 | # add_subdirectory(test) 141 | endif() 142 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official email address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | hi_public@reazon.jp. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /src/openarm/damiao_motor/dm_motor_device_collection.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | 21 | namespace openarm::damiao_motor { 22 | 23 | DMDeviceCollection::DMDeviceCollection(canbus::CANSocket& can_socket) 24 | : can_socket_(can_socket), 25 | can_packet_encoder_(std::make_unique()), 26 | can_packet_decoder_(std::make_unique()), 27 | device_collection_(std::make_unique(can_socket_)) {} 28 | 29 | void DMDeviceCollection::enable_all() { 30 | for (auto dm_device : get_dm_devices()) { 31 | auto& motor = dm_device->get_motor(); 32 | CANPacket enable_packet = CanPacketEncoder::create_enable_command(motor); 33 | send_command_to_device(dm_device, enable_packet); 34 | } 35 | } 36 | 37 | void DMDeviceCollection::disable_all() { 38 | for (auto dm_device : get_dm_devices()) { 39 | CANPacket disable_packet = CanPacketEncoder::create_disable_command(dm_device->get_motor()); 40 | send_command_to_device(dm_device, disable_packet); 41 | } 42 | } 43 | 44 | void DMDeviceCollection::set_zero(int i) { 45 | auto dm_device = get_dm_devices().at(i); 46 | auto zero_packet = CanPacketEncoder::create_set_zero_command(dm_device->get_motor()); 47 | send_command_to_device(dm_device, zero_packet); 48 | } 49 | 50 | void DMDeviceCollection::set_zero_all() { 51 | for (auto dm_device : get_dm_devices()) { 52 | CANPacket zero_packet = CanPacketEncoder::create_set_zero_command(dm_device->get_motor()); 53 | send_command_to_device(dm_device, zero_packet); 54 | } 55 | } 56 | 57 | void DMDeviceCollection::refresh_one(int i) { 58 | auto dm_device = get_dm_devices().at(i); 59 | auto& motor = dm_device->get_motor(); 60 | CANPacket refresh_packet = CanPacketEncoder::create_refresh_command(motor); 61 | send_command_to_device(dm_device, refresh_packet); 62 | } 63 | 64 | void DMDeviceCollection::refresh_all() { 65 | for (auto dm_device : get_dm_devices()) { 66 | auto& motor = dm_device->get_motor(); 67 | CANPacket refresh_packet = CanPacketEncoder::create_refresh_command(motor); 68 | send_command_to_device(dm_device, refresh_packet); 69 | } 70 | } 71 | 72 | void DMDeviceCollection::set_callback_mode_all(CallbackMode callback_mode) { 73 | for (auto dm_device : get_dm_devices()) { 74 | dm_device->set_callback_mode(callback_mode); 75 | } 76 | } 77 | 78 | void DMDeviceCollection::query_param_one(int i, int RID) { 79 | CANPacket param_query = 80 | CanPacketEncoder::create_query_param_command(get_dm_devices()[i]->get_motor(), RID); 81 | send_command_to_device(get_dm_devices()[i], param_query); 82 | } 83 | 84 | void DMDeviceCollection::query_param_all(int RID) { 85 | for (auto dm_device : get_dm_devices()) { 86 | CANPacket param_query = 87 | CanPacketEncoder::create_query_param_command(dm_device->get_motor(), RID); 88 | send_command_to_device(dm_device, param_query); 89 | } 90 | } 91 | 92 | void DMDeviceCollection::send_command_to_device(std::shared_ptr dm_device, 93 | const CANPacket& packet) { 94 | if (can_socket_.is_canfd_enabled()) { 95 | canfd_frame frame = dm_device->create_canfd_frame(packet.send_can_id, packet.data); 96 | can_socket_.write_canfd_frame(frame); 97 | } else { 98 | can_frame frame = dm_device->create_can_frame(packet.send_can_id, packet.data); 99 | can_socket_.write_can_frame(frame); 100 | } 101 | } 102 | 103 | void DMDeviceCollection::mit_control_one(int i, const MITParam& mit_param) { 104 | CANPacket mit_cmd = 105 | CanPacketEncoder::create_mit_control_command(get_dm_devices()[i]->get_motor(), mit_param); 106 | send_command_to_device(get_dm_devices()[i], mit_cmd); 107 | } 108 | 109 | void DMDeviceCollection::mit_control_all(const std::vector& mit_params) { 110 | for (size_t i = 0; i < mit_params.size(); i++) { 111 | mit_control_one(i, mit_params[i]); 112 | } 113 | } 114 | 115 | void DMDeviceCollection::posvel_control_one(int i, const PosVelParam& posvel_param) { 116 | CANPacket posvel_cmd = CanPacketEncoder::create_posvel_control_command( 117 | get_dm_devices()[i]->get_motor(), posvel_param); 118 | send_command_to_device(get_dm_devices()[i], posvel_cmd); 119 | } 120 | 121 | void DMDeviceCollection::posvel_control_all(const std::vector& posvel_params) { 122 | for (size_t i = 0; i < posvel_params.size(); i++) { 123 | posvel_control_one(i, posvel_params[i]); 124 | } 125 | } 126 | 127 | std::vector DMDeviceCollection::get_motors() const { 128 | std::vector motors; 129 | for (auto dm_device : get_dm_devices()) { 130 | motors.push_back(dm_device->get_motor()); 131 | } 132 | return motors; 133 | } 134 | 135 | Motor DMDeviceCollection::get_motor(int i) const { return get_dm_devices().at(i)->get_motor(); } 136 | 137 | std::vector> DMDeviceCollection::get_dm_devices() const { 138 | std::vector> dm_devices; 139 | for (const auto& [id, device] : device_collection_->get_devices()) { 140 | auto dm_device = std::dynamic_pointer_cast(device); 141 | if (dm_device) { 142 | dm_devices.push_back(dm_device); 143 | } 144 | } 145 | return dm_devices; 146 | } 147 | 148 | } // namespace openarm::damiao_motor 149 | -------------------------------------------------------------------------------- /setup/openarm_can_diagnosis.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | static void print_usage(const char* program_name) { 33 | std::cerr << "Usage: " << program_name << " [-fd]" << std::endl; 34 | } 35 | 36 | // Return true if the netdev is configured for CAN-FD (MTU == CANFD_MTU), false if Classical (MTU == 37 | // CAN_MTU) 38 | static bool iface_is_canfd(const char* ifname) { 39 | int s = socket(PF_CAN, SOCK_RAW, CAN_RAW); 40 | if (s < 0) { 41 | perror("socket"); 42 | return false; // fall back 43 | } 44 | struct ifreq ifr{}; 45 | std::strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); 46 | ifr.ifr_name[IFNAMSIZ - 1] = '\0'; 47 | ifr.ifr_name[IFNAMSIZ] = '\0'; 48 | if (ioctl(s, SIOCGIFMTU, &ifr) < 0) { 49 | perror("ioctl(SIOCGIFMTU)"); 50 | close(s); 51 | return false; 52 | } 53 | close(s); 54 | if (ifr.ifr_mtu == CANFD_MTU) return true; 55 | if (ifr.ifr_mtu == CAN_MTU) return false; 56 | std::cerr << "Warning: unexpected MTU " << ifr.ifr_mtu << " on " << ifname << "\n"; 57 | return false; 58 | } 59 | 60 | static const char* br_label(int br_code) { 61 | // simple mapping example 62 | switch (br_code) { 63 | case 9: 64 | return "5 Mbps"; 65 | case 4: 66 | return "1 Mbps"; 67 | default: 68 | return "(unknown)"; 69 | } 70 | } 71 | 72 | int main(int argc, char* argv[]) { 73 | std::cout << "OpenArm CAN diagnostics\n"; 74 | 75 | for (int i = 1; i < argc; ++i) { 76 | if (std::string_view(argv[i]) == "-h") { 77 | print_usage(argv[0]); 78 | return 0; 79 | } 80 | } 81 | 82 | // Args: [-fd] 83 | if (argc < 2) { 84 | print_usage(argv[0]); 85 | return 1; 86 | } 87 | std::string can_if = argv[1]; 88 | bool use_fd = false; 89 | if (argc >= 3) { 90 | std::string arg2 = argv[2]; 91 | if (arg2 == "-fd") 92 | use_fd = true; 93 | else { 94 | std::cerr << "Error: Unknown argument '" << arg2 << "'. Use -fd to enable CAN-FD.\n"; 95 | return 1; 96 | } 97 | } 98 | 99 | // Detect actual iface mode via MTU 100 | bool iface_fd = iface_is_canfd(can_if.c_str()); 101 | 102 | std::cout << "CAN interface: " << can_if << std::endl; 103 | std::cout << "Requested mode: " << (use_fd ? "CAN FD" : "Classical CAN") << std::endl; 104 | std::cout << "Interface mode: " << (iface_fd ? "CAN FD" : "Classical CAN") << std::endl; 105 | 106 | if (use_fd != iface_fd) { 107 | std::cout << "WARNING: requested mode and interface mode differ. " 108 | "Check `ip link` configuration or run with/without -fd accordingly.\n"; 109 | } 110 | 111 | std::cout << "Initializing OpenArm CAN..." << std::endl; 112 | // Use the actual args 113 | openarm::can::socket::OpenArm openarm(can_if, use_fd); 114 | 115 | // Initialize arm motors 116 | std::vector motor_types = { 117 | openarm::damiao_motor::MotorType::DM8009, openarm::damiao_motor::MotorType::DM8009, 118 | openarm::damiao_motor::MotorType::DM4340, openarm::damiao_motor::MotorType::DM4340, 119 | openarm::damiao_motor::MotorType::DM4310, openarm::damiao_motor::MotorType::DM4310, 120 | openarm::damiao_motor::MotorType::DM4310}; 121 | std::vector send_can_ids = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; 122 | std::vector recv_can_ids = {0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17}; 123 | openarm.init_arm_motors(motor_types, send_can_ids, recv_can_ids); 124 | 125 | // Initialize gripper 126 | std::cout << "Initializing gripper..." << std::endl; 127 | openarm.init_gripper_motor(openarm::damiao_motor::MotorType::DM4310, 0x08, 0x18); 128 | 129 | openarm.set_callback_mode_all(openarm::damiao_motor::CallbackMode::PARAM); 130 | 131 | std::cout << "Reading motor parameters ..." << std::endl; 132 | openarm.query_param_all((int)openarm::damiao_motor::RID::MST_ID); 133 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 134 | openarm.recv_all(); 135 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 136 | 137 | openarm.query_param_all((int)openarm::damiao_motor::RID::can_br); 138 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 139 | openarm.recv_all(); 140 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 141 | 142 | // read params 143 | const auto& arm_motors = openarm.get_arm().get_motors(); 144 | const auto& gripper_motors = openarm.get_gripper().get_motors(); 145 | 146 | std::vector missing_ids; 147 | 148 | for (size_t i = 0; i < arm_motors.size(); ++i) { 149 | const auto& motor = arm_motors[i]; 150 | double mst = motor.get_param((int)openarm::damiao_motor::RID::MST_ID); 151 | double br = motor.get_param((int)openarm::damiao_motor::RID::can_br); 152 | 153 | if (mst < 0 || br < 0 || !std::isfinite(mst) || !std::isfinite(br)) { 154 | std::cout << "[arm#" << i << "] id=0x" << std::hex << recv_can_ids[i] << std::dec 155 | << " -> NG (no response)\n"; 156 | missing_ids.push_back(recv_can_ids[i]); 157 | } else { 158 | std::cout << "[arm#" << i << "] queried_mst_id: " << static_cast(mst) 159 | << " queried_br: " << static_cast(br) << " (" 160 | << br_label(static_cast(br)) << ")\n"; 161 | } 162 | } 163 | 164 | for (size_t i = 0; i < gripper_motors.size(); ++i) { 165 | const auto& gr = gripper_motors[i]; 166 | double mst = gr.get_param((int)openarm::damiao_motor::RID::MST_ID); 167 | double br = gr.get_param((int)openarm::damiao_motor::RID::can_br); 168 | 169 | if (mst < 0 || br < 0 || !std::isfinite(mst) || !std::isfinite(br)) { 170 | std::cout << "[gripper] id=0x18 -> NG (no response)\n"; 171 | missing_ids.push_back(0x18); 172 | } else { 173 | std::cout << "[gripper] queried_mst_id: " << static_cast(mst) 174 | << " queried_br: " << static_cast(br) << " (" 175 | << br_label(static_cast(br)) << ")\n"; 176 | } 177 | } 178 | 179 | if (!missing_ids.empty()) { 180 | std::cout << "NG: failed IDs:"; 181 | for (auto id : missing_ids) std::cout << " 0x" << std::hex << id; 182 | std::cout << std::dec << std::endl; 183 | 184 | std::cout << "Hints:\n"; 185 | std::cout << " • Motor internal CAN bitrate may be different from host setting\n"; 186 | std::cout 187 | << " • USB2CAN adapter mode/config may be wrong (FD vs Classical, bitrate profile)\n"; 188 | std::cout << " • Wiring/power/termination/ID conflict may exist\n"; 189 | return 2; 190 | } else { 191 | std::cout << "OK: all motors responded\n"; 192 | return 0; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /setup/openarm-can-change-baudrate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2025 Enactic, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | DM Motor ID and Baudrate Writer script for writing baudrate and ID parameters to DM motors 20 | Work with python-can library 21 | Original author: @necobit (Co-Authored-By: Claude) 22 | """ 23 | 24 | import argparse 25 | import sys 26 | import can 27 | import time 28 | from typing import Optional 29 | 30 | 31 | class DMMotorWriter: 32 | """DM Motor parameter writer""" 33 | 34 | # Supported baudrates and their codes 35 | BAUDRATE_MAP = { 36 | 125000: 0, # 125K 37 | 200000: 1, # 200K 38 | 250000: 2, # 250K 39 | 500000: 3, # 500K 40 | 1000000: 4, # 1M 41 | 2000000: 5, # 2M 42 | 2500000: 6, # 2.5M 43 | 3200000: 7, # 3.2M 44 | 4000000: 8, # 4M 45 | 5000000: 9 # 5M 46 | } 47 | 48 | def __init__(self, socketcan_port: str = "can0"): 49 | self.socketcan_port = socketcan_port 50 | self.can_bus: Optional[can.BusABC] = None 51 | 52 | def connect(self) -> bool: 53 | """Connect to CAN bus""" 54 | try: 55 | print(f"Connecting to CAN interface: {self.socketcan_port}") 56 | self.can_bus = can.interface.Bus( 57 | channel=self.socketcan_port, 58 | interface='socketcan' 59 | ) 60 | print(f"✓ Connected to {self.socketcan_port}") 61 | return True 62 | except Exception as e: 63 | print(f"✗ Connection failed: {e}") 64 | return False 65 | 66 | def disconnect(self): 67 | """Disconnect from CAN bus""" 68 | if self.can_bus: 69 | self.can_bus.shutdown() 70 | self.can_bus = None 71 | print("Disconnected from CAN bus") 72 | 73 | def validate_baudrate(self, baudrate: int) -> bool: 74 | """Validate if baudrate is supported""" 75 | return baudrate in self.BAUDRATE_MAP 76 | 77 | def write_baudrate(self, motor_id: int, baudrate: int) -> bool: 78 | """Write baudrate to motor""" 79 | if not self.can_bus: 80 | print("✗ Not connected to CAN bus") 81 | return False 82 | 83 | if not self.validate_baudrate(baudrate): 84 | print(f"✗ Unsupported baudrate: {baudrate}") 85 | print(f"Supported baudrates: {list(self.BAUDRATE_MAP.keys())}") 86 | return False 87 | 88 | try: 89 | baudrate_code = self.BAUDRATE_MAP[baudrate] 90 | 91 | print( 92 | f"Writing baudrate {baudrate} (code: {baudrate_code}) to motor ID {motor_id}") 93 | 94 | # Set Baudrate (can_br) - RID 35 (0x23) 95 | baudrate_data = [ 96 | motor_id & 0xFF, 97 | (motor_id >> 8) & 0xFF, 98 | 0x55, 99 | 0x23, 100 | baudrate_code, 101 | 0x00, 102 | 0x00, 103 | 0x00 104 | ] 105 | 106 | baudrate_msg = can.Message( 107 | arbitration_id=0x7FF, 108 | data=baudrate_data, 109 | is_extended_id=False 110 | ) 111 | 112 | self.can_bus.send(baudrate_msg) 113 | time.sleep(0.1) # Wait for processing 114 | 115 | print(f"✓ Baudrate {baudrate} written to motor ID {motor_id}") 116 | return True 117 | 118 | except Exception as e: 119 | print(f"✗ Failed to write baudrate: {e}") 120 | return False 121 | 122 | def save_to_flash(self, motor_id: int) -> bool: 123 | """Save parameters to flash memory""" 124 | if not self.can_bus: 125 | print("✗ Not connected to CAN bus") 126 | return False 127 | 128 | try: 129 | print(f"Saving parameters to flash for motor ID {motor_id}") 130 | print("⚠️ Motor has a hard limit of writing to flash of 10000 times") 131 | print("⚠️ Motor will be disabled during save operation") 132 | 133 | # Step 1: Disable motor first (required for save operation) 134 | disable_data = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD] 135 | disable_msg = can.Message( 136 | arbitration_id=motor_id, 137 | data=disable_data, 138 | is_extended_id=False 139 | ) 140 | 141 | self.can_bus.send(disable_msg) 142 | time.sleep(0.5) # Wait for disable to take effect 143 | 144 | # Step 2: Send save command 145 | save_data = [ 146 | motor_id & 0xFF, 147 | (motor_id >> 8) & 0xFF, 148 | 0xAA, 149 | 0x00, 150 | 0x00, 151 | 0x00, 152 | 0x00, 153 | 0x00 154 | ] 155 | 156 | save_msg = can.Message( 157 | arbitration_id=0x7FF, 158 | data=save_data, 159 | is_extended_id=False 160 | ) 161 | 162 | self.can_bus.send(save_msg) 163 | time.sleep(0.5) # Wait for save operation (up to 30ms) 164 | 165 | print(f"✓ Parameters saved to flash for motor ID {motor_id}") 166 | print("⚠️ Motor is now in DISABLE mode") 167 | return True 168 | 169 | except Exception as e: 170 | print(f"✗ Failed to save to flash: {e}") 171 | return False 172 | 173 | 174 | def main(): 175 | parser = argparse.ArgumentParser( 176 | description="DM Motor ID and Baudrate Writer", 177 | formatter_class=argparse.RawDescriptionHelpFormatter, 178 | epilog=""" 179 | Examples: 180 | # Write baudrate 1000000 to motor ID 2 on can0 181 | openarm-can-change-baudrate --baudrate 1000000 --canid 2 --socketcan can0 182 | 183 | # Write baudrate and save to flash 184 | openarm-can-change-baudrate --baudrate 500000 --canid 1 --socketcan can0 --flash 185 | 186 | # Write to motor on different CAN port 187 | openarm-can-change-baudrate --baudrate 2000000 --canid 3 --socketcan can1 188 | 189 | Supported baudrates: 190 | 125000, 200000, 250000, 500000, 1000000, 2000000, 2500000, 3200000, 4000000, 5000000 191 | """ 192 | ) 193 | 194 | parser.add_argument( 195 | '-b', '--baudrate', 196 | type=int, 197 | required=True, 198 | help='Baudrate to write (125000, 200000, 250000, 500000, 1000000, 2000000, 2500000, 3200000, 4000000, 5000000)' 199 | ) 200 | 201 | parser.add_argument( 202 | '-c', '--canid', 203 | type=int, 204 | required=True, 205 | help='Motor CAN ID (slave ID) to write to (0-255)' 206 | ) 207 | 208 | parser.add_argument( 209 | '-s', '--socketcan', 210 | type=str, 211 | default='can0', 212 | help='SocketCAN port (default: can0)' 213 | ) 214 | 215 | parser.add_argument( 216 | '-f', '--flash', 217 | action='store_true', 218 | help='Save parameters to flash memory after writing' 219 | ) 220 | 221 | args = parser.parse_args() 222 | 223 | # Validate inputs 224 | if args.canid < 0 or args.canid > 255: 225 | print("✗ Error: CAN ID must be between 0 and 255") 226 | sys.exit(1) 227 | 228 | # Create writer instance 229 | writer = DMMotorWriter(args.socketcan) 230 | 231 | try: 232 | # Connect to CAN bus 233 | if not writer.connect(): 234 | print("✗ Failed to connect to CAN bus") 235 | sys.exit(1) 236 | 237 | # Write baudrate 238 | if not writer.write_baudrate(args.canid, args.baudrate): 239 | print("✗ Failed to write baudrate") 240 | sys.exit(1) 241 | 242 | # Save to flash if requested 243 | if args.flash: 244 | if not writer.save_to_flash(args.canid): 245 | print("✗ Failed to save to flash") 246 | sys.exit(1) 247 | 248 | print("✓ Operation completed successfully") 249 | 250 | except KeyboardInterrupt: 251 | print("\n⚠️ Operation cancelled by user") 252 | sys.exit(1) 253 | except Exception as e: 254 | print(f"✗ Unexpected error: {e}") 255 | sys.exit(1) 256 | finally: 257 | writer.disconnect() 258 | 259 | 260 | if __name__ == "__main__": 261 | main() 262 | -------------------------------------------------------------------------------- /src/openarm/damiao_motor/dm_motor_control.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace openarm::damiao_motor { 23 | 24 | // Command creation methods (return data array, can_id handled externally) 25 | CANPacket CanPacketEncoder::create_enable_command(const Motor& motor) { 26 | return {motor.get_send_can_id(), pack_command_data(0xFC)}; 27 | } 28 | 29 | CANPacket CanPacketEncoder::create_disable_command(const Motor& motor) { 30 | return {motor.get_send_can_id(), pack_command_data(0xFD)}; 31 | } 32 | 33 | CANPacket CanPacketEncoder::create_set_zero_command(const Motor& motor) { 34 | return {motor.get_send_can_id(), pack_command_data(0xFE)}; 35 | } 36 | 37 | CANPacket CanPacketEncoder::create_mit_control_command(const Motor& motor, 38 | const MITParam& mit_param) { 39 | return {motor.get_send_can_id(), pack_mit_control_data(motor.get_motor_type(), mit_param)}; 40 | } 41 | 42 | CANPacket CanPacketEncoder::create_posvel_control_command(const Motor& motor, 43 | const PosVelParam& posvel_param) { 44 | // pos vel mode needs extra 0x100 45 | return {motor.get_send_can_id() + 0x100, 46 | pack_posvel_control_data(motor.get_motor_type(), posvel_param)}; 47 | } 48 | 49 | CANPacket CanPacketEncoder::create_query_param_command(const Motor& motor, int RID) { 50 | return {0x7FF, pack_query_param_data(motor.get_send_can_id(), RID)}; 51 | } 52 | 53 | CANPacket CanPacketEncoder::create_refresh_command(const Motor& motor) { 54 | uint8_t send_can_id = motor.get_send_can_id(); 55 | std::vector data = {static_cast(send_can_id & 0xFF), 56 | static_cast((send_can_id >> 8) & 0xFF), 57 | 0xCC, 58 | 0x00, 59 | 0x00, 60 | 0x00, 61 | 0x00, 62 | 0x00}; 63 | return {0x7FF, data}; 64 | } 65 | 66 | // Data interpretation methods (use recv_can_id for received data) 67 | StateResult CanPacketDecoder::parse_motor_state_data(const Motor& motor, 68 | const std::vector& data) { 69 | if (data.size() < 8) { 70 | std::cerr << "Warning: Skipping motor state data less than 8 bytes" << std::endl; 71 | return {0, 0, 0, 0, 0, false}; 72 | } 73 | 74 | // Parse state data 75 | uint16_t q_uint = (static_cast(data[1]) << 8) | data[2]; 76 | uint16_t dq_uint = 77 | (static_cast(data[3]) << 4) | (static_cast(data[4]) >> 4); 78 | uint16_t tau_uint = (static_cast(data[4] & 0xf) << 8) | data[5]; 79 | int t_mos = static_cast(data[6]); 80 | int t_rotor = static_cast(data[7]); 81 | 82 | // Convert to physical values 83 | LimitParam limits = MOTOR_LIMIT_PARAMS[static_cast(motor.get_motor_type())]; 84 | double recv_q = CanPacketDecoder::uint_to_double(q_uint, -limits.pMax, limits.pMax, 16); 85 | double recv_dq = CanPacketDecoder::uint_to_double(dq_uint, -limits.vMax, limits.vMax, 12); 86 | double recv_tau = CanPacketDecoder::uint_to_double(tau_uint, -limits.tMax, limits.tMax, 12); 87 | 88 | return {recv_q, recv_dq, recv_tau, t_mos, t_rotor, true}; 89 | } 90 | 91 | ParamResult CanPacketDecoder::parse_motor_param_data(const std::vector& data) { 92 | if (data.size() < 8) return {0, NAN, false}; 93 | 94 | if ((data[2] == 0x33 || data[2] == 0x55)) { 95 | uint8_t RID = data[3]; 96 | double num; 97 | if (CanPacketDecoder::is_in_ranges(RID)) { 98 | num = CanPacketDecoder::uint8s_to_uint32(data[4], data[5], data[6], data[7]); 99 | } else { 100 | std::array float_bytes = {data[4], data[5], data[6], data[7]}; 101 | num = CanPacketDecoder::uint8s_to_float(float_bytes); 102 | } 103 | return {RID, num, true}; 104 | } else { 105 | std::cerr << "WARNING: INVALID PARAM DATA" << std::endl; 106 | return {0, NAN, false}; 107 | } 108 | } 109 | 110 | // Data packing utility methods 111 | std::vector CanPacketEncoder::pack_mit_control_data(MotorType motor_type, 112 | const MITParam& mit_param) { 113 | uint16_t kp_uint = double_to_uint(mit_param.kp, 0, 500, 12); 114 | uint16_t kd_uint = double_to_uint(mit_param.kd, 0, 5, 12); 115 | 116 | // Get motor limits based on type 117 | LimitParam limits = MOTOR_LIMIT_PARAMS[static_cast(motor_type)]; 118 | uint16_t q_uint = double_to_uint(mit_param.q, -(double)limits.pMax, (double)limits.pMax, 16); 119 | uint16_t dq_uint = double_to_uint(mit_param.dq, -(double)limits.vMax, (double)limits.vMax, 12); 120 | uint16_t tau_uint = 121 | double_to_uint(mit_param.tau, -(double)limits.tMax, (double)limits.tMax, 12); 122 | 123 | return {static_cast((q_uint >> 8) & 0xFF), 124 | static_cast(q_uint & 0xFF), 125 | static_cast(dq_uint >> 4), 126 | static_cast(((dq_uint & 0xF) << 4) | ((kp_uint >> 8) & 0xF)), 127 | static_cast(kp_uint & 0xFF), 128 | static_cast(kd_uint >> 4), 129 | static_cast(((kd_uint & 0xF) << 4) | ((tau_uint >> 8) & 0xF)), 130 | static_cast(tau_uint & 0xFF)}; 131 | } 132 | 133 | std::vector CanPacketEncoder::pack_posvel_control_data(MotorType motor_type, 134 | const PosVelParam& posvel_param) { 135 | double pos = posvel_param.q; 136 | double vel = posvel_param.dq; 137 | 138 | auto pb = float_to_uint8s(static_cast(pos)); 139 | auto vb = float_to_uint8s(static_cast(vel)); 140 | 141 | // Pack into 8 bytes: [pos(4)][vel(4)] 142 | return {pb[0], pb[1], pb[2], pb[3], vb[0], vb[1], vb[2], vb[3]}; 143 | } 144 | 145 | std::vector CanPacketEncoder::pack_query_param_data(uint32_t send_can_id, int RID) { 146 | return {static_cast(send_can_id & 0xFF), 147 | static_cast((send_can_id >> 8) & 0xFF), 148 | 0x33, 149 | static_cast(RID), 150 | 0x00, 151 | 0x00, 152 | 0x00, 153 | 0x00}; 154 | } 155 | 156 | std::vector CanPacketEncoder::pack_command_data(uint8_t cmd) { 157 | return {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, cmd}; 158 | } 159 | 160 | // Utility function implementations 161 | double CanPacketEncoder::limit_min_max(double x, double min, double max) { 162 | return std::max(min, std::min(x, max)); 163 | } 164 | 165 | uint16_t CanPacketEncoder::double_to_uint(double x, double x_min, double x_max, int bits) { 166 | x = limit_min_max(x, x_min, x_max); 167 | double span = x_max - x_min; 168 | double data_norm = (x - x_min) / span; 169 | return static_cast(data_norm * ((1 << bits) - 1)); 170 | } 171 | 172 | std::array CanPacketEncoder::float_to_uint8s(float value) { 173 | std::array bytes{}; 174 | std::memcpy(bytes.data(), &value, sizeof(float)); 175 | return bytes; 176 | } 177 | 178 | double CanPacketDecoder::uint_to_double(uint16_t x, double min, double max, int bits) { 179 | double span = max - min; 180 | double data_norm = static_cast(x) / ((1 << bits) - 1); 181 | return data_norm * span + min; 182 | } 183 | 184 | float CanPacketDecoder::uint8s_to_float(const std::array& bytes) { 185 | float value; 186 | std::memcpy(&value, bytes.data(), sizeof(float)); 187 | return value; 188 | } 189 | 190 | uint32_t CanPacketDecoder::uint8s_to_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, 191 | uint8_t byte4) { 192 | uint32_t value; 193 | uint8_t bytes[4] = {byte1, byte2, byte3, byte4}; 194 | std::memcpy(&value, bytes, sizeof(uint32_t)); 195 | return value; 196 | } 197 | 198 | bool CanPacketDecoder::is_in_ranges(int number) { 199 | return (7 <= number && number <= 10) || (13 <= number && number <= 16) || 200 | (35 <= number && number <= 36); 201 | } 202 | } // namespace openarm::damiao_motor 203 | -------------------------------------------------------------------------------- /setup/motor_check.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Enactic, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace { 22 | void print_usage(const char* program_name) { 23 | std::cout << "Usage: " << program_name << " [can_interface] [-fd]" 24 | << std::endl; 25 | std::cout << " send_can_id: The send CAN ID of the motor (e.g., 1, 2, 7)" << std::endl; 26 | std::cout << " recv_can_id: The receive CAN ID of the motor (e.g., 17, 18, 23)" << std::endl; 27 | std::cout << " can_interface: CAN interface name (default: can0)" << std::endl; 28 | std::cout << " -fd: Enable CAN-FD (default: disabled)" << std::endl; 29 | std::cout << std::endl; 30 | std::cout << "Example: " << program_name << " 1 17" << std::endl; 31 | std::cout << "Example: " << program_name << " 1 17 can1" << std::endl; 32 | std::cout << "Example: " << program_name << " 1 17 can1 -fd" << std::endl; 33 | } 34 | 35 | void print_motor_status(const openarm::damiao_motor::Motor& motor) { 36 | std::cout << "Motor ID: " << motor.get_send_can_id() << std::endl; 37 | std::cout << " Position: " << motor.get_position() << " rad" << std::endl; 38 | std::cout << " Velocity: " << motor.get_velocity() << " rad/s" << std::endl; 39 | std::cout << " Torque: " << motor.get_torque() << " Nm" << std::endl; 40 | std::cout << " Temperature (MOS): " << motor.get_state_tmos() << " °C" << std::endl; 41 | std::cout << " Temperature (Rotor): " << motor.get_state_trotor() << " °C" << std::endl; 42 | } 43 | } // namespace 44 | 45 | int main(int argc, char* argv[]) { 46 | for (int i = 1; i < argc; ++i) { 47 | if (std::string_view(argv[i]) == "-h") { 48 | print_usage(argv[0]); 49 | return 0; 50 | } 51 | } 52 | 53 | if (argc < 3 || argc > 5) { 54 | print_usage(argv[0]); 55 | return 1; 56 | } 57 | 58 | // Parse send and receive CAN IDs from command line 59 | uint32_t send_can_id, recv_can_id; 60 | try { 61 | send_can_id = std::stoul(argv[1]); 62 | recv_can_id = std::stoul(argv[2]); 63 | } catch (const std::exception& e) { 64 | std::cerr << "Error: Invalid CAN ID format" << std::endl; 65 | print_usage(argv[0]); 66 | return 1; 67 | } 68 | 69 | // Parse optional CAN interface and FD flag 70 | std::string can_interface = "can0"; // default 71 | bool use_fd = false; // default: disabled 72 | 73 | if (argc >= 4) { 74 | std::string arg4 = argv[3]; 75 | if (arg4 == "-fd") { 76 | use_fd = true; 77 | } else { 78 | can_interface = arg4; 79 | } 80 | } 81 | 82 | if (argc >= 5) { 83 | std::string arg5 = argv[4]; 84 | if (arg5 == "-fd") { 85 | use_fd = true; 86 | } else { 87 | std::cerr << "Error: Unknown argument '" << arg5 << "'. Use -fd to enable CAN-FD" 88 | << std::endl; 89 | print_usage(argv[0]); 90 | return 1; 91 | } 92 | } 93 | 94 | try { 95 | std::cout << "=== OpenArm Motor Control Script ===" << std::endl; 96 | std::cout << "Send CAN ID: " << send_can_id << std::endl; 97 | std::cout << "Receive CAN ID: " << recv_can_id << std::endl; 98 | std::cout << "CAN Interface: " << can_interface << std::endl; 99 | std::cout << "CAN-FD Enabled: " << (use_fd ? "Yes" : "No") << std::endl; 100 | std::cout << std::endl; 101 | 102 | // Initialize OpenArm with CAN interface 103 | std::cout << "Initializing OpenArm CAN..." << std::endl; 104 | openarm::can::socket::OpenArm openarm(can_interface, 105 | use_fd); // Use specified interface and FD setting 106 | 107 | // Initialize single motor 108 | std::cout << "Initializing motor..." << std::endl; 109 | openarm.init_arm_motors({openarm::damiao_motor::MotorType::DM4310}, {send_can_id}, 110 | {recv_can_id}); 111 | 112 | // Set callback mode to param for initial parameter reading 113 | openarm.set_callback_mode_all(openarm::damiao_motor::CallbackMode::PARAM); 114 | 115 | // Query motor parameters (Master ID, Baudrate, and Control Mode) 116 | std::cout << "Reading motor parameters..." << std::endl; 117 | openarm.query_param_all(static_cast(openarm::damiao_motor::RID::MST_ID)); 118 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 119 | openarm.recv_all(); 120 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 121 | 122 | openarm.query_param_all(static_cast(openarm::damiao_motor::RID::can_br)); 123 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 124 | openarm.recv_all(); 125 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 126 | 127 | openarm.query_param_all(static_cast(openarm::damiao_motor::RID::CTRL_MODE)); 128 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 129 | openarm.recv_all(); 130 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 131 | 132 | // Get motor and verify parameters 133 | const auto& motors = openarm.get_arm().get_motors(); 134 | if (!motors.empty()) { 135 | const auto& motor = motors[0]; 136 | double queried_mst_id = 137 | motor.get_param(static_cast(openarm::damiao_motor::RID::MST_ID)); 138 | double queried_baudrate = 139 | motor.get_param(static_cast(openarm::damiao_motor::RID::can_br)); 140 | double queried_control_mode = 141 | motor.get_param(static_cast(openarm::damiao_motor::RID::CTRL_MODE)); 142 | 143 | std::cout << "\n=== Motor Parameters ===" << std::endl; 144 | std::cout << "Send CAN ID: " << motor.get_send_can_id() << std::endl; 145 | std::cout << "Queried Master ID: " << queried_mst_id << std::endl; 146 | std::cout << "Queried Baudrate (1-9): " << queried_baudrate << std::endl; 147 | std::cout << "Queried Control Mode (1: MIT, 2: POS_VEL, 3: VEL, 4: TORQUE_POS): " 148 | << queried_control_mode << std::endl; 149 | if (queried_control_mode != static_cast(openarm::damiao_motor::ControlMode::MIT)) { 150 | std::cout << "Warning: Queried Control Mode (" << queried_control_mode 151 | << ") is not MIT. Currently not supported." << std::endl; 152 | } 153 | 154 | // Verify recv_can_id matches queried master ID 155 | if (static_cast(queried_mst_id) != recv_can_id) { 156 | std::cerr << "Error: Queried Master ID (" << queried_mst_id 157 | << ") does not match provided recv_can_id (" << recv_can_id << ")" 158 | << std::endl; 159 | return 1; 160 | } 161 | std::cout << "✓ Master ID verification passed" << std::endl; 162 | } 163 | 164 | // Switch to state callback mode for motor status updates 165 | openarm.set_callback_mode_all(openarm::damiao_motor::CallbackMode::STATE); 166 | 167 | // Enable the motor 168 | std::cout << "\n=== Enabling Motor ===" << std::endl; 169 | openarm.enable_all(); 170 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 171 | openarm.recv_all(); 172 | 173 | // Refresh 10 times at 10Hz (100ms intervals) 174 | std::cout << "\n=== Refreshing Motor Status (10Hz for 1 second) ===" << std::endl; 175 | for (int i = 1; i <= 10; i++) { 176 | openarm.refresh_all(); 177 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 178 | openarm.recv_all(); 179 | 180 | for (const auto& motor : openarm.get_arm().get_motors()) { 181 | std::cout << "\n--- Refresh " << i << "/10 ---" << std::endl; 182 | print_motor_status(motor); 183 | } 184 | } 185 | 186 | // Disable the motor 187 | std::cout << "\n=== Disabling Motor ===" << std::endl; 188 | openarm.disable_all(); 189 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 190 | openarm.recv_all(); 191 | 192 | std::cout << "\n=== Script Completed Successfully ===" << std::endl; 193 | 194 | } catch (const std::exception& e) { 195 | std::cerr << "Error: " << e.what() << std::endl; 196 | return -1; 197 | } 198 | 199 | return 0; 200 | } 201 | -------------------------------------------------------------------------------- /.github/workflows/package.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Enactic, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Package 16 | on: 17 | push: 18 | branches: 19 | - '**' 20 | - '!dependabot/**' 21 | tags: 22 | - '**' 23 | pull_request: 24 | concurrency: 25 | group: ${{ github.head_ref || github.sha }}-${{ github.workflow }} 26 | cancel-in-progress: true 27 | jobs: 28 | source: 29 | name: Source 30 | runs-on: ubuntu-latest 31 | timeout-minutes: 10 32 | steps: 33 | - uses: actions/checkout@v6 34 | - name: Install dependencies 35 | run: | 36 | sudo apt update 37 | sudo apt install -y reprotest 38 | - name: Prepare environment variables 39 | run: | 40 | if [ "${GITHUB_REF_TYPE}" = "tag" ]; then 41 | echo "VERSION=${GITHUB_REF_NAME}" >> "${GITHUB_ENV}" 42 | else 43 | VERSION=$(grep '^project' CMakeLists.txt | grep -o '[0-9.]*') 44 | echo "VERSION=${VERSION}" >> "${GITHUB_ENV}" 45 | fi 46 | - name: Test source archive reproducible 47 | run: | 48 | # We can't download anything in reprotest because it changes 49 | # the current time. If current time is changed, TLS 50 | # verification will be failed. 51 | # 52 | # TODO: We should make downloaded archive reproducible. 53 | rake vendor:download 54 | sudo reprotest \ 55 | "rake dist VERSION=${VERSION}" \ 56 | openarm-can-${VERSION}.tar.gz 57 | - name: Create source archive 58 | run: | 59 | rake dist 60 | sha256sum \ 61 | openarm-can-${VERSION}.tar.gz > \ 62 | openarm-can-${VERSION}.tar.gz.sha256 63 | sha512sum \ 64 | openarm-can-${VERSION}.tar.gz > \ 65 | openarm-can-${VERSION}.tar.gz.sha512 66 | - uses: actions/setup-python@v6 67 | with: 68 | python-version: 3 69 | - name: Install dependencies for Python source archive 70 | run: | 71 | pip install build 72 | - name: Create Python source archive 73 | run: | 74 | cd python 75 | python -m build --sdist 76 | cd dist 77 | sha256sum \ 78 | openarm_can-${VERSION}.tar.gz > \ 79 | openarm_can-${VERSION}.tar.gz.sha256 80 | sha512sum \ 81 | openarm_can-${VERSION}.tar.gz > \ 82 | openarm_can-${VERSION}.tar.gz.sha512 83 | mv * ../../ 84 | - uses: actions/upload-artifact@v6 85 | with: 86 | name: source 87 | path: | 88 | openarm-can-${{ env.VERSION }}.tar.gz 89 | openarm-can-${{ env.VERSION }}.tar.gz.sha256 90 | openarm-can-${{ env.VERSION }}.tar.gz.sha512 91 | openarm_can-${{ env.VERSION }}.tar.gz 92 | openarm_can-${{ env.VERSION }}.tar.gz.sha256 93 | openarm_can-${{ env.VERSION }}.tar.gz.sha512 94 | build: 95 | name: Build 96 | needs: source 97 | strategy: 98 | fail-fast: false 99 | matrix: 100 | id: 101 | - ubuntu-jammy-amd64 102 | - ubuntu-jammy-arm64 103 | - ubuntu-noble-amd64 104 | - ubuntu-noble-arm64 105 | env: 106 | APACHE_ARROW_REPOSITORY: ${{ github.workspace }}/apache-arrow 107 | # condition && true-case || false-case 108 | # == 109 | # condition ? true-case : false-case 110 | runs-on: >- 111 | ${{ (contains(matrix.id, 'arm64') || 112 | contains(matrix.id, 'aarch64')) && 'ubuntu-24.04-arm' || 113 | 'ubuntu-latest' }} 114 | timeout-minutes: 10 115 | steps: 116 | - uses: actions/checkout@v6 117 | with: 118 | submodules: recursive 119 | - uses: actions/checkout@v6 120 | with: 121 | path: apache-arrow 122 | repository: apache/arrow 123 | - name: Prepare environment variables 124 | run: | 125 | id=${{ matrix.id }} 126 | # ubuntu-noble-amd64 -> ubuntu-noble 127 | os_version=${id%-*} 128 | # ubuntu-noble -> ubuntu 129 | os=${os_version%-*} 130 | # ubuntu-noble -> noble 131 | version=${os_version##*-} 132 | # ubuntu-noble-amd64 -> amd64 133 | architecture=${id##*-} 134 | 135 | if [ "${os}" = "debian" ] || [ "${os}" = "ubuntu" ]; then 136 | TASK_NAMESPACE=apt 137 | if [ "${architecture}" = "amd64" ]; then 138 | echo "APT_TARGETS=${os_version}" >> "${GITHUB_ENV}" 139 | TEST_DOCKER_IMAGE="${os}:${version}" 140 | else 141 | echo "APT_TARGETS=${id}" >> "${GITHUB_ENV}" 142 | TEST_DOCKER_IMAGE="arm64v8/${os}:${version}" 143 | fi 144 | else 145 | TASK_NAMESPACE=yum 146 | # amazon-linux -> amazonlinux 147 | docker_os=${os/-/} 148 | if [ "${architecture}" = "x86_64" ]; then 149 | echo "YUM_TARGETS=${os_version}" >> "${GITHUB_ENV}" 150 | TEST_DOCKER_IMAGE="${docker_os}:${version}" 151 | else 152 | echo "YUM_TARGETS=${id}" >> "${GITHUB_ENV}" 153 | TEST_DOCKER_IMAGE="arm64v8/${docker_os}:${version}" 154 | fi 155 | fi 156 | echo "ARCHITECTURE=${architecture}" >> ${GITHUB_ENV} 157 | echo "TASK_NAMESPACE=${TASK_NAMESPACE}" >> ${GITHUB_ENV} 158 | echo "TEST_DOCKER_IMAGE=${TEST_DOCKER_IMAGE}" >> ${GITHUB_ENV} 159 | - name: Install dependencies 160 | run: | 161 | sudo apt update -o="APT::Acquire::Retries=3" 162 | sudo apt install -y -V -o="APT::Acquire::Retries=3" \ 163 | devscripts \ 164 | ruby 165 | - uses: actions/download-artifact@v7 166 | with: 167 | name: source 168 | - name: Update version 169 | if: github.ref_type != 'tag' 170 | run: | 171 | cd packages 172 | rake version:update 173 | - name: Login to GitHub Container registry 174 | uses: docker/login-action@v3 175 | with: 176 | registry: ghcr.io 177 | username: ${{ github.actor }} 178 | password: ${{ secrets.GITHUB_TOKEN }} 179 | - name: Build with docker 180 | env: 181 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 182 | run: | 183 | cd packages 184 | rake docker:pull || : 185 | rake ${TASK_NAMESPACE}:build BUILD_DIR=build 186 | if [ "${TASK_NAMESPACE}" = "yum" ] && [ "${ARCHITECTURE}" != "x86_64" ]; then 187 | # Remove SRPMs from non x86_64 artifacts 188 | rm -rf ${TASK_NAMESPACE}/repositories/*/*/source 189 | fi 190 | - uses: actions/upload-artifact@v6 191 | with: 192 | name: packages-${{ matrix.id }} 193 | path: packages/${{ env.TASK_NAMESPACE }}/repositories/ 194 | - name: Push the built Docker image 195 | continue-on-error: true 196 | run: | 197 | cd packages 198 | rake docker:push 199 | - name: Test 200 | run: | 201 | docker run \ 202 | --rm \ 203 | --volume ${PWD}:/host:ro \ 204 | ${TEST_DOCKER_IMAGE} \ 205 | /host/packages/${TASK_NAMESPACE}/test.sh 206 | release: 207 | name: Release 208 | needs: 209 | - source 210 | - build 211 | runs-on: ubuntu-latest 212 | environment: release 213 | permissions: 214 | contents: write 215 | discussions: write 216 | steps: 217 | - uses: actions/download-artifact@v7 218 | - name: Prepare package artifacts 219 | run: | 220 | set -x 221 | for packages_path in packages-*; do 222 | # packages-ubuntu-jammy-amd64 -> 223 | # ubuntu-jammy-amd64 224 | os_arch=${packages_path#packages-} 225 | # ubuntu-jammy-amd64 -> 226 | # ubuntu-jammy 227 | os=${os_arch%-*} 228 | 229 | mkdir -p release/${os}/ 230 | # packages-ubuntu-jammy-amd64/ubuntu/pool/jammy/ -> 231 | # release/ubuntu-jammy/ubuntu/pool/jammy/ 232 | rsync -a ${packages_path}/ release/${os}/ 233 | done 234 | for release_os_path in release/*; do 235 | # release/ubuntu-jammy -> 236 | # ubuntu-jammy 237 | os=$(basename ${release_os_path}) 238 | 239 | # release/ubuntu-jammy/ubuntu/pool/jammy/ -> 240 | # ubuntu-jammy/ubuntu/pool/jammy/ 241 | tar czf ${os}.tar.gz -C $(dirname ${release_os_path}) ${os} 242 | done 243 | - name: Create GitHub Release 244 | if: github.ref_type == 'tag' 245 | env: 246 | GH_TOKEN: ${{ github.token }} 247 | run: | 248 | version=${GITHUB_REF_NAME} 249 | gh release create ${version} \ 250 | --discussion-category Announcements \ 251 | --generate-notes \ 252 | --repo ${GITHUB_REPOSITORY} \ 253 | --title "OpenArm CAN ${version}" \ 254 | --verify-tag \ 255 | *.tar.gz \ 256 | source/* 257 | pypi: 258 | name: PyPI 259 | if: github.ref_type == 'tag' 260 | needs: 261 | - source 262 | runs-on: ubuntu-latest 263 | timeout-minutes: 10 264 | environment: pypi 265 | permissions: 266 | id-token: write 267 | steps: 268 | - uses: actions/download-artifact@v7 269 | with: 270 | name: source 271 | - name: Prepare directory structure 272 | run: | 273 | mkdir -p dist 274 | mv openarm_can-*.tar.gz dist/ 275 | - uses: pypa/gh-action-pypi-publish@release/v1 276 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------