diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 42f5b137e..872881d13 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile { "name": "rmcs-develop", - "image": "qzhhhi/rmcs-develop:latest", + "image": "qzhhhi/rmcs-develop:latest-full", "privileged": true, "mounts": [ { @@ -41,4 +41,4 @@ ] } } -} \ No newline at end of file +} diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml index 435d3f461..78e039a00 100644 --- a/.github/workflows/update-image.yml +++ b/.github/workflows/update-image.yml @@ -3,49 +3,344 @@ name: Build and Push RMCS Images on: workflow_dispatch: push: - paths: - - 'Dockerfile' branches: - - main + - main + paths: + - Dockerfile + - .github/workflows/update-image.yml + - .script/build-rmcs-cross + - rmcs_ws/toolchain.cmake + +env: + REGISTRY_IMAGE_BASE: qzhhhi/rmcs-base + REGISTRY_IMAGE_DEVELOP: qzhhhi/rmcs-develop + REGISTRY_IMAGE_RUNTIME: qzhhhi/rmcs-runtime jobs: - build-and-push: - runs-on: ubuntu-latest + build_base: + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and push by digest (Base) + id: build_base + uses: docker/build-push-action@v7 + with: + context: . + target: rmcs-base + platforms: linux/${{ matrix.arch }} + outputs: type=image,name=${{ env.REGISTRY_IMAGE_BASE }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=docker-build-base-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=docker-build-base-${{ matrix.arch }} + + - name: Export digest (Base) + run: | + mkdir -p "${{ runner.temp }}/digests-base" + printf '%s\n' "${{ steps.build_base.outputs.digest }}" > "${{ runner.temp }}/digests-base/${{ matrix.arch }}.digest" + + - name: Upload digest (Base) + uses: actions/upload-artifact@v6 + with: + name: digests-base-${{ matrix.arch }} + path: ${{ runner.temp }}/digests-base/*.digest + if-no-files-found: error + retention-days: 1 + + build_images: + needs: build_base + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up SSH keys - run: | - mkdir -p .ssh - chmod 700 .ssh - echo "${{ secrets.CONTAINER_ID_RSA }}" > .ssh/id_rsa - echo "${{ secrets.CONTAINER_ID_RSA_PUB }}" > .ssh/id_rsa.pub - chmod 600 .ssh/id_rsa - chmod 644 .ssh/id_rsa.pub - - - name: Build and push rmcs-develop:latest - uses: docker/build-push-action@v6 - with: - context: . - push: true - target: rmcs-develop - tags: qzhhhi/rmcs-develop:latest - - - name: Build and push rmcs-runtime:latest - uses: docker/build-push-action@v6 - with: - context: . - push: true - target: rmcs-runtime - tags: qzhhhi/rmcs-runtime:latest + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Set up SSH keys + run: | + mkdir -p .ssh + chmod 700 .ssh + echo "${{ secrets.CONTAINER_ID_RSA }}" > .ssh/id_rsa + echo "${{ secrets.CONTAINER_ID_RSA_PUB }}" > .ssh/id_rsa.pub + chmod 600 .ssh/id_rsa + chmod 644 .ssh/id_rsa.pub + + - name: Download digests (Base) + uses: actions/download-artifact@v7 + with: + path: ${{ runner.temp }}/digests/base + pattern: digests-base-* + merge-multiple: true + + - name: Resolve base digests + id: resolve + run: | + set -euo pipefail + base_dir="${{ runner.temp }}/digests/base" + base_digest_amd64="$(cat "${base_dir}/amd64.digest")" + base_digest_arm64="$(cat "${base_dir}/arm64.digest")" + echo "base_digest_amd64=${base_digest_amd64}" >> "$GITHUB_OUTPUT" + echo "base_digest_arm64=${base_digest_arm64}" >> "$GITHUB_OUTPUT" + + - name: Build and push by digest (Develop) + id: build_develop + uses: docker/build-push-action@v7 + with: + context: . + target: rmcs-develop + platforms: linux/${{ matrix.arch }} + outputs: type=image,name=${{ env.REGISTRY_IMAGE_DEVELOP }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=docker-build-develop-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=docker-build-develop-${{ matrix.arch }} + + - name: Export digest (Develop) + run: | + mkdir -p "${{ runner.temp }}/digests-develop-standard" + printf '%s\n' "${{ steps.build_develop.outputs.digest }}" > "${{ runner.temp }}/digests-develop-standard/${{ matrix.arch }}.digest" + + - name: Upload digest (Develop) + uses: actions/upload-artifact@v6 + with: + name: digests-develop-standard-${{ matrix.arch }} + path: ${{ runner.temp }}/digests-develop-standard/*.digest + if-no-files-found: error + retention-days: 1 + + - name: Build and push by digest (Develop Full) + id: build_develop_full + uses: docker/build-push-action@v7 + with: + context: . + target: rmcs-develop-full + platforms: linux/${{ matrix.arch }} + build-args: | + SYSROOT_IMAGE_AMD64=${{ env.REGISTRY_IMAGE_BASE }}@${{ steps.resolve.outputs.base_digest_amd64 }} + SYSROOT_IMAGE_ARM64=${{ env.REGISTRY_IMAGE_BASE }}@${{ steps.resolve.outputs.base_digest_arm64 }} + outputs: type=image,name=${{ env.REGISTRY_IMAGE_DEVELOP }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=docker-build-develop-full-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=docker-build-develop-full-${{ matrix.arch }} + + - name: Export digest (Develop Full) + run: | + mkdir -p "${{ runner.temp }}/digests-develop-full" + printf '%s\n' "${{ steps.build_develop_full.outputs.digest }}" > "${{ runner.temp }}/digests-develop-full/${{ matrix.arch }}.digest" + + - name: Upload digest (Develop Full) + uses: actions/upload-artifact@v6 + with: + name: digests-develop-full-${{ matrix.arch }} + path: ${{ runner.temp }}/digests-develop-full/*.digest + if-no-files-found: error + retention-days: 1 + + - name: Build and push by digest (Runtime) + id: build_runtime + uses: docker/build-push-action@v7 + with: + context: . + target: rmcs-runtime + platforms: linux/${{ matrix.arch }} + outputs: type=image,name=${{ env.REGISTRY_IMAGE_RUNTIME }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=docker-build-runtime-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=docker-build-runtime-${{ matrix.arch }} + + - name: Export digest (Runtime) + run: | + mkdir -p "${{ runner.temp }}/digests-runtime" + printf '%s\n' "${{ steps.build_runtime.outputs.digest }}" > "${{ runner.temp }}/digests-runtime/${{ matrix.arch }}.digest" + + - name: Upload digest (Runtime) + uses: actions/upload-artifact@v6 + with: + name: digests-runtime-${{ matrix.arch }} + path: ${{ runner.temp }}/digests-runtime/*.digest + if-no-files-found: error + retention-days: 1 + + verify_images: + needs: build_images + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + cross_target: arm64 + cross_triplet: aarch64-linux-gnu + expected_machine: AArch64 + - arch: arm64 + runner: ubuntu-24.04-arm + cross_target: amd64 + cross_triplet: x86_64-linux-gnu + expected_machine: X86-64 + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Download digests (Develop Full) + uses: actions/download-artifact@v7 + with: + path: ${{ runner.temp }}/digests/develop-full + pattern: digests-develop-full-* + merge-multiple: true + + - name: Resolve develop-full digest + id: resolve + run: | + set -euo pipefail + digest_file="${{ runner.temp }}/digests/develop-full/${{ matrix.arch }}.digest" + image_digest="$(cat "${digest_file}")" + echo "image_digest=${image_digest}" >> "$GITHUB_OUTPUT" + + - name: Smoke test (cross build + readelf) + env: + IMAGE_DIGEST: ${{ steps.resolve.outputs.image_digest }} + CROSS_TARGET: ${{ matrix.cross_target }} + CROSS_TRIPLET: ${{ matrix.cross_triplet }} + EXPECTED_MACHINE: ${{ matrix.expected_machine }} + run: | + set -euo pipefail + uid="$(id -u)" + gid="$(id -g)" + docker run --rm \ + --platform "linux/${{ matrix.arch }}" \ + --user "${uid}:${gid}" \ + -e HOME=/tmp/rmcs-home \ + -e RMCS_PATH=/workspaces/RMCS \ + -e CROSS_TARGET="${CROSS_TARGET}" \ + -e CROSS_TRIPLET="${CROSS_TRIPLET}" \ + -e EXPECTED_MACHINE="${EXPECTED_MACHINE}" \ + -v "$PWD:/workspaces/RMCS" \ + -w /workspaces/RMCS \ + "${{ env.REGISTRY_IMAGE_DEVELOP }}@${IMAGE_DIGEST}" \ + bash -lc ' + set -euo pipefail + mkdir -p "${HOME}" + test -d "/opt/sysroots/${CROSS_TARGET}" + command -v "${CROSS_TRIPLET}-gcc" + command -v "${CROSS_TRIPLET}-g++" + ./.script/build-rmcs-cross --target-arch "${CROSS_TARGET}" --packages-up-to rmcs_executor + lib_path="/workspaces/RMCS/rmcs_ws/install-cross-${CROSS_TARGET}/lib/librmcs_executor.so" + if [[ ! -f "${lib_path}" ]]; then + echo "Missing library: ${lib_path}" >&2 + exit 1 + fi + readelf -h "${lib_path}" | grep -q "Machine:.*${EXPECTED_MACHINE}" + ' + + promote: + runs-on: ubuntu-latest + needs: + - build_base + - build_images + - verify_images + steps: + - name: Download digests (Base) + uses: actions/download-artifact@v7 + with: + path: ${{ runner.temp }}/digests/base + pattern: digests-base-* + merge-multiple: true + + - name: Download digests (Develop) + uses: actions/download-artifact@v7 + with: + path: ${{ runner.temp }}/digests/develop-standard + pattern: digests-develop-standard-* + merge-multiple: true + + - name: Download digests (Develop Full) + uses: actions/download-artifact@v7 + with: + path: ${{ runner.temp }}/digests/develop-full + pattern: digests-develop-full-* + merge-multiple: true + + - name: Download digests (Runtime) + uses: actions/download-artifact@v7 + with: + path: ${{ runner.temp }}/digests/runtime + pattern: digests-runtime-* + merge-multiple: true + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Create manifest lists and push + run: | + set -euo pipefail + + create_manifest() { + local image="$1" + local tag="$2" + local digest_dir="$3" + local refs=() + + while IFS= read -r digest_file; do + refs+=("${image}@$(cat "${digest_file}")") + done < <(find "${digest_dir}" -maxdepth 1 -type f -name '*.digest' | sort) + + if [[ ${#refs[@]} -eq 0 ]]; then + echo "No digest files found in ${digest_dir}" >&2 + exit 1 + fi + + docker buildx imagetools create -t "${image}:${tag}" "${refs[@]}" + } + + create_manifest "${{ env.REGISTRY_IMAGE_BASE }}" "latest" "${{ runner.temp }}/digests/base" + create_manifest "${{ env.REGISTRY_IMAGE_DEVELOP }}" "latest" "${{ runner.temp }}/digests/develop-standard" + create_manifest "${{ env.REGISTRY_IMAGE_DEVELOP }}" "latest-full" "${{ runner.temp }}/digests/develop-full" + create_manifest "${{ env.REGISTRY_IMAGE_RUNTIME }}" "latest" "${{ runner.temp }}/digests/runtime" + + - name: Inspect images + run: | + docker buildx imagetools inspect "${{ env.REGISTRY_IMAGE_BASE }}:latest" + docker buildx imagetools inspect "${{ env.REGISTRY_IMAGE_DEVELOP }}:latest" + docker buildx imagetools inspect "${{ env.REGISTRY_IMAGE_DEVELOP }}:latest-full" + docker buildx imagetools inspect "${{ env.REGISTRY_IMAGE_RUNTIME }}:latest" diff --git a/.gitignore b/.gitignore index 80c67b8a9..05b12839b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ devel/ log/ build/ install/ +log-cross-*/ +build-cross-*/ +install-cross-*/ bin/ lib/ msg_gen/ @@ -69,4 +72,4 @@ id_rsa.pub record/ # For temperary development -develop_ws \ No newline at end of file +develop_ws diff --git a/.gitmodules b/.gitmodules index d62982951..61e5f08fb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ [submodule "rmcs_ws/src/fast_tf"] path = rmcs_ws/src/fast_tf url = https://github.com/qzhhhi/FastTF.git -[submodule "rmcs_ws/src/hikcamera"] - path = rmcs_ws/src/hikcamera - url = https://github.com/Alliance-Algorithm/ros2-hikcamera.git -[submodule "rmcs_ws/src/serial"] - path = rmcs_ws/src/serial - url = https://github.com/Alliance-Algorithm/ros2-serial.git diff --git a/.script/build-rmcs-cross b/.script/build-rmcs-cross new file mode 100755 index 000000000..21c987eca --- /dev/null +++ b/.script/build-rmcs-cross @@ -0,0 +1,165 @@ +#!/bin/bash + +set -euo pipefail + +: "${RMCS_PATH:=/workspaces/RMCS}" + +usage() { + cat <<'EOF' +Usage: + build-rmcs-cross --target-arch [colcon build args...] + +Examples: + build-rmcs-cross --target-arch arm64 + build-rmcs-cross --target-arch amd64 --packages-up-to rmcs_executor +EOF +} + +target_arch="" +colcon_args=() + +while (($# > 0)); do + case "$1" in + --target-arch) + if (($# < 2)); then + echo "> ERROR: --target-arch requires a value." + usage + exit 1 + fi + target_arch="$2" + shift 2 + ;; + --target-arch=*) + target_arch="${1#*=}" + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + colcon_args+=("$1") + shift + ;; + esac +done + +if [[ -z "${target_arch}" ]]; then + echo "> ERROR: --target-arch is required." + usage + exit 1 +fi + +case "${target_arch}" in +arm64) + target_triplet="aarch64-linux-gnu" + ;; +amd64) + target_triplet="x86_64-linux-gnu" + ;; +*) + echo "> ERROR: Unsupported target arch '${target_arch}'. Use arm64 or amd64." + exit 1 + ;; +esac + +workspace="${RMCS_PATH}/rmcs_ws" +toolchain_file="${workspace}/toolchain.cmake" +sysroot="${RMCS_SYSROOT:-/opt/sysroots/${target_arch}}" + +if [[ ! -d "${workspace}" ]]; then + echo "> ERROR: Workspace not found: ${workspace}" + exit 1 +fi + +if [[ ! -f "${toolchain_file}" ]]; then + echo "> ERROR: Toolchain file not found: ${toolchain_file}" + exit 1 +fi + +if [[ ! -d "${sysroot}" ]]; then + echo "> ERROR: Sysroot not found: ${sysroot}" + exit 1 +fi + +if ! command -v "${target_triplet}-gcc" &>/dev/null; then + echo "> ERROR: Missing cross compiler: ${target_triplet}-gcc" + exit 1 +fi + +if ! command -v "${target_triplet}-g++" &>/dev/null; then + echo "> ERROR: Missing cross compiler: ${target_triplet}-g++" + exit 1 +fi + +unset AMENT_PREFIX_PATH +unset CMAKE_PREFIX_PATH +unset COLCON_PREFIX_PATH +unset AMENT_CURRENT_PREFIX +unset PKG_CONFIG_PATH +unset PKG_CONFIG_LIBDIR +unset PKG_CONFIG_SYSROOT_DIR +unset ROS_PACKAGE_PATH +unset PYTHONPATH +unset PYTHONHOME +unset LD_LIBRARY_PATH +unset LIBRARY_PATH +unset CPATH +unset C_INCLUDE_PATH +unset CPLUS_INCLUDE_PATH +unset CFLAGS +unset CXXFLAGS +unset CPPFLAGS +unset LDFLAGS +unset CMAKE_INCLUDE_PATH +unset CMAKE_LIBRARY_PATH +unset CMAKE_PROGRAM_PATH +unset CMAKE_TOOLCHAIN_FILE +unset CMAKE_GENERATOR + +restore_nounset=0 +case "$-" in +*u*) + restore_nounset=1 + set +u + ;; +esac +source /opt/ros/jazzy/setup.bash +if ((restore_nounset)); then + set -u +fi + +export RMCS_TARGET_ARCH="${target_arch}" +export RMCS_TARGET_TRIPLET="${target_triplet}" +export RMCS_SYSROOT="${sysroot}" + +export CC="$(command -v "${target_triplet}-gcc")" +export CXX="$(command -v "${target_triplet}-g++")" + +export CMAKE_PREFIX_PATH="${RMCS_SYSROOT}/opt/ros/jazzy:${RMCS_SYSROOT}/usr/local:${RMCS_SYSROOT}/usr" +export AMENT_PREFIX_PATH="${RMCS_SYSROOT}/opt/ros/jazzy" +export COLCON_PREFIX_PATH="${RMCS_SYSROOT}/opt/ros/jazzy" +export PKG_CONFIG_PATH="" + +export PKG_CONFIG_SYSROOT_DIR="${RMCS_SYSROOT}" +export PKG_CONFIG_LIBDIR="${RMCS_SYSROOT}/usr/local/lib/${RMCS_TARGET_TRIPLET}/pkgconfig:${RMCS_SYSROOT}/usr/local/lib/pkgconfig:${RMCS_SYSROOT}/usr/local/share/pkgconfig:${RMCS_SYSROOT}/usr/lib/${RMCS_TARGET_TRIPLET}/pkgconfig:${RMCS_SYSROOT}/usr/lib/pkgconfig:${RMCS_SYSROOT}/usr/share/pkgconfig:${RMCS_SYSROOT}/lib/${RMCS_TARGET_TRIPLET}/pkgconfig:${RMCS_SYSROOT}/lib/pkgconfig:${RMCS_SYSROOT}/opt/ros/jazzy/lib/${RMCS_TARGET_TRIPLET}/pkgconfig:${RMCS_SYSROOT}/opt/ros/jazzy/lib/pkgconfig:${RMCS_SYSROOT}/opt/ros/jazzy/share/pkgconfig" + +cd "${workspace}" + +build_base="build-cross-${RMCS_TARGET_ARCH}" +install_base="install-cross-${RMCS_TARGET_ARCH}" +log_base="log-cross-${RMCS_TARGET_ARCH}" + +CLICOLOR_FORCE=1 NINJA_STATUS="" \ + colcon \ + --log-base "${log_base}" \ + build \ + --merge-install \ + --build-base "${build_base}" \ + --install-base "${install_base}" \ + "${colcon_args[@]}" \ + --cmake-args \ + -DCMAKE_TOOLCHAIN_FILE="${toolchain_file}" \ + -DRMCS_TARGET_ARCH="${RMCS_TARGET_ARCH}" \ + -DRMCS_SYSROOT="${RMCS_SYSROOT}" \ + -DRMCS_TARGET_TRIPLET="${RMCS_TARGET_TRIPLET}" diff --git a/.script/complete/_build-rmcs-cross b/.script/complete/_build-rmcs-cross new file mode 100644 index 000000000..180046369 --- /dev/null +++ b/.script/complete/_build-rmcs-cross @@ -0,0 +1,5 @@ +#compdef build-rmcs-cross + +_arguments \ + '--target-arch=[Cross compile target architecture]:target:(arm64 amd64)' \ + '*:colcon build args:' diff --git a/.script/host/rmcs b/.script/host/rmcs index 0b65f9322..f94a7cf75 100755 --- a/.script/host/rmcs +++ b/.script/host/rmcs @@ -3,7 +3,7 @@ set -euo pipefail readonly DEVELOPER_NAME="ubuntu" -readonly NVIM_PATH="/opt/nvim-linux-x86_64/bin/nvim" +readonly NVIM_PATH="/opt/nvim/bin/nvim" readonly NVIM_PORT=6666 readonly NVIM_HOST="localhost" diff --git a/Dockerfile b/Dockerfile index 5fce6b00f..d5fd544d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,16 @@ +# Sysroot source images for rmcs-develop-full. +# CI overrides these with digest-pinned references. For local builds, build +# rmcs-base for both architectures first and pass --build-arg explicitly; +# see docs/zh-cn/build_docker_image.md. +ARG SYSROOT_IMAGE_AMD64=rmcs-base:latest +ARG SYSROOT_IMAGE_ARM64=rmcs-base:latest + # Base container, provides a runtime environment FROM ros:jazzy AS rmcs-base +ARG TARGETARCH # Change bash as default shell instead of sh SHELL ["/bin/bash", "-c"] @@ -37,14 +45,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # Install openvino runtime -RUN wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB && \ - apt-key add ./GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB && \ - rm ./GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB && \ - echo "deb https://apt.repos.intel.com/openvino ubuntu24 main" > /etc/apt/sources.list.d/intel-openvino.list && \ - apt-get update && \ - apt-get install -y --no-install-recommends openvino-2025.2.0 && \ - apt-get autoremove -y && apt-get clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* +RUN if [ "${TARGETARCH}" = "amd64" ]; then \ + mkdir -p /etc/apt/keyrings && \ + wget -O /etc/apt/keyrings/intel-openvino.asc \ + https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB && \ + chmod 644 /etc/apt/keyrings/intel-openvino.asc && \ + echo "deb [signed-by=/etc/apt/keyrings/intel-openvino.asc] https://apt.repos.intel.com/openvino ubuntu24 main" \ + > /etc/apt/sources.list.d/intel-openvino.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends openvino-2025.2.0 && \ + apt-get autoremove -y && apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/*; \ + else \ + echo "Skipping OpenVINO install on ${TARGETARCH}"; \ + fi # Install Livox SDK RUN git clone https://github.com/Livox-SDK/Livox-SDK2.git && \ @@ -63,17 +77,28 @@ RUN --mount=type=bind,target=/rmcs_ws/src,source=rmcs_ws/src,readonly \ apt-get autoremove -y && apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* -# Install unison to allow file synchronization -RUN cd /tmp && \ - wget -O unison.tar.gz https://github.com/bcpierce00/unison/releases/download/v2.53.7/unison-2.53.7-ubuntu-x86_64.tar.gz && \ - mkdir -p unison && tar -zxf unison.tar.gz -C unison && \ - cp unison/bin/* /usr/local/bin && \ - rm -rf unison unison.tar.gz - - +# Build Unison from source because upstream does not ship arm64 release binaries +# and the distro package does not include unison-fsmonitor. +# SHA256 verified against Ubuntu Launchpad source package unison-2.53.8-1. +RUN export UNISON_VERSION=2.53.8 && \ + apt-get update && \ + apt-get install -y --no-install-recommends ocaml-nox && \ + curl -L "https://github.com/bcpierce00/unison/archive/refs/tags/v${UNISON_VERSION}.tar.gz" \ + -o "/tmp/unison-v${UNISON_VERSION}.tar.gz" && \ + echo "d0d30ea63e09fc8edf10bd8cbab238fffc8ed510d27741d06b5caa816abd58b6 /tmp/unison-v${UNISON_VERSION}.tar.gz" | sha256sum -c - && \ + tar -xzf "/tmp/unison-v${UNISON_VERSION}.tar.gz" -C /tmp && \ + cd "/tmp/unison-${UNISON_VERSION}" && \ + make -j"$(nproc)" && \ + make install PREFIX=/usr/local && \ + cd / && \ + rm -rf "/tmp/unison-${UNISON_VERSION}" "/tmp/unison-v${UNISON_VERSION}.tar.gz" && \ + apt-get purge -y --auto-remove ocaml-nox && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* # Developing container, works with devcontainer FROM rmcs-base AS rmcs-develop +ARG TARGETARCH # Install develop tools RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -121,14 +146,25 @@ RUN --mount=type=bind,target=/tmp/.ssh,source=.ssh,readonly=false \ chown -R 1000:1000 .unison # Install latest neovim -RUN curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.tar.gz && \ +RUN case "${TARGETARCH}" in \ + amd64) nvim_arch=x86_64 ;; \ + arm64) nvim_arch=arm64 ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac && \ + curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-${nvim_arch}.tar.gz && \ rm -rf /opt/nvim && \ - tar -C /opt -xzf nvim-linux-x86_64.tar.gz && \ - rm nvim-linux-x86_64.tar.gz -ENV PATH="${PATH}:/opt/nvim-linux-x86_64/bin" + tar -C /opt -xzf nvim-linux-${nvim_arch}.tar.gz && \ + mv /opt/nvim-linux-${nvim_arch} /opt/nvim && \ + rm nvim-linux-${nvim_arch}.tar.gz +ENV PATH="${PATH}:/opt/nvim/bin" # Install latest stable cmake for user ubuntu -RUN wget https://github.com/kitware/cmake/releases/download/v4.2.3/cmake-4.2.3-linux-x86_64.sh -O install.sh && \ +RUN case "${TARGETARCH}" in \ + amd64) cmake_arch=x86_64 ;; \ + arm64) cmake_arch=aarch64 ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac && \ + wget https://github.com/kitware/cmake/releases/download/v4.2.3/cmake-4.2.3-linux-${cmake_arch}.sh -O install.sh && \ mkdir -p /opt/cmake/ && bash install.sh --skip-license --prefix=/opt/cmake/ --exclude-subdir && \ rm install.sh @@ -151,6 +187,62 @@ RUN sh -c "$(wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools COPY --chown=1000:1000 .script/template/env_setup.bash env_setup.bash COPY --chown=1000:1000 .script/template/env_setup.zsh env_setup.zsh +FROM --platform=linux/amd64 ${SYSROOT_IMAGE_AMD64} AS rmcs-sysroot-amd64 +FROM --platform=linux/arm64 ${SYSROOT_IMAGE_ARM64} AS rmcs-sysroot-arm64 + +# Developing container with cross toolchains and opposite-arch sysroot. +FROM rmcs-develop AS rmcs-develop-full +ARG TARGETARCH + +USER root + +RUN apt-get update && \ + case "${TARGETARCH}" in \ + amd64) cross_triplet=aarch64-linux-gnu; cross_pkg_triplet=aarch64-linux-gnu ;; \ + arm64) cross_triplet=x86_64-linux-gnu; cross_pkg_triplet=x86-64-linux-gnu ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac && \ + apt-get install -y --no-install-recommends \ + "gcc-14-${cross_pkg_triplet}" "g++-14-${cross_pkg_triplet}" \ + "binutils-${cross_pkg_triplet}" && \ + update-alternatives --install "/usr/bin/${cross_triplet}-gcc" "${cross_triplet}-gcc" "/usr/bin/${cross_triplet}-gcc-14" 50 && \ + update-alternatives --install "/usr/bin/${cross_triplet}-g++" "${cross_triplet}-g++" "/usr/bin/${cross_triplet}-g++-14" 50 && \ + apt-get autoremove -y && apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* + +RUN mkdir -p /opt/sysroots && \ + case "${TARGETARCH}" in \ + amd64) mkdir -p /opt/sysroots/arm64 ;; \ + arm64) mkdir -p /opt/sysroots/amd64 ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac + +RUN --mount=from=rmcs-sysroot-amd64,target=/mnt/sysroot-amd64,readonly \ + --mount=from=rmcs-sysroot-arm64,target=/mnt/sysroot-arm64,readonly \ + set -euo pipefail && \ + case "${TARGETARCH}" in \ + amd64) tar \ + --exclude='./dev/*' \ + --exclude='./proc/*' \ + --exclude='./sys/*' \ + --exclude='./run/*' \ + --exclude='./tmp/*' \ + -C /mnt/sysroot-arm64 -cf - . | tar -C /opt/sysroots/arm64 -xf - ;; \ + arm64) tar \ + --exclude='./dev/*' \ + --exclude='./proc/*' \ + --exclude='./sys/*' \ + --exclude='./run/*' \ + --exclude='./tmp/*' \ + -C /mnt/sysroot-amd64 -cf - . | tar -C /opt/sysroots/amd64 -xf - ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac + +WORKDIR /home/ubuntu +ENV USER=ubuntu +ENV WORKDIR=/home/ubuntu +USER ubuntu + # Runtime container, will automatically launch the main program FROM rmcs-base AS rmcs-runtime diff --git a/README.md b/README.md index dc365f69b..75dd7ff67 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ RoboMaster Control System based on ROS2. docker pull qzhhhi/rmcs-develop:latest ``` +如需交叉编译环境,可下载: +```bash +docker pull qzhhhi/rmcs-develop:latest-full +``` + 也可自行使用 `Dockerfile` 构建,参见 [镜像构建指南](docs/zh-cn/build_docker_image.md)。 ### Step 2:克隆并打开仓库 @@ -67,6 +72,19 @@ build-rmcs Note: 用于开发的所有脚本均位于 `.script` 中,参见 开发脚本手册(TODO)。 +如需在 `latest-full` 中执行交叉编译,请根据当前容器架构选择对向目标: +```bash +build-rmcs-cross --target-arch arm64 +``` +适用于 `linux/amd64` 的 `latest-full` 变体。 + +```bash +build-rmcs-cross --target-arch amd64 +``` +适用于 `linux/arm64` 的 `latest-full` 变体。 + +详见 [交叉编译使用说明](docs/zh-cn/cross_build.md)。 + ### Step 5 (Optional):运行 编写代码并编译完成后,可以使用: diff --git a/docs/zh-cn/build_docker_image.md b/docs/zh-cn/build_docker_image.md index 88c5389aa..768f977e2 100644 --- a/docs/zh-cn/build_docker_image.md +++ b/docs/zh-cn/build_docker_image.md @@ -3,31 +3,60 @@ 于 Repo 根目录下,在终端中分别使用: ```bash -docker build . --target rmcs-runtime -t rmcs-runtime:latest +docker build . --target rmcs-base -t rmcs-base:latest ``` ```bash docker build . --target rmcs-develop -t rmcs-develop:latest ``` -构建开发容器和部署容器。 +```bash +docker build . --target rmcs-runtime -t rmcs-runtime:latest +``` + +构建基础容器、开发容器和部署容器。 + +## 构建 `rmcs-develop:latest-full` + +`latest-full` 需要对向架构的 `rmcs-base` 作为 sysroot 来源。 + +下面示例以在 `amd64` 主机上构建 `amd64 latest-full` 为例: + +```bash +docker buildx build . --platform linux/amd64 --target rmcs-base -t rmcs-base:linux-amd64 --load +``` + +```bash +docker buildx build . --platform linux/arm64 --target rmcs-base -t rmcs-base:linux-arm64 --load +``` + +```bash +docker buildx build . --platform linux/amd64 --target rmcs-develop-full -t rmcs-develop:latest-full \ + --build-arg SYSROOT_IMAGE_AMD64=rmcs-base:linux-amd64 \ + --build-arg SYSROOT_IMAGE_ARM64=rmcs-base:linux-arm64 \ + --load +``` + +如果是在 `arm64` 主机上构建,则交换目标平台即可(`latest-full` 的 sysroot 方向相反)。 + +## 代理构建 -需要注意的是,部分国家和地区会阻断对 `github` 的连接,此时构建需要使用代理: +部分国家和地区会阻断对 `github` 的连接,此时构建需要使用代理: ```bash docker build . --target rmcs-runtime -t rmcs-runtime:latest \ ---network host \ ---build-arg HTTP_PROXY=http://127.0.0.1:7890 \ ---build-arg HTTPS_PROXY=http://127.0.0.1:7890 + --network host \ + --build-arg HTTP_PROXY=http://127.0.0.1:7890 \ + --build-arg HTTPS_PROXY=http://127.0.0.1:7890 ``` ```bash docker build . --target rmcs-develop -t rmcs-develop:latest \ ---network host \ ---build-arg HTTP_PROXY=http://127.0.0.1:7890 \ ---build-arg HTTPS_PROXY=http://127.0.0.1:7890 + --network host \ + --build-arg HTTP_PROXY=http://127.0.0.1:7890 \ + --build-arg HTTPS_PROXY=http://127.0.0.1:7890 ``` 请自行把 `http://127.0.0.1:7890` 改为合适的代理地址。 -如果不使用本机回环地址 (127.0.0.1) 作为代理地址,构建参数中 `--network host` 可以省去。 \ No newline at end of file +如果不使用本机回环地址 (`127.0.0.1`) 作为代理地址,构建参数中的 `--network host` 可以省去。 diff --git a/docs/zh-cn/cross_build.md b/docs/zh-cn/cross_build.md new file mode 100644 index 000000000..226a3aa70 --- /dev/null +++ b/docs/zh-cn/cross_build.md @@ -0,0 +1,87 @@ +# RMCS 交叉编译使用说明(第一阶段) + +本文档说明 `rmcs-develop:latest-full`、`build-rmcs-cross` 与 CI 的约定。 + +## 1. 镜像语义 + +- `qzhhhi/rmcs-develop:latest`:原有开发镜像,行为不变。 +- `qzhhhi/rmcs-develop:latest-full`:`latest` 的超集,额外提供 cross 工具链与对向架构 sysroot。 + +对应关系: + +- `latest-full` 的 `linux/amd64` 变体内置 `/opt/sysroots/arm64` +- `latest-full` 的 `linux/arm64` 变体内置 `/opt/sysroots/amd64` + +## 2. 仓库内新增能力 + +- `rmcs_ws/toolchain.cmake`:双向 cross 参数化工具链文件。 +- `.script/build-rmcs-cross`:独立 cross 构建入口,不影响现有 `build-rmcs`。 + +`build-rmcs-cross` 默认输出目录: + +- `rmcs_ws/build-cross-` +- `rmcs_ws/install-cross-` +- `rmcs_ws/log-cross-` + +## 3. 使用方式 + +在 `latest-full` 容器里,根据当前容器架构执行对向架构的交叉编译: + +```bash +build-rmcs-cross --target-arch arm64 +``` + +适用于 `linux/amd64` 的 `latest-full` 变体。 + +```bash +build-rmcs-cross --target-arch amd64 +``` + +适用于 `linux/arm64` 的 `latest-full` 变体。 + +例如,可追加常见 `colcon build` 参数: + +```bash +build-rmcs-cross --target-arch arm64 --packages-up-to rmcs_executor +``` + +## 4. 构建环境隔离约束 + +`build-rmcs-cross` 会显式清理并重建以下环境,避免 host/target 串用: + +- `AMENT_PREFIX_PATH` +- `CMAKE_PREFIX_PATH` +- `COLCON_PREFIX_PATH` +- `PKG_CONFIG_PATH` +- `PKG_CONFIG_LIBDIR` +- `PKG_CONFIG_SYSROOT_DIR` + +并且默认启用: + +- `--merge-install` +- 不使用 `--symlink-install` + +## 5. 本地快速自检 + +```bash +test -d /opt/sysroots/arm64 +command -v aarch64-linux-gnu-gcc +./.script/build-rmcs-cross --target-arch arm64 --packages-up-to rmcs_core +readelf -h rmcs_ws/install-cross-arm64/lib/librmcs_core.so +``` + +若目标为 `amd64`,将上述 `arm64/aarch64-linux-gnu` 替换为 `amd64/x86_64-linux-gnu`。 + +本地自检故意覆盖到 `rmcs_core`,用于在推送前尽早暴露 workspace 级联依赖和交叉工具链问题;CI 只保留 `rmcs_executor` 最小 smoke,避免高频业务改动阻塞镜像推送。 + +## 6. CI 维护约定 + +- CI 先构建并推送 `qzhhhi/rmcs-base` 双架构 digest。 +- 构建 `latest-full` 时显式传入: + - `SYSROOT_IMAGE_AMD64=` + - `SYSROOT_IMAGE_ARM64=` +- `latest-full` 在每个架构 runner 上执行最小 smoke: + - sysroot 目录存在 + - cross 编译器可执行 + - `build-rmcs-cross --target-arch --packages-up-to rmcs_executor` 可完成构建 + - `readelf -h` 校验 `librmcs_executor.so` 架构 diff --git a/rmcs_ws/src/hikcamera b/rmcs_ws/src/hikcamera deleted file mode 160000 index 815952fd1..000000000 --- a/rmcs_ws/src/hikcamera +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 815952fd1388e121d2014956932f8796a1c8cdd1 diff --git a/rmcs_ws/src/rmcs_core/src/hardware/mecanum_hero.cpp b/rmcs_ws/src/rmcs_core/src/hardware/mecanum_hero.cpp deleted file mode 100644 index 3aabc5e7a..000000000 --- a/rmcs_ws/src/rmcs_core/src/hardware/mecanum_hero.cpp +++ /dev/null @@ -1,537 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "hardware/device/benewake.hpp" -#include "hardware/device/bmi088.hpp" -#include "hardware/device/can_packet.hpp" -#include "hardware/device/dji_motor.hpp" -#include "hardware/device/dr16.hpp" -#include "hardware/device/gy614.hpp" -#include "hardware/device/hipnuc.hpp" -#include "hardware/device/lk_motor.hpp" -#include "hardware/device/supercap.hpp" - -namespace rmcs_core::hardware { - -class MecanumHero - : public rmcs_executor::Component - , public rclcpp::Node { -public: - MecanumHero() - : Node{ - get_component_name(), - rclcpp::NodeOptions{}.automatically_declare_parameters_from_overrides(true)} - , command_component_( - create_partner_component(get_component_name() + "_command", *this)) { - - register_output("/tf", tf_); - tf_->set_transform( - Eigen::Translation3d{0.16, 0.0, 0.15}); - - gimbal_calibrate_subscription_ = create_subscription( - "/gimbal/calibrate", rclcpp::QoS{0}, [this](std_msgs::msg::Int32::UniquePtr&& msg) { - gimbal_calibrate_subscription_callback(std::move(msg)); - }); - - top_board_ = std::make_unique( - *this, *command_component_, get_parameter("board_serial_top_board").as_string()); - bottom_board_ = std::make_unique( - *this, *command_component_, get_parameter("board_serial_bottom_board").as_string()); - } - - MecanumHero(const MecanumHero&) = delete; - MecanumHero& operator=(const MecanumHero&) = delete; - MecanumHero(MecanumHero&&) = delete; - MecanumHero& operator=(MecanumHero&&) = delete; - - ~MecanumHero() override = default; - - void update() override { - top_board_->update(); - bottom_board_->update(); - } - - void command_update() { - top_board_->command_update(); - bottom_board_->command_update(); - } - -private: - void gimbal_calibrate_subscription_callback(std_msgs::msg::Int32::UniquePtr) { - RCLCPP_INFO( - get_logger(), "[gimbal calibration] New yaw offset: %ld", - bottom_board_->gimbal_yaw_motor_.calibrate_zero_point()); - RCLCPP_INFO( - get_logger(), "[gimbal calibration] New pitch offset: %ld", - top_board_->gimbal_pitch_motor_.calibrate_zero_point()); - } - - class HeroCommand : public rmcs_executor::Component { - public: - explicit HeroCommand(MecanumHero& hero) - : hero_(hero) {} - - void update() override { hero_.command_update(); } - - private: - MecanumHero& hero_; - }; - std::shared_ptr command_component_; - - class TopBoard final : private librmcs::agent::CBoard { - friend class MecanumHero; - - public: - explicit TopBoard( - MecanumHero& hero, HeroCommand& hero_command, std::string_view board_serial = {}) - : librmcs::agent::CBoard(board_serial) - , tf_(hero.tf_) - , imu_(1000, 0.2, 0.0) - , gy614_(hero, "/friction_wheels/temperature") - , benewake_(hero, "/gimbal/auto_aim/laser_distance") - , gimbal_pitch_motor_( - hero, hero_command, "/gimbal/pitch", - device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10} - .set_encoder_zero_point( - static_cast(hero.get_parameter("pitch_motor_zero_point").as_int()))) - // TODO: Bad CAN ID sequence, needs to be adjusted. - , gimbal_friction_wheels_( - {hero, hero_command, "/gimbal/second_left_friction", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio( - 1.)}, - {hero, hero_command, "/gimbal/second_right_friction", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508} - .set_reduction_ratio(1.) - .set_reversed()}, - {hero, hero_command, "/gimbal/first_left_friction", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}.set_reduction_ratio( - 1.)}, - {hero, hero_command, "/gimbal/first_right_friction", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508} - .set_reduction_ratio(1.) - .set_reversed()}) - , gimbal_scope_motor_( - hero, hero_command, "/gimbal/scope", - device::DjiMotor::Config{device::DjiMotor::Type::kM2006}) - , gimbal_player_viewer_motor_( - hero, hero_command, "/gimbal/player_viewer", - device::LkMotor::Config{device::LkMotor::Type::kMG4005Ei10}) { - - imu_.set_coordinate_mapping([](double x, double y, double z) { - // Get the mapping with the following code. - // The rotation angle must be an exact multiple of 90 degrees, otherwise use a - // matrix. - - // Eigen::AngleAxisd pitch_link_to_imu_link{ - // std::numbers::pi, Eigen::Vector3d::UnitZ()}; - // Eigen::Vector3d mapping = pitch_link_to_imu_link * Eigen::Vector3d{1, 2, 3}; - // std::cout << mapping << std::endl; - - return std::make_tuple(x, y, z); - }); - - hero.register_output("/gimbal/yaw/velocity_imu", gimbal_yaw_velocity_imu_); - hero.register_output("/gimbal/pitch/velocity_imu", gimbal_pitch_velocity_imu_); - - external_imu_thread_ = std::jthread([this, &hero](const std::stop_token& stop_token) { - external_imu_thread_main( - stop_token, hero.get_parameter("external_imu_port").as_string(), - hero.get_logger()); - }); - } - - TopBoard(const TopBoard&) = delete; - TopBoard& operator=(const TopBoard&) = delete; - TopBoard(TopBoard&&) = delete; - TopBoard& operator=(TopBoard&&) = delete; - - ~TopBoard() final = default; - - void update() { - imu_.update_status(); - Eigen::Quaterniond gimbal_imu_pose{imu_.q0(), imu_.q1(), imu_.q2(), imu_.q3()}; - - if (external_imu_available_.load(std::memory_order::relaxed)) { - external_imu_.update_status(); - gimbal_imu_pose.slerp(0.001, external_imu_.quaternion()); - imu_.q0() = gimbal_imu_pose.w(); - imu_.q1() = gimbal_imu_pose.x(); - imu_.q2() = gimbal_imu_pose.y(); - imu_.q3() = gimbal_imu_pose.z(); - } - - tf_->set_transform( - gimbal_imu_pose.conjugate()); - - gy614_.update_status(); - benewake_.update_status(); - - *gimbal_yaw_velocity_imu_ = imu_.gz(); - *gimbal_pitch_velocity_imu_ = imu_.gy(); - - gimbal_pitch_motor_.update_status(); - tf_->set_state( - gimbal_pitch_motor_.angle()); - - for (auto& motor : gimbal_friction_wheels_) - motor.update_status(); - - gimbal_scope_motor_.update_status(); - gimbal_player_viewer_motor_.update_status(); - } - - void command_update() { - auto builder = start_transmit(); - - builder.can1_transmit({ - .can_id = 0x200, - .can_data = - device::CanPacket8{ - gimbal_friction_wheels_[0].generate_command(), - gimbal_friction_wheels_[1].generate_command(), - gimbal_friction_wheels_[2].generate_command(), - gimbal_friction_wheels_[3].generate_command(), - } - .as_bytes(), - }); - - builder.can2_transmit({ - .can_id = 0x142, - .can_data = gimbal_pitch_motor_.generate_command().as_bytes(), - }); - - builder.can1_transmit({ - .can_id = 0x1ff, - .can_data = - device::CanPacket8{ - gimbal_scope_motor_.generate_command(), - device::CanPacket8::PaddingQuarter{}, - device::CanPacket8::PaddingQuarter{}, - device::CanPacket8::PaddingQuarter{}, - } - .as_bytes(), - }); - - builder.can2_transmit({ - .can_id = 0x141, - .can_data = gimbal_player_viewer_motor_ - .generate_angle_command(gimbal_player_viewer_motor_.control_angle()) - .as_bytes(), - }); - } - - private: - void can1_receive_callback(const librmcs::data::CanDataView& data) override { - if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] - return; - - auto can_id = data.can_id; - if (can_id == 0x201) { - gimbal_friction_wheels_[0].store_status(data.can_data); - } else if (can_id == 0x202) { - gimbal_friction_wheels_[1].store_status(data.can_data); - } else if (can_id == 0x203) { - gimbal_friction_wheels_[2].store_status(data.can_data); - } else if (can_id == 0x204) { - gimbal_friction_wheels_[3].store_status(data.can_data); - } else if (can_id == 0x205) { - gimbal_scope_motor_.store_status(data.can_data); - } - } - - void can2_receive_callback(const librmcs::data::CanDataView& data) override { - if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] - return; - - auto can_id = data.can_id; - if (can_id == 0x142) { - gimbal_pitch_motor_.store_status(data.can_data); - } else if (can_id == 0x141) { - gimbal_player_viewer_motor_.store_status(data.can_data); - } - } - - void uart1_receive_callback(const librmcs::data::UartDataView& data) override { - benewake_.store_status(data.uart_data.data(), data.uart_data.size()); - } - - void uart2_receive_callback(const librmcs::data::UartDataView& data) override { - gy614_.store_status(data.uart_data.data(), data.uart_data.size()); - } - - void accelerometer_receive_callback( - const librmcs::data::AccelerometerDataView& data) override { - imu_.store_accelerometer_status(data.x, data.y, data.z); - } - - void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { - imu_.store_gyroscope_status(data.x, data.y, data.z); - } - - void external_imu_thread_main( - const std::stop_token& stop_token, const std::string& port_name, - const rclcpp::Logger& logger) { - try { - serial::Serial serial{port_name, 115200, serial::Timeout::simpleTimeout(10)}; - rmcs_utility::FpsCounter fps_counter; - - while (!stop_token.stop_requested()) { - if (external_imu_.store_status(serial) && fps_counter.count()) { - const bool available = fps_counter.fps() > 350.0; - if (!available) - RCLCPP_WARN(logger, "External IMU low FPS: %.2f", fps_counter.fps()); - else if (!external_imu_available_.load(std::memory_order::relaxed)) - RCLCPP_INFO( - logger, "External IMU now available with FPS: %.2f", - fps_counter.fps()); - external_imu_available_.store(available, std::memory_order::relaxed); - } - } - } catch (const std::exception& e) { - external_imu_available_.store(false, std::memory_order::relaxed); - RCLCPP_ERROR(logger, "Exception in external IMU thread: %s", e.what()); - } - } - - OutputInterface& tf_; - - device::Bmi088 imu_; - device::Gy614 gy614_; - device::Benewake benewake_; - - OutputInterface gimbal_yaw_velocity_imu_; - OutputInterface gimbal_pitch_velocity_imu_; - - device::LkMotor gimbal_pitch_motor_; - - device::DjiMotor gimbal_friction_wheels_[4]; - - device::DjiMotor gimbal_scope_motor_; - device::LkMotor gimbal_player_viewer_motor_; - - rmcs_core::hardware::device::Hipnuc external_imu_; - std::atomic external_imu_available_ = false; - std::jthread external_imu_thread_; - }; - - class BottomBoard final : private librmcs::agent::CBoard { - friend class MecanumHero; - - public: - explicit BottomBoard( - MecanumHero& hero, HeroCommand& hero_command, std::string_view board_serial = {}) - : librmcs::agent::CBoard(board_serial) - , imu_(1000, 0.2, 0.0) - , tf_(hero.tf_) - , dr16_(hero) - , chassis_wheel_motors_( - {hero, hero_command, "/chassis/left_front_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}}, - {hero, hero_command, "/chassis/left_back_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}}, - {hero, hero_command, "/chassis/right_back_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}}, - {hero, hero_command, "/chassis/right_front_wheel", - device::DjiMotor::Config{device::DjiMotor::Type::kM3508}}) - , supercap_(hero, hero_command) - , gimbal_yaw_motor_( - hero, hero_command, "/gimbal/yaw", - device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10} - .set_encoder_zero_point( - static_cast(hero.get_parameter("yaw_motor_zero_point").as_int()))) - , gimbal_bullet_feeder_( - hero, hero_command, "/gimbal/bullet_feeder", - device::LkMotor::Config{device::LkMotor::Type::kMG5010Ei10} - .set_reversed() - .enable_multi_turn_angle()) { - - hero.register_output("/referee/serial", referee_serial_); - referee_serial_->read = [this](std::byte* buffer, size_t size) { - return referee_ring_buffer_receive_.pop_front_n( - [&buffer](std::byte byte) noexcept { *buffer++ = byte; }, size); - }; - referee_serial_->write = [this](const std::byte* buffer, size_t size) { - start_transmit().uart1_transmit({ - .uart_data = std::span{buffer, size} - }); - return size; - }; - - hero.register_output("/chassis/yaw/velocity_imu", chassis_yaw_velocity_imu_, 0); - } - - BottomBoard(const BottomBoard&) = delete; - BottomBoard& operator=(const BottomBoard&) = delete; - BottomBoard(BottomBoard&&) = delete; - BottomBoard& operator=(BottomBoard&&) = delete; - - ~BottomBoard() final = default; - - void update() { - imu_.update_status(); - dr16_.update_status(); - - *chassis_yaw_velocity_imu_ = imu_.gz(); - - for (auto& motor : chassis_wheel_motors_) - motor.update_status(); - - supercap_.update_status(); - - gimbal_yaw_motor_.update_status(); - tf_->set_state( - gimbal_yaw_motor_.angle()); - - gimbal_bullet_feeder_.update_status(); - } - - void command_update() { - auto builder = start_transmit(); - - builder.can1_transmit({ - .can_id = 0x200, - .can_data = - device::CanPacket8{ - chassis_wheel_motors_[0].generate_command(), - chassis_wheel_motors_[1].generate_command(), - chassis_wheel_motors_[2].generate_command(), - chassis_wheel_motors_[3].generate_command(), - } - .as_bytes(), - }); - - builder.can1_transmit({ - .can_id = 0x141, - .can_data = gimbal_bullet_feeder_ - .generate_torque_command(gimbal_bullet_feeder_.control_torque()) - .as_bytes(), - }); - - builder.can2_transmit({ - .can_id = 0x141, - .can_data = gimbal_yaw_motor_.generate_command().as_bytes(), - }); - - builder.can2_transmit({ - .can_id = 0x1FE, - .can_data = - device::CanPacket8{ - device::CanPacket8::PaddingQuarter{}, - device::CanPacket8::PaddingQuarter{}, - device::CanPacket8::PaddingQuarter{}, - supercap_.generate_command(), - } - .as_bytes(), - }); - } - - private: - void can1_receive_callback(const librmcs::data::CanDataView& data) override { - if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] - return; - - auto can_id = data.can_id; - if (can_id == 0x201) { - chassis_wheel_motors_[0].store_status(data.can_data); - } else if (can_id == 0x202) { - chassis_wheel_motors_[1].store_status(data.can_data); - } else if (can_id == 0x203) { - chassis_wheel_motors_[2].store_status(data.can_data); - } else if (can_id == 0x204) { - chassis_wheel_motors_[3].store_status(data.can_data); - } else if (can_id == 0x141) { - gimbal_bullet_feeder_.store_status(data.can_data); - } - } - - void can2_receive_callback(const librmcs::data::CanDataView& data) override { - if (data.is_extended_can_id || data.is_remote_transmission) [[unlikely]] - return; - - auto can_id = data.can_id; - if (can_id == 0x141) { - gimbal_yaw_motor_.store_status(data.can_data); - } else if (can_id == 0x300) { - supercap_.store_status(data.can_data); - } - } - - void uart1_receive_callback(const librmcs::data::UartDataView& data) override { - const auto* uart_data = data.uart_data.data(); - referee_ring_buffer_receive_.emplace_back_n( - [&uart_data](std::byte* storage) noexcept { *storage = *uart_data++; }, - data.uart_data.size()); - } - - void dbus_receive_callback(const librmcs::data::UartDataView& data) override { - dr16_.store_status(data.uart_data.data(), data.uart_data.size()); - } - - void accelerometer_receive_callback( - const librmcs::data::AccelerometerDataView& data) override { - imu_.store_accelerometer_status(data.x, data.y, data.z); - } - - void gyroscope_receive_callback(const librmcs::data::GyroscopeDataView& data) override { - imu_.store_gyroscope_status(data.x, data.y, data.z); - } - - device::Bmi088 imu_; - OutputInterface& tf_; - - OutputInterface chassis_yaw_velocity_imu_; - - device::Dr16 dr16_; - - device::DjiMotor chassis_wheel_motors_[4]; - device::Supercap supercap_; - - device::LkMotor gimbal_yaw_motor_; - - device::LkMotor gimbal_bullet_feeder_; - - rmcs_utility::RingBuffer referee_ring_buffer_receive_{256}; - OutputInterface referee_serial_; - }; - - OutputInterface tf_; - - rclcpp::Subscription::SharedPtr gimbal_calibrate_subscription_; - - std::unique_ptr top_board_; - std::unique_ptr bottom_board_; -}; - -} // namespace rmcs_core::hardware - -#include - -PLUGINLIB_EXPORT_CLASS(rmcs_core::hardware::MecanumHero, rmcs_executor::Component) diff --git a/rmcs_ws/src/serial b/rmcs_ws/src/serial deleted file mode 160000 index d50c40180..000000000 --- a/rmcs_ws/src/serial +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d50c401809f17cc5862bdbe37cc6e15737ba7c49 diff --git a/rmcs_ws/toolchain.cmake b/rmcs_ws/toolchain.cmake new file mode 100644 index 000000000..0a2b90fa1 --- /dev/null +++ b/rmcs_ws/toolchain.cmake @@ -0,0 +1,160 @@ +cmake_minimum_required(VERSION 3.16) + +foreach(_rmcs_var IN ITEMS RMCS_TARGET_ARCH RMCS_SYSROOT RMCS_TARGET_TRIPLET) + if((NOT DEFINED ${_rmcs_var} OR "${${_rmcs_var}}" STREQUAL "") AND DEFINED ENV{${_rmcs_var}}) + set(${_rmcs_var} "$ENV{${_rmcs_var}}") + endif() +endforeach() +unset(_rmcs_var) + +set(RMCS_TARGET_ARCH "${RMCS_TARGET_ARCH}" CACHE STRING "RMCS target architecture") +set(RMCS_SYSROOT "${RMCS_SYSROOT}" CACHE PATH "RMCS target sysroot path") +set(RMCS_TARGET_TRIPLET "${RMCS_TARGET_TRIPLET}" CACHE STRING "RMCS target compiler triplet") +list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES + RMCS_TARGET_ARCH + RMCS_SYSROOT + RMCS_TARGET_TRIPLET +) + +if(NOT RMCS_TARGET_ARCH) + message(FATAL_ERROR "RMCS_TARGET_ARCH is required. Supported values: arm64, amd64.") +endif() + +if(NOT RMCS_TARGET_TRIPLET) + if(RMCS_TARGET_ARCH STREQUAL "arm64") + set(RMCS_TARGET_TRIPLET "aarch64-linux-gnu") + elseif(RMCS_TARGET_ARCH STREQUAL "amd64") + set(RMCS_TARGET_TRIPLET "x86_64-linux-gnu") + else() + message(FATAL_ERROR "Unsupported RMCS_TARGET_ARCH: ${RMCS_TARGET_ARCH}") + endif() +endif() + +if(NOT RMCS_SYSROOT) + set(RMCS_SYSROOT "/opt/sysroots/${RMCS_TARGET_ARCH}") +endif() + +if(NOT EXISTS "${RMCS_SYSROOT}") + message(FATAL_ERROR "RMCS_SYSROOT does not exist: ${RMCS_SYSROOT}") +endif() + +set(CMAKE_SYSTEM_NAME Linux) +if(RMCS_TARGET_ARCH STREQUAL "arm64") + set(CMAKE_SYSTEM_PROCESSOR aarch64) +elseif(RMCS_TARGET_ARCH STREQUAL "amd64") + set(CMAKE_SYSTEM_PROCESSOR x86_64) +else() + message(FATAL_ERROR "Unsupported RMCS_TARGET_ARCH: ${RMCS_TARGET_ARCH}") +endif() + +set(CMAKE_SYSROOT "${RMCS_SYSROOT}") +set(_rmcs_find_root_path "${RMCS_SYSROOT}") +foreach(_rmcs_prefix_env IN ITEMS AMENT_PREFIX_PATH CMAKE_PREFIX_PATH) + if(DEFINED ENV{${_rmcs_prefix_env}} AND NOT "$ENV{${_rmcs_prefix_env}}" STREQUAL "") + string(REPLACE ":" ";" _rmcs_env_prefixes "$ENV{${_rmcs_prefix_env}}") + list(APPEND _rmcs_find_root_path ${_rmcs_env_prefixes}) + endif() +endforeach() +list(REMOVE_DUPLICATES _rmcs_find_root_path) +set(CMAKE_FIND_ROOT_PATH + "${_rmcs_find_root_path}" +) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +function(rmcs_find_host_program output_var) + set(options) + set(oneValueArgs ENV_VAR) + set(multiValueArgs NAMES) + cmake_parse_arguments(RMCS_FIND_HOST_PROGRAM "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT RMCS_FIND_HOST_PROGRAM_NAMES) + message(FATAL_ERROR "rmcs_find_host_program requires at least one candidate in NAMES.") + endif() + + unset(_rmcs_resolved_program CACHE) + unset(_rmcs_resolved_program) + string(REPLACE ":" ";" _rmcs_host_path_entries "$ENV{PATH}") + if(RMCS_FIND_HOST_PROGRAM_ENV_VAR + AND DEFINED ENV{${RMCS_FIND_HOST_PROGRAM_ENV_VAR}} + AND NOT "$ENV{${RMCS_FIND_HOST_PROGRAM_ENV_VAR}}" STREQUAL "") + set(_rmcs_program_candidates "$ENV{${RMCS_FIND_HOST_PROGRAM_ENV_VAR}}") + if(IS_ABSOLUTE "${_rmcs_program_candidates}") + if(NOT EXISTS "${_rmcs_program_candidates}" OR IS_DIRECTORY "${_rmcs_program_candidates}") + message(FATAL_ERROR + "${RMCS_FIND_HOST_PROGRAM_ENV_VAR} points to a missing executable: ${_rmcs_program_candidates}" + ) + endif() + set(_rmcs_resolved_program "${_rmcs_program_candidates}") + else() + find_program(_rmcs_resolved_program + NAMES "${_rmcs_program_candidates}" + PATHS ${_rmcs_host_path_entries} + NO_DEFAULT_PATH + ) + endif() + else() + set(_rmcs_program_candidates ${RMCS_FIND_HOST_PROGRAM_NAMES}) + find_program(_rmcs_resolved_program + NAMES ${_rmcs_program_candidates} + PATHS ${_rmcs_host_path_entries} + NO_DEFAULT_PATH + ) + endif() + + if(NOT _rmcs_resolved_program) + list(JOIN _rmcs_program_candidates ", " _rmcs_program_candidates_str) + message(FATAL_ERROR "Cannot resolve host program. Tried: ${_rmcs_program_candidates_str}") + endif() + + set(${output_var} "${_rmcs_resolved_program}" PARENT_SCOPE) +endfunction() + +rmcs_find_host_program(RMCS_C_COMPILER ENV_VAR CC NAMES "${RMCS_TARGET_TRIPLET}-gcc") +rmcs_find_host_program(RMCS_CXX_COMPILER ENV_VAR CXX NAMES "${RMCS_TARGET_TRIPLET}-g++") +rmcs_find_host_program(RMCS_PYTHON_EXECUTABLE NAMES python3) +rmcs_find_host_program(RMCS_PKG_CONFIG_EXECUTABLE NAMES pkg-config pkgconf) + +if(NOT RMCS_C_COMPILER) + message(FATAL_ERROR "Cannot find C compiler: ${RMCS_TARGET_TRIPLET}-gcc") +endif() +if(NOT RMCS_CXX_COMPILER) + message(FATAL_ERROR "Cannot find CXX compiler: ${RMCS_TARGET_TRIPLET}-g++") +endif() + +set(CMAKE_C_COMPILER "${RMCS_C_COMPILER}") +set(CMAKE_CXX_COMPILER "${RMCS_CXX_COMPILER}") +set(Python3_EXECUTABLE "${RMCS_PYTHON_EXECUTABLE}" CACHE FILEPATH "Host Python interpreter" FORCE) +set(AMENT_PYTHON_EXECUTABLE "${RMCS_PYTHON_EXECUTABLE}" CACHE FILEPATH "Host Python interpreter for ament" FORCE) +set(PKG_CONFIG_EXECUTABLE "${RMCS_PKG_CONFIG_EXECUTABLE}" CACHE FILEPATH "Host pkg-config executable" FORCE) + +if(CMAKE_GENERATOR MATCHES "Makefiles") + rmcs_find_host_program(RMCS_MAKE_PROGRAM NAMES gmake make) + set(CMAKE_MAKE_PROGRAM "${RMCS_MAKE_PROGRAM}" CACHE FILEPATH "Host make program" FORCE) +endif() + +set(CMAKE_PREFIX_PATH + "/opt/ros/jazzy" + "/usr/local" + "/usr" + CACHE STRING "Target-side prefix path for cross-compilation" +) + +set(ENV{PKG_CONFIG_SYSROOT_DIR} "${RMCS_SYSROOT}") +set(_rmcs_pkg_config_libdirs + "${RMCS_SYSROOT}/usr/local/lib/${RMCS_TARGET_TRIPLET}/pkgconfig" + "${RMCS_SYSROOT}/usr/local/lib/pkgconfig" + "${RMCS_SYSROOT}/usr/local/share/pkgconfig" + "${RMCS_SYSROOT}/usr/lib/${RMCS_TARGET_TRIPLET}/pkgconfig" + "${RMCS_SYSROOT}/usr/lib/pkgconfig" + "${RMCS_SYSROOT}/usr/share/pkgconfig" + "${RMCS_SYSROOT}/lib/${RMCS_TARGET_TRIPLET}/pkgconfig" + "${RMCS_SYSROOT}/lib/pkgconfig" + "${RMCS_SYSROOT}/opt/ros/jazzy/lib/${RMCS_TARGET_TRIPLET}/pkgconfig" + "${RMCS_SYSROOT}/opt/ros/jazzy/lib/pkgconfig" + "${RMCS_SYSROOT}/opt/ros/jazzy/share/pkgconfig" +) +list(JOIN _rmcs_pkg_config_libdirs ":" _rmcs_pkg_config_libdir) +set(ENV{PKG_CONFIG_LIBDIR} "${_rmcs_pkg_config_libdir}")