11
11
12
12
* Author(s): Dave Astels
13
13
* Based on https://github.com/SkoltechRobotics/rplidar by Artyom Pavlov
14
+ * and updates from https://github.com/Roboticia/RPLidar by Julien JEHL
14
15
15
16
Implementation Notes
16
17
--------------------
23
24
* Adafruit CircuitPython firmware for the supported boards:
24
25
https://github.com/adafruit/circuitpython/releases
25
26
26
- Version 0.0.1 does NOT support CircuitPython. Future versions will.
27
+ The Current Version does NOT support CircuitPython. Future versions will.
27
28
"""
28
29
29
30
import struct
30
31
import sys
31
32
import time
32
33
import warnings
34
+ from collections import namedtuple
33
35
34
36
# pylint:disable=invalid-name,undefined-variable,global-variable-not-assigned
35
- # pylint:disable=too-many-arguments,raise-missing-from
37
+ # pylint:disable=too-many-arguments,raise-missing-from,too-many-instance-attributes
36
38
37
39
__version__ = "0.0.0-auto.0"
38
40
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RPLIDAR.git"
46
48
STOP_BYTE = b"\x25 "
47
49
RESET_BYTE = b"\x40 "
48
50
49
- SCAN_BYTE = b"\x20 "
50
- FORCE_SCAN_BYTE = b"\x21 "
51
-
52
51
DESCRIPTOR_LEN = 7
53
52
INFO_LEN = 20
54
53
HEALTH_LEN = 3
55
54
56
55
INFO_TYPE = 4
57
56
HEALTH_TYPE = 6
58
- SCAN_TYPE = 129
59
57
60
58
# Constants & Command to start A2 motor
61
59
MAX_MOTOR_PWM = 1023
68
66
2 : "Error" ,
69
67
}
70
68
69
+ SCAN_TYPE_NORMAL = 0
70
+ SCAN_TYPE_FORCE = 1
71
+ SCAN_TYPE_EXPRESS = 2
72
+
73
+ _SCAN_TYPES = (
74
+ {"byte" : b"\x20 " , "response" : 129 , "size" : 5 },
75
+ {"byte" : b"\x21 " , "response" : 129 , "size" : 5 },
76
+ {"byte" : b"\x82 " , "response" : 130 , "size" : 84 },
77
+ )
78
+
79
+ express_packet = namedtuple ("express_packet" , "distance angle new_scan start_angle" )
80
+
71
81
72
82
class RPLidarException (Exception ):
73
83
"""Basic exception class for RPLidar"""
@@ -88,6 +98,17 @@ def _process_scan(raw):
88
98
return new_scan , quality , angle , distance
89
99
90
100
101
+ def _process_express_scan (data , new_angle , frame ):
102
+ new_scan = (new_angle < data .start_angle ) & (frame == 1 )
103
+ angle = (
104
+ data .start_angle
105
+ + ((new_angle - data .start_angle ) % 360 ) / 32 * frame
106
+ - data .angle [frame - 1 ]
107
+ ) % 360
108
+ distance = data .distance [frame - 1 ]
109
+ return new_scan , None , angle , distance
110
+
111
+
91
112
class RPLidar :
92
113
"""Class for communicating with RPLidar rangefinder scanners"""
93
114
@@ -97,6 +118,12 @@ class RPLidar:
97
118
timeout = 1 #: Serial port timeout
98
119
motor = False #: Is motor running?
99
120
baudrate = 115200 #: Baudrate for serial port
121
+ scanning = False
122
+ descriptor_size = 0
123
+ scan_type = SCAN_TYPE_NORMAL
124
+ express_frame = 32
125
+ express_data = False
126
+ express_old_data = None
100
127
101
128
def __init__ (self , motor_pin , port , baudrate = 115200 , timeout = 1 , logging = False ):
102
129
"""Initialize RPLidar object for communicating with the sensor.
@@ -152,7 +179,6 @@ def connect(self):
152
179
parity = serial .PARITY_NONE ,
153
180
stopbits = serial .STOPBITS_ONE ,
154
181
timeout = self .timeout ,
155
- dsrdtr = True ,
156
182
)
157
183
except serial .SerialException as err :
158
184
raise RPLidarException (
@@ -253,7 +279,7 @@ def info(self):
253
279
if dtype != INFO_TYPE :
254
280
raise RPLidarException ("Wrong response data type" )
255
281
raw = self ._read_response (dsize )
256
- serialnumber_bytes = struct .unpack ("BBBBBBBBBBBBBBBB" , raw [4 :])
282
+ serialnumber_bytes = struct .unpack ("B" * len ( raw [ 4 :]) , raw [4 :])
257
283
serialnumber = "" .join (reversed (["%02x" % b for b in serialnumber_bytes ]))
258
284
data = {
259
285
"model" : raw [0 ],
@@ -294,13 +320,67 @@ def health(self):
294
320
295
321
def clear_input (self ):
296
322
"""Clears input buffer by reading all available data"""
323
+ if self .scanning :
324
+ raise RPLidarException ("Clearing not allowed during active scanning!" )
325
+ self ._serial_port .flushInput ()
326
+ self .express_frame = 32
327
+ self .express_data = False
328
+
329
+ def start (self , scan_type = SCAN_TYPE_NORMAL ):
330
+ """Start the scanning process
331
+ Parameters
332
+ ----------
333
+ scan : normal, force or express.
334
+ """
335
+ if self .scanning :
336
+ raise RPLidarException ("Scanning already running!" )
337
+ # Start the scanning process, enable laser diode and the
338
+ # measurement system
339
+ status , error_code = self .health
340
+ self .log ("debug" , "Health status: %s [%d]" % (status , error_code ))
341
+ if status == _HEALTH_STATUSES [2 ]:
342
+ self .log (
343
+ "warning" ,
344
+ "Trying to reset sensor due to the error. "
345
+ "Error code: %d" % (error_code ),
346
+ )
347
+ self .reset ()
348
+ status , error_code = self .health
349
+ if status == _HEALTH_STATUSES [2 ]:
350
+ raise RPLidarException (
351
+ "RPLidar hardware failure. " "Error code: %d" % error_code
352
+ )
353
+ elif status == _HEALTH_STATUSES [1 ]:
354
+ self .log (
355
+ "warning" ,
356
+ "Warning sensor status detected! " "Error code: %d" % (error_code ),
357
+ )
358
+ cmd = _SCAN_TYPES [scan_type ]["byte" ]
359
+ self .log ("info" , "starting scan process in %s mode" % scan_type )
360
+
361
+ if scan_type == "express" :
362
+ self ._send_payload_cmd (cmd , b"\x00 \x00 \x00 \x00 \x00 " )
363
+ else :
364
+ self ._send_cmd (cmd )
365
+
366
+ dsize , is_single , dtype = self ._read_descriptor ()
367
+ if dsize != _SCAN_TYPES [scan_type ]["size" ]:
368
+ raise RPLidarException ("Wrong info reply length" )
369
+ if is_single :
370
+ raise RPLidarException ("Not a multiple response mode" )
371
+ if dtype != _SCAN_TYPES [scan_type ]["response" ]:
372
+ raise RPLidarException ("Wrong response data type" )
373
+ self .descriptor_size = dsize
374
+ self .scan_type = scan_type
375
+ self .scanning = True
297
376
298
377
def stop (self ):
299
378
"""Stops scanning process, disables laser diode and the measurement
300
379
system, moves sensor to the idle state."""
301
380
self .log ("info" , "Stopping scanning" )
302
381
self ._send_cmd (STOP_BYTE )
303
382
time .sleep (0.001 )
383
+ self .scanning = False
304
384
self .clear_input ()
305
385
306
386
def reset (self ):
@@ -309,8 +389,9 @@ def reset(self):
309
389
self .log ("info" , "Resetting the sensor" )
310
390
self ._send_cmd (RESET_BYTE )
311
391
time .sleep (0.002 )
392
+ self .clear_input ()
312
393
313
- def iter_measurements (self , max_buf_meas = 500 ):
394
+ def iter_measurements (self , max_buf_meas = 500 , scan_type = SCAN_TYPE_NORMAL ):
314
395
"""Iterate over measurements. Note that consumer must be fast enough,
315
396
otherwise data will be accumulated inside buffer and consumer will get
316
397
data with increasing lag.
@@ -334,37 +415,11 @@ def iter_measurements(self, max_buf_meas=500):
334
415
In millimeter unit. Set to 0 when measurement is invalid.
335
416
"""
336
417
self .start_motor ()
337
- status , error_code = self .health
338
- self .log ("debug" , "Health status: %s [%d]" % (status , error_code ))
339
- if status == _HEALTH_STATUSES [2 ]:
340
- self .log (
341
- "warning" ,
342
- "Trying to reset sensor due to the error. "
343
- "Error code: %d" % (error_code ),
344
- )
345
- self .reset ()
346
- status , error_code = self .health
347
- if status == _HEALTH_STATUSES [2 ]:
348
- raise RPLidarException (
349
- "RPLidar hardware failure. " "Error code: %d" % error_code
350
- )
351
- elif status == _HEALTH_STATUSES [1 ]:
352
- self .log (
353
- "warning" ,
354
- "Warning sensor status detected! " "Error code: %d" % (error_code ),
355
- )
356
- cmd = SCAN_BYTE
357
- self ._send_cmd (cmd )
358
- dsize , is_single , dtype = self ._read_descriptor ()
359
- if dsize != 5 :
360
- raise RPLidarException ("Wrong info reply length" )
361
- if is_single :
362
- raise RPLidarException ("Not a multiple response mode" )
363
- if dtype != SCAN_TYPE :
364
- raise RPLidarException ("Wrong response data type" )
418
+ if not self .scanning :
419
+ self .start (scan_type )
420
+
365
421
while True :
366
- raw = self ._read_response (dsize )
367
- self .log_bytes ("debug" , "Received scan response: " , raw )
422
+ dsize = self .descriptor_size
368
423
if max_buf_meas :
369
424
data_in_buf = self ._serial_port .in_waiting
370
425
if data_in_buf > max_buf_meas * dsize :
@@ -374,7 +429,50 @@ def iter_measurements(self, max_buf_meas=500):
374
429
"Clearing buffer..." % (data_in_buf // dsize , max_buf_meas ),
375
430
)
376
431
self ._serial_port .read (data_in_buf // dsize * dsize )
377
- yield _process_scan (raw )
432
+ if self .scan_type == SCAN_TYPE_NORMAL :
433
+ raw = self ._read_response (dsize )
434
+ self .log_bytes ("debug" , "Received scan response: " , raw )
435
+ yield _process_scan (raw )
436
+ elif self .scan_type == SCAN_TYPE_EXPRESS :
437
+ if self .express_frame == 32 :
438
+ self .express_frame = 0
439
+ if not self .express_data :
440
+ self .log ("debug" , "reading first time bytes" )
441
+ self .express_data = ExpressPacket .from_string (
442
+ self ._read_response (dsize )
443
+ )
444
+
445
+ self .express_old_data = self .express_data
446
+ self .log (
447
+ "debug" ,
448
+ "set old_data with start_angle %f"
449
+ % self .express_old_data .start_angle ,
450
+ )
451
+ self .express_data = ExpressPacket .from_string (
452
+ self ._read_response (dsize )
453
+ )
454
+ self .log (
455
+ "debug" ,
456
+ "set new_data with start_angle %f"
457
+ % self .express_data .start_angle ,
458
+ )
459
+
460
+ self .express_frame += 1
461
+ self .log (
462
+ "debug" ,
463
+ "process scan of frame %d with angle : "
464
+ "%f and angle new : %f"
465
+ % (
466
+ self .express_frame ,
467
+ self .express_old_data .start_angle ,
468
+ self .express_data .start_angle ,
469
+ ),
470
+ )
471
+ yield _process_express_scan (
472
+ self .express_old_data ,
473
+ self .express_data .start_angle ,
474
+ self .express_frame ,
475
+ )
378
476
379
477
def iter_measurments (self , max_buf_meas = 500 ):
380
478
"""For compatibility, this method wraps `iter_measurements`"""
@@ -414,3 +512,44 @@ def iter_scans(self, max_buf_meas=500, min_len=5):
414
512
scan = []
415
513
if quality > 0 and distance > 0 :
416
514
scan .append ((quality , angle , distance ))
515
+
516
+
517
+ class ExpressPacket (express_packet ):
518
+ """Class representing a Express type Packet"""
519
+
520
+ sync1 = 0xA
521
+ sync2 = 0x5
522
+ sign = {0 : 1 , 1 : - 1 }
523
+
524
+ @classmethod
525
+ def from_string (cls , data ):
526
+ """Decode and Instantiate the class from a string packet"""
527
+ packet = bytearray (data )
528
+
529
+ if (packet [0 ] >> 4 ) != cls .sync1 or (packet [1 ] >> 4 ) != cls .sync2 :
530
+ raise ValueError ("try to parse corrupted data ({})" .format (packet ))
531
+
532
+ checksum = 0
533
+ for b in packet [2 :]:
534
+ checksum ^= b
535
+ if checksum != (packet [0 ] & 0b00001111 ) + ((packet [1 ] & 0b00001111 ) << 4 ):
536
+ raise ValueError ("Invalid checksum ({})" .format (packet ))
537
+
538
+ new_scan = packet [3 ] >> 7
539
+ start_angle = (packet [2 ] + ((packet [3 ] & 0b01111111 ) << 8 )) / 64
540
+
541
+ d = a = ()
542
+ for i in range (0 , 80 , 5 ):
543
+ d += ((packet [i + 4 ] >> 2 ) + (packet [i + 5 ] << 6 ),)
544
+ a += (
545
+ ((packet [i + 8 ] & 0b00001111 ) + ((packet [i + 4 ] & 0b00000001 ) << 4 ))
546
+ / 8
547
+ * cls .sign [(packet [i + 4 ] & 0b00000010 ) >> 1 ],
548
+ )
549
+ d += ((packet [i + 6 ] >> 2 ) + (packet [i + 7 ] << 6 ),)
550
+ a += (
551
+ ((packet [i + 8 ] >> 4 ) + ((packet [i + 6 ] & 0b00000001 ) << 4 ))
552
+ / 8
553
+ * cls .sign [(packet [i + 6 ] & 0b00000010 ) >> 1 ],
554
+ )
555
+ return cls (d , a , new_scan , start_angle )
0 commit comments