diff --git a/.github/workflows/manual-dispatch.yml b/.github/workflows/manual-dispatch.yml new file mode 100644 index 0000000..3a28285 --- /dev/null +++ b/.github/workflows/manual-dispatch.yml @@ -0,0 +1,37 @@ +name: Manual Build and Push + +# Run workflow by manual dispatch +on: + workflow_dispatch: + inputs: + cl_branch: + description: 'If set, the desired branch of control libraries' + required: false + default: 'develop' + +jobs: + build-publish: + runs-on: ubuntu-latest + name: Build and publish image + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Build image + run: | + IMAGE_NAME=network-interfaces:latest + docker build . \ + --build-arg CL_BRANCH=${{ inputs.cl_branch }} \ + --tag ${IMAGE_NAME} + shell: bash + + - name: Login to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + shell: bash + + - name: Push image + run: | + IMAGE_NAME=network-interfaces:latest + docker tag ${IMAGE_NAME} ghcr.io/${{ github.repository_owner }}/${IMAGE_NAME} + docker push ghcr.io/${{ github.repository_owner }}/${IMAGE_NAME} + shell: bash \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5287c5e..db831ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,15 @@ Release Versions: +- [0.2.0](#020) - [0.1.0](#010) +## 0.2.0 + +Version 0.2.0 introduces a slightly different Dockerfile and contains a GitHub workflow to build and push an image with +the pre-built network interface libraries. Additionally, two loopback scripts have been added to simplify communication +testing. + ## 0.1.0 A set of drivers, protocols and libraries for communicating between software and hardware devices. diff --git a/Dockerfile b/Dockerfile index fc3c9cd..a6b47d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,36 @@ -ARG ROS_VERSION=foxy -FROM ghcr.io/aica-technology/ros2-control-libraries:${ROS_VERSION} AS core-dependencies +FROM ghcr.io/epfl-lasa/control-libraries/development-dependencies as source-dependencies -RUN sudo apt-get update && sudo apt-get install -y \ - libmbedtls-dev \ - libsodium-dev \ - libzmq3-dev \ - && sudo rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y libmbedtls-dev libsodium-dev libzmq3-dev \ + && apt-get clean && rm -rf /var/lib/apt/lists/* -# install cppzmq bindings -ARG CPPZMQ_VERSION=4.7.1 WORKDIR /tmp +ARG CPPZMQ_VERSION=4.7.1 RUN wget https://github.com/zeromq/cppzmq/archive/v${CPPZMQ_VERSION}.tar.gz -O cppzmq-${CPPZMQ_VERSION}.tar.gz RUN tar -xzf cppzmq-${CPPZMQ_VERSION}.tar.gz WORKDIR /tmp/cppzmq-${CPPZMQ_VERSION} RUN mkdir build && cd build && cmake .. -DCPPZMQ_BUILD_TESTS=OFF && make -j install + WORKDIR /tmp -RUN rm -rf cppzmq* +ARG CONTROL_LIBRARIES_BRANCH=develop +RUN git clone -b ${CONTROL_LIBRARIES_BRANCH} --depth 1 https://github.com/epfl-lasa/control-libraries.git +RUN cd control-libraries/source && sudo ./install.sh --auto --no-controllers --no-dynamical-systems --no-robot-model +RUN cd control-libraries/protocol && sudo ./install.sh --auto +RUN pip3 install control-libraries/python + +RUN rm -rf /tmp/* # install pyzmq RUN pip3 install pyzmq -WORKDIR ${HOME} +FROM source-dependencies as build-test -# Clean image -RUN sudo apt-get clean && sudo rm -rf /var/lib/apt/lists/* +WORKDIR ${HOME} +COPY --chown=${USER} ./cpp ./network_interfaces/cpp +COPY --chown=${USER} ./python ./network_interfaces/python -FROM core-dependencies AS build-test +RUN cd ./network_interfaces/cpp && mkdir build && cd build && cmake -DBUILD_TESTING=ON .. \ + && make -j && CTEST_OUTPUT_ON_FAILURE=1 make test && make -j install +RUN cd ./network_interfaces/python && pip3 install ./ && python3 -m unittest discover ./test --verbose -COPY ./ ./ -RUN cd cpp && mkdir build && cd build && cmake -DBUILD_TESTING=ON .. \ - && make -j all && CTEST_OUTPUT_ON_FAILURE=1 make test -RUN cd python && pip install ./ && python3 -m unittest discover ./network_interfaces/tests --verbose +# Clean image +RUN rm -rf ./network_interfaces diff --git a/README.md b/README.md index 4b84a98..9f19c07 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,83 @@ For Python, install the library simply with cd path/to/network-interfaces/python && pip3 install ./ ``` +### ZMQ loopback scripts + +Both the C++ and Python directory contain *loopback* executables that subscribe to the state (or command) +and publish a command (or state) in and endless loop. They can be used to check if the robot and the user are connected +and receive each other's messages correctly. To run the scripts, make the CMake project and/or install the Python +project, and then choose one of the following: + +``` +bash build.sh -t +aica-docker interactive aica-technology/network-interfaces:build-test -u ros2 +cd path/to/cpp/build +./zmq_loopback_state state_uri command_uri +./zmq_loopback_command state_uri command_uri + +cd path/to/python/scripts +python3 zmq_loopback_state.py state_uri command_uri +python3 zmq_loopback_command.py state_uri command_uri +``` + +Note that the scripts are provided with a correct combination of state and command URIs. There are examples of such +combinations below. Assume the robot state is on port 1601 and the command on 1602: + +#### Everything runs in one container / on the same host (network independent) + +If all applications run in the same container, or on the same host, the situation is: + +- The robot publishes its state on `0.0.0.0:1601` and listens to commands on `0.0.0.0:1602` with both sockets + non-binding: run `./zmq_loopback_state *:1601 *:1602` or `python3 zmq_loopback_state.py *:1601 *:1602` to receive and + print the robot's state. +- The controller sends the command on `*:1602` and receives the state on `*:1601` with both sockets binding: + run `./zmq_loopback_command 0.0.0.0:1601 0.0.0.0:1602` or + `python3 zmq_loopback_command.py 0.0.0.0:1601 0.0.0.0:1602` to receive and print the command and send back a random + state. + +#### One or more containers and host, all on host network and with no hostname + +Same as above. + +#### One container with host (container not on host network) + +The container is an SSH server or needs to be on a user-defined network, but the robot is connected directly to the host +machine. This is almost the same case as above: + +- The controller sends the command on `*:1602` and receives the state on `*:1601` with both sockets binding: + run `./zmq_loopback_command 0.0.0.0:1601 0.0.0.0:1602` or + `python3 zmq_loopback_command.py 0.0.0.0:1601 0.0.0.0:1602` to receive and print the command and send back a random + state. + +There is one important difference though: The container needs to bind ports 1601 and 1602 (i.e. +add `-p1601:1601 -p1602:1602` to the `docker run` command) explicitly such that the communication goes through. + +#### Several containers, user-defined bridge network with hostnames + +If the containers all run on a user-defined bridge network, the connecting sockets need to be provided with the hostname +of the binding sockets. For example, if the containers are running on network *aicanet* and have hostnames *robot* and +*controller*, respectively. + +- The robot publishes its state on `controller.aicanet:1601` and listens to commands on `controller.aicanet:1602` with + both sockets non-binding: run `./zmq_loopback_state *:1601 *:1602` or `python3 zmq_loopback_state.py *:1601 *:1602` to + receive and print the robot's state. +- The controller sends the command on `*:1602` and receives the state on `*:1601` with both sockets binding: + run `./zmq_loopback_command controller.aicanet:1601 controller.aicanet:1602` or ` + python3 zmq_loopback_command.py controller.aicanet:1601 controller.aicanet:1602` to receive and print the command and + send back a random state. + +#### Note + +- This list of combinations is not exhaustive. +- The binding sockets always have a URI like `*:port` whilst the connecting sockets need to provide the complete address + version (`0.0.0.0:port` if on localhost or `hostname.network:port` if on bridge network). + ## Development -To build and run a Docker container as an SSH toolchain server for remote development with +Build and run a Docker container as an SSH toolchain server for remote development with: ```console -bash build.sh -aica-docker server aica-technology/network-interfaces -u ros2 -p 8010 +bash build-server.sh -s ``` Note: This requires the installation of the `aica-docker` scripts diff --git a/build.sh b/build-test.sh old mode 100644 new mode 100755 similarity index 59% rename from build.sh rename to build-test.sh index d377707..d1ee3eb --- a/build.sh +++ b/build-test.sh @@ -1,23 +1,18 @@ #!/bin/bash -ROS_VERSION=foxy +CONTROL_LIBRARIES_BRANCH=develop IMAGE_NAME=aica-technology/network-interfaces -IMAGE_STAGE=core-dependencies +IMAGE_STAGE=build-test BUILD_FLAGS=() -HELP_MESSAGE="Usage: ./build.sh [--rebuild] [--test] [--verbose] - -Build a Docker container for remote development and/or running unittests. +HELP_MESSAGE="Usage: ./build.sh [-r] [-v] +Build a Docker container for running unittests. Options: -r, --rebuild Rebuild the image with no cache. - - -t, --test Build and run all the unittests. - -v, --verbose Show all the output of the Docker - build process - + build process. -h, --help Show this help message." while [ "$#" -gt 0 ]; do @@ -30,9 +25,9 @@ while [ "$#" -gt 0 ]; do esac done -BUILD_FLAGS+=(--build-arg ROS_VERSION="${ROS_VERSION}") +BUILD_FLAGS+=(--build-arg CONTROL_LIBRARIES_BRANCH="${CONTROL_LIBRARIES_BRANCH}") BUILD_FLAGS+=(-t "${IMAGE_NAME}:${IMAGE_STAGE}") BUILD_FLAGS+=(--target "${IMAGE_STAGE}") -docker pull ghcr.io/aica-technology/ros2-control-libraries:"${ROS_VERSION}" -DOCKER_BUILDKIT=1 docker build "${BUILD_FLAGS[@]}" . \ No newline at end of file +docker pull ghcr.io/epfl-lasa/control-libraries/development-dependencies || exit 1 +DOCKER_BUILDKIT=1 docker build "${BUILD_FLAGS[@]}" . diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index b013bc2..850ecf2 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.9) -project(network_interfaces VERSION 0.1.0) +project(network_interfaces VERSION 0.2.0) option(BUILD_TESTING "Build tests." OFF) @@ -36,9 +36,14 @@ install(DIRECTORY include/ DESTINATION include ) +add_executable(zmq_loopback_state scripts/zmq_loopback_state.cpp) +target_link_libraries(zmq_loopback_state clproto cppzmq state_representation) + +add_executable(zmq_loopback_command scripts/zmq_loopback_command.cpp) +target_link_libraries(zmq_loopback_command clproto cppzmq state_representation) + if(BUILD_TESTING) - add_executable(test_communication test/test_communication.cpp) - target_link_libraries(test_communication ${GTEST_LIBRARIES} pthread) - add_test(NAME test_communication COMMAND test_communication) - target_link_libraries(test_communication clproto cppzmq state_representation) + add_executable(test_zmq_communication test/test_zmq_communication.cpp) + target_link_libraries(test_zmq_communication ${GTEST_LIBRARIES} pthread clproto cppzmq state_representation) + add_test(NAME test_zmq_communication COMMAND test_zmq_communication) endif() diff --git a/cpp/scripts/zmq_loopback_command.cpp b/cpp/scripts/zmq_loopback_command.cpp new file mode 100644 index 0000000..8548ccb --- /dev/null +++ b/cpp/scripts/zmq_loopback_command.cpp @@ -0,0 +1,35 @@ +#include +#include +#include + +#include "network_interfaces/zmq/network.h" + +int main(int argc, char** argv) { + std::string state_uri, command_uri; + if (argc != 3) { + std::cerr << "Provide two arguments: state_uri command_uri" << std::endl; + return 1; + } else { + state_uri = argv[1]; + command_uri = argv[2]; + } + ::zmq::context_t context(1); + ::zmq::socket_t subscriber, publisher; + network_interfaces::zmq::configure_subscriber(context, subscriber, command_uri, false); + network_interfaces::zmq::configure_publisher(context, publisher, state_uri, false); + + network_interfaces::zmq::StateMessage state; + state.joint_state = state_representation::JointState::Random("loopback", 7); + state.ee_state = state_representation::CartesianState::Random("loopback_ee"); + network_interfaces::zmq::CommandMessage command; + + while (true) { + network_interfaces::zmq::send(state, publisher); + if (network_interfaces::zmq::receive(command, subscriber)) { + std::cout << command << std::endl; + } + usleep(100000); + } + + return 0; +} diff --git a/cpp/scripts/zmq_loopback_state.cpp b/cpp/scripts/zmq_loopback_state.cpp new file mode 100644 index 0000000..d6e47c7 --- /dev/null +++ b/cpp/scripts/zmq_loopback_state.cpp @@ -0,0 +1,35 @@ +#include +#include + +#include "network_interfaces/zmq/network.h" + +int main(int argc, char** argv) { + std::string state_uri, command_uri; + if (argc != 3) { + std::cerr << "Provide two arguments: state_uri command_uri" << std::endl; + return 1; + } else { + state_uri = argv[1]; + command_uri = argv[2]; + } + ::zmq::context_t context(1); + ::zmq::socket_t subscriber, publisher; + network_interfaces::zmq::configure_subscriber(context, subscriber, state_uri, true); + network_interfaces::zmq::configure_publisher(context, publisher, command_uri, true); + + network_interfaces::zmq::StateMessage state; + network_interfaces::zmq::CommandMessage command; + + while (true) { + if (network_interfaces::zmq::receive(state, subscriber)) { + std::cout << state << std::endl; + command.joint_state = state_representation::JointState::Zero(state.joint_state.get_name(), state.joint_state.get_names()); + command.joint_state.set_positions(state.joint_state.get_positions()); + command.control_type = std::vector{3}; + network_interfaces::zmq::send(command, publisher); + } + usleep(100000); + } + + return 0; +} diff --git a/cpp/test/test_communication.cpp b/cpp/test/test_zmq_communication.cpp similarity index 100% rename from cpp/test/test_communication.cpp rename to cpp/test/test_zmq_communication.cpp diff --git a/dev-server.sh b/dev-server.sh new file mode 100755 index 0000000..f86bc7d --- /dev/null +++ b/dev-server.sh @@ -0,0 +1,35 @@ +#!/bin/bash +CONTROL_LIBRARIES_BRANCH=develop +REMOTE_SSH_PORT=4420 + +IMAGE_NAME=aica-technology/network-interfaces +IMAGE_STAGE=source-dependencies + +BUILD_FLAGS=() + +HELP_MESSAGE="Usage: ./dev-server.sh [-r] [-v] + +Build a Docker container for remote development. +Options: + -r, --rebuild Rebuild the image with no cache. + -v, --verbose Show all the output of the Docker + build process. + -h, --help Show this help message." + +while [ "$#" -gt 0 ]; do + case "$1" in + -r|--rebuild) BUILD_FLAGS+=(--no-cache); shift 1;; + -v|--verbose) BUILD_FLAGS+=(--progress=plain); shift 1;; + -h|--help) echo "${HELP_MESSAGE}"; exit 0;; + *) echo "Unknown option: $1" >&2; echo "${HELP_MESSAGE}"; exit 1;; + esac +done + +BUILD_FLAGS+=(--build-arg CONTROL_LIBRARIES_BRANCH="${CONTROL_LIBRARIES_BRANCH}") +BUILD_FLAGS+=(-t "${IMAGE_NAME}:${IMAGE_STAGE}") +BUILD_FLAGS+=(--target "${IMAGE_STAGE}") + +docker pull ghcr.io/epfl-lasa/control-libraries/development-dependencies || exit 1 +DOCKER_BUILDKIT=1 docker build "${BUILD_FLAGS[@]}" . || exit 1 + +aica-docker server "${IMAGE_NAME}:${IMAGE_STAGE}" -u developer -p "${REMOTE_SSH_PORT}" diff --git a/python/scripts/zmq_loopback_command.py b/python/scripts/zmq_loopback_command.py new file mode 100644 index 0000000..bc79dfb --- /dev/null +++ b/python/scripts/zmq_loopback_command.py @@ -0,0 +1,35 @@ +import argparse +import time + +import zmq +from state_representation import CartesianState, JointState + +from network_interfaces.zmq import network + + +def main(state_uri, command_uri): + context = zmq.Context(1) + subscriber = network.configure_subscriber(context, command_uri, False) + publisher = network.configure_publisher(context, state_uri, False) + + state = network.StateMessage() + state.joint_state = JointState().Random("loopback", 7) + state.ee_state = CartesianState().Random("loopback_ee") + + while True: + network.send_state(state, publisher) + command = network.receive_command(subscriber) + if command: + print(command) + time.sleep(0.1) + + exit(0) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("state_uri", type=str, help="URI of the state socket") + parser.add_argument("command_uri", type=str, help="URI of the command socket") + args = parser.parse_args() + + main(args.state_uri, args.command_uri) diff --git a/python/scripts/zmq_loopback_state.py b/python/scripts/zmq_loopback_state.py new file mode 100644 index 0000000..5de87e0 --- /dev/null +++ b/python/scripts/zmq_loopback_state.py @@ -0,0 +1,36 @@ +import argparse +import time + +import zmq +from state_representation import JointState + +from network_interfaces.zmq import network + + +def main(state_uri, command_uri): + context = zmq.Context(1) + subscriber = network.configure_subscriber(context, state_uri, True) + publisher = network.configure_publisher(context, command_uri, True) + + command = network.CommandMessage() + + while True: + state = network.receive_state(subscriber) + if state: + print(state) + command.joint_state = JointState().Zero(state.joint_state.get_name(), state.joint_state.get_names()) + command.joint_state.set_positions(state.joint_state.get_positions()) + command.control_type = [3] + network.send_command(command, publisher) + time.sleep(0.1) + + exit(0) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("state_uri", type=str, help="URI of the state socket") + parser.add_argument("command_uri", type=str, help="URI of the command socket") + args = parser.parse_args() + + main(args.state_uri, args.command_uri) diff --git a/python/setup.py b/python/setup.py index 78b5d82..b7e9a24 100644 --- a/python/setup.py +++ b/python/setup.py @@ -2,8 +2,10 @@ setuptools.setup( name="network_interfaces", - version="0.1.0", + version="0.2.0", description="This package implements network interfaces of AICA", + maintainer="Dominic Reber", + maintainer_email="dominic@aica.tech", url="https://github.com/aica-technology/network-interfaces", packages=setuptools.find_packages(), install_requires=[ diff --git a/python/network_interfaces/tests/test_communication.py b/python/test/test_zmq_communication.py similarity index 98% rename from python/network_interfaces/tests/test_communication.py rename to python/test/test_zmq_communication.py index 3935730..5d4011c 100644 --- a/python/network_interfaces/tests/test_communication.py +++ b/python/test/test_zmq_communication.py @@ -10,7 +10,7 @@ from network_interfaces.zmq import network -class TestNetworkInterface(unittest.TestCase): +class TestZMQNetworkInterface(unittest.TestCase): robot_state = None robot_joint_state = None robot_jacobian = None