From 80720ad58856c2de0d22c5e7bb9c75689194e064 Mon Sep 17 00:00:00 2001 From: Miekale Date: Wed, 12 Mar 2025 15:04:08 +0000 Subject: [PATCH 01/36] creating can package: debugging docker up --- docker/interfacing/interfacing.Dockerfile | 49 ++++++++++++ modules/docker-compose.interfacing.yaml | 15 ++++ .../interfacing/can/README.md | 0 src/interfacing/can/config/params.yaml | 9 +++ src/interfacing/can/launch/can.launch.py | 39 +++++++++ src/interfacing/can/package.xml | 18 +++++ src/interfacing/can/producer/__init__.py | 0 src/interfacing/can/producer/can_core.py | 35 ++++++++ src/interfacing/can/producer/can_node.py | 79 +++++++++++++++++++ src/interfacing/can/resource/can | 0 src/interfacing/can/setup.cfg | 4 + src/interfacing/can/setup.py | 34 ++++++++ src/interfacing/can/test/test_can.py | 23 ++++++ src/interfacing/can/test/test_copyright.py | 23 ++++++ src/interfacing/can/test/test_flake8.py | 25 ++++++ src/interfacing/can/test/test_pep257.py | 23 ++++++ watod_scripts/watod-setup-docker-env.sh | 2 + 17 files changed, 378 insertions(+) create mode 100644 docker/interfacing/interfacing.Dockerfile create mode 100644 modules/docker-compose.interfacing.yaml rename modules_future/docker-compose.interfacing.yaml => src/interfacing/can/README.md (100%) create mode 100644 src/interfacing/can/config/params.yaml create mode 100755 src/interfacing/can/launch/can.launch.py create mode 100644 src/interfacing/can/package.xml create mode 100755 src/interfacing/can/producer/__init__.py create mode 100644 src/interfacing/can/producer/can_core.py create mode 100755 src/interfacing/can/producer/can_node.py create mode 100644 src/interfacing/can/resource/can create mode 100644 src/interfacing/can/setup.cfg create mode 100755 src/interfacing/can/setup.py create mode 100644 src/interfacing/can/test/test_can.py create mode 100755 src/interfacing/can/test/test_copyright.py create mode 100755 src/interfacing/can/test/test_flake8.py create mode 100755 src/interfacing/can/test/test_pep257.py diff --git a/docker/interfacing/interfacing.Dockerfile b/docker/interfacing/interfacing.Dockerfile new file mode 100644 index 0000000..553ac42 --- /dev/null +++ b/docker/interfacing/interfacing.Dockerfile @@ -0,0 +1,49 @@ +ARG BASE_IMAGE=ghcr.io/watonomous/robot_base/base:humble-ubuntu22.04 + +################################ Source ################################ +FROM ${BASE_IMAGE} AS source + +WORKDIR ${AMENT_WS}/src + +# Copy in source code +COPY src/interfacing/can can +COPY src/wato_msgs/sample_msgs sample_msgs + +# Scan for rosdeps +RUN apt-get -qq update && rosdep update && \ + rosdep install --from-paths . --ignore-src -r -s \ + | grep 'apt-get install' \ + | awk '{print $3}' \ + | sort > /tmp/colcon_install_list + +################################# Dependencies ################################ +FROM ${BASE_IMAGE} AS dependencies + +# Install Rosdep requirements +COPY --from=source /tmp/colcon_install_list /tmp/colcon_install_list +RUN apt-fast install -qq -y --no-install-recommends $(cat /tmp/colcon_install_list) + +# Copy in source code from source stage +WORKDIR ${AMENT_WS} +COPY --from=source ${AMENT_WS}/src src + +# Dependency Cleanup +WORKDIR / +RUN apt-get -qq autoremove -y && apt-get -qq autoclean && apt-get -qq clean && \ + rm -rf /root/* /root/.ros /tmp/* /var/lib/apt/lists/* /usr/share/doc/* + +################################ Build ################################ +FROM dependencies AS build + +# Build ROS2 packages +WORKDIR ${AMENT_WS} +RUN . /opt/ros/$ROS_DISTRO/setup.sh && \ + colcon build \ + --cmake-args -DCMAKE_BUILD_TYPE=Release --install-base ${WATONOMOUS_INSTALL} + +# Source and Build Artifact Cleanup +RUN rm -rf src/* build/* devel/* install/* log/* + +# Entrypoint will run before any CMD on launch. Sources ~/opt//setup.bash and ~/ament_ws/install/setup.bash +COPY docker/wato_ros_entrypoint.sh ${AMENT_WS}/wato_ros_entrypoint.sh +ENTRYPOINT ["./wato_ros_entrypoint.sh"] diff --git a/modules/docker-compose.interfacing.yaml b/modules/docker-compose.interfacing.yaml new file mode 100644 index 0000000..a572257 --- /dev/null +++ b/modules/docker-compose.interfacing.yaml @@ -0,0 +1,15 @@ +version: "3.8" + +services: + interfacing: + build: &interfacing + context: .. + dockerfile: docker/interfacing/interfacing.Dockerfile + cache_from: + - "${INTERFACING_IMAGE:?}:${TAG}" + - "${INTERFACING_IMAGE:?}:main" + image: "${INTERFACING_IMAGE:?}:${TAG}" + profiles: [deploy, develop] + command: /bin/bash -c "ros2 launch interfacing interfacing.launch.py" + volumes: + - ${MONO_DIR}/src/interfacing:/root/ament_ws/src/interfacing diff --git a/modules_future/docker-compose.interfacing.yaml b/src/interfacing/can/README.md similarity index 100% rename from modules_future/docker-compose.interfacing.yaml rename to src/interfacing/can/README.md diff --git a/src/interfacing/can/config/params.yaml b/src/interfacing/can/config/params.yaml new file mode 100644 index 0000000..cca06ec --- /dev/null +++ b/src/interfacing/can/config/params.yaml @@ -0,0 +1,9 @@ +# more info on YAML configs for ROS2 Params: +# https://roboticsbackend.com/ros2-yaml-params/ +can_node: + ros__parameters: + pos_x: 1.0 + pos_y: 1.0 + pos_z: 1.0 + + velocity: 1.0 \ No newline at end of file diff --git a/src/interfacing/can/launch/can.launch.py b/src/interfacing/can/launch/can.launch.py new file mode 100755 index 0000000..7ccb921 --- /dev/null +++ b/src/interfacing/can/launch/can.launch.py @@ -0,0 +1,39 @@ +# Copyright 2023 WATonomous +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from ament_index_python.packages import get_package_share_directory + +from launch import LaunchDescription +from launch_ros.actions import Node + + +def generate_launch_description(): + # To load the yaml file, we are searching for its + # path is the share directory. Check setup.py for how + # the param file got there + param_file_path = os.path.join( + get_package_share_directory('can'), + 'config', + 'params.yaml' + ) + + return LaunchDescription([ + Node( + package='can', + name='can_node', + executable='can_node', + parameters=[param_file_path] + ) + ]) diff --git a/src/interfacing/can/package.xml b/src/interfacing/can/package.xml new file mode 100644 index 0000000..14e0154 --- /dev/null +++ b/src/interfacing/can/package.xml @@ -0,0 +1,18 @@ + + + + can + 0.0.0 + can communication layer + eddyzhou + Apache2.0: License declaration + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/src/interfacing/can/producer/__init__.py b/src/interfacing/can/producer/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/src/interfacing/can/producer/can_core.py b/src/interfacing/can/producer/can_core.py new file mode 100644 index 0000000..ee433e8 --- /dev/null +++ b/src/interfacing/can/producer/can_core.py @@ -0,0 +1,35 @@ +# Copyright 2023 WATonomous +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + + +class CanCore(): + + def __init__(self, pos_x, pos_y, pos_z, vel): + # Init member variables for serialization + self.__pos_x = pos_x + self.__pos_y = pos_y + self.__pos_z = pos_z + self.__velocity = vel + + def update_position(self): + # velocity in 3D delta_x = delta_y = delta_z + self.__pos_x += self.__velocity / math.sqrt(3) + self.__pos_y += self.__velocity / math.sqrt(3) + self.__pos_z += self.__velocity / math.sqrt(3) + + def serialize_data(self): + return "x:" + str(self.__pos_x) + ";y:" + \ + str(self.__pos_y) + ";z:" + str(self.__pos_z) + ";" diff --git a/src/interfacing/can/producer/can_node.py b/src/interfacing/can/producer/can_node.py new file mode 100755 index 0000000..533d1fb --- /dev/null +++ b/src/interfacing/can/producer/can_node.py @@ -0,0 +1,79 @@ +# Copyright 2023 WATonomous +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import rclpy +from rclpy.node import Node + +from sample_msgs.msg import Unfiltered +from can.can_core import CanCore + + +class CanNode(Node): + + def __init__(self): + super().__init__('python_can') + # Declare and get the parameters + self.declare_parameter('pos_x', 0.0) + self.declare_parameter('pos_y', 0.0) + self.declare_parameter('pos_z', 0.0) + self.declare_parameter('velocity', 0.0) + + # For parameters, we need to explicitely declare its type for Python to know + # what to do with it + pos_x = self.get_parameter('pos_x').get_parameter_value().double_value + pos_y = self.get_parameter('pos_y').get_parameter_value().double_value + pos_z = self.get_parameter('pos_z').get_parameter_value().double_value + velocity = self.get_parameter('velocity').get_parameter_value().double_value + + # Initialize can core logic for serialization + self.__can = CanCore(pos_x, pos_y, pos_z, velocity) + + # Initialize ROS2 constructs + queue_size = 10 + self.publisher_ = self.create_publisher(Unfiltered, '/unfiltered_topic', queue_size) + + timer_period = 0.5 + self.timer = self.create_timer(timer_period, self.__publish_position) + + def __publish_position(self): + self.__can.update_position() + msg = Unfiltered() + + msg.data = self.__can.serialize_data() + msg.valid = True + msg.timestamp = int(time.time() * 1000) + + self.get_logger().info(f'Publishing: {msg.data}') + + self.publisher_.publish(msg) + + +def main(args=None): + rclpy.init(args=args) + + python_can = CanNode() + + rclpy.spin(python_can) + + # Destroy the node explicitly + # (optional - otherwise it will be done automatically + # when the garbage collector destroys the node object) + python_can.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() diff --git a/src/interfacing/can/resource/can b/src/interfacing/can/resource/can new file mode 100644 index 0000000..e69de29 diff --git a/src/interfacing/can/setup.cfg b/src/interfacing/can/setup.cfg new file mode 100644 index 0000000..6c9e367 --- /dev/null +++ b/src/interfacing/can/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/can +[install] +install-scripts=$base/lib/can diff --git a/src/interfacing/can/setup.py b/src/interfacing/can/setup.py new file mode 100755 index 0000000..c3f7dac --- /dev/null +++ b/src/interfacing/can/setup.py @@ -0,0 +1,34 @@ +import os +from glob import glob +from setuptools import setup + +package_name = 'can' + +setup( + name=package_name, + version='0.0.0', + packages=[package_name], + data_files=[ + # Install marker file in the package index + ('share/ament_index/resource_index/packages', ['resource/' + package_name]), + # Include our package.xml file + (os.path.join('share', package_name), ['package.xml']), + # Include all launch files + (os.path.join('share', package_name, 'launch'), + glob(os.path.join('launch', '*.launch.py'))), + # Include config files for parameters + (os.path.join('share', package_name, 'config'), glob(os.path.join('config', '*.yaml'))), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='eddyzhou, aryanafrouzi', + maintainer_email='e23zhou@watonomous.ca, aafrouzi@watonomous.ca', + description='TODO: Package description', + license='TODO: License declaration', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'can_node = can.can_node:main' + ], + }, +) diff --git a/src/interfacing/can/test/test_can.py b/src/interfacing/can/test/test_can.py new file mode 100644 index 0000000..7252792 --- /dev/null +++ b/src/interfacing/can/test/test_can.py @@ -0,0 +1,23 @@ +# Copyright 2023 WATonomous +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from can.can_core import CanCore + + +# def test_update_position(): +# can_core = CanCore(1, 1, 1, 1) +# can_core.update_position() + +# assert can_core.serialize_data() == \ +# "x:1.5773502691896257;y:1.5773502691896257;z:1.5773502691896257;" diff --git a/src/interfacing/can/test/test_copyright.py b/src/interfacing/can/test/test_copyright.py new file mode 100755 index 0000000..cc8ff03 --- /dev/null +++ b/src/interfacing/can/test/test_copyright.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_copyright.main import main +import pytest + + +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/src/interfacing/can/test/test_flake8.py b/src/interfacing/can/test/test_flake8.py new file mode 100755 index 0000000..27ee107 --- /dev/null +++ b/src/interfacing/can/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/src/interfacing/can/test/test_pep257.py b/src/interfacing/can/test/test_pep257.py new file mode 100755 index 0000000..b234a38 --- /dev/null +++ b/src/interfacing/can/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings' diff --git a/watod_scripts/watod-setup-docker-env.sh b/watod_scripts/watod-setup-docker-env.sh index 0db3992..cc76050 100644 --- a/watod_scripts/watod-setup-docker-env.sh +++ b/watod_scripts/watod-setup-docker-env.sh @@ -54,6 +54,7 @@ SAMPLES_TRANSFORMER_IMAGE=${SAMPLES_TRANSFORMER_IMAGE:-"$REGISTRY_URL/samples/sa # Images INFRASTRUCTURE_FOXGLOVE_IMAGE=${INFRASTRUCTURE_FOXGLOVE_IMAGE:-"$REGISTRY_URL/infrastructure/foxglove"} +INTERFACING_IMAGE=${INTERFACING_IMAGE:-"$REGISTRY_URL/interfacing"} ## --------------------------- Ports ------------------------------ @@ -87,3 +88,4 @@ echo "SAMPLES_TRANSFORMER_IMAGE=$SAMPLES_TRANSFORMER_IMAGE" >> "$MODULES_DIR/.en # Images echo "INFRASTRUCTURE_FOXGLOVE_IMAGE=$INFRASTRUCTURE_FOXGLOVE_IMAGE" >> "$MODULES_DIR/.env" +echo "INTERFACING_IMAGE=$INTERFACING_IMAGE" >> "$MODULES_DIR/.env" From fa96f7ebefbcd79d5b23516d03d80fbe21b946e3 Mon Sep 17 00:00:00 2001 From: Miekale Date: Wed, 12 Mar 2025 15:31:19 +0000 Subject: [PATCH 02/36] interfacing docker workind --- modules/docker-compose.interfacing.yaml | 2 +- .../can/{producer => can}/__init__.py | 0 .../can/{producer => can}/can_core.py | 11 +-- src/interfacing/can/can/can_node.py | 34 ++++++++ src/interfacing/can/package.xml | 2 +- src/interfacing/can/producer/can_node.py | 79 ------------------- src/interfacing/can/setup.py | 4 +- 7 files changed, 39 insertions(+), 93 deletions(-) rename src/interfacing/can/{producer => can}/__init__.py (100%) rename src/interfacing/can/{producer => can}/can_core.py (69%) create mode 100755 src/interfacing/can/can/can_node.py delete mode 100755 src/interfacing/can/producer/can_node.py diff --git a/modules/docker-compose.interfacing.yaml b/modules/docker-compose.interfacing.yaml index a572257..b7c71f0 100644 --- a/modules/docker-compose.interfacing.yaml +++ b/modules/docker-compose.interfacing.yaml @@ -10,6 +10,6 @@ services: - "${INTERFACING_IMAGE:?}:main" image: "${INTERFACING_IMAGE:?}:${TAG}" profiles: [deploy, develop] - command: /bin/bash -c "ros2 launch interfacing interfacing.launch.py" + command: /bin/bash -c "ros2 launch can can.launch.py" volumes: - ${MONO_DIR}/src/interfacing:/root/ament_ws/src/interfacing diff --git a/src/interfacing/can/producer/__init__.py b/src/interfacing/can/can/__init__.py similarity index 100% rename from src/interfacing/can/producer/__init__.py rename to src/interfacing/can/can/__init__.py diff --git a/src/interfacing/can/producer/can_core.py b/src/interfacing/can/can/can_core.py similarity index 69% rename from src/interfacing/can/producer/can_core.py rename to src/interfacing/can/can/can_core.py index ee433e8..a9be87e 100644 --- a/src/interfacing/can/producer/can_core.py +++ b/src/interfacing/can/can/can_core.py @@ -19,16 +19,7 @@ class CanCore(): def __init__(self, pos_x, pos_y, pos_z, vel): # Init member variables for serialization - self.__pos_x = pos_x - self.__pos_y = pos_y - self.__pos_z = pos_z - self.__velocity = vel - - def update_position(self): - # velocity in 3D delta_x = delta_y = delta_z - self.__pos_x += self.__velocity / math.sqrt(3) - self.__pos_y += self.__velocity / math.sqrt(3) - self.__pos_z += self.__velocity / math.sqrt(3) + print("test") def serialize_data(self): return "x:" + str(self.__pos_x) + ";y:" + \ diff --git a/src/interfacing/can/can/can_node.py b/src/interfacing/can/can/can_node.py new file mode 100755 index 0000000..89281db --- /dev/null +++ b/src/interfacing/can/can/can_node.py @@ -0,0 +1,34 @@ +# Copyright 2023 WATonomous +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + +import rclpy +from rclpy.node import Node + +from sample_msgs.msg import Unfiltered +from can.can_core import CanCore + + +class CanNode(Node): + def __init__(self): + print("test") + def publish_position(self): + print("test") + +def main(args=None): + print("hello") + +if __name__ == '__main__': + main() diff --git a/src/interfacing/can/package.xml b/src/interfacing/can/package.xml index 14e0154..174962d 100644 --- a/src/interfacing/can/package.xml +++ b/src/interfacing/can/package.xml @@ -4,7 +4,7 @@ can 0.0.0 can communication layer - eddyzhou + miekale Apache2.0: License declaration ament_copyright diff --git a/src/interfacing/can/producer/can_node.py b/src/interfacing/can/producer/can_node.py deleted file mode 100755 index 533d1fb..0000000 --- a/src/interfacing/can/producer/can_node.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2023 WATonomous -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import rclpy -from rclpy.node import Node - -from sample_msgs.msg import Unfiltered -from can.can_core import CanCore - - -class CanNode(Node): - - def __init__(self): - super().__init__('python_can') - # Declare and get the parameters - self.declare_parameter('pos_x', 0.0) - self.declare_parameter('pos_y', 0.0) - self.declare_parameter('pos_z', 0.0) - self.declare_parameter('velocity', 0.0) - - # For parameters, we need to explicitely declare its type for Python to know - # what to do with it - pos_x = self.get_parameter('pos_x').get_parameter_value().double_value - pos_y = self.get_parameter('pos_y').get_parameter_value().double_value - pos_z = self.get_parameter('pos_z').get_parameter_value().double_value - velocity = self.get_parameter('velocity').get_parameter_value().double_value - - # Initialize can core logic for serialization - self.__can = CanCore(pos_x, pos_y, pos_z, velocity) - - # Initialize ROS2 constructs - queue_size = 10 - self.publisher_ = self.create_publisher(Unfiltered, '/unfiltered_topic', queue_size) - - timer_period = 0.5 - self.timer = self.create_timer(timer_period, self.__publish_position) - - def __publish_position(self): - self.__can.update_position() - msg = Unfiltered() - - msg.data = self.__can.serialize_data() - msg.valid = True - msg.timestamp = int(time.time() * 1000) - - self.get_logger().info(f'Publishing: {msg.data}') - - self.publisher_.publish(msg) - - -def main(args=None): - rclpy.init(args=args) - - python_can = CanNode() - - rclpy.spin(python_can) - - # Destroy the node explicitly - # (optional - otherwise it will be done automatically - # when the garbage collector destroys the node object) - python_can.destroy_node() - rclpy.shutdown() - - -if __name__ == '__main__': - main() diff --git a/src/interfacing/can/setup.py b/src/interfacing/can/setup.py index c3f7dac..059a41d 100755 --- a/src/interfacing/can/setup.py +++ b/src/interfacing/can/setup.py @@ -21,8 +21,8 @@ ], install_requires=['setuptools'], zip_safe=True, - maintainer='eddyzhou, aryanafrouzi', - maintainer_email='e23zhou@watonomous.ca, aafrouzi@watonomous.ca', + maintainer='miekale', + maintainer_email='miekale.smith@uwaterloo.ca', description='TODO: Package description', license='TODO: License declaration', tests_require=['pytest'], From 627b418f0d1a474230e232990863262392e7a627 Mon Sep 17 00:00:00 2001 From: Miekale <125765041+Miekale@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:31:39 -0400 Subject: [PATCH 03/36] updating arch (#5) --- docs/Architecture_Map.odg | Bin 29050 -> 33422 bytes docs/Architecture_Map.pdf | Bin 64457 -> 32941 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/Architecture_Map.odg b/docs/Architecture_Map.odg index b31782d374420d8cb5c75fbc5c01f346bc50e1f1..2b2dc7b75a8b5075c65e317ea7602476d1b46a37 100644 GIT binary patch literal 33422 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μQpJU@|^)n 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 literal 29050 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�GqD)z^m;R}xgVP_#^W^S?wY5$ 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 e#%}23% Date: Sun, 20 Apr 2025 12:29:52 -0400 Subject: [PATCH 04/36] adding doc comment --- docs/Architecture_Map.odg | Bin 33422 -> 33338 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/Architecture_Map.odg b/docs/Architecture_Map.odg index 2b2dc7b75a8b5075c65e317ea7602476d1b46a37..175b96c5b905d40560ca655be4b54fa889dd5e0d 100644 GIT binary patch delta 27245 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 AYybcN delta 27327 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 W5$~h(8lrjgB;wdHrZN9q@&5pn+q_Z$ From 105775434bb2b8f346df0c744bbb5ac6e58eb574 Mon Sep 17 00:00:00 2001 From: Miekale <125765041+Miekale@users.noreply.github.com> Date: Sun, 20 Apr 2025 13:40:52 -0400 Subject: [PATCH 05/36] Miekale embedded structure (#6) * updating arch * Adding embedded directory, changing src -> autonomy * repairing actions * merging to main * actions updates * fix: naming convention * Fix code style issues with Autopep8 * Fix code style issues with clang_format --------- Co-authored-by: WATonomousAdmin --- .../templates/check_src_changes/action.yml | 41 +++++++++------ .../check_src_changes/check_src_changes.sh | 15 +++++- .../docker_context/docker_context.sh | 12 +++-- .github/workflows/linting_auto.yml | 2 +- README.md | 8 ++- {src => autonomy}/samples/README.md | 0 .../samples/cpp/aggregator/CMakeLists.txt | 0 .../aggregator/include/aggregator_core.hpp | 18 +++---- .../aggregator/include/aggregator_node.hpp | 24 ++++----- .../aggregator/launch/aggregator.launch.py | 0 .../samples/cpp/aggregator/package.xml | 0 .../cpp/aggregator/src/aggregator_core.cpp | 42 +++++++++++++++ .../cpp/aggregator/src/aggregator_node.cpp | 41 +++++++++++++++ .../cpp/aggregator/test/aggregator_test.cpp | 17 +++--- .../samples/cpp/producer/CMakeLists.txt | 0 .../samples/cpp/producer/config/params.yaml | 0 .../cpp/producer/include/producer_core.hpp | 12 ++--- .../cpp/producer/include/producer_node.hpp | 17 +++--- .../cpp/producer/launch/producer.launch.py | 0 .../samples/cpp/producer/package.xml | 0 .../cpp/producer/src/producer_core.cpp | 26 ++++------ .../cpp/producer/src/producer_node.cpp | 40 +++++++------- .../cpp/producer/test/producer_test.cpp | 9 ++-- .../samples/cpp/transformer/CMakeLists.txt | 0 .../cpp/transformer/config/params.yaml | 0 .../transformer/include/transformer_core.hpp | 22 ++++---- .../transformer/include/transformer_node.hpp | 15 +++--- .../transformer/launch/transformer.launch.py | 0 .../samples/cpp/transformer/package.xml | 0 .../cpp/transformer/src/transformer_core.cpp | 46 +++++++--------- .../cpp/transformer/src/transformer_node.cpp | 25 ++++----- .../cpp/transformer/test/transformer_test.cpp | 48 ++++++++--------- .../python/aggregator/aggregator/__init__.py | 0 .../aggregator/aggregator/aggregator_core.py | 0 .../aggregator/aggregator/aggregator_node.py | 0 .../aggregator/launch/aggregator.launch.py | 0 .../samples/python/aggregator/package.xml | 0 .../python/aggregator/resource/aggregator | 0 .../samples/python/aggregator/setup.cfg | 0 .../samples/python/aggregator/setup.py | 2 +- .../python/aggregator/test/test_aggregator.py | 0 .../python/aggregator/test/test_copyright.py | 0 .../python/aggregator/test/test_flake8.py | 0 .../python/aggregator/test/test_pep257.py | 0 .../python/producer/config/params.yaml | 0 .../python/producer/launch/producer.launch.py | 0 .../samples/python/producer/package.xml | 0 .../python/producer/producer/__init__.py | 0 .../python/producer/producer/producer_core.py | 0 .../python/producer/producer/producer_node.py | 0 .../samples/python/producer/resource/producer | 0 .../samples/python/producer/setup.cfg | 0 .../samples/python/producer/setup.py | 0 .../python/producer/test/test_copyright.py | 0 .../python/producer/test/test_flake8.py | 0 .../python/producer/test/test_pep257.py | 0 .../python/producer/test/test_producer.py | 0 .../python/transformer/config/params.yaml | 0 .../transformer/launch/transformer.launch.py | 0 .../samples/python/transformer/package.xml | 0 .../python/transformer/resource/transformer | 0 .../samples/python/transformer/setup.cfg | 0 .../samples/python/transformer/setup.py | 0 .../python/transformer/test/test_copyright.py | 0 .../python/transformer/test/test_flake8.py | 0 .../python/transformer/test/test_pep257.py | 0 .../transformer/test/test_transformer.py | 0 .../transformer/transformer/__init__.py | 0 .../transformer/transformer_core.py | 0 .../transformer/transformer_node.py | 0 .../samples/sample_msgs/CMakeLists.txt | 0 .../samples/sample_msgs/msg/Filtered.msg | 0 .../samples/sample_msgs/msg/FilteredArray.msg | 0 .../samples/sample_msgs/msg/Metadata.msg | 0 .../samples/sample_msgs/msg/Unfiltered.msg | 0 .../samples/sample_msgs/package.xml | 0 {src => autonomy}/samples/samples_diagram.svg | 0 .../wato_msgs/sample_msgs/CMakeLists.txt | 0 .../wato_msgs/sample_msgs/msg/Filtered.msg | 0 .../sample_msgs/msg/FilteredArray.msg | 0 .../wato_msgs/sample_msgs/msg/Metadata.msg | 0 .../wato_msgs/sample_msgs/msg/Unfiltered.msg | 0 .../wato_msgs/sample_msgs/package.xml | 0 .../wato_asd_training_foxglove_config .json | 4 +- .../foxglove/foxglove.Dockerfile | 2 +- docker/samples/cpp_aggregator.Dockerfile | 4 +- docker/samples/cpp_producer.Dockerfile | 4 +- docker/samples/cpp_transformer.Dockerfile | 4 +- docker/samples/py_aggregator.Dockerfile | 4 +- docker/samples/py_producer.Dockerfile | 4 +- docker/samples/py_transformer.Dockerfile | 4 +- .../simulation/isaac_sim/isaac_sim.Dockerfile | 4 +- docs/Architecture_Map.odg | Bin 33338 -> 33558 bytes docs/Architecture_Map.pdf | Bin 32941 -> 33987 bytes embedded/README.md | 0 modules/docker-compose.samples.yaml | 24 ++++----- .../cpp/aggregator/src/aggregator_core.cpp | 47 ----------------- .../cpp/aggregator/src/aggregator_node.cpp | 49 ------------------ 98 files changed, 306 insertions(+), 330 deletions(-) rename {src => autonomy}/samples/README.md (100%) rename {src => autonomy}/samples/cpp/aggregator/CMakeLists.txt (100%) rename {src => autonomy}/samples/cpp/aggregator/include/aggregator_core.hpp (87%) rename {src => autonomy}/samples/cpp/aggregator/include/aggregator_node.hpp (78%) rename {src => autonomy}/samples/cpp/aggregator/launch/aggregator.launch.py (100%) rename {src => autonomy}/samples/cpp/aggregator/package.xml (100%) create mode 100644 autonomy/samples/cpp/aggregator/src/aggregator_core.cpp create mode 100644 autonomy/samples/cpp/aggregator/src/aggregator_node.cpp rename {src => autonomy}/samples/cpp/aggregator/test/aggregator_test.cpp (88%) rename {src => autonomy}/samples/cpp/producer/CMakeLists.txt (100%) rename {src => autonomy}/samples/cpp/producer/config/params.yaml (100%) rename {src => autonomy}/samples/cpp/producer/include/producer_core.hpp (90%) rename {src => autonomy}/samples/cpp/producer/include/producer_node.hpp (86%) rename {src => autonomy}/samples/cpp/producer/launch/producer.launch.py (100%) rename {src => autonomy}/samples/cpp/producer/package.xml (100%) rename {src => autonomy}/samples/cpp/producer/src/producer_core.cpp (54%) rename {src => autonomy}/samples/cpp/producer/src/producer_node.cpp (62%) rename {src => autonomy}/samples/cpp/producer/test/producer_test.cpp (87%) rename {src => autonomy}/samples/cpp/transformer/CMakeLists.txt (100%) rename {src => autonomy}/samples/cpp/transformer/config/params.yaml (100%) rename {src => autonomy}/samples/cpp/transformer/include/transformer_core.hpp (83%) rename {src => autonomy}/samples/cpp/transformer/include/transformer_node.hpp (86%) rename {src => autonomy}/samples/cpp/transformer/launch/transformer.launch.py (100%) rename {src => autonomy}/samples/cpp/transformer/package.xml (100%) rename {src => autonomy}/samples/cpp/transformer/src/transformer_core.cpp (64%) rename {src => autonomy}/samples/cpp/transformer/src/transformer_node.cpp (69%) rename {src => autonomy}/samples/cpp/transformer/test/transformer_test.cpp (68%) rename {src => autonomy}/samples/python/aggregator/aggregator/__init__.py (100%) rename {src => autonomy}/samples/python/aggregator/aggregator/aggregator_core.py (100%) rename {src => autonomy}/samples/python/aggregator/aggregator/aggregator_node.py (100%) rename {src => autonomy}/samples/python/aggregator/launch/aggregator.launch.py (100%) rename {src => autonomy}/samples/python/aggregator/package.xml (100%) rename {src => autonomy}/samples/python/aggregator/resource/aggregator (100%) rename {src => autonomy}/samples/python/aggregator/setup.cfg (100%) rename {src => autonomy}/samples/python/aggregator/setup.py (94%) rename {src => autonomy}/samples/python/aggregator/test/test_aggregator.py (100%) rename {src => autonomy}/samples/python/aggregator/test/test_copyright.py (100%) rename {src => autonomy}/samples/python/aggregator/test/test_flake8.py (100%) rename {src => autonomy}/samples/python/aggregator/test/test_pep257.py (100%) rename {src => autonomy}/samples/python/producer/config/params.yaml (100%) rename {src => autonomy}/samples/python/producer/launch/producer.launch.py (100%) rename {src => autonomy}/samples/python/producer/package.xml (100%) rename {src => autonomy}/samples/python/producer/producer/__init__.py (100%) rename {src => autonomy}/samples/python/producer/producer/producer_core.py (100%) rename {src => autonomy}/samples/python/producer/producer/producer_node.py (100%) rename {src => autonomy}/samples/python/producer/resource/producer (100%) rename {src => autonomy}/samples/python/producer/setup.cfg (100%) rename {src => autonomy}/samples/python/producer/setup.py (100%) rename {src => autonomy}/samples/python/producer/test/test_copyright.py (100%) rename {src => autonomy}/samples/python/producer/test/test_flake8.py (100%) rename {src => autonomy}/samples/python/producer/test/test_pep257.py (100%) rename {src => autonomy}/samples/python/producer/test/test_producer.py (100%) rename {src => autonomy}/samples/python/transformer/config/params.yaml (100%) rename {src => autonomy}/samples/python/transformer/launch/transformer.launch.py (100%) rename {src => autonomy}/samples/python/transformer/package.xml (100%) rename {src => autonomy}/samples/python/transformer/resource/transformer (100%) rename {src => autonomy}/samples/python/transformer/setup.cfg (100%) rename {src => autonomy}/samples/python/transformer/setup.py (100%) rename {src => autonomy}/samples/python/transformer/test/test_copyright.py (100%) rename {src => autonomy}/samples/python/transformer/test/test_flake8.py (100%) rename {src => autonomy}/samples/python/transformer/test/test_pep257.py (100%) rename {src => autonomy}/samples/python/transformer/test/test_transformer.py (100%) rename {src => autonomy}/samples/python/transformer/transformer/__init__.py (100%) rename {src => autonomy}/samples/python/transformer/transformer/transformer_core.py (100%) rename {src => autonomy}/samples/python/transformer/transformer/transformer_node.py (100%) rename {src => autonomy}/samples/sample_msgs/CMakeLists.txt (100%) rename {src => autonomy}/samples/sample_msgs/msg/Filtered.msg (100%) rename {src => autonomy}/samples/sample_msgs/msg/FilteredArray.msg (100%) rename {src => autonomy}/samples/sample_msgs/msg/Metadata.msg (100%) rename {src => autonomy}/samples/sample_msgs/msg/Unfiltered.msg (100%) rename {src => autonomy}/samples/sample_msgs/package.xml (100%) rename {src => autonomy}/samples/samples_diagram.svg (100%) rename {src => autonomy}/wato_msgs/sample_msgs/CMakeLists.txt (100%) rename {src => autonomy}/wato_msgs/sample_msgs/msg/Filtered.msg (100%) rename {src => autonomy}/wato_msgs/sample_msgs/msg/FilteredArray.msg (100%) rename {src => autonomy}/wato_msgs/sample_msgs/msg/Metadata.msg (100%) rename {src => autonomy}/wato_msgs/sample_msgs/msg/Unfiltered.msg (100%) rename {src => autonomy}/wato_msgs/sample_msgs/package.xml (100%) create mode 100644 embedded/README.md delete mode 100644 src/samples/cpp/aggregator/src/aggregator_core.cpp delete mode 100644 src/samples/cpp/aggregator/src/aggregator_node.cpp diff --git a/.github/templates/check_src_changes/action.yml b/.github/templates/check_src_changes/action.yml index fead96a..0d00a30 100644 --- a/.github/templates/check_src_changes/action.yml +++ b/.github/templates/check_src_changes/action.yml @@ -12,59 +12,68 @@ runs: with: fetch-depth: 0 - - name: Find changed files outside of src directory - id: changed-files-outside-src + - name: Find changed files outside of development folders + id: changed-files-outside-development uses: tj-actions/changed-files@v42 with: - files: '!src/**' + files: | + !autonomy/** + !embedded/** - - name: Find changed files inside src/wato_msgs folder + - name: Find changed files inside autonomy/wato_msgs folder id: changed-files-wato-msgs uses: tj-actions/changed-files@v42 with: - files: src/wato_msgs/** + files: autonomy/wato_msgs/** - - name: Find changed files inside src/controller folder + - name: Find changed files inside autonomy/controller folder id: changed-files-controller uses: tj-actions/changed-files@v42 with: - files: src/controller/** + files: autonomy/controller/** - - name: Get changed files inside src/interfacing folder + - name: Get changed files inside autonomy/interfacing folder id: changed-files-interfacing uses: tj-actions/changed-files@v42 with: - files: src/interfacing/** + files: autonomy/interfacing/** - - name: Get changed files inside src/perception folder + - name: Get changed files inside autonomy/perception folder id: changed-files-perception uses: tj-actions/changed-files@v42 with: - files: src/perception/** + files: autonomy/perception/** - - name: Get changed files inside src/samples folder + - name: Get changed files inside autonomy/samples folder id: changed-files-samples uses: tj-actions/changed-files@v42 with: - files: src/samples/** + files: autonomy/samples/** - - name: Get changed files inside src/simulation folder + - name: Get changed files inside autonomy/simulation folder id: changed-files-simulation uses: tj-actions/changed-files@v42 with: - files: src/simulation/** + files: autonomy/simulation/** + + - name: Find changed files inside embedded folder + id: changed-files-embedded + uses: tj-actions/changed-files@v42 + with: + files: embedded/** - name: Create list of changed modules id: output-changes env: INFRASTRUCTURE_CHANGED: > ${{ steps.changed-files-wato-msgs.outputs.any_changed == 'true' - || steps.changed-files-outside-src.outputs.any_changed == 'true' }} + || steps.changed-files-outside-development.outputs.any_changed == 'true' }} CONTROLLER_CHANGED: ${{ steps.changed-files-controller.outputs.any_changed }} INTERFACING_CHANGED: ${{ steps.changed-files-interfacing.outputs.any_changed }} PERCEPTION_CHANGED: ${{ steps.changed-files-perception.outputs.any_changed }} SAMPLES_CHANGED: ${{ steps.changed-files-samples.outputs.any_changed }} SIMULATION_CHANGED: ${{ steps.changed-files-simulation.outputs.any_changed }} + EMBEDDED_CHANGED: ${{ steps.changed-files-embedded.outputs.any_changed }} run: ${{ github.action_path }}/check_src_changes.sh shell: bash \ No newline at end of file diff --git a/.github/templates/check_src_changes/check_src_changes.sh b/.github/templates/check_src_changes/check_src_changes.sh index 6ad905f..d5f26c1 100755 --- a/.github/templates/check_src_changes/check_src_changes.sh +++ b/.github/templates/check_src_changes/check_src_changes.sh @@ -34,14 +34,27 @@ if [ $SIMULATION_CHANGED == 'true' ]; then echo "Detected simulation changes" MODIFIED_MODULES+="simulation " fi +# Embedded +if [ $EMBEDDED_CHANGED == 'true' ]; then + echo "::notice:: Detected infrastructure changes" + MODIFIED_MODULES+="embedded" +fi # Infrastructure if [ $INFRASTRUCTURE_CHANGED == 'true' ]; then echo "::notice:: Detected infrastructure changes" - MODIFIED_MODULES="infrastructure" + MODIFIED_MODULES+="infrastructure" +fi + +# Embedded +if [ $EMBEDDED_CHANGED == 'true' ]; then + echo "::notice:: Detected infrastructure changes" + MODIFIED_MODULES="embedded" else echo "::notice:: MODIFIED_MODULES are $MODIFIED_MODULES" fi + + # Output lis echo "modified_modules=$MODIFIED_MODULES" >> $GITHUB_OUTPUT \ No newline at end of file diff --git a/.github/templates/docker_context/docker_context.sh b/.github/templates/docker_context/docker_context.sh index 3239b46..30fcd57 100755 --- a/.github/templates/docker_context/docker_context.sh +++ b/.github/templates/docker_context/docker_context.sh @@ -13,7 +13,7 @@ json_objects=() # Check for infrastructure changes TEST_ALL=false -MODIFIED_MODULES="infrastructure" + if [[ $MODIFIED_MODULES = "infrastructure" ]]; then TEST_ALL=true echo Testing all "$MODULES_DIR" @@ -21,17 +21,22 @@ fi # Loop through each module while read -r module; do + # Retrieve docker compose service names services=$(docker compose -f "$module" --profile deploy --profile develop config --services) module_out=$(basename $(echo "$module" | sed -n 's/modules\/docker-compose\.\(.*\)\.yaml/\1/p')) + # Skip simulation module - if [[ 'simulation' = $module_out ]]; then + # TODO: Add custom handling for embedded testing + if [[ 'simulation' = $module_out || 'embedded' = $module_out ]]; then continue fi + # Only work with modules that are modified if [[ $MODIFIED_MODULES != *$module_out* && $TEST_ALL = "false" ]]; then continue fi + # Loop through each service while read -r service_out; do # Temporarily skip perception services that have too large image size @@ -39,7 +44,7 @@ while read -r module; do # [[ "$service_out" == "camera_object_detection" ]] || \ # continue # fi - # Construct JSON object for each service with module and service name + # TODO: Expose whole profile object to env dockerfile=$(docker compose -f "$module" --profile deploy --profile develop config | yq ".services.$service_out.build.dockerfile") json_object=$(jq -nc --arg module_out "$module_out" --arg service_out "$service_out" --arg dockerfile "$dockerfile" \ @@ -49,6 +54,7 @@ while read -r module; do done <<< "$services" done <<< "$modules" + # Convert the array of JSON objects to a single JSON array json_services=$(jq -nc '[( $ARGS.positional[] | fromjson )]' --args -- ${json_objects[*]}) diff --git a/.github/workflows/linting_auto.yml b/.github/workflows/linting_auto.yml index 0e2804d..c4a6700 100644 --- a/.github/workflows/linting_auto.yml +++ b/.github/workflows/linting_auto.yml @@ -26,7 +26,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v1 with: - python-version: 3.9.21 + python-version: 3.9.22 - name: Install Python dependencies run: pip install autopep8 clang-format diff --git a/README.md b/README.md index 12d77f5..0fe747b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ humanoid ├── modules │ └── docker-compose.samples.yaml ├── scripts -├── src +├── autonomy │ ├── perception │ ├── wato_msgs │ │ └── sample_msgs @@ -34,6 +34,10 @@ humanoid │ ├── controller │ ├── interfacing │ ├── simulation +├── embedded +│ ├── ESP32 +│ ├── STM32 +│ ├── torque-testing └── watod ``` ## Documentation @@ -43,7 +47,7 @@ Before developing please read these documents. 1. [Documentation Structure of Repo](docs/README.md) 2. [WATO Infrastructure Development Docs](https://github.com/WATonomous/wato_monorepo/tree/main/docs/dev/) -3. [ROS Node/Core Structure Docs](src/samples/README.md) +3. [ROS Node/Core Structure Docs](autonomy/samples/README.md) ## Dependencies: - Ubuntu >= 22.04, Windows (WSL), and MacOS. diff --git a/src/samples/README.md b/autonomy/samples/README.md similarity index 100% rename from src/samples/README.md rename to autonomy/samples/README.md diff --git a/src/samples/cpp/aggregator/CMakeLists.txt b/autonomy/samples/cpp/aggregator/CMakeLists.txt similarity index 100% rename from src/samples/cpp/aggregator/CMakeLists.txt rename to autonomy/samples/cpp/aggregator/CMakeLists.txt diff --git a/src/samples/cpp/aggregator/include/aggregator_core.hpp b/autonomy/samples/cpp/aggregator/include/aggregator_core.hpp similarity index 87% rename from src/samples/cpp/aggregator/include/aggregator_core.hpp rename to autonomy/samples/cpp/aggregator/include/aggregator_core.hpp index 7912f7d..9dcea65 100644 --- a/src/samples/cpp/aggregator/include/aggregator_core.hpp +++ b/autonomy/samples/cpp/aggregator/include/aggregator_core.hpp @@ -3,18 +3,16 @@ #include "rclcpp/rclcpp.hpp" -#include "sample_msgs/msg/unfiltered.hpp" #include "sample_msgs/msg/filtered_array.hpp" +#include "sample_msgs/msg/unfiltered.hpp" -namespace samples -{ +namespace samples { /** * Implementation of the internal logic used by the Aggregator Node to * calculate the operating frequency of topics. */ -class AggregatorCore -{ +class AggregatorCore { public: /** * Aggregator constructor. @@ -44,16 +42,14 @@ class AggregatorCore * * @param msg */ - void add_raw_msg( - const sample_msgs::msg::Unfiltered::SharedPtr msg); + void add_raw_msg(const sample_msgs::msg::Unfiltered::SharedPtr msg); /** * Used to keep track of latest timestamp and number of messages received * over the "filtered" topic. Should be called before filtered_frequency(). * * @param msg */ - void add_filtered_msg( - const sample_msgs::msg::FilteredArray::SharedPtr msg); + void add_filtered_msg(const sample_msgs::msg::FilteredArray::SharedPtr msg); private: // Number of message received on "unfiltered" and "filtered" topics. @@ -70,6 +66,6 @@ class AggregatorCore int64_t latest_filtered_time_; }; -} // namespace samples +} // namespace samples -#endif // AGGREGATOR_CORE_HPP_ +#endif // AGGREGATOR_CORE_HPP_ diff --git a/src/samples/cpp/aggregator/include/aggregator_node.hpp b/autonomy/samples/cpp/aggregator/include/aggregator_node.hpp similarity index 78% rename from src/samples/cpp/aggregator/include/aggregator_node.hpp rename to autonomy/samples/cpp/aggregator/include/aggregator_node.hpp index 0255fed..a273a2e 100644 --- a/src/samples/cpp/aggregator/include/aggregator_node.hpp +++ b/autonomy/samples/cpp/aggregator/include/aggregator_node.hpp @@ -3,8 +3,8 @@ #include "rclcpp/rclcpp.hpp" -#include "sample_msgs/msg/unfiltered.hpp" #include "sample_msgs/msg/filtered_array.hpp" +#include "sample_msgs/msg/unfiltered.hpp" #include "aggregator_core.hpp" @@ -12,8 +12,7 @@ * Implementation of a ROS2 node that listens to the "unfiltered" and "filtered" * topics and echoes the operating frequency of the topic to the console. */ -class AggregatorNode : public rclcpp::Node -{ +class AggregatorNode : public rclcpp::Node { public: // Configure pubsub nodes to keep last 20 messages. // https://docs.ros.org/en/foxy/Concepts/About-Quality-of-Service-Settings.html @@ -26,31 +25,32 @@ class AggregatorNode : public rclcpp::Node private: /** - * A ROS2 subscription node callback used to unpack raw data from the "unfiltered" - * topic and echo the operating frequency of the topic to the console. + * A ROS2 subscription node callback used to unpack raw data from the + * "unfiltered" topic and echo the operating frequency of the topic to the + * console. * * @param msg a raw message from the "unfiltered" topic */ - void unfiltered_callback( - const sample_msgs::msg::Unfiltered::SharedPtr msg); + void unfiltered_callback(const sample_msgs::msg::Unfiltered::SharedPtr msg); /** * A ROS2 subscription node callback used to unpack processed data from the - * "filtered" topic and echo the operating frequency of the topic to the console. + * "filtered" topic and echo the operating frequency of the topic to the + * console. * * @param msg a processed message from the "filtered" topic */ - void filtered_callback( - const sample_msgs::msg::FilteredArray::SharedPtr msg); + void filtered_callback(const sample_msgs::msg::FilteredArray::SharedPtr msg); // ROS2 subscriber listening to the unfiltered topic. rclcpp::Subscription::SharedPtr raw_sub_; // ROS2 subscriber listening to the filtered topic. - rclcpp::Subscription::SharedPtr filtered_sub_; + rclcpp::Subscription::SharedPtr + filtered_sub_; // Object containing methods to determine the operating frequency on topics. samples::AggregatorCore aggregator_; }; -#endif // AGGREGATOR_NODE_HPP_ +#endif // AGGREGATOR_NODE_HPP_ diff --git a/src/samples/cpp/aggregator/launch/aggregator.launch.py b/autonomy/samples/cpp/aggregator/launch/aggregator.launch.py similarity index 100% rename from src/samples/cpp/aggregator/launch/aggregator.launch.py rename to autonomy/samples/cpp/aggregator/launch/aggregator.launch.py diff --git a/src/samples/cpp/aggregator/package.xml b/autonomy/samples/cpp/aggregator/package.xml similarity index 100% rename from src/samples/cpp/aggregator/package.xml rename to autonomy/samples/cpp/aggregator/package.xml diff --git a/autonomy/samples/cpp/aggregator/src/aggregator_core.cpp b/autonomy/samples/cpp/aggregator/src/aggregator_core.cpp new file mode 100644 index 0000000..58f9e6a --- /dev/null +++ b/autonomy/samples/cpp/aggregator/src/aggregator_core.cpp @@ -0,0 +1,42 @@ +#include + +#include "aggregator_core.hpp" + +namespace samples { + +AggregatorCore::AggregatorCore(int64_t timestamp) + : raw_msg_count_(0), filtered_msg_count_(0), start_(timestamp), + latest_raw_time_(-1), latest_filtered_time_(-1) {} + +double AggregatorCore::raw_frequency() const { + if (latest_raw_time_ <= start_) { + return 0.0; + } + return static_cast(raw_msg_count_) / (latest_raw_time_ - start_); +} + +double AggregatorCore::filtered_frequency() const { + if (latest_filtered_time_ <= start_) { + return 0.0; + } + return static_cast(filtered_msg_count_) / + (latest_filtered_time_ - start_); +} + +void AggregatorCore::add_raw_msg( + const sample_msgs::msg::Unfiltered::SharedPtr msg) { + latest_raw_time_ = + std::max(static_cast(msg->timestamp), latest_raw_time_); + raw_msg_count_++; +} + +void AggregatorCore::add_filtered_msg( + const sample_msgs::msg::FilteredArray::SharedPtr msg) { + for (auto filtered_msg : msg->packets) { + latest_filtered_time_ = std::max( + static_cast(filtered_msg.timestamp), latest_filtered_time_); + } + filtered_msg_count_++; +} + +} // namespace samples diff --git a/autonomy/samples/cpp/aggregator/src/aggregator_node.cpp b/autonomy/samples/cpp/aggregator/src/aggregator_node.cpp new file mode 100644 index 0000000..8bf5b87 --- /dev/null +++ b/autonomy/samples/cpp/aggregator/src/aggregator_node.cpp @@ -0,0 +1,41 @@ +#include +#include + +#include "aggregator_node.hpp" + +AggregatorNode::AggregatorNode() + : Node("aggregator"), + aggregator_(samples::AggregatorCore( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count())) { + raw_sub_ = this->create_subscription( + "/unfiltered_topic", ADVERTISING_FREQ, + std::bind(&AggregatorNode::unfiltered_callback, this, + std::placeholders::_1)); + filtered_sub_ = this->create_subscription( + "/filtered_topic", ADVERTISING_FREQ, + std::bind(&AggregatorNode::filtered_callback, this, + std::placeholders::_1)); +} + +void AggregatorNode::unfiltered_callback( + const sample_msgs::msg::Unfiltered::SharedPtr msg) { + aggregator_.add_raw_msg(msg); + RCLCPP_INFO(this->get_logger(), "Raw Frequency(msg/s): %f", + aggregator_.raw_frequency() * 1000); +} + +void AggregatorNode::filtered_callback( + const sample_msgs::msg::FilteredArray::SharedPtr msg) { + aggregator_.add_filtered_msg(msg); + RCLCPP_INFO(this->get_logger(), "Filtered Frequency(msg/s): %f", + aggregator_.filtered_frequency() * 1000); +} + +int main(int argc, char **argv) { + rclcpp::init(argc, argv); + rclcpp::spin(std::make_shared()); + rclcpp::shutdown(); + return 0; +} diff --git a/src/samples/cpp/aggregator/test/aggregator_test.cpp b/autonomy/samples/cpp/aggregator/test/aggregator_test.cpp similarity index 88% rename from src/samples/cpp/aggregator/test/aggregator_test.cpp rename to autonomy/samples/cpp/aggregator/test/aggregator_test.cpp index a7add4b..3e5c2db 100644 --- a/src/samples/cpp/aggregator/test/aggregator_test.cpp +++ b/autonomy/samples/cpp/aggregator/test/aggregator_test.cpp @@ -1,13 +1,12 @@ #include #include -#include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" +#include "gtest/gtest.h" #include "aggregator_core.hpp" -TEST(AggregatorTest, RawDivisionByZero) -{ +TEST(AggregatorTest, RawDivisionByZero) { samples::AggregatorCore aggregator(0); auto msg = std::make_shared(); msg->timestamp = 0; @@ -16,8 +15,7 @@ TEST(AggregatorTest, RawDivisionByZero) EXPECT_DOUBLE_EQ(0.0, aggregator.raw_frequency()); } -TEST(AggregatorTest, FilteredDivisionByZero) -{ +TEST(AggregatorTest, FilteredDivisionByZero) { samples::AggregatorCore aggregator(1); auto filtered_packet = std::make_shared(); std::vector msgs; @@ -30,8 +28,7 @@ TEST(AggregatorTest, FilteredDivisionByZero) EXPECT_DOUBLE_EQ(0.0, aggregator.filtered_frequency()); } -TEST(AggregatorTest, RawFrequencyAddSingleMessage) -{ +TEST(AggregatorTest, RawFrequencyAddSingleMessage) { samples::AggregatorCore aggregator(0); auto msg = std::make_shared(); @@ -40,8 +37,7 @@ TEST(AggregatorTest, RawFrequencyAddSingleMessage) EXPECT_DOUBLE_EQ(0.5, aggregator.raw_frequency()); } -TEST(AggregatorTest, RawFrequencyAddMultipleMessages) -{ +TEST(AggregatorTest, RawFrequencyAddMultipleMessages) { samples::AggregatorCore aggregator(0); auto msg = std::make_shared(); @@ -58,8 +54,7 @@ TEST(AggregatorTest, RawFrequencyAddMultipleMessages) EXPECT_DOUBLE_EQ(0.6, aggregator.raw_frequency()); } -TEST(AggregatorTest, FilteredUnorderedTimestamps) -{ +TEST(AggregatorTest, FilteredUnorderedTimestamps) { samples::AggregatorCore aggregator(0); auto filtered_packet = std::make_shared(); std::vector msgs; diff --git a/src/samples/cpp/producer/CMakeLists.txt b/autonomy/samples/cpp/producer/CMakeLists.txt similarity index 100% rename from src/samples/cpp/producer/CMakeLists.txt rename to autonomy/samples/cpp/producer/CMakeLists.txt diff --git a/src/samples/cpp/producer/config/params.yaml b/autonomy/samples/cpp/producer/config/params.yaml similarity index 100% rename from src/samples/cpp/producer/config/params.yaml rename to autonomy/samples/cpp/producer/config/params.yaml diff --git a/src/samples/cpp/producer/include/producer_core.hpp b/autonomy/samples/cpp/producer/include/producer_core.hpp similarity index 90% rename from src/samples/cpp/producer/include/producer_core.hpp rename to autonomy/samples/cpp/producer/include/producer_core.hpp index 8a12649..806f08a 100644 --- a/src/samples/cpp/producer/include/producer_core.hpp +++ b/autonomy/samples/cpp/producer/include/producer_core.hpp @@ -3,15 +3,13 @@ #include "sample_msgs/msg/unfiltered.hpp" -namespace samples -{ +namespace samples { /** * Implementation of the internal logic used by the Producer Node to * serialize and update coordinates. */ -class ProducerCore -{ +class ProducerCore { public: /** * Producer constructor. @@ -51,7 +49,7 @@ class ProducerCore * * @param[out] msg an unfiltered message with empty data field */ - void serialize_coordinates(sample_msgs::msg::Unfiltered & msg) const; + void serialize_coordinates(sample_msgs::msg::Unfiltered &msg) const; private: // Coordinate values @@ -63,6 +61,6 @@ class ProducerCore double velocity_; }; -} // namespace samples +} // namespace samples -#endif // PRODUCER_CORE_HPP_ +#endif // PRODUCER_CORE_HPP_ diff --git a/src/samples/cpp/producer/include/producer_node.hpp b/autonomy/samples/cpp/producer/include/producer_node.hpp similarity index 86% rename from src/samples/cpp/producer/include/producer_node.hpp rename to autonomy/samples/cpp/producer/include/producer_node.hpp index a52ab95..7ad7807 100644 --- a/src/samples/cpp/producer/include/producer_node.hpp +++ b/autonomy/samples/cpp/producer/include/producer_node.hpp @@ -3,8 +3,8 @@ #include -#include "rclcpp/rclcpp.hpp" #include "rcl_interfaces/msg/set_parameters_result.hpp" +#include "rclcpp/rclcpp.hpp" #include "sample_msgs/msg/unfiltered.hpp" @@ -14,8 +14,7 @@ * Implementation of a ROS2 node that generates unfiltered ROS2 messages on a * time interval. */ -class ProducerNode : public rclcpp::Node -{ +class ProducerNode : public rclcpp::Node { public: // Configure pubsub nodes to keep last 20 messages. // https://docs.ros.org/en/foxy/Concepts/About-Quality-of-Service-Settings.html @@ -38,11 +37,12 @@ class ProducerNode : public rclcpp::Node /** * Callback used to dynamically update velocity data at runtime. * - * @param parameters list of parameters (only velocity in this case) that were modified + * @param parameters list of parameters (only velocity in this case) that were + * modified * @returns status message indicating whether update was successful */ - rcl_interfaces::msg::SetParametersResult parameters_callback( - const std::vector & parameters); + rcl_interfaces::msg::SetParametersResult + parameters_callback(const std::vector ¶meters); // ROS2 publisher sending raw messages to the unfiltered topic. rclcpp::Publisher::SharedPtr data_pub_; @@ -53,8 +53,9 @@ class ProducerNode : public rclcpp::Node // Callback to dynamically modify node parameters. rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr param_cb_; - // Producer implementation containing logic for coordinate serialization and management. + // Producer implementation containing logic for coordinate serialization and + // management. samples::ProducerCore producer_; }; -#endif // PRODUCER_NODE_HPP_ +#endif // PRODUCER_NODE_HPP_ diff --git a/src/samples/cpp/producer/launch/producer.launch.py b/autonomy/samples/cpp/producer/launch/producer.launch.py similarity index 100% rename from src/samples/cpp/producer/launch/producer.launch.py rename to autonomy/samples/cpp/producer/launch/producer.launch.py diff --git a/src/samples/cpp/producer/package.xml b/autonomy/samples/cpp/producer/package.xml similarity index 100% rename from src/samples/cpp/producer/package.xml rename to autonomy/samples/cpp/producer/package.xml diff --git a/src/samples/cpp/producer/src/producer_core.cpp b/autonomy/samples/cpp/producer/src/producer_core.cpp similarity index 54% rename from src/samples/cpp/producer/src/producer_core.cpp rename to autonomy/samples/cpp/producer/src/producer_core.cpp index ffa27d7..0b0a1db 100644 --- a/src/samples/cpp/producer/src/producer_core.cpp +++ b/autonomy/samples/cpp/producer/src/producer_core.cpp @@ -3,38 +3,30 @@ #include "producer_core.hpp" -namespace samples -{ +namespace samples { ProducerCore::ProducerCore(float x, float y, float z) -: pos_x_(x), pos_y_(y), pos_z_(z), velocity_(0) -{ -} + : pos_x_(x), pos_y_(y), pos_z_(z), velocity_(0) {} -void ProducerCore::update_velocity(int velocity) -{ - velocity_ = velocity; -} +void ProducerCore::update_velocity(int velocity) { velocity_ = velocity; } -void ProducerCore::update_position(double pos_x, double pos_y, double pos_z) -{ +void ProducerCore::update_position(double pos_x, double pos_y, double pos_z) { pos_x_ = pos_x; pos_y_ = pos_y; pos_z_ = pos_z; } -void ProducerCore::update_coordinates() -{ +void ProducerCore::update_coordinates() { pos_x_ += velocity_ / sqrt(3); pos_y_ += velocity_ / sqrt(3); pos_z_ += velocity_ / sqrt(3); } -void ProducerCore::serialize_coordinates(sample_msgs::msg::Unfiltered & msg) const -{ +void ProducerCore::serialize_coordinates( + sample_msgs::msg::Unfiltered &msg) const { msg.data = "x:" + std::to_string(pos_x_) + ";y:" + std::to_string(pos_y_) + - ";z:" + std::to_string(pos_z_) + ";"; + ";z:" + std::to_string(pos_z_) + ";"; msg.valid = true; } -} // namespace samples +} // namespace samples diff --git a/src/samples/cpp/producer/src/producer_node.cpp b/autonomy/samples/cpp/producer/src/producer_node.cpp similarity index 62% rename from src/samples/cpp/producer/src/producer_node.cpp rename to autonomy/samples/cpp/producer/src/producer_node.cpp index b313eca..e0a46c3 100644 --- a/src/samples/cpp/producer/src/producer_node.cpp +++ b/autonomy/samples/cpp/producer/src/producer_node.cpp @@ -5,14 +5,13 @@ #include "producer_node.hpp" ProducerNode::ProducerNode(int delay_ms) -: Node("producer"), producer_(samples::ProducerCore()) -{ - data_pub_ = - this->create_publisher("/unfiltered_topic", ADVERTISING_FREQ); + : Node("producer"), producer_(samples::ProducerCore()) { + data_pub_ = this->create_publisher( + "/unfiltered_topic", ADVERTISING_FREQ); - timer_ = this->create_wall_timer( - std::chrono::milliseconds(delay_ms), - std::bind(&ProducerNode::timer_callback, this)); + timer_ = + this->create_wall_timer(std::chrono::milliseconds(delay_ms), + std::bind(&ProducerNode::timer_callback, this)); // Define the default values for parameters if not defined in params.yaml this->declare_parameter("pos_x", 0.0); @@ -25,20 +24,21 @@ ProducerNode::ProducerNode(int delay_ms) rclcpp::Parameter pos_z = this->get_parameter("pos_z"); rclcpp::Parameter velocity = this->get_parameter("velocity"); - producer_.update_position(pos_x.as_double(), pos_y.as_double(), pos_z.as_double()); + producer_.update_position(pos_x.as_double(), pos_y.as_double(), + pos_z.as_double()); producer_.update_velocity(velocity.as_double()); - param_cb_ = this->add_on_set_parameters_callback( - std::bind(&ProducerNode::parameters_callback, this, std::placeholders::_1)); + param_cb_ = this->add_on_set_parameters_callback(std::bind( + &ProducerNode::parameters_callback, this, std::placeholders::_1)); } -void ProducerNode::timer_callback() -{ +void ProducerNode::timer_callback() { producer_.update_coordinates(); auto msg = sample_msgs::msg::Unfiltered(); msg.timestamp = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); + std::chrono::system_clock::now().time_since_epoch()) + .count(); producer_.serialize_coordinates(msg); RCLCPP_INFO(this->get_logger(), "Publishing: %s", msg.data.c_str()); @@ -46,26 +46,24 @@ void ProducerNode::timer_callback() } rcl_interfaces::msg::SetParametersResult ProducerNode::parameters_callback( - const std::vector & parameters) -{ + const std::vector ¶meters) { rcl_interfaces::msg::SetParametersResult result; result.successful = false; result.reason = ""; - for (const auto & parameter : parameters) { + for (const auto ¶meter : parameters) { if (parameter.get_name() == "velocity" && - parameter.get_type() == rclcpp::ParameterType::PARAMETER_INTEGER) - { + parameter.get_type() == rclcpp::ParameterType::PARAMETER_INTEGER) { producer_.update_velocity(parameter.as_int()); - RCLCPP_INFO(this->get_logger(), "Velocity successfully set to %d", parameter.as_int()); + RCLCPP_INFO(this->get_logger(), "Velocity successfully set to %d", + parameter.as_int()); result.successful = true; } } return result; } -int main(int argc, char ** argv) -{ +int main(int argc, char **argv) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared(500)); rclcpp::shutdown(); diff --git a/src/samples/cpp/producer/test/producer_test.cpp b/autonomy/samples/cpp/producer/test/producer_test.cpp similarity index 87% rename from src/samples/cpp/producer/test/producer_test.cpp rename to autonomy/samples/cpp/producer/test/producer_test.cpp index fd59440..6dac461 100644 --- a/src/samples/cpp/producer/test/producer_test.cpp +++ b/autonomy/samples/cpp/producer/test/producer_test.cpp @@ -2,8 +2,7 @@ #include "producer_core.hpp" -TEST(ProducerTest, ValidSerialization) -{ +TEST(ProducerTest, ValidSerialization) { auto producer = samples::ProducerCore(11, -12, 0); auto msg = sample_msgs::msg::Unfiltered(); producer.serialize_coordinates(msg); @@ -12,8 +11,7 @@ TEST(ProducerTest, ValidSerialization) EXPECT_EQ(msg.data, "x:11.000000;y:-12.000000;z:0.000000;"); } -TEST(ProducerTest, NoCoordinateUpdate) -{ +TEST(ProducerTest, NoCoordinateUpdate) { auto producer = samples::ProducerCore(); producer.update_coordinates(); @@ -22,8 +20,7 @@ TEST(ProducerTest, NoCoordinateUpdate) EXPECT_EQ(msg.data, "x:0.000000;y:0.000000;z:0.000000;"); } -TEST(ProducerTest, MultipleCoordinateUpdate) -{ +TEST(ProducerTest, MultipleCoordinateUpdate) { auto producer = samples::ProducerCore(); auto msg = sample_msgs::msg::Unfiltered(); diff --git a/src/samples/cpp/transformer/CMakeLists.txt b/autonomy/samples/cpp/transformer/CMakeLists.txt similarity index 100% rename from src/samples/cpp/transformer/CMakeLists.txt rename to autonomy/samples/cpp/transformer/CMakeLists.txt diff --git a/src/samples/cpp/transformer/config/params.yaml b/autonomy/samples/cpp/transformer/config/params.yaml similarity index 100% rename from src/samples/cpp/transformer/config/params.yaml rename to autonomy/samples/cpp/transformer/config/params.yaml diff --git a/src/samples/cpp/transformer/include/transformer_core.hpp b/autonomy/samples/cpp/transformer/include/transformer_core.hpp similarity index 83% rename from src/samples/cpp/transformer/include/transformer_core.hpp rename to autonomy/samples/cpp/transformer/include/transformer_core.hpp index 8bb2d38..1c97418 100644 --- a/src/samples/cpp/transformer/include/transformer_core.hpp +++ b/autonomy/samples/cpp/transformer/include/transformer_core.hpp @@ -3,18 +3,16 @@ #include -#include "sample_msgs/msg/unfiltered.hpp" #include "sample_msgs/msg/filtered.hpp" +#include "sample_msgs/msg/unfiltered.hpp" -namespace samples -{ +namespace samples { /** * Implementation for the internal logic for the Transformer ROS2 * node performing data processing and validation. */ -class TransformerCore -{ +class TransformerCore { public: // Size of buffer before processed messages are published. static constexpr int BUFFER_CAPACITY = 10; @@ -45,8 +43,8 @@ class TransformerCore * @param unfiltered a raw message * @returns whether message's 'valid' field is set */ - bool validate_message( - const sample_msgs::msg::Unfiltered::SharedPtr unfiltered); + bool + validate_message(const sample_msgs::msg::Unfiltered::SharedPtr unfiltered); /** * Enqueue message into an array of processed messages to "filtered" topic. @@ -55,7 +53,7 @@ class TransformerCore * @param msg a processed message to be published * @returns whether buffer is full after adding new message */ - bool enqueue_message(const sample_msgs::msg::Filtered & msg); + bool enqueue_message(const sample_msgs::msg::Filtered &msg); /** * Deserializes the data field of the unfiltered ROS2 message. @@ -66,8 +64,8 @@ class TransformerCore * @returns whether deserialization was successful */ bool deserialize_coordinate( - const sample_msgs::msg::Unfiltered::SharedPtr unfiltered, - sample_msgs::msg::Filtered & filtered); + const sample_msgs::msg::Unfiltered::SharedPtr unfiltered, + sample_msgs::msg::Filtered &filtered); private: // Buffer storing processed messages until BUFFER_CAPACITY. Clear after @@ -75,6 +73,6 @@ class TransformerCore std::vector buffer_; }; -} // namespace samples +} // namespace samples -#endif // TRANSFORMER_CORE_HPP_ +#endif // TRANSFORMER_CORE_HPP_ diff --git a/src/samples/cpp/transformer/include/transformer_node.hpp b/autonomy/samples/cpp/transformer/include/transformer_node.hpp similarity index 86% rename from src/samples/cpp/transformer/include/transformer_node.hpp rename to autonomy/samples/cpp/transformer/include/transformer_node.hpp index ffda282..2af4766 100644 --- a/src/samples/cpp/transformer/include/transformer_node.hpp +++ b/autonomy/samples/cpp/transformer/include/transformer_node.hpp @@ -10,15 +10,15 @@ #include "transformer_core.hpp" /** - * Implementation of a ROS2 node that converts unfiltered messages to filtered_array - * messages. + * Implementation of a ROS2 node that converts unfiltered messages to + * filtered_array messages. * * Listens to the "unfiltered" topic and filters out data with invalid fields * and odd timestamps. Once the node collects BUFFER_CAPACITY messages it packs - * the processed messages into an array and publishes it to the "filtered" topic. + * the processed messages into an array and publishes it to the "filtered" + * topic. */ -class TransformerNode : public rclcpp::Node -{ +class TransformerNode : public rclcpp::Node { public: // Configure pubsub nodes to keep last 20 messages. // https://docs.ros.org/en/foxy/Concepts/About-Quality-of-Service-Settings.html @@ -36,8 +36,7 @@ class TransformerNode : public rclcpp::Node * * @param msg a raw message from the "unfiltered" topic */ - void unfiltered_callback( - const sample_msgs::msg::Unfiltered::SharedPtr msg); + void unfiltered_callback(const sample_msgs::msg::Unfiltered::SharedPtr msg); // ROS2 subscriber listening to the unfiltered topic. rclcpp::Subscription::SharedPtr raw_sub_; @@ -49,4 +48,4 @@ class TransformerNode : public rclcpp::Node samples::TransformerCore transformer_; }; -#endif // TRANSFORMER_NODE_HPP_ +#endif // TRANSFORMER_NODE_HPP_ diff --git a/src/samples/cpp/transformer/launch/transformer.launch.py b/autonomy/samples/cpp/transformer/launch/transformer.launch.py similarity index 100% rename from src/samples/cpp/transformer/launch/transformer.launch.py rename to autonomy/samples/cpp/transformer/launch/transformer.launch.py diff --git a/src/samples/cpp/transformer/package.xml b/autonomy/samples/cpp/transformer/package.xml similarity index 100% rename from src/samples/cpp/transformer/package.xml rename to autonomy/samples/cpp/transformer/package.xml diff --git a/src/samples/cpp/transformer/src/transformer_core.cpp b/autonomy/samples/cpp/transformer/src/transformer_core.cpp similarity index 64% rename from src/samples/cpp/transformer/src/transformer_core.cpp rename to autonomy/samples/cpp/transformer/src/transformer_core.cpp index e62d3f9..a0a8f58 100644 --- a/src/samples/cpp/transformer/src/transformer_core.cpp +++ b/autonomy/samples/cpp/transformer/src/transformer_core.cpp @@ -3,30 +3,23 @@ #include "transformer_core.hpp" -namespace samples -{ +namespace samples { -TransformerCore::TransformerCore() -{} +TransformerCore::TransformerCore() {} -std::vector TransformerCore::buffer_messages() const -{ +std::vector +TransformerCore::buffer_messages() const { return buffer_; } -void TransformerCore::clear_buffer() -{ - buffer_.clear(); -} +void TransformerCore::clear_buffer() { buffer_.clear(); } bool TransformerCore::validate_message( - const sample_msgs::msg::Unfiltered::SharedPtr unfiltered) -{ + const sample_msgs::msg::Unfiltered::SharedPtr unfiltered) { return unfiltered->valid; } -bool TransformerCore::enqueue_message(const sample_msgs::msg::Filtered & msg) -{ +bool TransformerCore::enqueue_message(const sample_msgs::msg::Filtered &msg) { if (buffer_.size() < BUFFER_CAPACITY) { buffer_.push_back(msg); } @@ -34,44 +27,43 @@ bool TransformerCore::enqueue_message(const sample_msgs::msg::Filtered & msg) } bool TransformerCore::deserialize_coordinate( - const sample_msgs::msg::Unfiltered::SharedPtr unfiltered, - sample_msgs::msg::Filtered & filtered) -{ + const sample_msgs::msg::Unfiltered::SharedPtr unfiltered, + sample_msgs::msg::Filtered &filtered) { std::string serialized_position = unfiltered->data; auto start_pos = serialized_position.find("x:"); auto end_pos = serialized_position.find(";"); // Validate that the substrings were found if (start_pos == std::string::npos || end_pos == std::string::npos || - end_pos < start_pos) - { + end_pos < start_pos) { return false; } // Offset index to start of x_pos start_pos += 2; // Splice string and convert position to float - float x = std::stof(serialized_position.substr(start_pos, end_pos - start_pos)); + float x = + std::stof(serialized_position.substr(start_pos, end_pos - start_pos)); start_pos = serialized_position.find("y:", end_pos + 1); end_pos = serialized_position.find(";", end_pos + 1); if (start_pos == std::string::npos || end_pos == std::string::npos || - end_pos < start_pos) - { + end_pos < start_pos) { return false; } // Offset index to start of y_pos start_pos += 2; - float y = std::stof(serialized_position.substr(start_pos, end_pos - start_pos)); + float y = + std::stof(serialized_position.substr(start_pos, end_pos - start_pos)); start_pos = serialized_position.find("z:", end_pos + 1); end_pos = serialized_position.find(";", end_pos + 1); if (start_pos == std::string::npos || end_pos == std::string::npos || - end_pos < start_pos) - { + end_pos < start_pos) { return false; } // Offset index to start of z_pos start_pos += 2; - float z = std::stof(serialized_position.substr(start_pos, end_pos - start_pos)); + float z = + std::stof(serialized_position.substr(start_pos, end_pos - start_pos)); filtered.pos_x = x; filtered.pos_y = y; @@ -79,4 +71,4 @@ bool TransformerCore::deserialize_coordinate( return true; } -} // namespace samples +} // namespace samples diff --git a/src/samples/cpp/transformer/src/transformer_node.cpp b/autonomy/samples/cpp/transformer/src/transformer_node.cpp similarity index 69% rename from src/samples/cpp/transformer/src/transformer_node.cpp rename to autonomy/samples/cpp/transformer/src/transformer_node.cpp index 95cada4..f67b02d 100644 --- a/src/samples/cpp/transformer/src/transformer_node.cpp +++ b/autonomy/samples/cpp/transformer/src/transformer_node.cpp @@ -3,23 +3,21 @@ #include "transformer_node.hpp" TransformerNode::TransformerNode() -: Node("transformer"), transformer_(samples::TransformerCore()) -{ + : Node("transformer"), transformer_(samples::TransformerCore()) { raw_sub_ = this->create_subscription( - "/unfiltered_topic", ADVERTISING_FREQ, - std::bind( - &TransformerNode::unfiltered_callback, this, - std::placeholders::_1)); - transform_pub_ = - this->create_publisher("/filtered_topic", ADVERTISING_FREQ); + "/unfiltered_topic", ADVERTISING_FREQ, + std::bind(&TransformerNode::unfiltered_callback, this, + std::placeholders::_1)); + transform_pub_ = this->create_publisher( + "/filtered_topic", ADVERTISING_FREQ); // Define the default values for parameters if not defined in params.yaml this->declare_parameter("version", rclcpp::ParameterValue(0)); this->declare_parameter("compression_method", rclcpp::ParameterValue(0)); } -void TransformerNode::unfiltered_callback(const sample_msgs::msg::Unfiltered::SharedPtr msg) -{ +void TransformerNode::unfiltered_callback( + const sample_msgs::msg::Unfiltered::SharedPtr msg) { if (!transformer_.validate_message(msg)) { return; } @@ -32,7 +30,7 @@ void TransformerNode::unfiltered_callback(const sample_msgs::msg::Unfiltered::Sh filtered.timestamp = msg->timestamp; filtered.metadata.version = this->get_parameter("version").as_int(); filtered.metadata.compression_method = - this->get_parameter("compression_method").as_int(); + this->get_parameter("compression_method").as_int(); if (transformer_.enqueue_message(filtered)) { RCLCPP_INFO(this->get_logger(), "Buffer Capacity Reached. PUBLISHING..."); @@ -42,15 +40,14 @@ void TransformerNode::unfiltered_callback(const sample_msgs::msg::Unfiltered::Sh transformer_.clear_buffer(); // Construct FilteredArray object - for (auto & packet : buffer) { + for (auto &packet : buffer) { filtered_msgs.packets.push_back(packet); } transform_pub_->publish(filtered_msgs); } } -int main(int argc, char ** argv) -{ +int main(int argc, char **argv) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared()); rclcpp::shutdown(); diff --git a/src/samples/cpp/transformer/test/transformer_test.cpp b/autonomy/samples/cpp/transformer/test/transformer_test.cpp similarity index 68% rename from src/samples/cpp/transformer/test/transformer_test.cpp rename to autonomy/samples/cpp/transformer/test/transformer_test.cpp index 48fe847..fde4855 100644 --- a/src/samples/cpp/transformer/test/transformer_test.cpp +++ b/autonomy/samples/cpp/transformer/test/transformer_test.cpp @@ -2,22 +2,21 @@ #include #include -#include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" +#include "gtest/gtest.h" #include "transformer_core.hpp" /** * When writing a large number of tests it is desirable to wrap any common - * setup or cleanup logic in a test fixture. This improves readability and reduces - * the amount of boilerplate code in each test. For more information checkout + * setup or cleanup logic in a test fixture. This improves readability and + * reduces the amount of boilerplate code in each test. For more information + * checkout * https://google.github.io/googletest/primer.html#same-data-multiple-tests */ -class TransformerFixtureTest : public ::testing::Test -{ +class TransformerFixtureTest : public ::testing::Test { public: - void SetUp(int enqueue_count) - { + void SetUp(int enqueue_count) { auto filtered = sample_msgs::msg::Filtered(); for (int i = 0; i < enqueue_count; i++) { transformer.enqueue_message(filtered); @@ -29,19 +28,18 @@ class TransformerFixtureTest : public ::testing::Test }; /** - * Parameterized tests let us test the same logic with different parameters without - * having to write boilerplate for multiple tests. For more information checkout + * Parameterized tests let us test the same logic with different parameters + * without having to write boilerplate for multiple tests. For more information + * checkout * https://google.github.io/googletest/advanced.html#value-parameterized-tests */ class TransformerParameterizedTest - : public ::testing::TestWithParam> -{ + : public ::testing::TestWithParam> { protected: samples::TransformerCore transformer; }; -TEST(TransformerTest, FilterInvalidField) -{ +TEST(TransformerTest, FilterInvalidField) { auto unfiltered = std::make_shared(); unfiltered->valid = false; @@ -50,8 +48,7 @@ TEST(TransformerTest, FilterInvalidField) EXPECT_FALSE(valid); } -TEST_F(TransformerFixtureTest, BufferCapacity) -{ +TEST_F(TransformerFixtureTest, BufferCapacity) { SetUp(samples::TransformerCore::BUFFER_CAPACITY - 1); // Place last message that fits in buffer @@ -68,8 +65,7 @@ TEST_F(TransformerFixtureTest, BufferCapacity) EXPECT_EQ(size, 10); } -TEST_F(TransformerFixtureTest, ClearBuffer) -{ +TEST_F(TransformerFixtureTest, ClearBuffer) { // Place 3 messages in buffer SetUp(3); @@ -81,8 +77,7 @@ TEST_F(TransformerFixtureTest, ClearBuffer) EXPECT_EQ(size, 0); } -TEST_P(TransformerParameterizedTest, SerializationValidation) -{ +TEST_P(TransformerParameterizedTest, SerializationValidation) { auto [serialized_field, valid] = GetParam(); auto filtered = sample_msgs::msg::Filtered(); auto unfiltered = std::make_shared(); @@ -93,11 +88,10 @@ TEST_P(TransformerParameterizedTest, SerializationValidation) // Parameterized testing lets us validate all edge cases for serialization // using one test case. INSTANTIATE_TEST_CASE_P( - Serialization, TransformerParameterizedTest, - ::testing::Values( - std::make_tuple("x:1;y:2;z:3", false), - std::make_tuple("z:1;y:2;x:3;", false), - std::make_tuple("x:1,y:2,z:3", false), - std::make_tuple("x:3;", false), - std::make_tuple("x:3;y:2;z:3;", true), - std::make_tuple("x:3;y:22; z:11;", true))); + Serialization, TransformerParameterizedTest, + ::testing::Values(std::make_tuple("x:1;y:2;z:3", false), + std::make_tuple("z:1;y:2;x:3;", false), + std::make_tuple("x:1,y:2,z:3", false), + std::make_tuple("x:3;", false), + std::make_tuple("x:3;y:2;z:3;", true), + std::make_tuple("x:3;y:22; z:11;", true))); diff --git a/src/samples/python/aggregator/aggregator/__init__.py b/autonomy/samples/python/aggregator/aggregator/__init__.py similarity index 100% rename from src/samples/python/aggregator/aggregator/__init__.py rename to autonomy/samples/python/aggregator/aggregator/__init__.py diff --git a/src/samples/python/aggregator/aggregator/aggregator_core.py b/autonomy/samples/python/aggregator/aggregator/aggregator_core.py similarity index 100% rename from src/samples/python/aggregator/aggregator/aggregator_core.py rename to autonomy/samples/python/aggregator/aggregator/aggregator_core.py diff --git a/src/samples/python/aggregator/aggregator/aggregator_node.py b/autonomy/samples/python/aggregator/aggregator/aggregator_node.py similarity index 100% rename from src/samples/python/aggregator/aggregator/aggregator_node.py rename to autonomy/samples/python/aggregator/aggregator/aggregator_node.py diff --git a/src/samples/python/aggregator/launch/aggregator.launch.py b/autonomy/samples/python/aggregator/launch/aggregator.launch.py similarity index 100% rename from src/samples/python/aggregator/launch/aggregator.launch.py rename to autonomy/samples/python/aggregator/launch/aggregator.launch.py diff --git a/src/samples/python/aggregator/package.xml b/autonomy/samples/python/aggregator/package.xml similarity index 100% rename from src/samples/python/aggregator/package.xml rename to autonomy/samples/python/aggregator/package.xml diff --git a/src/samples/python/aggregator/resource/aggregator b/autonomy/samples/python/aggregator/resource/aggregator similarity index 100% rename from src/samples/python/aggregator/resource/aggregator rename to autonomy/samples/python/aggregator/resource/aggregator diff --git a/src/samples/python/aggregator/setup.cfg b/autonomy/samples/python/aggregator/setup.cfg similarity index 100% rename from src/samples/python/aggregator/setup.cfg rename to autonomy/samples/python/aggregator/setup.cfg diff --git a/src/samples/python/aggregator/setup.py b/autonomy/samples/python/aggregator/setup.py similarity index 94% rename from src/samples/python/aggregator/setup.py rename to autonomy/samples/python/aggregator/setup.py index b0afb9f..f77c180 100755 --- a/src/samples/python/aggregator/setup.py +++ b/autonomy/samples/python/aggregator/setup.py @@ -14,7 +14,7 @@ # Include our package.xml file (os.path.join('share', package_name), ['package.xml']), # Include all launch files. - (os.path.join('share', package_name, 'launch'), \ + (os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*.launch.py'))), ], install_requires=['setuptools'], diff --git a/src/samples/python/aggregator/test/test_aggregator.py b/autonomy/samples/python/aggregator/test/test_aggregator.py similarity index 100% rename from src/samples/python/aggregator/test/test_aggregator.py rename to autonomy/samples/python/aggregator/test/test_aggregator.py diff --git a/src/samples/python/aggregator/test/test_copyright.py b/autonomy/samples/python/aggregator/test/test_copyright.py similarity index 100% rename from src/samples/python/aggregator/test/test_copyright.py rename to autonomy/samples/python/aggregator/test/test_copyright.py diff --git a/src/samples/python/aggregator/test/test_flake8.py b/autonomy/samples/python/aggregator/test/test_flake8.py similarity index 100% rename from src/samples/python/aggregator/test/test_flake8.py rename to autonomy/samples/python/aggregator/test/test_flake8.py diff --git a/src/samples/python/aggregator/test/test_pep257.py b/autonomy/samples/python/aggregator/test/test_pep257.py similarity index 100% rename from src/samples/python/aggregator/test/test_pep257.py rename to autonomy/samples/python/aggregator/test/test_pep257.py diff --git a/src/samples/python/producer/config/params.yaml b/autonomy/samples/python/producer/config/params.yaml similarity index 100% rename from src/samples/python/producer/config/params.yaml rename to autonomy/samples/python/producer/config/params.yaml diff --git a/src/samples/python/producer/launch/producer.launch.py b/autonomy/samples/python/producer/launch/producer.launch.py similarity index 100% rename from src/samples/python/producer/launch/producer.launch.py rename to autonomy/samples/python/producer/launch/producer.launch.py diff --git a/src/samples/python/producer/package.xml b/autonomy/samples/python/producer/package.xml similarity index 100% rename from src/samples/python/producer/package.xml rename to autonomy/samples/python/producer/package.xml diff --git a/src/samples/python/producer/producer/__init__.py b/autonomy/samples/python/producer/producer/__init__.py similarity index 100% rename from src/samples/python/producer/producer/__init__.py rename to autonomy/samples/python/producer/producer/__init__.py diff --git a/src/samples/python/producer/producer/producer_core.py b/autonomy/samples/python/producer/producer/producer_core.py similarity index 100% rename from src/samples/python/producer/producer/producer_core.py rename to autonomy/samples/python/producer/producer/producer_core.py diff --git a/src/samples/python/producer/producer/producer_node.py b/autonomy/samples/python/producer/producer/producer_node.py similarity index 100% rename from src/samples/python/producer/producer/producer_node.py rename to autonomy/samples/python/producer/producer/producer_node.py diff --git a/src/samples/python/producer/resource/producer b/autonomy/samples/python/producer/resource/producer similarity index 100% rename from src/samples/python/producer/resource/producer rename to autonomy/samples/python/producer/resource/producer diff --git a/src/samples/python/producer/setup.cfg b/autonomy/samples/python/producer/setup.cfg similarity index 100% rename from src/samples/python/producer/setup.cfg rename to autonomy/samples/python/producer/setup.cfg diff --git a/src/samples/python/producer/setup.py b/autonomy/samples/python/producer/setup.py similarity index 100% rename from src/samples/python/producer/setup.py rename to autonomy/samples/python/producer/setup.py diff --git a/src/samples/python/producer/test/test_copyright.py b/autonomy/samples/python/producer/test/test_copyright.py similarity index 100% rename from src/samples/python/producer/test/test_copyright.py rename to autonomy/samples/python/producer/test/test_copyright.py diff --git a/src/samples/python/producer/test/test_flake8.py b/autonomy/samples/python/producer/test/test_flake8.py similarity index 100% rename from src/samples/python/producer/test/test_flake8.py rename to autonomy/samples/python/producer/test/test_flake8.py diff --git a/src/samples/python/producer/test/test_pep257.py b/autonomy/samples/python/producer/test/test_pep257.py similarity index 100% rename from src/samples/python/producer/test/test_pep257.py rename to autonomy/samples/python/producer/test/test_pep257.py diff --git a/src/samples/python/producer/test/test_producer.py b/autonomy/samples/python/producer/test/test_producer.py similarity index 100% rename from src/samples/python/producer/test/test_producer.py rename to autonomy/samples/python/producer/test/test_producer.py diff --git a/src/samples/python/transformer/config/params.yaml b/autonomy/samples/python/transformer/config/params.yaml similarity index 100% rename from src/samples/python/transformer/config/params.yaml rename to autonomy/samples/python/transformer/config/params.yaml diff --git a/src/samples/python/transformer/launch/transformer.launch.py b/autonomy/samples/python/transformer/launch/transformer.launch.py similarity index 100% rename from src/samples/python/transformer/launch/transformer.launch.py rename to autonomy/samples/python/transformer/launch/transformer.launch.py diff --git a/src/samples/python/transformer/package.xml b/autonomy/samples/python/transformer/package.xml similarity index 100% rename from src/samples/python/transformer/package.xml rename to autonomy/samples/python/transformer/package.xml diff --git a/src/samples/python/transformer/resource/transformer b/autonomy/samples/python/transformer/resource/transformer similarity index 100% rename from src/samples/python/transformer/resource/transformer rename to autonomy/samples/python/transformer/resource/transformer diff --git a/src/samples/python/transformer/setup.cfg b/autonomy/samples/python/transformer/setup.cfg similarity index 100% rename from src/samples/python/transformer/setup.cfg rename to autonomy/samples/python/transformer/setup.cfg diff --git a/src/samples/python/transformer/setup.py b/autonomy/samples/python/transformer/setup.py similarity index 100% rename from src/samples/python/transformer/setup.py rename to autonomy/samples/python/transformer/setup.py diff --git a/src/samples/python/transformer/test/test_copyright.py b/autonomy/samples/python/transformer/test/test_copyright.py similarity index 100% rename from src/samples/python/transformer/test/test_copyright.py rename to autonomy/samples/python/transformer/test/test_copyright.py diff --git a/src/samples/python/transformer/test/test_flake8.py b/autonomy/samples/python/transformer/test/test_flake8.py similarity index 100% rename from src/samples/python/transformer/test/test_flake8.py rename to autonomy/samples/python/transformer/test/test_flake8.py diff --git a/src/samples/python/transformer/test/test_pep257.py b/autonomy/samples/python/transformer/test/test_pep257.py similarity index 100% rename from src/samples/python/transformer/test/test_pep257.py rename to autonomy/samples/python/transformer/test/test_pep257.py diff --git a/src/samples/python/transformer/test/test_transformer.py b/autonomy/samples/python/transformer/test/test_transformer.py similarity index 100% rename from src/samples/python/transformer/test/test_transformer.py rename to autonomy/samples/python/transformer/test/test_transformer.py diff --git a/src/samples/python/transformer/transformer/__init__.py b/autonomy/samples/python/transformer/transformer/__init__.py similarity index 100% rename from src/samples/python/transformer/transformer/__init__.py rename to autonomy/samples/python/transformer/transformer/__init__.py diff --git a/src/samples/python/transformer/transformer/transformer_core.py b/autonomy/samples/python/transformer/transformer/transformer_core.py similarity index 100% rename from src/samples/python/transformer/transformer/transformer_core.py rename to autonomy/samples/python/transformer/transformer/transformer_core.py diff --git a/src/samples/python/transformer/transformer/transformer_node.py b/autonomy/samples/python/transformer/transformer/transformer_node.py similarity index 100% rename from src/samples/python/transformer/transformer/transformer_node.py rename to autonomy/samples/python/transformer/transformer/transformer_node.py diff --git a/src/samples/sample_msgs/CMakeLists.txt b/autonomy/samples/sample_msgs/CMakeLists.txt similarity index 100% rename from src/samples/sample_msgs/CMakeLists.txt rename to autonomy/samples/sample_msgs/CMakeLists.txt diff --git a/src/samples/sample_msgs/msg/Filtered.msg b/autonomy/samples/sample_msgs/msg/Filtered.msg similarity index 100% rename from src/samples/sample_msgs/msg/Filtered.msg rename to autonomy/samples/sample_msgs/msg/Filtered.msg diff --git a/src/samples/sample_msgs/msg/FilteredArray.msg b/autonomy/samples/sample_msgs/msg/FilteredArray.msg similarity index 100% rename from src/samples/sample_msgs/msg/FilteredArray.msg rename to autonomy/samples/sample_msgs/msg/FilteredArray.msg diff --git a/src/samples/sample_msgs/msg/Metadata.msg b/autonomy/samples/sample_msgs/msg/Metadata.msg similarity index 100% rename from src/samples/sample_msgs/msg/Metadata.msg rename to autonomy/samples/sample_msgs/msg/Metadata.msg diff --git a/src/samples/sample_msgs/msg/Unfiltered.msg b/autonomy/samples/sample_msgs/msg/Unfiltered.msg similarity index 100% rename from src/samples/sample_msgs/msg/Unfiltered.msg rename to autonomy/samples/sample_msgs/msg/Unfiltered.msg diff --git a/src/samples/sample_msgs/package.xml b/autonomy/samples/sample_msgs/package.xml similarity index 100% rename from src/samples/sample_msgs/package.xml rename to autonomy/samples/sample_msgs/package.xml diff --git a/src/samples/samples_diagram.svg b/autonomy/samples/samples_diagram.svg similarity index 100% rename from src/samples/samples_diagram.svg rename to autonomy/samples/samples_diagram.svg diff --git a/src/wato_msgs/sample_msgs/CMakeLists.txt b/autonomy/wato_msgs/sample_msgs/CMakeLists.txt similarity index 100% rename from src/wato_msgs/sample_msgs/CMakeLists.txt rename to autonomy/wato_msgs/sample_msgs/CMakeLists.txt diff --git a/src/wato_msgs/sample_msgs/msg/Filtered.msg b/autonomy/wato_msgs/sample_msgs/msg/Filtered.msg similarity index 100% rename from src/wato_msgs/sample_msgs/msg/Filtered.msg rename to autonomy/wato_msgs/sample_msgs/msg/Filtered.msg diff --git a/src/wato_msgs/sample_msgs/msg/FilteredArray.msg b/autonomy/wato_msgs/sample_msgs/msg/FilteredArray.msg similarity index 100% rename from src/wato_msgs/sample_msgs/msg/FilteredArray.msg rename to autonomy/wato_msgs/sample_msgs/msg/FilteredArray.msg diff --git a/src/wato_msgs/sample_msgs/msg/Metadata.msg b/autonomy/wato_msgs/sample_msgs/msg/Metadata.msg similarity index 100% rename from src/wato_msgs/sample_msgs/msg/Metadata.msg rename to autonomy/wato_msgs/sample_msgs/msg/Metadata.msg diff --git a/src/wato_msgs/sample_msgs/msg/Unfiltered.msg b/autonomy/wato_msgs/sample_msgs/msg/Unfiltered.msg similarity index 100% rename from src/wato_msgs/sample_msgs/msg/Unfiltered.msg rename to autonomy/wato_msgs/sample_msgs/msg/Unfiltered.msg diff --git a/src/wato_msgs/sample_msgs/package.xml b/autonomy/wato_msgs/sample_msgs/package.xml similarity index 100% rename from src/wato_msgs/sample_msgs/package.xml rename to autonomy/wato_msgs/sample_msgs/package.xml diff --git a/config/wato_asd_training_foxglove_config .json b/config/wato_asd_training_foxglove_config .json index e9ea578..fbfb313 100644 --- a/config/wato_asd_training_foxglove_config .json +++ b/config/wato_asd_training_foxglove_config .json @@ -136,7 +136,7 @@ "instanceId": "9e830917-d6fe-4f25-8e7b-25afa5a47d21", "layerId": "foxglove.Urdf", "sourceType": "url", - "url": "https://raw.githubusercontent.com/WATonomous/wato_asd_training/refs/heads/main/src/gazebo/launch/robot.urdf", + "url": "https://raw.githubusercontent.com/WATonomous/wato_asd_training/refs/heads/main/autonomy/gazebo/launch/robot.urdf", "filePath": "", "parameter": "", "topic": "", @@ -174,7 +174,7 @@ "instanceId": "cadf14d8-072e-4af6-8fd0-60bf81d00399", "layerId": "foxglove.Urdf", "sourceType": "url", - "url": "https://raw.githubusercontent.com/WATonomous/wato_asd_training/refs/heads/main/src/gazebo/launch/env.urdf", + "url": "https://raw.githubusercontent.com/WATonomous/wato_asd_training/refs/heads/main/autonomy/gazebo/launch/env.urdf", "filePath": "", "parameter": "", "topic": "", diff --git a/docker/infrastructure/foxglove/foxglove.Dockerfile b/docker/infrastructure/foxglove/foxglove.Dockerfile index 45d65c5..657d13a 100644 --- a/docker/infrastructure/foxglove/foxglove.Dockerfile +++ b/docker/infrastructure/foxglove/foxglove.Dockerfile @@ -6,7 +6,7 @@ FROM ${BASE_IMAGE} AS source WORKDIR ${AMENT_WS}/src # Copy in source code -COPY src/wato_msgs wato_msgs +COPY autonomy/wato_msgs wato_msgs # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ diff --git a/docker/samples/cpp_aggregator.Dockerfile b/docker/samples/cpp_aggregator.Dockerfile index 2f52d0a..42b9339 100644 --- a/docker/samples/cpp_aggregator.Dockerfile +++ b/docker/samples/cpp_aggregator.Dockerfile @@ -6,8 +6,8 @@ FROM ${BASE_IMAGE} AS source WORKDIR ${AMENT_WS}/src # Copy in source code -COPY src/samples/cpp/aggregator aggregator -COPY src/wato_msgs/sample_msgs sample_msgs +COPY autonomy/samples/cpp/aggregator aggregator +COPY autonomy/wato_msgs/sample_msgs sample_msgs # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ diff --git a/docker/samples/cpp_producer.Dockerfile b/docker/samples/cpp_producer.Dockerfile index 3a87d5d..33f3552 100644 --- a/docker/samples/cpp_producer.Dockerfile +++ b/docker/samples/cpp_producer.Dockerfile @@ -6,8 +6,8 @@ FROM ${BASE_IMAGE} AS source WORKDIR ${AMENT_WS}/src # Copy in source code -COPY src/samples/cpp/producer producer -COPY src/wato_msgs/sample_msgs sample_msgs +COPY autonomy/samples/cpp/producer producer +COPY autonomy/wato_msgs/sample_msgs sample_msgs # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ diff --git a/docker/samples/cpp_transformer.Dockerfile b/docker/samples/cpp_transformer.Dockerfile index 3ba7394..9929f2c 100644 --- a/docker/samples/cpp_transformer.Dockerfile +++ b/docker/samples/cpp_transformer.Dockerfile @@ -6,8 +6,8 @@ FROM ${BASE_IMAGE} AS source WORKDIR ${AMENT_WS}/src # Copy in source code -COPY src/samples/cpp/transformer transformer -COPY src/wato_msgs/sample_msgs sample_msgs +COPY autonomy/samples/cpp/transformer transformer +COPY autonomy/wato_msgs/sample_msgs sample_msgs # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ diff --git a/docker/samples/py_aggregator.Dockerfile b/docker/samples/py_aggregator.Dockerfile index a4c2008..bbd4051 100644 --- a/docker/samples/py_aggregator.Dockerfile +++ b/docker/samples/py_aggregator.Dockerfile @@ -6,8 +6,8 @@ FROM ${BASE_IMAGE} AS source WORKDIR ${AMENT_WS}/src # Copy in source code -COPY src/samples/python/aggregator aggregator -COPY src/wato_msgs/sample_msgs sample_msgs +COPY autonomy/samples/python/aggregator aggregator +COPY autonomy/wato_msgs/sample_msgs sample_msgs # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ diff --git a/docker/samples/py_producer.Dockerfile b/docker/samples/py_producer.Dockerfile index 33380b9..ae46da4 100644 --- a/docker/samples/py_producer.Dockerfile +++ b/docker/samples/py_producer.Dockerfile @@ -6,8 +6,8 @@ FROM ${BASE_IMAGE} AS source WORKDIR ${AMENT_WS}/src # Copy in source code -COPY src/samples/python/producer producer -COPY src/wato_msgs/sample_msgs sample_msgs +COPY autonomy/samples/python/producer producer +COPY autonomy/wato_msgs/sample_msgs sample_msgs # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ diff --git a/docker/samples/py_transformer.Dockerfile b/docker/samples/py_transformer.Dockerfile index 50b8f00..51f1810 100644 --- a/docker/samples/py_transformer.Dockerfile +++ b/docker/samples/py_transformer.Dockerfile @@ -6,8 +6,8 @@ FROM ${BASE_IMAGE} AS source WORKDIR ${AMENT_WS}/src # Copy in source code -COPY src/samples/python/transformer transformer -COPY src/wato_msgs/sample_msgs sample_msgs +COPY autonomy/samples/python/transformer transformer +COPY autonomy/wato_msgs/sample_msgs sample_msgs # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ diff --git a/docker/simulation/isaac_sim/isaac_sim.Dockerfile b/docker/simulation/isaac_sim/isaac_sim.Dockerfile index f7fb3cd..d391542 100644 --- a/docker/simulation/isaac_sim/isaac_sim.Dockerfile +++ b/docker/simulation/isaac_sim/isaac_sim.Dockerfile @@ -6,8 +6,8 @@ FROM ${BASE_IMAGE} AS source WORKDIR ${AMENT_WS}/src # Copy in source code -COPY src/samples/cpp/aggregator aggregator -COPY src/wato_msgs/sample_msgs sample_msgs +COPY autonomy/samples/cpp/aggregator aggregator +COPY autonomy/wato_msgs/sample_msgs sample_msgs # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ diff --git a/docs/Architecture_Map.odg b/docs/Architecture_Map.odg index 175b96c5b905d40560ca655be4b54fa889dd5e0d..a9eadb725a9f5bd49d3f80ef5e101bdf3c729d4a 100644 GIT binary patch delta 28835 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%0DhO<d3k{4`d9| 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 delta 28618 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 diff --git a/docs/Architecture_Map.pdf b/docs/Architecture_Map.pdf index b80eaf3e609ef8111561bce5115e6a452805d1dd..4fa3780ba4cade00578a42d79d725b1b030afa89 100644 GIT binary patch delta 32471 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 - -#include "aggregator_core.hpp" - -namespace samples -{ - -AggregatorCore::AggregatorCore(int64_t timestamp) -: raw_msg_count_(0), filtered_msg_count_(0), start_(timestamp), - latest_raw_time_(-1), latest_filtered_time_(-1) -{} - -double AggregatorCore::raw_frequency() const -{ - if (latest_raw_time_ <= start_) { - return 0.0; - } - return static_cast(raw_msg_count_) / (latest_raw_time_ - start_); -} - -double AggregatorCore::filtered_frequency() const -{ - if (latest_filtered_time_ <= start_) { - return 0.0; - } - return static_cast(filtered_msg_count_) / (latest_filtered_time_ - start_); -} - -void AggregatorCore::add_raw_msg( - const sample_msgs::msg::Unfiltered::SharedPtr msg) -{ - latest_raw_time_ = std::max( - static_cast(msg->timestamp), latest_raw_time_); - raw_msg_count_++; -} - -void AggregatorCore::add_filtered_msg( - const sample_msgs::msg::FilteredArray::SharedPtr msg) -{ - for (auto filtered_msg : msg->packets) { - latest_filtered_time_ = std::max( - static_cast(filtered_msg.timestamp), latest_filtered_time_); - } - filtered_msg_count_++; -} - -} // namespace samples diff --git a/src/samples/cpp/aggregator/src/aggregator_node.cpp b/src/samples/cpp/aggregator/src/aggregator_node.cpp deleted file mode 100644 index 679e3af..0000000 --- a/src/samples/cpp/aggregator/src/aggregator_node.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include -#include - -#include "aggregator_node.hpp" - -AggregatorNode::AggregatorNode() -: Node("aggregator"), - aggregator_( - samples::AggregatorCore( - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count())) -{ - raw_sub_ = this->create_subscription( - "/unfiltered_topic", ADVERTISING_FREQ, - std::bind( - &AggregatorNode::unfiltered_callback, this, - std::placeholders::_1)); - filtered_sub_ = this->create_subscription( - "/filtered_topic", ADVERTISING_FREQ, - std::bind( - &AggregatorNode::filtered_callback, this, - std::placeholders::_1)); -} - -void AggregatorNode::unfiltered_callback( - const sample_msgs::msg::Unfiltered::SharedPtr msg) -{ - aggregator_.add_raw_msg(msg); - RCLCPP_INFO( - this->get_logger(), "Raw Frequency(msg/s): %f", - aggregator_.raw_frequency() * 1000); -} - -void AggregatorNode::filtered_callback( - const sample_msgs::msg::FilteredArray::SharedPtr msg) -{ - aggregator_.add_filtered_msg(msg); - RCLCPP_INFO( - this->get_logger(), "Filtered Frequency(msg/s): %f", - aggregator_.filtered_frequency() * 1000); -} - -int main(int argc, char ** argv) -{ - rclcpp::init(argc, argv); - rclcpp::spin(std::make_shared()); - rclcpp::shutdown(); - return 0; -} From 43c5b6990bd005a705ef36011eda564b1ced2fb7 Mon Sep 17 00:00:00 2001 From: miekale Date: Sun, 27 Apr 2025 13:52:31 -0400 Subject: [PATCH 06/36] updating to new structure --- docker/interfacing/interfacing.Dockerfile | 4 ++-- modules/docker-compose.interfacing.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/interfacing/interfacing.Dockerfile b/docker/interfacing/interfacing.Dockerfile index 553ac42..8e0053c 100644 --- a/docker/interfacing/interfacing.Dockerfile +++ b/docker/interfacing/interfacing.Dockerfile @@ -6,8 +6,8 @@ FROM ${BASE_IMAGE} AS source WORKDIR ${AMENT_WS}/src # Copy in source code -COPY src/interfacing/can can -COPY src/wato_msgs/sample_msgs sample_msgs +COPY autonomy/interfacing/can can +COPY autonomy/wato_msgs/sample_msgs sample_msgs # Scan for rosdeps RUN apt-get -qq update && rosdep update && \ diff --git a/modules/docker-compose.interfacing.yaml b/modules/docker-compose.interfacing.yaml index b7c71f0..5b0ca34 100644 --- a/modules/docker-compose.interfacing.yaml +++ b/modules/docker-compose.interfacing.yaml @@ -12,4 +12,4 @@ services: profiles: [deploy, develop] command: /bin/bash -c "ros2 launch can can.launch.py" volumes: - - ${MONO_DIR}/src/interfacing:/root/ament_ws/src/interfacing + - ${MONO_DIR}/autonomy/interfacing:/root/ament_ws/src/interfacing From 6509a52a32a2d63c54a427c5672940fc5c47d303 Mon Sep 17 00:00:00 2001 From: miekale Date: Sun, 27 Apr 2025 15:03:01 -0400 Subject: [PATCH 07/36] moving to autonomy --- {src => autonomy}/interfacing/can/README.md | 0 {src => autonomy}/interfacing/can/can/__init__.py | 0 {src => autonomy}/interfacing/can/can/can_core.py | 0 {src => autonomy}/interfacing/can/can/can_node.py | 0 {src => autonomy}/interfacing/can/config/params.yaml | 0 {src => autonomy}/interfacing/can/launch/can.launch.py | 0 {src => autonomy}/interfacing/can/package.xml | 0 {src => autonomy}/interfacing/can/resource/can | 0 {src => autonomy}/interfacing/can/setup.cfg | 0 {src => autonomy}/interfacing/can/setup.py | 0 {src => autonomy}/interfacing/can/test/test_can.py | 0 {src => autonomy}/interfacing/can/test/test_copyright.py | 0 {src => autonomy}/interfacing/can/test/test_flake8.py | 0 {src => autonomy}/interfacing/can/test/test_pep257.py | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename {src => autonomy}/interfacing/can/README.md (100%) rename {src => autonomy}/interfacing/can/can/__init__.py (100%) rename {src => autonomy}/interfacing/can/can/can_core.py (100%) rename {src => autonomy}/interfacing/can/can/can_node.py (100%) rename {src => autonomy}/interfacing/can/config/params.yaml (100%) rename {src => autonomy}/interfacing/can/launch/can.launch.py (100%) rename {src => autonomy}/interfacing/can/package.xml (100%) rename {src => autonomy}/interfacing/can/resource/can (100%) rename {src => autonomy}/interfacing/can/setup.cfg (100%) rename {src => autonomy}/interfacing/can/setup.py (100%) rename {src => autonomy}/interfacing/can/test/test_can.py (100%) rename {src => autonomy}/interfacing/can/test/test_copyright.py (100%) rename {src => autonomy}/interfacing/can/test/test_flake8.py (100%) rename {src => autonomy}/interfacing/can/test/test_pep257.py (100%) diff --git a/src/interfacing/can/README.md b/autonomy/interfacing/can/README.md similarity index 100% rename from src/interfacing/can/README.md rename to autonomy/interfacing/can/README.md diff --git a/src/interfacing/can/can/__init__.py b/autonomy/interfacing/can/can/__init__.py similarity index 100% rename from src/interfacing/can/can/__init__.py rename to autonomy/interfacing/can/can/__init__.py diff --git a/src/interfacing/can/can/can_core.py b/autonomy/interfacing/can/can/can_core.py similarity index 100% rename from src/interfacing/can/can/can_core.py rename to autonomy/interfacing/can/can/can_core.py diff --git a/src/interfacing/can/can/can_node.py b/autonomy/interfacing/can/can/can_node.py similarity index 100% rename from src/interfacing/can/can/can_node.py rename to autonomy/interfacing/can/can/can_node.py diff --git a/src/interfacing/can/config/params.yaml b/autonomy/interfacing/can/config/params.yaml similarity index 100% rename from src/interfacing/can/config/params.yaml rename to autonomy/interfacing/can/config/params.yaml diff --git a/src/interfacing/can/launch/can.launch.py b/autonomy/interfacing/can/launch/can.launch.py similarity index 100% rename from src/interfacing/can/launch/can.launch.py rename to autonomy/interfacing/can/launch/can.launch.py diff --git a/src/interfacing/can/package.xml b/autonomy/interfacing/can/package.xml similarity index 100% rename from src/interfacing/can/package.xml rename to autonomy/interfacing/can/package.xml diff --git a/src/interfacing/can/resource/can b/autonomy/interfacing/can/resource/can similarity index 100% rename from src/interfacing/can/resource/can rename to autonomy/interfacing/can/resource/can diff --git a/src/interfacing/can/setup.cfg b/autonomy/interfacing/can/setup.cfg similarity index 100% rename from src/interfacing/can/setup.cfg rename to autonomy/interfacing/can/setup.cfg diff --git a/src/interfacing/can/setup.py b/autonomy/interfacing/can/setup.py similarity index 100% rename from src/interfacing/can/setup.py rename to autonomy/interfacing/can/setup.py diff --git a/src/interfacing/can/test/test_can.py b/autonomy/interfacing/can/test/test_can.py similarity index 100% rename from src/interfacing/can/test/test_can.py rename to autonomy/interfacing/can/test/test_can.py diff --git a/src/interfacing/can/test/test_copyright.py b/autonomy/interfacing/can/test/test_copyright.py similarity index 100% rename from src/interfacing/can/test/test_copyright.py rename to autonomy/interfacing/can/test/test_copyright.py diff --git a/src/interfacing/can/test/test_flake8.py b/autonomy/interfacing/can/test/test_flake8.py similarity index 100% rename from src/interfacing/can/test/test_flake8.py rename to autonomy/interfacing/can/test/test_flake8.py diff --git a/src/interfacing/can/test/test_pep257.py b/autonomy/interfacing/can/test/test_pep257.py similarity index 100% rename from src/interfacing/can/test/test_pep257.py rename to autonomy/interfacing/can/test/test_pep257.py From 6e3eb0dd6055bd61761ca2c8f9447926ddf943bc Mon Sep 17 00:00:00 2001 From: miekale Date: Thu, 8 May 2025 12:43:35 -0400 Subject: [PATCH 08/36] adding changes --- .../interfacing/aggregator/CMakeLists.txt | 76 ++++++++++++++++++ .../aggregator/include/aggregator_core.hpp | 71 ++++++++++++++++ .../aggregator/include/aggregator_node.hpp | 56 +++++++++++++ .../aggregator/launch/aggregator.launch.py | 14 ++++ autonomy/interfacing/aggregator/package.xml | 23 ++++++ .../aggregator/src/aggregator_core.cpp | 42 ++++++++++ .../aggregator/src/aggregator_node.cpp | 41 ++++++++++ .../aggregator/test/aggregator_test.cpp | 73 +++++++++++++++++ autonomy/interfacing/can/can/can_node.py | 3 +- autonomy/interfacing/test.cpp | 0 docker/interfacing/interfacing.Dockerfile | 3 +- docs/Architecture_Map.odg | Bin 33558 -> 22942 bytes modules/docker-compose.interfacing.yaml | 5 +- 13 files changed, 404 insertions(+), 3 deletions(-) create mode 100644 autonomy/interfacing/aggregator/CMakeLists.txt create mode 100644 autonomy/interfacing/aggregator/include/aggregator_core.hpp create mode 100644 autonomy/interfacing/aggregator/include/aggregator_node.hpp create mode 100755 autonomy/interfacing/aggregator/launch/aggregator.launch.py create mode 100644 autonomy/interfacing/aggregator/package.xml create mode 100644 autonomy/interfacing/aggregator/src/aggregator_core.cpp create mode 100644 autonomy/interfacing/aggregator/src/aggregator_node.cpp create mode 100644 autonomy/interfacing/aggregator/test/aggregator_test.cpp create mode 100644 autonomy/interfacing/test.cpp diff --git a/autonomy/interfacing/aggregator/CMakeLists.txt b/autonomy/interfacing/aggregator/CMakeLists.txt new file mode 100644 index 0000000..bc85e07 --- /dev/null +++ b/autonomy/interfacing/aggregator/CMakeLists.txt @@ -0,0 +1,76 @@ +cmake_minimum_required(VERSION 3.10) +project(aggregator) + +# Set compiler to use C++ 17 standard +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# Search for dependencies required for building this package +find_package(ament_cmake REQUIRED) # ROS2 build tool +find_package(rclcpp REQUIRED) # ROS2 C++ package +find_package(sample_msgs REQUIRED) # Custom package containing ROS2 messages + +# Compiles source files into a library +# A library is not executed, instead other executables can link +# against it to access defined methods and classes. +# We build a library so that the methods defined can be used by +# both the unit test and ROS2 node executables. +add_library(aggregator_lib + src/aggregator_core.cpp) +# Indicate to compiler where to search for header files +target_include_directories(aggregator_lib + PUBLIC include) +# Add ROS2 dependencies required by package +ament_target_dependencies(aggregator_lib rclcpp sample_msgs) + +# By default tests are built. This can be overridden by modifying the +# colcon build command to include the -DBUILD_TESTING=OFF flag. +option(BUILD_TESTING "Build tests" ON) +if(BUILD_TESTING) + # Search for dependencies required for building tests + linting + find_package(ament_cmake_gtest REQUIRED) + find_package(ament_lint_auto REQUIRED) + find_package(ament_lint_common REQUIRED) + + # Remove the default C++ and copyright linter + list(APPEND AMENT_LINT_AUTO_EXCLUDE + ament_cmake_cpplint + ament_cmake_copyright) + + # Reinstall cpplint ignoring copyright errors + ament_cpplint(FILTERS "-legal/copyright") + + ament_lint_auto_find_test_dependencies() + + # Create test executable from source files + ament_add_gtest(aggregator_test test/aggregator_test.cpp) + # Link to the previously built library to access Aggregator classes and methods + target_link_libraries(aggregator_test aggregator_lib) + + # Copy executable to installation location + install(TARGETS + aggregator_test + DESTINATION lib/${PROJECT_NAME}) +endif() + +# Create ROS2 node executable from source files +add_executable(aggregator_node src/aggregator_node.cpp) +# Link to the previously built library to access Aggregator classes and methods +target_link_libraries(aggregator_node aggregator_lib) + +# Copy executable to installation location +install(TARGETS + aggregator_node + DESTINATION lib/${PROJECT_NAME}) + +# Copy launch files to installation location +install(DIRECTORY + launch + DESTINATION share/${PROJECT_NAME}) + +ament_package() diff --git a/autonomy/interfacing/aggregator/include/aggregator_core.hpp b/autonomy/interfacing/aggregator/include/aggregator_core.hpp new file mode 100644 index 0000000..9dcea65 --- /dev/null +++ b/autonomy/interfacing/aggregator/include/aggregator_core.hpp @@ -0,0 +1,71 @@ +#ifndef AGGREGATOR_CORE_HPP_ +#define AGGREGATOR_CORE_HPP_ + +#include "rclcpp/rclcpp.hpp" + +#include "sample_msgs/msg/filtered_array.hpp" +#include "sample_msgs/msg/unfiltered.hpp" + +namespace samples { + +/** + * Implementation of the internal logic used by the Aggregator Node to + * calculate the operating frequency of topics. + */ +class AggregatorCore { +public: + /** + * Aggregator constructor. + * + * @param timestamp the Unix timestamp https://en.wikipedia.org/wiki/Unix_time + */ + explicit AggregatorCore(int64_t timestamp); + + /** + * Calculates the operating frequency on the "unfiltered" topic. Handles + * invalid timestamps and division by zero by returning zero. + * + * @returns frequency of messages on "unfiltered" topic + */ + double raw_frequency() const; + /** + * Calculates the operating frequency on the "filtered" topic. Handles + * invalid timestamps and division by zero by returning zero. + * + * @returns frequency of messages on "filtered" topic + */ + double filtered_frequency() const; + + /** + * Used to keep track of latest timestamp and number of messages received + * over the "unfiltered" topic. Should be called before raw_frequency(). + * + * @param msg + */ + void add_raw_msg(const sample_msgs::msg::Unfiltered::SharedPtr msg); + /** + * Used to keep track of latest timestamp and number of messages received + * over the "filtered" topic. Should be called before filtered_frequency(). + * + * @param msg + */ + void add_filtered_msg(const sample_msgs::msg::FilteredArray::SharedPtr msg); + +private: + // Number of message received on "unfiltered" and "filtered" topics. + int raw_msg_count_; + int filtered_msg_count_; + + // Unix timestamp used to determine the amount of time that has passed + // since the beginning of the program. + int64_t start_; + + // Unix timestamps for last time a message was received on the "unfiltered" + // and "filtered" topics. + int64_t latest_raw_time_; + int64_t latest_filtered_time_; +}; + +} // namespace samples + +#endif // AGGREGATOR_CORE_HPP_ diff --git a/autonomy/interfacing/aggregator/include/aggregator_node.hpp b/autonomy/interfacing/aggregator/include/aggregator_node.hpp new file mode 100644 index 0000000..a273a2e --- /dev/null +++ b/autonomy/interfacing/aggregator/include/aggregator_node.hpp @@ -0,0 +1,56 @@ +#ifndef AGGREGATOR_NODE_HPP_ +#define AGGREGATOR_NODE_HPP_ + +#include "rclcpp/rclcpp.hpp" + +#include "sample_msgs/msg/filtered_array.hpp" +#include "sample_msgs/msg/unfiltered.hpp" + +#include "aggregator_core.hpp" + +/** + * Implementation of a ROS2 node that listens to the "unfiltered" and "filtered" + * topics and echoes the operating frequency of the topic to the console. + */ +class AggregatorNode : public rclcpp::Node { +public: + // Configure pubsub nodes to keep last 20 messages. + // https://docs.ros.org/en/foxy/Concepts/About-Quality-of-Service-Settings.html + static constexpr int ADVERTISING_FREQ = 20; + + /** + * Aggregator node constructor. + */ + AggregatorNode(); + +private: + /** + * A ROS2 subscription node callback used to unpack raw data from the + * "unfiltered" topic and echo the operating frequency of the topic to the + * console. + * + * @param msg a raw message from the "unfiltered" topic + */ + void unfiltered_callback(const sample_msgs::msg::Unfiltered::SharedPtr msg); + + /** + * A ROS2 subscription node callback used to unpack processed data from the + * "filtered" topic and echo the operating frequency of the topic to the + * console. + * + * @param msg a processed message from the "filtered" topic + */ + void filtered_callback(const sample_msgs::msg::FilteredArray::SharedPtr msg); + + // ROS2 subscriber listening to the unfiltered topic. + rclcpp::Subscription::SharedPtr raw_sub_; + + // ROS2 subscriber listening to the filtered topic. + rclcpp::Subscription::SharedPtr + filtered_sub_; + + // Object containing methods to determine the operating frequency on topics. + samples::AggregatorCore aggregator_; +}; + +#endif // AGGREGATOR_NODE_HPP_ diff --git a/autonomy/interfacing/aggregator/launch/aggregator.launch.py b/autonomy/interfacing/aggregator/launch/aggregator.launch.py new file mode 100755 index 0000000..d0486d5 --- /dev/null +++ b/autonomy/interfacing/aggregator/launch/aggregator.launch.py @@ -0,0 +1,14 @@ +from launch import LaunchDescription +from launch_ros.actions import Node + + +def generate_launch_description(): + """Launch aggregator node.""" + aggregator_node = Node( + package='aggregator', + executable='aggregator_node', + ) + + return LaunchDescription([ + aggregator_node + ]) diff --git a/autonomy/interfacing/aggregator/package.xml b/autonomy/interfacing/aggregator/package.xml new file mode 100644 index 0000000..13366d4 --- /dev/null +++ b/autonomy/interfacing/aggregator/package.xml @@ -0,0 +1,23 @@ + + + aggregator + 0.0.0 + A sample ROS package for pubsub communication + + Cheran Mahalingam + Apache2.0 + + + ament_cmake + rclcpp + sample_msgs + + ament_lint_auto + ament_lint_common + ament_cmake_gtest + + + + ament_cmake + + \ No newline at end of file diff --git a/autonomy/interfacing/aggregator/src/aggregator_core.cpp b/autonomy/interfacing/aggregator/src/aggregator_core.cpp new file mode 100644 index 0000000..58f9e6a --- /dev/null +++ b/autonomy/interfacing/aggregator/src/aggregator_core.cpp @@ -0,0 +1,42 @@ +#include + +#include "aggregator_core.hpp" + +namespace samples { + +AggregatorCore::AggregatorCore(int64_t timestamp) + : raw_msg_count_(0), filtered_msg_count_(0), start_(timestamp), + latest_raw_time_(-1), latest_filtered_time_(-1) {} + +double AggregatorCore::raw_frequency() const { + if (latest_raw_time_ <= start_) { + return 0.0; + } + return static_cast(raw_msg_count_) / (latest_raw_time_ - start_); +} + +double AggregatorCore::filtered_frequency() const { + if (latest_filtered_time_ <= start_) { + return 0.0; + } + return static_cast(filtered_msg_count_) / + (latest_filtered_time_ - start_); +} + +void AggregatorCore::add_raw_msg( + const sample_msgs::msg::Unfiltered::SharedPtr msg) { + latest_raw_time_ = + std::max(static_cast(msg->timestamp), latest_raw_time_); + raw_msg_count_++; +} + +void AggregatorCore::add_filtered_msg( + const sample_msgs::msg::FilteredArray::SharedPtr msg) { + for (auto filtered_msg : msg->packets) { + latest_filtered_time_ = std::max( + static_cast(filtered_msg.timestamp), latest_filtered_time_); + } + filtered_msg_count_++; +} + +} // namespace samples diff --git a/autonomy/interfacing/aggregator/src/aggregator_node.cpp b/autonomy/interfacing/aggregator/src/aggregator_node.cpp new file mode 100644 index 0000000..8bf5b87 --- /dev/null +++ b/autonomy/interfacing/aggregator/src/aggregator_node.cpp @@ -0,0 +1,41 @@ +#include +#include + +#include "aggregator_node.hpp" + +AggregatorNode::AggregatorNode() + : Node("aggregator"), + aggregator_(samples::AggregatorCore( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count())) { + raw_sub_ = this->create_subscription( + "/unfiltered_topic", ADVERTISING_FREQ, + std::bind(&AggregatorNode::unfiltered_callback, this, + std::placeholders::_1)); + filtered_sub_ = this->create_subscription( + "/filtered_topic", ADVERTISING_FREQ, + std::bind(&AggregatorNode::filtered_callback, this, + std::placeholders::_1)); +} + +void AggregatorNode::unfiltered_callback( + const sample_msgs::msg::Unfiltered::SharedPtr msg) { + aggregator_.add_raw_msg(msg); + RCLCPP_INFO(this->get_logger(), "Raw Frequency(msg/s): %f", + aggregator_.raw_frequency() * 1000); +} + +void AggregatorNode::filtered_callback( + const sample_msgs::msg::FilteredArray::SharedPtr msg) { + aggregator_.add_filtered_msg(msg); + RCLCPP_INFO(this->get_logger(), "Filtered Frequency(msg/s): %f", + aggregator_.filtered_frequency() * 1000); +} + +int main(int argc, char **argv) { + rclcpp::init(argc, argv); + rclcpp::spin(std::make_shared()); + rclcpp::shutdown(); + return 0; +} diff --git a/autonomy/interfacing/aggregator/test/aggregator_test.cpp b/autonomy/interfacing/aggregator/test/aggregator_test.cpp new file mode 100644 index 0000000..3e5c2db --- /dev/null +++ b/autonomy/interfacing/aggregator/test/aggregator_test.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include "rclcpp/rclcpp.hpp" +#include "gtest/gtest.h" + +#include "aggregator_core.hpp" + +TEST(AggregatorTest, RawDivisionByZero) { + samples::AggregatorCore aggregator(0); + auto msg = std::make_shared(); + msg->timestamp = 0; + + aggregator.add_raw_msg(msg); + EXPECT_DOUBLE_EQ(0.0, aggregator.raw_frequency()); +} + +TEST(AggregatorTest, FilteredDivisionByZero) { + samples::AggregatorCore aggregator(1); + auto filtered_packet = std::make_shared(); + std::vector msgs; + auto msg = sample_msgs::msg::Filtered(); + msg.timestamp = 1; + msgs.push_back(msg); + filtered_packet->packets = msgs; + + aggregator.add_filtered_msg(filtered_packet); + EXPECT_DOUBLE_EQ(0.0, aggregator.filtered_frequency()); +} + +TEST(AggregatorTest, RawFrequencyAddSingleMessage) { + samples::AggregatorCore aggregator(0); + auto msg = std::make_shared(); + + msg->timestamp = 2; + aggregator.add_raw_msg(msg); + EXPECT_DOUBLE_EQ(0.5, aggregator.raw_frequency()); +} + +TEST(AggregatorTest, RawFrequencyAddMultipleMessages) { + samples::AggregatorCore aggregator(0); + auto msg = std::make_shared(); + + msg->timestamp = 2; + aggregator.add_raw_msg(msg); + EXPECT_DOUBLE_EQ(0.5, aggregator.raw_frequency()); + + msg->timestamp = 1; + aggregator.add_raw_msg(msg); + EXPECT_DOUBLE_EQ(1.0, aggregator.raw_frequency()); + + msg->timestamp = 5; + aggregator.add_raw_msg(msg); + EXPECT_DOUBLE_EQ(0.6, aggregator.raw_frequency()); +} + +TEST(AggregatorTest, FilteredUnorderedTimestamps) { + samples::AggregatorCore aggregator(0); + auto filtered_packet = std::make_shared(); + std::vector msgs; + auto msg = sample_msgs::msg::Filtered(); + + msg.timestamp = 5; + msgs.push_back(msg); + msg.timestamp = 2; + msgs.push_back(msg); + msg.timestamp = 3; + msgs.push_back(msg); + filtered_packet->packets = msgs; + + aggregator.add_filtered_msg(filtered_packet); + EXPECT_DOUBLE_EQ(0.2, aggregator.filtered_frequency()); +} diff --git a/autonomy/interfacing/can/can/can_node.py b/autonomy/interfacing/can/can/can_node.py index 89281db..dcdf212 100755 --- a/autonomy/interfacing/can/can/can_node.py +++ b/autonomy/interfacing/can/can/can_node.py @@ -28,7 +28,8 @@ def publish_position(self): print("test") def main(args=None): - print("hello") + while True: + time.sleep(1) if __name__ == '__main__': main() diff --git a/autonomy/interfacing/test.cpp b/autonomy/interfacing/test.cpp new file mode 100644 index 0000000..e69de29 diff --git a/docker/interfacing/interfacing.Dockerfile b/docker/interfacing/interfacing.Dockerfile index 8e0053c..1439123 100644 --- a/docker/interfacing/interfacing.Dockerfile +++ b/docker/interfacing/interfacing.Dockerfile @@ -21,7 +21,8 @@ FROM ${BASE_IMAGE} AS dependencies # Install Rosdep requirements COPY --from=source /tmp/colcon_install_list /tmp/colcon_install_list -RUN apt-fast install -qq -y --no-install-recommends $(cat /tmp/colcon_install_list) +RUN apt-fast install -qq -y --no-install-recommends \ + $(cat /tmp/colcon_install_list) can-utils # Copy in source code from source stage WORKDIR ${AMENT_WS} diff --git a/docs/Architecture_Map.odg b/docs/Architecture_Map.odg index a9eadb725a9f5bd49d3f80ef5e101bdf3c729d4a..b20c7875c4343f1190434156e622579ee933586b 100644 GIT binary patch delta 19218 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 Jo6Dl&zW{TZjD-LI delta 29909 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 SF3)_z((c_5fK7-0Q~N(=0tb%( diff --git a/modules/docker-compose.interfacing.yaml b/modules/docker-compose.interfacing.yaml index 5b0ca34..a135d24 100644 --- a/modules/docker-compose.interfacing.yaml +++ b/modules/docker-compose.interfacing.yaml @@ -11,5 +11,8 @@ services: image: "${INTERFACING_IMAGE:?}:${TAG}" profiles: [deploy, develop] command: /bin/bash -c "ros2 launch can can.launch.py" + privileged: true + devices: + - /dev/canable:/dev/canable volumes: - - ${MONO_DIR}/autonomy/interfacing:/root/ament_ws/src/interfacing + - ${MONO_DIR}/autonomy/interfacing:/root/ament_ws/src/interfacing \ No newline at end of file From a0eff26a807498efddce5069fe829d88ef2bdf75 Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Wed, 21 May 2025 18:39:04 -0400 Subject: [PATCH 09/36] Mounting CANable onto /dev/canable port --- docker/interfacing/interfacing.Dockerfile | 5 ++++- modules/docker-compose.interfacing.yaml | 5 ++++- watod-config.sh | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docker/interfacing/interfacing.Dockerfile b/docker/interfacing/interfacing.Dockerfile index 1439123..0b74b4f 100644 --- a/docker/interfacing/interfacing.Dockerfile +++ b/docker/interfacing/interfacing.Dockerfile @@ -22,7 +22,7 @@ FROM ${BASE_IMAGE} AS dependencies # Install Rosdep requirements COPY --from=source /tmp/colcon_install_list /tmp/colcon_install_list RUN apt-fast install -qq -y --no-install-recommends \ - $(cat /tmp/colcon_install_list) can-utils + $(cat /tmp/colcon_install_list) can-utils net-tools iproute2 # Copy in source code from source stage WORKDIR ${AMENT_WS} @@ -45,6 +45,9 @@ RUN . /opt/ros/$ROS_DISTRO/setup.sh && \ # Source and Build Artifact Cleanup RUN rm -rf src/* build/* devel/* install/* log/* +# pass through the udev-created symlinks to the container. +ENV UDEV=1 + # Entrypoint will run before any CMD on launch. Sources ~/opt//setup.bash and ~/ament_ws/install/setup.bash COPY docker/wato_ros_entrypoint.sh ${AMENT_WS}/wato_ros_entrypoint.sh ENTRYPOINT ["./wato_ros_entrypoint.sh"] diff --git a/modules/docker-compose.interfacing.yaml b/modules/docker-compose.interfacing.yaml index a135d24..082cf3a 100644 --- a/modules/docker-compose.interfacing.yaml +++ b/modules/docker-compose.interfacing.yaml @@ -11,8 +11,11 @@ services: image: "${INTERFACING_IMAGE:?}:${TAG}" profiles: [deploy, develop] command: /bin/bash -c "ros2 launch can can.launch.py" - privileged: true + privileged: false devices: - /dev/canable:/dev/canable + network_mode: host # Allow the container to access the host's network interfaces + cap_add: # Grant the capability to configure network interfaces + - NET_ADMIN volumes: - ${MONO_DIR}/autonomy/interfacing:/root/ament_ws/src/interfacing \ No newline at end of file diff --git a/watod-config.sh b/watod-config.sh index d7ac13e..2a97203 100755 --- a/watod-config.sh +++ b/watod-config.sh @@ -17,7 +17,7 @@ ## - simulation : starts simulation ## - samples : starts sample ROS2 pubsub nodes -ACTIVE_MODULES="" +ACTIVE_MODULES="interfacing" ################################# MODE OF OPERATION ################################# ## Possible modes of operation when running watod. From 04a2de4c043647dedbe94215e4ddcc225d8ff96b Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Fri, 23 May 2025 14:24:14 -0400 Subject: [PATCH 10/36] Convert CAN package to C++ --- autonomy/interfacing/can/CMakeLists.txt | 67 +++++++++++++++++++ autonomy/interfacing/can/can/can_core.py | 26 ------- autonomy/interfacing/can/can/can_node.py | 35 ---------- autonomy/interfacing/can/config/params.yaml | 15 ++++- autonomy/interfacing/can/include/can_core.hpp | 18 +++++ autonomy/interfacing/can/include/can_node.hpp | 14 ++++ autonomy/interfacing/can/launch/can.launch.py | 62 +++++++++-------- autonomy/interfacing/can/package.xml | 19 +++--- autonomy/interfacing/can/resource/can | 0 autonomy/interfacing/can/setup.cfg | 4 -- autonomy/interfacing/can/setup.py | 34 ---------- autonomy/interfacing/can/src/can_core.cpp | 10 +++ autonomy/interfacing/can/src/can_node.cpp | 12 ++++ .../{can/__init__.py => test/test_can.cpp} | 0 autonomy/interfacing/can/test/test_can.py | 23 ------- .../interfacing/can/test/test_copyright.py | 23 ------- autonomy/interfacing/can/test/test_flake8.py | 25 ------- autonomy/interfacing/can/test/test_pep257.py | 23 ------- 18 files changed, 174 insertions(+), 236 deletions(-) create mode 100644 autonomy/interfacing/can/CMakeLists.txt delete mode 100644 autonomy/interfacing/can/can/can_core.py delete mode 100755 autonomy/interfacing/can/can/can_node.py create mode 100644 autonomy/interfacing/can/include/can_core.hpp create mode 100644 autonomy/interfacing/can/include/can_node.hpp mode change 100755 => 100644 autonomy/interfacing/can/launch/can.launch.py delete mode 100644 autonomy/interfacing/can/resource/can delete mode 100644 autonomy/interfacing/can/setup.cfg delete mode 100755 autonomy/interfacing/can/setup.py create mode 100644 autonomy/interfacing/can/src/can_core.cpp create mode 100644 autonomy/interfacing/can/src/can_node.cpp rename autonomy/interfacing/can/{can/__init__.py => test/test_can.cpp} (100%) mode change 100755 => 100644 delete mode 100644 autonomy/interfacing/can/test/test_can.py delete mode 100755 autonomy/interfacing/can/test/test_copyright.py delete mode 100755 autonomy/interfacing/can/test/test_flake8.py delete mode 100755 autonomy/interfacing/can/test/test_pep257.py diff --git a/autonomy/interfacing/can/CMakeLists.txt b/autonomy/interfacing/can/CMakeLists.txt new file mode 100644 index 0000000..adad801 --- /dev/null +++ b/autonomy/interfacing/can/CMakeLists.txt @@ -0,0 +1,67 @@ +cmake_minimum_required(VERSION 3.10) +project(can) + +# Set compiler to C++ 17 Standard +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# Find dependencies +find_package(ament_cmake REQUIRED) # ROS2 build tool +find_package(rclcpp REQUIRED) # ROS2 C++ package +find_package(std_msgs REQUIRED) # ROS2 message package + +# Compiles source files into a library +# A library is not executed, instead other executables can link +# against it to access defined methods and classes. +# We build a library so that the methods defined can be used by +# both the unit test and ROS2 node executables. +add_library(can_lib + src/can_core.cpp +) + +# Indicate to compiler where to search for header files +target_include_directories(can_lib + PUBLIC + include +) + +# Add ROS2 dependencies required by package +ament_target_dependencies(can_lib + rclcpp + std_msgs +) + +# Create the can_node executable +add_executable(can_node src/can_node.cpp src/can_core.cpp) +ament_target_dependencies(can_node rclcpp std_msgs) +# Link to the previously built libraries +target_link_libraries(can_node can_lib) + +# Copy executable to installation location +install(TARGETS + can_node + DESTINATION lib/${PROJECT_NAME}) + +# Install launch and config files +install(DIRECTORY + launch + config + DESTINATION share/${PROJECT_NAME}) + +# Testing +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() + + # Add tests + # ament_add_gtest(can_test test/can_test.cpp) + # target_include_directories(can_test PUBLIC include) + # target_link_libraries(can_test) +endif() + +ament_package() diff --git a/autonomy/interfacing/can/can/can_core.py b/autonomy/interfacing/can/can/can_core.py deleted file mode 100644 index a9be87e..0000000 --- a/autonomy/interfacing/can/can/can_core.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2023 WATonomous -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math - - -class CanCore(): - - def __init__(self, pos_x, pos_y, pos_z, vel): - # Init member variables for serialization - print("test") - - def serialize_data(self): - return "x:" + str(self.__pos_x) + ";y:" + \ - str(self.__pos_y) + ";z:" + str(self.__pos_z) + ";" diff --git a/autonomy/interfacing/can/can/can_node.py b/autonomy/interfacing/can/can/can_node.py deleted file mode 100755 index dcdf212..0000000 --- a/autonomy/interfacing/can/can/can_node.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2023 WATonomous -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import rclpy -from rclpy.node import Node - -from sample_msgs.msg import Unfiltered -from can.can_core import CanCore - - -class CanNode(Node): - def __init__(self): - print("test") - def publish_position(self): - print("test") - -def main(args=None): - while True: - time.sleep(1) - -if __name__ == '__main__': - main() diff --git a/autonomy/interfacing/can/config/params.yaml b/autonomy/interfacing/can/config/params.yaml index cca06ec..7e3d121 100644 --- a/autonomy/interfacing/can/config/params.yaml +++ b/autonomy/interfacing/can/config/params.yaml @@ -1,7 +1,16 @@ -# more info on YAML configs for ROS2 Params: -# https://roboticsbackend.com/ros2-yaml-params/ -can_node: +/** + * Parameters for the CAN node + */ +can_node: ros__parameters: + # The name of the CAN interface to use (e.g., can0, can1, slcan0) + can_interface: "can0" + + # Publishing rate in Hz for checking incoming CAN messages + publish_rate_hz: 50 + + # more info on YAML configs for ROS2 Params: + # https://roboticsbackend.com/ros2-yaml-params/ pos_x: 1.0 pos_y: 1.0 pos_z: 1.0 diff --git a/autonomy/interfacing/can/include/can_core.hpp b/autonomy/interfacing/can/include/can_core.hpp new file mode 100644 index 0000000..27e3bd7 --- /dev/null +++ b/autonomy/interfacing/can/include/can_core.hpp @@ -0,0 +1,18 @@ +#ifndef CAN_CORE_HPP +#define CAN_CORE_HPP + +#include "rclcpp/rclcpp.hpp" + +namespace autonomy +{ + +class CanCore { + public: + CanCore(const rclcpp::Logger& logger); + + private: + rclcpp::Logger logger_; +}; +} + +#endif // CAN_CORE_HPP diff --git a/autonomy/interfacing/can/include/can_node.hpp b/autonomy/interfacing/can/include/can_node.hpp new file mode 100644 index 0000000..fb118c8 --- /dev/null +++ b/autonomy/interfacing/can/include/can_node.hpp @@ -0,0 +1,14 @@ +#ifndef CAN_NODE_HPP +#define CAN_NODE_HPP + +#include "rclcpp/rclcpp.hpp" +#include "can_core.hpp" + +class CanNode : public rclcpp::Node { +public: + CanNode(); + +private: +}; + +#endif // CAN_NODE_HPP diff --git a/autonomy/interfacing/can/launch/can.launch.py b/autonomy/interfacing/can/launch/can.launch.py old mode 100755 new mode 100644 index 7ccb921..34753f6 --- a/autonomy/interfacing/can/launch/can.launch.py +++ b/autonomy/interfacing/can/launch/can.launch.py @@ -1,39 +1,37 @@ -# Copyright 2023 WATonomous -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from ament_index_python.packages import get_package_share_directory - from launch import LaunchDescription from launch_ros.actions import Node - +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration def generate_launch_description(): - # To load the yaml file, we are searching for its - # path is the share directory. Check setup.py for how - # the param file got there - param_file_path = os.path.join( - get_package_share_directory('can'), - 'config', - 'params.yaml' + # Declare launch arguments + can_interface_arg = DeclareLaunchArgument( + 'can_interface', + default_value='can0', + description='Name of the CAN interface to use (e.g., can0)' ) - + + publish_rate_arg = DeclareLaunchArgument( + 'publish_rate_hz', + default_value='50', + description='Rate in Hz at which to check for CAN messages' + ) + + # Create the CAN node + can_node = Node( + package='can', + executable='can_node', + name='can_node', + parameters=[{ + 'can_interface': LaunchConfiguration('can_interface'), + 'publish_rate_hz': LaunchConfiguration('publish_rate_hz') + }], + output='screen' + ) + + # Return the launch description return LaunchDescription([ - Node( - package='can', - name='can_node', - executable='can_node', - parameters=[param_file_path] - ) + can_interface_arg, + publish_rate_arg, + can_node ]) diff --git a/autonomy/interfacing/can/package.xml b/autonomy/interfacing/can/package.xml index 174962d..3cd227d 100644 --- a/autonomy/interfacing/can/package.xml +++ b/autonomy/interfacing/can/package.xml @@ -3,16 +3,19 @@ can 0.0.0 - can communication layer - miekale - Apache2.0: License declaration + ROS 2 C++ package for CAN bus communication + Gavin Tranquilino + Apache2.0 - ament_copyright - ament_flake8 - ament_pep257 - python3-pytest + ament_cmake + + rclcpp + std_msgs + + ament_lint_auto + ament_lint_common - ament_python + ament_cmake diff --git a/autonomy/interfacing/can/resource/can b/autonomy/interfacing/can/resource/can deleted file mode 100644 index e69de29..0000000 diff --git a/autonomy/interfacing/can/setup.cfg b/autonomy/interfacing/can/setup.cfg deleted file mode 100644 index 6c9e367..0000000 --- a/autonomy/interfacing/can/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[develop] -script_dir=$base/lib/can -[install] -install-scripts=$base/lib/can diff --git a/autonomy/interfacing/can/setup.py b/autonomy/interfacing/can/setup.py deleted file mode 100755 index 059a41d..0000000 --- a/autonomy/interfacing/can/setup.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -from glob import glob -from setuptools import setup - -package_name = 'can' - -setup( - name=package_name, - version='0.0.0', - packages=[package_name], - data_files=[ - # Install marker file in the package index - ('share/ament_index/resource_index/packages', ['resource/' + package_name]), - # Include our package.xml file - (os.path.join('share', package_name), ['package.xml']), - # Include all launch files - (os.path.join('share', package_name, 'launch'), - glob(os.path.join('launch', '*.launch.py'))), - # Include config files for parameters - (os.path.join('share', package_name, 'config'), glob(os.path.join('config', '*.yaml'))), - ], - install_requires=['setuptools'], - zip_safe=True, - maintainer='miekale', - maintainer_email='miekale.smith@uwaterloo.ca', - description='TODO: Package description', - license='TODO: License declaration', - tests_require=['pytest'], - entry_points={ - 'console_scripts': [ - 'can_node = can.can_node:main' - ], - }, -) diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp new file mode 100644 index 0000000..c3c3e0c --- /dev/null +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -0,0 +1,10 @@ +#include "can_core.hpp" + +namespace autonomy +{ + +CanCore::CanCore(const rclcpp::Logger& logger) : logger_(logger) { + RCLCPP_INFO(logger_, "CanCore object initialized."); +} + +} diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp new file mode 100644 index 0000000..294cc66 --- /dev/null +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -0,0 +1,12 @@ +#include "can_node.hpp" + +CanNode::CanNode() : Node("can_node") { + RCLCPP_INFO(this->get_logger(), "CAN Node has been initialized"); +} + +int main(int argc, char **argv) { + rclcpp::init(argc, argv); + rclcpp::spin(std::make_shared()); + rclcpp::shutdown(); + return 0; +} diff --git a/autonomy/interfacing/can/can/__init__.py b/autonomy/interfacing/can/test/test_can.cpp old mode 100755 new mode 100644 similarity index 100% rename from autonomy/interfacing/can/can/__init__.py rename to autonomy/interfacing/can/test/test_can.cpp diff --git a/autonomy/interfacing/can/test/test_can.py b/autonomy/interfacing/can/test/test_can.py deleted file mode 100644 index 7252792..0000000 --- a/autonomy/interfacing/can/test/test_can.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2023 WATonomous -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from can.can_core import CanCore - - -# def test_update_position(): -# can_core = CanCore(1, 1, 1, 1) -# can_core.update_position() - -# assert can_core.serialize_data() == \ -# "x:1.5773502691896257;y:1.5773502691896257;z:1.5773502691896257;" diff --git a/autonomy/interfacing/can/test/test_copyright.py b/autonomy/interfacing/can/test/test_copyright.py deleted file mode 100755 index cc8ff03..0000000 --- a/autonomy/interfacing/can/test/test_copyright.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_copyright.main import main -import pytest - - -@pytest.mark.copyright -@pytest.mark.linter -def test_copyright(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found errors' diff --git a/autonomy/interfacing/can/test/test_flake8.py b/autonomy/interfacing/can/test/test_flake8.py deleted file mode 100755 index 27ee107..0000000 --- a/autonomy/interfacing/can/test/test_flake8.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2017 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_flake8.main import main_with_errors -import pytest - - -@pytest.mark.flake8 -@pytest.mark.linter -def test_flake8(): - rc, errors = main_with_errors(argv=[]) - assert rc == 0, \ - 'Found %d code style errors / warnings:\n' % len(errors) + \ - '\n'.join(errors) diff --git a/autonomy/interfacing/can/test/test_pep257.py b/autonomy/interfacing/can/test/test_pep257.py deleted file mode 100755 index b234a38..0000000 --- a/autonomy/interfacing/can/test/test_pep257.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_pep257.main import main -import pytest - - -@pytest.mark.linter -@pytest.mark.pep257 -def test_pep257(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found code style errors / warnings' From 5176ec0ba740ed4ea1c210b35ae53ca22b8ca18f Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Fri, 23 May 2025 14:47:18 -0400 Subject: [PATCH 11/36] Add CAN package README.md template --- autonomy/interfacing/can/README.md | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/autonomy/interfacing/can/README.md b/autonomy/interfacing/can/README.md index e69de29..a1f0a5f 100644 --- a/autonomy/interfacing/can/README.md +++ b/autonomy/interfacing/can/README.md @@ -0,0 +1,56 @@ +## CAN Interfacing Package Documentation + +The documentation for the CAN interfacing package follows a standard WATOnomous package level scheme. As shown below: +``` +can/ +├── CMakeLists.txt +├── README.md +├── package.xml +├── config/ +│ └── params.yaml +├── include/ +│ ├── can_core.hpp +│ └── can_node.hpp +├── launch/ +│ └── can.launch.py +├── src/ +│ ├── can_core.cpp +│ └── can_node.cpp +└── test/ + └── test_can.cpp +``` + +### Package Level Overview + +#### Purpose +This ROS 2 package serves as a communication bridge between a high-level controller and an embedded hand system via CAN bus. The package enables: +- Bidirectional communication between ROS 2 nodes and embedded systems +- Protocol translation between ROS 2 messages and CAN Bus frames +- Real-time feedback loop for hand control +- Integration of embedded hand systems within the WATonomous ROS2 ecosystem + +The package implements the following workflow: +1. Receive ROS 2 movement messages from the controller/behaviour node +2. Convert and send these messages as CAN frames through a USB CAN transceiver (accessible within a Docker container) +3. Receive CAN frames from the embedded system containing hand odometry +4. Convert and publish arm odometry as ROS2 messages for feedback to the controller + +#### Inputs & Outputs +- Data flow, including message types, service calls, or file interactions + +#### Key Features +- Key classes, nodes, or scripts, along with their relationships + +#### Usage +- How to build, run, and test the package + +#### Configuration +- Relevant parameters, environment variables, or dependencies + +### Package Architecture + + +### Infrastructure Documentation +1. [Documentation Structure of Repo](docs/README.md) +2. [Project Infrastructure Development Docs](https://github.com/WATonomous/wato_monorepo/tree/main/docs/dev/) +3. [ROS Structure Docs](src/samples/README.md) \ No newline at end of file From a4bcd126a6152e9e12025b643e24e55e0ba19240 Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Mon, 26 May 2025 12:17:30 -0400 Subject: [PATCH 12/36] Added TODOs --- autonomy/interfacing/can/CMakeLists.txt | 4 + autonomy/interfacing/can/README.md | 24 +- autonomy/interfacing/can/config/params.yaml | 29 ++- autonomy/interfacing/can/include/can_core.hpp | 65 ++++- autonomy/interfacing/can/include/can_node.hpp | 1 + autonomy/interfacing/can/src/can_core.cpp | 239 +++++++++++++++++- autonomy/interfacing/can/src/can_node.cpp | 32 ++- 7 files changed, 375 insertions(+), 19 deletions(-) diff --git a/autonomy/interfacing/can/CMakeLists.txt b/autonomy/interfacing/can/CMakeLists.txt index adad801..260cf43 100644 --- a/autonomy/interfacing/can/CMakeLists.txt +++ b/autonomy/interfacing/can/CMakeLists.txt @@ -36,6 +36,10 @@ ament_target_dependencies(can_lib std_msgs ) +# Link system libraries for CAN support +# Note: Using system CAN libraries instead of libsocketcan for broader compatibility +target_link_libraries(can_lib) + # Create the can_node executable add_executable(can_node src/can_node.cpp src/can_core.cpp) ament_target_dependencies(can_node rclcpp std_msgs) diff --git a/autonomy/interfacing/can/README.md b/autonomy/interfacing/can/README.md index a1f0a5f..0093967 100644 --- a/autonomy/interfacing/can/README.md +++ b/autonomy/interfacing/can/README.md @@ -39,7 +39,29 @@ The package implements the following workflow: - Data flow, including message types, service calls, or file interactions #### Key Features -- Key classes, nodes, or scripts, along with their relationships + +**Architecture Design Pattern: Core vs Node Separation** + +This package follows a clean architecture pattern that separates concerns between hardware abstraction and ROS integration: + +- **CanCore** (`can_core.hpp/.cpp`): + - Pure C++ class handling low-level CAN bus operations + - Hardware abstraction layer for CAN communication + - Reusable component independent of ROS + - Responsibilities: socket management, frame transmission/reception, interface setup + - Could be used in non-ROS applications or embedded systems + +- **CanNode** (`can_node.hpp/.cpp`): + - ROS 2 Node class managing ROS ecosystem integration + - Message translation between ROS and CAN protocols + - ROS-specific functionality: publishers, subscribers, parameters, timers + - Responsibilities: ROS message handling, topic management, service calls + +This separation provides: +- **Single Responsibility**: Each class has one clear purpose +- **Testability**: CanCore can be unit tested without ROS overhead +- **Reusability**: CanCore can be used in other projects +- **Maintainability**: CAN protocol changes only affect CanCore, ROS changes only affect CanNode #### Usage - How to build, run, and test the package diff --git a/autonomy/interfacing/can/config/params.yaml b/autonomy/interfacing/can/config/params.yaml index 7e3d121..2e26db2 100644 --- a/autonomy/interfacing/can/config/params.yaml +++ b/autonomy/interfacing/can/config/params.yaml @@ -3,16 +3,23 @@ */ can_node: ros__parameters: - # The name of the CAN interface to use (e.g., can0, can1, slcan0) - can_interface: "can0" + # CAN Interface Configuration + can_interface: "can0" # CAN interface name (can0, can1, etc.) + device_path: "/dev/ttyACM0" # Serial device path for SLCAN + bustype: "slcan" # Bus type: "socketcan" or "slcan" + bitrate: 500000 # CAN bitrate in bps (125000, 250000, 500000, 1000000) - # Publishing rate in Hz for checking incoming CAN messages - publish_rate_hz: 50 - - # more info on YAML configs for ROS2 Params: - # https://roboticsbackend.com/ros2-yaml-params/ - pos_x: 1.0 - pos_y: 1.0 - pos_z: 1.0 + # Communication Settings + publish_rate_hz: 50 # Rate for checking incoming CAN messages + receive_timeout_ms: 100 # Timeout for receiving CAN messages - velocity: 1.0 \ No newline at end of file + # CAN Message IDs (example configuration) + hand_command_id: 0x100 # CAN ID for sending hand commands + hand_status_id: 0x200 # CAN ID for receiving hand status + hand_odometry_id: 0x300 # CAN ID for receiving hand odometry + + # Hand Control Parameters + max_finger_position: 100.0 # Maximum finger position value + max_finger_velocity: 50.0 # Maximum finger velocity value + position_scale_factor: 1.0 # Scaling factor for position commands + velocity_scale_factor: 1.0 # Scaling factor for velocity commands \ No newline at end of file diff --git a/autonomy/interfacing/can/include/can_core.hpp b/autonomy/interfacing/can/include/can_core.hpp index 27e3bd7..52ce8d9 100644 --- a/autonomy/interfacing/can/include/can_core.hpp +++ b/autonomy/interfacing/can/include/can_core.hpp @@ -2,17 +2,74 @@ #define CAN_CORE_HPP #include "rclcpp/rclcpp.hpp" +#include +#include +#include namespace autonomy { +struct CanMessage { + uint32_t id; // CAN message ID + std::vector data; // Message data (up to 8 bytes for standard CAN) + bool is_extended_id; // Extended frame format flag + bool is_remote_frame; // Remote transmission request flag + uint64_t timestamp_us; // Timestamp in microseconds +}; + +struct CanConfig { + std::string interface_name; // CAN interface name (e.g., "can0") + std::string device_path; // Device path for SLCAN (e.g., "/dev/ttyACM0") + std::string bustype; // Bus type: "socketcan" or "slcan" + uint32_t bitrate; // Bitrate in bps + uint32_t receive_timeout_ms; // Receive timeout in milliseconds +}; + class CanCore { - public: +public: CanCore(const rclcpp::Logger& logger); - - private: - rclcpp::Logger logger_; + + // CAN Interface Management + bool initialize(const CanConfig& config); + bool shutdown(); + bool isInitialized() const; + + // Message Transmission + bool sendMessage(const CanMessage& message); + bool sendMessage(uint32_t id, const std::vector& data, bool is_extended_id = false); + + // Message Reception + bool receiveMessage(CanMessage& message); + bool receiveMessage(uint32_t& id, std::vector& data, bool& is_extended_id); + + // Configuration and Status + bool setBitrate(uint32_t bitrate); + uint32_t getBitrate() const; + std::string getInterfaceInfo() const; + bool isConnected() const; + + // Error Handling + std::string getLastError() const; + void clearErrors(); + +private: + rclcpp::Logger logger_; + + // Internal state + int socket_fd_; + bool initialized_; + bool connected_; + CanConfig config_; + std::string last_error_; + + // Internal helper methods + bool setupSocketCan(); + bool setupSlcan(); + bool configureInterface(); + bool validateMessage(const CanMessage& message); + void logCanMessage(const CanMessage& message, bool is_tx); }; + } #endif // CAN_CORE_HPP diff --git a/autonomy/interfacing/can/include/can_node.hpp b/autonomy/interfacing/can/include/can_node.hpp index fb118c8..33ffab5 100644 --- a/autonomy/interfacing/can/include/can_node.hpp +++ b/autonomy/interfacing/can/include/can_node.hpp @@ -9,6 +9,7 @@ class CanNode : public rclcpp::Node { CanNode(); private: + autonomy::CanCore can_; }; #endif // CAN_NODE_HPP diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp index c3c3e0c..3458ef1 100644 --- a/autonomy/interfacing/can/src/can_core.cpp +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -1,10 +1,245 @@ #include "can_core.hpp" +#include +#include +#include +#include +#include +#include +#include namespace autonomy { -CanCore::CanCore(const rclcpp::Logger& logger) : logger_(logger) { - RCLCPP_INFO(logger_, "CanCore object initialized."); +CanCore::CanCore(const rclcpp::Logger& logger) + : logger_(logger), socket_fd_(-1), initialized_(false), connected_(false) +{ + RCLCPP_INFO(logger_, "CAN Core initialized"); +} + +bool CanCore::initialize(const CanConfig& config) +{ + RCLCPP_INFO(logger_, "Initializing CAN interface: %s", config.interface_name.c_str()); + + config_ = config; + + if (config.bustype == "socketcan") { + return setupSocketCan(); + } else if (config.bustype == "slcan") { + return setupSlcan(); + } else { + last_error_ = "Unsupported bus type: " + config.bustype; + RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + return false; + } +} + +bool CanCore::shutdown() +{ + RCLCPP_INFO(logger_, "Shutting down CAN interface"); + + if (socket_fd_ >= 0) { + close(socket_fd_); + socket_fd_ = -1; + } + + initialized_ = false; + connected_ = false; + + return true; +} + +bool CanCore::isInitialized() const +{ + return initialized_; +} + +bool CanCore::sendMessage(const CanMessage& message) +{ + if (!validateMessage(message)) { + return false; + } + + if (!connected_) { + last_error_ = "CAN interface not connected"; + RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + return false; + } + + // TODO: Implement actual CAN message transmission using socket_fd_ + logCanMessage(message, true); + RCLCPP_DEBUG(logger_, "Sent CAN message ID: 0x%X", message.id); + + return true; +} + +bool CanCore::sendMessage(uint32_t id, const std::vector& data, bool is_extended_id) +{ + CanMessage message; + message.id = id; + message.data = data; + message.is_extended_id = is_extended_id; + message.is_remote_frame = false; + message.timestamp_us = 0; // Will be set by system + + return sendMessage(message); +} + +bool CanCore::receiveMessage(CanMessage& message) +{ + if (!connected_) { + last_error_ = "CAN interface not connected"; + RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + return false; + } + + // TODO: Implement actual CAN message reception using socket_fd_ + // For now, return false indicating no message received + return false; +} + +bool CanCore::receiveMessage(uint32_t& id, std::vector& data, bool& is_extended_id) +{ + CanMessage message; + if (receiveMessage(message)) { + id = message.id; + data = message.data; + is_extended_id = message.is_extended_id; + return true; + } + + return false; +} + +bool CanCore::setBitrate(uint32_t bitrate) +{ + RCLCPP_INFO(logger_, "Setting bitrate to: %u", bitrate); + + config_.bitrate = bitrate; + + // TODO: Implement actual bitrate configuration + // This would require interface reconfiguration + + return true; +} + +uint32_t CanCore::getBitrate() const +{ + return config_.bitrate; +} + +std::string CanCore::getInterfaceInfo() const +{ + return "Interface: " + config_.interface_name + + ", Type: " + config_.bustype + + ", Bitrate: " + std::to_string(config_.bitrate); +} + +bool CanCore::isConnected() const +{ + return connected_; +} + +std::string CanCore::getLastError() const +{ + return last_error_; +} + +void CanCore::clearErrors() +{ + last_error_.clear(); +} + +bool CanCore::setupSocketCan() +{ + RCLCPP_INFO(logger_, "Setting up SocketCAN interface"); + + // Create socket + socket_fd_ = socket(PF_CAN, SOCK_RAW, CAN_RAW); + if (socket_fd_ < 0) { + last_error_ = "Failed to create SocketCAN socket"; + RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + return false; + } + + // TODO: Implement complete SocketCAN setup + // - Configure interface address + // - Bind socket to interface + // - Set socket options + + initialized_ = true; + connected_ = true; + + RCLCPP_INFO(logger_, "SocketCAN interface setup completed"); + return true; +} + +bool CanCore::setupSlcan() +{ + RCLCPP_INFO(logger_, "Setting up SLCAN interface"); + + // TODO: Implement SLCAN setup + // - Open serial device + // - Configure SLCAN protocol + // - Set bitrate + // - Open CAN interface + + initialized_ = true; + connected_ = true; + + RCLCPP_INFO(logger_, "SLCAN interface setup completed"); + return true; +} + +bool CanCore::configureInterface() +{ + RCLCPP_INFO(logger_, "Configuring CAN interface"); + + // TODO: Implement interface configuration + // - Set filters + // - Configure timeouts + // - Set error handling options + + return true; +} + +bool CanCore::validateMessage(const CanMessage& message) +{ + // Check message ID range + if (message.is_extended_id && message.id > 0x1FFFFFFF) { + last_error_ = "Extended CAN ID out of range"; + RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + return false; + } + + if (!message.is_extended_id && message.id > 0x7FF) { + last_error_ = "Standard CAN ID out of range"; + RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + return false; + } + + // Check data length + if (message.data.size() > 8) { + last_error_ = "CAN data length exceeds 8 bytes"; + RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + return false; + } + + return true; +} + +void CanCore::logCanMessage(const CanMessage& message, bool is_tx) +{ + std::string direction = is_tx ? "TX" : "RX"; + std::string data_str; + + for (const auto& byte : message.data) { + if (!data_str.empty()) data_str += " "; + data_str += std::to_string(byte); + } + + RCLCPP_DEBUG(logger_, "%s CAN: ID=0x%X, Data=[%s], Ext=%s", + direction.c_str(), message.id, data_str.c_str(), + message.is_extended_id ? "true" : "false"); } } diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index 294cc66..132216f 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -1,7 +1,37 @@ #include "can_node.hpp" -CanNode::CanNode() : Node("can_node") { +CanNode::CanNode() : Node("can_node"), can_(autonomy::CanCore(this->get_logger())) { RCLCPP_INFO(this->get_logger(), "CAN Node has been initialized"); + + // Load parameters from config/params.yaml + this->declare_parameter("can_interface", "can0"); + this->declare_parameter("device_path", "/dev/ttyACM0"); + this->declare_parameter("bustype", "slcan"); + this->declare_parameter("bitrate", 500000); + + // Get parameter values + std::string can_interface = this->get_parameter("can_interface").as_string(); + std::string device_path = this->get_parameter("device_path").as_string(); + std::string bustype = this->get_parameter("bustype").as_string(); + int bitrate = this->get_parameter("bitrate").as_int(); + + RCLCPP_INFO(this->get_logger(), "Loaded parameters: interface=%s, bustype=%s, bitrate=%d", + can_interface.c_str(), bustype.c_str(), bitrate); + + // Configure CanCore + autonomy::CanConfig config; + config.interface_name = can_interface; + config.device_path = device_path; + config.bustype = bustype; + config.bitrate = bitrate; + config.receive_timeout_ms = 100; + + // Initialize the CAN interface + if (can_.initialize(config)) { + RCLCPP_INFO(this->get_logger(), "CAN Core interface initialized successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to initialize CAN Core: %s", can_.getLastError().c_str()); + } } int main(int argc, char **argv) { From 7ff6029191da709d76b96e4ab1195b4723668acc Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Tue, 27 May 2025 15:25:24 -0400 Subject: [PATCH 13/36] Successfully run slcand to initialize can0 as a can device with master settings --- autonomy/interfacing/can/CMakeLists.txt | 7 + autonomy/interfacing/can/config/params.yaml | 6 +- autonomy/interfacing/can/launch/can.launch.py | 11 +- autonomy/interfacing/can/scripts/setup_can.sh | 35 +++++ autonomy/interfacing/can/src/can_core.cpp | 126 ++++++++++-------- autonomy/interfacing/can/src/can_node.cpp | 2 +- 6 files changed, 123 insertions(+), 64 deletions(-) create mode 100755 autonomy/interfacing/can/scripts/setup_can.sh diff --git a/autonomy/interfacing/can/CMakeLists.txt b/autonomy/interfacing/can/CMakeLists.txt index 260cf43..12cdd39 100644 --- a/autonomy/interfacing/can/CMakeLists.txt +++ b/autonomy/interfacing/can/CMakeLists.txt @@ -11,6 +11,7 @@ if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") endif() # Find dependencies +find_package(ament_index_cpp REQUIRED) # ROS tool to find shared directories of installed packages find_package(ament_cmake REQUIRED) # ROS2 build tool find_package(rclcpp REQUIRED) # ROS2 C++ package find_package(std_msgs REQUIRED) # ROS2 message package @@ -32,6 +33,7 @@ target_include_directories(can_lib # Add ROS2 dependencies required by package ament_target_dependencies(can_lib + ament_index_cpp rclcpp std_msgs ) @@ -57,6 +59,11 @@ install(DIRECTORY config DESTINATION share/${PROJECT_NAME}) +# Install scripts with execute permissions +install(PROGRAMS + scripts/setup_can.sh + DESTINATION share/${PROJECT_NAME}/scripts) + # Testing if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) diff --git a/autonomy/interfacing/can/config/params.yaml b/autonomy/interfacing/can/config/params.yaml index 2e26db2..26d93be 100644 --- a/autonomy/interfacing/can/config/params.yaml +++ b/autonomy/interfacing/can/config/params.yaml @@ -1,11 +1,9 @@ -/** - * Parameters for the CAN node - */ +# Parameters for the CAN node can_node: ros__parameters: # CAN Interface Configuration can_interface: "can0" # CAN interface name (can0, can1, etc.) - device_path: "/dev/ttyACM0" # Serial device path for SLCAN + device_path: "/dev/canable" # Serial device path for SLCAN bustype: "slcan" # Bus type: "socketcan" or "slcan" bitrate: 500000 # CAN bitrate in bps (125000, 250000, 500000, 1000000) diff --git a/autonomy/interfacing/can/launch/can.launch.py b/autonomy/interfacing/can/launch/can.launch.py index 34753f6..c8a630b 100644 --- a/autonomy/interfacing/can/launch/can.launch.py +++ b/autonomy/interfacing/can/launch/can.launch.py @@ -2,8 +2,17 @@ from launch_ros.actions import Node from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration +from ament_index_python.packages import get_package_share_directory +import os def generate_launch_description(): + # Get the path to the config file + config_file = os.path.join( + get_package_share_directory('can'), + 'config', + 'params.yaml' + ) + # Declare launch arguments can_interface_arg = DeclareLaunchArgument( 'can_interface', @@ -22,7 +31,7 @@ def generate_launch_description(): package='can', executable='can_node', name='can_node', - parameters=[{ + parameters=[config_file, { 'can_interface': LaunchConfiguration('can_interface'), 'publish_rate_hz': LaunchConfiguration('publish_rate_hz') }], diff --git a/autonomy/interfacing/can/scripts/setup_can.sh b/autonomy/interfacing/can/scripts/setup_can.sh new file mode 100755 index 0000000..64ebe44 --- /dev/null +++ b/autonomy/interfacing/can/scripts/setup_can.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status. +set -e + +DEVICE_PATH="$1" +INTERFACE_NAME="$2" +BITRATE_CODE="$3" # e.g., -s6 for 500k, -s8 for 1M + +if [ -z "$DEVICE_PATH" ] || [ -z "$INTERFACE_NAME" ] || [ -z "$BITRATE_CODE" ]; then + echo "Usage: $0 " + echo "Example: $0 /dev/ttyACM0 can0 -s6" + exit 1 +fi + +echo "Attempting to set up CAN interface $INTERFACE_NAME on $DEVICE_PATH with bitrate code $BITRATE_CODE..." + +# Check if slcand is already running for this device/interface to avoid errors +if pgrep -f "slcand.*$DEVICE_PATH.*$INTERFACE_NAME"; then + echo "slcand appears to be already running for $DEVICE_PATH and $INTERFACE_NAME. Assuming interface is (or will be) up." +else + echo "Starting slcand: slcand -o -c $BITRATE_CODE $DEVICE_PATH $INTERFACE_NAME" + slcand -o -c "$BITRATE_CODE" "$DEVICE_PATH" "$INTERFACE_NAME" + # Add a small delay to allow slcand to establish the interface + sleep 1 +fi + +echo "Bringing up interface: ifconfig $INTERFACE_NAME up" +ifconfig "$INTERFACE_NAME" up + +echo "Setting txqueuelen: ifconfig $INTERFACE_NAME txqueuelen 1000" +ifconfig "$INTERFACE_NAME" txqueuelen 1000 + +echo "CAN interface $INTERFACE_NAME setup complete." +exit 0 \ No newline at end of file diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp index 3458ef1..74dc275 100644 --- a/autonomy/interfacing/can/src/can_core.cpp +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -1,11 +1,16 @@ #include "can_core.hpp" -#include -#include -#include -#include +#include // Core socket functions for SocketCAN [socket(), bind(), send(), recv()] +#include // Definitions for can frames +#include // CAN RAW +#include #include -#include +#include #include +#include // For std::system to call the external script for slcand +#include // checking if script files exist + +#include "ament_index_cpp/get_package_share_directory.hpp" // to load the bash scripts +#include "ament_index_cpp/get_package_prefix.hpp" // defines PackageNotFoundError namespace autonomy { @@ -55,59 +60,22 @@ bool CanCore::isInitialized() const bool CanCore::sendMessage(const CanMessage& message) { - if (!validateMessage(message)) { - return false; - } - - if (!connected_) { - last_error_ = "CAN interface not connected"; - RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); - return false; - } - - // TODO: Implement actual CAN message transmission using socket_fd_ - logCanMessage(message, true); - RCLCPP_DEBUG(logger_, "Sent CAN message ID: 0x%X", message.id); - - return true; + return 0; } bool CanCore::sendMessage(uint32_t id, const std::vector& data, bool is_extended_id) { - CanMessage message; - message.id = id; - message.data = data; - message.is_extended_id = is_extended_id; - message.is_remote_frame = false; - message.timestamp_us = 0; // Will be set by system - - return sendMessage(message); + return 0; } bool CanCore::receiveMessage(CanMessage& message) { - if (!connected_) { - last_error_ = "CAN interface not connected"; - RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); - return false; - } - - // TODO: Implement actual CAN message reception using socket_fd_ - // For now, return false indicating no message received - return false; + return 0; } bool CanCore::receiveMessage(uint32_t& id, std::vector& data, bool& is_extended_id) { - CanMessage message; - if (receiveMessage(message)) { - id = message.id; - data = message.data; - is_extended_id = message.is_extended_id; - return true; - } - - return false; + return 0; } bool CanCore::setBitrate(uint32_t bitrate) @@ -117,7 +85,6 @@ bool CanCore::setBitrate(uint32_t bitrate) config_.bitrate = bitrate; // TODO: Implement actual bitrate configuration - // This would require interface reconfiguration return true; } @@ -175,19 +142,62 @@ bool CanCore::setupSocketCan() bool CanCore::setupSlcan() { - RCLCPP_INFO(logger_, "Setting up SLCAN interface"); - - // TODO: Implement SLCAN setup - // - Open serial device - // - Configure SLCAN protocol - // - Set bitrate - // - Open CAN interface + RCLCPP_INFO(logger_, "Setting up SLCAN interface '%s' via external script.", config_.interface_name.c_str()); + RCLCPP_INFO(logger_, " Device path for script: %s", config_.device_path.c_str()); + RCLCPP_INFO(logger_, " Bitrate for script: %u", config_.bitrate); + + // Run the slcand daemon to initialize the CAN bus + std::string package_share_directory; + try { + package_share_directory = ament_index_cpp::get_package_share_directory("can"); // the package we're in is can + } catch (const ament_index_cpp::PackageNotFoundError& e) { + RCLCPP_ERROR(logger_, "Package not found for script path: %s", e.what()); + last_error_ = "Could not find package 'can' to locate setup script."; + return false; + } + std::string script_path = package_share_directory + "/scripts/setup_can.sh"; + + // Check if the script actually exists at this script_path + struct stat buffer; + if (stat(script_path.c_str(), &buffer) != 0) { + RCLCPP_ERROR(logger_, "Setup script not found"); + last_error_ = "Setup script not found"; + return false; + } + + std::string bitrate_code; + + // Map config_.bitrate to the slcan code (e.g., 500000 -> "-s6") + if (config_.bitrate == 10000) bitrate_code = "-s0"; + else if (config_.bitrate == 20000) bitrate_code = "-s1"; + else if (config_.bitrate == 50000) bitrate_code = "-s2"; + else if (config_.bitrate == 100000) bitrate_code = "-s3"; + else if (config_.bitrate == 125000) bitrate_code = "-s4"; + else if (config_.bitrate == 250000) bitrate_code = "-s5"; + else if (config_.bitrate == 500000) bitrate_code = "-s6"; + else if (config_.bitrate == 750000) bitrate_code = "-s7"; + else if (config_.bitrate == 1000000) bitrate_code = "-s8"; + else { + last_error_ = "Unsupported bitrate for slcan script: " + std::to_string(config_.bitrate); + RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + return false; + } + + std::string command = script_path + " " + config_.device_path + " " + config_.interface_name + " " + bitrate_code; - initialized_ = true; - connected_ = true; + RCLCPP_INFO(logger_, "Executing CAN setup script: %s", command.c_str()); + int result = std::system(command.c_str()); - RCLCPP_INFO(logger_, "SLCAN interface setup completed"); - return true; + if (result != 0) { + last_error_ = "CAN setup script '" + script_path + "' failed with exit code " + std::to_string(result) + ". Check script output and permissions."; + RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + return false; + } + RCLCPP_INFO(logger_, "CAN setup script executed successfully."); + + // by now, a new CAN interface should be created based on the name given in the can/config/params.yml + RCLCPP_INFO(logger_, "Proceeding to configure '%s' as a SocketCAN interface.", config_.interface_name.c_str()); + return setupSocketCan(); } bool CanCore::configureInterface() diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index 132216f..01b541f 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -5,7 +5,7 @@ CanNode::CanNode() : Node("can_node"), can_(autonomy::CanCore(this->get_logger() // Load parameters from config/params.yaml this->declare_parameter("can_interface", "can0"); - this->declare_parameter("device_path", "/dev/ttyACM0"); + this->declare_parameter("device_path", "/dev/canable"); this->declare_parameter("bustype", "slcan"); this->declare_parameter("bitrate", 500000); From 92469edfddb7f42bcd1cc1c08b79d4974462255d Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Tue, 27 May 2025 16:48:03 -0400 Subject: [PATCH 14/36] Remove unecessary params --- autonomy/interfacing/can/config/params.yaml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/autonomy/interfacing/can/config/params.yaml b/autonomy/interfacing/can/config/params.yaml index 26d93be..3b1e485 100644 --- a/autonomy/interfacing/can/config/params.yaml +++ b/autonomy/interfacing/can/config/params.yaml @@ -9,15 +9,4 @@ can_node: # Communication Settings publish_rate_hz: 50 # Rate for checking incoming CAN messages - receive_timeout_ms: 100 # Timeout for receiving CAN messages - - # CAN Message IDs (example configuration) - hand_command_id: 0x100 # CAN ID for sending hand commands - hand_status_id: 0x200 # CAN ID for receiving hand status - hand_odometry_id: 0x300 # CAN ID for receiving hand odometry - - # Hand Control Parameters - max_finger_position: 100.0 # Maximum finger position value - max_finger_velocity: 50.0 # Maximum finger velocity value - position_scale_factor: 1.0 # Scaling factor for position commands - velocity_scale_factor: 1.0 # Scaling factor for velocity commands \ No newline at end of file + receive_timeout_ms: 100 # Timeout for receiving CAN messages \ No newline at end of file From 4da06111a15a4e7fa3e817811149839cfc04738c Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Tue, 27 May 2025 17:22:51 -0400 Subject: [PATCH 15/36] Added TODO comments on the empty functions --- autonomy/interfacing/can/src/can_core.cpp | 77 ++++++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp index 74dc275..2d1661a 100644 --- a/autonomy/interfacing/can/src/can_core.cpp +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -60,22 +60,64 @@ bool CanCore::isInitialized() const bool CanCore::sendMessage(const CanMessage& message) { - return 0; + // TODO: Implement CAN message transmission using SocketCAN + // Steps to implement: + // 1. Validate the message using validateMessage() + // 2. Create a struct can_frame and populate it with message data + // 3. Set frame ID (message.id) and handle extended frame flag (CAN_EFF_FLAG) + // 4. Copy message.data to frame.data (up to 8 bytes) + // 5. Use send() or write() system call to transmit via socket_fd_ + // 6. Log the message using logCanMessage(message, true) + // 7. Return true on success, false on failure (and set last_error_) + + RCLCPP_WARN(logger_, "sendMessage not yet implemented"); + return false; } bool CanCore::sendMessage(uint32_t id, const std::vector& data, bool is_extended_id) { - return 0; + // TODO: Convert parameters to CanMessage struct and call the main sendMessage function + // Steps to implement: + // 1. Create a CanMessage struct + // 2. Set message.id = id + // 3. Set message.data = data + // 4. Set message.is_extended_id = is_extended_id + // 5. Set message.is_remote_frame = false (normal data frame) + // 6. Set message.timestamp_us = current time in microseconds + // 7. Call sendMessage(message) and return its result + + RCLCPP_WARN(logger_, "sendMessage(id, data, extended) not yet implemented"); + return false; } bool CanCore::receiveMessage(CanMessage& message) { - return 0; + // TODO: Implement CAN message reception using SocketCAN + // Steps to implement: + // 1. Check if socket_fd_ is valid and interface is connected + // 2. Use recv() or read() with timeout to receive from socket_fd_ + // 3. Parse received struct can_frame into CanMessage + // 4. Extract frame ID and check for extended frame (CAN_EFF_FLAG) + // 5. Copy frame data to message.data vector + // 6. Set message.timestamp_us to current time + // 7. Log the message using logCanMessage(message, false) + // 8. Return true if message received, false if timeout/error + + RCLCPP_WARN(logger_, "receiveMessage not yet implemented"); + return false; } bool CanCore::receiveMessage(uint32_t& id, std::vector& data, bool& is_extended_id) { - return 0; + // TODO: Implement convenience wrapper for receiveMessage + // Steps to implement: + // 1. Create a CanMessage struct + // 2. Call receiveMessage(message) + // 3. If successful, extract id, data, and is_extended_id from message + // 4. Return the result from receiveMessage(message) + + RCLCPP_WARN(logger_, "receiveMessage(id, data, extended) not yet implemented"); + return false; } bool CanCore::setBitrate(uint32_t bitrate) @@ -85,6 +127,11 @@ bool CanCore::setBitrate(uint32_t bitrate) config_.bitrate = bitrate; // TODO: Implement actual bitrate configuration + // Notes: + // - For SLCAN: Bitrate is set during interface creation via slcand -sX parameter + // - For SocketCAN: Bitrate is typically set using 'ip link set canX type can bitrate Y' + // - This function mainly updates the config for future interface reconfigurations + // - Consider implementing runtime bitrate changes for interfaces that support it return true; } @@ -129,9 +176,13 @@ bool CanCore::setupSocketCan() } // TODO: Implement complete SocketCAN setup - // - Configure interface address - // - Bind socket to interface - // - Set socket options + // Steps to implement: + // 1. Configure interface address using struct sockaddr_can + // 2. Use if_nametoindex() to get interface index from config_.interface_name + // 3. Bind socket to the CAN interface using bind() + // 4. Set socket options (timeouts, filters, etc.) using setsockopt() + // 5. Consider setting CAN_RAW_RECV_OWN_MSGS, CAN_RAW_FD_FRAMES if needed + // 6. Test the connection by attempting a simple operation initialized_ = true; connected_ = true; @@ -205,9 +256,15 @@ bool CanCore::configureInterface() RCLCPP_INFO(logger_, "Configuring CAN interface"); // TODO: Implement interface configuration - // - Set filters - // - Configure timeouts - // - Set error handling options + // Steps to implement: + // 1. Set CAN filters using setsockopt() with CAN_RAW_FILTER + // - Define which CAN IDs this node should receive + // - Example: struct can_filter rfilter[1]; rfilter[0].can_id = 0x123; + // 2. Configure receive timeouts using SO_RCVTIMEO + // 3. Set error handling options (CAN_RAW_ERR_FILTER for error frames) + // 4. Configure loopback behavior (CAN_RAW_LOOPBACK, CAN_RAW_RECV_OWN_MSGS) + // 5. For CAN-FD support: set CAN_RAW_FD_FRAMES if needed + // 6. Validate configuration by testing a simple operation return true; } From fcc36c5d16dc091f73b60b7f3ea2de1b6082355a Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Tue, 27 May 2025 19:21:40 -0400 Subject: [PATCH 16/36] Add README.md --- autonomy/interfacing/can/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/autonomy/interfacing/can/README.md b/autonomy/interfacing/can/README.md index 0093967..8d53c18 100644 --- a/autonomy/interfacing/can/README.md +++ b/autonomy/interfacing/can/README.md @@ -13,6 +13,8 @@ can/ │ └── can_node.hpp ├── launch/ │ └── can.launch.py +├── scripts/ +│ └── setup_can.sh ├── src/ │ ├── can_core.cpp │ └── can_node.cpp @@ -69,6 +71,20 @@ This separation provides: #### Configuration - Relevant parameters, environment variables, or dependencies +#### Dependencies +- List of required packages, libraries, or tools + - `rclcpp`: ROS 2 C++ client library + - `can_msgs`: Custom message definitions for CAN frames + - `socketcan`: Linux socket CAN interface + - `ament_cmake`: Build system for ROS 2 packages + +#### Config +- Configuration files, environment variables, or command-line arguments + - `config/params.yaml`: Contains parameters for CAN interface setup, such as: + - `can_interface`: Name of the CAN interface (e.g., `can0`) +m down - `baud_rate`: CAN bus speed (e.g., `500000`) + - `frame_timeout`: Timeout for receiving frames in milliseconds + ### Package Architecture From 3171dcd0031420f5f7d679b98eb7fc424e7b421c Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Tue, 27 May 2025 19:51:34 -0400 Subject: [PATCH 17/36] Add setup script comment --- autonomy/interfacing/can/scripts/setup_can.sh | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/autonomy/interfacing/can/scripts/setup_can.sh b/autonomy/interfacing/can/scripts/setup_can.sh index 64ebe44..6629d12 100755 --- a/autonomy/interfacing/can/scripts/setup_can.sh +++ b/autonomy/interfacing/can/scripts/setup_can.sh @@ -1,11 +1,21 @@ #!/bin/bash +# Purpose: Sets up a CAN interface using slcand (daemon for serial CAN interfaces) +# This script configures a CAN interface by: +# 1. Starting the slcand daemon to create a network interface from a serial device +# 2. Bringing up the CAN interface +# 3. Setting the transmission queue length +# +# The script takes three parameters all found in the autonomy/interfacing/can/config/params.yaml: +# - device_path: Serial device path (e.g., /dev/ttyACM0) +# - interface_name: Name for the CAN interface (e.g., can0) +# - bitrate_code: Bitrate configuration code (e.g., -s6 for 500k, -s8 for 1M) -# Exit immediately if a command exits with a non-zero status. set -e +# These parameters are found in the autonomy/interfacing/can/config/params.yaml DEVICE_PATH="$1" INTERFACE_NAME="$2" -BITRATE_CODE="$3" # e.g., -s6 for 500k, -s8 for 1M +BITRATE_CODE="$3" # e.g., -s6 for 500k, -s8 for 1M if [ -z "$DEVICE_PATH" ] || [ -z "$INTERFACE_NAME" ] || [ -z "$BITRATE_CODE" ]; then echo "Usage: $0 " @@ -28,7 +38,7 @@ fi echo "Bringing up interface: ifconfig $INTERFACE_NAME up" ifconfig "$INTERFACE_NAME" up -echo "Setting txqueuelen: ifconfig $INTERFACE_NAME txqueuelen 1000" +echo "Setting txqueuelen: ifconfig $INTERFACE_NAME txqueuelen 1000" # Transmisson queue length ifconfig "$INTERFACE_NAME" txqueuelen 1000 echo "CAN interface $INTERFACE_NAME setup complete." From f892ae79014d2cbd2c5d69a338b597cfab71863a Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Mon, 2 Jun 2025 14:28:17 -0400 Subject: [PATCH 18/36] Fix README.md Typo --- autonomy/interfacing/can/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autonomy/interfacing/can/README.md b/autonomy/interfacing/can/README.md index 8d53c18..54c4249 100644 --- a/autonomy/interfacing/can/README.md +++ b/autonomy/interfacing/can/README.md @@ -82,7 +82,7 @@ This separation provides: - Configuration files, environment variables, or command-line arguments - `config/params.yaml`: Contains parameters for CAN interface setup, such as: - `can_interface`: Name of the CAN interface (e.g., `can0`) -m down - `baud_rate`: CAN bus speed (e.g., `500000`) + - `baud_rate`: CAN bus speed (e.g., `500000`) - `frame_timeout`: Timeout for receiving frames in milliseconds ### Package Architecture From 4c77a440ebea2623990b2acede94c6c712dc0b30 Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Mon, 2 Jun 2025 14:58:53 -0400 Subject: [PATCH 19/36] Add a test_controller node that can be run in the container to publish some data to a /test_controller topic to send over CAN --- autonomy/interfacing/can/CMakeLists.txt | 20 ++++--- autonomy/interfacing/can/README.md | 32 +++++++++- .../interfacing/can/launch/can_test.launch.py | 55 +++++++++++++++++ .../interfacing/can/test/test_controller.cpp | 59 +++++++++++++++++++ 4 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 autonomy/interfacing/can/launch/can_test.launch.py create mode 100644 autonomy/interfacing/can/test/test_controller.cpp diff --git a/autonomy/interfacing/can/CMakeLists.txt b/autonomy/interfacing/can/CMakeLists.txt index 12cdd39..7ec9e9f 100644 --- a/autonomy/interfacing/can/CMakeLists.txt +++ b/autonomy/interfacing/can/CMakeLists.txt @@ -64,15 +64,17 @@ install(PROGRAMS scripts/setup_can.sh DESTINATION share/${PROJECT_NAME}/scripts) -# Testing -if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - ament_lint_auto_find_test_dependencies() +# Testing and Development +find_package(ament_lint_auto REQUIRED) +ament_lint_auto_find_test_dependencies() - # Add tests - # ament_add_gtest(can_test test/can_test.cpp) - # target_include_directories(can_test PUBLIC include) - # target_link_libraries(can_test) -endif() +# Add test executables (comment out if not needed in production) +# Test controller that publishes to /test_controller topic +add_executable(test_controller_node test/test_controller.cpp) +ament_target_dependencies(test_controller_node rclcpp std_msgs) + +install(TARGETS + test_controller_node + DESTINATION lib/${PROJECT_NAME}) ament_package() diff --git a/autonomy/interfacing/can/README.md b/autonomy/interfacing/can/README.md index 54c4249..98c559a 100644 --- a/autonomy/interfacing/can/README.md +++ b/autonomy/interfacing/can/README.md @@ -66,7 +66,37 @@ This separation provides: - **Maintainability**: CAN protocol changes only affect CanCore, ROS changes only affect CanNode #### Usage -- How to build, run, and test the package + +**Building the Package** +Build the package using the standard ROS 2 build process within the interfacing container. + +**Running the CAN Node** +1. Start the interfacing container +2. Access the container: `sudo ./watod -t interfacing` +3. Source the environment: `source /opt/watonomous/setup.bash` +4. Launch the CAN node: `ros2 launch can can.launch.py` + +**Running the Test Controller Node** +The test controller node publishes test messages to `/test_controller` topic at 1Hz for testing purposes. + +1. In a separate terminal, access the interfacing container: `sudo ./watod -t interfacing` +2. Source the environment: `source /opt/watonomous/setup.bash` +3. Run the test controller using one of these options: + + **Option 1: Run the test controller node directly** + ```bash + ros2 run can test_controller_node + ``` + + **Option 2: Use the test launch file that includes both nodes** + ```bash + ros2 launch can can_test.launch.py + ``` + +**Verifying the Setup** +- Check running nodes: `ros2 node list` +- Check available topics: `ros2 topic list` +- Monitor test messages: `ros2 topic echo /test_controller` #### Configuration - Relevant parameters, environment variables, or dependencies diff --git a/autonomy/interfacing/can/launch/can_test.launch.py b/autonomy/interfacing/can/launch/can_test.launch.py new file mode 100644 index 0000000..9b35144 --- /dev/null +++ b/autonomy/interfacing/can/launch/can_test.launch.py @@ -0,0 +1,55 @@ +from launch import LaunchDescription +from launch_ros.actions import Node +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration +from ament_index_python.packages import get_package_share_directory +import os + +def generate_launch_description(): + # Get the path to the config file + config_file = os.path.join( + get_package_share_directory('can'), + 'config', + 'params.yaml' + ) + + # Declare launch arguments + can_interface_arg = DeclareLaunchArgument( + 'can_interface', + default_value='can0', + description='Name of the CAN interface to use (e.g., can0)' + ) + + publish_rate_arg = DeclareLaunchArgument( + 'publish_rate_hz', + default_value='50', + description='Rate in Hz at which to check for CAN messages' + ) + + # Create the CAN node + can_node = Node( + package='can', + executable='can_node', + name='can_node', + parameters=[config_file, { + 'can_interface': LaunchConfiguration('can_interface'), + 'publish_rate_hz': LaunchConfiguration('publish_rate_hz') + }], + output='screen' + ) + + # Create the test controller node (publishes to /test_controller at 1Hz) + test_controller_node = Node( + package='can', + executable='test_controller_node', + name='test_controller', + output='screen' + ) + + # Return the launch description + return LaunchDescription([ + can_interface_arg, + publish_rate_arg, + can_node, + test_controller_node + ]) diff --git a/autonomy/interfacing/can/test/test_controller.cpp b/autonomy/interfacing/can/test/test_controller.cpp new file mode 100644 index 0000000..355139e --- /dev/null +++ b/autonomy/interfacing/can/test/test_controller.cpp @@ -0,0 +1,59 @@ +#include +#include +#include + +class TestControllerNode : public rclcpp::Node +{ +public: + TestControllerNode() : Node("test_controller") + { + // Create publisher for test controller messages + publisher_ = this->create_publisher("/test_controller", 10); + + // Create timer to publish messages at 1 Hz + timer_ = this->create_wall_timer( + std::chrono::seconds(1), + std::bind(&TestControllerNode::publish_test_data, this) + ); + + RCLCPP_INFO(this->get_logger(), "Test Controller Node started - publishing to /test_controller at 1 Hz"); + } + +private: + void publish_test_data() + { + auto message = std_msgs::msg::String(); + + // Simple test message with timestamp + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + + message.data = "test_data:timestamp:" + std::to_string(time_t) + ";status:active;"; + + // Publish the message + publisher_->publish(message); + + RCLCPP_INFO(this->get_logger(), "Published: %s", message.data.c_str()); + } + + rclcpp::Publisher::SharedPtr publisher_; + rclcpp::TimerBase::SharedPtr timer_; +}; + +int main(int argc, char** argv) +{ + rclcpp::init(argc, argv); + + auto node = std::make_shared(); + + RCLCPP_INFO(node->get_logger(), "Starting Test Controller..."); + + try { + rclcpp::spin(node); + } catch (const std::exception& e) { + RCLCPP_ERROR(node->get_logger(), "Exception in test controller: %s", e.what()); + } + + rclcpp::shutdown(); + return 0; +} \ No newline at end of file From fcdfa2b8f538eb5a3d79d8f1329d689350a10f9c Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Mon, 2 Jun 2025 18:03:37 -0400 Subject: [PATCH 20/36] Create subscriber for any list of topics in params.yaml --- autonomy/interfacing/can/config/params.yaml | 6 +- autonomy/interfacing/can/include/can_core.hpp | 13 +- autonomy/interfacing/can/include/can_node.hpp | 20 +++ autonomy/interfacing/can/launch/can.launch.py | 17 ++- .../interfacing/can/launch/can_test.launch.py | 55 --------- autonomy/interfacing/can/src/can_node.cpp | 114 +++++++++++++++++- 6 files changed, 165 insertions(+), 60 deletions(-) delete mode 100644 autonomy/interfacing/can/launch/can_test.launch.py diff --git a/autonomy/interfacing/can/config/params.yaml b/autonomy/interfacing/can/config/params.yaml index 3b1e485..5bbf96c 100644 --- a/autonomy/interfacing/can/config/params.yaml +++ b/autonomy/interfacing/can/config/params.yaml @@ -9,4 +9,8 @@ can_node: # Communication Settings publish_rate_hz: 50 # Rate for checking incoming CAN messages - receive_timeout_ms: 100 # Timeout for receiving CAN messages \ No newline at end of file + receive_timeout_ms: 100000 # Timeout for receiving CAN messages + + # Topics to subscribe to (message types will be auto-detected) + topics: + - "/test_controller" \ No newline at end of file diff --git a/autonomy/interfacing/can/include/can_core.hpp b/autonomy/interfacing/can/include/can_core.hpp index 52ce8d9..b9554f8 100644 --- a/autonomy/interfacing/can/include/can_core.hpp +++ b/autonomy/interfacing/can/include/can_core.hpp @@ -11,9 +11,12 @@ namespace autonomy struct CanMessage { uint32_t id; // CAN message ID - std::vector data; // Message data (up to 8 bytes for standard CAN) + std::vector data; // Message data (up to 8 bytes for classic CAN, up to 64 bytes for CAN-FD) bool is_extended_id; // Extended frame format flag bool is_remote_frame; // Remote transmission request flag + bool is_fd_frame; // CAN-FD frame flag + bool is_brs; // Bit Rate Switch (for CAN-FD) + bool is_esi; // Error State Indicator (for CAN-FD) uint64_t timestamp_us; // Timestamp in microseconds }; @@ -21,8 +24,10 @@ struct CanConfig { std::string interface_name; // CAN interface name (e.g., "can0") std::string device_path; // Device path for SLCAN (e.g., "/dev/ttyACM0") std::string bustype; // Bus type: "socketcan" or "slcan" - uint32_t bitrate; // Bitrate in bps + uint32_t bitrate; // Bitrate in bps for arbitration phase + uint32_t data_bitrate; // Data bitrate in bps for CAN-FD data phase uint32_t receive_timeout_ms; // Receive timeout in milliseconds + bool enable_canfd; // Enable CAN-FD support }; class CanCore { @@ -37,6 +42,7 @@ class CanCore { // Message Transmission bool sendMessage(const CanMessage& message); bool sendMessage(uint32_t id, const std::vector& data, bool is_extended_id = false); + bool sendCanFdMessage(uint32_t id, const std::vector& data, bool is_extended_id = false, bool is_brs = true); // Message Reception bool receiveMessage(CanMessage& message); @@ -44,7 +50,10 @@ class CanCore { // Configuration and Status bool setBitrate(uint32_t bitrate); + bool setDataBitrate(uint32_t data_bitrate); uint32_t getBitrate() const; + uint32_t getDataBitrate() const; + bool isCanFdEnabled() const; std::string getInterfaceInfo() const; bool isConnected() const; diff --git a/autonomy/interfacing/can/include/can_node.hpp b/autonomy/interfacing/can/include/can_node.hpp index 33ffab5..c7d3775 100644 --- a/autonomy/interfacing/can/include/can_node.hpp +++ b/autonomy/interfacing/can/include/can_node.hpp @@ -2,7 +2,18 @@ #define CAN_NODE_HPP #include "rclcpp/rclcpp.hpp" +#include "rclcpp/generic_subscription.hpp" +#include "std_msgs/msg/string.hpp" #include "can_core.hpp" +#include +#include +#include + +struct TopicConfig { + // name of the topic, and the message type + std::string name; + std::string type; +}; class CanNode : public rclcpp::Node { public: @@ -10,6 +21,15 @@ class CanNode : public rclcpp::Node { private: autonomy::CanCore can_; + std::vector topic_configs_; + std::vector> subscribers_; + + // Methods + void loadTopicConfigurations(); + void createSubscribers(); + std::string discoverTopicType(const std::string& topic_name); + void genericTopicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type); + void processMessageForCan(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type); }; #endif // CAN_NODE_HPP diff --git a/autonomy/interfacing/can/launch/can.launch.py b/autonomy/interfacing/can/launch/can.launch.py index c8a630b..ed775e1 100644 --- a/autonomy/interfacing/can/launch/can.launch.py +++ b/autonomy/interfacing/can/launch/can.launch.py @@ -38,9 +38,24 @@ def generate_launch_description(): output='screen' ) + # ============================================================================ + # TEST CONTROLLER NODE (for development/testing only) + # Comment out the following section if you want to disable the test controller + # ============================================================================ + test_controller_node = Node( + package='can', + executable='test_controller_node', + name='test_controller', + output='screen' + ) + # ============================================================================ + # End of test controller section + # ============================================================================ + # Return the launch description return LaunchDescription([ can_interface_arg, publish_rate_arg, - can_node + can_node, + test_controller_node # Comment out this line to disable test controller ]) diff --git a/autonomy/interfacing/can/launch/can_test.launch.py b/autonomy/interfacing/can/launch/can_test.launch.py deleted file mode 100644 index 9b35144..0000000 --- a/autonomy/interfacing/can/launch/can_test.launch.py +++ /dev/null @@ -1,55 +0,0 @@ -from launch import LaunchDescription -from launch_ros.actions import Node -from launch.actions import DeclareLaunchArgument -from launch.substitutions import LaunchConfiguration -from ament_index_python.packages import get_package_share_directory -import os - -def generate_launch_description(): - # Get the path to the config file - config_file = os.path.join( - get_package_share_directory('can'), - 'config', - 'params.yaml' - ) - - # Declare launch arguments - can_interface_arg = DeclareLaunchArgument( - 'can_interface', - default_value='can0', - description='Name of the CAN interface to use (e.g., can0)' - ) - - publish_rate_arg = DeclareLaunchArgument( - 'publish_rate_hz', - default_value='50', - description='Rate in Hz at which to check for CAN messages' - ) - - # Create the CAN node - can_node = Node( - package='can', - executable='can_node', - name='can_node', - parameters=[config_file, { - 'can_interface': LaunchConfiguration('can_interface'), - 'publish_rate_hz': LaunchConfiguration('publish_rate_hz') - }], - output='screen' - ) - - # Create the test controller node (publishes to /test_controller at 1Hz) - test_controller_node = Node( - package='can', - executable='test_controller_node', - name='test_controller', - output='screen' - ) - - # Return the launch description - return LaunchDescription([ - can_interface_arg, - publish_rate_arg, - can_node, - test_controller_node - ]) diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index 01b541f..b025618 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -1,4 +1,8 @@ #include "can_node.hpp" +#include +#include +#include +#include CanNode::CanNode() : Node("can_node"), can_(autonomy::CanCore(this->get_logger())) { RCLCPP_INFO(this->get_logger(), "CAN Node has been initialized"); @@ -24,7 +28,7 @@ CanNode::CanNode() : Node("can_node"), can_(autonomy::CanCore(this->get_logger() config.device_path = device_path; config.bustype = bustype; config.bitrate = bitrate; - config.receive_timeout_ms = 100; + config.receive_timeout_ms = 10000; // Initialize the CAN interface if (can_.initialize(config)) { @@ -32,6 +36,114 @@ CanNode::CanNode() : Node("can_node"), can_(autonomy::CanCore(this->get_logger() } else { RCLCPP_ERROR(this->get_logger(), "Failed to initialize CAN Core: %s", can_.getLastError().c_str()); } + + // Load topic configurations and create subscribers + loadTopicConfigurations(); + createSubscribers(); +} + +void CanNode::loadTopicConfigurations() { + try { + // Declare and get the topics parameter as a string array + this->declare_parameter("topics", std::vector{"/test_controller"}); + auto topic_names = this->get_parameter("topics").as_string_array(); + + for (const auto& topic_name : topic_names) { + // Discover the message type for this topic + std::string topic_type = discoverTopicType(topic_name); + + if (!topic_type.empty()) { + TopicConfig config; + config.name = topic_name; + config.type = topic_type; + topic_configs_.push_back(config); + + RCLCPP_INFO(this->get_logger(), "Loaded topic config: %s (%s)", + topic_name.c_str(), topic_type.c_str()); + } else { + RCLCPP_WARN(this->get_logger(), "Could not discover message type for topic: %s", + topic_name.c_str()); + } + } + + if (topic_configs_.empty()) { + RCLCPP_WARN(this->get_logger(), "No valid topics found in configuration - CAN node will not subscribe to any topics"); + } + + } catch (const std::exception& e) { + RCLCPP_ERROR(this->get_logger(), "Error loading topic configurations: %s", e.what()); + } +} + +std::string CanNode::discoverTopicType(const std::string& topic_name) { + // Get topic information from ROS graph + auto topic_names_and_types = this->get_topic_names_and_types(); + + for (const auto& topic_info : topic_names_and_types) { + if (topic_info.first == topic_name) { + if (!topic_info.second.empty()) { + // Return the first message type (there's usually only one) + return topic_info.second[0]; + } + } + } + + // If topic is not found, wait a bit and try again (topic might not be published yet) + RCLCPP_INFO(this->get_logger(), "Topic '%s' not found, waiting for it to become available...", topic_name.c_str()); + + // Wait up to 10 seconds for the topic to appear (increased from 5) + for (int i = 0; i < 100; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + topic_names_and_types = this->get_topic_names_and_types(); + + for (const auto& topic_info : topic_names_and_types) { + if (topic_info.first == topic_name) { + if (!topic_info.second.empty()) { + RCLCPP_INFO(this->get_logger(), "Found topic '%s' with type '%s'", + topic_name.c_str(), topic_info.second[0].c_str()); + return topic_info.second[0]; + } + } + } + } + + RCLCPP_ERROR(this->get_logger(), "Timeout waiting for topic '%s' to become available", topic_name.c_str()); + return ""; +} + +void CanNode::createSubscribers() { + for (const auto& topic_config : topic_configs_) { + // Create generic subscriber that can handle any message type + auto subscriber = this->create_generic_subscription( + topic_config.name, + topic_config.type, + 10, + [this, topic_name = topic_config.name, topic_type = topic_config.type] + (std::shared_ptr msg) { + this->genericTopicCallback(msg, topic_name, topic_type); + } + ); + + subscribers_.push_back(subscriber); + RCLCPP_INFO(this->get_logger(), "Created generic subscriber for topic: %s (type: %s)", + topic_config.name.c_str(), topic_config.type.c_str()); + } +} + +void CanNode::genericTopicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type) { + RCLCPP_INFO(this->get_logger(), "Received message on topic '%s' (type: %s), serialized size: %zu bytes", + topic_name.c_str(), topic_type.c_str(), msg->size()); + + // Just log the received message for now + // The actual CAN message processing will be handled by can_core when needed +} + +void CanNode::processMessageForCan(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type) { + // This method is currently not used but kept for future CAN integration + // When needed, this will interface with can_core to send CAN-FD frames + (void)msg; // Suppress unused parameter warning + (void)topic_name; + (void)topic_type; } int main(int argc, char **argv) { From 7e735db51fccc0af29c6a1bfe38864e41356e392 Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Mon, 2 Jun 2025 18:08:58 -0400 Subject: [PATCH 21/36] Updated the README.md for how to enable/disable test publisher nodes --- autonomy/interfacing/can/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autonomy/interfacing/can/README.md b/autonomy/interfacing/can/README.md index 98c559a..673cd8e 100644 --- a/autonomy/interfacing/can/README.md +++ b/autonomy/interfacing/can/README.md @@ -80,7 +80,7 @@ Build the package using the standard ROS 2 build process within the interfacing The test controller node publishes test messages to `/test_controller` topic at 1Hz for testing purposes. 1. In a separate terminal, access the interfacing container: `sudo ./watod -t interfacing` -2. Source the environment: `source /opt/watonomous/setup.bash` +2. Source the environment: `source ./wato_ros_entrypoint.sh` 3. Run the test controller using one of these options: **Option 1: Run the test controller node directly** From a686316c6a3d10653361399d5a3f593b085db7aa Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Mon, 2 Jun 2025 18:09:56 -0400 Subject: [PATCH 22/36] removing debug statements from the test_controller --- autonomy/interfacing/can/README.md | 24 ++----------------- .../interfacing/can/test/test_controller.cpp | 2 +- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/autonomy/interfacing/can/README.md b/autonomy/interfacing/can/README.md index 673cd8e..0a739fc 100644 --- a/autonomy/interfacing/can/README.md +++ b/autonomy/interfacing/can/README.md @@ -73,30 +73,10 @@ Build the package using the standard ROS 2 build process within the interfacing **Running the CAN Node** 1. Start the interfacing container 2. Access the container: `sudo ./watod -t interfacing` -3. Source the environment: `source /opt/watonomous/setup.bash` +3. Source the environment: `source ./watod_ros_entrypoint.sh` 4. Launch the CAN node: `ros2 launch can can.launch.py` -**Running the Test Controller Node** -The test controller node publishes test messages to `/test_controller` topic at 1Hz for testing purposes. - -1. In a separate terminal, access the interfacing container: `sudo ./watod -t interfacing` -2. Source the environment: `source ./wato_ros_entrypoint.sh` -3. Run the test controller using one of these options: - - **Option 1: Run the test controller node directly** - ```bash - ros2 run can test_controller_node - ``` - - **Option 2: Use the test launch file that includes both nodes** - ```bash - ros2 launch can can_test.launch.py - ``` - -**Verifying the Setup** -- Check running nodes: `ros2 node list` -- Check available topics: `ros2 topic list` -- Monitor test messages: `ros2 topic echo /test_controller` +**Note**: The launch file includes a test controller node for development/testing. To disable it for production use, comment out the test controller section in `launch/can.launch.py` (lines marked with "TEST CONTROLLER NODE" comments). #### Configuration - Relevant parameters, environment variables, or dependencies diff --git a/autonomy/interfacing/can/test/test_controller.cpp b/autonomy/interfacing/can/test/test_controller.cpp index 355139e..a9a06bb 100644 --- a/autonomy/interfacing/can/test/test_controller.cpp +++ b/autonomy/interfacing/can/test/test_controller.cpp @@ -33,7 +33,7 @@ class TestControllerNode : public rclcpp::Node // Publish the message publisher_->publish(message); - RCLCPP_INFO(this->get_logger(), "Published: %s", message.data.c_str()); + // RCLCPP_INFO(this->get_logger(), "Published: %s", message.data.c_str()); } rclcpp::Publisher::SharedPtr publisher_; From 5aac94c3b9f8b76064ad022f4fe8c9fcd753873d Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Mon, 2 Jun 2025 18:16:29 -0400 Subject: [PATCH 23/36] Remove CAN bus publishing from Can Node --- autonomy/interfacing/can/include/can_node.hpp | 1 - autonomy/interfacing/can/src/can_node.cpp | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/autonomy/interfacing/can/include/can_node.hpp b/autonomy/interfacing/can/include/can_node.hpp index c7d3775..60f8093 100644 --- a/autonomy/interfacing/can/include/can_node.hpp +++ b/autonomy/interfacing/can/include/can_node.hpp @@ -29,7 +29,6 @@ class CanNode : public rclcpp::Node { void createSubscribers(); std::string discoverTopicType(const std::string& topic_name); void genericTopicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type); - void processMessageForCan(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type); }; #endif // CAN_NODE_HPP diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index b025618..d267a0d 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -133,17 +133,6 @@ void CanNode::createSubscribers() { void CanNode::genericTopicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type) { RCLCPP_INFO(this->get_logger(), "Received message on topic '%s' (type: %s), serialized size: %zu bytes", topic_name.c_str(), topic_type.c_str(), msg->size()); - - // Just log the received message for now - // The actual CAN message processing will be handled by can_core when needed -} - -void CanNode::processMessageForCan(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type) { - // This method is currently not used but kept for future CAN integration - // When needed, this will interface with can_core to send CAN-FD frames - (void)msg; // Suppress unused parameter warning - (void)topic_name; - (void)topic_type; } int main(int argc, char **argv) { From 8cf420791ced63da6b35a512bce73ae133bb8faf Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Tue, 3 Jun 2025 17:37:12 -0400 Subject: [PATCH 24/36] simplified CAN core and node --- autonomy/interfacing/can/include/can_core.hpp | 21 --- autonomy/interfacing/can/include/can_node.hpp | 2 +- autonomy/interfacing/can/src/can_core.cpp | 161 +----------------- autonomy/interfacing/can/src/can_node.cpp | 4 +- 4 files changed, 7 insertions(+), 181 deletions(-) diff --git a/autonomy/interfacing/can/include/can_core.hpp b/autonomy/interfacing/can/include/can_core.hpp index b9554f8..0fa772f 100644 --- a/autonomy/interfacing/can/include/can_core.hpp +++ b/autonomy/interfacing/can/include/can_core.hpp @@ -27,7 +27,6 @@ struct CanConfig { uint32_t bitrate; // Bitrate in bps for arbitration phase uint32_t data_bitrate; // Data bitrate in bps for CAN-FD data phase uint32_t receive_timeout_ms; // Receive timeout in milliseconds - bool enable_canfd; // Enable CAN-FD support }; class CanCore { @@ -41,25 +40,9 @@ class CanCore { // Message Transmission bool sendMessage(const CanMessage& message); - bool sendMessage(uint32_t id, const std::vector& data, bool is_extended_id = false); - bool sendCanFdMessage(uint32_t id, const std::vector& data, bool is_extended_id = false, bool is_brs = true); // Message Reception bool receiveMessage(CanMessage& message); - bool receiveMessage(uint32_t& id, std::vector& data, bool& is_extended_id); - - // Configuration and Status - bool setBitrate(uint32_t bitrate); - bool setDataBitrate(uint32_t data_bitrate); - uint32_t getBitrate() const; - uint32_t getDataBitrate() const; - bool isCanFdEnabled() const; - std::string getInterfaceInfo() const; - bool isConnected() const; - - // Error Handling - std::string getLastError() const; - void clearErrors(); private: rclcpp::Logger logger_; @@ -69,14 +52,10 @@ class CanCore { bool initialized_; bool connected_; CanConfig config_; - std::string last_error_; // Internal helper methods bool setupSocketCan(); bool setupSlcan(); - bool configureInterface(); - bool validateMessage(const CanMessage& message); - void logCanMessage(const CanMessage& message, bool is_tx); }; } diff --git a/autonomy/interfacing/can/include/can_node.hpp b/autonomy/interfacing/can/include/can_node.hpp index 60f8093..0d956ed 100644 --- a/autonomy/interfacing/can/include/can_node.hpp +++ b/autonomy/interfacing/can/include/can_node.hpp @@ -28,7 +28,7 @@ class CanNode : public rclcpp::Node { void loadTopicConfigurations(); void createSubscribers(); std::string discoverTopicType(const std::string& topic_name); - void genericTopicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type); + void topicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type); }; #endif // CAN_NODE_HPP diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp index 2d1661a..d3d5465 100644 --- a/autonomy/interfacing/can/src/can_core.cpp +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -32,8 +32,7 @@ bool CanCore::initialize(const CanConfig& config) } else if (config.bustype == "slcan") { return setupSlcan(); } else { - last_error_ = "Unsupported bus type: " + config.bustype; - RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + RCLCPP_ERROR(logger_, "Unsupported bus type: %s", config.bustype.c_str()); return false; } } @@ -61,108 +60,19 @@ bool CanCore::isInitialized() const bool CanCore::sendMessage(const CanMessage& message) { // TODO: Implement CAN message transmission using SocketCAN - // Steps to implement: - // 1. Validate the message using validateMessage() - // 2. Create a struct can_frame and populate it with message data - // 3. Set frame ID (message.id) and handle extended frame flag (CAN_EFF_FLAG) - // 4. Copy message.data to frame.data (up to 8 bytes) - // 5. Use send() or write() system call to transmit via socket_fd_ - // 6. Log the message using logCanMessage(message, true) - // 7. Return true on success, false on failure (and set last_error_) RCLCPP_WARN(logger_, "sendMessage not yet implemented"); return false; } -bool CanCore::sendMessage(uint32_t id, const std::vector& data, bool is_extended_id) -{ - // TODO: Convert parameters to CanMessage struct and call the main sendMessage function - // Steps to implement: - // 1. Create a CanMessage struct - // 2. Set message.id = id - // 3. Set message.data = data - // 4. Set message.is_extended_id = is_extended_id - // 5. Set message.is_remote_frame = false (normal data frame) - // 6. Set message.timestamp_us = current time in microseconds - // 7. Call sendMessage(message) and return its result - - RCLCPP_WARN(logger_, "sendMessage(id, data, extended) not yet implemented"); - return false; -} - bool CanCore::receiveMessage(CanMessage& message) { // TODO: Implement CAN message reception using SocketCAN - // Steps to implement: - // 1. Check if socket_fd_ is valid and interface is connected - // 2. Use recv() or read() with timeout to receive from socket_fd_ - // 3. Parse received struct can_frame into CanMessage - // 4. Extract frame ID and check for extended frame (CAN_EFF_FLAG) - // 5. Copy frame data to message.data vector - // 6. Set message.timestamp_us to current time - // 7. Log the message using logCanMessage(message, false) - // 8. Return true if message received, false if timeout/error RCLCPP_WARN(logger_, "receiveMessage not yet implemented"); return false; } -bool CanCore::receiveMessage(uint32_t& id, std::vector& data, bool& is_extended_id) -{ - // TODO: Implement convenience wrapper for receiveMessage - // Steps to implement: - // 1. Create a CanMessage struct - // 2. Call receiveMessage(message) - // 3. If successful, extract id, data, and is_extended_id from message - // 4. Return the result from receiveMessage(message) - - RCLCPP_WARN(logger_, "receiveMessage(id, data, extended) not yet implemented"); - return false; -} - -bool CanCore::setBitrate(uint32_t bitrate) -{ - RCLCPP_INFO(logger_, "Setting bitrate to: %u", bitrate); - - config_.bitrate = bitrate; - - // TODO: Implement actual bitrate configuration - // Notes: - // - For SLCAN: Bitrate is set during interface creation via slcand -sX parameter - // - For SocketCAN: Bitrate is typically set using 'ip link set canX type can bitrate Y' - // - This function mainly updates the config for future interface reconfigurations - // - Consider implementing runtime bitrate changes for interfaces that support it - - return true; -} - -uint32_t CanCore::getBitrate() const -{ - return config_.bitrate; -} - -std::string CanCore::getInterfaceInfo() const -{ - return "Interface: " + config_.interface_name + - ", Type: " + config_.bustype + - ", Bitrate: " + std::to_string(config_.bitrate); -} - -bool CanCore::isConnected() const -{ - return connected_; -} - -std::string CanCore::getLastError() const -{ - return last_error_; -} - -void CanCore::clearErrors() -{ - last_error_.clear(); -} - bool CanCore::setupSocketCan() { RCLCPP_INFO(logger_, "Setting up SocketCAN interface"); @@ -170,8 +80,7 @@ bool CanCore::setupSocketCan() // Create socket socket_fd_ = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (socket_fd_ < 0) { - last_error_ = "Failed to create SocketCAN socket"; - RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + RCLCPP_ERROR(logger_, "Failed to create SocketCAN socket"); return false; } @@ -203,7 +112,6 @@ bool CanCore::setupSlcan() package_share_directory = ament_index_cpp::get_package_share_directory("can"); // the package we're in is can } catch (const ament_index_cpp::PackageNotFoundError& e) { RCLCPP_ERROR(logger_, "Package not found for script path: %s", e.what()); - last_error_ = "Could not find package 'can' to locate setup script."; return false; } std::string script_path = package_share_directory + "/scripts/setup_can.sh"; @@ -212,7 +120,6 @@ bool CanCore::setupSlcan() struct stat buffer; if (stat(script_path.c_str(), &buffer) != 0) { RCLCPP_ERROR(logger_, "Setup script not found"); - last_error_ = "Setup script not found"; return false; } @@ -229,8 +136,7 @@ bool CanCore::setupSlcan() else if (config_.bitrate == 750000) bitrate_code = "-s7"; else if (config_.bitrate == 1000000) bitrate_code = "-s8"; else { - last_error_ = "Unsupported bitrate for slcan script: " + std::to_string(config_.bitrate); - RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + RCLCPP_ERROR(logger_, "Unsupported bitrate for slcan script: %u", config_.bitrate); return false; } @@ -240,8 +146,7 @@ bool CanCore::setupSlcan() int result = std::system(command.c_str()); if (result != 0) { - last_error_ = "CAN setup script '" + script_path + "' failed with exit code " + std::to_string(result) + ". Check script output and permissions."; - RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); + RCLCPP_ERROR(logger_, "CAN setup script failed with exit code %d. Check script output and permissions.", result); return false; } RCLCPP_INFO(logger_, "CAN setup script executed successfully."); @@ -251,62 +156,4 @@ bool CanCore::setupSlcan() return setupSocketCan(); } -bool CanCore::configureInterface() -{ - RCLCPP_INFO(logger_, "Configuring CAN interface"); - - // TODO: Implement interface configuration - // Steps to implement: - // 1. Set CAN filters using setsockopt() with CAN_RAW_FILTER - // - Define which CAN IDs this node should receive - // - Example: struct can_filter rfilter[1]; rfilter[0].can_id = 0x123; - // 2. Configure receive timeouts using SO_RCVTIMEO - // 3. Set error handling options (CAN_RAW_ERR_FILTER for error frames) - // 4. Configure loopback behavior (CAN_RAW_LOOPBACK, CAN_RAW_RECV_OWN_MSGS) - // 5. For CAN-FD support: set CAN_RAW_FD_FRAMES if needed - // 6. Validate configuration by testing a simple operation - - return true; -} - -bool CanCore::validateMessage(const CanMessage& message) -{ - // Check message ID range - if (message.is_extended_id && message.id > 0x1FFFFFFF) { - last_error_ = "Extended CAN ID out of range"; - RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); - return false; - } - - if (!message.is_extended_id && message.id > 0x7FF) { - last_error_ = "Standard CAN ID out of range"; - RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); - return false; - } - - // Check data length - if (message.data.size() > 8) { - last_error_ = "CAN data length exceeds 8 bytes"; - RCLCPP_ERROR(logger_, "%s", last_error_.c_str()); - return false; - } - - return true; -} - -void CanCore::logCanMessage(const CanMessage& message, bool is_tx) -{ - std::string direction = is_tx ? "TX" : "RX"; - std::string data_str; - - for (const auto& byte : message.data) { - if (!data_str.empty()) data_str += " "; - data_str += std::to_string(byte); - } - - RCLCPP_DEBUG(logger_, "%s CAN: ID=0x%X, Data=[%s], Ext=%s", - direction.c_str(), message.id, data_str.c_str(), - message.is_extended_id ? "true" : "false"); -} - } diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index d267a0d..56310fc 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -120,7 +120,7 @@ void CanNode::createSubscribers() { 10, [this, topic_name = topic_config.name, topic_type = topic_config.type] (std::shared_ptr msg) { - this->genericTopicCallback(msg, topic_name, topic_type); + this->topicCallback(msg, topic_name, topic_type); } ); @@ -130,7 +130,7 @@ void CanNode::createSubscribers() { } } -void CanNode::genericTopicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type) { +void CanNode::topicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type) { RCLCPP_INFO(this->get_logger(), "Received message on topic '%s' (type: %s), serialized size: %zu bytes", topic_name.c_str(), topic_type.c_str(), msg->size()); } From db538a20cf89f1bfd0892a8942b1ed30ac9b072c Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Tue, 3 Jun 2025 20:08:14 -0400 Subject: [PATCH 25/36] Create High Level CAN Message struct --- autonomy/interfacing/can/include/can_node.hpp | 4 + autonomy/interfacing/can/src/can_core.cpp | 82 +++++++++++++++---- autonomy/interfacing/can/src/can_node.cpp | 71 ++++++++++++++-- 3 files changed, 134 insertions(+), 23 deletions(-) diff --git a/autonomy/interfacing/can/include/can_node.hpp b/autonomy/interfacing/can/include/can_node.hpp index 0d956ed..4ff0d96 100644 --- a/autonomy/interfacing/can/include/can_node.hpp +++ b/autonomy/interfacing/can/include/can_node.hpp @@ -29,6 +29,10 @@ class CanNode : public rclcpp::Node { void createSubscribers(); std::string discoverTopicType(const std::string& topic_name); void topicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type); + + // Helper methods + uint32_t generateCanId(const std::string& topic_name); + autonomy::CanMessage createCanMessage(const std::string& topic_name, std::shared_ptr ros_msg); }; #endif // CAN_NODE_HPP diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp index d3d5465..e5d4eca 100644 --- a/autonomy/interfacing/can/src/can_core.cpp +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -8,6 +8,7 @@ #include #include // For std::system to call the external script for slcand #include // checking if script files exist +#include // For errno #include "ament_index_cpp/get_package_share_directory.hpp" // to load the bash scripts #include "ament_index_cpp/get_package_prefix.hpp" // defines PackageNotFoundError @@ -59,13 +60,30 @@ bool CanCore::isInitialized() const bool CanCore::sendMessage(const CanMessage& message) { - // TODO: Implement CAN message transmission using SocketCAN - - RCLCPP_WARN(logger_, "sendMessage not yet implemented"); - return false; + // Log the details of the received CAN message + RCLCPP_INFO(logger_, "sendMessage called with CanMessage:"); + RCLCPP_INFO(logger_, " ID: 0x%X (%s)", message.id, message.is_extended_id ? "Extended" : "Standard"); + RCLCPP_INFO(logger_, " Timestamp (us): %lu", message.timestamp_us); + RCLCPP_INFO(logger_, " Flags: FD=%d, BRS=%d, ESI=%d, RTR=%d", + message.is_fd_frame, message.is_brs, message.is_esi, message.is_remote_frame); + RCLCPP_INFO(logger_, " Data Length: %zu bytes", message.data.size()); + + if (!message.data.empty()) { + std::stringstream ss; + ss << " Data: "; + for (size_t i = 0; i < message.data.size(); ++i) { + ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(message.data[i]) << " "; + } + RCLCPP_INFO(logger_, "%s", ss.str().c_str()); + } else { + RCLCPP_INFO(logger_, " Data: (empty)"); + } + + // For now, we are just logging and not actually sending. + return true; } -bool CanCore::receiveMessage(CanMessage& message) +bool CanCore::receiveMessage([[maybe_unused]] CanMessage& message) { // TODO: Implement CAN message reception using SocketCAN @@ -75,28 +93,56 @@ bool CanCore::receiveMessage(CanMessage& message) bool CanCore::setupSocketCan() { - RCLCPP_INFO(logger_, "Setting up SocketCAN interface"); + RCLCPP_INFO(logger_, "Setting up SocketCAN interface: %s", config_.interface_name.c_str()); // Create socket socket_fd_ = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (socket_fd_ < 0) { - RCLCPP_ERROR(logger_, "Failed to create SocketCAN socket"); + RCLCPP_ERROR(logger_, "Failed to create SocketCAN socket: %s", strerror(errno)); + return false; + } + + // Enable CAN FD frames + int enable_canfd = 1; + if (setsockopt(socket_fd_, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &enable_canfd, sizeof(enable_canfd)) < 0) { + RCLCPP_ERROR(logger_, "Failed to enable CAN FD frames on socket: %s", strerror(errno)); + close(socket_fd_); + socket_fd_ = -1; + return false; + } + RCLCPP_INFO(logger_, "CAN FD frames enabled on socket."); + + // Get interface index + struct ifreq ifr; + std::strncpy(ifr.ifr_name, config_.interface_name.c_str(), IFNAMSIZ - 1); + ifr.ifr_name[IFNAMSIZ - 1] = '\\0'; // Ensure null termination + + if (ioctl(socket_fd_, SIOCGIFINDEX, &ifr) < 0) { + RCLCPP_ERROR(logger_, "Failed to get interface index for '%s': %s", config_.interface_name.c_str(), strerror(errno)); + close(socket_fd_); + socket_fd_ = -1; + return false; + } + RCLCPP_INFO(logger_, "Interface index for '%s' is %d.", config_.interface_name.c_str(), ifr.ifr_ifindex); + + // Configure socket address + struct sockaddr_can addr; + addr.can_family = AF_CAN; + addr.can_ifindex = ifr.ifr_ifindex; + + // Bind socket + if (bind(socket_fd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + RCLCPP_ERROR(logger_, "Failed to bind SocketCAN socket to interface '%s' (index %d): %s", + config_.interface_name.c_str(), addr.can_ifindex, strerror(errno)); + close(socket_fd_); + socket_fd_ = -1; return false; } - - // TODO: Implement complete SocketCAN setup - // Steps to implement: - // 1. Configure interface address using struct sockaddr_can - // 2. Use if_nametoindex() to get interface index from config_.interface_name - // 3. Bind socket to the CAN interface using bind() - // 4. Set socket options (timeouts, filters, etc.) using setsockopt() - // 5. Consider setting CAN_RAW_RECV_OWN_MSGS, CAN_RAW_FD_FRAMES if needed - // 6. Test the connection by attempting a simple operation initialized_ = true; - connected_ = true; + connected_ = true; // Assuming bind success means connected for SocketCAN raw - RCLCPP_INFO(logger_, "SocketCAN interface setup completed"); + RCLCPP_INFO(logger_, "SocketCAN interface '%s' setup and bound successfully.", config_.interface_name.c_str()); return true; } diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index 56310fc..c7bf931 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -3,8 +3,9 @@ #include #include #include +#include // Required for std::memcpy -CanNode::CanNode() : Node("can_node"), can_(autonomy::CanCore(this->get_logger())) { +CanNode::CanNode() : Node("can_node"), can_(this->get_logger()) { RCLCPP_INFO(this->get_logger(), "CAN Node has been initialized"); // Load parameters from config/params.yaml @@ -34,7 +35,7 @@ CanNode::CanNode() : Node("can_node"), can_(autonomy::CanCore(this->get_logger() if (can_.initialize(config)) { RCLCPP_INFO(this->get_logger(), "CAN Core interface initialized successfully"); } else { - RCLCPP_ERROR(this->get_logger(), "Failed to initialize CAN Core: %s", can_.getLastError().c_str()); + RCLCPP_ERROR(this->get_logger(), "Failed to initialize CAN Core"); } // Load topic configurations and create subscribers @@ -130,9 +131,69 @@ void CanNode::createSubscribers() { } } -void CanNode::topicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type) { - RCLCPP_INFO(this->get_logger(), "Received message on topic '%s' (type: %s), serialized size: %zu bytes", - topic_name.c_str(), topic_type.c_str(), msg->size()); +void CanNode::topicCallback(std::shared_ptr msg, const std::string& topic_name, [[maybe_unused]] const std::string& topic_type) { + // Create CAN message from ROS message + autonomy::CanMessage can_message = createCanMessage(topic_name, msg); + + // Send the CAN message + if (can_.sendMessage(can_message)) { + return; + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to send CAN message for topic '%s'", topic_name.c_str()); + } +} + +uint32_t CanNode::generateCanId(const std::string& topic_name) { + // Generate a CAN ID from topic name using hash + // Use the first 29 bits for extended CAN ID (CAN 2.0B) + std::hash hasher; + uint32_t hash = static_cast(hasher(topic_name)); + + // Mask to 29 bits for extended CAN ID (0x1FFFFFFF) + // For CAN-FD, we can use the full extended ID range + return hash & 0x1FFFFFFF; +} + +autonomy::CanMessage CanNode::createCanMessage(const std::string& topic_name, std::shared_ptr ros_msg) { + autonomy::CanMessage can_msg; + + // Generate CAN ID from topic name + can_msg.id = generateCanId(topic_name); + + can_msg.is_fd_frame = true; + can_msg.is_extended_id = true; + can_msg.is_remote_frame = false; + can_msg.is_brs = true; // Enable bit rate switching for faster data transmission + can_msg.is_esi = false; // Error state indicator (false = active error state) + + // Get current timestamp + auto now = std::chrono::high_resolution_clock::now(); + can_msg.timestamp_us = std::chrono::duration_cast( + now.time_since_epoch()).count(); + + // Package the ROS message data into CAN frame. CAN-FD supports up to 64 bytes of data + const size_t max_can_fd_data_size = 64; + size_t ros_msg_size = ros_msg->size(); + + // Data Packaging + if (ros_msg_size <= max_can_fd_data_size) { // if message fits in single CAN-FD frame + can_msg.data.resize(ros_msg_size); + std::memcpy(can_msg.data.data(), ros_msg->get_rcl_serialized_message().buffer, ros_msg_size); + + RCLCPP_DEBUG(this->get_logger(), "Packaged %zu bytes from topic '%s' into single CAN-FD frame with ID 0x%X", + ros_msg_size, topic_name.c_str(), can_msg.id); + } else { + // Message is too large for single CAN-FD frame + // For now, we'll truncate to fit in one frame and log a warning + // TODO: Implement multi-frame transmission protocol + RCLCPP_WARN(this->get_logger(), "Message from topic '%s' (%zu bytes) exceeds CAN-FD frame limit (%zu bytes). Truncating.", + topic_name.c_str(), ros_msg_size, max_can_fd_data_size); + + can_msg.data.resize(max_can_fd_data_size); + std::memcpy(can_msg.data.data(), ros_msg->get_rcl_serialized_message().buffer, max_can_fd_data_size); + } + + return can_msg; } int main(int argc, char **argv) { From 5e351bda5e254fba37719e7d9ae3f61cf1cbeb58 Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Wed, 4 Jun 2025 17:52:27 -0400 Subject: [PATCH 26/36] Send ROS2 topics over CAN bus with SocketCAN successfully --- autonomy/interfacing/can/config/params.yaml | 6 +- autonomy/interfacing/can/include/can_core.hpp | 5 +- autonomy/interfacing/can/include/can_node.hpp | 2 +- autonomy/interfacing/can/src/can_core.cpp | 104 ++++++++------ autonomy/interfacing/can/src/can_node.cpp | 128 +++++++++++++----- .../interfacing/can/test/test_controller.cpp | 6 +- 6 files changed, 164 insertions(+), 87 deletions(-) diff --git a/autonomy/interfacing/can/config/params.yaml b/autonomy/interfacing/can/config/params.yaml index 5bbf96c..77055fc 100644 --- a/autonomy/interfacing/can/config/params.yaml +++ b/autonomy/interfacing/can/config/params.yaml @@ -4,12 +4,12 @@ can_node: # CAN Interface Configuration can_interface: "can0" # CAN interface name (can0, can1, etc.) device_path: "/dev/canable" # Serial device path for SLCAN - bustype: "slcan" # Bus type: "socketcan" or "slcan" - bitrate: 500000 # CAN bitrate in bps (125000, 250000, 500000, 1000000) + bustype: "slcan" # Bus type to create a CAN interface: "socketcan" or "slcan" + bitrate: 1000000 # CAN bitrate in bps (125000, 250000, 500000, 1000000) # Communication Settings publish_rate_hz: 50 # Rate for checking incoming CAN messages - receive_timeout_ms: 100000 # Timeout for receiving CAN messages + receive_timeout_ms: 10000 # Timeout for receiving CAN messages # Topics to subscribe to (message types will be auto-detected) topics: diff --git a/autonomy/interfacing/can/include/can_core.hpp b/autonomy/interfacing/can/include/can_core.hpp index 0fa772f..09114a6 100644 --- a/autonomy/interfacing/can/include/can_core.hpp +++ b/autonomy/interfacing/can/include/can_core.hpp @@ -11,12 +11,9 @@ namespace autonomy struct CanMessage { uint32_t id; // CAN message ID - std::vector data; // Message data (up to 8 bytes for classic CAN, up to 64 bytes for CAN-FD) + std::vector data; // Message data (up to 8 bytes for classic CAN) bool is_extended_id; // Extended frame format flag bool is_remote_frame; // Remote transmission request flag - bool is_fd_frame; // CAN-FD frame flag - bool is_brs; // Bit Rate Switch (for CAN-FD) - bool is_esi; // Error State Indicator (for CAN-FD) uint64_t timestamp_us; // Timestamp in microseconds }; diff --git a/autonomy/interfacing/can/include/can_node.hpp b/autonomy/interfacing/can/include/can_node.hpp index 4ff0d96..175dee2 100644 --- a/autonomy/interfacing/can/include/can_node.hpp +++ b/autonomy/interfacing/can/include/can_node.hpp @@ -32,7 +32,7 @@ class CanNode : public rclcpp::Node { // Helper methods uint32_t generateCanId(const std::string& topic_name); - autonomy::CanMessage createCanMessage(const std::string& topic_name, std::shared_ptr ros_msg); + std::vector createCanMessages(const std::string& topic_name, std::shared_ptr ros_msg); }; #endif // CAN_NODE_HPP diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp index e5d4eca..39268c2 100644 --- a/autonomy/interfacing/can/src/can_core.cpp +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -1,6 +1,6 @@ #include "can_core.hpp" #include // Core socket functions for SocketCAN [socket(), bind(), send(), recv()] -#include // Definitions for can frames +#include // Definitions for can frames and CAN FD #include // CAN RAW #include #include @@ -8,7 +8,6 @@ #include #include // For std::system to call the external script for slcand #include // checking if script files exist -#include // For errno #include "ament_index_cpp/get_package_share_directory.hpp" // to load the bash scripts #include "ament_index_cpp/get_package_prefix.hpp" // defines PackageNotFoundError @@ -60,30 +59,71 @@ bool CanCore::isInitialized() const bool CanCore::sendMessage(const CanMessage& message) { - // Log the details of the received CAN message - RCLCPP_INFO(logger_, "sendMessage called with CanMessage:"); + if (!initialized_ || socket_fd_ < 0) { + RCLCPP_ERROR(logger_, "CAN interface not initialized or socket not valid. Cannot send message."); + return false; + } + + RCLCPP_INFO(logger_, "Sending CAN frame:"); RCLCPP_INFO(logger_, " ID: 0x%X (%s)", message.id, message.is_extended_id ? "Extended" : "Standard"); - RCLCPP_INFO(logger_, " Timestamp (us): %lu", message.timestamp_us); - RCLCPP_INFO(logger_, " Flags: FD=%d, BRS=%d, ESI=%d, RTR=%d", - message.is_fd_frame, message.is_brs, message.is_esi, message.is_remote_frame); RCLCPP_INFO(logger_, " Data Length: %zu bytes", message.data.size()); + + // Log the actual data bytes + std::string data_hex = ""; + for (size_t i = 0; i < message.data.size(); ++i) { + char hex_byte[8]; + snprintf(hex_byte, sizeof(hex_byte), "%02X", message.data[i]); + data_hex += hex_byte; + if (i < message.data.size() - 1) data_hex += " "; + } + RCLCPP_INFO(logger_, " Data: [%s]", data_hex.c_str()); + + ssize_t bytes_written = -1; + size_t expected_frame_size = 0; - if (!message.data.empty()) { - std::stringstream ss; - ss << " Data: "; - for (size_t i = 0; i < message.data.size(); ++i) { - ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(message.data[i]) << " "; - } - RCLCPP_INFO(logger_, "%s", ss.str().c_str()); + // Classic CAN frame only + struct can_frame frame; + std::memset(&frame, 0, sizeof(frame)); + + frame.can_id = message.id; + if (message.is_extended_id) { + frame.can_id |= CAN_EFF_FLAG; + } + if (message.is_remote_frame) { + frame.can_id |= CAN_RTR_FLAG; + } + + if (message.data.size() > CAN_MAX_DLEN) { + RCLCPP_WARN(logger_, "Classic CAN message data size (%zu) exceeds classic CAN max DLC (%d). Truncating.", + message.data.size(), CAN_MAX_DLEN); + frame.can_dlc = CAN_MAX_DLEN; } else { - RCLCPP_INFO(logger_, " Data: (empty)"); + frame.can_dlc = static_cast<__u8>(message.data.size()); + } + + if (!message.is_remote_frame) { + std::memcpy(frame.data, message.data.data(), frame.can_dlc); } - // For now, we are just logging and not actually sending. + expected_frame_size = sizeof(struct can_frame); + bytes_written = write(socket_fd_, &frame, expected_frame_size); + + if (bytes_written < 0) { + RCLCPP_ERROR(logger_, "Failed to write CAN frame to socket: %s", strerror(errno)); + return false; + } + + if (static_cast(bytes_written) < expected_frame_size) { + RCLCPP_WARN(logger_, "Could not send full CAN frame. Sent %zd bytes, expected %zu bytes.", + bytes_written, expected_frame_size); + return false; + } + + RCLCPP_INFO(logger_, "CAN frame sent successfully! (%zd bytes written)", bytes_written); return true; } -bool CanCore::receiveMessage([[maybe_unused]] CanMessage& message) +bool CanCore::receiveMessage(CanMessage& message) { // TODO: Implement CAN message reception using SocketCAN @@ -94,7 +134,7 @@ bool CanCore::receiveMessage([[maybe_unused]] CanMessage& message) bool CanCore::setupSocketCan() { RCLCPP_INFO(logger_, "Setting up SocketCAN interface: %s", config_.interface_name.c_str()); - + // Create socket socket_fd_ = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (socket_fd_ < 0) { @@ -102,47 +142,33 @@ bool CanCore::setupSocketCan() return false; } - // Enable CAN FD frames - int enable_canfd = 1; - if (setsockopt(socket_fd_, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &enable_canfd, sizeof(enable_canfd)) < 0) { - RCLCPP_ERROR(logger_, "Failed to enable CAN FD frames on socket: %s", strerror(errno)); - close(socket_fd_); - socket_fd_ = -1; - return false; - } - RCLCPP_INFO(logger_, "CAN FD frames enabled on socket."); - // Get interface index struct ifreq ifr; std::strncpy(ifr.ifr_name, config_.interface_name.c_str(), IFNAMSIZ - 1); - ifr.ifr_name[IFNAMSIZ - 1] = '\\0'; // Ensure null termination - + ifr.ifr_name[IFNAMSIZ - 1] = '\0'; // Ensure null termination if (ioctl(socket_fd_, SIOCGIFINDEX, &ifr) < 0) { - RCLCPP_ERROR(logger_, "Failed to get interface index for '%s': %s", config_.interface_name.c_str(), strerror(errno)); + RCLCPP_ERROR(logger_, "Failed to get interface index for %s: %s", config_.interface_name.c_str(), strerror(errno)); close(socket_fd_); socket_fd_ = -1; return false; } - RCLCPP_INFO(logger_, "Interface index for '%s' is %d.", config_.interface_name.c_str(), ifr.ifr_ifindex); - // Configure socket address + // Bind socket to the CAN interface struct sockaddr_can addr; addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; - // Bind socket if (bind(socket_fd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - RCLCPP_ERROR(logger_, "Failed to bind SocketCAN socket to interface '%s' (index %d): %s", - config_.interface_name.c_str(), addr.can_ifindex, strerror(errno)); + RCLCPP_ERROR(logger_, "Failed to bind SocketCAN socket to %s: %s", config_.interface_name.c_str(), strerror(errno)); close(socket_fd_); socket_fd_ = -1; return false; } - + initialized_ = true; - connected_ = true; // Assuming bind success means connected for SocketCAN raw + connected_ = true; - RCLCPP_INFO(logger_, "SocketCAN interface '%s' setup and bound successfully.", config_.interface_name.c_str()); + RCLCPP_INFO(logger_, "SocketCAN interface %s setup completed successfully (Classic CAN mode).", config_.interface_name.c_str()); return true; } diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index c7bf931..cbf6677 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -132,14 +132,23 @@ void CanNode::createSubscribers() { } void CanNode::topicCallback(std::shared_ptr msg, const std::string& topic_name, [[maybe_unused]] const std::string& topic_type) { - // Create CAN message from ROS message - autonomy::CanMessage can_message = createCanMessage(topic_name, msg); + // Create CAN message(s) from ROS message + std::vector can_messages = createCanMessages(topic_name, msg); - // Send the CAN message - if (can_.sendMessage(can_message)) { - return; - } else { - RCLCPP_ERROR(this->get_logger(), "Failed to send CAN message for topic '%s'", topic_name.c_str()); + // Send the CAN message(s) + int successful_sends = 0; + for (const auto& can_message : can_messages) { + if (can_.sendMessage(can_message)) { + successful_sends++; + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to send CAN message for topic '%s' (ID 0x%X)", topic_name.c_str(), can_message.id); + // Optionally, decide if you want to stop sending other fragments if one fails + } + } + + if (can_messages.size() > 1) { + RCLCPP_INFO(this->get_logger(), "Successfully sent %d/%zu CAN frames for topic '%s'", + successful_sends, can_messages.size(), topic_name.c_str()); } } @@ -154,46 +163,91 @@ uint32_t CanNode::generateCanId(const std::string& topic_name) { return hash & 0x1FFFFFFF; } -autonomy::CanMessage CanNode::createCanMessage(const std::string& topic_name, std::shared_ptr ros_msg) { - autonomy::CanMessage can_msg; +std::vector CanNode::createCanMessages(const std::string& topic_name, std::shared_ptr ros_msg) { + std::vector messages_to_send; - // Generate CAN ID from topic name - can_msg.id = generateCanId(topic_name); + uint32_t can_id = generateCanId(topic_name); - can_msg.is_fd_frame = true; - can_msg.is_extended_id = true; - can_msg.is_remote_frame = false; - can_msg.is_brs = true; // Enable bit rate switching for faster data transmission - can_msg.is_esi = false; // Error state indicator (false = active error state) - - // Get current timestamp - auto now = std::chrono::high_resolution_clock::now(); - can_msg.timestamp_us = std::chrono::duration_cast( - now.time_since_epoch()).count(); - - // Package the ROS message data into CAN frame. CAN-FD supports up to 64 bytes of data - const size_t max_can_fd_data_size = 64; + // Common properties for all messages/fragments + bool is_extended = true; + bool is_rtr = false; + + auto now_chrono = std::chrono::high_resolution_clock::now(); + uint64_t timestamp = std::chrono::duration_cast( + now_chrono.time_since_epoch()).count(); + + const uint8_t* ros_msg_buffer = ros_msg->get_rcl_serialized_message().buffer; size_t ros_msg_size = ros_msg->size(); + + // Determine max data payload per frame + // For Classic CAN, it's 8 bytes maximum + // We'll reserve 1 byte for sequence number if fragmentation is needed. + const size_t max_data_bytes_per_classic_frame = 8; - // Data Packaging - if (ros_msg_size <= max_can_fd_data_size) { // if message fits in single CAN-FD frame + size_t max_payload_per_frame = max_data_bytes_per_classic_frame; + size_t data_chunk_size = max_payload_per_frame; + bool needs_fragmentation = ros_msg_size > max_payload_per_frame; + + if (needs_fragmentation) { + // Reserve 1 byte for sequence number if fragmenting + data_chunk_size = max_payload_per_frame - 1; + if (data_chunk_size == 0 && max_payload_per_frame > 0) { // Should not happen with CAN_MAX_DLEN >= 1 + RCLCPP_ERROR(this->get_logger(), "Calculated data_chunk_size is 0, this should not happen. Max payload: %zu", max_payload_per_frame); + return messages_to_send; // Return empty, indicates error + } + } + + if (!needs_fragmentation) { + autonomy::CanMessage can_msg; + can_msg.id = can_id; + can_msg.is_extended_id = is_extended; + can_msg.is_remote_frame = is_rtr; + can_msg.timestamp_us = timestamp; + can_msg.data.resize(ros_msg_size); - std::memcpy(can_msg.data.data(), ros_msg->get_rcl_serialized_message().buffer, ros_msg_size); + std::memcpy(can_msg.data.data(), ros_msg_buffer, ros_msg_size); - RCLCPP_DEBUG(this->get_logger(), "Packaged %zu bytes from topic '%s' into single CAN-FD frame with ID 0x%X", + RCLCPP_DEBUG(this->get_logger(), "Packaged %zu bytes from topic '%s' into single CAN frame with ID 0x%X (Classic CAN)", ros_msg_size, topic_name.c_str(), can_msg.id); + messages_to_send.push_back(can_msg); } else { - // Message is too large for single CAN-FD frame - // For now, we'll truncate to fit in one frame and log a warning - // TODO: Implement multi-frame transmission protocol - RCLCPP_WARN(this->get_logger(), "Message from topic '%s' (%zu bytes) exceeds CAN-FD frame limit (%zu bytes). Truncating.", - topic_name.c_str(), ros_msg_size, max_can_fd_data_size); - - can_msg.data.resize(max_can_fd_data_size); - std::memcpy(can_msg.data.data(), ros_msg->get_rcl_serialized_message().buffer, max_can_fd_data_size); + RCLCPP_INFO(this->get_logger(), "Message from topic '%s' (%zu bytes) is too large for a single CAN frame (max %zu bytes). Fragmenting.", + topic_name.c_str(), ros_msg_size, max_payload_per_frame); + + size_t bytes_sent = 0; + uint8_t sequence_number = 0; + + while (bytes_sent < ros_msg_size) { + autonomy::CanMessage can_fragment; + can_fragment.id = can_id; // All fragments share the same ID for now + can_fragment.is_extended_id = is_extended; + can_fragment.is_remote_frame = is_rtr; + can_fragment.timestamp_us = timestamp; // Could also update timestamp per fragment + + size_t current_fragment_payload_size = std::min(data_chunk_size, ros_msg_size - bytes_sent); + + can_fragment.data.resize(1 + current_fragment_payload_size); // 1 byte for sequence number + can_fragment.data[0] = sequence_number; + std::memcpy(can_fragment.data.data() + 1, ros_msg_buffer + bytes_sent, current_fragment_payload_size); + + messages_to_send.push_back(can_fragment); + + RCLCPP_INFO(this->get_logger(), "Created fragment %u for topic '%s' (ID 0x%X), size %zu (payload %zu)", + sequence_number, topic_name.c_str(), can_fragment.id, can_fragment.data.size(), current_fragment_payload_size); + + bytes_sent += current_fragment_payload_size; + sequence_number++; + + if (sequence_number == 0 && bytes_sent < ros_msg_size) { // Rollover, too many fragments + RCLCPP_ERROR(this->get_logger(), "Too many fragments for message from topic '%s'. Max 256 fragments supported with 1-byte sequence.", topic_name.c_str()); + messages_to_send.clear(); // Indicate error by returning no messages + return messages_to_send; + } + } + RCLCPP_INFO(this->get_logger(), "Fragmented message from topic '%s' into %zu frames.", topic_name.c_str(), messages_to_send.size()); } - return can_msg; + return messages_to_send; } int main(int argc, char **argv) { diff --git a/autonomy/interfacing/can/test/test_controller.cpp b/autonomy/interfacing/can/test/test_controller.cpp index a9a06bb..cf25538 100644 --- a/autonomy/interfacing/can/test/test_controller.cpp +++ b/autonomy/interfacing/can/test/test_controller.cpp @@ -24,16 +24,16 @@ class TestControllerNode : public rclcpp::Node { auto message = std_msgs::msg::String(); - // Simple test message with timestamp + // Create a readable test message with timestamp auto now = std::chrono::system_clock::now(); auto time_t = std::chrono::system_clock::to_time_t(now); - message.data = "test_data:timestamp:" + std::to_string(time_t) + ";status:active;"; + message.data = "Hello from WATonomous! Timestamp: " + std::to_string(time_t) + " Status: Active"; // Publish the message publisher_->publish(message); - // RCLCPP_INFO(this->get_logger(), "Published: %s", message.data.c_str()); + RCLCPP_INFO(this->get_logger(), "Published: %s", message.data.c_str()); } rclcpp::Publisher::SharedPtr publisher_; From b76914c8e17aa31182b757fdb0d50f07e549c54b Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Thu, 5 Jun 2025 18:47:18 -0400 Subject: [PATCH 27/36] Review and change some comments --- autonomy/interfacing/can/src/can_core.cpp | 3 --- autonomy/interfacing/can/src/can_node.cpp | 20 +++++++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp index 39268c2..8bddd64 100644 --- a/autonomy/interfacing/can/src/can_core.cpp +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -94,8 +94,6 @@ bool CanCore::sendMessage(const CanMessage& message) } if (message.data.size() > CAN_MAX_DLEN) { - RCLCPP_WARN(logger_, "Classic CAN message data size (%zu) exceeds classic CAN max DLC (%d). Truncating.", - message.data.size(), CAN_MAX_DLEN); frame.can_dlc = CAN_MAX_DLEN; } else { frame.can_dlc = static_cast<__u8>(message.data.size()); @@ -227,5 +225,4 @@ bool CanCore::setupSlcan() RCLCPP_INFO(logger_, "Proceeding to configure '%s' as a SocketCAN interface.", config_.interface_name.c_str()); return setupSocketCan(); } - } diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index cbf6677..217665f 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -50,8 +50,7 @@ void CanNode::loadTopicConfigurations() { auto topic_names = this->get_parameter("topics").as_string_array(); for (const auto& topic_name : topic_names) { - // Discover the message type for this topic - std::string topic_type = discoverTopicType(topic_name); + std::string topic_type = discoverTopicType(topic_name); // Discover the message type for a given topic if (!topic_type.empty()) { TopicConfig config; @@ -77,13 +76,11 @@ void CanNode::loadTopicConfigurations() { } std::string CanNode::discoverTopicType(const std::string& topic_name) { - // Get topic information from ROS graph - auto topic_names_and_types = this->get_topic_names_and_types(); + auto topic_names_and_types = this->get_topic_names_and_types(); // get topic information from ROS graph for (const auto& topic_info : topic_names_and_types) { if (topic_info.first == topic_name) { if (!topic_info.second.empty()) { - // Return the first message type (there's usually only one) return topic_info.second[0]; } } @@ -92,7 +89,7 @@ std::string CanNode::discoverTopicType(const std::string& topic_name) { // If topic is not found, wait a bit and try again (topic might not be published yet) RCLCPP_INFO(this->get_logger(), "Topic '%s' not found, waiting for it to become available...", topic_name.c_str()); - // Wait up to 10 seconds for the topic to appear (increased from 5) + // Wait up to 10 seconds for the topic to appear for (int i = 0; i < 100; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); topic_names_and_types = this->get_topic_names_and_types(); @@ -132,17 +129,15 @@ void CanNode::createSubscribers() { } void CanNode::topicCallback(std::shared_ptr msg, const std::string& topic_name, [[maybe_unused]] const std::string& topic_type) { - // Create CAN message(s) from ROS message - std::vector can_messages = createCanMessages(topic_name, msg); + std::vector can_messages = createCanMessages(topic_name, msg); // Create CAN message(s) from ROS message - // Send the CAN message(s) + // Send CAN message int successful_sends = 0; for (const auto& can_message : can_messages) { if (can_.sendMessage(can_message)) { successful_sends++; } else { RCLCPP_ERROR(this->get_logger(), "Failed to send CAN message for topic '%s' (ID 0x%X)", topic_name.c_str(), can_message.id); - // Optionally, decide if you want to stop sending other fragments if one fails } } @@ -168,9 +163,8 @@ std::vector CanNode::createCanMessages(const std::string& uint32_t can_id = generateCanId(topic_name); - // Common properties for all messages/fragments - bool is_extended = true; - bool is_rtr = false; + bool is_extended = true; // Use extended CAN ID (29 bits) + bool is_rtr = false; // Remote Transmission Request auto now_chrono = std::chrono::high_resolution_clock::now(); uint64_t timestamp = std::chrono::duration_cast( From 35c3d60154a1f411c0876f86e0721b22b413666d Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Fri, 6 Jun 2025 15:05:12 -0400 Subject: [PATCH 28/36] Cleaning up debug logs for sending messages. Only send logs of sending # CAN frames from topic --- autonomy/interfacing/can/src/can_core.cpp | 27 ++++++++++++----------- autonomy/interfacing/can/src/can_node.cpp | 9 ++++---- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp index 8bddd64..e667831 100644 --- a/autonomy/interfacing/can/src/can_core.cpp +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -64,19 +64,20 @@ bool CanCore::sendMessage(const CanMessage& message) return false; } - RCLCPP_INFO(logger_, "Sending CAN frame:"); - RCLCPP_INFO(logger_, " ID: 0x%X (%s)", message.id, message.is_extended_id ? "Extended" : "Standard"); - RCLCPP_INFO(logger_, " Data Length: %zu bytes", message.data.size()); + // Below are just some debug logs to show the message being sent + // RCLCPP_INFO(logger_, "Sending CAN frame:"); + // RCLCPP_INFO(logger_, " ID: 0x%X (%s)", message.id, message.is_extended_id ? "Extended" : "Standard"); + // RCLCPP_INFO(logger_, " Data Length: %zu bytes", message.data.size()); - // Log the actual data bytes - std::string data_hex = ""; - for (size_t i = 0; i < message.data.size(); ++i) { - char hex_byte[8]; - snprintf(hex_byte, sizeof(hex_byte), "%02X", message.data[i]); - data_hex += hex_byte; - if (i < message.data.size() - 1) data_hex += " "; - } - RCLCPP_INFO(logger_, " Data: [%s]", data_hex.c_str()); + // // Log the actual data bytes + // std::string data_hex = ""; + // for (size_t i = 0; i < message.data.size(); ++i) { + // char hex_byte[8]; + // snprintf(hex_byte, sizeof(hex_byte), "%02X", message.data[i]); + // data_hex += hex_byte; + // if (i < message.data.size() - 1) data_hex += " "; + // } + // RCLCPP_INFO(logger_, " Data: [%s]", data_hex.c_str()); ssize_t bytes_written = -1; size_t expected_frame_size = 0; @@ -117,7 +118,7 @@ bool CanCore::sendMessage(const CanMessage& message) return false; } - RCLCPP_INFO(logger_, "CAN frame sent successfully! (%zd bytes written)", bytes_written); + // RCLCPP_INFO(logger_, "CAN frame sent successfully! (%zd bytes written)", bytes_written); return true; } diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index 217665f..8142d39 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -175,7 +175,7 @@ std::vector CanNode::createCanMessages(const std::string& // Determine max data payload per frame // For Classic CAN, it's 8 bytes maximum - // We'll reserve 1 byte for sequence number if fragmentation is needed. + // But reserve 1 byte for sequence number if fragmentation is needed. const size_t max_data_bytes_per_classic_frame = 8; size_t max_payload_per_frame = max_data_bytes_per_classic_frame; @@ -226,8 +226,9 @@ std::vector CanNode::createCanMessages(const std::string& messages_to_send.push_back(can_fragment); - RCLCPP_INFO(this->get_logger(), "Created fragment %u for topic '%s' (ID 0x%X), size %zu (payload %zu)", - sequence_number, topic_name.c_str(), can_fragment.id, can_fragment.data.size(), current_fragment_payload_size); + // This is just logging that fragment creation was successful + // RCLCPP_INFO(this->get_logger(), "Created fragment %u for topic '%s' (ID 0x%X), size %zu (payload %zu)", + // sequence_number, topic_name.c_str(), can_fragment.id, can_fragment.data.size(), current_fragment_payload_size); bytes_sent += current_fragment_payload_size; sequence_number++; @@ -238,7 +239,7 @@ std::vector CanNode::createCanMessages(const std::string& return messages_to_send; } } - RCLCPP_INFO(this->get_logger(), "Fragmented message from topic '%s' into %zu frames.", topic_name.c_str(), messages_to_send.size()); + // RCLCPP_INFO(this->get_logger(), "Fragmented message from topic '%s' into %zu frames.", topic_name.c_str(), messages_to_send.size()); } return messages_to_send; From f58fc7e6fcd88ccac65ebebb8783959c9634a1a4 Mon Sep 17 00:00:00 2001 From: Gavin Tranquilino Date: Fri, 6 Jun 2025 17:54:23 -0400 Subject: [PATCH 29/36] Message listener --- autonomy/interfacing/can/include/can_core.hpp | 1 + autonomy/interfacing/can/include/can_node.hpp | 2 + autonomy/interfacing/can/src/can_core.cpp | 73 ++++++++++++++++++- autonomy/interfacing/can/src/can_node.cpp | 36 ++++++++- 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/autonomy/interfacing/can/include/can_core.hpp b/autonomy/interfacing/can/include/can_core.hpp index 09114a6..36bb194 100644 --- a/autonomy/interfacing/can/include/can_core.hpp +++ b/autonomy/interfacing/can/include/can_core.hpp @@ -12,6 +12,7 @@ namespace autonomy struct CanMessage { uint32_t id; // CAN message ID std::vector data; // Message data (up to 8 bytes for classic CAN) + uint8_t dlc; // Data Length Code bool is_extended_id; // Extended frame format flag bool is_remote_frame; // Remote transmission request flag uint64_t timestamp_us; // Timestamp in microseconds diff --git a/autonomy/interfacing/can/include/can_node.hpp b/autonomy/interfacing/can/include/can_node.hpp index 175dee2..534af57 100644 --- a/autonomy/interfacing/can/include/can_node.hpp +++ b/autonomy/interfacing/can/include/can_node.hpp @@ -23,12 +23,14 @@ class CanNode : public rclcpp::Node { autonomy::CanCore can_; std::vector topic_configs_; std::vector> subscribers_; + rclcpp::TimerBase::SharedPtr receive_timer_; // Timer to periodically check for CAN messages // Methods void loadTopicConfigurations(); void createSubscribers(); std::string discoverTopicType(const std::string& topic_name); void topicCallback(std::shared_ptr msg, const std::string& topic_name, const std::string& topic_type); + void receiveCanMessages(); // Method to be called by the timer // Helper methods uint32_t generateCanId(const std::string& topic_name); diff --git a/autonomy/interfacing/can/src/can_core.cpp b/autonomy/interfacing/can/src/can_core.cpp index e667831..a3946b7 100644 --- a/autonomy/interfacing/can/src/can_core.cpp +++ b/autonomy/interfacing/can/src/can_core.cpp @@ -5,6 +5,7 @@ #include #include #include +#include // For fcntl #include #include // For std::system to call the external script for slcand #include // checking if script files exist @@ -124,10 +125,60 @@ bool CanCore::sendMessage(const CanMessage& message) bool CanCore::receiveMessage(CanMessage& message) { - // TODO: Implement CAN message reception using SocketCAN + if (!initialized_ || socket_fd_ < 0) { + RCLCPP_ERROR(logger_, "CAN interface not initialized or socket not valid. Cannot receive message."); + return false; + } + + struct can_frame frame; + ssize_t bytes_read = read(socket_fd_, &frame, sizeof(struct can_frame)); + + if (bytes_read < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // No data available right now (non-blocking mode) + return false; + } + RCLCPP_ERROR(logger_, "Failed to read CAN frame from socket: %s", strerror(errno)); + return false; + } + + if (bytes_read == 0) { + // According to read(2), a return value of 0 indicates end-of-file. + // For a socket, this can mean the peer has performed an orderly shutdown. + // For raw CAN sockets, this is less typical than EAGAIN for no data. + // RCLCPP_WARN(logger_, "Read 0 bytes from CAN socket. Peer might have closed connection or no data."); + return false; + } + + if (static_cast(bytes_read) < sizeof(struct can_frame)) { + RCLCPP_WARN(logger_, "Incomplete CAN frame received. Read %zd bytes, expected %zu bytes.", + bytes_read, sizeof(struct can_frame)); + return false; + } + + message.is_extended_id = (frame.can_id & CAN_EFF_FLAG) ? true : false; + message.is_remote_frame = (frame.can_id & CAN_RTR_FLAG) ? true : false; - RCLCPP_WARN(logger_, "receiveMessage not yet implemented"); - return false; + if (message.is_extended_id) { + message.id = frame.can_id & CAN_EFF_MASK; + } else { + message.id = frame.can_id & CAN_SFF_MASK; + } + + message.dlc = frame.can_dlc; + message.data.resize(frame.can_dlc); + if (!message.is_remote_frame) { + std::memcpy(message.data.data(), frame.data, frame.can_dlc); + } else { + // For RTR frames, data field is irrelevant but dlc indicates requested data length + message.data.clear(); + } + + // This is to log received message details for debugging + RCLCPP_INFO(logger_, "CAN frame received: ID=0x%X, Extended=%d, RTR=%d", + message.id, message.is_extended_id, message.is_remote_frame); + + return true; } bool CanCore::setupSocketCan() @@ -141,6 +192,22 @@ bool CanCore::setupSocketCan() return false; } + // Set socket to non-blocking + int flags = fcntl(socket_fd_, F_GETFL, 0); + if (flags == -1) { + RCLCPP_ERROR(logger_, "Failed to get socket flags: %s", strerror(errno)); + close(socket_fd_); + socket_fd_ = -1; + return false; + } + if (fcntl(socket_fd_, F_SETFL, flags | O_NONBLOCK) == -1) { + RCLCPP_ERROR(logger_, "Failed to set socket to non-blocking: %s", strerror(errno)); + close(socket_fd_); + socket_fd_ = -1; + return false; + } + RCLCPP_INFO(logger_, "Socket set to non-blocking mode."); + // Get interface index struct ifreq ifr; std::strncpy(ifr.ifr_name, config_.interface_name.c_str(), IFNAMSIZ - 1); diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index 8142d39..ddccc06 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -13,15 +13,17 @@ CanNode::CanNode() : Node("can_node"), can_(this->get_logger()) { this->declare_parameter("device_path", "/dev/canable"); this->declare_parameter("bustype", "slcan"); this->declare_parameter("bitrate", 500000); + this->declare_parameter("receive_poll_interval_ms", 10); // Parameter for polling interval // Get parameter values std::string can_interface = this->get_parameter("can_interface").as_string(); std::string device_path = this->get_parameter("device_path").as_string(); std::string bustype = this->get_parameter("bustype").as_string(); int bitrate = this->get_parameter("bitrate").as_int(); + long receive_poll_interval_ms = this->get_parameter("receive_poll_interval_ms").as_int(); - RCLCPP_INFO(this->get_logger(), "Loaded parameters: interface=%s, bustype=%s, bitrate=%d", - can_interface.c_str(), bustype.c_str(), bitrate); + RCLCPP_INFO(this->get_logger(), "Loaded parameters: interface=%s, bustype=%s, bitrate=%d, poll_interval_ms=%ld", + can_interface.c_str(), bustype.c_str(), bitrate, receive_poll_interval_ms); // Configure CanCore autonomy::CanConfig config; @@ -29,11 +31,18 @@ CanNode::CanNode() : Node("can_node"), can_(this->get_logger()) { config.device_path = device_path; config.bustype = bustype; config.bitrate = bitrate; - config.receive_timeout_ms = 10000; + config.receive_timeout_ms = 10000; // This specific timeout in CanConfig might be for other uses or can be reviewed. // Initialize the CAN interface if (can_.initialize(config)) { RCLCPP_INFO(this->get_logger(), "CAN Core interface initialized successfully"); + + // Setup a timer to periodically call receiveCanMessages + receive_timer_ = this->create_wall_timer( + std::chrono::milliseconds(receive_poll_interval_ms), + std::bind(&CanNode::receiveCanMessages, this)); + RCLCPP_INFO(this->get_logger(), "CAN message receive timer started with %ld ms interval.", receive_poll_interval_ms); + } else { RCLCPP_ERROR(this->get_logger(), "Failed to initialize CAN Core"); } @@ -147,6 +156,27 @@ void CanNode::topicCallback(std::shared_ptr msg, cons } } +void CanNode::receiveCanMessages() { + autonomy::CanMessage received_msg; + // Attempt to receive a message. CanCore::receiveMessage is non-blocking. + if (can_.receiveMessage(received_msg)) { + // Print the received message details to the console + std::stringstream ss; + ss << "Received CAN Message: ID=0x" << std::hex << received_msg.id + << std::dec << ", DLC=" << static_cast(received_msg.dlc) + << ", Extended=" << received_msg.is_extended_id + << ", RTR=" << received_msg.is_remote_frame + << ", Data=[ "; + for (size_t i = 0; i < received_msg.data.size(); ++i) { + ss << "0x" << std::hex << static_cast(received_msg.data[i]) << (i < received_msg.data.size() - 1 ? " " : ""); + } + ss << std::dec << " ]"; // Switch back to decimal for any further logging if needed + RCLCPP_INFO(this->get_logger(), "%s", ss.str().c_str()); + } + // If receiveMessage returns false, it means no message was available at that moment (non-blocking read) + // or an error occurred (which CanCore should log). No action needed here for no-message case. +} + uint32_t CanNode::generateCanId(const std::string& topic_name) { // Generate a CAN ID from topic name using hash // Use the first 29 bits for extended CAN ID (CAN 2.0B) From bbdb16b0b0c4fbb1dad0bf44c51e9f1ff5c30871 Mon Sep 17 00:00:00 2001 From: gavintranquilino Date: Wed, 11 Jun 2025 15:52:38 -0400 Subject: [PATCH 30/36] Package level documenation for common_msgs --- autonomy/wato_msgs/common_msgs/README.md | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 autonomy/wato_msgs/common_msgs/README.md diff --git a/autonomy/wato_msgs/common_msgs/README.md b/autonomy/wato_msgs/common_msgs/README.md new file mode 100644 index 0000000..fe5f2e0 --- /dev/null +++ b/autonomy/wato_msgs/common_msgs/README.md @@ -0,0 +1,64 @@ +# Common Messages (common_msgs) + +## File Tree/Package Structure +This message package follows standard ROS2 package scheme: +``` +common_msgs/ +├── CMakeLists.txt +├── package.xml +├── README.md +└── msg/ + ├── ArmPose.msg + ├── HandPose.msg + └── JointState.msg +``` + +## Purpose +The `common_msgs` package serves as a global package for message definitions that are commonly used across various components and packages within the humanoid project. + +## Inputs & Outputs +This package primarily defines message structures. Therefore: +- **Outputs**: Provides `.msg` definitions that other ROS 2 packages can import and use. +- **Inputs**: Does not process data or subscribe to topics; it only defines data structures. + +## Key Features +- **Message Definitions**: Contains a collection of `.msg` files, each defining a specific data structure. These messages are designed to be generic and applicable in multiple contexts. + +### Message Definitions +- `ArmPose.msg`: Defines the pose of the humanoid arm, including optional HandPose inside. +- `HandPose.msg`: Defines the pose of the humanoid hand, including each JointState of each finger's joints. +- `JointState.msg`: Defines the state of a joint, including position, velocity, orientation, and effort. + +## Usage +To use the messages defined in this package within another ROS 2 package: + +1. **Copy the package into your Docker image's source workspace**: Add the following line to your Dockerfile (typically `root/docker/MODULE_NAME/MODULE_NAME.Dockerfile`), typically in the section where you copy your source code: + ```dockerfile + # # Copy in source code + # # COPY autonomy/wato_msgs/sample_msgs sample_msgs + COPY autonomy/wato_msgs/common_msgs common_msgs + ``` + +2. **Add Dependency**: Ensure that `common_msgs` is listed as a dependency in the `package.xml` of your consuming package: + ```xml + common_msgs + ``` +3. **Include in CMakeLists.txt** (for C++ packages): + ```cmake + find_package(common_msgs REQUIRED) + ament_target_dependencies(your_target_name common_msgs) + ``` +4. **Import in Python scripts**: + ```python + from common_msgs.msg import YourMessageName + ``` +5. **Include in C++ code**: + ```cpp + #include "common_msgs/msg/your_message_name.hpp" + ``` + +### Testing +Currently, this package primarily contains message definitions, so testing focuses on ensuring they can be correctly compiled and used by dependent packages. + +## Configuration +- **Parameters**: No configurable parameters are defined within this package itself. From e8e54954f0c7674daffe19c65a36e9827f288c39 Mon Sep 17 00:00:00 2001 From: gavintranquilino Date: Wed, 11 Jun 2025 16:05:59 -0400 Subject: [PATCH 31/36] Update each message description --- autonomy/wato_msgs/common_msgs/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autonomy/wato_msgs/common_msgs/README.md b/autonomy/wato_msgs/common_msgs/README.md index fe5f2e0..73ea65d 100644 --- a/autonomy/wato_msgs/common_msgs/README.md +++ b/autonomy/wato_msgs/common_msgs/README.md @@ -25,9 +25,9 @@ This package primarily defines message structures. Therefore: - **Message Definitions**: Contains a collection of `.msg` files, each defining a specific data structure. These messages are designed to be generic and applicable in multiple contexts. ### Message Definitions -- `ArmPose.msg`: Defines the pose of the humanoid arm, including optional HandPose inside. -- `HandPose.msg`: Defines the pose of the humanoid hand, including each JointState of each finger's joints. -- `JointState.msg`: Defines the state of a joint, including position, velocity, orientation, and effort. +- `ArmPose.msg`: Describes the pose of an arm, including its major joints and optionally the hand. +- `HandPose.msg`: Details the pose of a hand, including individual finger joint states. +- `JointState.msg`: A custom message (similar message structure to [sensor_msgs/JointState](https://docs.ros.org/en/humble/p/sensor_msgs/msg/JointState.html)) to represent a single joint. ## Usage To use the messages defined in this package within another ROS 2 package: From 459c025460f131247e1fb47767c254fd61e7ef3f Mon Sep 17 00:00:00 2001 From: miekale Date: Sat, 21 Jun 2025 17:10:56 -0400 Subject: [PATCH 32/36] adding support for topic specific handlers --- .gitignore | 1 + autonomy/interfacing/can/config/params.yaml | 9 +- autonomy/interfacing/can/include/can_node.hpp | 4 +- autonomy/interfacing/can/src/can_node.cpp | 124 +++++++++++++++--- 4 files changed, 115 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 7411f32..64716e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .env __pycache__ .vscode/ +cursor/ watod-config.local.sh diff --git a/autonomy/interfacing/can/config/params.yaml b/autonomy/interfacing/can/config/params.yaml index 77055fc..d94f7fb 100644 --- a/autonomy/interfacing/can/config/params.yaml +++ b/autonomy/interfacing/can/config/params.yaml @@ -9,8 +9,11 @@ can_node: # Communication Settings publish_rate_hz: 50 # Rate for checking incoming CAN messages - receive_timeout_ms: 10000 # Timeout for receiving CAN messages - + receive_timeout_ms: 10000 # Timeout for receiving CAN messages # Topics to subscribe to (message types will be auto-detected) topics: - - "/test_controller" \ No newline at end of file + - "/test_controller" + - "/cmd_arm_joint" + - "/cmd_arm_ee" + - "/cmd_hand_joint" + - "/cmd_hand_ee" \ No newline at end of file diff --git a/autonomy/interfacing/can/include/can_node.hpp b/autonomy/interfacing/can/include/can_node.hpp index 534af57..577b121 100644 --- a/autonomy/interfacing/can/include/can_node.hpp +++ b/autonomy/interfacing/can/include/can_node.hpp @@ -22,7 +22,7 @@ class CanNode : public rclcpp::Node { private: autonomy::CanCore can_; std::vector topic_configs_; - std::vector> subscribers_; + std::unordered_map> subscribers_; rclcpp::TimerBase::SharedPtr receive_timer_; // Timer to periodically check for CAN messages // Methods @@ -34,7 +34,7 @@ class CanNode : public rclcpp::Node { // Helper methods uint32_t generateCanId(const std::string& topic_name); - std::vector createCanMessages(const std::string& topic_name, std::shared_ptr ros_msg); + std::vector createCanMessages(const std::string& topic_name, std::shared_ptr ros_msg, uint32_t can_id = 0); }; #endif // CAN_NODE_HPP diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index ddccc06..ca7b7de 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -120,27 +120,115 @@ std::string CanNode::discoverTopicType(const std::string& topic_name) { void CanNode::createSubscribers() { for (const auto& topic_config : topic_configs_) { - // Create generic subscriber that can handle any message type - auto subscriber = this->create_generic_subscription( - topic_config.name, - topic_config.type, - 10, - [this, topic_name = topic_config.name, topic_type = topic_config.type] - (std::shared_ptr msg) { - this->topicCallback(msg, topic_name, topic_type); - } - ); + if (topic_config.name == "/test_controller") { + subscribers_[topic_config.name] = this->create_generic_subscription( + topic_config.name, + topic_config.type, + 10, + [this](std::shared_ptr msg) { + this->handleControllerTopic(msg, "/test_controller"); + } + ); + } else if (topic_config.name == "/cmd_arm_joint") { + subscribers_[topic_config.name] = this->create_generic_subscription( + topic_config.name, + topic_config.type, + 10, + [this](std::shared_ptr msg) { + this->handleJointTopic(msg, "/cmd_arm_joint"); + } + ); + } else if (topic_config.name == "/cmd_hand_joint") { + subscribers_[topic_config.name] = this->create_generic_subscription( + topic_config.name, + topic_config.type, + 10, + [this](std::shared_ptr msg) { + this->handleJointTopic(msg, "/cmd_hand_joint"); + } + ); + } else if (topic_config.name == "/cmd_arm_ee") { + subscribers_[topic_config.name] = this->create_generic_subscription( + topic_config.name, + topic_config.type, + 10, + [this](std::shared_ptr msg) { + this->handleEndEffectorTopic(msg, "/cmd_arm_ee"); + } + ); + } else if (topic_config.name == "/cmd_hand_ee") { + subscribers_[topic_config.name] = this->create_generic_subscription( + topic_config.name, + topic_config.type, + 10, + [this](std::shared_ptr msg) { + this->handleEndEffectorTopic(msg, "/cmd_hand_ee"); + } + ); + } else { + subscribers_[topic_config.name] = this->create_generic_subscription( + topic_config.name, + topic_config.type, + 10, + [this, topic_name = topic_config.name](std::shared_ptr msg) { + this->handleGenericTopic(msg, topic_name); + } + ); + } - subscribers_.push_back(subscriber); RCLCPP_INFO(this->get_logger(), "Created generic subscriber for topic: %s (type: %s)", topic_config.name.c_str(), topic_config.type.c_str()); } } -void CanNode::topicCallback(std::shared_ptr msg, const std::string& topic_name, [[maybe_unused]] const std::string& topic_type) { - std::vector can_messages = createCanMessages(topic_name, msg); // Create CAN message(s) from ROS message +// Add these new handler methods +void CanNode::handleControllerTopic(std::shared_ptr msg, const std::string& topic_name) { + // Custom CAN ID range for controller messages + uint32_t can_id = 0x100; // Controller messages start at 0x100 +y std::vector can_messages = createCanMessages(topic_name, msg, base_can_id); + + // Send with high priority + for (const auto& can_message : can_messages) { + if (!can_.sendMessage(can_message)) { + RCLCPP_ERROR(this->get_logger(), "Failed to send controller CAN message (ID 0x%X)", can_message.id); + } + } +} + +void CanNode::handleJointTopic(std::shared_ptr msg, const std::string& topic_name) { + // Custom CAN ID range for joint messages + uint32_t base_can_id = 0x200; // Joint messages start at 0x200 + std::vector can_messages = createCanMessages(topic_name, msg, base_can_id); + + // Send joint messages + for (const auto& can_message : can_messages) { + if (!can_.sendMessage(can_message)) { + RCLCPP_ERROR(this->get_logger(), "Failed to send joint CAN message (ID 0x%X)", can_message.id); + } + } +} + +void CanNode::handleEndEffectorTopic(std::shared_ptr msg, const std::string& topic_name) { + // Custom CAN ID range for end effector messages + uint32_t base_can_id = 0x300; // End effector messages start at 0x300 + std::vector can_messages = createCanMessages(topic_name, msg, base_can_id); - // Send CAN message + // Send end effector messages + for (const auto& can_message : can_messages) { + if (!can_.sendMessage(can_message)) { + RCLCPP_ERROR(this->get_logger(), "Failed to send end effector CAN message (ID 0x%X)", can_message.id); + } + } + + +void CanNode::handleGenericTopic(std::shared_ptr msg, const std::string& topic_name) { + // Original generic handling + std::vector can_messages = createCanMessages(topic_name, msg); + sendCanMessages(can_messages, topic_name); +} + +// Helper method to reduce code duplication +void CanNode::sendCanMessages(const std::vector& can_messages, const std::string& topic_name) { int successful_sends = 0; for (const auto& can_message : can_messages) { if (can_.sendMessage(can_message)) { @@ -177,6 +265,8 @@ void CanNode::receiveCanMessages() { // or an error occurred (which CanCore should log). No action needed here for no-message case. } + +// Not sure if this is needed, but it's here for now uint32_t CanNode::generateCanId(const std::string& topic_name) { // Generate a CAN ID from topic name using hash // Use the first 29 bits for extended CAN ID (CAN 2.0B) @@ -188,11 +278,9 @@ uint32_t CanNode::generateCanId(const std::string& topic_name) { return hash & 0x1FFFFFFF; } -std::vector CanNode::createCanMessages(const std::string& topic_name, std::shared_ptr ros_msg) { +std::vector CanNode::createCanMessages(const std::string& topic_name, std::shared_ptr ros_msg, uint32_t can_id) { std::vector messages_to_send; - - uint32_t can_id = generateCanId(topic_name); - + bool is_extended = true; // Use extended CAN ID (29 bits) bool is_rtr = false; // Remote Transmission Request From 50c6ddfd15086981293d93cb3f5b75f7183eb891 Mon Sep 17 00:00:00 2001 From: miekale Date: Sat, 21 Jun 2025 17:15:13 -0400 Subject: [PATCH 33/36] adding dummy topics --- autonomy/interfacing/can/src/can_node.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index ca7b7de..2803119 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -181,7 +181,7 @@ void CanNode::createSubscribers() { } } -// Add these new handler methods +// dummy handler for testing void CanNode::handleControllerTopic(std::shared_ptr msg, const std::string& topic_name) { // Custom CAN ID range for controller messages uint32_t can_id = 0x100; // Controller messages start at 0x100 From 1e0e8091de3fafcb3dd35ccbb3f30dfb687e71a8 Mon Sep 17 00:00:00 2001 From: miekale Date: Sat, 21 Jun 2025 17:15:20 -0400 Subject: [PATCH 34/36] adding dummy topics --- autonomy/interfacing/can/src/can_node.cpp | 39 ++++++----------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index 2803119..58f1c67 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -135,7 +135,7 @@ void CanNode::createSubscribers() { topic_config.type, 10, [this](std::shared_ptr msg) { - this->handleJointTopic(msg, "/cmd_arm_joint"); + this->handleArmJointTopic(msg, "/cmd_arm_joint"); } ); } else if (topic_config.name == "/cmd_hand_joint") { @@ -144,7 +144,7 @@ void CanNode::createSubscribers() { topic_config.type, 10, [this](std::shared_ptr msg) { - this->handleJointTopic(msg, "/cmd_hand_joint"); + this->handleHandJointTopic(msg, "/cmd_hand_joint"); } ); } else if (topic_config.name == "/cmd_arm_ee") { @@ -153,7 +153,7 @@ void CanNode::createSubscribers() { topic_config.type, 10, [this](std::shared_ptr msg) { - this->handleEndEffectorTopic(msg, "/cmd_arm_ee"); + this->handleArmEeTopic(msg, "/cmd_arm_ee"); } ); } else if (topic_config.name == "/cmd_hand_ee") { @@ -162,7 +162,7 @@ void CanNode::createSubscribers() { topic_config.type, 10, [this](std::shared_ptr msg) { - this->handleEndEffectorTopic(msg, "/cmd_hand_ee"); + this->handleHandEeTopic(msg, "/cmd_hand_ee"); } ); } else { @@ -182,10 +182,10 @@ void CanNode::createSubscribers() { } // dummy handler for testing -void CanNode::handleControllerTopic(std::shared_ptr msg, const std::string& topic_name) { +void CanNode::handleArmJointTopic(std::shared_ptr msg, const std::string& topic_name) { // Custom CAN ID range for controller messages uint32_t can_id = 0x100; // Controller messages start at 0x100 -y std::vector can_messages = createCanMessages(topic_name, msg, base_can_id); + std::vector can_messages = createCanMessages(topic_name, msg, can_id); // Send with high priority for (const auto& can_message : can_messages) { @@ -195,30 +195,9 @@ y std::vector can_messages = createCanMessages(topic_name } } -void CanNode::handleJointTopic(std::shared_ptr msg, const std::string& topic_name) { - // Custom CAN ID range for joint messages - uint32_t base_can_id = 0x200; // Joint messages start at 0x200 - std::vector can_messages = createCanMessages(topic_name, msg, base_can_id); - - // Send joint messages - for (const auto& can_message : can_messages) { - if (!can_.sendMessage(can_message)) { - RCLCPP_ERROR(this->get_logger(), "Failed to send joint CAN message (ID 0x%X)", can_message.id); - } - } -} - -void CanNode::handleEndEffectorTopic(std::shared_ptr msg, const std::string& topic_name) { - // Custom CAN ID range for end effector messages - uint32_t base_can_id = 0x300; // End effector messages start at 0x300 - std::vector can_messages = createCanMessages(topic_name, msg, base_can_id); - - // Send end effector messages - for (const auto& can_message : can_messages) { - if (!can_.sendMessage(can_message)) { - RCLCPP_ERROR(this->get_logger(), "Failed to send end effector CAN message (ID 0x%X)", can_message.id); - } - } +void CanNode::handleHandJointTopic(std::shared_ptr msg, const std::string& topic_name) {} +void CanNode::handleHandEeTopic(std::shared_ptr msg, const std::string& topic_name) {} +void CanNode::handleArmEeTopic(std::shared_ptr msg, const std::string& topic_name) {} void CanNode::handleGenericTopic(std::shared_ptr msg, const std::string& topic_name) { From 64e16ba5c8ad7cfcd3696700ec36f79ed575e8ed Mon Sep 17 00:00:00 2001 From: miekale Date: Sat, 21 Jun 2025 17:34:02 -0400 Subject: [PATCH 35/36] commenting out info --- autonomy/interfacing/can/src/can_node.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index 58f1c67..121d45e 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -184,15 +184,9 @@ void CanNode::createSubscribers() { // dummy handler for testing void CanNode::handleArmJointTopic(std::shared_ptr msg, const std::string& topic_name) { // Custom CAN ID range for controller messages - uint32_t can_id = 0x100; // Controller messages start at 0x100 - std::vector can_messages = createCanMessages(topic_name, msg, can_id); - - // Send with high priority - for (const auto& can_message : can_messages) { - if (!can_.sendMessage(can_message)) { - RCLCPP_ERROR(this->get_logger(), "Failed to send controller CAN message (ID 0x%X)", can_message.id); - } - } + // uint32_t can_id = 0x100; // set based on robot message + // std::vector can_messages = createCanMessages(topic_name, msg, can_id); + // sendCanMessages(can_messages, topic_name); } void CanNode::handleHandJointTopic(std::shared_ptr msg, const std::string& topic_name) {} From 07769f715879431bb91c328ca9051d31cb2b0d49 Mon Sep 17 00:00:00 2001 From: miekale Date: Sat, 21 Jun 2025 18:14:28 -0400 Subject: [PATCH 36/36] adding mit motors files --- autonomy/interfacing/can/config/params.yaml | 4 ++ .../can/include/mit_motor_control.hpp | 6 ++ autonomy/interfacing/can/src/can_node.cpp | 2 + .../interfacing/can/src/mit_motor_control.cpp | 70 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 autonomy/interfacing/can/include/mit_motor_control.hpp create mode 100644 autonomy/interfacing/can/src/mit_motor_control.cpp diff --git a/autonomy/interfacing/can/config/params.yaml b/autonomy/interfacing/can/config/params.yaml index d94f7fb..4415c42 100644 --- a/autonomy/interfacing/can/config/params.yaml +++ b/autonomy/interfacing/can/config/params.yaml @@ -10,6 +10,10 @@ can_node: # Communication Settings publish_rate_hz: 50 # Rate for checking incoming CAN messages receive_timeout_ms: 10000 # Timeout for receiving CAN messages + + profile_type: "trapezoidal" # "trapezoidal" or "triangular" + max_acc: 1.0 # m/s^2 + # Topics to subscribe to (message types will be auto-detected) topics: - "/test_controller" diff --git a/autonomy/interfacing/can/include/mit_motor_control.hpp b/autonomy/interfacing/can/include/mit_motor_control.hpp new file mode 100644 index 0000000..48f5193 --- /dev/null +++ b/autonomy/interfacing/can/include/mit_motor_control.hpp @@ -0,0 +1,6 @@ +#include "can_node.hpp" + +// MIT Motor Control Functions +void moveMIT(double velocity, double force, double position, int id); +void moveProfile(const std::string& profile_type, double max_acc); + diff --git a/autonomy/interfacing/can/src/can_node.cpp b/autonomy/interfacing/can/src/can_node.cpp index 121d45e..1fbf116 100644 --- a/autonomy/interfacing/can/src/can_node.cpp +++ b/autonomy/interfacing/can/src/can_node.cpp @@ -1,4 +1,5 @@ #include "can_node.hpp" +#include "mit_motor_control.hpp" #include #include #include @@ -217,6 +218,7 @@ void CanNode::sendCanMessages(const std::vector& can_messa } } + void CanNode::receiveCanMessages() { autonomy::CanMessage received_msg; // Attempt to receive a message. CanCore::receiveMessage is non-blocking. diff --git a/autonomy/interfacing/can/src/mit_motor_control.cpp b/autonomy/interfacing/can/src/mit_motor_control.cpp new file mode 100644 index 0000000..58193f1 --- /dev/null +++ b/autonomy/interfacing/can/src/mit_motor_control.cpp @@ -0,0 +1,70 @@ +void CanNode::moveMIT(double velocity, double force, double position, int id) { + // Create CAN message for MIT (Massachusetts Institute of Technology) control mode + // This typically involves velocity, force, and position parameters + + autonomy::CanMessage can_msg; + can_msg.id = id; // MIT control command ID + can_msg.is_extended_id = true; + can_msg.is_remote_frame = false; + can_msg.dlc = 8; // 8 bytes for the control parameters + + // Pack the control parameters into the CAN message data + // Assuming IEEE 754 double precision format (8 bytes total) + // We'll use 2 bytes for velocity, 2 for force, 2 for position, 2 reserved + uint16_t vel_scaled = static_cast(velocity * 1000.0); // Scale for precision + uint16_t force_scaled = static_cast(force * 1000.0); + uint16_t pos_scaled = static_cast(position * 1000.0); + + can_msg.data[0] = (vel_scaled >> 8) & 0xFF; // Velocity high byte + can_msg.data[1] = vel_scaled & 0xFF; // Velocity low byte + can_msg.data[2] = (force_scaled >> 8) & 0xFF; // Force high byte + can_msg.data[3] = force_scaled & 0xFF; // Force low byte + can_msg.data[4] = (pos_scaled >> 8) & 0xFF; // Position high byte + can_msg.data[5] = pos_scaled & 0xFF; // Position low byte + can_msg.data[6] = 0x00; // Reserved + can_msg.data[7] = 0x00; // Reserved + + // Send the CAN message + if (can_.sendMessage(can_msg)) { + RCLCPP_INFO(this->get_logger(), "MIT control message sent - V:%.3f, F:%.3f, P:%.3f", + velocity, force, position); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to send MIT control message"); + } +} + +void CanNode::moveProfile(const std::string& profile_type, double max_acc) { + // Create CAN message for profile-based motion control + // Profile types could be: "trapezoidal", "s-curve", "polynomial", etc. + + autonomy::CanMessage can_msg; + can_msg.id = 0x201; // Profile control command ID + can_msg.is_extended_id = true; + can_msg.is_remote_frame = false; + can_msg.dlc = 8; // 8 bytes for profile parameters + + // Encode profile type (first 4 bytes) + std::hash hasher; + uint32_t profile_hash = static_cast(hasher(profile_type)); + + can_msg.data[0] = (profile_hash >> 24) & 0xFF; // Profile type hash high byte + can_msg.data[1] = (profile_hash >> 16) & 0xFF; // Profile type hash mid-high byte + can_msg.data[2] = (profile_hash >> 8) & 0xFF; // Profile type hash mid-low byte + can_msg.data[3] = profile_hash & 0xFF; // Profile type hash low byte + + // Encode maximum acceleration (last 4 bytes) + uint32_t acc_scaled = static_cast(max_acc * 1000.0); // Scale for precision + + can_msg.data[4] = (acc_scaled >> 24) & 0xFF; // Max acc high byte + can_msg.data[5] = (acc_scaled >> 16) & 0xFF; // Max acc mid-high byte + can_msg.data[6] = (acc_scaled >> 8) & 0xFF; // Max acc mid-low byte + can_msg.data[7] = acc_scaled & 0xFF; // Max acc low byte + + // Send the CAN message + if (can_.sendMessage(can_msg)) { + RCLCPP_INFO(this->get_logger(), "Profile control message sent - Type:%s, MaxAcc:%.3f", + profile_type.c_str(), max_acc); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to send profile control message"); + } +} \ No newline at end of file