SW Sonar - Weber-State-Submarine-Project/Submarine GitHub Wiki
Software Implementation for Sonar Systems
This page documents the implementation details of the Front Sonar (Ping2), Side Sonar, and Down Sonar, including their roles, functionalities, and respective ROS 2 node implementations.
Front Sonar (Blue Robotics Ping2)
Description
The Front Sonar utilizes the Ping2 sensor from Blue Robotics for forward distance measurements. It publishes data to the /scan/front
topic as a LaserScan
message and to /scan/front/points
as a PointCloud2
message. It also interacts with an /odom
publisher for localization and a /turning
subscriber to reset distance buffers during navigation turns.
Key Features:
- Publishes raw and averaged distance data.
- Implements a buffer for smoothing noisy sonar readings.
- Handles outlier readings and ensures robustness during turns.
Code:
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan, PointCloud2, PointField
from std_msgs.msg import Header
import numpy as np
import struct
import sys
import site
import time
from std_msgs.msg import Int32
from nav_msgs.msg import Odometry
site.addsitedir('/home/submarine/.local/share/pipx/venvs/bluerobotics-ping/lib/python3.12/site-packages')
from brping import Ping1D
class PingSonarNode(Node):
def __init__(self):
super().__init__('ping_sonar_node')
self.scan_publisher = self.create_publisher(LaserScan, '/scan/front', 10)
self.pointcloud_publisher = self.create_publisher(PointCloud2, '/scan/front/points', 10)
self.odom_publisher = self.create_publisher(Odometry, 'odom', 10)
self.turning_subscriber = self.create_subscription(
Int32,
'/turning',
self.turning_callback,
10)
self.distance_buffer = [] # List of 5 valid Distance Data values
self.BUFFER_SIZE = 5
self.MAX_DISTANCE_JUMP = 1 # Meters
self.TURN_DISTANCE_THRESHOLD = 3 # Meters
self.STUCK_COUNT_LIMIT = 10
self.prev_distance = 0
self.turn_state = 0
self.stuck_count = 0
self.ping = Ping1D()
while True:
self.get_logger().info("Attempting to connect to Ping2...")
self.ping.connect_serial('/dev/ttyUSB0', 115200)
if self.ping.initialize():
self.get_logger().info("Successfully initialized Ping2.")
break
else:
self.get_logger().error("Failed to initialize Ping2! Retrying...")
time.sleep(5) # Wait for 5 seconds before retrying
self.ping.set_mode_auto(1)
#self.get_logger().info(f"Gain Val: {self.ping.get_gain_setting()}")
#self.get_logger().info(f"Range Val: {self.ping.get_range()}")
self.timer = self.create_timer(0.01, self.timer_callback)
def turning_callback(self,msg):
self.turn_state = msg.data
def timer_callback(self):
data = self.ping.get_distance()
if data:
distance = data['distance'] / 1000.0 # Convert mm to meters
prev_distance_lower = self.prev_distance - self.MAX_DISTANCE_JUMP
prev_distance_upper = self.prev_distance + self.MAX_DISTANCE_JUMP
# Reset Values when turning
if self.turn_state == 1 and distance < self.TURN_DISTANCE_THRESHOLD:
self.distance_buffer = []
self.prev_distance = 0
return
# Logic to determine if valid data
if prev_distance_lower < distance < prev_distance_upper or self.prev_distance == 0:
self.distance_buffer.append(distance)
else:
#self.get_logger().warn(f"Distance out of range --- Distance_read: {distance} Previous_Distance: {self.prev_distance} Max_Jump: {self.MAX_DISTANCE_JUMP}")
self.stuck_count += 1
# Corner Case: If sonar reads bad data for STUCK_COUNT_LIMIT, it will reset itself based on new distance
if self.stuck_count >= self.STUCK_COUNT_LIMIT:
self.distance_buffer = []
self.stuck_count = 0
self.prev_distance = distance
# Limits Buffer Size
if len(self.distance_buffer) > self.BUFFER_SIZE:
self.distance_buffer.pop(0)
# Logic to avg distance
if len(self.distance_buffer) == self.BUFFER_SIZE:
avg_distance = sum(self.distance_buffer) / self.BUFFER_SIZE
self.prev_distance = avg_distance
self.publish_scan(avg_distance)
self.publish_pointcloud(avg_distance)
#self.get_logger().info(f'Raw_distance: {distance} Avg_Distance {avg_distance}')
#else:
#self.get_logger().warn("Failed to get distance data from Ping2!")
def publish_scan(self, distance):
scan_msg = LaserScan()
scan_msg.header.stamp = self.get_clock().now().to_msg()
scan_msg.header.frame_id = 'front_sonar'
scan_msg.angle_min = 0.0
scan_msg.angle_max = 0.0
scan_msg.angle_increment = 0.0
scan_msg.time_increment = 0.0
scan_msg.scan_time = 0.1
scan_msg.range_min = 0.5
scan_msg.range_max = 100.0
scan_msg.ranges = [distance]
scan_msg.intensities = []
self.scan_publisher.publish(scan_msg)
def publish_pointcloud(self, distance):
header = Header()
header.stamp = self.get_clock().now().to_msg()
header.frame_id = 'front_sonar'
points = np.array([distance, 0.0, 0.0](/Weber-State-Submarine-Project/Submarine/wiki/distance,-0.0,-0.0), dtype=np.float32)
pointcloud_msg = PointCloud2()
pointcloud_msg.header = header
pointcloud_msg.height = 1
pointcloud_msg.width = points.shape[0]
pointcloud_msg.fields = [
PointField(name='x', offset=0, datatype=PointField.FLOAT32, count=1),
PointField(name='y', offset=4, datatype=PointField.FLOAT32, count=1),
PointField(name='z', offset=8, datatype=PointField.FLOAT32, count=1),
]
pointcloud_msg.is_bigendian = False
pointcloud_msg.point_step = 12
pointcloud_msg.row_step = pointcloud_msg.point_step * points.shape[0]
pointcloud_msg.is_dense = True
pointcloud_msg.data = np.asarray(points, np.float32).tobytes()
self.pointcloud_publisher.publish(pointcloud_msg)
def main(args=None):
rclpy.init(args=args)
node = PingSonarNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Side Sonar (DFRobotics IP68 Sonar)
Description
The Side Sonar uses a DFRobotics IP68 sonar sensor to measure distances on the left side of the boat. It publishes data to the /scan/left
topic as a LaserScan
message and to /scan/left/points
as a PointCloud2
message. The sonar operates via UART communication.
Key Features:
- Publishes real-time side distance data.
- Handles distance validation using checksum validation from the sensor.
- Provides fallback mechanisms for stale or invalid readings.
Code:
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan, PointCloud2
from std_msgs.msg import Header
import serial
import time
import struct
from sensor_msgs_py import point_cloud2 as pc2
#import sensor_msgs.point_cloud2 as pc2
class SonarPublisher(Node):
def __init__(self):
super().__init__('sonar_publisher')
self.laser_publisher = self.create_publisher(LaserScan, '/scan/left', 10)
self.point_cloud_publisher = self.create_publisher(PointCloud2, '/scan/left/points', 10)
self.timer = self.create_timer(0.1, self.timer_callback)
self.ser = serial.Serial('/dev/ttyUSB1', 115200, timeout=1)
self.COM = 0x55
self.buffer_RTT = [0, 0, 0, 0]
self.Distance = 0
self.Last_dist = 0
def timer_callback(self):
self.ser.write(bytes([self.COM]))
time.sleep(0.1)
if self.ser.in_waiting > 0:
time.sleep(0.004)
if self.ser.read() == b'\xff':
self.buffer_RTT[0] = 0xff
for i in range(1, 4):
self.buffer_RTT[i] = ord(self.ser.read())
CS = (self.buffer_RTT[0] + self.buffer_RTT[1] + self.buffer_RTT[2]) % 256
if self.buffer_RTT[3] == CS:
self.Distance = (self.buffer_RTT[1] << 8) + self.buffer_RTT[2]
distance_in_meters = self.Distance / 1000.0
if self.Distance > 0:
# Publish LaserScan message
laser_msg = LaserScan()
laser_msg.header.stamp = self.get_clock().now().to_msg()
laser_msg.header.frame_id = 'left_sonar'
laser_msg.angle_min = -0.0872665 # -5 degrees in radians
laser_msg.angle_max = 0.0872665 # 5 degrees in radians
laser_msg.angle_increment = 0.174533 # 10 degrees in radians
laser_msg.time_increment = 0.0
laser_msg.scan_time = 0.1
laser_msg.range_min = 0.02 # 2 cm
laser_msg.range_max = 6.0 # 600 cm
laser_msg.ranges = [distance_in_meters,distance_in_meters]
self.laser_publisher.publish(laser_msg)
# Publish PointCloud2 message
header = Header()
header.stamp = self.get_clock().now().to_msg()
header.frame_id = 'left_sonar'
points = [(distance_in_meters, 0.0, 0.0)]
point_cloud_msg = pc2.create_cloud_xyz32(header, points)
self.point_cloud_publisher.publish(point_cloud_msg)
self.Last_dist = distance_in_meters
else:
# Publish LaserScan message
laser_msg = LaserScan()
laser_msg.header.stamp = self.get_clock().now().to_msg()
laser_msg.header.frame_id = 'left_sonar'
laser_msg.angle_min = -0.0872665 # -5 degrees in radians
laser_msg.angle_max = 0.0872665 # 5 degrees in radians
laser_msg.angle_increment = 0.174533 # 10 degrees in radians
laser_msg.time_increment = 0.0
laser_msg.scan_time = 0.1
laser_msg.range_min = 0.02 # 2 cm
laser_msg.range_max = 6.0 # 600 cm
laser_msg.ranges = [self.Last_dist,self.Last_dist]
self.laser_publisher.publish(laser_msg)
# Publish PointCloud2 message
header = Header()
header.stamp = self.get_clock().now().to_msg()
header.frame_id = 'left_sonar'
points = [(self.Last_dist, 0.0, 0.0)]
point_cloud_msg = pc2.create_cloud_xyz32(header, points)
self.point_cloud_publisher.publish(point_cloud_msg)
#self.get_logger().info(f'Distance: {distance_in_meters:.3f}m')
def main(args=None):
rclpy.init(args=args)
sonar_publisher = SonarPublisher()
rclpy.spin(sonar_publisher)
sonar_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Down Sonar (DFRobotics IP68 Sonar)
Description
The Down Sonar uses a DFRobotics IP68 sonar sensor to measure depth directly beneath the boat. It publishes data to the /scan/down
topic as a LaserScan
message and to /scan/down/points
as a PointCloud2
message. This sonar ensures accurate depth measurements for creating a 3D map of the pool floor.
Key Features:
- Publishes real-time depth data.
- Implements a reset mechanism for inconsistent readings.
- Ensures robust measurements by comparing current and previous values.
Code
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan, PointCloud2
from std_msgs.msg import Header
import serial
import time
import struct
from sensor_msgs_py import point_cloud2 as pc2
#import sensor_msgs.point_cloud2 as pc2
class SonarPublisher(Node):
def __init__(self):
super().__init__('sonar_publisher')
self.laser_publisher = self.create_publisher(LaserScan, '/scan/down', 10)
self.point_cloud_publisher = self.create_publisher(PointCloud2, '/scan/down/points', 10)
self.timer = self.create_timer(0.1, self.timer_callback)
self.ser = serial.Serial('/dev/ttyUSB2', 115200, timeout=1)
self.COM = 0x55
self.buffer_RTT = [0, 0, 0, 0]
self.Distance = 0
self.prev_dist = None
self.reset_counter = 0
def timer_callback(self):
self.ser.write(bytes([self.COM]))
time.sleep(0.1)
if self.ser.in_waiting > 0:
time.sleep(0.004)
if self.ser.read() == b'\xff':
self.buffer_RTT[0] = 0xff
for i in range(1, 4):
self.buffer_RTT[i] = ord(self.ser.read())
CS = (self.buffer_RTT[0] + self.buffer_RTT[1] + self.buffer_RTT[2]) % 256
if self.buffer_RTT[3] == CS:
self.Distance = (self.buffer_RTT[1] << 8) + self.buffer_RTT[2]
distance_in_meters = self.Distance / 1000.0
diff = 100
if distance_in_meters !=0 and self.prev_dist == None:
self.prev_dist = distance_in_meters
if self.prev_dist != None:
diff = abs(distance_in_meters - self.prev_dist)
# Publish LaserScan message
if distance_in_meters != 0 and (diff <= 0.5 or self.reset_counter > 3):
reset_counter = 0
laser_msg = LaserScan()
laser_msg.header.stamp = self.get_clock().now().to_msg()
laser_msg.header.frame_id = 'down_sonar'
laser_msg.angle_min = -0.0872665 # -5 degrees in radians
laser_msg.angle_max = 0.0872665 # 5 degrees in radians
laser_msg.angle_increment = 0.174533 # 10 degrees in radians
laser_msg.time_increment = 0.0
laser_msg.scan_time = 0.1
laser_msg.range_min = 0.02 # 2 cm
laser_msg.range_max = 6.0 # 600 cm
laser_msg.ranges = [distance_in_meters]
self.laser_publisher.publish(laser_msg)
# Publish PointCloud2 message
header = Header()
header.stamp = self.get_clock().now().to_msg()
header.frame_id = 'down_sonar'
points = [(distance_in_meters, 0.0, 0.0)]
point_cloud_msg = pc2.create_cloud_xyz32(header, points)
self.point_cloud_publisher.publish(point_cloud_msg)
#self.get_logger().info(f'Distance: {distance_in_meters:.3f}m')
self.prev_dist = distance_in_meters
if diff >= 0.5:
self.reset_counter += 1
def main(args=None):
rclpy.init(args=args)
sonar_publisher = SonarPublisher()
rclpy.spin(sonar_publisher)
sonar_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()