Created
October 31, 2025 09:39
-
-
Save svetlyak40wt/eddee9a9f7fc9fbcb633cd729cd31e0c to your computer and use it in GitHub Desktop.
LD19 Lidar reader for Common Lisp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (uiop:define-package #:app/hardware/ld19 | |
| (:use :cl) | |
| (:import-from :cserial-port | |
| :close-serial | |
| :with-serial | |
| :read-serial-byte-vector) | |
| (:export :make-frames-reader | |
| :update-lidar-points-from-frames)) | |
| (in-package #:app/hardware/ld19) | |
| ;; -------------------- Constants -------------------- | |
| (defconstant +header+ #x54) | |
| (defconstant +verlen+ #x2C) | |
| (defconstant +frame-len+ 47) | |
| (defconstant +points-per-pack+ 12) | |
| (defconstant +bytes-per-point+ 3) | |
| ;; -------------------- Classes -------------------- | |
| (defclass ld19-point () | |
| ((distance :initarg :distance | |
| :type float | |
| :accessor point-distance) | |
| (intensity :initarg :intensity | |
| :type integer | |
| :accessor point-intensity) | |
| (angle :initarg :angle | |
| :type float | |
| :accessor point-angle))) | |
| (defmethod print-object ((frame ld19-point) stream) | |
| "Print ld19-point object showing angle, distance and intencity" | |
| (print-unreadable-object (frame stream :type t) | |
| (format stream "angle=~a distance=~A intencity=~A" | |
| (point-angle frame) | |
| (point-distance frame) | |
| (point-intensity frame)))) | |
| (defclass ld19-frame () | |
| ((lidar-speed :initarg :speed :accessor frame-lidar-speed) | |
| (start-angle :initarg :start-angle :accessor frame-start-angle) | |
| (end-angle :initarg :end-angle :accessor frame-end-angle) | |
| (timestamp :initarg :timestamp :accessor frame-timestamp) | |
| (points :initarg :points :accessor frame-points))) | |
| (defmethod print-object ((frame ld19-frame) stream) | |
| "Print ld19-frame object showing timestamp, lidar-speed, start-angle and end-angle" | |
| (print-unreadable-object (frame stream :type t) | |
| (format stream "timestamp=~a speed=~a start-angle=~a end-angle=~a" | |
| (frame-timestamp frame) | |
| (frame-lidar-speed frame) | |
| (frame-start-angle frame) | |
| (frame-end-angle frame)))) | |
| ;; -------------------- Helper Functions -------------------- | |
| (defun get-2-bytes-lsb-msb (buffer offset) | |
| "Read 2 bytes from buffer in little-endian format (LSB first, MSB second)" | |
| (logior (aref buffer offset) | |
| (ash (aref buffer (1+ offset)) 8))) | |
| (defun angle-step (start-angle end-angle) | |
| "Calculate angle step between start and end angles for 12 points" | |
| (let ((angle-diff (mod (- end-angle start-angle) 36000))) | |
| (if (> +points-per-pack+ 1) | |
| (/ angle-diff | |
| (1- +points-per-pack+)) | |
| 0))) | |
| (defun angle-from-step (start-angle step point-index) | |
| "Calculate angle for a specific point index" | |
| (/ (mod (+ start-angle | |
| (* step point-index)) | |
| 36000) | |
| 100.0)) | |
| ;; -------------------- CRC Functions -------------------- | |
| (defparameter *crc-table* | |
| #(#x00 #x4D #x9A #xD7 #x79 #x34 #xE3 #xAE #xF2 #xBF #x68 #x25 #x8B #xC6 #x11 #x5C | |
| #xA9 #xE4 #x33 #x7E #xD0 #x9D #x4A #x07 #x5B #x16 #xC1 #x8C #x22 #x6F #xB8 #xF5 | |
| #x1F #x52 #x85 #xC8 #x66 #x2B #xFC #xB1 #xED #xA0 #x77 #x3A #x94 #xD9 #x0E #x43 | |
| #xB6 #xFB #x2C #x61 #xCF #x82 #x55 #x18 #x44 #x09 #xDE #x93 #x3D #x70 #xA7 #xEA | |
| #x3E #x73 #xA4 #xE9 #x47 #x0A #xDD #x90 #xCC #x81 #x56 #x1B #xB5 #xF8 #x2F #x62 | |
| #x97 #xDA #x0D #x40 #xEE #xA3 #x74 #x39 #x65 #x28 #xFF #xB2 #x1C #x51 #x86 #xCB | |
| #x21 #x6C #xBB #xF6 #x58 #x15 #xC2 #x8F #xD3 #x9E #x49 #x04 #xAA #xE7 #x30 #x7D | |
| #x88 #xC5 #x12 #x5F #xF1 #xBC #x6B #x26 #x7A #x37 #xE0 #xAD #x03 #x4E #x99 #xD4 | |
| #x7C #x31 #xE6 #xAB #x05 #x48 #x9F #xD2 #x8E #xC3 #x14 #x59 #xF7 #xBA #x6D #x20 | |
| #xD5 #x98 #x4F #x02 #xAC #xE1 #x36 #x7B #x27 #x6A #xBD #xF0 #x5E #x13 #xC4 #x89 | |
| #x63 #x2E #xF9 #xB4 #x1A #x57 #x80 #xCD #x91 #xDC #x0B #x46 #xE8 #xA5 #x72 #x3F | |
| #xCA #x87 #x50 #x1D #xB3 #xFE #x29 #x64 #x38 #x75 #xA2 #xEF #x41 #x0C #xDB #x96 | |
| #x42 #x0F #xD8 #x95 #x3B #x76 #xA1 #xEC #xB0 #xFD #x2A #x67 #xC9 #x84 #x53 #x1E | |
| #xEB #xA6 #x71 #x3C #x92 #xDF #x08 #x45 #x19 #x54 #x83 #xCE #x60 #x2D #xFA #xB7 | |
| #x5D #x10 #xC7 #x8A #x24 #x69 #xBE #xF3 #xAF #xE2 #x35 #x78 #xD6 #x9B #x4C #x01 | |
| #xF4 #xB9 #x6E #x23 #x8D #xC0 #x17 #x5A #x06 #x4B #x9C #xD1 #x7F #x32 #xE5 #xA8)) | |
| (defun cal-crc8-from-buffer (buffer len-without-crc) | |
| "Calculate CRC8 using lookup table. Equivalent to C++ _calCRC8FromBuffer. | |
| Starts with 0xD8 (pre-calculated value for header 0x54 0x2C)." | |
| (let ((crc #xD8)) | |
| (loop for i from 0 below len-without-crc | |
| for byte = (aref buffer i) | |
| do (setf crc (aref *crc-table* (logand (logxor crc byte) #xFF)))) | |
| crc)) | |
| (defun checksum-ok (frame) | |
| "Check frame integrity using CRC8 algorithm from LD19 specification. | |
| Frame structure: [0x54 0x2C] [44 bytes data] [1 byte CRC] | |
| CRC is calculated for bytes 2-45 (indices 2 to 45 inclusive)." | |
| (when (< (length frame) +frame-len+) | |
| (return-from checksum-ok nil)) | |
| (let* ((data-buffer (subseq frame 2 46)) ; bytes 2-45 (44 bytes) | |
| (expected-crc (aref frame 46)) ; CRC byte at index 46 | |
| (calculated-crc (cal-crc8-from-buffer data-buffer 44))) | |
| (= calculated-crc expected-crc))) | |
| (defun extract-frame-from-buffer (buffer) | |
| (loop with length = (if (array-has-fill-pointer-p buffer) | |
| (fill-pointer buffer) | |
| (length buffer)) | |
| for i from 0 to (- length 2) | |
| do (when (and (= (aref buffer i) +header+) | |
| (= (aref buffer (1+ i)) +verlen+)) | |
| (when (>= (- length i) | |
| +frame-len+) | |
| (let ((frame (subseq buffer i (+ i +frame-len+)))) | |
| (when (checksum-ok frame) | |
| (return frame))))))) | |
| (defun read-frame (byte-reader) | |
| "Read LD19 frame using byte-reader function, following C++ implementation pattern" | |
| ;; Look for header (0x54 0x2C) - equivalent to "T," in C++ code | |
| (loop for byte1 = (funcall byte-reader) | |
| do (when (= byte1 +header+) | |
| (let ((byte2 (funcall byte-reader))) | |
| (when (= byte2 +verlen+) | |
| ;; Found header, now read 45 bytes of data | |
| (let ((buffer (make-array 45 :element-type '(unsigned-byte 8)))) | |
| (loop for i from 0 below 45 | |
| do (setf (aref buffer i) (funcall byte-reader))) | |
| ;; Parse frame data | |
| (let* ((lidar-speed (get-2-bytes-lsb-msb buffer 0)) | |
| (start-angle (get-2-bytes-lsb-msb buffer 2)) | |
| (points '()) | |
| (end-angle (get-2-bytes-lsb-msb buffer 40)) | |
| (timestamp (get-2-bytes-lsb-msb buffer 42)) | |
| (crc-check (aref buffer 44))) | |
| ;; Check CRC | |
| (when (= (cal-crc8-from-buffer buffer 44) crc-check) | |
| ;; Parse 12 points (equivalent to C++ LidarPoint data array) | |
| (let ((step (angle-step start-angle end-angle))) | |
| (loop for i from 0 below +points-per-pack+ | |
| for offset = (+ 4 (* i 3)) | |
| do (let* ((dist-mm (get-2-bytes-lsb-msb buffer offset)) | |
| (dist-meters (/ dist-mm | |
| 1000.0)) | |
| (intensity (aref buffer (+ offset 2))) | |
| (angle (angle-from-step start-angle step i))) | |
| (push (make-instance 'ld19-point | |
| :distance dist-meters | |
| :intensity intensity | |
| :angle angle) | |
| points))) | |
| ;; Return frame with points in correct order | |
| (return (make-instance 'ld19-frame | |
| :speed lidar-speed | |
| :start-angle start-angle | |
| :end-angle end-angle | |
| :timestamp timestamp | |
| :points (reverse points)))))))))))) | |
| (defun make-frame-reader (read-chunk-func) | |
| "Create a frame reader function that uses byte-by-byte reading" | |
| (let ((buffer nil) | |
| (pointer nil)) | |
| (labels ((next-byte () | |
| (when (or (null buffer) | |
| (= pointer (length buffer))) | |
| (setf buffer (funcall read-chunk-func) | |
| pointer 0)) | |
| (prog1 (aref buffer pointer) | |
| (incf pointer))) | |
| (frame-reader () | |
| (read-frame #'next-byte))) | |
| #'frame-reader))) | |
| (defun test-chunks-parsing (&key limit) | |
| (let ((num-chunks 0) | |
| (successes 0)) | |
| (loop with paths = (directory #P"/Users/art/projects/my/mts-true-tech-polufinal/data/raw-frames-2025-10-29/*.frame") | |
| for filename in (if limit | |
| (serapeum:take limit paths) | |
| paths) | |
| for data = (alexandria:read-file-into-byte-vector filename) | |
| for parsed = (extract-frame-from-buffer data) | |
| do (incf num-chunks) | |
| when parsed | |
| do (incf successes) | |
| finally (return (list :total num-chunks | |
| :successes successes | |
| :ratio (float (* (/ successes num-chunks) | |
| 100))))))) | |
| (define-condition no-more-chunks (error) | |
| ()) | |
| (defun test-new-frame-reader (&key (filemask #P"data/raw-frames-2025-10-29/*.frame")) | |
| "Test the new frame reader with raw frame files" | |
| (let ((num-chunks 0) | |
| (successes 0)) | |
| (flet ((make-chunk-reader () | |
| (let ((paths (directory filemask))) | |
| (flet ((read-chunk () | |
| (cond | |
| (paths | |
| (let ((buffer (alexandria:read-file-into-byte-vector (first paths)))) | |
| (incf num-chunks) | |
| (setf paths (rest paths)) | |
| ;; Я вычитывал чанки неверное и из-за этого кусок в 128 байт всегда добавлялся в начало, а буфер рос | |
| ;; Теперь надо откусить только первый кусок | |
| (subseq buffer 0 128))) | |
| (t | |
| (error 'no-more-chunks))))) | |
| #'read-chunk)))) | |
| (handler-case | |
| (loop with frame-reader = (make-frame-reader (make-chunk-reader)) | |
| for frame = (funcall frame-reader) | |
| when frame | |
| do (incf successes)) | |
| (no-more-chunks () | |
| (values (list :total num-chunks | |
| :successes successes | |
| :ratio (float (* (/ successes num-chunks) | |
| 100))))))))) | |
| (defun read-few-test-frames (&key (limit 3) | |
| (path #P"data/raw-frames-2025-10-29/*.frame") | |
| &aux (num-chunks-readed 0)) | |
| "Test the new frame reader with raw frame files" | |
| (flet ((make-chunk-reader () | |
| (let ((paths (directory path))) | |
| (flet ((read-chunk () | |
| (cond | |
| (paths | |
| (let ((buffer (alexandria:read-file-into-byte-vector (first paths)))) | |
| (incf num-chunks-readed) | |
| (setf paths (rest paths)) | |
| ;; Я вычитывал чанки неверное и из-за этого кусок в 128 байт всегда добавлялся в начало, а буфер рос | |
| ;; Теперь надо откусить только первый кусок | |
| (subseq buffer 0 128))) | |
| (t | |
| (error 'no-more-chunks))))) | |
| #'read-chunk)))) | |
| (values | |
| (uiop:while-collecting (collect-frames) | |
| (handler-case | |
| (loop with frame-reader = (make-frame-reader (make-chunk-reader)) | |
| for frame = (funcall frame-reader) | |
| for i from 0 below limit | |
| when frame | |
| do (collect-frames frame)) | |
| (no-more-chunks () | |
| (values)))) | |
| num-chunks-readed))) | |
| (defun update-lidar-points-from-frames (lidar-points frames) | |
| (unless (= (length lidar-points) | |
| 360) | |
| (error "Only lidars with 360 points are supported in this hackathon!")) | |
| (loop for frame in frames | |
| do (loop for point in (frame-points frame) | |
| for angle = (point-angle point) | |
| for index = (floor angle) | |
| do (setf (aref lidar-points index) | |
| (point-distance point))))) | |
| (defun make-lidar-points-from-frames (&key (limit 100) | |
| (path "data/raw-frames-2025-10-29-2/*.frame")) | |
| (loop with result = (make-array 360 :element-type 'float | |
| :initial-element 0.0) | |
| for frame in (read-few-test-frames :limit limit | |
| :path path) | |
| do (loop for point in (frame-points frame) | |
| for angle = (point-angle point) | |
| for index = (floor angle) | |
| do (setf (aref result index) | |
| (point-distance point))) | |
| finally | |
| (let ((lidar (app/models/robot:robot-lidar app/state:*robot*))) | |
| ;; (app/world:reset-world) | |
| (setf (app/models/lidar:lidar-points lidar) | |
| result) | |
| (return result)))) | |
| (defun make-frames-reader (&key (port "/dev/ttyUSB0") (baud-rate 230400) (buffer-size 128)) | |
| "Возвращает читальщик фреймов с реального устройства." | |
| (let ((connection (cserial-port:open-serial port :baud-rate baud-rate))) | |
| (flet ((read-chunk () | |
| (let ((buffer (make-array buffer-size :element-type '(unsigned-byte 8)))) | |
| (cserial-port:read-serial-byte-vector buffer connection) | |
| buffer))) | |
| (let ((frame-reader (make-frame-reader #'read-chunk))) | |
| (flet ((read-frames (n) | |
| (cond | |
| ((and (symbolp n) | |
| (eql n :stop)) | |
| (close-serial connection)) | |
| (t | |
| (loop for i from 0 below n | |
| collect (funcall frame-reader)))))) | |
| (values #'read-frames)))))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment