|
15| 0
|
[项目] ROS2雷达小车中篇:四层车身搭建与简易避障实现 |

本帖最后由 云天 于 2026-8-6 11:42 编辑 【项目背景】 在上一篇《ROS2雷达小车开篇》中,我们完成了树莓派4B的系统安装、ROS2 Humble环境搭建,以及RPLIDAR C1雷达驱动的编译和测试。小车已经能通过键盘控制跑起来了,但距离“自主”还有一步之遥。 这一次,我们要做三件事:
![]() 【SSH服务配置】 终于可以无线调试了 在上一篇中,我们尝试SSH登录时遇到了 Permission denied (publickey) 的问题,始终无法用密码登录。折腾了好久,终于找到了根源。 问题现象
问题根源 Ubuntu Server默认通过PAM(可插拔认证模块)来验证密码,而PAM在SSH中走的是 KbdInteractiveAuthentication(键盘交互认证)通道。即使 PasswordAuthentication 设为 yes,如果 KbdInteractiveAuthentication 是 no,系统根本不会走到密码验证那一步。 解决方案 在树莓派本地终端执行: 找到并修改以下两行(确保值为 yes,且行首没有 #): 保存退出(Ctrl+O,回车,Ctrl+X),重启SSH服务: 现在在电脑上就可以愉快地无线连接了: 经验总结:如果SSH连不上,除了检查 PasswordAuthentication,一定要看看 KbdInteractiveAuthentication。这两个要同时为 yes 才能用密码登录。 【车身结构设计】四层亚克力“三明治” 为了让整车布局清晰、便于调试和穿线,我设计了一个四层亚克力车身结构,用激光切割机加工,层与层之间用铜柱支撑。 硬件清单
![]() ![]() ![]() ![]() ![]() 设计要点
小贴士:亚克力板切割前,先用纸板打样确认孔位。铜柱螺柱规格统一选用M3,便于采购和安装。【软件架构】 树莓派 + Arduino 双控方案 整车采用树莓派(决策层)+ Arduino(执行层)的经典架构:
串口通信格式:
Arduino负责接收串口指令并驱动L298P电机驱动板。L298P Shield的默认引脚为:电机A方向D12、PWM D10;电机B方向D13、PWM D11。 【树莓派 ROS2 节点代码】1. 电机控制器节点(motor_controller.py) 订阅 /cmd_vel,将速度指令转换为左右轮PWM值,通过串口发送给Arduino。 #!/usr/bin/env python3 import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist import serial import time class MotorController(Node): def __init__(self): super().__init__('motor_controller') # 参数配置 self.declare_parameter('serial_port', '/dev/ttyACM0') self.declare_parameter('baud_rate', 115200) self.declare_parameter('wheel_base', 0.26) # 轮距(米) self.declare_parameter('wheel_radius', 0.033) # 轮子半径(米) self.declare_parameter('max_speed', 0.5) # 最大线速度 self.serial_port = self.get_parameter('serial_port').value self.baud_rate = self.get_parameter('baud_rate').value self.wheel_base = self.get_parameter('wheel_base').value self.wheel_radius = self.get_parameter('wheel_radius').value self.max_speed = self.get_parameter('max_speed').value # 初始化串口 try: self.ser = serial.Serial(self.serial_port, self.baud_rate, timeout=0.1) self.get_logger().info(f'Serial port {self.serial_port} opened successfully') time.sleep(2) except Exception as e: self.get_logger().error(f'Failed to open serial port: {e}') return # 订阅 /cmd_vel self.subscription = self.create_subscription( Twist, '/cmd_vel', self.cmd_vel_callback, 10 ) self.get_logger().info('Motor controller node started') def cmd_vel_callback(self, msg): vx = msg.linear.x vz = msg.angular.z vx = max(-self.max_speed, min(self.max_speed, vx)) vz = max(-2.0, min(2.0, vz)) # 差速运动学计算 left_velocity = (vx - vz * self.wheel_base / 2.0) / self.wheel_radius right_velocity = (vx + vz * self.wheel_base / 2.0) / self.wheel_radius max_rad_speed = self.max_speed / self.wheel_radius left_pwm = int((left_velocity / max_rad_speed) * 255) right_pwm = int((right_velocity / max_rad_speed) * 255) left_pwm = max(-255, min(255, left_pwm)) right_pwm = max(-255, min(255, right_pwm)) # 最小PWM阈值(防止电机不转,尤其是转向时) MIN_PWM = 80 if left_pwm != 0 and abs(left_pwm) < MIN_PWM: left_pwm = MIN_PWM if left_pwm > 0 else -MIN_PWM if right_pwm != 0 and abs(right_pwm) < MIN_PWM: right_pwm = MIN_PWM if right_pwm > 0 else -MIN_PWM self.send_motor_command(left_pwm, right_pwm) self.get_logger().debug(f'vx={vx:.2f}, vz={vz:.2f} -> L={left_pwm}, R={right_pwm}') def send_motor_command(self, left_pwm, right_pwm): try: cmd = f'M {left_pwm} {right_pwm}\n' self.ser.write(cmd.encode()) except Exception as e: self.get_logger().error(f'Serial send error: {e}') def destroy_node(self): try: self.send_motor_command(0, 0) self.ser.close() except: pass super().destroy_node() def main(args=None): rclpy.init(args=args) node = MotorController() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main() 订阅雷达 /scan 话题,实现“直走 → 遇障 → 看左右 → 转向”的简易避障逻辑。 #!/usr/bin/env python3 import rclpy from rclpy.node import Node from sensor_msgs.msg import LaserScan from geometry_msgs.msg import Twist import math class WallFollower(Node): def __init__(self): super().__init__('wall_follower') self.declare_parameter('forward_speed', 0.2) self.declare_parameter('rotate_speed', 1.0) # 提高转向速度 self.declare_parameter('danger_distance', 0.5) self.declare_parameter('scan_angle', 30) self.forward_speed = self.get_parameter('forward_speed').value self.rotate_speed = self.get_parameter('rotate_speed').value self.danger_distance = self.get_parameter('danger_distance').value self.scan_angle = self.get_parameter('scan_angle').value self.state = 'FORWARD' self.turn_direction = 0 self.turn_start_time = 0 self.turn_duration = 2.1 # 120度 @ 1.0 rad/s self.scan_sub = self.create_subscription(LaserScan, '/scan', self.scan_callback, 10) self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10) self.get_logger().info('Wall follower node started') def scan_callback(self, msg): if self.state == 'FORWARD': self.do_forward_behavior(msg) elif self.state == 'TURNING': self.do_turning_behavior() def do_forward_behavior(self, msg): front_min = self.get_min_distance_in_angle(msg, -30, 30) if front_min > self.danger_distance: self.move_forward() return self.stop() left_dist = self.get_min_distance_in_angle(msg, 30, 90) right_dist = self.get_min_distance_in_angle(msg, -90, -30) self.get_logger().info(f'Front: {front_min:.2f}m, Left: {left_dist:.2f}m, Right: {right_dist:.2f}m') if left_dist > right_dist: self.turn_direction = 1 self.get_logger().info('Turning LEFT') else: self.turn_direction = -1 self.get_logger().info('Turning RIGHT') self.turn_duration = 2.1 / self.rotate_speed # 120度 self.turn_start_time = self.get_clock().now().seconds_nanoseconds()[0] self.state = 'TURNING' def do_turning_behavior(self): elapsed = self.get_clock().now().seconds_nanoseconds()[0] - self.turn_start_time if elapsed < self.turn_duration: twist = Twist() twist.angular.z = self.turn_direction * self.rotate_speed self.cmd_pub.publish(twist) else: self.state = 'FORWARD' self.stop() def get_min_distance_in_angle(self, scan, start_deg, end_deg): start_rad = math.radians(start_deg) end_rad = math.radians(end_deg) angle_min = scan.angle_min angle_increment = scan.angle_increment min_dist = float('inf') for i, range_val in enumerate(scan.ranges): if range_val < 0.01: continue angle = angle_min + i * angle_increment if start_rad <= end_rad: if start_rad <= angle <= end_rad: if range_val < min_dist: min_dist = range_val else: if angle >= start_rad or angle <= end_rad: if range_val < min_dist: min_dist = range_val return min_dist if min_dist != float('inf') else 10.0 def move_forward(self): twist = Twist() twist.linear.x = self.forward_speed self.cmd_pub.publish(twist) def stop(self): self.cmd_pub.publish(Twist()) def main(args=None): rclpy.init(args=args) node = WallFollower() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main() 【三个终端:启动命令】 项目运行时,需要同时开启三个终端(通过SSH连接树莓派后,在电脑上开三个终端窗口即可)。 终端1:启动雷达驱动![]() 终端2:启动电机控制器 ![]() 终端3:启动自主避障节点 如果想让小车跑得快一点或慢一点,可以在启动时调整参数:![]() 【调试中的真实问题与解决问题】 问题1:SSH连不上
经过调试,小车目前能够:
【下一步计划】
中篇到此结束,下篇我们将进入SLAM建图和自主导航的世界。 |
沪公网安备31011502402448© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed