├── MANIFEST.in ├── .clang-format ├── src ├── scan_batcher.cpp ├── velodyne_decoder │ ├── calibrations │ │ ├── __init__.py │ │ ├── Puck Hi-Res.yml │ │ ├── VLP-16.yml │ │ ├── HDL-32E.yml │ │ ├── VLP-32A.yml │ │ ├── VLP-32B.yml │ │ ├── VLP-32C.yml │ │ ├── Alpha Prime.yml │ │ └── HDL-64E_S1-utexas.yml │ ├── __init__.py │ ├── bag.py │ ├── pcap.py │ ├── util.py │ └── hdl64e.py ├── stream_decoder.cpp ├── time_conversion.cpp ├── scan_decoder.cpp ├── types.cpp ├── telemetry_packet.cpp ├── calibration.cpp ├── calib_db.cpp └── python.cpp ├── requirements-dev.txt ├── test_package ├── CMakeLists.txt ├── example.cpp └── conanfile.py ├── vcpkg.json ├── include └── velodyne_decoder │ ├── stream_decoder.h │ ├── scan_decoder.h │ ├── config.h │ ├── time_conversion.h │ ├── scan_batcher.h │ ├── ros │ └── ros_scan_batcher.h │ ├── calibration.h │ ├── scan_batcher.inl │ ├── telemetry_packet.h │ ├── packet_decoder.h │ └── types.h ├── cmake ├── velodyne_decoder-config.cmake.in └── third_party.cmake ├── test ├── conftest.py ├── test_calib.py ├── data │ └── mock_vls128_dual.py ├── test_pcap.py └── test_ros.py ├── LICENSE ├── pyproject.toml ├── conanfile.py ├── .github └── workflows │ ├── publish.yml │ └── build.yml ├── CMakeLists.txt ├── .gitignore ├── README.md └── CHANGELOG.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include src/velodyne_decoder/calibrations/*.yml 2 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | ColumnLimit: 100 3 | TabWidth: 2 4 | IndentPPDirectives: AfterHash 5 | AlignConsecutiveAssignments: true 6 | -------------------------------------------------------------------------------- /src/scan_batcher.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Martin Valgur 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #include "velodyne_decoder/scan_batcher.h" 5 | 6 | namespace velodyne_decoder { 7 | 8 | template class ScanBatcher; 9 | 10 | } // namespace velodyne_decoder -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | --extra-index-url https://rospypi.github.io/simple/ 2 | pytest 3 | requests 4 | rosbag 5 | pathlib; python_version < '3' 6 | rospy < 1.15; python_version < '3' 7 | rosbag < 1.15; python_version < '3' 8 | numpy>=2; python_version >= "3.9" 9 | numpy<2; python_version < "3.9" 10 | -------------------------------------------------------------------------------- /test_package/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(PackageTest LANGUAGES CXX) 3 | 4 | find_package(velodyne_decoder REQUIRED) 5 | 6 | add_executable(example example.cpp) 7 | target_link_libraries(example PRIVATE velodyne_decoder::velodyne_decoder) 8 | target_compile_features(example PRIVATE cxx_std_17) 9 | -------------------------------------------------------------------------------- /test_package/example.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main() { 7 | velodyne_decoder::Config config; 8 | velodyne_decoder::StreamDecoder stream_decoder(config); 9 | } 10 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "velodyne-decoder", 3 | "description": "Decoder for raw Velodyne packet data", 4 | "version-string": "3.0.0", 5 | "license": "BSD-3-Clause", 6 | "homepage": "https://github.com/valgur/velodyne_decoder", 7 | "maintainers": [ 8 | "Martin Valgur " 9 | ], 10 | "builtin-baseline": "c9919121dde6f61c0436adda94624636e041226b", 11 | "dependencies": [ 12 | "yaml-cpp", 13 | "ms-gsl" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/velodyne_decoder/calibrations/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Martin Valgur 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | """ 5 | Except for HDL-64E, the calibration YAML files are for reference only. 6 | The equivalent data is now stored in src/calib_db.cpp instead. 7 | """ 8 | 9 | import importlib_resources 10 | 11 | import velodyne_decoder as vd 12 | 13 | 14 | def get_bundled_calibration(calib_file): 15 | """Return a Calibration object for the given calibration file. 16 | 17 | E.g. get_bundled_calibration("HDL-64E_S3-VeloView.yml"). 18 | """ 19 | path = importlib_resources.files(__package__) / calib_file 20 | with importlib_resources.as_file(path) as calib_path: 21 | return vd.Calibration.read(str(calib_path)) 22 | -------------------------------------------------------------------------------- /test_package/conanfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from conan import ConanFile 4 | from conan.tools.build import cross_building 5 | from conan.tools.cmake import CMake, cmake_layout 6 | 7 | 8 | class VelodyneDecoderTestPkg(ConanFile): 9 | test_type = "explicit" 10 | settings = "os", "compiler", "build_type", "arch" 11 | generators = "CMakeDeps", "CMakeToolchain" 12 | 13 | def requirements(self): 14 | self.requires(self.tested_reference_str) 15 | 16 | def build(self): 17 | cmake = CMake(self) 18 | cmake.configure() 19 | cmake.build() 20 | 21 | def layout(self): 22 | cmake_layout(self) 23 | 24 | def test(self): 25 | if not cross_building(self): 26 | cmd = os.path.join(self.cpp.build.bindirs[0], "example") 27 | self.run(cmd, env="conanrun") 28 | -------------------------------------------------------------------------------- /include/velodyne_decoder/stream_decoder.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Martin Valgur 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #pragma once 5 | 6 | #include "velodyne_decoder/config.h" 7 | #include "velodyne_decoder/scan_batcher.h" 8 | #include "velodyne_decoder/scan_decoder.h" 9 | #include "velodyne_decoder/types.h" 10 | 11 | #include 12 | #include 13 | 14 | namespace velodyne_decoder { 15 | 16 | class StreamDecoder { 17 | public: 18 | explicit StreamDecoder(const Config &config); 19 | 20 | std::optional> decode(const VelodynePacket &packet); 21 | 22 | /** 23 | * @brief Decodes all remaining packets in the buffer and returns the last scan, if any. 24 | */ 25 | std::optional> finish(); 26 | 27 | private: 28 | std::pair decodeCollectedPackets(); 29 | 30 | private: 31 | ScanBatcher<> scan_batcher_; 32 | ScanDecoder scan_decoder_; 33 | }; 34 | 35 | } // namespace velodyne_decoder 36 | -------------------------------------------------------------------------------- /src/velodyne_decoder/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2023, Martin Valgur 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | from enum import IntEnum 5 | 6 | # noinspection PyUnresolvedReferences 7 | from .velodyne_decoder_pylib import ( 8 | Config, 9 | ScanDecoder, 10 | StreamDecoder, 11 | Calibration, 12 | VelodynePacket, 13 | TelemetryPacket, 14 | PacketVector, 15 | Model, 16 | ReturnMode, 17 | PACKET_SIZE, 18 | TELEMETRY_PACKET_SIZE, 19 | __version__ as _pylib_version, 20 | ) 21 | 22 | from . import hdl64e 23 | from . import util 24 | from .calibrations import get_bundled_calibration 25 | from .pcap import read_pcap 26 | from .bag import read_bag 27 | 28 | 29 | class PointField(IntEnum): 30 | """Point field indices in the decoded point cloud for contiguous NumPy arrays.""" 31 | x = 0 32 | y = 1 33 | z = 2 34 | intensity = 3 35 | time = 4 36 | column = 5 37 | ring = 6 38 | return_type = 7 39 | 40 | 41 | __version__ = _pylib_version 42 | -------------------------------------------------------------------------------- /src/stream_decoder.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Martin Valgur 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #include "velodyne_decoder/stream_decoder.h" 5 | 6 | #include 7 | #include 8 | 9 | namespace velodyne_decoder { 10 | 11 | StreamDecoder::StreamDecoder(const Config &config) : scan_batcher_(config), scan_decoder_(config) {} 12 | 13 | std::optional> // 14 | StreamDecoder::decode(const VelodynePacket &packet) { 15 | bool scan_complete = scan_batcher_.push(packet); 16 | if (scan_complete) 17 | return decodeCollectedPackets(); 18 | return std::nullopt; 19 | } 20 | 21 | std::pair StreamDecoder::decodeCollectedPackets() { 22 | auto result = scan_decoder_.decode(*scan_batcher_.scanPackets()); 23 | scan_batcher_.reset(); 24 | return result; 25 | } 26 | 27 | std::optional> StreamDecoder::finish() { 28 | if (scan_batcher_.empty()) { 29 | return std::nullopt; 30 | } 31 | return decodeCollectedPackets(); 32 | } 33 | 34 | } // namespace velodyne_decoder 35 | -------------------------------------------------------------------------------- /include/velodyne_decoder/scan_decoder.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2023, Martin Valgur 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #include "velodyne_decoder/config.h" 9 | #include "velodyne_decoder/packet_decoder.h" 10 | #include "velodyne_decoder/types.h" 11 | 12 | namespace velodyne_decoder { 13 | 14 | class ScanDecoder { 15 | public: 16 | explicit ScanDecoder(const Config &config); 17 | 18 | std::pair decode(const std::vector &scan_packets); 19 | std::pair decode(const std::vector &scan_packets); 20 | 21 | /// Detected or configured model ID of the sensor 22 | [[nodiscard]] std::optional modelId() const; 23 | 24 | /// The return mode of the sensor based on the last received packet 25 | [[nodiscard]] std::optional returnMode() const; 26 | 27 | private: 28 | velodyne_decoder::PacketDecoder packet_decoder_; 29 | velodyne_decoder::PointCloud cloud_; 30 | 31 | bool timestamp_first_packet_; 32 | }; 33 | 34 | } // namespace velodyne_decoder 35 | -------------------------------------------------------------------------------- /src/time_conversion.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Matthew Pitropov, Joshua Whitley 2 | // Copyright (c) 2021-2023, Martin Valgur 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | 5 | #include "velodyne_decoder/time_conversion.h" 6 | 7 | #include 8 | #include 9 | 10 | namespace velodyne_decoder { 11 | 12 | Time resolveHourAmbiguity(const Time packet_time, const Time reference_time) { 13 | const double HOUR = 3600; 14 | const double HALF_HOUR = 1800; 15 | if (packet_time > reference_time + HALF_HOUR) 16 | return packet_time - HOUR; 17 | if (packet_time < reference_time - HALF_HOUR) 18 | return packet_time + HOUR; 19 | return packet_time; 20 | } 21 | 22 | Time getPacketTimestamp(const uint32_t usec_since_toh, const Time reference_time) { 23 | const int HOUR_TO_SEC = 3600; 24 | uint32_t cur_hour = (uint32_t)std::floor(reference_time) / HOUR_TO_SEC; 25 | Time packet_time = cur_hour * HOUR_TO_SEC + usec_since_toh * 1e-6; 26 | if (reference_time > 0) 27 | return resolveHourAmbiguity(packet_time, reference_time); 28 | return packet_time; 29 | } 30 | 31 | Time getPacketTimestamp(gsl::span pkt_data, Time reference_time) { 32 | uint32_t usec_since_toh = parseUint32(&pkt_data[PACKET_SIZE - 6]); 33 | return getPacketTimestamp(usec_since_toh, reference_time); 34 | } 35 | 36 | } // namespace velodyne_decoder 37 | -------------------------------------------------------------------------------- /src/scan_decoder.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2023, Martin Valgur 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #include "velodyne_decoder/scan_decoder.h" 5 | #include "velodyne_decoder/types.h" 6 | 7 | namespace velodyne_decoder { 8 | 9 | ScanDecoder::ScanDecoder(const Config &config) 10 | : packet_decoder_(config), timestamp_first_packet_(config.timestamp_first_packet) {} 11 | 12 | std::pair ScanDecoder::decode(const std::vector &scan_packets) { 13 | cloud_.clear(); 14 | TimePair scan_stamp = 15 | timestamp_first_packet_ ? scan_packets.front().stamp : scan_packets.back().stamp; 16 | for (const auto &packet : scan_packets) { 17 | packet_decoder_.unpack(packet, scan_stamp, cloud_); 18 | } 19 | std::pair result; 20 | result.first = scan_stamp; 21 | result.second.reserve(cloud_.capacity()); 22 | cloud_.swap(result.second); 23 | return result; 24 | } 25 | 26 | std::pair 27 | ScanDecoder::decode(const std::vector &scan_packets) { 28 | std::vector packet_views; 29 | packet_views.reserve(scan_packets.size()); 30 | for (PacketView packet : scan_packets) 31 | packet_views.emplace_back(packet); 32 | return decode(packet_views); 33 | } 34 | 35 | std::optional ScanDecoder::modelId() const { return packet_decoder_.modelId(); } 36 | 37 | std::optional ScanDecoder::returnMode() const { return packet_decoder_.returnMode(); } 38 | 39 | } // namespace velodyne_decoder 40 | -------------------------------------------------------------------------------- /cmake/velodyne_decoder-config.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | include("${CMAKE_CURRENT_LIST_DIR}/velodyne_decoder-targets.cmake") 4 | set_and_check(velodyne_decoder_INCLUDE_DIRS "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@") 5 | set_and_check(velodyne_decoder_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@") 6 | set_and_check(velodyne_decoder_LIBRARY_DIR "@PACKAGE_CMAKE_INSTALL_LIBDIR@") 7 | set(velodyne_decoder_LIBRARIES velodyne_decoder::velodyne_decoder) 8 | check_required_components(velodyne_decoder) 9 | 10 | set(velodyne_decoder_BUNDLED_THIRD_PARTY_LIBS @INSTALL_THIRD_PARTY@) 11 | if(velodyne_decoder_BUNDLED_THIRD_PARTY_LIBS) 12 | # TODO: This info could be generated with a custom Conan deployer instead 13 | # e.g. https://github.com/conan-io/conan-extensions/blob/main/extensions/deployers/runtime_zip_deploy.py 14 | add_library(velodyne_decoder_third_party INTERFACE) 15 | add_library(yaml-cpp::yaml-cpp ALIAS velodyne_decoder_third_party) 16 | add_library(Microsoft.GSL::GSL ALIAS velodyne_decoder_third_party) 17 | 18 | target_include_directories(velodyne_decoder_third_party INTERFACE "@PACKAGE_THIRD_PARTY_INCLUDEDIR@") 19 | target_link_directories(velodyne_decoder_third_party INTERFACE "@PACKAGE_THIRD_PARTY_LIBDIR@") 20 | target_link_libraries(velodyne_decoder_third_party INTERFACE yaml-cpp) 21 | target_compile_definitions(velodyne_decoder_third_party INTERFACE YAML_CPP_STATIC_DEFINE) 22 | else() 23 | include(CMakeFindDependencyMacro) 24 | find_dependency(yaml-cpp REQUIRED CONFIG) 25 | find_dependency(Microsoft.GSL REQUIRED CONFIG) 26 | endif() 27 | -------------------------------------------------------------------------------- /src/velodyne_decoder/calibrations/Puck Hi-Res.yml: -------------------------------------------------------------------------------- 1 | # converted from https://gitlab.kitware.com/LidarView/velodyneplugin/-/tree/0cddc1f92e69b7b26ffc5619aceb1c00854442e6/CalibrationsFiles 2 | # using https://wiki.ros.org/velodyne_pointcloud#gen_calibration.py 3 | distance_resolution: 0.002 4 | lasers: 5 | - laser_id: 0 6 | rot_correction: 0 7 | vert_correction: -0.1745329 8 | - laser_id: 1 9 | rot_correction: 0 10 | vert_correction: 0.01164135 11 | - laser_id: 2 12 | rot_correction: 0 13 | vert_correction: -0.1512677 14 | - laser_id: 3 15 | rot_correction: 0 16 | vert_correction: 0.03490658 17 | - laser_id: 4 18 | rot_correction: 0 19 | vert_correction: -0.127985 20 | - laser_id: 5 21 | rot_correction: 0 22 | vert_correction: 0.05817182 23 | - laser_id: 6 24 | rot_correction: 0 25 | vert_correction: -0.1047198 26 | - laser_id: 7 27 | rot_correction: 0 28 | vert_correction: 0.08145452 29 | - laser_id: 8 30 | rot_correction: 0 31 | vert_correction: -0.08145452 32 | - laser_id: 9 33 | rot_correction: 0 34 | vert_correction: 0.1047198 35 | - laser_id: 10 36 | rot_correction: 0 37 | vert_correction: -0.05817182 38 | - laser_id: 11 39 | rot_correction: 0 40 | vert_correction: 0.127985 41 | - laser_id: 12 42 | rot_correction: 0 43 | vert_correction: -0.03490658 44 | - laser_id: 13 45 | rot_correction: 0 46 | vert_correction: 0.1512677 47 | - laser_id: 14 48 | rot_correction: 0 49 | vert_correction: -0.01164135 50 | - laser_id: 15 51 | rot_correction: 0 52 | vert_correction: 0.1745329 53 | num_lasers: 16 -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2023, Martin Valgur 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | from pathlib import Path 5 | 6 | import pytest 7 | import requests 8 | 9 | # logging with rospy from rosbag hangs in Docker otherwise due to 10 | # https://github.com/ros/ros_comm/blob/842f0f02/tools/rosgraph/src/rosgraph/roslogging.py#L64-L71 11 | # going into an infinite loop for some reason. 12 | import logging 13 | logging.getLogger("rosout").setLevel(logging.CRITICAL) 14 | 15 | root_dir = Path(__file__).parent.parent 16 | data_dir = Path(__file__).parent / "data" 17 | base_url = "https://github.com/valgur/velodyne_decoder/releases/download/v1.0.1/" 18 | 19 | 20 | def fetch(name): 21 | url = base_url + name 22 | if not data_dir.exists(): 23 | data_dir.mkdir() 24 | path = data_dir / name 25 | if not path.exists(): 26 | with path.open("wb") as f: 27 | f.write(requests.get(url).content) 28 | return path 29 | 30 | 31 | @pytest.fixture 32 | def sample_pcap_path(): 33 | return fetch("vlp16.pcap") 34 | 35 | 36 | @pytest.fixture 37 | def sample_bag_path(): 38 | return fetch("vlp16.bag") 39 | 40 | 41 | @pytest.fixture(scope="module") 42 | def pcl_struct_dtype(): 43 | return { 44 | "names": ["x", "y", "z", "intensity", "time", "column", "ring", "return_type"], 45 | "formats": [" 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "velodyne_decoder/calibration.h" 13 | #include "velodyne_decoder/types.h" 14 | 15 | namespace velodyne_decoder { 16 | 17 | struct Config { 18 | // PacketDecoder params 19 | float min_range = 0.1; ///< minimum range to publish (m) 20 | float max_range = 250; ///< maximum range to publish (m) 21 | float min_angle = 0; ///< minimum angle to publish (deg) 22 | float max_angle = 360; ///< maximum angle to publish (deg) 23 | /// model ID, optional for most models (exceptions: HDL-64E, VLP-32A, VLP-32B) 24 | std::optional model; 25 | /// calibration info, optional 26 | std::optional calibration; 27 | 28 | // ScanBatcher params 29 | /// Azimuth at which to start a new scan (deg). 30 | /// If unset, the scan is split whenever it covers >= 360 deg at an arbitrary azimuth. 31 | std::optional cut_angle = std::nullopt; 32 | /// whether we are timestamping based on the first or last packet in the scan 33 | bool timestamp_first_packet = false; 34 | 35 | static const std::vector SUPPORTED_MODELS; 36 | }; 37 | 38 | inline const std::vector Config::SUPPORTED_MODELS = // 39 | {ModelId::HDL64E_S1, ModelId::HDL64E_S2, ModelId::HDL64E_S3, ModelId::HDL32E, 40 | ModelId::VLP32A, ModelId::VLP32B, ModelId::VLP32C, ModelId::VLP16, 41 | ModelId::PuckHiRes, ModelId::AlphaPrime}; 42 | 43 | } // namespace velodyne_decoder 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Martin Valgur 4 | Copyright (c) 2007-2021 Austin Robot Technology, and others 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /src/types.cpp: -------------------------------------------------------------------------------- 1 | #include "velodyne_decoder/types.h" 2 | #include "velodyne_decoder/time_conversion.h" 3 | 4 | #include 5 | 6 | namespace velodyne_decoder { 7 | 8 | TimePair::TimePair(Time host, Time device) : host(host), device(device) {} 9 | 10 | TimePair::TimePair(Time host_stamp, gsl::span data) 11 | : host(host_stamp), device(getPacketTimestamp(data, host_stamp)) {} 12 | 13 | VelodynePacket::VelodynePacket(TimePair stamp, gsl::span data) 14 | : stamp(stamp), data() { 15 | std::copy(data.begin(), data.end(), this->data.begin()); 16 | } 17 | 18 | VelodynePacket::VelodynePacket(Time host_stamp, gsl::span data) 19 | : VelodynePacket(TimePair(host_stamp, data), data) {} 20 | 21 | PacketView::PacketView(TimePair stamp, gsl::span data) 22 | : stamp(stamp), data(data) {} 23 | 24 | PacketView::PacketView(Time host_stamp, gsl::span data) 25 | : stamp(host_stamp, data), data(data) {} 26 | 27 | PacketView::PacketView(const VelodynePacket &packet) : stamp(packet.stamp), data(packet.data) {} 28 | 29 | VelodynePoint::VelodynePoint(float x, float y, float z, float intensity, float time, 30 | uint16_t column, uint8_t ring, ReturnMode return_type) 31 | : x(x), y(y), z(z), intensity(intensity), time(time), column(column), ring(ring), 32 | return_type(return_type) {} 33 | 34 | ReturnMode operator|(ReturnMode lhs, ReturnMode rhs) { 35 | return static_cast(static_cast(lhs) | static_cast(rhs)); 36 | } 37 | 38 | ReturnMode operator&(ReturnMode lhs, ReturnMode rhs) { 39 | return static_cast(static_cast(lhs) & static_cast(rhs)); 40 | } 41 | 42 | } // namespace velodyne_decoder -------------------------------------------------------------------------------- /include/velodyne_decoder/time_conversion.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Matthew Pitropov, Joshua Whitley 2 | // Copyright (c) 2021-2023, Martin Valgur 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | 5 | #pragma once 6 | 7 | #include "velodyne_decoder/types.h" 8 | 9 | #include 10 | #include 11 | 12 | namespace velodyne_decoder { 13 | 14 | constexpr uint32_t parseUint32(const uint8_t *data) { 15 | return (((uint32_t)data[3]) << 24u | // 16 | ((uint32_t)data[2]) << 16u | // 17 | ((uint32_t)data[1]) << 8u | // 18 | ((uint32_t)data[0])); 19 | } 20 | 21 | /** @brief Function used to check that hour assigned to timestamp in conversion is 22 | * correct. Velodyne only returns time since the top of the hour, so if the computer clock 23 | * and the velodyne clock (gps-synchronized) are a little off, there is a chance the wrong 24 | * hour may be associated with the timestamp 25 | * 26 | * @param packet_time timestamp recovered from velodyne 27 | * @param reference_time time coming from computer's clock 28 | * @return timestamp from velodyne, possibly shifted by 1 hour if the function arguments 29 | * disagree by more than a half-hour. 30 | */ 31 | Time resolveHourAmbiguity(Time packet_time, Time reference_time); 32 | 33 | /** 34 | * @brief Merges the the top of the hour timestamp from the Velodyne lidar with a 35 | * reference timestamp, i.e. time from the computer's clock or a GPS receiver. 36 | * 37 | * @param usec_since_toh number of microseconds from the top of the hour 38 | * @param reference_time reference time as a timestamp in seconds since the Unix epoch 39 | * @return timestamp in seconds since the Unix epoch 40 | */ 41 | Time getPacketTimestamp(uint32_t usec_since_toh, Time reference_time); 42 | 43 | Time getPacketTimestamp(gsl::span pkt_data, Time reference_time); 44 | 45 | } // namespace velodyne_decoder 46 | -------------------------------------------------------------------------------- /include/velodyne_decoder/scan_batcher.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Martin Valgur 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #pragma once 5 | 6 | #include "velodyne_decoder/config.h" 7 | #include "velodyne_decoder/types.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace velodyne_decoder { 15 | 16 | template class ScanBatcher { 17 | public: 18 | explicit inline ScanBatcher(const Config &config); 19 | 20 | /** @brief Pushes a packet into the batcher. 21 | * @return true if the scan is complete, false otherwise. 22 | */ 23 | inline bool push(const PacketT &packet); 24 | 25 | [[nodiscard]] inline bool empty() const; 26 | 27 | [[nodiscard]] inline size_t size() const; 28 | 29 | /** @brief True if the current scan is complete. 30 | */ 31 | [[nodiscard]] inline bool scanComplete() const; 32 | 33 | /** @brief The contents of the current scan. 34 | */ 35 | [[nodiscard]] inline const std::shared_ptr> &scanPackets() const; 36 | 37 | inline void reset(std::shared_ptr> scan_packets); 38 | 39 | inline void reset(); 40 | 41 | private: 42 | std::shared_ptr> scan_packets_ = std::make_shared>(); 43 | int initial_azimuth_ = -1; 44 | int coverage_ = 0; 45 | bool scan_complete_ = false; 46 | std::optional kept_last_packet_ = std::nullopt; 47 | 48 | // config 49 | const bool is_device_time_valid_; 50 | const int cut_angle_; 51 | const double duration_threshold_ = 0.3; // max scan duration at ~4 Hz 52 | }; 53 | 54 | inline PacketView toPacketView(const VelodynePacket &packet) { return {packet}; } 55 | 56 | extern template class ScanBatcher; 57 | 58 | } // namespace velodyne_decoder 59 | 60 | #include "scan_batcher.inl" 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "velodyne-decoder" 3 | version = "3.1.0" 4 | description = "Decoder for raw Velodyne packet data" 5 | readme = "README.md" 6 | requires-python = ">= 3.8" 7 | authors = [{name = "Martin Valgur", email = "martin.valgur@gmail.com"}] 8 | urls."Homepage" = "https://github.com/valgur/velodyne_decoder" 9 | urls."Bug Reports" = "https://github.com/valgur/velodyne_decoder/issues" 10 | urls."Source" = "https://github.com/valgur/velodyne_decoder" 11 | keywords = [ 12 | "Velodyne", 13 | "pointcloud", 14 | "PCL", 15 | ] 16 | license = {text = "BSD-3-Clause"} 17 | classifiers = [ 18 | "License :: OSI Approved :: BSD License", 19 | "Programming Language :: Python :: 3", 20 | "Operating System :: OS Independent", 21 | ] 22 | dependencies = [ 23 | "numpy", 24 | "importlib_resources", 25 | "dpkt", 26 | "pyyaml", 27 | ] 28 | 29 | [project.scripts] 30 | extract-hdl64e-calibration = "velodyne_decoder.hdl64e:cli" 31 | 32 | [tool.setuptools] 33 | zip-safe = false 34 | include-package-data = true 35 | 36 | [build-system] 37 | requires = [ 38 | "scikit-build-core >=0.4.3", 39 | "conan >=2.0.5", 40 | "nanobind" 41 | ] 42 | build-backend = "scikit_build_core.build" 43 | 44 | [tool.scikit-build] 45 | cmake.minimum-version = "3.15" 46 | cmake.targets = ["python_bindings"] 47 | install.components = ["python"] 48 | wheel.license-files = ["LICENSE"] 49 | build-dir = "build/{wheel_tag}" 50 | # Build stable ABI wheels for CPython 3.12+ 51 | wheel.py-api = "cp312" 52 | 53 | [tool.scikit-build.cmake.define] 54 | BUILD_PYTHON = true 55 | BUILD_SHARED_LIBS = false 56 | 57 | [tool.cibuildwheel] 58 | build = "*" 59 | # Disable building of PyPy wheels on all platforms 60 | skip = "pp*" 61 | before-test = "pip install -r {project}/requirements-dev.txt" 62 | test-command = "pytest {project}/test --color=yes -v" 63 | build-verbosity = 1 64 | archs = "auto64" 65 | 66 | # Needed for full C++17 support 67 | [tool.cibuildwheel.macos.environment] 68 | MACOSX_DEPLOYMENT_TARGET = "10.15" 69 | -------------------------------------------------------------------------------- /test/test_calib.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2023, Martin Valgur 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import math 5 | 6 | import pytest 7 | import velodyne_decoder as vd 8 | import yaml 9 | 10 | MODELS = list(vd.Model.__members__) 11 | 12 | 13 | def test_config_constructor(calib_data_dir): 14 | vd.Config() 15 | vd.Config(model=vd.Model.VLP16) 16 | calibration = vd.Calibration.read(str(calib_data_dir / "VLP-16.yml")) 17 | vd.Config(model=vd.Model.VLP16, calibration=calibration) 18 | 19 | 20 | @pytest.mark.parametrize("model_id", MODELS) 21 | def test_bundled_calibrations(model_id): 22 | model_id = vd.Model.__members__[model_id] 23 | calib = None 24 | if model_id.name.startswith("HDL64E"): 25 | calib_file = { 26 | vd.Model.HDL64E_S1: "HDL-64E_S1-utexas.yml", 27 | vd.Model.HDL64E_S2: "HDL-64E_S2.1-sztaki.yml", 28 | vd.Model.HDL64E_S3: "HDL-64E_S3-VeloView.yml", 29 | }[model_id] 30 | calib = vd.get_bundled_calibration(calib_file) 31 | vd.ScanDecoder(vd.Config(model=model_id, calibration=calib)) 32 | 33 | 34 | @pytest.mark.parametrize("model_id", [m for m in MODELS if not m.startswith("HDL64E")]) 35 | def test_default_calib_vertical_offsets(model_id): 36 | """Check that the pre-defined vertical offset calibration values are sensible.""" 37 | model_id = vd.Model.__members__[model_id] 38 | calib = vd.Calibration.default_calibs[model_id] 39 | lasers = yaml.safe_load(calib.to_string())["lasers"] 40 | focal_distance = { 41 | vd.Model.AlphaPrime: 58.63e-3, 42 | vd.Model.HDL32E: 28.83e-3, 43 | vd.Model.PuckHiRes: 41.91e-3, 44 | vd.Model.VLP16: 41.91e-3, 45 | vd.Model.VLP32A: 42.4e-3, 46 | vd.Model.VLP32B: 42.4e-3, 47 | vd.Model.VLP32C: 42.4e-3, 48 | }[model_id] 49 | for laser in lasers: 50 | approx_offset = focal_distance * math.tan(-laser["vert_correction"]) 51 | thr = 0.5e-3 if model_id == vd.Model.PuckHiRes else 0.1e-3 52 | assert pytest.approx(approx_offset, abs=thr) == laser["vert_offset_correction"] 53 | -------------------------------------------------------------------------------- /src/velodyne_decoder/bag.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2023, Martin Valgur 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import sys 5 | from collections import namedtuple 6 | 7 | import velodyne_decoder as vd 8 | 9 | is_py2 = sys.version_info[0] == 2 10 | 11 | 12 | def read_bag( 13 | bag_file, 14 | config=None, 15 | topics=None, 16 | as_pcl_structs=False, 17 | time_range=(None, None), 18 | ): 19 | """Decodes and yields all point clouds stored in a ROS bag file. 20 | 21 | `model` parameter must be set in the provided config. 22 | 23 | Parameters 24 | ---------- 25 | bag_file : path or file handle 26 | config : Config, optional 27 | topics : str or list of str 28 | as_pcl_structs : bool 29 | If False, the returned NumPy arrays will be a contiguous array of floats (default). 30 | If True, the returned NumPy arrays will contain PCL-compatible structs with dtype 31 | {'names': ['x', 'y', 'z', 'intensity', 'ring', 'time'], 32 | 'formats': [' 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace velodyne_decoder { 15 | inline PacketView toPacketView(const velodyne_msgs::VelodynePacket &packet_msg) { 16 | return {packet_msg.stamp.toSec(), gsl::span{packet_msg.data}}; 17 | } 18 | } // namespace velodyne_decoder 19 | 20 | #include 21 | #include 22 | 23 | namespace velodyne_decoder { 24 | 25 | class RosScanBatcher { 26 | public: 27 | using Base = ScanBatcher; 28 | 29 | inline explicit RosScanBatcher(const Config &config, const std::string frame_id) 30 | : frame_id_(frame_id), timestamp_first_packet_(config.timestamp_first_packet), 31 | scan_batcher_(config) { 32 | reset(); 33 | } 34 | 35 | inline void reset() { 36 | scan_msg_ = boost::make_shared(); 37 | scan_batcher_.reset({&scan_msg_->packets, [](auto *) {}}); 38 | } 39 | 40 | inline bool push(const velodyne_msgs::VelodynePacket &packet) { 41 | return scan_batcher_.push(packet); 42 | } 43 | 44 | [[nodiscard]] inline bool scanComplete() const { return scan_batcher_.scanComplete(); } 45 | 46 | [[nodiscard]] inline const boost::shared_ptr &scanMsg() const { 47 | scan_msg_->header.frame_id = frame_id_; 48 | scan_msg_->header.stamp = timestamp_first_packet_ ? scan_msg_->packets.front().stamp 49 | : scan_msg_->packets.back().stamp; 50 | return scan_msg_; 51 | } 52 | 53 | private: 54 | std::string frame_id_; 55 | bool timestamp_first_packet_; 56 | ScanBatcher scan_batcher_; 57 | boost::shared_ptr scan_msg_; 58 | }; 59 | 60 | } // namespace velodyne_decoder 61 | -------------------------------------------------------------------------------- /test/data/mock_vls128_dual.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2023, Martin Valgur 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | """ 5 | Convert a single-return VLS-128 pcap file into a mock dual-return VLS-128 pcap file. 6 | All returns are both strongest and last. 7 | """ 8 | import struct 9 | from pathlib import Path 10 | 11 | import dpkt 12 | from tqdm.auto import tqdm 13 | 14 | pcap_path = Path(__file__).parent / "VLS-128_single/VLS-128_single.pcap" 15 | output_path = Path(__file__).parent / "VLS-128_dual/VLS-128_dual.pcap" 16 | 17 | PACKET_SIZE = 1206 18 | 19 | FIRING_DURATION = (8 / 3) * 20 * 1e-6 20 | 21 | with pcap_path.open("rb") as fin, output_path.open("wb") as fout: 22 | pcap_out = dpkt.pcap.Writer(fout) 23 | dummy_block = bytearray(100) 24 | packet_count = sum(1 for _ in dpkt.pcap.Reader(fin)) 25 | fin.seek(0) 26 | for pcap_stamp, buf in tqdm(dpkt.pcap.Reader(fin), total=packet_count): 27 | eth = dpkt.ethernet.Ethernet(buf) 28 | try: 29 | data = dpkt.ethernet.Ethernet(buf) 30 | except dpkt.dpkt.UnpackError: 31 | continue 32 | while hasattr(data, "data"): 33 | data = data.data 34 | if len(data) != PACKET_SIZE: 35 | pcap_out.writepkt(buf, stamp) 36 | continue 37 | # Parse 38 | blocks = [data[i * 100 : (i + 1) * 100] for i in range(12)] 39 | stamp, dual_return_mode, model_id = struct.unpack("= t0 40 | assert stamp.device >= t0 41 | 42 | 43 | def _struct_pcd_to_contiguous(pcd_struct): 44 | shape = (len(pcd_struct), len(pcd_struct.dtype.fields)) 45 | converted_pcd = np.empty(shape, dtype=np.float32) 46 | for i, field in enumerate(pcd_struct.dtype.fields): 47 | converted_pcd[:, i] = pcd_struct[field] 48 | return converted_pcd 49 | 50 | 51 | def test_pcap_structs_match_contiguous(sample_pcap_path): 52 | """Check that the contents of as_pcl_structs=True/False match.""" 53 | pcds_struct = list(vd.read_pcap(sample_pcap_path, as_pcl_structs=True)) 54 | pcds_contiguous = list(vd.read_pcap(sample_pcap_path, as_pcl_structs=False)) 55 | assert len(pcds_struct) == len(pcds_contiguous) 56 | for (stamp_struct, pcd_struct), (stamp_contiguous, pcd_contiguous) in zip(pcds_struct, pcds_contiguous): 57 | assert stamp_struct.host == stamp_contiguous.host 58 | assert stamp_struct.device == stamp_contiguous.device 59 | cast_struct_pcd = _struct_pcd_to_contiguous(pcd_struct) 60 | assert np.allclose(cast_struct_pcd, pcd_contiguous, rtol=1e-6) 61 | -------------------------------------------------------------------------------- /include/velodyne_decoder/calibration.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, 2019, Austin Robot Technology, Piyush Khandelwal, Joshua Whitley 2 | // Copyright (c) 2021-2023, Martin Valgur 3 | // SPDX-License-Identifier: BSD-3-Clause 4 | 5 | #pragma once 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "velodyne_decoder/types.h" 14 | 15 | namespace velodyne_decoder { 16 | 17 | /** \brief correction values for a single laser 18 | * 19 | * Correction values for a single laser (as provided by db.xml from 20 | * Velodyne). Includes parameters for Velodyne HDL-64E S2.1. 21 | * 22 | * http://velodynelidar.com/lidar/products/manual/63-HDL64E%20S2%20Manual_Rev%20D_2011_web.pdf 23 | */ 24 | 25 | /** \brief Correction information for a single laser. */ 26 | struct LaserCorrection { 27 | /** parameters in db.xml */ 28 | float rot_correction = 0; 29 | float vert_correction = 0; 30 | float dist_correction = 0; 31 | bool two_pt_correction_available = false; 32 | float dist_correction_x = 0; 33 | float dist_correction_y = 0; 34 | float vert_offset_correction = 0; 35 | float horiz_offset_correction = 0; 36 | int max_intensity = 255; 37 | int min_intensity = 0; 38 | float focal_distance = 0; 39 | float focal_slope = 0; 40 | 41 | uint16_t laser_idx = -1; ///< index of the laser in the measurements block of a packet 42 | uint16_t laser_ring = -1; ///< ring number for this laser 43 | }; 44 | 45 | /** \brief Calibration information for the entire device. */ 46 | class Calibration { 47 | public: 48 | float distance_resolution_m = 0.002f; 49 | std::vector laser_corrections; 50 | int num_lasers = 0; 51 | 52 | public: 53 | Calibration() = default; 54 | explicit Calibration(const std::string &calibration_file); 55 | Calibration(std::vector laser_corrs, float distance_resolution_m); 56 | 57 | bool isAdvancedCalibration() const; 58 | 59 | static Calibration read(const std::string &calibration_file); 60 | void write(const std::string &calibration_file) const; 61 | 62 | static Calibration fromString(const std::string &calibration_content); 63 | std::string toString() const; 64 | 65 | private: 66 | void assignRingNumbers(); 67 | }; 68 | 69 | class CalibDB { 70 | public: 71 | CalibDB(); 72 | 73 | Calibration getDefaultCalibration(ModelId model_id) const; 74 | std::vector getAvailableModels() const; 75 | 76 | const std::unordered_map &getAllDefaultCalibrations() const { 77 | return calibrations_; 78 | }; 79 | 80 | private: 81 | std::unordered_map calibrations_; 82 | }; 83 | 84 | } // namespace velodyne_decoder 85 | -------------------------------------------------------------------------------- /test/test_ros.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2023, Martin Valgur 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | import numpy as np 4 | import velodyne_decoder as vd 5 | 6 | 7 | def test_bag_as_contiguous_array(sample_bag_path): 8 | pcds = list(vd.read_bag(sample_bag_path, topics="/velodyne_packets", as_pcl_structs=False)) 9 | assert len(pcds) == 13 10 | stamp, pcd, topic, frame_id = pcds[0] 11 | assert topic == "/velodyne_packets" 12 | assert stamp.host == 1636622716.742135 13 | assert pcd.shape == (27300, 8) 14 | assert pcd.dtype.name == "float32" 15 | assert frame_id == "velodyne" 16 | # Sanity check that the point clouds are actually distinct 17 | assert not all(len(x) == len(pcd) for _, x, _, _ in pcds) 18 | 19 | 20 | def test_bag_as_struct_array(sample_bag_path, pcl_struct_dtype): 21 | pcds = list(vd.read_bag(sample_bag_path, topics="/velodyne_packets", as_pcl_structs=True)) 22 | assert len(pcds) == 13 23 | stamp, pcd, topic, frame_id = pcds[0] 24 | assert topic == "/velodyne_packets" 25 | assert stamp.host == 1636622716.742135 26 | assert pcd.shape == (27300,) 27 | assert pcd.dtype == pcl_struct_dtype 28 | assert frame_id == "velodyne" 29 | assert not all(len(x) == len(pcd) for _, x, _, _ in pcds) 30 | 31 | 32 | def test_bag_automatic_topic(sample_bag_path): 33 | pcds = list(vd.read_bag(sample_bag_path)) 34 | assert len(pcds) == 13 35 | stamp, pcd, topic, frame_id = pcds[0] 36 | assert topic == "/velodyne_packets" 37 | assert stamp.host == 1636622716.742135 38 | assert pcd.shape == (27300, 8) 39 | assert frame_id == "velodyne" 40 | 41 | 42 | def test_bag_time_range(sample_bag_path): 43 | t0 = 1636622717 44 | pcds = list(vd.read_bag(sample_bag_path, time_range=(t0, None))) 45 | assert pcds[0].stamp.host >= t0 46 | 47 | 48 | def _struct_pcd_to_contiguous(pcd_struct): 49 | shape = (len(pcd_struct), len(pcd_struct.dtype.fields)) 50 | converted_pcd = np.empty(shape, dtype=np.float32) 51 | for i, field in enumerate(pcd_struct.dtype.fields): 52 | converted_pcd[:, i] = pcd_struct[field] 53 | return converted_pcd 54 | 55 | 56 | def test_pcap_structs_match_contiguous(sample_bag_path): 57 | """Check that the contents of as_pcl_structs=True/False match.""" 58 | pcds_struct = list(vd.read_bag(sample_bag_path, topics="/velodyne_packets", as_pcl_structs=True)) 59 | pcds_contiguous = list(vd.read_bag(sample_bag_path, topics="/velodyne_packets", as_pcl_structs=False)) 60 | assert len(pcds_struct) == len(pcds_contiguous) 61 | for (stamp_struct, pcd_struct, _, _), (stamp_contiguous, pcd_contiguous, _, _) in zip(pcds_struct, pcds_contiguous): 62 | assert stamp_struct.host == stamp_contiguous.host 63 | assert stamp_struct.device == stamp_contiguous.device 64 | cast_struct_pcd = _struct_pcd_to_contiguous(pcd_struct) 65 | assert np.allclose(cast_struct_pcd, pcd_contiguous, rtol=1e-6) 66 | -------------------------------------------------------------------------------- /src/velodyne_decoder/calibrations/HDL-32E.yml: -------------------------------------------------------------------------------- 1 | # converted from https://gitlab.kitware.com/LidarView/velodyneplugin/-/tree/0cddc1f92e69b7b26ffc5619aceb1c00854442e6/CalibrationsFiles 2 | # using https://wiki.ros.org/velodyne_pointcloud#gen_calibration.py 3 | distance_resolution: 0.002 4 | lasers: 5 | - laser_id: 0 6 | rot_correction: 0 7 | vert_correction: -0.5352925 8 | - laser_id: 1 9 | rot_correction: 0 10 | vert_correction: -0.1628392 11 | - laser_id: 2 12 | rot_correction: 0 13 | vert_correction: -0.5119051 14 | - laser_id: 3 15 | rot_correction: 0 16 | vert_correction: -0.1396263 17 | - laser_id: 4 18 | rot_correction: 0 19 | vert_correction: -0.4886922 20 | - laser_id: 5 21 | rot_correction: 0 22 | vert_correction: -0.1164135 23 | - laser_id: 6 24 | rot_correction: 0 25 | vert_correction: -0.4654793 26 | - laser_id: 7 27 | rot_correction: 0 28 | vert_correction: -0.09302605 29 | - laser_id: 8 30 | rot_correction: 0 31 | vert_correction: -0.4420919 32 | - laser_id: 9 33 | rot_correction: 0 34 | vert_correction: -0.06981317 35 | - laser_id: 10 36 | rot_correction: 0 37 | vert_correction: -0.418879 38 | - laser_id: 11 39 | rot_correction: 0 40 | vert_correction: -0.04660029 41 | - laser_id: 12 42 | rot_correction: 0 43 | vert_correction: -0.3956662 44 | - laser_id: 13 45 | rot_correction: 0 46 | vert_correction: -0.02321288 47 | - laser_id: 14 48 | rot_correction: 0 49 | vert_correction: -0.3722787 50 | - laser_id: 15 51 | rot_correction: 0 52 | vert_correction: 0 53 | - laser_id: 16 54 | rot_correction: 0 55 | vert_correction: -0.3490658 56 | - laser_id: 17 57 | rot_correction: 0 58 | vert_correction: 0.02321288 59 | - laser_id: 18 60 | rot_correction: 0 61 | vert_correction: -0.325853 62 | - laser_id: 19 63 | rot_correction: 0 64 | vert_correction: 0.04660029 65 | - laser_id: 20 66 | rot_correction: 0 67 | vert_correction: -0.3024656 68 | - laser_id: 21 69 | rot_correction: 0 70 | vert_correction: 0.06981317 71 | - laser_id: 22 72 | rot_correction: 0 73 | vert_correction: -0.2792527 74 | - laser_id: 23 75 | rot_correction: 0 76 | vert_correction: 0.09302605 77 | - laser_id: 24 78 | rot_correction: 0 79 | vert_correction: -0.2560398 80 | - laser_id: 25 81 | rot_correction: 0 82 | vert_correction: 0.1164135 83 | - laser_id: 26 84 | rot_correction: 0 85 | vert_correction: -0.2326524 86 | - laser_id: 27 87 | rot_correction: 0 88 | vert_correction: 0.1396263 89 | - laser_id: 28 90 | rot_correction: 0 91 | vert_correction: -0.2094395 92 | - laser_id: 29 93 | rot_correction: 0 94 | vert_correction: 0.1628392 95 | - laser_id: 30 96 | rot_correction: 0 97 | vert_correction: -0.1862266 98 | - laser_id: 31 99 | rot_correction: 0 100 | vert_correction: 0.1862266 101 | num_lasers: 32 -------------------------------------------------------------------------------- /src/velodyne_decoder/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021-2023, Martin Valgur 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import struct 5 | from contextlib import contextmanager 6 | import warnings 7 | 8 | import dpkt 9 | import numpy as np 10 | 11 | 12 | def iter_pcap(pcap_file, time_range=(None, None)): 13 | start_time, end_time = time_range 14 | with _fopen(pcap_file, "rb") as f: 15 | for stamp, buf in dpkt.pcap.Reader(f): 16 | if start_time is not None and stamp < start_time: 17 | continue 18 | if end_time is not None and stamp > end_time: 19 | break 20 | # Get the innermost layer of the packet 21 | # typically Ethernet -> IP -> UDP -> raw Velodyne data 22 | try: 23 | data = dpkt.ethernet.Ethernet(buf) 24 | except dpkt.dpkt.UnpackError: 25 | continue 26 | while hasattr(data, "data"): 27 | data = data.data 28 | yield stamp, data 29 | 30 | 31 | def iter_bag(bag_file, topics=None, default_msg_types=None, time_range=(None, None)): 32 | with warnings.catch_warnings(): 33 | # Suppress an irrelevant deprecation warning from the Cryptodome package due to 'import imp' 34 | warnings.filterwarnings("ignore", category=DeprecationWarning) 35 | from rosbag import Bag 36 | from rospy import Time 37 | 38 | if isinstance(bag_file, Bag): 39 | bag = bag_file 40 | else: 41 | bag = Bag(str(bag_file)) 42 | 43 | # Find relevant topics automatically, if not specified 44 | if topics is None and default_msg_types is not None: 45 | topics = [] 46 | for topic, info in bag.get_type_and_topic_info()[1].items(): 47 | if info.msg_type in default_msg_types: 48 | topics.append(topic) 49 | if not topics: 50 | return 51 | 52 | start_time, end_time = time_range 53 | if start_time is not None and not isinstance(start_time, Time): 54 | start_time = Time.from_sec(start_time) 55 | if end_time is not None and not isinstance(end_time, Time): 56 | end_time = Time.from_sec(end_time) 57 | 58 | try: 59 | for topic, msg, ros_time in bag.read_messages(topics, start_time, end_time): 60 | yield topic, msg, ros_time 61 | finally: 62 | if not isinstance(bag_file, Bag): 63 | bag.close() 64 | 65 | 66 | @contextmanager 67 | def _fopen(filein, *args, **kwargs): 68 | if isinstance(filein, str): # filename 69 | with open(filein, *args, **kwargs) as f: 70 | yield f 71 | elif hasattr(filein, "open"): # pathlib.Path 72 | with filein.open(*args, **kwargs) as f: 73 | yield f 74 | else: # file-like object 75 | yield filein 76 | 77 | 78 | def parse_packet(data): 79 | stamp, dual_return_mode, model_id = struct.unpack(" 10 | #include 11 | #include 12 | #include 13 | 14 | namespace velodyne_decoder { 15 | 16 | inline int getAzimuth(const PacketView &packet) { 17 | // get the azimuth of the last block 18 | uint16_t azimuth = 0; 19 | memcpy(&azimuth, &packet.data[(BLOCKS_PER_PACKET - 1) * SIZE_BLOCK + 2], sizeof(azimuth)); 20 | return (int)azimuth; 21 | } 22 | 23 | template 24 | ScanBatcher::ScanBatcher(const Config &config) 25 | : is_device_time_valid_(!(config.model.has_value() && config.model == ModelId::HDL64E_S1)), 26 | cut_angle_(!config.cut_angle || config.cut_angle < 0 27 | ? -1 28 | : (int)(std::lround(*config.cut_angle * 100)) % 36000) { 29 | if (cut_angle_ >= 0) { 30 | initial_azimuth_ = cut_angle_; 31 | } 32 | } 33 | 34 | template bool ScanBatcher::push(const T &packet) { 35 | PacketView packet_view = toPacketView(packet); 36 | TimePair stamp = packet_view.stamp; 37 | if (!is_device_time_valid_) { 38 | // HDL-64E S1 does not contain device time info in its packets. 39 | stamp.device = stamp.host; 40 | } 41 | 42 | if (scan_complete_) { 43 | reset(); 44 | } 45 | 46 | int azimuth = getAzimuth(packet_view); 47 | if (initial_azimuth_ < 0) { 48 | initial_azimuth_ = azimuth; 49 | } 50 | 51 | double duration = empty() ? 0.0 : stamp.host - toPacketView(scan_packets_->front()).stamp.host; 52 | if (duration > duration_threshold_) { 53 | kept_last_packet_ = packet; 54 | scan_complete_ = true; 55 | return true; 56 | } 57 | 58 | scan_packets_->push_back(packet); 59 | 60 | const int MAX_ANGLE = 36000; 61 | int new_coverage = (azimuth - initial_azimuth_ + MAX_ANGLE) % MAX_ANGLE; 62 | scan_complete_ = coverage_ > MAX_ANGLE / 2 && coverage_ > new_coverage; 63 | coverage_ = new_coverage; 64 | return scan_complete_; 65 | } 66 | 67 | template bool ScanBatcher::empty() const { return scan_packets_->empty(); } 68 | 69 | template size_t ScanBatcher::size() const { return scan_packets_->size(); } 70 | 71 | template bool ScanBatcher::scanComplete() const { return scan_complete_; } 72 | 73 | template const std::shared_ptr> &ScanBatcher::scanPackets() const { 74 | return scan_packets_; 75 | } 76 | 77 | template void ScanBatcher::reset(std::shared_ptr> scan_packets) { 78 | if (cut_angle_ < 0) { 79 | initial_azimuth_ = empty() ? -1 : getAzimuth(toPacketView(scan_packets_->back())); 80 | } else { 81 | initial_azimuth_ = cut_angle_; 82 | } 83 | scan_packets->clear(); 84 | scan_packets_ = std::move(scan_packets); 85 | scan_complete_ = false; 86 | coverage_ = 0; 87 | if (kept_last_packet_) { 88 | push(std::move(*kept_last_packet_)); 89 | kept_last_packet_ = std::nullopt; 90 | } 91 | } 92 | 93 | template void ScanBatcher::reset() { reset(std::make_shared>()); } 94 | 95 | } // namespace velodyne_decoder 96 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [ published ] 5 | workflow_dispatch: 6 | jobs: 7 | build_sdist: 8 | name: Build source distribution 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - uses: actions/setup-python@v5 14 | with: 15 | python-version: '3.13' 16 | 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | python -m pip install --upgrade build twine 21 | 22 | - name: Build SDist 23 | run: python -m build --sdist 24 | 25 | - name: Check metadata 26 | run: python -m twine check dist/* 27 | 28 | - name: Test 29 | run: | 30 | python -m pip install dist/*.tar.gz pytest requests 31 | pytest test/ --ignore test/test_ros.py --color=yes -v 32 | 33 | - uses: actions/upload-artifact@v3 34 | with: 35 | path: dist/*.tar.gz 36 | 37 | build_wheels: 38 | name: ${{ matrix.platform[0] }} - ${{ matrix.platform[2] }} 39 | runs-on: ${{ matrix.platform[1] }} 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | platform: 44 | # Build only for 64-bit platforms that have binaries for NumPy 45 | - [Linux, ubuntu-latest, x86_64] 46 | # - [Linux, ubuntu-latest, i686] 47 | - [Linux, ubuntu-latest, aarch64] 48 | # - [Linux, ubuntu-latest, ppc64le] 49 | # - [Linux, ubuntu-latest, s390x] 50 | - [Windows, windows-latest, AMD64] 51 | # - [Windows, windows-latest, x86] 52 | # - [Windows, windows-latest, ARM64] 53 | - [MacOS, macos-latest, x86_64] 54 | - [MacOS, macos-latest, arm64] 55 | # - [MacOS, macos-latest, universal2] 56 | steps: 57 | - uses: actions/checkout@v4 58 | 59 | - name: Set up QEMU 60 | if: runner.os == 'Linux' 61 | uses: docker/setup-qemu-action@v3 62 | with: 63 | platforms: all 64 | 65 | - uses: actions/setup-python@v5 66 | with: 67 | python-version: '3.13' 68 | 69 | - name: Download test data 70 | run: | 71 | python -m pip install pytest requests 72 | python -m pytest test --color=yes || true 73 | 74 | - name: Install cibuildwheel 75 | run: python -m pip install cibuildwheel 76 | 77 | - name: Build wheels 78 | run: python -m cibuildwheel --output-dir wheelhouse 79 | env: 80 | CIBW_ARCHS: ${{ matrix.platform[2] }} 81 | # Needed for C++17 support on macOS 82 | MACOSX_DEPLOYMENT_TARGET: 10.14 83 | 84 | - name: Inspect 85 | run: ls wheelhouse/ 86 | 87 | - name: Upload wheels 88 | uses: actions/upload-artifact@v3 89 | with: 90 | path: wheelhouse/*.whl 91 | 92 | upload_all: 93 | name: Upload if release 94 | needs: [ build_wheels, build_sdist ] 95 | runs-on: ubuntu-latest 96 | if: github.event_name == 'release' && github.event.action == 'published' 97 | steps: 98 | - uses: actions/setup-python@v5 99 | with: 100 | python-version: '3.13' 101 | 102 | - uses: actions/download-artifact@v3 103 | with: 104 | name: artifact 105 | path: dist 106 | 107 | - uses: pypa/gh-action-pypi-publish@release/v1 108 | with: 109 | user: __token__ 110 | password: ${{ secrets.PYPI_API_TOKEN }} 111 | -------------------------------------------------------------------------------- /include/velodyne_decoder/telemetry_packet.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Martin Valgur 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "velodyne_decoder/types.h" 13 | 14 | namespace velodyne_decoder { 15 | 16 | struct NmeaInfo { 17 | double longitude = 0; ///< Longitude in degrees 18 | double latitude = 0; ///< Latitude in degrees 19 | double altitude = 0; ///< Altitude above WGS84 ellipsoid in meters (always 0 for GPRMC) 20 | uint16_t utc_year = 0; ///< UTC year (always 0 for GPGGA) 21 | uint8_t utc_month = 0; ///< UTC month (always 0 for GPGGA) 22 | uint8_t utc_day = 0; ///< UTC day (always 0 for GPGGA) 23 | uint8_t utc_hours = 0; ///< UTC hours 24 | uint8_t utc_minutes = 0; ///< UTC minutes 25 | float utc_seconds = 0; ///< UTC seconds 26 | bool fix_available = false; ///< Position fix available 27 | 28 | /// UTC timestamp as Unix time, or seconds since midnight if date is missing 29 | [[nodiscard]] Time utcTime() const; 30 | }; 31 | 32 | struct TelemetryPacket { 33 | /// Reason for the last ADC calibration 34 | enum class AdcCalibReason : uint8_t { 35 | NO_CALIBRATION = 0, ///< No calibration 36 | POWER_ON = 1, ///< Power-on calibration performed 37 | MANUAL = 2, ///< Manual calibration performed 38 | DELTA_TEMPERATURE = 3, ///< Delta temperature calibration performed 39 | PERIODIC = 4, ///< Periodic calibration performed 40 | }; 41 | /// Pulse Per Second (PPS) status 42 | enum class PpsStatus : uint8_t { 43 | ABSENT = 0, ///< No PPS detected 44 | SYNCHRONIZING = 1, ///< Synchronizing to PPS 45 | LOCKED = 2, ///< PPS Locked 46 | ERROR = 3, ///< Error 47 | }; 48 | 49 | // fields set by all firmware versions 50 | uint32_t usec_since_toh; ///< Number of microseconds elapsed since the top of the hour 51 | PpsStatus pps_status; ///< Pulse Per Second (PPS) status 52 | std::string nmea_sentence; ///< GPRMC or GPGGA NMEA sentence 53 | 54 | // fields set by newer firmware versions only (since mid-2018) 55 | uint8_t temp_board_top; ///< Temperature of top board, 0 to 150 °C 56 | uint8_t temp_board_bottom; ///< Temperature of bottom board, 0 to 150 °C 57 | bool thermal_shutdown; ///< Thermal status, true if thermal shutdown 58 | uint8_t temp_at_shutdown; ///< Temperature of unit when thermal shutdown occurred 59 | uint8_t temp_at_powerup; ///< Temperature of unit (bottom board) at power up 60 | uint8_t temp_during_adc_calibration; ///< Temperature when ADC calibration last ran, 0 to 150 °C 61 | int16_t temp_change_since_adc_calibration; ///< Change in temperature since last ADC calibration, 62 | ///< -150 to 150°C 63 | uint32_t seconds_since_adc_calibration; ///< Elapsed seconds since last ADC calibration 64 | AdcCalibReason adc_calibration_reason; ///< Reason for the last ADC calibration 65 | bool adc_calib_in_progress; ///< ADC calibration in progress 66 | bool adc_delta_temp_limit_exceeded; ///< ADC calibration: delta temperature limit has been met 67 | bool adc_period_exceeded; ///< ADC calibration: periodic time elapsed limit has been met 68 | 69 | explicit TelemetryPacket(gsl::span raw_data); 70 | 71 | [[nodiscard]] std::optional parseNmea() const; 72 | 73 | /// Timestamp from the NMEA sentence 74 | [[nodiscard]] std::optional