diff --git a/.github/workflows/update-ghcr.yml b/.github/workflows/update-ghcr.yml index d884c2bc..6336f010 100644 --- a/.github/workflows/update-ghcr.yml +++ b/.github/workflows/update-ghcr.yml @@ -12,6 +12,7 @@ permissions: jobs: build-and-push: + if: ${{ github.repository == 'KTH-SML/svea' }} runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/docker/Dockerfile b/docker/Dockerfile index b8d092d8..f9942408 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,7 +20,7 @@ RUN apt-get update -y # This is for any added ROS packages COPY src ./src -COPY requirements.txt ./ +COPY entrypoint requirements.txt ./ RUN rosdep install \ --rosdistro $ROSDISTRO \ --from-paths src \ diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index ee4b0ed7..77c5238e 100755 --- a/docker/Dockerfile.base +++ b/docker/Dockerfile.base @@ -69,7 +69,7 @@ RUN mkdir -p /var/run/sshd # Bash reminder: # username="${creds%%:*}" # remove everything after the first ':' # password="${creds#*:}" # remove everything before the first ':' -RUN echo "root:${USER_CREDENTIALS#*:}" | chpasswd +RUN echo "${USER_CREDENTIALS%%:*}:${USER_CREDENTIALS#*:}" | chpasswd # SSH config: allow root + password login (keep pubkey auth on too) RUN sed -ri 's/^#?PasswordAuthentication .*/PasswordAuthentication yes/' /etc/ssh/sshd_config && \ @@ -99,6 +99,12 @@ RUN if [ -f src/mocap4ros2_qualisys/dependency_repos.repos ]; then \ vcs import src < src/mocap4ros2_qualisys/dependency_repos.repos; \ fi +# Zenoh +RUN echo "deb [trusted=yes] https://download.eclipse.org/zenoh/debian-repo/ /" | tee -a /etc/apt/sources.list > /dev/null && \ + apt update -y && \ + apt install --no-install-recommends -y zenoh-bridge-ros2dds && \ + rm -rf /var/lib/apt/lists/* + # Install dependencies via rosdep and pip RUN apt-get update -y && \ rosdep update \ @@ -141,4 +147,4 @@ RUN chmod +x ./entrypoint ENTRYPOINT ["./entrypoint"] -CMD ["bash"] +CMD ["bash"] \ No newline at end of file diff --git a/entrypoint b/entrypoint index 6a236f23..04255c87 100755 --- a/entrypoint +++ b/entrypoint @@ -24,6 +24,6 @@ if [ "$MICROROS_DISABLE_SHM" = "1" ] ; then fi # To build anything that has been added by volume... -colcon build --symlink-install +colcon build --symlink-install || true exec "$@" diff --git a/requirements.txt b/requirements.txt index 973c93a8..8cb58e80 100755 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,8 @@ # You can read more here: https://pip.pypa.io/en/stable/reference/requirements-file-format/#requirements-file-format numpy -matplotlib +matplotlib # deprecated +pillow # This is for MPC casadi==3.6.7 diff --git a/src/el2425/launch/traffic_light.xml b/src/el2425/launch/traffic_light.xml new file mode 100644 index 00000000..dcfd58fc --- /dev/null +++ b/src/el2425/launch/traffic_light.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/el2425/package.xml b/src/el2425/package.xml new file mode 100644 index 00000000..c4ef871b --- /dev/null +++ b/src/el2425/package.xml @@ -0,0 +1,16 @@ + + + + el2425 + 0.0.0 + The el2425 package + Kaj Munhoz Arfvidsson + TODO: License declaration + + svea_core + rclpy + + + ament_python + + diff --git a/src/el2425/resource/el2425 b/src/el2425/resource/el2425 new file mode 100644 index 00000000..e69de29b diff --git a/src/el2425/scripts/traffic_light.py b/src/el2425/scripts/traffic_light.py new file mode 100755 index 00000000..19f641fe --- /dev/null +++ b/src/el2425/scripts/traffic_light.py @@ -0,0 +1,107 @@ +#! /usr/bin/env python3 + +# imports +import random + +from rclpy.clock import Clock, Duration +from std_msgs.msg import String, Float32 + +from svea_core import rosonic as rx +from svea_core.interfaces import ShowMarker + +class traffic_light(rx.Node): + + ## Constants + + GREEN_TIME = 5. # [s] + YELLOW_TIME = 2. # [s] + RED_TIME = 5. # [s] + + # dictionary for states + TRANSITIONS = { + 'Rd': 'YG', # Red -> Yellow-Green + 'YG': 'Gr', # Yellow-Green -> Green + 'Gr': 'YR', # Green -> Yellow-Red + 'YR': 'Rd', # Yellow-Red -> Red + } + + ## Parameters + + rate = rx.Parameter(10) + alpha = rx.Parameter(1.0) + + pos_x = rx.Parameter(0.0) + pos_y = rx.Parameter(0.0) + + ## Publishers + + state_pub = rx.Publisher(String, '~/state') + time_pub = rx.Publisher(Float32, '~/time_left') + + ## Interfaces + + marker = ShowMarker() + + ## Methods + + def on_startup(self): + + assert 0 <= self.alpha <= min(self.RED_TIME, self.GREEN_TIME) + + self.switch('Rd') # initial state is red + self._loop_tmr = self.create_timer(1.0 / self.rate, self.loop) + + def loop(self): + + now = Clock().now() + + # calculate time remaining for current state + time_left = (self._time0 + self._delta - now).nanoseconds / 1e9 + + # if time has elapsed, then switch to next state + if time_left <= 0: + self.switch() + + # publish current state and time remaining + msg = String() + msg.data = self._state + self.state_pub.publish(msg) + + msg = Float32() + msg.data = time_left + self.time_pub.publish(msg) + + # visualization for the lights + self.visualize_traffic_lights() + + def switch(self, to=None): + """ + Change the traffic light to the next state + """ + self._state = (to if to is not None else + self.TRANSITIONS[self._state]) + + delta = (self.RED_TIME if self._state == 'Rd' else + self.GREEN_TIME if self._state == 'Gr' else + self.YELLOW_TIME) + if self._state in ('Rd', 'Gr'): + delta += random.uniform(-self.alpha, +self.alpha) + + # delta determines how long each state lasts + self._delta = Duration(seconds=delta) + + self._time0 = Clock().now() + + def visualize_traffic_lights(self): + """Publish the positions of traffic lights for visualization in RViz.""" + + color = ('red' if self._state == 'Rd' else + 'green' if self._state == 'Gr' else + 'yellow' if self._state in ('YG', 'YR') else + 'black') + + self.marker.place([self.pos_x, self.pos_y, 1.0], color=color) + +if __name__ == '__main__': + + traffic_light.main() diff --git a/src/el2425/setup.py b/src/el2425/setup.py new file mode 100644 index 00000000..a9fed363 --- /dev/null +++ b/src/el2425/setup.py @@ -0,0 +1,27 @@ +import os +from glob import glob +import xml.etree.ElementTree as ET +from setuptools import find_packages, setup + +package = ET.parse('package.xml').getroot() +name = package.find('name').text + +setup( + name=name, + version='0.0.0', + packages=find_packages(include=[name, f"{name}.*"]), + data_files=[ + ('share/ament_index/resource_index/packages', [f'resource/{name}']), + (f'share/{name}', ['package.xml']), + (f'share/{name}/launch', glob('launch/*.xml')), + (f'lib/{name}', glob('scripts/*.py')), + (os.path.join('share', name, 'launch'), glob(os.path.join('launch', '*launch.[pxy][yma]*'))), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer=package.find('maintainer').text, + maintainer_email=package.find('maintainer').get('email'), + description=package.find('description').text, + license=package.find('license').text, + tests_require=['pytest'], +) diff --git a/src/svea_core/launch/lli_test.xml b/src/svea_core/_launch/lli_test.xml similarity index 100% rename from src/svea_core/launch/lli_test.xml rename to src/svea_core/_launch/lli_test.xml diff --git a/src/svea_core/launch/plot_map.xml b/src/svea_core/_launch/plot_map.xml similarity index 100% rename from src/svea_core/launch/plot_map.xml rename to src/svea_core/_launch/plot_map.xml diff --git a/src/svea_core/launch/save_map.xml b/src/svea_core/_launch/save_map.xml similarity index 100% rename from src/svea_core/launch/save_map.xml rename to src/svea_core/_launch/save_map.xml diff --git a/src/svea_core/launch/state_publisher_launch.xml b/src/svea_core/_launch/state_publisher_launch.xml similarity index 100% rename from src/svea_core/launch/state_publisher_launch.xml rename to src/svea_core/_launch/state_publisher_launch.xml diff --git a/src/svea_core/launch/svea_vizualization.launch.py b/src/svea_core/_launch/svea_vizualization.launch.py similarity index 100% rename from src/svea_core/launch/svea_vizualization.launch.py rename to src/svea_core/_launch/svea_vizualization.launch.py diff --git a/src/svea_core/launch/map_and_foxglove.xml b/src/svea_core/launch/map_and_foxglove.xml index 1baeba3b..2f5e5ab4 100644 --- a/src/svea_core/launch/map_and_foxglove.xml +++ b/src/svea_core/launch/map_and_foxglove.xml @@ -1,19 +1,20 @@ - + + - + - + + - + - diff --git a/src/svea_core/launch/simulation.xml b/src/svea_core/launch/simulation.xml index 3b9d9649..0878f4b8 100644 --- a/src/svea_core/launch/simulation.xml +++ b/src/svea_core/launch/simulation.xml @@ -1,16 +1,38 @@ + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - - - - diff --git a/src/svea_core/launch/svea.xml b/src/svea_core/launch/svea.xml new file mode 100644 index 00000000..c9cf728c --- /dev/null +++ b/src/svea_core/launch/svea.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/svea_core/launch/svea_launch.xml b/src/svea_core/launch/svea_launch.xml deleted file mode 100644 index 84af3c14..00000000 --- a/src/svea_core/launch/svea_launch.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/svea_core/launch/visualization.xml b/src/svea_core/launch/visualization.xml new file mode 100644 index 00000000..d64f0261 --- /dev/null +++ b/src/svea_core/launch/visualization.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/svea_core/params/zenoh.json5 b/src/svea_core/params/zenoh.json5 new file mode 100644 index 00000000..71af5656 --- /dev/null +++ b/src/svea_core/params/zenoh.json5 @@ -0,0 +1,52 @@ +{ + plugins: { + ros2dds: { + + //// + //// allow / deny: Specify the lists of ROS 2 interfaces that are allowed or denied to be routed over Zenoh. + //// Each element of the lists is a regular expression that must match the full interface name. + //// You cannot set both 'allow' and 'deny' in the same configuration. + //// If neither 'allow' nor 'deny' are set, all interfaces are allowed. + //// Use 'allow' to allow only the specified interfaces. If an interface type is set to an empty list + //// or is not specified at all, it means that NO such interface is allowed. + //// Use 'deny' to allow all except the specified interfaces. If an interface type is set to an empty list + //// or is not specified at all, it means that ALL such interface are allowed. + allow: { + publishers: ["/fleet/.+"], + subscribers: ["/fleet/.+"], + service_servers: ["/fleet/.+"], + service_clients: ["/fleet/.+"], + action_servers: ["/fleet/.+"], + action_clients: ["/fleet/.+"], + }, + + "timestamp_check": { + "enabled": false + } + + }, + + }, + + //// + //// Which endpoints to connect to. E.g. tcp/localhost:7447. + //// By configuring the endpoints, it is possible to tell zenoh which remote router or other zenoh-bridge-ros2dds to connect to at startup. + //// + connect: { + endpoints: [ + "tcp/labs.itrl.kth.se:7447" + ] + }, + + //// + //// Configure the scouting mechanisms and their behaviours + //// + scouting: { + /// The UDP multicast scouting configuration. + multicast: { + /// Whether multicast scouting is enabled or not + enabled: false, + }, + }, + +} diff --git a/src/svea_core/scripts/encoder_filter.py b/src/svea_core/scripts/encoder_filter.py index 17763c05..2a71a704 100755 --- a/src/svea_core/scripts/encoder_filter.py +++ b/src/svea_core/scripts/encoder_filter.py @@ -32,8 +32,10 @@ class encoder_filter(rx.Node): override = None encoder_time = None + frame_id = rx.Parameter('wheel_encoder') + ## Publishers ## - encoder_re_pub = rx.Publisher(TwistWithCovarianceStamped, '/encoder/filtered', qos_pubber) + encoder_re_pub = rx.Publisher(TwistWithCovarianceStamped, '/lli/filtered/encoders', qos_pubber) ## Subscribers ## @rx.Subscriber(Bool, '/lli/remote/override', qos_subber) @@ -56,6 +58,7 @@ def ctrl_throttle_sub(self, ctrl_throttle_msg): @rx.Subscriber(TwistWithCovarianceStamped, '/lli/sensor/encoders', qos_subber) def encoder_sub(self, encoder_msg): + encoder_msg.header.frame_id = self.frame_id current_time = encoder_msg.header.stamp.sec + encoder_msg.header.stamp.nanosec * 1e-9 # 如果是第一次回调,直接记录时间,不计算dt diff --git a/src/svea_core/scripts/imu_bias_remover.py b/src/svea_core/scripts/imu_bias_remover.py index 036d638a..7bb09c37 100755 --- a/src/svea_core/scripts/imu_bias_remover.py +++ b/src/svea_core/scripts/imu_bias_remover.py @@ -30,8 +30,10 @@ class imu_bias_remove(rx.Node): sample_count = 100 sample_counter = 0 + frame_id = rx.Parameter('imu') + ## Publishers ## - imu_re_pub = rx.Publisher(Imu, '/imu/filtered', qos_pubber) + imu_re_pub = rx.Publisher(Imu, '/lli/filtered/imu', qos_pubber) ## Subscribers ## @rx.Subscriber(Imu, '/lli/sensor/imu', qos_subber) @@ -53,6 +55,7 @@ def imu_sub(self, imu_msg): self.get_logger().info(f"IMU bias sampled: angular_z: {self.bias_angular_z}, linear_x: {self.bias_linear_x}, linear_y: {self.bias_linear_y}") else: + imu_msg.header.frame_id = self.frame_id imu_msg.angular_velocity.z -= self.bias_angular_z imu_msg.angular_velocity.z = imu_msg.angular_velocity.z / 4.0 imu_msg.linear_acceleration.x -= self.bias_linear_x diff --git a/src/svea_core/scripts/lidar_timer.py b/src/svea_core/scripts/lidar_timer.py index 38f47642..f1e50930 100755 --- a/src/svea_core/scripts/lidar_timer.py +++ b/src/svea_core/scripts/lidar_timer.py @@ -1,9 +1,9 @@ #! /usr/bin/env python3 - from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy, QoSHistoryPolicy from sensor_msgs.msg import LaserScan -from rclpy.time import Duration - +from std_msgs.msg import Header +from rclpy.time import Duration, Time +from tf2_ros import Buffer, TransformListener, LookupException, ConnectivityException, ExtrapolationException from svea_core import rosonic as rx qos_pubber = QoSProfile( @@ -13,7 +13,6 @@ depth=1, ) - qos_subber = QoSProfile( reliability=QoSReliabilityPolicy.BEST_EFFORT, # BEST_EFFORT history=QoSHistoryPolicy.KEEP_LAST, # Keep the last N messages @@ -21,26 +20,72 @@ depth=10, # Size of the queue ) - class lidar_filter(rx.Node): """ Correcting Encoder Diraction """ + from_topic = rx.Parameter('scan') + to_topic = rx.Parameter('scan/filtered') + + target_frame = rx.Parameter('base_link') + source_frame = rx.Parameter('odom') + ## Publishers ## - encoder_re_pub = rx.Publisher(LaserScan, '/scan/filtered', qos_pubber) + encoder_re_pub = rx.Publisher(LaserScan, to_topic, qos_pubber) ## Subscribers ## - @rx.Subscriber(LaserScan, '/scan', qos_subber) + @rx.Subscriber(LaserScan, from_topic, qos_subber) def laser_callback(self, laser_msg): - now = self.get_clock().now() - adjusted_time = now - Duration(seconds=0.01) - laser_msg.header.stamp = adjusted_time.to_msg() + laser_msg.header.stamp = self.header.stamp self.encoder_re_pub.publish(laser_msg) def on_startup(self): - pass + self.tf_buffer = Buffer() + self.tf_listener = TransformListener(self.tf_buffer, self) +<<<<<<< HEAD + +======= +>>>>>>> el2425/main + # Use a timer to check periodically + self.timer = self.create_timer(0.03, self.get_latest_transform_header) + self.header = Header() + self.header.stamp = self.get_clock().now().to_msg() +<<<<<<< HEAD + +======= + +>>>>>>> el2425/main + def get_latest_transform_header(self): + try: + # "latest" means we don't specify a time + transform = self.tf_buffer.lookup_transform( +<<<<<<< HEAD + target_frame='base_link', # target + source_frame='map', # source + time=Time() # latest available + ) + +======= + target_frame=self.target_frame, # target + source_frame=self.source_frame, # source + time=Time() # latest available + ) +>>>>>>> el2425/main + self.header = transform.header + # self.get_logger().info( + # f"Latest transform header:\n" + # f" Frame ID: {self.header.frame_id}\n" + # f" Child Frame ID: {transform.child_frame_id}\n" + # f" Stamp: {self.header.stamp.sec}.{self.header.stamp.nanosec}" + # ) +<<<<<<< HEAD + +======= +>>>>>>> el2425/main + except (LookupException, ConnectivityException, ExtrapolationException) as e: + self.get_logger().warn(f"Transform not available: {e}") if __name__ == '__main__': - lidar_filter.main() + lidar_filter.main() \ No newline at end of file diff --git a/src/svea_core/scripts/model_viz.py b/src/svea_core/scripts/model_viz.py deleted file mode 100755 index 8501175d..00000000 --- a/src/svea_core/scripts/model_viz.py +++ /dev/null @@ -1,87 +0,0 @@ -#! /usr/bin/env python3 - -from math import sin, cos, pi -import rclpy -from rclpy.node import Node -from rclpy.qos import QoSProfile -from geometry_msgs.msg import Quaternion -from tf2_ros import TransformBroadcaster, TransformStamped - -class Publish3Dcar(Node): - - def __init__(self): - rclpy.init() - super().__init__('Publish3Dcar') - - qos_profile = QoSProfile(depth=10) - self.broadcaster = TransformBroadcaster(self, qos=qos_profile) - self.nodeName = self.get_name() - self.get_logger().info("{0} started".format(self.nodeName)) - - degree = pi / 180.0 - loop_rate = self.create_rate(30) - - # robot state - tilt = 0. - tinc = degree - swivel = 0. - angle = 0. - height = 0. - hinc = 0.005 - - # message declarations - namespace = self.get_namespace() - self.get_logger().info("Namespace: {0}".format(namespace)) - frame_id = namespace + '/odom' if namespace != '/' else 'odom' - child_id = namespace + '/axis' if namespace != '/' else 'axis' - odom_trans = TransformStamped() - odom_trans.header.frame_id = frame_id - odom_trans.child_frame_id = child_id - - try: - while rclpy.ok(): - rclpy.spin_once(self) - - # update joint_state - now = self.get_clock().now() - - # update transform - # (moving in a circle with radius=2) - odom_trans.header.stamp = now.to_msg() - odom_trans.transform.translation.x = cos(angle)*2 - odom_trans.transform.translation.y = sin(angle)*2 - odom_trans.transform.translation.z = 0.7 - odom_trans.transform.rotation = \ - euler_to_quaternion(0, 0, angle + pi/2) # roll,pitch,yaw - - # send the joint state and transform - self.broadcaster.sendTransform(odom_trans) - - # Create new robot state - tilt += tinc - if tilt < -0.5 or tilt > 0.0: - tinc *= -1 - height += hinc - if height > 0.2 or height < 0.0: - hinc *= -1 - swivel += degree - angle += degree/4 - - # This will adjust as needed per iteration - loop_rate.sleep() - - except KeyboardInterrupt: - pass - -def euler_to_quaternion(roll, pitch, yaw): - qx = sin(roll/2) * cos(pitch/2) * cos(yaw/2) - cos(roll/2) * sin(pitch/2) * sin(yaw/2) - qy = cos(roll/2) * sin(pitch/2) * cos(yaw/2) + sin(roll/2) * cos(pitch/2) * sin(yaw/2) - qz = cos(roll/2) * cos(pitch/2) * sin(yaw/2) - sin(roll/2) * sin(pitch/2) * cos(yaw/2) - qw = cos(roll/2) * cos(pitch/2) * cos(yaw/2) + sin(roll/2) * sin(pitch/2) * sin(yaw/2) - return Quaternion(x=qx, y=qy, z=qz, w=qw) - -def main(): - node = Publish3Dcar() - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/src/svea_core/scripts/sim_lidar.py b/src/svea_core/scripts/sim_lidar.py index 34c20c4b..aaeadf75 100755 --- a/src/svea_core/scripts/sim_lidar.py +++ b/src/svea_core/scripts/sim_lidar.py @@ -76,12 +76,13 @@ class sim_lidar(rx.Node): ## Parameters ## odometry_top = rx.Parameter('odometry/local') + laser_frame = rx.Parameter('laser') _viz_points_topic = 'viz_lidar_points' _viz_rays_topic = 'viz_lidar_rays' _viz_edges_topic = 'viz_edges' ## Publishers ## - _scan_pub = rx.Publisher(LaserScan, '/scan', qos_pubber) + _scan_pub = rx.Publisher(LaserScan, 'scan', qos_pubber) _viz_points_pub = rx.Publisher(PointCloud, _viz_points_topic, qos_pubber) _viz_rays_pub = rx.Publisher(Marker, _viz_rays_topic, qos_pubber) _viz_edges_pub = rx.Publisher(Marker, _viz_edges_topic, qos_pubber) @@ -129,7 +130,7 @@ def on_startup(self): self._scan_msg = LaserScan() self._scan_msg.header.stamp = rclpy.clock.Clock().now().to_msg() - self._scan_msg.header.frame_id = 'laser' + self._scan_msg.header.frame_id = self.laser_frame self._scan_msg.angle_min = self.ANGLE_MIN self._scan_msg.angle_max = self.ANGLE_MAX self._scan_msg.angle_increment = self.INCREMENT diff --git a/src/svea_core/scripts/sim_svea.py b/src/svea_core/scripts/sim_svea.py index ffd98aad..65daca82 100755 --- a/src/svea_core/scripts/sim_svea.py +++ b/src/svea_core/scripts/sim_svea.py @@ -5,7 +5,10 @@ subscriptions and publications that match the real car platform. Intended for debugging code BEFORE running on a real car. -Author: Frank Jiang +Contributors: +- Frank Jiang +- Kaj Munhoz Arfvidsson +- Li Chen """ import math @@ -30,7 +33,6 @@ depth=1, ) - qos_subber = QoSProfile( reliability=QoSReliabilityPolicy.RELIABLE, # Reliable history=QoSHistoryPolicy.KEEP_LAST, # Keep the last N messages @@ -68,20 +70,24 @@ class sim_svea(rx.Node): ## Parameters ## time_step = 0.025 - publish_tf = rx.Parameter(True) + initial_pose_x = rx.Parameter(0.0) + initial_pose_y = rx.Parameter(0.0) + initial_pose_a = rx.Parameter(0.0) + + odometry_top = rx.Parameter('odometry/local') + + # In simulation, we want lli to be namespaced steering_request_top = rx.Parameter('lli/ctrl/steering') throttle_request_top = rx.Parameter('lli/ctrl/throttle') highgear_request_top = rx.Parameter('lli/ctrl/highgear') diff_request_top = rx.Parameter('lli/ctrl/diff') - odometry_top = rx.Parameter('odometry/local') - map_frame = rx.Parameter('map') odom_frame = rx.Parameter('odom') - self_frame = rx.Parameter('base_link') - - state = rx.Parameter([0.0, 0.0, 0.0, 0.0]) # x, y, yaw, velocity + base_frame = rx.Parameter('base_link') + + publish_tf = rx.Parameter(True) ## Publishers ## odometry_pub = rx.Publisher(Odometry, odometry_top, qos_pubber) @@ -92,12 +98,12 @@ class sim_svea(rx.Node): def steering_request_cb(self, steering_request_msg): self.steering_req = steering_request_msg.data * -1 self.last_ctrl_time = self.clock.now().to_msg() - + @rx.Subscriber(Int8, throttle_request_top, qos_subber) def throttle_request_cb(self, throttle_request_msg): self.velocity_req = throttle_request_msg.data self.last_ctrl_time = self.clock.now().to_msg() - + @rx.Subscriber(Bool, highgear_request_top, qos_subber) def highgear_request_cb(self, highgear_request_msg): self.highgear = highgear_request_msg.data @@ -116,16 +122,17 @@ def on_startup(self): self.last_ctrl_time = self.clock.now().to_msg() self.last_pub_time = self.clock.now().to_msg() - self.model = Bicycle4DWithESC(initial_state=self.state) + self.model = Bicycle4DWithESC(initial_state=[ + self.initial_pose_x, + self.initial_pose_y, + self.initial_pose_a, + 0.0, + ]) self.steering_req = 0.0 self.velocity_req = 0.0 self.highgear = False self.diff = False - namespace = self.get_namespace() - self.odom_frame = namespace + '/' + self.odom_frame if namespace != '/' else self.odom_frame - self.self_frame = namespace + '/' + self.self_frame if namespace != '/' else self.self_frame - if self.publish_tf: # for broadcasting fake tf tree self.tf_br = tf2_ros.TransformBroadcaster(self) @@ -154,7 +161,7 @@ def sim_loop(self): odom_msg = Odometry() odom_msg.header.stamp = curr_time odom_msg.header.frame_id = self.map_frame - odom_msg.child_frame_id = self.self_frame + odom_msg.child_frame_id = self.base_frame odom_msg.header.stamp = curr_time odom_msg.pose.pose.position.x = x odom_msg.pose.pose.position.y = y @@ -202,7 +209,7 @@ def _broadcast_tf(self, odom_msg): odom2base = TransformStamped() odom2base.header.stamp = odom_msg.header.stamp odom2base.header.frame_id = self.odom_frame - odom2base.child_frame_id = self.self_frame + odom2base.child_frame_id = self.base_frame odom2base.transform.translation.x = odom_msg.pose.pose.position.x odom2base.transform.translation.y = odom_msg.pose.pose.position.y odom2base.transform.translation.z = odom_msg.pose.pose.position.z diff --git a/src/svea_core/setup.py b/src/svea_core/setup.py index 42caf9d8..e105ac96 100644 --- a/src/svea_core/setup.py +++ b/src/svea_core/setup.py @@ -15,7 +15,7 @@ (f'share/{name}', ['package.xml']), (f'share/{name}/launch', glob('launch/*.xml')), (os.path.join('share', name, 'launch'), glob(os.path.join('launch', '*launch.[pxy][yma]*'))), - (f'share/{name}/params', glob('params/*.yaml')), + (f'share/{name}/params', glob('params/*') + glob('params/**/*')), (f'share/{name}/maps', glob('maps/*')), (f'lib/{name}', glob('scripts/*.py')), (f'share/{name}/urdf', glob('urdf/*')), diff --git a/src/svea_core/svea_core/__pycache__/__init__.cpython-312.pyc b/src/svea_core/svea_core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..0935f54a Binary files /dev/null and b/src/svea_core/svea_core/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/svea_core/svea_core/controllers/__pycache__/__init__.cpython-312.pyc b/src/svea_core/svea_core/controllers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..25bc83df Binary files /dev/null and b/src/svea_core/svea_core/controllers/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/svea_core/svea_core/controllers/__pycache__/pure_pursuit.cpython-312.pyc b/src/svea_core/svea_core/controllers/__pycache__/pure_pursuit.cpython-312.pyc new file mode 100644 index 00000000..84d4f79a Binary files /dev/null and b/src/svea_core/svea_core/controllers/__pycache__/pure_pursuit.cpython-312.pyc differ diff --git a/src/svea_core/svea_core/interfaces/__init__.py b/src/svea_core/svea_core/interfaces/__init__.py index 208baab4..3e9351c5 100644 --- a/src/svea_core/svea_core/interfaces/__init__.py +++ b/src/svea_core/svea_core/interfaces/__init__.py @@ -1,2 +1,8 @@ +# Core Interfaces from .actuation import * from .localization import * + +# Utility Interfaces +from .path import * +from .marker import * + diff --git a/src/svea_core/svea_core/interfaces/__pycache__/__init__.cpython-312.pyc b/src/svea_core/svea_core/interfaces/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..85bdd284 Binary files /dev/null and b/src/svea_core/svea_core/interfaces/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/svea_core/svea_core/interfaces/__pycache__/actuation.cpython-312.pyc b/src/svea_core/svea_core/interfaces/__pycache__/actuation.cpython-312.pyc new file mode 100644 index 00000000..bf4787db Binary files /dev/null and b/src/svea_core/svea_core/interfaces/__pycache__/actuation.cpython-312.pyc differ diff --git a/src/svea_core/svea_core/interfaces/__pycache__/localization.cpython-312.pyc b/src/svea_core/svea_core/interfaces/__pycache__/localization.cpython-312.pyc new file mode 100644 index 00000000..f3f9d24a Binary files /dev/null and b/src/svea_core/svea_core/interfaces/__pycache__/localization.cpython-312.pyc differ diff --git a/src/svea_core/svea_core/interfaces/actuation.py b/src/svea_core/svea_core/interfaces/actuation.py index bdb5936f..d2846a94 100644 --- a/src/svea_core/svea_core/interfaces/actuation.py +++ b/src/svea_core/svea_core/interfaces/actuation.py @@ -58,10 +58,17 @@ class ActuationInterface(rx.Field): is_sim = rx.Parameter(False) - steering_pub = rx.Publisher(Int8, 'lli/ctrl/steering', qos_profile=QoS_DEFAULT) - throttle_pub = rx.Publisher(Int8, 'lli/ctrl/throttle', qos_profile=QoS_DEFAULT) - highgear_pub = rx.Publisher(Bool, 'lli/ctrl/highgear', qos_profile=QoS_RELIABLE) - diff_pub = rx.Publisher(Bool, 'lli/ctrl/diff', qos_profile=QoS_RELIABLE) + steering_top = rx.Parameter('/lli/ctrl/steering') + steering_pub = rx.Publisher(Int8, steering_top, qos_profile=QoS_DEFAULT) + + throttle_top = rx.Parameter('/lli/ctrl/throttle') + throttle_pub = rx.Publisher(Int8, throttle_top, qos_profile=QoS_DEFAULT) + + highgear_top = rx.Parameter('/lli/ctrl/highgear') + highgear_pub = rx.Publisher(Bool, highgear_top, qos_profile=QoS_RELIABLE) + + diff_top = rx.Parameter('/lli/ctrl/diff') + diff_pub = rx.Publisher(Bool, diff_top, qos_profile=QoS_RELIABLE) def __init__(self, rate=20, use_acceleration=False, highgear=False, diff=False): self.acceleration = use_acceleration diff --git a/src/svea_core/svea_core/interfaces/localization.py b/src/svea_core/svea_core/interfaces/localization.py index 71ddd8a5..7067a785 100644 --- a/src/svea_core/svea_core/interfaces/localization.py +++ b/src/svea_core/svea_core/interfaces/localization.py @@ -36,10 +36,13 @@ class LocalizationInterface(rx.Field): as state information is available. """ - class _InterfaceParameters(rx.NamedField): - odom_top = rx.Parameter('odometry/local') + localization = rx.namespace( + map_frame = rx.Parameter('map'), + odom_frame = rx.Parameter('self/odom'), + base_frame = rx.Parameter('self/base_link'), + odom_top = rx.Parameter('odometry/local'), + ) - _params = _InterfaceParameters(name='localization') _odom_msg = Odometry() def __init__(self) -> None: @@ -57,7 +60,7 @@ def on_startup(self): self.node.get_logger().info("Localization interface is ready.") return self._is_started - @rx.Subscriber(Odometry, _params.odom_top, qos_profile=qos_profile) + @rx.Subscriber(Odometry, localization.odom_top, qos_profile=qos_profile) def _odom_cb(self, msg: Odometry) -> None: if not self._is_started(): return @@ -72,8 +75,8 @@ def _odom_cb(self, msg: Odometry) -> None: def transform_odom( self, odom: Odometry, - pose_target: str = "map", - twist_target: str = "base_link", + pose_target: str | None = None, + twist_target: str | None = None, timeout_s: float = 0.2, ) -> Odometry: """ @@ -88,6 +91,11 @@ def transform_odom( assert self._is_started(), 'Localization interface not started yet' + if pose_target is None: + pose_target = self.localization.map_frame + if twist_target is None: + twist_target = self.localization.base_frame + # ---- Source frames from the incoming message ---- pose_source = odom.header.frame_id or "" twist_source = odom.child_frame_id or pose_source or "" diff --git a/src/svea_core/svea_core/interfaces/marker.py b/src/svea_core/svea_core/interfaces/marker.py new file mode 100644 index 00000000..b5a330cc --- /dev/null +++ b/src/svea_core/svea_core/interfaces/marker.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +from typing import Sequence + +import tf_transformations as tf +from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy, QoSHistoryPolicy +from visualization_msgs.msg import Marker +from PIL import ImageColor + +from .. import rosonic as rx + +qos_pubber = QoSProfile( + reliability=QoSReliabilityPolicy.RELIABLE, + durability=QoSDurabilityPolicy.VOLATILE, + history=QoSHistoryPolicy.KEEP_LAST, + depth=1, +) + +class ShowMarker(rx.NamedField): + + # {parent} will expand to name of interface + _pub = rx.Publisher(Marker, '~/{parent}', qos_profile=qos_pubber) + + def place( + self, + position: Sequence[float] | None = None, + orientation: Sequence[float] | None = None, + frame_id: str = "map", + color: str | Sequence[int] = 'white', + shape: int = Marker.SPHERE, + scale: float | Sequence[float] = 0.5, + **kwds, + ) -> None: + + if position is None: + position = [0.0, 0.0, 0.0] + elif len(position) == 2: + position = [position[0], position[1], 0.0] + elif len(position) != 3: + raise ValueError("Position must be a sequence of three floats.") + + if orientation is None: + orientation = [0.0, 0.0, 0.0, 1.0] + elif len(orientation) == 3: + x, y, z = tf.quaternion_from_euler(orientation[0], orientation[1], orientation[2]) + orientation = [x, y, z, 1.0] + elif len(orientation) != 4: + raise ValueError("Orientation must be a sequence of three (Euler) or four (quaternion) floats.") + + if isinstance(scale, (int, float)): + scale = [scale] * 3 + elif len(scale) != 3: + raise ValueError("Scale must be a float or a sequence of three floats.") + + r, g, b, a = self.parse_color(color) + + self.node.get_logger().debug(f"Placing marker at {position}") + marker = Marker() + marker.header.frame_id = frame_id + marker.header.stamp = self.node.get_clock().now().to_msg() + marker.type = shape + marker.action = Marker.ADD + marker.pose.position.x = position[0] + marker.pose.position.y = position[1] + marker.pose.position.z = position[2] if len(position) > 2 else 0.0 + marker.pose.orientation.x = orientation[0] + marker.pose.orientation.y = orientation[1] + marker.pose.orientation.z = orientation[2] + marker.pose.orientation.w = orientation[3] + marker.scale.x = scale[0] + marker.scale.y = scale[1] + marker.scale.z = scale[2] + marker.color.r = r + marker.color.g = g + marker.color.b = b + marker.color.a = a + + self._pub.publish(marker) + + @staticmethod + def parse_color(input_color, default_alpha=1.0): + """ + Parse color input and return in RGBA format. + + Args: + input_color: Input color, can be: + - Color name string (e.g., 'red', 'blue') + - Hex string (e.g., '#FF0000', '#FF0000FF') + - RGB/RGBA tuple (e.g., (255, 0, 0) or (1.0, 0.0, 0.0, 0.5)) + default_alpha: Default alpha value when input color has no alpha channel (0.0-1.0) + + Returns: + RGBA tuple with all components in 0.0–1.0 range. + """ + + # If already a tuple or list + if isinstance(input_color, (tuple, list)) and len(input_color) in (3, 4): + if all(isinstance(c, int) and 0 <= c <= 255 for c in input_color[:3]): + r, g, b = [c / 255.0 for c in input_color[:3]] + a = input_color[3] / 255.0 if len(input_color) == 4 else default_alpha + return r, g, b, a + elif all(isinstance(c, float) and 0.0 <= c <= 1.0 for c in input_color[:3]): + r, g, b = input_color[:3] + a = input_color[3] if len(input_color) == 4 else default_alpha + return r, g, b, a + + # If string input (color name or hex) + if isinstance(input_color, str): + try: + # Pillow returns RGB or RGBA as integers 0–255 + rgb = ImageColor.getrgb(input_color) + if len(rgb) == 3: + r, g, b = [c / 255.0 for c in rgb] + return r, g, b, default_alpha + elif len(rgb) == 4: + r, g, b, a = [c / 255.0 for c in rgb] + return r, g, b, a + except ValueError: + pass # unrecognized string + + # Fallback — return white + return 1.0, 1.0, 1.0, 1.0 diff --git a/src/svea_core/svea_core/interfaces/path.py b/src/svea_core/svea_core/interfaces/path.py new file mode 100644 index 00000000..9c4ae100 --- /dev/null +++ b/src/svea_core/svea_core/interfaces/path.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python + +from collections import deque + +from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy, QoSHistoryPolicy +from rclpy.time import Time +from rclpy.clock import Clock +from nav_msgs.msg import Path, Odometry +from geometry_msgs.msg import PoseStamped +import tf_transformations as tf + +from .. import rosonic as rx + +qos_subber = QoSProfile( + reliability=QoSReliabilityPolicy.RELIABLE, # Reliable + durability=QoSDurabilityPolicy.VOLATILE, # Volatile + history=QoSHistoryPolicy.KEEP_LAST, # Keep the last N messages + depth=10, # Size of the queue +) + +qos_pubber = QoSProfile( + reliability=QoSReliabilityPolicy.RELIABLE, + durability=QoSDurabilityPolicy.VOLATILE, + history=QoSHistoryPolicy.KEEP_LAST, + depth=1, +) + +class ShowPath(rx.Field): + """ + TODO: + - Build more general interface for navigation (with nav2 topics?) + """ + + PATH_MAX_LEN = 50 + + odometry_top = rx.Parameter('odometry/local') + + past_path_pub = rx.Publisher(Path, 'past_path', qos_pubber) + path_plan_pub = rx.Publisher(Path, 'path_plan', qos_pubber) + + @rx.Subscriber(Odometry, odometry_top, qos_subber) + def odometry_callback(self, msg): + self.past_path.append(self._odom_to_pose(msg)) + path = Path() + path.header = msg.header + path.poses = self.past_path + self.past_path_pub.publish(path) + + def __init__(self): + + self.past_path = deque(maxlen=self.PATH_MAX_LEN) + + def publish_path(self, x_list, y_list, yaw_list=None, t_list=None): + """Publish trajectory visualization + + Args: + x_list: x trajectory in [m] + y_list: y trajecotory in [m] + yaw_list: yaw trajectory in [rad], defaults to None + t_list: time trajectory in [s], defaults to None + """ + + path = Path() + path.header.stamp = Clock().now().to_msg() + path.header.frame_id = 'map' + + poses = [] + for i, (x, y) in enumerate(zip(x_list, y_list)): + + curr_pose = PoseStamped() + curr_pose.header.frame_id = path.header.frame_id + curr_pose.pose.position.x = x + curr_pose.pose.position.y = y + + if yaw_list is not None: + yaw = yaw_list[i] + quat = tf.transformations.quaternion_from_euler(0.0, 0.0, yaw) + curr_pose.pose.orientation.x = quat[0] + curr_pose.pose.orientation.y = quat[1] + curr_pose.pose.orientation.z = quat[2] + curr_pose.pose.orientation.w = quat[3] + + if t_list is not None: + t = t_list[i] + curr_pose.header.stamp = Time(secs = t).to_msg() + else: + curr_pose.header.stamp = path.header.stamp + + poses.append(curr_pose) + + path.poses = poses + self.path_plan_pub.publish(path) + + def _odom_to_pose(self, msg): + """Odometry -> PoseStamped""" + pose = PoseStamped() + pose.header = msg.header + pose.pose = msg.pose.pose + return pose + diff --git a/src/svea_core/svea_core/models/__pycache__/__init__.cpython-312.pyc b/src/svea_core/svea_core/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..6be19764 Binary files /dev/null and b/src/svea_core/svea_core/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/svea_core/svea_core/models/__pycache__/bicycle.cpython-312.pyc b/src/svea_core/svea_core/models/__pycache__/bicycle.cpython-312.pyc new file mode 100644 index 00000000..6da4c8a4 Binary files /dev/null and b/src/svea_core/svea_core/models/__pycache__/bicycle.cpython-312.pyc differ diff --git a/src/svea_core/svea_core/rosonic.py b/src/svea_core/svea_core/rosonic.py index 26b8a02a..531ac2d6 100644 --- a/src/svea_core/svea_core/rosonic.py +++ b/src/svea_core/svea_core/rosonic.py @@ -236,7 +236,9 @@ def __rosonic_register__(self, owner: 'Resource', *, name: str | None = None): Args: owner (Resource): The parent resource that owns this one. - name (str | None): Optional name override for this resource. + name (str | Callable[[], str] | None): + Optional name override for this resource. Can be lazy-evaluated + via a callable returning a string. ns (str | None): Optional namespace override for this resource. Raises: @@ -252,11 +254,13 @@ def __rosonic_register__(self, owner: 'Resource', *, name: str | None = None): if owner is self: assert self.__rosonic_name__ is not None, f"Root resource '{self}' must have a name" + assert not callable(self.__rosonic_name__), f"Root resource '{self}' name cannot be lazy-evaluated" else: assert owner._is_registered(), f"Resource '{owner}' owning '{self}' is not registered" # The following check only makes sense for non-root, named resources - if self.__rosonic_name__ is not None: + # NOTE: Does this make sense at all? Some resources may want the same name, e.g. pub/sub pairs. + if self.__rosonic_name__ is not None and not callable(self.__rosonic_name__): fullname = f"{owner.__rosonic_fullname__}/{self.__rosonic_name__}" assert fullname not in self.__rosonic_lookup__, \ f"Resource '{fullname}' already registered" @@ -286,6 +290,9 @@ def __rosonic_fullname__(self) -> str: name = self.__rosonic_name__ owner = self.__rosonic_owner__ + if callable(name): + name = '?' + if name is None: return owner.__rosonic_fullname__ elif self._is_root() or self._is_absolute_name(name): @@ -312,6 +319,9 @@ def __rosonic_relname__(self) -> str: name = self.__rosonic_name__ owner = self.__rosonic_owner__ + if callable(name): + name = '?' + if name is None: return owner.__rosonic_relname__ elif self._is_root(): @@ -362,6 +372,16 @@ def __rosonic_startup__(self, node: NodeBase) -> None: self.__rosonic_node__ = node + # resolve lazy name if any + if callable(self.__rosonic_name__): + self.__rosonic_name__ = self.__rosonic_name__() + + if isinstance(self.__rosonic_name__, str): + self.__rosonic_name__ = self.__rosonic_name__.format( + parent=self.__rosonic_owner__.__rosonic_relname__, + parent_fullname=self.__rosonic_owner__.__rosonic_fullname__, + ) + self.on_startup() self.__rosonic_started__ = True @@ -430,6 +450,23 @@ def _get_root(self) -> Resource: return (self if self._is_root() else self._get_root()) + def start(self, node: Node): + """ + Starts this resource manually. + + This method can be used to start a resource outside of the normal node + startup sequence. It invokes `__rosonic_startup__()` with the provided + node. + + Args: + node (Node): The ROS 2 node instance to associate with this + resource. + + Raises: + AssertionError: If the resource is already started. + """ + assert not self._is_started(), f"Resource '{self}' is already started" + self.__rosonic_startup__(node) # Only for typing class _RegisteredResource(Resource): @@ -443,6 +480,8 @@ class _StartedResource(_RegisteredResource): __rosonic_node__: NodeBase +### Base Resource Types + class Node(Resource, NodeBase): """ Base class for all SVEA nodes. @@ -587,6 +626,14 @@ def __set_name__(self, owner: type, name: str) -> None: owner.__rosonic_preregistered__ += (self,) + +def namespace(name=None, /, **resources): + ns = type('Namespace', (NamedField,), resources) + return ns(name=name) + + +### Resource Types + class Parameter(NamedField): """ Declarative Parameter resource. @@ -611,6 +658,7 @@ class MyNode(rx.Node): ### Notes: - The parameter is not resolved (no value) until node startup is complete. - Until then, accessing the field will return the Parameter object itself. + - TODO: Dynamically updatable parameters? """ def __init__(self, *args, name: str | None = None): @@ -640,8 +688,10 @@ def on_startup(self): node = self.__rosonic_node__ name = self.__rosonic_relname__ - if self.value is ...: + if not node.has_parameter(name): node.declare_parameter(name, *self.args) + + if self.value is ...: self.value = node.get_parameter(name).value class Publisher(NamedField): @@ -691,13 +741,20 @@ def __init__(self, msg_type, topic=None, qos_profile=None): Args: msg_type (type): The message type (e.g., `std_msgs.msg.String`). - topic (str | Parameter): Topic name as string or Parameter - reference. + topic (None | str | Parameter): Topic name as string or Parameter + reference, None to use the default resource name. qos_profile: Optional QoSProfile for the publisher. """ - super().__init__(name=topic if topic is str else None) + + # If topic is Parameter, we cannot assign the topic name as a rosonic resource name. + # In that case, we leave it '?' and let the publisher lazily evaluate the name. + def lazy_name(): + assert isinstance(topic, Parameter), "lazy_name should only be used with Parameter topics" + assert topic._is_started(), f"Resource '{self}' depend on '{topic}' which has not started yet" + return topic.value + + super().__init__(name=lazy_name if isinstance(topic, Parameter) else topic) self.msg_type = msg_type - self.topic = topic self.qos_profile = qos_profile self.publisher = None @@ -719,14 +776,9 @@ def on_startup(self): """ node = self.__rosonic_node__ msg_type = self.msg_type - topic = (self.topic if self.topic is not None else - self.__rosonic_fullname__) + topic = self.__rosonic_relname__ qos_profile = self.qos_profile or rclpy.qos.qos_profile_default - if isinstance(topic, Parameter): - assert topic._is_started(), f"Resource '{self}' depend on '{topic}' which has not started yet" - topic = topic.value - if not isinstance(msg_type, type): raise RuntimeError(f"Message type must be a class, not {type(msg_type)}") if not isinstance(topic, str): @@ -779,15 +831,22 @@ def __init__(self, msg_type, topic=None, qos_profile=None): Args: msg_type (type): The message type (e.g., `std_msgs.msg.String`). - name (str | Parameter): Topic name as string or Parameter - reference. + name (None | str | Parameter): + Topic name as string or Parameter reference. None to use the default + resource name. qos_profile: Optional QoSProfile for the subscriber. """ - super().__init__(name=topic if topic is str else None) + # If topic is Parameter, we cannot assign the topic name as a rosonic resource name. + # In that case, we leave it '?' and let the publisher lazily evaluate the name. + def lazy_name(): + assert isinstance(topic, Parameter), "lazy_name should only be used with Parameter topics" + assert topic._is_started(), f"Resource '{self}' depend on '{topic}' which has not started yet" + return topic.value + + super().__init__(name=lazy_name if isinstance(topic, Parameter) else topic) self.subscriber = None self.msg_type = msg_type - self.topic = topic self.qos_profile = qos_profile self.subscriber = None self.callback = None @@ -812,14 +871,9 @@ def on_startup(self): node = self.__rosonic_node__ msg_type = self.msg_type - topic = (self.topic if self.topic is not None else - self.__rosonic_fullname__) + topic = self.__rosonic_relname__ qos_profile = self.qos_profile or rclpy.qos.qos_profile_default - if isinstance(topic, Parameter): - assert topic._is_started(), f"Resource '{self}' depend on '{topic}' which has not started yet" - topic = topic.value - if not isinstance(msg_type, type): raise RuntimeError(f"Message type must be a class, not {type(msg_type)}") if not isinstance(topic, str): @@ -915,7 +969,7 @@ def on_startup(self): owner = self.__rosonic_owner__ period = self.period if isinstance(period, Parameter): - assert _is_started(period), f"Resource '{self}' depend on '{period}' which has not started yet" + assert self._is_started(period), f"Resource '{self}' depend on '{period}' which has not started yet" period = period.value wrapped_callback = lambda: self.callback(owner) self.tmr = node.create_timer(period, wrapped_callback, **self.kwds) diff --git a/src/svea_core/svea_core/utils/__init__.py b/src/svea_core/svea_core/utils/__init__.py index b0c88f49..e69de29b 100644 --- a/src/svea_core/svea_core/utils/__init__.py +++ b/src/svea_core/svea_core/utils/__init__.py @@ -1,2 +0,0 @@ -from .markers import * -from .path import * \ No newline at end of file diff --git a/src/svea_core/svea_core/utils/markers.py b/src/svea_core/svea_core/utils/markers.py deleted file mode 100755 index c3f906b8..00000000 --- a/src/svea_core/svea_core/utils/markers.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python -from .. import rosonic as rx -from matplotlib.colors import to_rgba, is_color_like -from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy, QoSHistoryPolicy -from visualization_msgs.msg import Marker -from rclpy.clock import Clock -import tf_transformations as tf -from builtin_interfaces.msg import Duration - -qos_pubber = QoSProfile( - reliability=QoSReliabilityPolicy.RELIABLE, - durability=QoSDurabilityPolicy.VOLATILE, - history=QoSHistoryPolicy.KEEP_LAST, - depth=1, -) - - -class PlaceMarker(rx.Field): - def __init__(self, **kwds) -> None: - if namespace := kwds.get('name_space', None): - self.ns = kwds.get('name_space', None) - else: - self.ns = None - - def on_startup(self): - return self - - def marker(self, name:str, color, position, orientation = [0.0, 0.0, 0.0, 0.0], shape = Marker.SPHERE, **kwds): - - topic_name = '/marker/' + name - mark_pub = self.node.create_publisher(Marker, topic_name, qos_pubber) - - marker = Marker() - - marker.header.frame_id = "map" - marker.header.stamp = self.node.get_clock().now().to_msg() - - marker.type = shape - marker.action = Marker.ADD - - # Position for mark - marker.pose.position.x = position[0] - marker.pose.position.y = position[1] - if len(position) > 2: - marker.pose.position.z = position[2] - else: - marker.pose.position.z = 0.0 - - #Orientation for mark - if len(orientation) == 1: - if isinstance(orientation, list): - orientation = orientation[0] - x, y, z, w = tf.quaternion_from_euler(ak = orientation) - elif len(orientation) == 3: - x, y, z, w = tf.quaternion_from_euler(orientation[0], orientation[1], orientation[2]) - elif len(orientation) == 4: - x = orientation[0] - y = orientation[1] - z = orientation[2] - w = orientation[3] - marker.pose.orientation.x = x - marker.pose.orientation.y = y - marker.pose.orientation.z = z - marker.pose.orientation.w = w - - # size - marker.scale.x = 0.5 - marker.scale.y = 0.5 - marker.scale.z = 0.5 - - # Color for mark - r, g, b, a = parse_color(color) - marker.color.r = r - marker.color.g = g - marker.color.b = b - marker.color.a = a - - # 发布 Marker - mark_pub.publish(marker) - - - def traffic_light_marker(self, number=0, position=[0.0,0.0], light_status='Rd'): - name = f"traffic_{number}" - position = list(position) - position.append(0.5) - position = tuple(position) - if light_status == 'Rd': - self.marker(name, "#FF0000", position) - elif light_status == 'Gr': - self.marker(name, (0.0, 1.0, 0.0), position) - else: - self.marker(name, 'yellow', position) - - - - -def parse_color(input_color, default_alpha=1.0): - """ - Parse color input and return in RGBA format - - Args: - input_color: Input color, can be: - - Color name string (e.g., 'red', 'blue') - - Hex string (e.g., '#FF0000', '#FF0000FF') - - RGB/RGBA tuple (e.g., (255, 0, 0) or (1.0, 0.0, 0.0, 0.5)) - default_alpha: Default alpha value when input color has no alpha channel (0.0-1.0) - - Returns: - RGBA tuple with all components in 0.0-1.0 range - """ - - # If input is already in RGBA format (tuple/list with 3 or 4 values), return directly - if isinstance(input_color, (tuple, list)) and len(input_color) in (3, 4): - # Check if components are integers in 0-255 range - if all(isinstance(c, int) and 0 <= c <= 255 for c in input_color[:3]): - if len(input_color) == 3: - return input_color[0]/255.0, input_color[1]/255.0, input_color[2]/255.0, default_alpha - else: - return input_color[0]/255.0, input_color[1]/255.0, input_color[2]/255.0, input_color[3]/255.0 - # Check if components are floats in 0.0-1.0 range - elif all(isinstance(c, float) and 0.0 <= c <= 1.0 for c in input_color[:3]): - if len(input_color) == 3: - return input_color[0], input_color[1], input_color[2], default_alpha - else: - return input_color[0], input_color[1], input_color[2], input_color[3] - - # Check if input is a valid color string - if isinstance(input_color, str): - if input_color.startswith('#'): - # Process hex color - hex_color = input_color.lstrip('#') - length = len(hex_color) - - if length == 3: # e.g., #RGB - r = int(hex_color[0]*2, 16) / 255.0 - g = int(hex_color[1]*2, 16) / 255.0 - b = int(hex_color[2]*2, 16) / 255.0 - return r, g, b, default_alpha - elif length == 6: # e.g., #RRGGBB - r = int(hex_color[0:2], 16) / 255.0 - g = int(hex_color[2:4], 16) / 255.0 - b = int(hex_color[4:6], 16) / 255.0 - return r, g, b, default_alpha - elif length == 8: # e.g., #RRGGBBAA - r = int(hex_color[0:2], 16) / 255.0 - g = int(hex_color[2:4], 16) / 255.0 - b = int(hex_color[4:6], 16) / 255.0 - a = int(hex_color[6:8], 16) / 255.0 - return r, g, b, a - - # Use matplotlib's color name conversion - if is_color_like(input_color): - color = to_rgba(input_color) - return color[0], color[1], color[2], color[3] - - # Raise exception if format is unrecognized - return 1.0, 1.0, 1.0, 1.0 diff --git a/src/svea_core/svea_core/utils/path.py b/src/svea_core/svea_core/utils/path.py deleted file mode 100644 index 33b45cee..00000000 --- a/src/svea_core/svea_core/utils/path.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python -from .. import rosonic as rx -from matplotlib.colors import to_rgba, is_color_like -from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy, QoSHistoryPolicy -from geometry_msgs.msg import Point, Point32, PolygonStamped, PointStamped -from rclpy.clock import Clock -import tf_transformations as tf -from nav_msgs.msg import Path, Odometry -from geometry_msgs.msg import Pose, PoseStamped, PoseArray -from rclpy.time import Time -from collections import deque - -qos_subber = QoSProfile( - reliability=QoSReliabilityPolicy.RELIABLE, # Reliable - history=QoSHistoryPolicy.KEEP_LAST, # Keep the last N messages - durability=QoSDurabilityPolicy.VOLATILE, # Volatile - depth=10, # Size of the queue -) - -qos_pubber = QoSProfile( - reliability=QoSReliabilityPolicy.RELIABLE, - durability=QoSDurabilityPolicy.VOLATILE, - history=QoSHistoryPolicy.KEEP_LAST, - depth=1, -) - -class ShowPath(rx.Field): - - TRAVEL_DIST_THRESH = 0.1 - PATH_MAX_LEN = 100 - chassis_height=0.06 - - odometry_top = rx.Parameter('odometry/local') - - past_path_pub = rx.Publisher(Path,'past_path',qos_pubber) - path_pub = rx.Publisher(Path, 'path_plan',qos_pubber) - - @rx.Subscriber(Odometry, odometry_top, qos_subber) - def odometry_callback(self, msg): - new_path = self.odom_to_pose_stampeds(msg) - self.past_path += new_path - path = Path() - path.header = msg.header - path.poses = self.past_path - self.past_path_pub.publish(path) - - def __ini__(self): - - self.past_path = deque(maxlen=self.PATH_MAX_LEN) - - def on_startup(self): - - self.traj_x = [] - self.traj_y = [] - self.x = [] - self.y = [] - self.past_path= [] - - def odom_to_pose_stampeds(self, msg): - poses = [] - - curr_pose = PoseStamped() - curr_pose.header = msg.header - curr_pose.pose = msg.pose.pose - poses.append(curr_pose) - return poses - - def lists_to_pose_stampeds(self, x_list, y_list, yaw_list=None, t_list=None): - poses = [] - for i in range(len(x_list)): - x = x_list[i] - y = y_list[i] - - curr_pose = PoseStamped() - curr_pose.header.frame_id = 'map' - curr_pose.pose.position.x = x - curr_pose.pose.position.y = y - - if not yaw_list is None: - yaw = yaw_list[i] - quat = tf.transformations.quaternion_from_euler(0.0, 0.0, yaw) - curr_pose.pose.orientation.x = quat[0] - curr_pose.pose.orientation.y = quat[1] - curr_pose.pose.orientation.z = quat[2] - curr_pose.pose.orientation.w = quat[3] - - if not t_list is None: - t = t_list[i] - curr_pose.header.stamp = Time(secs = t).to_msg() - else: - curr_pose.header.stamp = Clock().now().to_msg() - - poses.append(curr_pose) - return poses - - def lists_to_poses(self, x_list, y_list, yaw_list=None): - poses = [] - for i in range(len(x_list)): - x = x_list[i] - y = y_list[i] - - curr_pose = Pose() - curr_pose.position.x = x - curr_pose.position.y = y - - if not yaw_list is None: - yaw = yaw_list[i] - quat = tf.quaternion_from_euler(0.0, 0.0, yaw) - curr_pose.orientation.x = quat[0] - curr_pose.orientation.y = quat[1] - curr_pose.orientation.z = quat[2] - curr_pose.orientation.w = quat[3] - - poses.append(curr_pose) - return poses - - - def publish_path(self, x_list, y_list, yaw_list=None, t_list=None): - """Publish trajectory visualization to rviz - *t_list untested - - :param x_list: x trajectory in [m] - :type x_list: list - :param y_list: y trajecotory in [m] - :type y_list: list - :param yaw_list: yaw trajectory in [rad], defaults to None - :type yaw_list: list - :param t_list: time trajectory in [s], defaults to None - :type t_list: list - """ - - path = Path() - path.header.stamp = Clock().now().to_msg() - path.header.frame_id = 'map' - - path.poses = self.lists_to_pose_stampeds(x_list, y_list, yaw_list, t_list) - self.path_pub.publish(path) - - def publish_target(self, target_publisher, x, y): - """ - Publish target point for visualization in rviz - - :param target_publisher: ROS publisher to broadcast target with - :type target_publisher: rospy.topics.Publisher - """ - target_pt = PointStamped() - target_pt.header.stamp = Clock().now().to_msg() - target_pt.header.frame_id = 'map' - target_pt.point.x = x - target_pt.point.y = y - target_pt.point.z = self.chassis_height - target_publisher.publish(target_pt) - - def publish_pose_array(self, poses_publisher, x_list, y_list, yaw_list): - """Publish list of poses for visualization to rviz - - :param poses_publisher: ROS publisher to broadcast trajectory with - :type poses_publisher: rospy.topics.Publisher - :param x_list: x coordinates in [m] - :type x_list: list - :param y_list: y coordinates in [m] - :type y_list: list - :param yaw_list: yaw coordinates in [rad] - :type yaw_list: list - """ - pose_array = PoseArray() - pose_array.header.stamp = Clock().now().to_msg() - pose_array.header.frame_id = 'map' - pose_array.poses = self.lists_to_poses(x_list, y_list, yaw_list) - poses_publisher.publish(pose_array) \ No newline at end of file diff --git a/src/svea_examples/launch/floor2.xml b/src/svea_examples/launch/floor2.xml index a669ec1b..a9053b27 100644 --- a/src/svea_examples/launch/floor2.xml +++ b/src/svea_examples/launch/floor2.xml @@ -1,38 +1,102 @@ - + - - - - - - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/svea_examples/scripts/pure_pursuit.py b/src/svea_examples/scripts/pure_pursuit.py index c700950a..ef06023a 100755 --- a/src/svea_examples/scripts/pure_pursuit.py +++ b/src/svea_examples/scripts/pure_pursuit.py @@ -2,17 +2,13 @@ import numpy as np -from geometry_msgs.msg import PoseWithCovarianceStamped -from tf_transformations import quaternion_from_euler - from svea_core.interfaces import LocalizationInterface from svea_core.controllers.pure_pursuit import PurePursuitController -from svea_core.interfaces import ActuationInterface +from svea_core.interfaces import ActuationInterface, ShowMarker, ShowPath from svea_core import rosonic as rx -from svea_core.utils import PlaceMarker, ShowPath -class pure_pursuit(rx.Node): # Inherit from rx.Node +class pure_pursuit(rx.Node): r"""Pure Pursuit example script for SVEA. @@ -46,24 +42,23 @@ class pure_pursuit(rx.Node): # Inherit from rx.Node points: List of points defining the path to follow. actuation: Actuation interface for sending control commands. localizer: Localization interface for receiving state information. - mark: PlaceMarker for visualizing the goal. + goal_mark: ShowMarker for visualizing the goal. path: ShowPath for visualizing the path. """ DELTA_TIME = 0.1 TRAJ_LEN = 20 - points = rx.Parameter(['[-2.3, -7.1]', '[10.5, 11.7]', '[5.7, 15.0]', '[-7.0, -4.0]']) - state = rx.Parameter([-7.4, -15.3, 0.9, 0.0]) # x, y, yaw, vel + points = rx.Parameter('[[-2.3, -7.1], [10.5, 11.7], [5.7, 15.0], [-7.0, -4.0]]') target_velocity = rx.Parameter(0.6) # Interfaces + actuation = ActuationInterface() localizer = LocalizationInterface() - # Goal Visualization - mark = PlaceMarker() - # Path Visualization - #path = ShowPath() + + goal_marker = ShowMarker() # for goal visualization + path = ShowPath() # for path visualization def on_startup(self): """ @@ -76,9 +71,8 @@ def on_startup(self): The controller is set to not finished initially, and a timer is created to call the loop method at regular intervals. """ - # Convert POINTS to numerical lists if loaded as strings - if isinstance(self.points[0], str): - self._points = [eval(point) for point in self.points] + # Convert parameter to numerical list + self._points = eval(self.points) self.controller = PurePursuitController() self.controller.target_velocity = self.target_velocity @@ -88,7 +82,7 @@ def on_startup(self): self.curr = 0 self.goal = self._points[self.curr] - self.mark.marker('goal','blue',self.goal) + self.goal_marker.place([*self.goal, 0.5], color='blue') self.update_traj(x, y) self.create_timer(self.DELTA_TIME, self.loop) @@ -110,7 +104,7 @@ def loop(self): self.update_traj(x, y) steering, velocity = self.controller.compute_control(state) - self.get_logger().info(f"Steering: {steering}, Velocity: {velocity}") + # self.get_logger().info(f"Steering: {steering}, Velocity: {velocity}") self.actuation.send_control(steering, velocity) def update_goal(self): @@ -124,7 +118,7 @@ def update_goal(self): self.goal = self._points[self.curr] self.controller.is_finished = False # Mark the goal - self.mark.marker('goal','blue',self.goal) + self.goal_marker.place([*self.goal, 0.5], color='blue') def update_traj(self, x, y): """ @@ -137,7 +131,7 @@ def update_traj(self, x, y): ys = np.linspace(y, self.goal[1], self.TRAJ_LEN) self.controller.traj_x = xs self.controller.traj_y = ys - #self.path.publish_path(xs,ys) + self.path.publish_path(xs,ys) if __name__ == '__main__': pure_pursuit.main() diff --git a/src/svea_localization/launch/localization.py b/src/svea_localization/launch/localization.py deleted file mode 100644 index 3afcb1af..00000000 --- a/src/svea_localization/launch/localization.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright (c) 2018 Intel Corporation -# -# 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.actions import DeclareLaunchArgument, SetEnvironmentVariable -from launch.substitutions import LaunchConfiguration -from launch_ros.actions import Node -from nav2_common.launch import RewrittenYaml - - -def generate_launch_description(): - # Get the launch directory - bringup_dir = get_package_share_directory('svea_localization') - svea_core_dir = get_package_share_directory('svea_core') - - namespace = LaunchConfiguration('namespace') - map_yaml_file = LaunchConfiguration('map') - use_sim_time = LaunchConfiguration('use_sim_time') - autostart = LaunchConfiguration('autostart') - params_file = LaunchConfiguration('params_file') - # lifecycle_nodes = ['map_server', 'amcl'] - lifecycle_nodes = ['amcl'] - initial_pose_a = LaunchConfiguration('initial_pose_a') - initial_pose_x = LaunchConfiguration('initial_pose_x') - initial_pose_y = LaunchConfiguration('initial_pose_y') - - # Map fully qualified names to relative ones so the node's namespace can be prepended. - # In case of the transforms (tf), currently, there doesn't seem to be a better alternative - # https://github.com/ros/geometry2/issues/32 - # https://github.com/ros/robot_state_publisher/pull/30 - # TODO(orduno) Substitute with `PushNodeRemapping` - # https://github.com/ros2/launch_ros/issues/56 - remappings = [('/tf', 'tf'), - ('/tf_static', 'tf_static')] - - # Create our own temporary YAML files that include substitutions - param_substitutions = { - 'use_sim_time': use_sim_time, - 'yaml_filename': map_yaml_file, - 'initial_pose_a': initial_pose_a, - 'initial_pose_x': initial_pose_x, - 'initial_pose_y': initial_pose_y} - - configured_params = RewrittenYaml( - source_file=params_file, - root_key=namespace, - param_rewrites=param_substitutions, - convert_types=True) - - return LaunchDescription([ - # Set env var to print messages to stdout immediately - SetEnvironmentVariable('RCUTILS_LOGGING_BUFFERED_STREAM', '1'), - - DeclareLaunchArgument( - 'namespace', default_value='', - description='Top-level namespace'), - - DeclareLaunchArgument( - 'map', - default_value=os.path.join(svea_core_dir, 'maps', 'sml.yaml'), - description='Full path to map yaml file to load'), - - DeclareLaunchArgument( - 'use_sim_time', default_value='false', - description='Use simulation (Gazebo) clock if true'), - - DeclareLaunchArgument( - 'autostart', default_value='true', - description='Automatically startup the nav2 stack'), - - DeclareLaunchArgument( - 'params_file', - default_value=os.path.join(bringup_dir, 'params', 'localize.yaml'), - description='Full path to the ROS2 parameters file to use'), - - DeclareLaunchArgument("initial_pose_x", default_value="0.0" ), - - DeclareLaunchArgument("initial_pose_y", default_value="0.0" ), - - DeclareLaunchArgument("initial_pose_a", default_value="1.52" ), - - # Node( - # package='nav2_map_server', - # executable='map_server', - # name='map_server', - # output='screen', - # parameters=[configured_params], - # remappings=remappings), - - Node( - package='nav2_amcl', - executable='amcl', - name='amcl', - output='screen', - parameters=[configured_params, - {'initial_pose.x': initial_pose_x}, - {'initial_pose.y': initial_pose_y}, - {'initial_pose.yaw': initial_pose_a}], - remappings=remappings), - - Node( - package='nav2_lifecycle_manager', - executable='lifecycle_manager', - name='lifecycle_manager_localization', - output='screen', - parameters=[{'use_sim_time': use_sim_time}, - {'autostart': autostart}, - {'node_names': lifecycle_nodes}]) - ]) diff --git a/src/svea_localization/launch/localization.xml b/src/svea_localization/launch/localization.xml new file mode 100644 index 00000000..67097b58 --- /dev/null +++ b/src/svea_localization/launch/localization.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/svea_localization/launch/localize.xml b/src/svea_localization/launch/localize.xml deleted file mode 100644 index 47b88479..00000000 --- a/src/svea_localization/launch/localize.xml +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/svea_localization/launch/sensors/lidar.xml b/src/svea_localization/launch/sensors/lidar.xml index c0a8e336..aef66a33 100644 --- a/src/svea_localization/launch/sensors/lidar.xml +++ b/src/svea_localization/launch/sensors/lidar.xml @@ -5,8 +5,8 @@ - - + + @@ -15,7 +15,7 @@ - + diff --git a/src/svea_localization/launch/transforms.xml b/src/svea_localization/launch/transforms.xml index 1ce8bae9..24eac92b 100644 --- a/src/svea_localization/launch/transforms.xml +++ b/src/svea_localization/launch/transforms.xml @@ -1,22 +1,50 @@ - - - - - - - - - - - + - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/svea_localization/params/amcl/localize.yaml b/src/svea_localization/params/amcl/localize.yaml index 38a2a81e..23dc4b55 100644 --- a/src/svea_localization/params/amcl/localize.yaml +++ b/src/svea_localization/params/amcl/localize.yaml @@ -44,7 +44,7 @@ amcl: resample_interval: 1 tf_broadcast: true map_topic: map - scan_topic: /scan + scan_topic: /scan/filtered set_initial_pose: true initial_pose: x: 0.0 diff --git a/src/svea_localization/params/robot_localization/global_ekf.yaml b/src/svea_localization/params/robot_localization/global_ekf.yaml index 8b382f24..6ea81a76 100644 --- a/src/svea_localization/params/robot_localization/global_ekf.yaml +++ b/src/svea_localization/params/robot_localization/global_ekf.yaml @@ -31,6 +31,17 @@ base_link_frame: base_link # Defaults to "base_link" if unspecified world_frame: map # Defaults to the value of odom_frame if unspecified + ########################################## + # Initial State ########################## + + initial_state: [ + 0.0, 0.0, 0.0, # x, y, z, + 0.0, 0.0, 0.0, # roll, pitch, yaw, + 0.0, 0.0, 0.0, # x_dot, y_dot, z_dot, + 0.0, 0.0, 0.0, # roll_dot, pitch_dot, yaw_dot, + 0.0, 0.0, 0.0 # x_dot_dot, y_dot_dot, z_dot_dot, + ] + ########################################## # GPS Odometry ########################### odom0: odometry/gps diff --git a/util/config.sh b/util/config.sh index 7d7d1c39..094add13 100755 --- a/util/config.sh +++ b/util/config.sh @@ -9,6 +9,12 @@ # BUILD_CONFIG="base-amd64" # BUILD_CONFIG="base-arm64" +# # EL2425 - build base image +# IMAGE_TAG="ghcr.io/kth-sml/svea:el2425-base" +# BUILD_CONFIG="ghcr" + +# EL2425 - normal build +BUILD_TAG="ghcr.io/kth-sml/svea:el2425-base" main() { @@ -27,6 +33,7 @@ main() { BUILD_CONFIG="$( \ switch "$BUILD_CONFIG" \ "host" "$_host" \ + "base" "base-$_host" \ "base-host" "base-$_host" \ "$BUILD_CONFIG" \ )" @@ -45,14 +52,6 @@ main() { withdefault BUILD_TAG "ghcr.io/kth-sml/svea:latest" withdefault IMAGE_TAG "$REPOSITORY_NAME" withdefault IMAGE_PUSH "0" - elif [ "$BUILD_CONFIG" = "base-host" ]; then - # building for host platform - withdefault BUILD_PLATFORM "$(uname -m)" - withdefault BUILD_CONTEXT "$REPOSITORY_PATH" - withdefault BUILD_FILE "docker/Dockerfile.base" - withdefault BUILD_TAG "ros:$ROSDISTRO-ros-base" - withdefault IMAGE_TAG "ghcr.io/kth-sml/svea:latest" - withdefault IMAGE_PUSH "0" elif [ "$BUILD_CONFIG" = "base-amd64" ]; then # building for x86_64 withdefault BUILD_PLATFORM "linux/amd64" @@ -83,7 +82,7 @@ main() { fi if [ "$BUILD_FILE" = "docker/Dockerfile.base" ]; then - withdefault USER_CREDENTIALS "svea:SVEA-Pass!" # TODO: Setup regular user instead of root + withdefault USER_CREDENTIALS "root:SVEA-Pass!" # TODO: Setup regular user instead of root fi withdefault CONTAINER_NAME "$REPOSITORY_NAME" @@ -262,4 +261,4 @@ isempty() { ################################################################################ ################################################################################ -main +main \ No newline at end of file diff --git a/util/run b/util/run index 8a61b923..ed351310 100755 --- a/util/run +++ b/util/run @@ -20,7 +20,8 @@ fi append ARGS \ -it \ -p 2222:22 \ - -p "8765:8765" \ + -p 7447:7447 \ + -p 8765:8765 \ -v "$SHARED_VOLUME" \ -e "TERM=xterm-256color" \ --rm