|
| 1 | +// Copyright 2024 TIER IV, Inc. |
| 2 | + |
| 3 | +#include "hesai/test_ptc/ptc_test.hpp" |
| 4 | +#include "hesai/test_ptc/tcp_socket_mock.hpp" |
| 5 | +#include "hesai/test_ptc/tcp_socket_replay.hpp" |
| 6 | +#include "nebula_common/hesai/hesai_common.hpp" |
| 7 | +#include "nebula_common/nebula_common.hpp" |
| 8 | +#include "nebula_hw_interfaces/nebula_hw_interfaces_hesai/connections/tcp.hpp" |
| 9 | +#include "nebula_hw_interfaces/nebula_hw_interfaces_hesai/hesai_cmd_response.hpp" |
| 10 | + |
| 11 | +#include <gmock/gmock-cardinalities.h> |
| 12 | +#include <gmock/gmock-spec-builders.h> |
| 13 | +#include <gmock/gmock.h> |
| 14 | +#include <gtest/gtest.h> |
| 15 | + |
| 16 | +#include <cmath> |
| 17 | +#include <exception> |
| 18 | +#include <filesystem> |
| 19 | +#include <iomanip> |
| 20 | +#include <memory> |
| 21 | +#include <sstream> |
| 22 | + |
| 23 | +#ifndef _TEST_RESOURCES_PATH |
| 24 | +static_assert(false, "No test resources path defined"); |
| 25 | +#endif |
| 26 | + |
| 27 | +#define GTEST_SKIP_PRINT(x) \ |
| 28 | + do { \ |
| 29 | + std::cout << x << '\n'; \ |
| 30 | + GTEST_SKIP() << x; \ |
| 31 | + } while (0) |
| 32 | + |
| 33 | +#define ASSERT_NO_THROW_PRINT(expr) \ |
| 34 | + do { \ |
| 35 | + try { \ |
| 36 | + expr; \ |
| 37 | + } catch (const std::exception & e) { \ |
| 38 | + std::cout << e.what() << '\n'; \ |
| 39 | + ASSERT_NO_THROW(throw e); \ |
| 40 | + } \ |
| 41 | + } while (0) |
| 42 | + |
| 43 | +namespace nebula::drivers |
| 44 | +{ |
| 45 | + |
| 46 | +using testing::_; |
| 47 | +using testing::AtLeast; |
| 48 | +using testing::Exactly; |
| 49 | +using testing::InSequence; |
| 50 | + |
| 51 | +const SensorModel g_models_under_test[] = { |
| 52 | + SensorModel::HESAI_PANDAR64, SensorModel::HESAI_PANDAR40P, SensorModel::HESAI_PANDARQT64, |
| 53 | + SensorModel::HESAI_PANDARQT128, SensorModel::HESAI_PANDARXT32, SensorModel::HESAI_PANDARAT128, |
| 54 | + SensorModel::HESAI_PANDAR128_E4X, |
| 55 | +}; |
| 56 | + |
| 57 | +const uint16_t g_u16_invalid = 0x4242; |
| 58 | +const uint16_t g_ptc_port = 9347; |
| 59 | +const size_t g_ptc_header_size = 8; |
| 60 | +const char g_host_ip[] = "192.168.42.42"; |
| 61 | +const char g_sensor_ip[] = "192.168.84.84"; |
| 62 | + |
| 63 | +auto make_sensor_config(SensorModel model) |
| 64 | +{ |
| 65 | + uint16_t rotation_speed = 600; |
| 66 | + uint16_t sync_angle = 0; |
| 67 | + double cut_angle = 0.0; |
| 68 | + uint16_t cloud_min_angle = 0; |
| 69 | + uint16_t cloud_max_angle = 360; |
| 70 | + |
| 71 | + if (model == SensorModel::HESAI_PANDARAT128) { |
| 72 | + rotation_speed = 200; |
| 73 | + sync_angle = 30; |
| 74 | + cut_angle = 150.0; |
| 75 | + cloud_min_angle = 30; |
| 76 | + cloud_max_angle = 150; |
| 77 | + } |
| 78 | + |
| 79 | + HesaiSensorConfiguration config{ |
| 80 | + LidarConfigurationBase{ |
| 81 | + EthernetSensorConfigurationBase{ |
| 82 | + SensorConfigurationBase{model, "test"}, g_host_ip, g_sensor_ip, g_u16_invalid}, |
| 83 | + ReturnMode::UNKNOWN, |
| 84 | + g_u16_invalid, |
| 85 | + g_u16_invalid, |
| 86 | + CoordinateMode::UNKNOWN, |
| 87 | + NAN, |
| 88 | + NAN, |
| 89 | + false, |
| 90 | + {}, |
| 91 | + false}, |
| 92 | + "", |
| 93 | + g_u16_invalid, |
| 94 | + sync_angle, |
| 95 | + cut_angle, |
| 96 | + 0.1, |
| 97 | + "", |
| 98 | + rotation_speed, |
| 99 | + cloud_min_angle, |
| 100 | + cloud_max_angle, |
| 101 | + PtpProfile::IEEE_802_1AS_AUTO, |
| 102 | + 0, |
| 103 | + PtpTransportType::L2, |
| 104 | + PtpSwitchType::NON_TSN}; |
| 105 | + |
| 106 | + return std::make_shared<HesaiSensorConfiguration>(config); |
| 107 | +} |
| 108 | + |
| 109 | +TEST_P(PtcTest, ConnectionLifecycle) |
| 110 | +{ |
| 111 | + /* Constructor does not immediately connect, destructor closes socket */ { |
| 112 | + auto tcp_sock_ptr = std::make_shared<connections::MockTcpSocket>(); |
| 113 | + auto & tcp_sock = *tcp_sock_ptr; |
| 114 | + |
| 115 | + EXPECT_CALL(tcp_sock, close()).Times(AtLeast(1)); |
| 116 | + auto hw_interface = make_hw_interface(tcp_sock_ptr); |
| 117 | + } |
| 118 | + |
| 119 | + /* Full lifecycle without sending/receiving */ { |
| 120 | + auto tcp_sock_ptr = std::make_shared<connections::MockTcpSocket>(); |
| 121 | + auto & tcp_sock = *tcp_sock_ptr; |
| 122 | + |
| 123 | + InSequence seq; |
| 124 | + EXPECT_CALL(tcp_sock, init(g_host_ip, _, g_sensor_ip, g_ptc_port)).Times(Exactly(1)); |
| 125 | + EXPECT_CALL(tcp_sock, bind()).Times(Exactly(1)); |
| 126 | + EXPECT_CALL(tcp_sock, close()).Times(AtLeast(1)); |
| 127 | + |
| 128 | + auto cfg = make_sensor_config(GetParam()); |
| 129 | + |
| 130 | + auto hw_interface = make_hw_interface(tcp_sock_ptr); |
| 131 | + hw_interface->SetSensorConfiguration(cfg); |
| 132 | + hw_interface->InitializeTcpDriver(); |
| 133 | + hw_interface->FinalizeTcpDriver(); |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +TEST_P(PtcTest, PtcCommunication) |
| 138 | +{ |
| 139 | + const auto & model = GetParam(); |
| 140 | + |
| 141 | + // //////////////////////////////////////// |
| 142 | + // Set up database-based replay TCP socket |
| 143 | + // //////////////////////////////////////// |
| 144 | + |
| 145 | + using ptc_handler_t = connections::ReplayTcpSocket::ptc_handler_t; |
| 146 | + using header_callback_t = connections::ReplayTcpSocket::header_callback_t; |
| 147 | + using payload_callback_t = connections::ReplayTcpSocket::payload_callback_t; |
| 148 | + using completion_callback_t = connections::ReplayTcpSocket::completion_callback_t; |
| 149 | + using connections::message_t; |
| 150 | + |
| 151 | + auto conversation_db_path = std::filesystem::path(_TEST_RESOURCES_PATH) / "hesai" / |
| 152 | + (sensor_model_to_string(model) + ".json"); |
| 153 | + if (!std::filesystem::exists(conversation_db_path)) { |
| 154 | + GTEST_SKIP_PRINT("conversation DB " << conversation_db_path << " does not exist"); |
| 155 | + } |
| 156 | + |
| 157 | + auto conversation_db_exp = connections::parse_conversation_db(conversation_db_path); |
| 158 | + if (!conversation_db_exp.has_value()) { |
| 159 | + std::cout << "ParseError: " << conversation_db_exp.error().what() << '\n'; |
| 160 | + GTEST_SKIP_PRINT( |
| 161 | + "conversation DB for model " |
| 162 | + << model << " could not be parsed due to ParseError: " << conversation_db_exp.error().what()); |
| 163 | + } |
| 164 | + |
| 165 | + connections::conversation_db_t conversation_db = conversation_db_exp.value(); |
| 166 | + |
| 167 | + ptc_handler_t cb = [&conversation_db]( |
| 168 | + const message_t & request, const header_callback_t & cb_header, |
| 169 | + const payload_callback_t & cb_payload, |
| 170 | + const completion_callback_t & cb_completion) { |
| 171 | + if (conversation_db.find(request) == conversation_db.end()) { |
| 172 | + std::stringstream ss; |
| 173 | + ss << "0x"; |
| 174 | + for (const uint8_t & byte : request) { |
| 175 | + ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(byte); |
| 176 | + } |
| 177 | + GTEST_SKIP_PRINT("request " << ss.str() << " not found in conversation DB"); |
| 178 | + } |
| 179 | + |
| 180 | + const auto & responses = conversation_db[request]; |
| 181 | + |
| 182 | + message_t complete_response; |
| 183 | + |
| 184 | + for (const message_t & response : responses) { |
| 185 | + complete_response.insert(complete_response.end(), response.cbegin(), response.cend()); |
| 186 | + } |
| 187 | + |
| 188 | + auto header = message_t( |
| 189 | + complete_response.cbegin(), std::next(complete_response.cbegin(), g_ptc_header_size)); |
| 190 | + cb_header(header); |
| 191 | + cb_payload(complete_response); |
| 192 | + cb_completion(); |
| 193 | + }; |
| 194 | + |
| 195 | + auto tcp_sock_ptr = std::make_shared<connections::ReplayTcpSocket>(std::move(cb)); |
| 196 | + |
| 197 | + // //////////////////////////////////////// |
| 198 | + // Test HW interface |
| 199 | + // //////////////////////////////////////// |
| 200 | + |
| 201 | + auto hw_interface = make_hw_interface(tcp_sock_ptr); |
| 202 | + |
| 203 | + auto cfg = make_sensor_config(GetParam()); |
| 204 | + hw_interface->SetSensorConfiguration(cfg); |
| 205 | + hw_interface->InitializeTcpDriver(); |
| 206 | + |
| 207 | + // //////////////////////////////////////// |
| 208 | + // Applicable to all models |
| 209 | + // //////////////////////////////////////// |
| 210 | + |
| 211 | + std::shared_ptr<HesaiConfigBase> config; |
| 212 | + ASSERT_NO_THROW_PRINT(config = hw_interface->GetConfig()); |
| 213 | + ASSERT_NE(config, nullptr); |
| 214 | + |
| 215 | + std::shared_ptr<HesaiInventoryBase> inventory; |
| 216 | + ASSERT_NO_THROW_PRINT(inventory = hw_interface->GetInventory()); |
| 217 | + ASSERT_NE(inventory, nullptr); |
| 218 | + |
| 219 | + std::vector<uint8_t> calibration; |
| 220 | + ASSERT_NO_THROW_PRINT(calibration = hw_interface->GetLidarCalibrationBytes()); |
| 221 | + ASSERT_FALSE(calibration.empty()); |
| 222 | + |
| 223 | + std::shared_ptr<HesaiLidarStatusBase> status; |
| 224 | + ASSERT_NO_THROW_PRINT(status = hw_interface->GetLidarStatus()); |
| 225 | + ASSERT_NE(status, nullptr); |
| 226 | +} |
| 227 | + |
| 228 | +INSTANTIATE_TEST_SUITE_P(TestMain, PtcTest, testing::ValuesIn(g_models_under_test)); |
| 229 | + |
| 230 | +} // namespace nebula::drivers |
| 231 | + |
| 232 | +int main(int argc, char * argv[]) |
| 233 | +{ |
| 234 | + ::testing::InitGoogleTest(&argc, argv); |
| 235 | + return RUN_ALL_TESTS(); |
| 236 | +}; |
0 commit comments