11#!/usr/bin/env python3
22# Copyright 2025 Bertrik Sikken <bertrik@sikken.nl>
3+ import math
34import threading
45from enum import Enum
56
67import rclpy
78import serial
89from rclpy .node import Node
910from sensor_msgs .msg import LaserScan
11+ from std_msgs .msg import Header
1012
1113
1214class FrameExtractor :
@@ -21,22 +23,22 @@ def __init__(self) -> None:
2123 self .length = 0
2224 self .data = bytearray (60 )
2325
24- def add_data (self , b ):
26+ def __add_data (self , b ):
2527 self .data [self .index ] = b
26- self .index = self . index + 1
28+ self .index += 1
2729
28- def process (self , b ):
30+ def process (self , b ) -> bytes | None :
2931 """ processes one byte, returns True if a full frame was received """
3032 match self .state :
3133 case self .State .HEADER_1 :
3234 self .index = 0
3335 if b == 0x55 :
34- self .add_data (b )
36+ self .__add_data (b )
3537 self .state = self .State .HEADER_2
3638
3739 case self .State .HEADER_2 :
3840 if b == 0xAA :
39- self .add_data (b )
41+ self .__add_data (b )
4042 self .length = len (self .data )
4143 self .state = self .State .COLLECT
4244 else :
@@ -45,15 +47,12 @@ def process(self, b):
4547
4648 case self .State .COLLECT :
4749 if self .index < self .length :
48- self .add_data (b )
50+ self .__add_data (b )
4951 if self .index == self .length :
5052 # done
5153 self .state = self .State .HEADER_1
52- return True
53- return False
54-
55- def get_data (self ):
56- return self .data
54+ return self .data
55+ return None
5756
5857
5958class MysteryLidar :
@@ -62,52 +61,84 @@ def __init__(self, device):
6261 self .extractor = FrameExtractor ()
6362
6463 def open (self ) -> None :
65- self .serial .open ()
64+ if not self .serial .is_open :
65+ self .serial .open ()
6666
6767 def poll (self ) -> bytes | None :
6868 num = self .serial .in_waiting
6969 while num > 0 :
70- num = num - 1
71- b = self .serial .read ()
72- done = self .extractor .process (b )
73- if done :
74- return self . extractor . get_data ()
70+ num -= 1
71+ b = self .serial .read ()[ 0 ]
72+ result = self .extractor .process (b )
73+ if result :
74+ return result
7575 return None
7676
7777 def close (self ) -> None :
7878 self .serial .close ()
7979
8080
8181class LidarNode (Node ):
82-
8382 def __init__ (self ):
8483 super ().__init__ ('lidar' )
85- self .declare_parameter ('device' , value = '/dev/ttyLIDAR' )
84+ self .declare_parameter ('device' , value = '/dev/ttyUSB0' )
85+ self .declare_parameter ('topic' , value = '/scan' )
86+ self .declare_parameter ('frame_id' , value = 'laser' )
8687
8788 device = self .get_parameter ('device' ).get_parameter_value ().string_value
8889 self .lidar = MysteryLidar (device )
8990
90- topic_name = self .get_parameter ('topic' ).get_parameter_value ().string_value
91- self .publisher = self .create_publisher (LaserScan , topic_name , 10 )
91+ self .frame_id = self .get_parameter ('frame_id' ).get_parameter_value ().string_value
92+
93+ topic = self .get_parameter ('topic' ).get_parameter_value ().string_value
94+ self .publisher = self .create_publisher (LaserScan , topic , 10 )
9295
9396 self .thread = threading .Thread (target = self .node_task ())
9497 self .thread .start ()
9598
96- def node_task (self ):
99+ def node_task (self ) -> None :
97100 self .lidar .open ()
98101 while rclpy .ok ():
99102 frame = self .lidar .poll ()
100103 if frame :
101104 self .publish_scan (frame )
102105 self .lidar .close ()
103106
104- def publish_scan (self , frame : bytes ):
105- msg = LaserScan ()
106- # TODO decode frame into msg
107+ def publish_scan (self , frame : bytes ) -> None :
108+ fsa = (frame [6 ] + (frame [7 ] << 8 )) / 64.0 - 640
109+ lsa = (frame [56 ] + (frame [57 ] << 8 )) / 64.0 - 640
110+ if lsa < fsa :
111+ lsa += 360
112+ step = (lsa - fsa ) / 16
113+
114+ header = Header (frame_id = self .frame_id , stamp = self .get_clock ().now ().to_msg ())
115+ msg = LaserScan (header = header )
116+ msg .angle_min = math .radians (fsa )
117+ msg .angle_max = math .radians (lsa )
118+ msg .angle_increment = math .radians (step )
119+ msg .range_min = 0.0
120+ msg .range_max = float (0x3FFF )
121+ # time between full 360 sweep: approx 6 rotations per second
122+ msg .scan_time = 1.0 / 6
123+ # time between rays: 6 fps, 36*16 rays per scan
124+ msg .time_increment = msg .scan_time * 10 / (360 * 16 )
125+
126+ idx = 8
127+ for i in range (16 ):
128+ dist = (frame [idx ] + (frame [idx + 1 ] << 8 )) & 0x3FFF
129+ intensity = frame [idx + 2 ]
130+ idx += 3
131+ msg .ranges .append (float (dist ))
132+ msg .intensities .append (float (intensity ))
133+
107134 self .publisher .publish (msg )
108135
109136
110137def main (args = None ):
138+ # Set up a transformer using:
139+ # ros2 run tf2_ros static_transform_publisher 0 0 0 0 0 0 base_link laser
140+ # Visualize using rvis2, add a 'LaserScan' display and choose topic '/scan' and style 'Points'
141+ #
111142 rclpy .init (args = args )
112143 lidar_node = LidarNode ()
113144 rclpy .spin (lidar_node )
0 commit comments