Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Aeva Aeries II #169

Open
wants to merge 58 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
c9be206
refactor(mt_queue): move to nebula_common
mojomex Jun 26, 2024
438f986
refactor(expected): add shorthand value_or_throw function
mojomex Jun 26, 2024
ca35b24
feat(hw_interfaces): light-weight TCP/UDP interfaces with abstractions
mojomex Jun 26, 2024
e60b7e8
feat(nebula_common): add nlohmann_json and helper functions
mojomex Jun 26, 2024
d4d0e7c
chore(watchdog_timer): clean up watchdog timer
mojomex Jun 26, 2024
2838e77
chore: make merged changed comply with pre-commit
mojomex Jun 26, 2024
26aeef4
feat(aeva): add sensor model and type support
mojomex Jun 26, 2024
fa5665c
feat(aeva): add low-level Aeva TCP protocol support
mojomex Jun 26, 2024
56bdf16
feat(aeva): add parsers for most important data streams
mojomex Jun 26, 2024
15f00ca
feat(aeva): add hardware interface
mojomex Jun 26, 2024
6f96519
test(aeva): add hw interface test
mojomex Jun 26, 2024
9ce7ba6
feat(aeva): add decoder
mojomex Jun 26, 2024
2db7929
feat(aeva): add ros wrapper
mojomex Jun 26, 2024
d23c93d
feat(aeva): add launch and config files, fix cspell
mojomex Jun 26, 2024
e00572c
docs(aeva): added to supported sensors
mojomex Jun 27, 2024
fdaac59
docs(aeva): add XYZVIRCAEDT point type
mojomex Jun 27, 2024
9441d9d
fix(aeries2_decoder): un-mirror the pointcloud
mojomex Jun 28, 2024
3b94166
refactor(aeva)!: rename `v` point field to `distance_rate`
mojomex Jun 28, 2024
1ee0c92
fix(nebula_ros): remove erroneous header import
mojomex Jun 28, 2024
903e1b7
fix(aeva): prevent fallthrough in telemetry parsing switch
mojomex Jul 1, 2024
cc353ce
fix(aeva): fix starving when registering raw packet callback
mojomex Jul 4, 2024
9394cb6
chore(aeva): allow for slightly lower pointcloud frequency, more visi…
mojomex Jul 4, 2024
b309508
chore(aeva)!: change `distance_rate` point field to `range_rate`
mojomex Jul 4, 2024
8964f6b
fix(aeva): output return types
mojomex Jul 4, 2024
7011769
fix(aeva): display error codes correctly in monitor
mojomex Jul 4, 2024
e309ad5
chore(aeva): align format with pretty-printing PR
mojomex Jul 5, 2024
32d80f3
feat(aeva): support runtime parameter changes
mojomex Jul 5, 2024
2fb591b
chore(aeva): rename occurrences of `aries` to `aeries`
mojomex Jul 8, 2024
7896bd1
fix(aeva_api): stop thread gracefully
mojomex Jul 8, 2024
65258c3
test(aeva): fix tests
mojomex Jul 8, 2024
185b0d8
fix(aeva): allow multiple responses in reconfig stream, thread safety…
mojomex Jul 8, 2024
f180138
feat(aeva): switch to json config, add many more config options
mojomex Jul 8, 2024
d6cd72f
chore(aeva): set frame id to `nebula`
mojomex Jul 8, 2024
063d87b
feat(aeva): correct handling of the currently active return mode
mojomex Jul 8, 2024
1519e99
chore: accept suggestion to make pointcloud topic namespace local
mojomex Jul 22, 2024
6ac8184
chore: remove json_fwd header imports
mojomex Jul 22, 2024
e1be90c
chore: move diagnostics topic to local namespace
mojomex Jul 22, 2024
03622ad
fix(aeva): increase buffer size for packet replay as drops were observed
mojomex Jul 23, 2024
a353f72
fix(aeva): add missing diagnostic_updater dependency in CMakeLists.txt
mojomex Sep 3, 2024
7d15607
fix(aeva): delete elevation_auto_adjustment param that is not present…
mojomex Nov 12, 2024
0c94de3
fix(aeva): change referenced function names after merge
mojomex Nov 12, 2024
6c5b183
docs(aeva): add parameter table akin to #195
mojomex Nov 12, 2024
8f8171f
docs(aeva): mention tested firmware version (14.0.0)
mojomex Nov 12, 2024
0af2516
refactor(aeva): implement clang-tidy changes
mojomex Nov 18, 2024
c692aa2
chore(aeva): rename Aeries2 to Aeries II
mojomex Nov 18, 2024
7bccfec
chore(aeva_api): put constants and types to the top of the file
mojomex Nov 18, 2024
6c3bd3b
refactor(aeva): create aeva namespace inside connections namespace
mojomex Nov 18, 2024
24d135f
chore(aeva): move hardcoded port numbers to constants
mojomex Nov 18, 2024
4fcd9ee
chore(aeva): remove pointer hackery for cleaner code
mojomex Nov 18, 2024
2b2fe38
chore(nebula_common): add version requirement for nlohmann_json
mojomex Nov 18, 2024
f039c36
chore(aeva): use arg with choices instead of textual descriptions for…
mojomex Nov 18, 2024
8139c6d
feat(nebula_launch): add Aeva sensors to the generic launch file
mojomex Nov 18, 2024
2dc01ad
chore(hesai): remove spurious mt_queue import after Aeva rebase
mojomex Dec 16, 2024
a75e32f
chore(aeva): remove pragma once in C file
mojomex Dec 16, 2024
b248dee
test(aeva): fix wrongly compiledand thus skipped unit test
mojomex Dec 16, 2024
896943d
ci(pre-commit): autofix
pre-commit-ci[bot] Dec 16, 2024
0c16d01
fix(aeva): remove JSON schemarequirement of now-non-existent parameter
mojomex Dec 16, 2024
dc8f483
Merge branch 'aeva-aries2-support' of github.com:mojomex/nebula into …
mojomex Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"words": [
"adctp",
"Adctp",
"aeva",
"applicate",
"AT",
"autosar",
Expand All @@ -21,6 +22,7 @@
"gptp",
"Helios",
"Hesai",
"hfov",
"horiz",
"Idat",
"ipaddr",
Expand Down
1 change: 1 addition & 0 deletions docs/parameters/vendors/aeva/aeries2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ json_to_markdown("nebula_ros/schema/Aeries2.schema.json") }}
16 changes: 16 additions & 0 deletions docs/point_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ These definitions can be found in the `nebula_common/include/point_types.hpp`.
| `distance` | `float` | `m` | Distance from the sensor origin. |
| `timestamp` | `uint32` | `ns` | Time of detection relative to the pointcloud timestamp. |

## PointXYZVIRCAEDT

| Field | Type | Units | Description |
| --------------- | -------- | ----- | --------------------------------------------------------------------------- |
| `x` | `float` | `m` | The point's cartesian x coordinate. |
| `y` | `float` | `m` | The point's cartesian y coordinate. |
| `z` | `float` | `m` | The point's cartesian z coordinate. |
| `distance_rate` | `float` | `m/s` | The point's velocity component in the direction of the sensor's origin. |
| `intensity` | `uint8` | | The intensity of the return as reported by the sensor. |
| `return type` | `uint8` | | Whether the point was the first, strongest, last, etc. of multiple returns. |
| `channel` | `uint16` | | The ID of the laser channel that produced the point. |
| `azimuth` | `float` | `rad` | The point's azimuth in polar coordinates. |
| `elevation` | `float` | `rad` | The point's elevation in polar coordinates. |
| `distance` | `float` | `m` | The point's distance from the sensor origin. |
| `timestamp` | `uint32` | `ns` | The time the point was detected relative to the pointcloud timestamp. |

## [Deprecated] PointXYZIR

| Field | Type | Units | Description |
Expand Down
8 changes: 8 additions & 0 deletions docs/supported_sensors.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ The `sensor_model` parameter below decides which sensor driver is launched.
| Bpearl | Bpearl | Bpearl.param.yaml | ⚠️ |
| Helios | Helios | Helios.param.yaml | ⚠️ |

## Aeva LiDARs

| Model | `sensor_model` | Configuration file | Test status |
| --------- | -------------- | ------------------ | ----------- |
| Aeries II | Aeries2 | Aeries2.param.yaml | ⚠️\* |

\*: The Aeries II has been tested with firmware version 14.0.0.

## Continental radars

| Model | `sensor_model` | Configuration file | Test status |
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ nav:
- parameters/vendors/continental/common.md
- ARS548: parameters/vendors/continental/ars548.md
- SRR520: parameters/vendors/continental/srr520.md
- Aeva:
- Aeries II: parameters/vendors/aeva/aeries2.md
- Point cloud types: point_types.md
- Design: design.md
- Tutorials: tutorials.md
Expand Down
2 changes: 1 addition & 1 deletion nebula_common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ project(nebula_common)
find_package(ament_cmake_auto REQUIRED)
find_package(PCL REQUIRED COMPONENTS common)
find_package(yaml-cpp REQUIRED)
find_package(nlohmann_json REQUIRED)
find_package(nlohmann_json 3.10.5 REQUIRED)

# Default to C++17
if (NOT CMAKE_CXX_STANDARD)
Expand Down
67 changes: 67 additions & 0 deletions nebula_common/include/nebula_common/aeva/config_types.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2024 TIER IV, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include "nebula_common/nebula_common.hpp"
#include "nebula_common/util/parsing.hpp"

#include <nlohmann/json.hpp>

#include <optional>
#include <ostream>
#include <string>

namespace nebula::drivers::aeva
{

using nlohmann::json;

struct Aeries2Config : public SensorConfigurationBase
{
std::string sensor_ip;
json tree;

[[nodiscard]] std::optional<ReturnMode> get_return_mode() const

Check warning on line 36 in nebula_common/include/nebula_common/aeva/config_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/config_types.hpp#L36

Added line #L36 was not covered by tests
{
auto mode_name = util::get_if_exists<std::string>(tree, {"dsp_control", "second_peak_type"});

Check warning on line 38 in nebula_common/include/nebula_common/aeva/config_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/config_types.hpp#L38

Added line #L38 was not covered by tests

if (!mode_name) return {};
if (mode_name == "strongest") return ReturnMode::DUAL_STRONGEST_SECONDSTRONGEST;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: although no performance benefit in this case, a switch looks nicer (IMO)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C++ doesn't allow switch statements for strings 🥲 I agree though

if (mode_name == "farthest") return ReturnMode::DUAL_STRONGEST_LAST;
if (mode_name == "closest") return ReturnMode::DUAL_STRONGEST_FIRST;

Check warning on line 43 in nebula_common/include/nebula_common/aeva/config_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/config_types.hpp#L41-L43

Added lines #L41 - L43 were not covered by tests

return ReturnMode::UNKNOWN;

Check warning on line 45 in nebula_common/include/nebula_common/aeva/config_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/config_types.hpp#L45

Added line #L45 was not covered by tests
}

friend std::ostream & operator<<(std::ostream & os, const Aeries2Config & arg)

Check warning on line 48 in nebula_common/include/nebula_common/aeva/config_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/config_types.hpp#L48

Added line #L48 was not covered by tests
{
os << "Aeva Aeries II Sensor Configuration:\n";
os << "Sensor Model: " << arg.sensor_model << '\n';
os << "Frame ID: " << arg.frame_id << '\n';

Check warning on line 52 in nebula_common/include/nebula_common/aeva/config_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/config_types.hpp#L50-L52

Added lines #L50 - L52 were not covered by tests
os << "Sensor IP: " << arg.sensor_ip;

for (const auto & category : arg.tree.items()) {
os << '\n' << category.key() << ":";
auto category_settings = category.value();

Check warning on line 57 in nebula_common/include/nebula_common/aeva/config_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/config_types.hpp#L56-L57

Added lines #L56 - L57 were not covered by tests
for (const auto & setting : category_settings.items()) {
os << "\n " << setting.key() << ": " << setting.value();

Check warning on line 59 in nebula_common/include/nebula_common/aeva/config_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/config_types.hpp#L59

Added line #L59 was not covered by tests
}
}

return os;

Check warning on line 63 in nebula_common/include/nebula_common/aeva/config_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/config_types.hpp#L63

Added line #L63 was not covered by tests
}
};

} // namespace nebula::drivers::aeva
196 changes: 196 additions & 0 deletions nebula_common/include/nebula_common/aeva/packet_types.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright 2024 TIER IV, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <nlohmann/json.hpp>

#include <cstddef>
#include <cstdint>
#include <optional>
#include <ostream>
#include <string>
#include <vector>

namespace nebula::drivers::aeva
{
using nlohmann::json;

#pragma pack(push, 1)

template <typename storage_t, size_t n_fractional_bits>
struct FixedPoint
{
[[nodiscard]] float value() const { return value_ * std::pow(2., -1. * n_fractional_bits); }

Check warning on line 35 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L35

Added line #L35 was not covered by tests

private:
storage_t value_;
};

template <size_t n_bytes>
struct Padding
{
uint8_t padding[n_bytes]; // NOLINT
};

struct SomeIpHeader
{
uint16_t service_id;
uint16_t method_id;
uint32_t message_length;
uint16_t client_id;
uint16_t session_id;
uint8_t protocol;
uint8_t interface_version;
uint8_t message_type;
uint8_t return_code;
uint32_t tp_header_offset : 28;
uint32_t tp_header : 4;
};

struct MessageHeader
{
uint8_t major_version : 4;
uint8_t minor_version : 4;
uint8_t message_type;
uint16_t sequence_id;
uint32_t message_length;
int64_t acquisition_time_ns;
int64_t publish_time_ns;
};

struct PointcloudMsgSubheaderAndMetadata
{
uint16_t aeva_marker;
uint8_t platform;
uint8_t reserved_1;
uint16_t ns_per_index;
uint64_t first_point_timestamp_ns;
int32_t frame_sync_index;
uint32_t period_ns;
uint32_t n_entries;
uint32_t capacity;
uint16_t num_beams : 4;
uint16_t num_peaks : 2;
uint16_t range_scale : 10;
uint8_t line_index;
uint8_t max_line_index;
uint32_t face_index : 4;
uint32_t n_faces : 4;
uint32_t reserved_2 : 3;
uint32_t sensitivity_mode : 3;
uint32_t frame_parity : 1;
uint32_t reserved_3 : 1;
uint32_t window_measurement : 1;
uint32_t reserved_5 : 1;
uint32_t discard_line : 1;
uint32_t ping_pong : 1;
uint32_t reserved_6 : 12;
FixedPoint<int16_t, 9> mirror_rps;
uint16_t dither_step : 5;
uint16_t max_dither_step : 5;
uint16_t reserved_7 : 6;
uint8_t reserved_8[12];
};

struct PointCloudPoint
{
FixedPoint<int16_t, 15> azimuth;
FixedPoint<int16_t, 15> elevation;
FixedPoint<uint16_t, 7> range;
FixedPoint<int16_t, 8> velocity;
uint8_t intensity;
uint8_t signal_quality;
uint32_t beam_id : 3;
uint32_t peak_id : 2;
uint32_t line_transition : 1;
uint32_t valid : 1;
uint32_t dynamic : 1;
uint32_t reserved_1 : 1;
uint32_t up_sweep : 1;
uint32_t in_ambiguity_region : 1;
uint32_t reserved_2 : 9;
uint32_t peak_width : 4;
uint32_t is_single_detection : 1;
uint32_t reserved_3 : 7;
};

struct PointCloudMessage
{
PointcloudMsgSubheaderAndMetadata header;
std::vector<PointCloudPoint> points;
};

enum class TelemetryDataType : uint8_t {
kUInt8 = 0,
kInt8 = 1,
kUInt16 = 2,
kInt16 = 3,
kUInt32 = 4,
kInt32 = 5,
kUInt64 = 6,
kInt64 = 7,
kFloat = 8,
kDouble = 9,
kChar = 10
};

enum class ReconfigRequestType : uint8_t {
kManifestRequest = 0,
kManifestResponse = 1,
kChangeRequest = 2,
kChangeApproved = 3,
kChangeIgnored = 4,
kInvalid = 5
};

inline std::ostream & operator<<(std::ostream & os, const ReconfigRequestType & arg)
{
switch (arg) {

Check warning on line 160 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L160

Added line #L160 was not covered by tests
case ReconfigRequestType::kManifestRequest:
return os << "manifest request";

Check warning on line 162 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L162

Added line #L162 was not covered by tests
case ReconfigRequestType::kManifestResponse:
return os << "manifest response";

Check warning on line 164 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L164

Added line #L164 was not covered by tests
case ReconfigRequestType::kChangeRequest:
return os << "change request";

Check warning on line 166 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L166

Added line #L166 was not covered by tests
case ReconfigRequestType::kChangeApproved:
return os << "change approved";

Check warning on line 168 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L168

Added line #L168 was not covered by tests
case ReconfigRequestType::kChangeIgnored:
return os << "change ignored";
default:
return os << "invalid: " + std::to_string(static_cast<int>(arg));

Check warning on line 172 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L170-L172

Added lines #L170 - L172 were not covered by tests
}
}

struct HealthCode
{
explicit HealthCode(uint32_t value) : value_(value) {}

Check warning on line 178 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L178

Added line #L178 was not covered by tests

[[nodiscard]] bool is_error() const { return (value_ & g_error_mask) != 0; }
[[nodiscard]] uint32_t get() const { return value_ & ~g_error_mask; }

Check warning on line 181 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L180-L181

Added lines #L180 - L181 were not covered by tests

private:
uint32_t value_;
static const uint32_t g_error_mask = 1u << 31u;
};

struct ReconfigMessage

Check warning on line 188 in nebula_common/include/nebula_common/aeva/packet_types.hpp

View check run for this annotation

Codecov / codecov/patch

nebula_common/include/nebula_common/aeva/packet_types.hpp#L188

Added line #L188 was not covered by tests
{
ReconfigRequestType type = ReconfigRequestType::kInvalid;
std::optional<json> body;
};

#pragma pack(pop)

} // namespace nebula::drivers::aeva
46 changes: 46 additions & 0 deletions nebula_common/include/nebula_common/aeva/point_types.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2024 TIER IV, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <pcl/point_types.h>

namespace nebula::drivers::aeva
{

struct EIGEN_ALIGN16 PointXYZVIRCAEDT
{
float x;
float y;
float z;
float range_rate;
uint8_t intensity;
uint8_t return_type;
uint16_t channel;
float azimuth;
float elevation;
float distance;
uint32_t time_stamp;
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};

} // namespace nebula::drivers::aeva

POINT_CLOUD_REGISTER_POINT_STRUCT( // NOLINT
nebula::drivers::aeva::PointXYZVIRCAEDT,
(float, x,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes is this how pre-commit wanted the line break? :P

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, tried to coax it to do it differently but it's stubborn.

x)(float, y, y)(float, z, z)(float, range_rate, range_rate)(std::uint8_t, intensity, intensity)(
std::uint8_t, return_type,
return_type)(std::uint16_t, channel, channel)(float, azimuth, azimuth)(
float, elevation, elevation)(float, distance, distance)(std::uint32_t, time_stamp, time_stamp))
Loading
Loading