from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument, EmitEvent, RegisterEventHandler, LogInfo
from launch.events import matches_action
+from launch.substitutions import LaunchConfiguration, PathJoinSubstitution, Command, FileContent
+from launch.conditions import IfCondition
from launch_ros.actions import Node, LifecycleNode
from launch_ros.events.lifecycle import ChangeState
from launch_ros.event_handlers import OnStateTransition
-from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
-from launch.conditions import IfCondition
+from launch_ros.parameter_descriptions import ParameterValue
+from launch_ros.substitutions import FindPackageShare
from lifecycle_msgs.msg import Transition
# save map: ros2 run nav2_map_server map_saver_cli -f /mapfolder/mapfilename
name='a4wd3_display',
output="screen",
),
+ # URDF-based static frames (base_link -> laser, imu). Replaces the former
+ # tf_base_scan / tf_base_imu static_transform_publisher nodes (duplicate
+ # publishers of the same static TF cause conflicts).
+ Node(
+ package='robot_state_publisher',
+ executable='robot_state_publisher',
+ name='robot_state_publisher',
+ parameters=[{'robot_description': ParameterValue(FileContent(PathJoinSubstitution([FindPackageShare('a4wd3'), 'urdf', 'a4wd3.urdf'])), value_type=str)}],
+ output="screen"
+ ),
Node(
package='bno085_uart',
executable='bno085',
parameters=[{"port": "/dev/ttyS2"}],
output="screen",
),
- Node(
- package='tf2_ros',
- executable='static_transform_publisher',
- name='tf_base_imu',
- arguments = ['--x', '-0.02', '--y', '-0.013', '--roll', '3.1416', '--yaw', '0.0', '--frame-id', 'base_link', '--child-frame-id', 'imu'],
- output="screen"
- ),
+ # NOTE: base_link -> imu and base_link -> laser static transforms now come from
+ # robot_state_publisher (urdf/a4wd3.urdf) instead of static_transform_publisher.
Node(
condition=IfCondition(with_gps),
package='tf2_ros',
arguments = ['--x', '-0.04', '--y', '-0.035', '--frame-id', 'base_link', '--child-frame-id', 'gps'],
output="screen"
),
- Node(
- package='tf2_ros',
- executable='static_transform_publisher',
- name='tf_base_scan',
- arguments = ['--x', '0.08125', '--y', '0.07425', '--yaw', '3.141592653589793', '--frame-id', 'base_link', '--child-frame-id', 'laser'],
- output="screen"
- ),
+ # (tf_base_scan removed: base_link -> laser now comes from the URDF via robot_state_publisher)
Node(
package='robot_localization',
executable='ekf_node',
--- /dev/null
+<?xml version="1.0"?>
+<!-- A4WD3 chassis URDF (no manipulator). Generated 2026-10-11.
+ Sources:
+ - /home/erik/a4wd3-git/launch/a4wd3_launch.py static transforms (tf_base_scan, tf_base_imu)
+ - A4WD3-Wheeled.stp (user CAD, true scale: overall width 437.0 mm matches measurement)
+ - rplidar-a2m4-r1.stl (Slamtec official model; flat-panel lidar, scan axis = mesh Z)
+ Frame conventions:
+ LIVE base_link (as published by hw_node.cpp odom TF): x = forward, y = left, z = up,
+ ORIGIN AT WHEELBASE CENTER / AXLE HEIGHT (z=0 at axle plane; ground ~79 mm below,
+ i.e. one wheel rolling radius of ~158 mm dia).
+ The launch-file static_transform_publisher values are expressed in THIS live frame,
+ so they are used VERBATIM as fixed joints here (this URDF replaces those publishers).
+ Chassis CAD frame (for reference): x_c = right, y_c = up, z_c = aft (+z_c = aft,
+ verified via waffle M3-pair labels: FRONT pairs at z_c=-125, BACK at z_c=+125).
+ Mesh transform applied to the chassis STL: live_x = -cad_z, live_y = -cad_x,
+ live_z = cad_y - 35.0 mm (wheel centers sit at cad_y = 35.0 mm).
+ Masses: REAL (scale 2026-10-11: 7.0 kg total incl. lidar + battery; A2M4(r1) = 0.19 kg per
+ manufacturer). Inertia tensors: ESTIMATED from mass + footprint - refine if precision needed. -->
+<robot name="a4wd3">
+
+ <link name="base_link">
+ <!-- COM from STEP BRep analysis: CAD (0.07, 38.69, 0.53) mm -> live (-0.53, -0.07, 3.69) mm -->
+ <inertial>
+ <origin xyz="-0.00053 -0.00007 0.00369" rpy="0 0 0"/>
+ <!-- REAL MASS (user scale 2026-10-11): whole robot incl. lidar + battery = 7.0 kg;
+ laser link = 0.19 kg (manufacturer spec A2M4(r1)) => base_link = 6.81 kg.
+ NOTE: the visual/collision mesh EXCLUDES the 4 wheel solids (they were removed from
+ the STEP before tessellation); their mass is still counted here. Inertia scaled from
+ the 9 kg placeholder by 6.81/9. -->
+ <mass value="6.81"/>
+ <inertia ixx="0.21" ixy="0" ixz="0" iyy="0.15" iyz="0" izz="0.32"/>
+ </inertial>
+ <visual>
+ <origin xyz="0 0 0" rpy="0 0 0"/>
+ <geometry>
+ <mesh filename="package://a4wd3/meshes/a4wd3_base_rviz.stl"/>
+ </geometry>
+ <material name="chassis_grey">
+ <color rgba="0.45 0.45 0.45 1.0"/>
+ </material>
+ </visual>
+ <collision>
+ <origin xyz="0 0 0" rpy="0 0 0"/>
+ <geometry>
+ <mesh filename="package://a4wd3/meshes/a4wd3_base_rviz.stl"/>
+ </geometry>
+ </collision>
+ </link>
+
+ <!-- RP-LIDAR A2M4 (r1), flat panel. Launch tf_base_scan VERBATIM:
+ t=(+0.08125, +0.07425, 0) m, yaw = pi (live base_link frame).
+ => 81.25 mm forward, 74.25 mm left, at axle height; scan plane ~74 mm above axles
+ (~153 mm above floor). Visual origin offset puts the mesh's scan plane
+ (mesh z=+0.1125, body axis at mesh x=-0.006, y=+0.028) onto the laser frame origin. -->
+ <joint name="base_to_laser" type="fixed">
+ <parent link="base_link"/>
+ <child link="laser"/>
+ <origin xyz="0.08125 0.07425 0" rpy="0 0 3.14159"/>
+ </joint>
+ <link name="laser">
+ <inertial>
+ <origin xyz="0 0 0" rpy="0 0 0"/>
+ <!-- 0.19 kg: manufacturer spec for A2M4(r1) -->
+ <mass value="0.19"/>
+ <inertia ixx="0.0001" ixy="0" ixz="0" iyy="0.0001" iyz="0" izz="0.0002"/>
+ </inertial>
+ <visual>
+ <origin xyz="0.006 -0.028 -0.1125" rpy="0 0 0"/>
+ <geometry>
+ <mesh filename="package://a4wd3/meshes/rplidar_a2m4_decimated.stl"/>
+ </geometry>
+ <material name="lidar_black">
+ <color rgba="0.1 0.1 0.1 1.0"/>
+ </material>
+ </visual>
+ <collision>
+ <origin xyz="0 0 0" rpy="0 0 0"/>
+ <geometry>
+ <box size="0.136 0.041 0.075"/>
+ </geometry>
+ </collision>
+ </link>
+
+ <!-- BNO085 IMU. Launch tf_base_imu VERBATIM: t=(-0.02, -0.013, 0) m, roll = pi. -->
+ <joint name="base_to_imu" type="fixed">
+ <parent link="base_link"/>
+ <child link="imu"/>
+ <origin xyz="-0.02 -0.013 0" rpy="3.14159 0 0"/>
+ </joint>
+ <link name="imu">
+ <inertial>
+ <origin xyz="0 0 0" rpy="0 0 0"/>
+ <mass value="0.01"/>
+ <inertia ixx="1e-6" ixy="0" ixz="0" iyy="1e-6" iyz="0" izz="1e-6"/>
+ </inertial>
+ </link>
+
+</robot>