diff --git a/.gitignore b/.gitignore index a2bb453b..6b099c07 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ COLCON_IGNORE .catkin_tools core /tmp +/prompt.md diff --git a/fixposition-sdk b/fixposition-sdk index 8c37c260..3a664467 160000 --- a/fixposition-sdk +++ b/fixposition-sdk @@ -1 +1 @@ -Subproject commit 8c37c260beeebe0fe26738c469f1806430f9f19d +Subproject commit 3a664467b64a69a47ba292ed00f346232f75f0fe diff --git a/fixposition_driver_lib/CMakeLists.txt b/fixposition_driver_lib/CMakeLists.txt index c52ac2d4..689fed54 100644 --- a/fixposition_driver_lib/CMakeLists.txt +++ b/fixposition_driver_lib/CMakeLists.txt @@ -45,7 +45,35 @@ add_library( src/params.cpp ) -target_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES} ${fpsdk_common_LIBRARIES} pthread) +target_link_libraries(${PROJECT_NAME} + ${Boost_LIBRARIES} + ${fpsdk_common_LIBRARIES} + pthread +) + + +# TESTS ================================================================================================================= + +# Defines BUILD_TESTING (default ON), calls enable_testing() +include(CTest) + +# Build tests only if GTest is available +if(BUILD_TESTING) + find_package(GTest 1.11.0 QUIET) + if(GTest_FOUND) + add_executable(${PROJECT_NAME}_llh_trafo_test + test/llh_trafo_test.cpp + ) + target_link_libraries(${PROJECT_NAME}_llh_trafo_test + PRIVATE + ${PROJECT_NAME} + GTest::gtest_main + ) + add_test(NAME ${PROJECT_NAME}_llh_trafo_test COMMAND ${PROJECT_NAME}_llh_trafo_test) + else() + message(STATUS "${PROJECT_NAME}: GTest not found, not building tests") + endif() +endif() # INSTALL ============================================================================================================== diff --git a/fixposition_driver_lib/cmake/fixposition_driver_lib-config.cmake.in b/fixposition_driver_lib/cmake/fixposition_driver_lib-config.cmake.in index cb4bbf37..36429b8b 100644 --- a/fixposition_driver_lib/cmake/fixposition_driver_lib-config.cmake.in +++ b/fixposition_driver_lib/cmake/fixposition_driver_lib-config.cmake.in @@ -1,5 +1,11 @@ @PACKAGE_INIT@ +include(CMakeFindDependencyMacro) +set(_fixposition_driver_lib_package_prefix_dir "${PACKAGE_PREFIX_DIR}") +find_dependency(fpsdk_common) +set(PACKAGE_PREFIX_DIR "${_fixposition_driver_lib_package_prefix_dir}") +unset(_fixposition_driver_lib_package_prefix_dir) + set(@PROJECT_NAME@_FOUND ON) set_and_check(@PROJECT_NAME@_INCLUDE_DIRS "${PACKAGE_PREFIX_DIR}/include") set_and_check(@PROJECT_NAME@_LIBRARY_DIRS "${PACKAGE_PREFIX_DIR}/lib") diff --git a/fixposition_driver_lib/include/fixposition_driver_lib/helper.hpp b/fixposition_driver_lib/include/fixposition_driver_lib/helper.hpp index 466d903a..56e94bf2 100644 --- a/fixposition_driver_lib/include/fixposition_driver_lib/helper.hpp +++ b/fixposition_driver_lib/include/fixposition_driver_lib/helper.hpp @@ -16,6 +16,7 @@ /* LIBC/STL */ #include +#include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include /* PACKAGE */ #include "fixposition_driver_lib/params.hpp" @@ -307,6 +309,35 @@ struct FusionEpochData { FusionEpochData CompleteAndReset(const fpsdk::common::parser::fpa::FpaEoePayload& eoe); }; +#if FPSDK_USE_PROJ +/** + * @brief Initialise ECEF to LLH transformer + * + * Creates a PROJ transformer (see fpsdk::common::trafo::Transformer) if params.datum_llh_enabled_ is true. The source + * CRS must be geocentric. The target CRS must be either geographic 3D, or compound geographic 2D plus vertical. + * + * @param[in] params Driver parameters (datum_llh_...) + * @param[out] llh_trafo The transformer, nullptr if disabled + * @param[out] error Error message, on failure + * + * @returns true if disabled or if the transformer is valid, false otherwise + */ +bool InitLlhTrafo(const DriverParams& params, std::unique_ptr& llh_trafo, + std::string& error); + +/** + * @brief Convert ECEF to latitude, longitude and height + * + * @param[in] llh_trafo Transformer from InitLlhTrafo(), nullptr to use the built-in WGS84 conversion + * @param[in] ecef ECEF position [m] + * @param[out] llh_rad Latitude [rad], longitude [rad], height [m] + * + * @returns true on success, false if the transformation failed + */ +bool EcefToLlhRad(const fpsdk::common::trafo::Transformer* llh_trafo, const Eigen::Vector3d& ecef, + Eigen::Vector3d& llh_rad); +#endif // FPSDK_USE_PROJ + void HelloWorld(); /* ****************************************************************************************************************** */ diff --git a/fixposition_driver_lib/include/fixposition_driver_lib/params.hpp b/fixposition_driver_lib/include/fixposition_driver_lib/params.hpp index 331c6865..cf42af67 100644 --- a/fixposition_driver_lib/include/fixposition_driver_lib/params.hpp +++ b/fixposition_driver_lib/include/fixposition_driver_lib/params.hpp @@ -41,6 +41,9 @@ struct DriverParams { bool raw_output_ = false; bool cov_warning_ = false; bool nav2_mode_ = false; + bool datum_llh_enabled_ = false; + std::string datum_llh_ecef_crs_ = "EPSG:4978"; + std::string datum_llh_llh_crs_ = "EPSG:4979"; enum class VelTopicType { UNSPECIFIED, TWIST, TWISTWITHCOV, ODOMETRY }; bool converter_enabled_ = false; diff --git a/fixposition_driver_lib/src/helper.cpp b/fixposition_driver_lib/src/helper.cpp index 232c4a8e..c863db69 100644 --- a/fixposition_driver_lib/src/helper.cpp +++ b/fixposition_driver_lib/src/helper.cpp @@ -12,10 +12,12 @@ */ /* LIBC/STL */ +#include #include /* EXTERNAL */ #include +#include #include #include #include @@ -161,13 +163,12 @@ bool OdometryData::ConvertToEnu(const TfData& tf_ecef_enu0) { const Eigen::Matrix cov_ecef = pose.cov; // Extract data from the TF message (using the arrow operator) - const Eigen::Vector3d t_ecef_enu0 = tf_ecef_enu0.translation; const Eigen::Quaterniond q_ecef_enu0 = tf_ecef_enu0.rotation; const Eigen::Matrix3d rot_ecef_enu0 = q_ecef_enu0.toRotationMatrix(); // Convert position in ECEF into position in ENU - const Eigen::Vector3d t_enu_body = - fpsdk::common::trafo::TfEnuEcef(t_ecef_body, fpsdk::common::trafo::TfWgs84LlhEcef(t_ecef_enu0)); + const Eigen::Vector3d wgs84llh_ref = fpsdk::common::trafo::TfWgs84LlhEcef(tf_ecef_enu0.translation); + const Eigen::Vector3d t_enu_body = fpsdk::common::trafo::TfEnuEcef(t_ecef_body, wgs84llh_ref); const Eigen::Quaterniond q_enu_body = q_ecef_enu0.inverse() * q_ecef_body; // Convert covariance matrix to ENU @@ -415,6 +416,63 @@ FusionEpochData FusionEpochData::CompleteAndReset(const fpsdk::common::parser::f // --------------------------------------------------------------------------------------------------------------------- +#if FPSDK_USE_PROJ +bool InitLlhTrafo(const DriverParams& params, std::unique_ptr& llh_trafo, std::string& error) { + llh_trafo.reset(); + error.clear(); + if (!params.datum_llh_enabled_) { + return true; + } + + // Normalized output has longitude, latitude and height order regardless of the CRS's native axis order. + trafo::TransformerOpts opts; + opts.precise_ = true; + opts.normalize_axes_ = true; + auto new_trafo = std::make_unique("datum_llh"); + if (!new_trafo->Init(params.datum_llh_ecef_crs_, params.datum_llh_llh_crs_, opts)) { + error = "invalid CRS or transformation unavailable"; + return false; + } + + const auto source = new_trafo->SourceCrs(); + const auto target = new_trafo->TargetCrs(); + if (source.type_ != trafo::CrsType::GEOCENTRIC) { + error = "source CRS must be geocentric"; + return false; + } + const std::vector geo2d_vertical = {trafo::CrsType::GEOGRAPHIC_2D, trafo::CrsType::VERTICAL}; + if (!((target.type_ == trafo::CrsType::GEOGRAPHIC_3D) || + ((target.type_ == trafo::CrsType::COMPOUND) && (target.sub_types_ == geo2d_vertical)))) { + error = "target CRS must be geographic 3D or geographic 2D plus vertical"; + return false; + } + + llh_trafo = std::move(new_trafo); + return true; +} + +bool EcefToLlhRad(const trafo::Transformer* llh_trafo, const Eigen::Vector3d& ecef, Eigen::Vector3d& llh_rad) { + if (llh_trafo == nullptr) { + llh_rad = trafo::TfWgs84LlhEcef(ecef); + return true; + } + + Eigen::Vector3d lonlath; + if (!llh_trafo->Transform(ecef, lonlath)) { + // Report only once + static std::atomic warned = false; + if (!warned.exchange(true)) { + WARNING("PROJ ECEF to LLH transformation failed"); + } + return false; + } + llh_rad = {math::DegToRad(lonlath.y()), math::DegToRad(lonlath.x()), lonlath.z()}; + return true; +} +#endif // FPSDK_USE_PROJ + +// --------------------------------------------------------------------------------------------------------------------- + void HelloWorld() { NOTICE("Fixposition driver (https://github.com/fixposition/fixposition_driver)"); NOTICE("Fixposition SDK %s", utils::GetVersionString()); diff --git a/fixposition_driver_lib/test/llh_trafo_test.cpp b/fixposition_driver_lib/test/llh_trafo_test.cpp new file mode 100644 index 00000000..368618ff --- /dev/null +++ b/fixposition_driver_lib/test/llh_trafo_test.cpp @@ -0,0 +1,127 @@ +/** + * \verbatim + * ___ ___ + * \ \ / / + * \ \/ / Copyright (c) Fixposition AG (www.fixposition.com) and contributors + * / /\ \ License: see the LICENSE file + * /__/ \__\ + * \endverbatim + */ + +/* LIBC/STL */ +#include +#include +#include + +/* EXTERNAL */ +#include + +#include +#include + +/* PACKAGE */ +#include "fixposition_driver_lib/helper.hpp" +#include "fixposition_driver_lib/params.hpp" + +namespace fixposition { +/* ****************************************************************************************************************** */ + +TEST(LlhTrafo, ParamsUseWgs84ThreeDimensionalDefaults) { + const DriverParams params; + EXPECT_FALSE(params.datum_llh_enabled_); + EXPECT_EQ(params.datum_llh_ecef_crs_, "EPSG:4978"); + EXPECT_EQ(params.datum_llh_llh_crs_, "EPSG:4979"); +} + +TEST(OdometryData, EnuOriginAlwaysUsesEllipsoidalWgs84Height) { + const Eigen::Vector3d llh_ref(fpsdk::common::math::DegToRad(47.0), fpsdk::common::math::DegToRad(8.0), 500.0); + const Eigen::Vector3d ecef_ref = fpsdk::common::trafo::TfEcefWgs84Llh(llh_ref); + + TfData tf; + tf.valid = true; + tf.translation = ecef_ref; + tf.rotation.setIdentity(); + + OdometryData odometry; + odometry.pose.position = ecef_ref; + odometry.pose.orientation.setIdentity(); + ASSERT_TRUE(odometry.ConvertToEnu(tf)); + EXPECT_LT(odometry.pose.position.norm(), 1e-3) << odometry.pose.position.transpose(); +} + +#if FPSDK_USE_PROJ +using fpsdk::common::trafo::Transformer; + +// Driver params with datum LLH enabled +static DriverParams LlhParams(const std::string& ecef_crs, const std::string& llh_crs) { + DriverParams params; + params.datum_llh_enabled_ = true; + params.datum_llh_ecef_crs_ = ecef_crs; + params.datum_llh_llh_crs_ = llh_crs; + return params; +} + +TEST(LlhTrafo, DisabledUsesBuiltInWgs84Conversion) { + DriverParams params; + params.datum_llh_ecef_crs_ = "not a CRS"; + params.datum_llh_llh_crs_ = "also not a CRS"; + std::unique_ptr llh_trafo; + std::string error; + ASSERT_TRUE(InitLlhTrafo(params, llh_trafo, error)); + EXPECT_FALSE(llh_trafo); + EXPECT_TRUE(error.empty()); + + const Eigen::Vector3d expected_llh(fpsdk::common::math::DegToRad(47.0), fpsdk::common::math::DegToRad(8.0), 500.0); + const Eigen::Vector3d ecef = fpsdk::common::trafo::TfEcefWgs84Llh(expected_llh); + Eigen::Vector3d actual_llh; + ASSERT_TRUE(EcefToLlhRad(llh_trafo.get(), ecef, actual_llh)); + EXPECT_TRUE(actual_llh.isApprox(expected_llh, 1e-8)); +} + +TEST(LlhTrafo, RejectsInvalidAndNonGeographicCrs) { + std::unique_ptr llh_trafo; + std::string error; + EXPECT_FALSE(InitLlhTrafo(LlhParams("EPSG:4978", "EPSG:4979+3855"), llh_trafo, error)); + EXPECT_FALSE(llh_trafo); + EXPECT_FALSE(error.empty()); + EXPECT_FALSE(InitLlhTrafo(LlhParams("EPSG:4978", "EPSG:2056"), llh_trafo, error)); + EXPECT_FALSE(llh_trafo); + EXPECT_EQ(error, "target CRS must be geographic 3D or geographic 2D plus vertical"); + EXPECT_FALSE(InitLlhTrafo(LlhParams("EPSG:4326", "EPSG:4979"), llh_trafo, error)); + EXPECT_FALSE(llh_trafo); + EXPECT_EQ(error, "source CRS must be geocentric"); +} + +TEST(LlhTrafo, MatchesBuiltInWgs84Conversion) { + std::unique_ptr llh_trafo; + std::string error; + ASSERT_TRUE(InitLlhTrafo(LlhParams("EPSG:4978", "EPSG:4979"), llh_trafo, error)) << error; + ASSERT_TRUE(llh_trafo); + + // Checks lon/lat swap and deg to rad + const Eigen::Vector3d expected_llh(fpsdk::common::math::DegToRad(47.0), fpsdk::common::math::DegToRad(8.0), 500.0); + const Eigen::Vector3d ecef = fpsdk::common::trafo::TfEcefWgs84Llh(expected_llh); + Eigen::Vector3d actual_llh; + ASSERT_TRUE(EcefToLlhRad(llh_trafo.get(), ecef, actual_llh)); + EXPECT_TRUE(actual_llh.isApprox(expected_llh, 1e-8)) << actual_llh.transpose(); +} + +TEST(LlhTrafo, TransformsOnAThreadDifferentFromInitialization) { + std::unique_ptr llh_trafo; + std::string error; + ASSERT_TRUE(InitLlhTrafo(LlhParams("EPSG:4978", "EPSG:4979"), llh_trafo, error)) << error; + + const Eigen::Vector3d expected_llh(fpsdk::common::math::DegToRad(47.0), fpsdk::common::math::DegToRad(8.0), 500.0); + const Eigen::Vector3d ecef = fpsdk::common::trafo::TfEcefWgs84Llh(expected_llh); + Eigen::Vector3d actual_llh; + bool success = false; + std::thread worker([&]() { success = EcefToLlhRad(llh_trafo.get(), ecef, actual_llh); }); + worker.join(); + + ASSERT_TRUE(success); + EXPECT_TRUE(actual_llh.isApprox(expected_llh, 1e-8)); +} +#endif + +/* ****************************************************************************************************************** */ +} // namespace fixposition diff --git a/fixposition_driver_ros1/include/fixposition_driver_ros1/data_to_ros1.hpp b/fixposition_driver_ros1/include/fixposition_driver_ros1/data_to_ros1.hpp index 6ef1e550..78a49e3c 100644 --- a/fixposition_driver_ros1/include/fixposition_driver_ros1/data_to_ros1.hpp +++ b/fixposition_driver_ros1/include/fixposition_driver_ros1/data_to_ros1.hpp @@ -15,7 +15,6 @@ #define __FIXPOSITION_DRIVER_ROS1_DATA_TO_ROS1_HPP__ /* LIBC/STL */ -#include #include /* EXTERNAL */ @@ -39,6 +38,9 @@ void PublishFpaOdometry(const fpsdk::common::parser::fpa::FpaOdometryPayload& pa void PublishFpaOdometryDataImu(const fpsdk::common::parser::fpa::FpaOdometryPayload& payload, bool nav2_mode_, ros::Publisher& pub); void PublishFpaOdometryDataNavSatFix(const fpsdk::common::parser::fpa::FpaOdometryPayload& payload, bool nav2_mode_, +#if FPSDK_USE_PROJ + const fpsdk::common::trafo::Transformer* llh_trafo, +#endif ros::Publisher& pub); void PublishFpaOdomenu(const fpsdk::common::parser::fpa::FpaOdomenuPayload& payload, ros::Publisher& pub); void PublishFpaOdomenuVector3Stamped(const fpsdk::common::parser::fpa::FpaOdomenuPayload& payload, ros::Publisher& pub); @@ -77,7 +79,11 @@ void PublishParserMsg(const fpsdk::common::parser::ParserMsg& msg, ros::Publishe void PublishNmeaEpochData(const NmeaEpochData& data, ros::Publisher& pub); void PublishOdometryData(const OdometryData& data, ros::Publisher& pub); void PublishJumpWarning(const JumpDetector& jump_detector, ros::Publisher& pub); -void PublishDatum(const geometry_msgs::Vector3& payload, const ros::Time& stamp, ros::Publisher& pub); +void PublishDatum(const geometry_msgs::Vector3& payload, const ros::Time& stamp, +#if FPSDK_USE_PROJ + const fpsdk::common::trafo::Transformer* llh_trafo, +#endif + ros::Publisher& pub); void PublishFusionEpochData(const FusionEpochData& data, ros::Publisher& pub); /* ****************************************************************************************************************** */ diff --git a/fixposition_driver_ros1/include/fixposition_driver_ros1/fixposition_driver_node.hpp b/fixposition_driver_ros1/include/fixposition_driver_ros1/fixposition_driver_node.hpp index 3581ade3..02bde18a 100644 --- a/fixposition_driver_ros1/include/fixposition_driver_ros1/fixposition_driver_node.hpp +++ b/fixposition_driver_ros1/include/fixposition_driver_ros1/fixposition_driver_node.hpp @@ -63,6 +63,10 @@ class FixpositionDriverNode { DriverParams params_; //!< Sensor/driver parameters FixpositionDriver driver_; //!< Sensor driver +#if FPSDK_USE_PROJ + std::unique_ptr llh_trafo_; //!< ECEF to LLH, nullptr = built-in WGS84 +#endif + // ROS publishers // - FP_A messages ros::Publisher fpa_eoe_pub_; //!< FP_A-EOE message diff --git a/fixposition_driver_ros1/launch/config.yaml b/fixposition_driver_ros1/launch/config.yaml index fb65e611..957b39f8 100644 --- a/fixposition_driver_ros1/launch/config.yaml +++ b/fixposition_driver_ros1/launch/config.yaml @@ -54,6 +54,13 @@ raw_output: false # Enable raw messages output cov_warning: false # Enable covariance warnings nav2_mode: false # Enable nav2 mode +# Optional: omitting this block preserves the legacy WGS-84 LLH output. +datum_llh: + enabled: false # Use PROJ to convert the altitude published on odometry_llh and datum + ecef_crs: "EPSG:4978" # Geocentric source CRS of the sensor's ECEF coordinates; normally do not change + llh_crs: "EPSG:4979" # Geographic output CRS. For EGM2008 orthometric height use "EPSG:4326+3855" + # and install the us_nga_egm08_25.tif PROJ grid. Missing grids are treated as errors. + converter: enabled: false topic_type: "Twist" # Supported types: nav_msgs/{Twist, TwistWithCov, Odometry} diff --git a/fixposition_driver_ros1/src/data_to_ros1.cpp b/fixposition_driver_ros1/src/data_to_ros1.cpp index 942ba1bd..e2f8414f 100644 --- a/fixposition_driver_ros1/src/data_to_ros1.cpp +++ b/fixposition_driver_ros1/src/data_to_ros1.cpp @@ -153,7 +153,11 @@ void PublishFpaOdometryDataImu(const fpa::FpaOdometryPayload& payload, bool nav2 // --------------------------------------------------------------------------------------------------------------------- -void PublishFpaOdometryDataNavSatFix(const fpa::FpaOdometryPayload& payload, bool nav2_mode_, ros::Publisher& pub) { +void PublishFpaOdometryDataNavSatFix(const fpa::FpaOdometryPayload& payload, bool nav2_mode_, +#if FPSDK_USE_PROJ + const trafo::Transformer* llh_trafo, +#endif + ros::Publisher& pub) { if (pub.getNumSubscribers() > 0) { sensor_msgs::NavSatFix msg; msg.header.stamp = ros1::ConvTime(FpaGpsTimeToTime(payload.gps_time)); @@ -173,14 +177,21 @@ void PublishFpaOdometryDataNavSatFix(const fpa::FpaOdometryPayload& payload, boo msg.position_covariance_type = msg.COVARIANCE_TYPE_UNKNOWN; cov_map = Eigen::Matrix3d::Zero(); // FIXME: necessary? } else { - const Eigen::Vector3d llh_pos = trafo::TfWgs84LlhEcef(pose.position); + Eigen::Vector3d llh_pos; +#if FPSDK_USE_PROJ + if (!EcefToLlhRad(llh_trafo, pose.position, llh_pos)) { + return; + } +#else + llh_pos = trafo::TfWgs84LlhEcef(pose.position); +#endif msg.latitude = math::RadToDeg(llh_pos(0)); msg.longitude = math::RadToDeg(llh_pos(1)); msg.altitude = llh_pos(2); // Populate LLH covariance const Eigen::Matrix3d p_cov_e = pose.cov.topLeftCorner(3, 3); - const Eigen::Matrix3d C_l_e = trafo::RotEnuEcef(pose.position); + const Eigen::Matrix3d C_l_e = trafo::RotEnuEcef(llh_pos.x(), llh_pos.y()); const Eigen::Matrix3d p_cov_l = C_l_e * p_cov_e * C_l_e.transpose(); cov_map = p_cov_l; msg.position_covariance_type = msg.COVARIANCE_TYPE_KNOWN; @@ -864,7 +875,11 @@ void PublishJumpWarning(const JumpDetector& jump_detector, ros::Publisher& pub) // --------------------------------------------------------------------------------------------------------------------- -void PublishDatum(const geometry_msgs::Vector3& payload, const ros::Time& stamp, ros::Publisher& pub) { +void PublishDatum(const geometry_msgs::Vector3& payload, const ros::Time& stamp, +#if FPSDK_USE_PROJ + const trafo::Transformer* llh_trafo, +#endif + ros::Publisher& pub) { if (pub.getNumSubscribers() > 0) { sensor_msgs::NavSatFix msg; msg.header.stamp = stamp; @@ -872,7 +887,14 @@ void PublishDatum(const geometry_msgs::Vector3& payload, const ros::Time& stamp, // Populate LLH position const Eigen::Vector3d position = {payload.x, payload.y, payload.z}; - const Eigen::Vector3d llh_pos = trafo::TfWgs84LlhEcef(position); + Eigen::Vector3d llh_pos; +#if FPSDK_USE_PROJ + if (!EcefToLlhRad(llh_trafo, position, llh_pos)) { + return; + } +#else + llh_pos = trafo::TfWgs84LlhEcef(position); +#endif msg.latitude = math::RadToDeg(llh_pos(0)); msg.longitude = math::RadToDeg(llh_pos(1)); msg.altitude = llh_pos(2); diff --git a/fixposition_driver_ros1/src/fixposition_driver_node.cpp b/fixposition_driver_ros1/src/fixposition_driver_node.cpp index d5e8de60..c1306717 100644 --- a/fixposition_driver_ros1/src/fixposition_driver_node.cpp +++ b/fixposition_driver_ros1/src/fixposition_driver_node.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -45,7 +46,21 @@ FixpositionDriverNode::FixpositionDriverNode(const DriverParams& params, ros::No params_ { params }, driver_ { params_ }, nmea_epoch_data_ { params_.nmea_epoch_ } // clang-format on -{} +{ +#if FPSDK_USE_PROJ + std::string error; + if (!InitLlhTrafo(params_, llh_trafo_, error)) { + throw std::runtime_error("Failed initializing PROJ transformation: " + error); + } + if (llh_trafo_) { + ROS_INFO("PROJ enabled for ECEF to LLH output conversion"); + } +#else + if (params_.datum_llh_enabled_) { + throw std::runtime_error("Failed initializing PROJ transformation: driver was built without PROJ support"); + } +#endif +} FixpositionDriverNode::~FixpositionDriverNode() { StopNode(); } @@ -81,7 +96,11 @@ bool FixpositionDriverNode::StartNode() { auto odometry_payload = dynamic_cast(payload); PublishFpaOdometry(odometry_payload, fpa_odometry_pub_); PublishFpaOdometryDataImu(odometry_payload, params_.nav2_mode_, poiimu_pub_); +#if FPSDK_USE_PROJ + PublishFpaOdometryDataNavSatFix(odometry_payload, params_.nav2_mode_, llh_trafo_.get(), odometry_llh_pub_); +#else PublishFpaOdometryDataNavSatFix(odometry_payload, params_.nav2_mode_, odometry_llh_pub_); +#endif OdometryData odometry_data; odometry_data.SetFromFpaOdomPayload(odometry_payload); PublishOdometryData(odometry_data, odometry_ecef_pub_); @@ -701,8 +720,12 @@ void FixpositionDriverNode::PublishNav2Tf() { // Send the transform tf_br_.sendTransform(tf_odom_base); - // Publish WGS84 datum + // Publish datum in the configured LLH CRS +#if FPSDK_USE_PROJ + PublishDatum(trans_ecef_enu0, tfs_.enu0_poi_->header.stamp, llh_trafo_.get(), datum_pub_); +#else PublishDatum(trans_ecef_enu0, tfs_.enu0_poi_->header.stamp, datum_pub_); +#endif } /* ****************************************************************************************************************** */ diff --git a/fixposition_driver_ros1/src/params.cpp b/fixposition_driver_ros1/src/params.cpp index 14a90d79..43e7f4db 100644 --- a/fixposition_driver_ros1/src/params.cpp +++ b/fixposition_driver_ros1/src/params.cpp @@ -37,6 +37,14 @@ static bool LoadRosParam(const std::string& name, T& value) { return true; } +static bool HasRosParam(const std::string& name) { + try { + return ros::param::has(name); + } catch (ros::InvalidNameException& e) { + return false; + } +} + bool LoadParamsFromRos1(const std::string& ns, DriverParams& params) { bool ok = true; ROS_INFO("DriverParams: loading from %s", ns.c_str()); @@ -82,6 +90,21 @@ bool LoadParamsFromRos1(const std::string& ns, DriverParams& params) { ROS_WARN("Failed loading %s/nav2_mode param", ns.c_str()); ok = false; } + // Optional, keep defaults if datum_llh absent + if (HasRosParam(ns + "/datum_llh")) { + if (!LoadRosParam(ns + "/datum_llh/enabled", params.datum_llh_enabled_)) { + ROS_WARN("Failed loading %s/datum_llh/enabled param", ns.c_str()); + ok = false; + } + if (!LoadRosParam(ns + "/datum_llh/ecef_crs", params.datum_llh_ecef_crs_)) { + ROS_WARN("Failed loading %s/datum_llh/ecef_crs param", ns.c_str()); + ok = false; + } + if (!LoadRosParam(ns + "/datum_llh/llh_crs", params.datum_llh_llh_crs_)) { + ROS_WARN("Failed loading %s/datum_llh/llh_crs param", ns.c_str()); + ok = false; + } + } if (!LoadRosParam(ns + "/converter/enabled", params.converter_enabled_)) { ROS_WARN("Failed loading %s/converter/enabled param", ns.c_str()); ok = false; @@ -146,6 +169,9 @@ bool LoadParamsFromRos1(const std::string& ns, DriverParams& params) { ROS_INFO("DriverParams: raw_output=%s", params.raw_output_ ? "true" : "false"); ROS_INFO("DriverParams: cov_warning=%s", params.cov_warning_ ? "true" : "false"); ROS_INFO("DriverParams: nav2_mode=%s", params.nav2_mode_ ? "true" : "false"); + ROS_INFO("DriverParams: datum_llh_enabled=%s", params.datum_llh_enabled_ ? "true" : "false"); + ROS_INFO("DriverParams: datum_llh_ecef_crs=%s", params.datum_llh_ecef_crs_.c_str()); + ROS_INFO("DriverParams: datum_llh_llh_crs=%s", params.datum_llh_llh_crs_.c_str()); ROS_INFO("DriverParams: converter_enabled=%s", params.converter_enabled_ ? "true" : "false"); ROS_INFO("DriverParams: converter_topic_type=%s", topic_type_string_.c_str()); ROS_INFO("DriverParams: converter_input_topic=%s", params.converter_input_topic_.c_str()); diff --git a/fixposition_driver_ros2/include/fixposition_driver_ros2/data_to_ros2.hpp b/fixposition_driver_ros2/include/fixposition_driver_ros2/data_to_ros2.hpp index b3f9ab35..a71e18c8 100644 --- a/fixposition_driver_ros2/include/fixposition_driver_ros2/data_to_ros2.hpp +++ b/fixposition_driver_ros2/include/fixposition_driver_ros2/data_to_ros2.hpp @@ -15,7 +15,6 @@ #define __FIXPOSITION_DRIVER_ROS2_DATA_TO_ROS2_HPP__ /* LIBC/STL */ -#include #include /* EXTERNAL */ @@ -40,6 +39,9 @@ void PublishFpaOdometry(const fpsdk::common::parser::fpa::FpaOdometryPayload& pa void PublishFpaOdometryDataImu(const fpsdk::common::parser::fpa::FpaOdometryPayload& payload, bool nav2_mode_, rclcpp::Publisher::SharedPtr& pub); void PublishFpaOdometryDataNavSatFix(const fpsdk::common::parser::fpa::FpaOdometryPayload& payload, bool nav2_mode_, +#if FPSDK_USE_PROJ + const fpsdk::common::trafo::Transformer* llh_trafo, +#endif rclcpp::Publisher::SharedPtr& pub); void PublishFpaOdomenu(const fpsdk::common::parser::fpa::FpaOdomenuPayload& payload, rclcpp::Publisher::SharedPtr& pub); @@ -105,6 +107,9 @@ void PublishNmeaEpochData(const NmeaEpochData& data, rclcpp::Publisher::SharedPtr& pub); void PublishJumpWarning(const JumpDetector& jump_detector, rclcpp::Publisher::SharedPtr& pub); void PublishDatum(const geometry_msgs::msg::Vector3& payload, const builtin_interfaces::msg::Time& stamp, +#if FPSDK_USE_PROJ + const fpsdk::common::trafo::Transformer* llh_trafo, +#endif rclcpp::Publisher::SharedPtr& pub); void PublishFusionEpochData(const FusionEpochData& data, rclcpp::Publisher::SharedPtr& pub); diff --git a/fixposition_driver_ros2/include/fixposition_driver_ros2/fixposition_driver_node.hpp b/fixposition_driver_ros2/include/fixposition_driver_ros2/fixposition_driver_node.hpp index 44451d69..e784fc76 100644 --- a/fixposition_driver_ros2/include/fixposition_driver_ros2/fixposition_driver_node.hpp +++ b/fixposition_driver_ros2/include/fixposition_driver_ros2/fixposition_driver_node.hpp @@ -67,6 +67,10 @@ class FixpositionDriverNode { FixpositionDriver driver_; //!< Sensor driver rclcpp::QoS qos_settings_; //!< QoS settings +#if FPSDK_USE_PROJ + std::unique_ptr llh_trafo_; //!< ECEF to LLH, nullptr = built-in WGS84 +#endif + // ROS publishers // - FP_A messages rclcpp::Publisher::SharedPtr fpa_eoe_pub_; //!< FP_A-EOE message diff --git a/fixposition_driver_ros2/launch/config.yaml b/fixposition_driver_ros2/launch/config.yaml index 6b951101..6767c796 100644 --- a/fixposition_driver_ros2/launch/config.yaml +++ b/fixposition_driver_ros2/launch/config.yaml @@ -56,6 +56,13 @@ cov_warning: false # Enable covariance warnings nav2_mode: false # Enable nav2 mode + # Optional: omitting this block preserves the legacy WGS-84 LLH output. + datum_llh: + enabled: false # Use PROJ to convert the altitude published on odometry_llh and datum + ecef_crs: "EPSG:4978" # Geocentric source CRS of the sensor's ECEF coordinates; normally do not change + llh_crs: "EPSG:4979" # Geographic output CRS. For EGM2008 orthometric height use "EPSG:4326+3855" + # and install the us_nga_egm08_25.tif PROJ grid. Missing grids are treated as errors. + converter: enabled: false topic_type: "Twist" # Supported types: nav_msgs/{Twist, TwistWithCov, Odometry} diff --git a/fixposition_driver_ros2/src/data_to_ros2.cpp b/fixposition_driver_ros2/src/data_to_ros2.cpp index e9952743..be0a42c3 100644 --- a/fixposition_driver_ros2/src/data_to_ros2.cpp +++ b/fixposition_driver_ros2/src/data_to_ros2.cpp @@ -154,6 +154,9 @@ void PublishFpaOdometryDataImu(const fpa::FpaOdometryPayload& payload, bool nav2 // --------------------------------------------------------------------------------------------------------------------- void PublishFpaOdometryDataNavSatFix(const fpa::FpaOdometryPayload& payload, bool nav2_mode_, +#if FPSDK_USE_PROJ + const trafo::Transformer* llh_trafo, +#endif rclcpp::Publisher::SharedPtr& pub) { if (pub->get_subscription_count() > 0) { sensor_msgs::msg::NavSatFix msg; @@ -174,14 +177,21 @@ void PublishFpaOdometryDataNavSatFix(const fpa::FpaOdometryPayload& payload, boo msg.position_covariance_type = msg.COVARIANCE_TYPE_UNKNOWN; cov_map = Eigen::Matrix3d::Zero(); // FIXME: necessary? } else { - const Eigen::Vector3d llh_pos = trafo::TfWgs84LlhEcef(pose.position); + Eigen::Vector3d llh_pos; +#if FPSDK_USE_PROJ + if (!EcefToLlhRad(llh_trafo, pose.position, llh_pos)) { + return; + } +#else + llh_pos = trafo::TfWgs84LlhEcef(pose.position); +#endif msg.latitude = math::RadToDeg(llh_pos(0)); msg.longitude = math::RadToDeg(llh_pos(1)); msg.altitude = llh_pos(2); // Populate LLH covariance const Eigen::Matrix3d p_cov_e = pose.cov.topLeftCorner(3, 3); - const Eigen::Matrix3d C_l_e = trafo::RotEnuEcef(pose.position); + const Eigen::Matrix3d C_l_e = trafo::RotEnuEcef(llh_pos.x(), llh_pos.y()); const Eigen::Matrix3d p_cov_l = C_l_e * p_cov_e * C_l_e.transpose(); cov_map = p_cov_l; msg.position_covariance_type = msg.COVARIANCE_TYPE_KNOWN; @@ -885,6 +895,9 @@ void PublishJumpWarning(const JumpDetector& jump_detector, rclcpp::Publisher::SharedPtr& pub) { if (pub->get_subscription_count() > 0) { sensor_msgs::msg::NavSatFix msg; @@ -893,7 +906,14 @@ void PublishDatum(const geometry_msgs::msg::Vector3& payload, const builtin_inte // Populate LLH position const Eigen::Vector3d position = {payload.x, payload.y, payload.z}; - const Eigen::Vector3d llh_pos = trafo::TfWgs84LlhEcef(position); + Eigen::Vector3d llh_pos; +#if FPSDK_USE_PROJ + if (!EcefToLlhRad(llh_trafo, position, llh_pos)) { + return; + } +#else + llh_pos = trafo::TfWgs84LlhEcef(position); +#endif msg.latitude = math::RadToDeg(llh_pos(0)); msg.longitude = math::RadToDeg(llh_pos(1)); msg.altitude = llh_pos(2); diff --git a/fixposition_driver_ros2/src/fixposition_driver_node.cpp b/fixposition_driver_ros2/src/fixposition_driver_node.cpp index 5352e3cf..e94426fe 100644 --- a/fixposition_driver_ros2/src/fixposition_driver_node.cpp +++ b/fixposition_driver_ros2/src/fixposition_driver_node.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include /* EXTERNAL */ @@ -66,6 +67,20 @@ FixpositionDriverNode::FixpositionDriverNode(std::shared_ptr nh, else if (params_.qos_type_ == "default_long") { qos_settings_ = rclcpp::QoS(rclcpp::KeepLast(10), rmw_qos_profile_default); } + +#if FPSDK_USE_PROJ + std::string error; + if (!InitLlhTrafo(params_, llh_trafo_, error)) { + throw std::runtime_error("Failed initializing PROJ transformation: " + error); + } + if (llh_trafo_) { + RCLCPP_INFO(logger_, "PROJ enabled for ECEF to LLH output conversion"); + } +#else + if (params_.datum_llh_enabled_) { + throw std::runtime_error("Failed initializing PROJ transformation: driver was built without PROJ support"); + } +#endif } FixpositionDriverNode::~FixpositionDriverNode() {} @@ -106,7 +121,11 @@ bool FixpositionDriverNode::StartNode() { auto odometry_payload = dynamic_cast(payload); PublishFpaOdometry(odometry_payload, fpa_odometry_pub_); PublishFpaOdometryDataImu(odometry_payload, params_.nav2_mode_, poiimu_pub_); +#if FPSDK_USE_PROJ + PublishFpaOdometryDataNavSatFix(odometry_payload, params_.nav2_mode_, llh_trafo_.get(), odometry_llh_pub_); +#else PublishFpaOdometryDataNavSatFix(odometry_payload, params_.nav2_mode_, odometry_llh_pub_); +#endif OdometryData odometry_data; odometry_data.SetFromFpaOdomPayload(odometry_payload); PublishOdometryData(odometry_data, odometry_ecef_pub_); @@ -736,8 +755,12 @@ void FixpositionDriverNode::PublishNav2Tf() { tf_odom_base.transform = tf2::toMsg(tf_ENU0POISH); tf_br_->sendTransform(tf_odom_base); - // Publish WGS84 datum + // Publish datum in the configured LLH CRS +#if FPSDK_USE_PROJ + PublishDatum(trans_ecef_enu0, tfs_.enu0_poi_->header.stamp, llh_trafo_.get(), datum_pub_); +#else PublishDatum(trans_ecef_enu0, tfs_.enu0_poi_->header.stamp, datum_pub_); +#endif } /* ****************************************************************************************************************** */ diff --git a/fixposition_driver_ros2/src/params.cpp b/fixposition_driver_ros2/src/params.cpp index 1e1298da..7e78992c 100644 --- a/fixposition_driver_ros2/src/params.cpp +++ b/fixposition_driver_ros2/src/params.cpp @@ -36,6 +36,9 @@ bool LoadParamsFromRos2(std::shared_ptr& nh, DriverParams& params) const std::string RAW_OUTPUT = "raw_output"; const std::string COV_WARNING = "cov_warning"; const std::string NAV2_MODE = "nav2_mode"; + const std::string DATUM_LLH_ENABLED = "datum_llh.enabled"; + const std::string DATUM_LLH_ECEF_CRS = "datum_llh.ecef_crs"; + const std::string DATUM_LLH_LLH_CRS = "datum_llh.llh_crs"; const std::string CONVERTER_ENABLED = "converter.enabled"; const std::string CONVERTER_INPUT_TOPIC = "converter.input_topic"; const std::string CONVERTER_SCALE_FACTOR = "converter.scale_factor"; @@ -58,6 +61,11 @@ bool LoadParamsFromRos2(std::shared_ptr& nh, DriverParams& params) nh->declare_parameter(RAW_OUTPUT, params.raw_output_); nh->declare_parameter(COV_WARNING, params.cov_warning_); nh->declare_parameter(NAV2_MODE, params.nav2_mode_); + // Capture the declared values directly. Parameter overrides are applied by declare_parameter(); when no override + // exists, the DriverParams defaults keep configurations created by older releases backward-compatible. + params.datum_llh_enabled_ = nh->declare_parameter(DATUM_LLH_ENABLED, params.datum_llh_enabled_); + params.datum_llh_ecef_crs_ = nh->declare_parameter(DATUM_LLH_ECEF_CRS, params.datum_llh_ecef_crs_); + params.datum_llh_llh_crs_ = nh->declare_parameter(DATUM_LLH_LLH_CRS, params.datum_llh_llh_crs_); nh->declare_parameter(CONVERTER_ENABLED, params.converter_enabled_); nh->declare_parameter(CONVERTER_INPUT_TOPIC, params.converter_input_topic_); nh->declare_parameter(CONVERTER_SCALE_FACTOR, params.converter_scale_factor_); @@ -178,6 +186,9 @@ bool LoadParamsFromRos2(std::shared_ptr& nh, DriverParams& params) RCLCPP_INFO(logger, "DriverParams: raw_output=%s", params.raw_output_ ? "true" : "false"); RCLCPP_INFO(logger, "DriverParams: cov_warning=%s", params.cov_warning_ ? "true" : "false"); RCLCPP_INFO(logger, "DriverParams: nav2_mode=%s", params.nav2_mode_ ? "true" : "false"); + RCLCPP_INFO(logger, "DriverParams: datum_llh_enabled=%s", params.datum_llh_enabled_ ? "true" : "false"); + RCLCPP_INFO(logger, "DriverParams: datum_llh_ecef_crs=%s", params.datum_llh_ecef_crs_.c_str()); + RCLCPP_INFO(logger, "DriverParams: datum_llh_llh_crs=%s", params.datum_llh_llh_crs_.c_str()); RCLCPP_INFO(logger, "DriverParams: converter_enabled=%s", params.converter_enabled_ ? "true" : "false"); RCLCPP_INFO(logger, "DriverParams: converter_topic_type=%s", topic_type_string_.c_str()); RCLCPP_INFO(logger, "DriverParams: converter_input_topic=%s", params.converter_input_topic_.c_str());