From 4ad13c36400e52a0e9407a4c6e464f5b4839e259 Mon Sep 17 00:00:00 2001 From: Julian Oes Date: Mon, 13 Jul 2026 11:43:05 +1200 Subject: [PATCH 1/5] core: switch JSON library from jsoncpp to nlohmann/json Replace the compiled jsoncpp dependency with the header-only nlohmann/json (v3.11.3, already present transitively via libevents). - Parse paths use the non-throwing json::parse(..., allow_exceptions=false) with predicate-guarded access to stay abort-free under -fno-exceptions. - Rework the MavlinkDirect serializer (libmav_receiver) onto nlohmann::ordered_json + dump(), which fixes a latent string-escaping bug and renders NaN/Inf as null, while preserving field order. - Migrate mission import and component metadata (parse and serialize) and the system tests accordingly. - Update build wiring, packaging (.pc, CMake config, debian), CI, docs, and the mavlink_direct_sender example. --- .github/workflows/linux.yml | 5 +- cpp/cmake/Modules/common/Findjsoncpp.cmake | 52 ---- cpp/debian/control | 4 +- cpp/examples/mavlink_direct_sender/sender.cpp | 2 +- cpp/src/MAVSDKConfig.cmake.in | 2 +- cpp/src/mavsdk.pc.in | 2 +- cpp/src/mavsdk/CMakeLists.txt | 4 +- cpp/src/mavsdk/core/libmav_conversions.cpp | 116 +++++---- cpp/src/mavsdk/core/libmav_receiver.cpp | 104 ++------ .../core/mavlink_component_metadata.cpp | 50 ++-- .../core/mavlink_component_metadata.hpp | 4 +- .../component_metadata_server_impl.cpp | 19 +- .../plugins/mission_raw/mission_import.cpp | 223 ++++++++++------- .../plugins/mission_raw/mission_import.hpp | 22 +- cpp/src/system_tests/CMakeLists.txt | 4 +- cpp/src/system_tests/intercept.cpp | 50 ++-- cpp/src/system_tests/mavlink_direct.cpp | 235 +++++++++--------- .../mavlink_direct_forwarding.cpp | 24 +- .../system_tests/mavlink_direct_server.cpp | 32 +-- .../param_change_notification.cpp | 20 +- .../system_tests/request_message_rapid.cpp | 10 +- cpp/src/unit_tests/CMakeLists.txt | 4 +- cpp/third_party/CMakeLists.txt | 2 +- .../{jsoncpp => nlohmann_json}/CMakeLists.txt | 20 +- cpp/tools/build-with-system-deps.sh | 4 +- docs/en/cpp/guide/build_linux.md | 2 +- docs/en/cpp/guide/mavlink_direct.md | 15 +- 27 files changed, 487 insertions(+), 544 deletions(-) delete mode 100644 cpp/cmake/Modules/common/Findjsoncpp.cmake rename cpp/third_party/{jsoncpp => nlohmann_json}/CMakeLists.txt (70%) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 707595d9dd..4ee7c8f030 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -359,7 +359,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y liblzma-dev libtinyxml2-dev libjsoncpp-dev libcurl4-openssl-dev libssl-dev libasio-dev libfmt-dev + sudo apt-get install -y liblzma-dev libtinyxml2-dev nlohmann-json3-dev libcurl4-openssl-dev libssl-dev libasio-dev libfmt-dev - name: Build with system dependencies run: ./tools/build-with-system-deps.sh - name: unit tests @@ -385,7 +385,8 @@ jobs: sudo apt-get update && sudo apt-get install -y \ python3-venv \ libasio-dev \ - libfmt-dev + libfmt-dev \ + nlohmann-json3-dev - name: git permission workaround run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" diff --git a/cpp/cmake/Modules/common/Findjsoncpp.cmake b/cpp/cmake/Modules/common/Findjsoncpp.cmake deleted file mode 100644 index 21dd6eaccf..0000000000 --- a/cpp/cmake/Modules/common/Findjsoncpp.cmake +++ /dev/null @@ -1,52 +0,0 @@ -find_package(jsoncpp CONFIG QUIET) - -if(NOT TARGET JsonCpp::JsonCpp) - if(JsonCpp_FOUND) - message(STATUS "Found jsoncpp via config file: ${JsonCpp_DIR}") - if(NOT JsonCpp_LIBRARY) - if(TARGET jsoncpp) - set(JsonCpp_LIBRARY jsoncpp) - elseif(TARGET JsonCpp::JsonCpp) - set(JsonCpp_LIBRARY JsonCpp::JsonCpp) - endif() - endif() - else() - # Try to locate it via pkg-config. If that yields a result, this is - # considered authoritative. This MUST be done in order to ensure that it - # works correctly in multi-arch builds. - find_package(PkgConfig QUIET) - if(PKG_CONFIG_FOUND) - pkg_check_modules(PC_JSONCPP QUIET IMPORTED_TARGET GLOBAL jsoncpp) - if(PC_JSONCPP_FOUND) - add_library(JsonCpp::JsonCpp ALIAS PkgConfig::PC_JSONCPP) - endif() - endif() - - if(NOT PC_JSONCPP_FOUND) - find_path(JsonCpp_INCLUDE_DIR - NAMES json/json.h - HINTS ${PC_JSONCPP_INCLUDEDIR} ${PC_JSONCPP_INCLUDE_DIRS}) - find_library(JsonCpp_LIBRARY - NAMES jsoncpp - HINTS ${PC_JSONCPP_LIBDIR} ${PC_JSONCPP_LIBRARY_DIRS}) - - include(FindPackageHandleStandardArgs) - find_package_handle_standard_args(JsonCpp DEFAULT_MSG JsonCpp_LIBRARY JsonCpp_INCLUDE_DIR) - - mark_as_advanced(JsonCpp_INCLUDE_DIR JsonCpp_LIBRARY) - endif() - endif() - - if (JsonCpp_FOUND) - add_library(JsonCpp::JsonCpp INTERFACE IMPORTED) - set_target_properties(JsonCpp::JsonCpp PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${JsonCpp_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES "${JsonCpp_LIBRARY}") - if(NOT JsonCpp_LIBRARY) - unset(JsonCpp_LIBRARY CACHE) - endif() - if(NOT JsonCpp_INCLUDE_DIR) - unset(JsonCpp_INCLUDE_DIR CACHE) - endif() - endif() -endif() diff --git a/cpp/debian/control b/cpp/debian/control index 07a385e663..5d960cb42a 100644 --- a/cpp/debian/control +++ b/cpp/debian/control @@ -1,7 +1,7 @@ Source: mavsdk Priority: optional Maintainer: hcb -Build-Depends: debhelper (>= 11), libjsoncpp-dev, libcurl4-openssl-dev, libtinyxml2-dev +Build-Depends: debhelper (>= 11), nlohmann-json3-dev, libcurl4-openssl-dev, libtinyxml2-dev Standards-Version: 4.1.3 Section: libs Homepage: https://mavsdk.mavlink.io/main/en/index.html @@ -19,6 +19,6 @@ Description: MAVSDK API for Mavlink-controlled vehicles Package: libmavsdk0 Architecture: any Multi-Arch: same -Depends: ${shlibs:Depends}, ${misc:Depends}, libjsoncpp1, libcurl4, libtinyxml2-6a +Depends: ${shlibs:Depends}, ${misc:Depends}, libcurl4, libtinyxml2-6a Description: MAVSDK API for Mavlink-controlled vehicles This provides the runtime libraries for the mavsdk C++ library. diff --git a/cpp/examples/mavlink_direct_sender/sender.cpp b/cpp/examples/mavlink_direct_sender/sender.cpp index 5fd23d63ce..0729909de9 100644 --- a/cpp/examples/mavlink_direct_sender/sender.cpp +++ b/cpp/examples/mavlink_direct_sender/sender.cpp @@ -66,7 +66,7 @@ int main(int argc, char** argv) // On 3 segments we see an obstacle, the closest is at 7m. // // In a real application this JSON message content would of course be - // assembled manually, or using a json library like jsoncpp or nlohmann/json. + // assembled manually, or using a json library like nlohmann/json. obstacle_distance.fields_json = R"({ "time_usec": 12345678, "sensor_type": 3, diff --git a/cpp/src/MAVSDKConfig.cmake.in b/cpp/src/MAVSDKConfig.cmake.in index c5ad4e6ff9..8ab4cf1850 100644 --- a/cpp/src/MAVSDKConfig.cmake.in +++ b/cpp/src/MAVSDKConfig.cmake.in @@ -7,7 +7,7 @@ if(NOT @BUILD_SHARED_LIBS@) endif() find_dependency(Threads REQUIRED) - find_dependency(jsoncpp REQUIRED) + find_dependency(nlohmann_json REQUIRED) find_dependency(picosha2 REQUIRED) find_dependency(tinyxml2 REQUIRED) find_dependency(mav REQUIRED) diff --git a/cpp/src/mavsdk.pc.in b/cpp/src/mavsdk.pc.in index 320c33d15a..b488c8cbf1 100644 --- a/cpp/src/mavsdk.pc.in +++ b/cpp/src/mavsdk.pc.in @@ -6,6 +6,6 @@ includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@ Name: MAVSDK Description: API and library for MAVLink compatible systems written in C++17 Version: @MAVSDK_VERSION_STRING@ -Requires.private: libcurl jsoncpp tinyxml2 +Requires.private: libcurl nlohmann_json tinyxml2 Libs: -L"${libdir}" -lmavsdk Cflags: -I"${includedir}" -I"${includedir}/mavsdk" diff --git a/cpp/src/mavsdk/CMakeLists.txt b/cpp/src/mavsdk/CMakeLists.txt index 39277b608b..a1ab40fc15 100644 --- a/cpp/src/mavsdk/CMakeLists.txt +++ b/cpp/src/mavsdk/CMakeLists.txt @@ -2,7 +2,7 @@ add_library(mavsdk) cmake_policy(SET CMP0076 NEW) -find_package(jsoncpp REQUIRED) +find_package(nlohmann_json REQUIRED) find_package(tinyxml2 REQUIRED) @@ -16,7 +16,7 @@ target_link_libraries(mavsdk PUBLIC ${CMAKE_DL_LIBS} PRIVATE - JsonCpp::JsonCpp + nlohmann_json::nlohmann_json tinyxml2::tinyxml2 LibEvents::LibEvents mav::mav diff --git a/cpp/src/mavsdk/core/libmav_conversions.cpp b/cpp/src/mavsdk/core/libmav_conversions.cpp index 1c7f01c7e5..def1a94f95 100644 --- a/cpp/src/mavsdk/core/libmav_conversions.cpp +++ b/cpp/src/mavsdk/core/libmav_conversions.cpp @@ -3,7 +3,7 @@ #include "log.hpp" #include -#include +#include #include #include #include @@ -12,34 +12,34 @@ namespace mavsdk { bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) { - Json::Value json; - Json::Reader reader; + // Parse without exceptions (MAVSDK is built with -fno-exceptions); a parse + // error yields a discarded value instead of throwing. + auto json = nlohmann::json::parse(json_string, nullptr, false); - if (!reader.parse(json_string, json)) { + if (json.is_discarded() || !json.is_object()) { LogErr("Failed to parse JSON: {}", json_string); return false; } // Iterate through all JSON fields and set them in the libmav message - for (const auto& field_name : json.getMemberNames()) { - const Json::Value& field_value = json[field_name]; - + for (const auto& [field_name, field_value] : json.items()) { // Convert JSON values to appropriate types and set in message // libmav handles type casting based on field definition, so we just need to pass - // int64/uint64 - if (field_value.isInt() || field_value.isInt64()) { - int64_t value = field_value.asInt64(); + // int64/uint64. Check unsigned first since nlohmann stores non-negative + // integer literals as unsigned. + if (field_value.is_number_unsigned()) { + uint64_t value = field_value.get(); auto result = msg.set(field_name, value); if (result != ::mav::MessageResult::Success) { - LogWarn("Failed to set integer field {} = {}", field_name, value); + LogWarn("Failed to set unsigned integer field {} = {}", field_name, value); } - } else if (field_value.isUInt() || field_value.isUInt64()) { - uint64_t value = field_value.asUInt64(); + } else if (field_value.is_number_integer()) { + int64_t value = field_value.get(); auto result = msg.set(field_name, value); if (result != ::mav::MessageResult::Success) { - LogWarn("Failed to set unsigned integer field {} = {}", field_name, value); + LogWarn("Failed to set integer field {} = {}", field_name, value); } - } else if (field_value.isNull() || field_value.isDouble()) { + } else if (field_value.is_null() || field_value.is_number_float()) { // Handle float/double values (including null -> NaN) auto field_opt = msg.type().getField(field_name); if (!field_opt) { @@ -51,16 +51,16 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) ::mav::MessageResult result = ::mav::MessageResult::FieldNotFound; if (field.type.base_type == ::mav::FieldType::BaseType::FLOAT) { - if (field_value.isNull()) { + if (field_value.is_null()) { result = msg.set(field_name, std::numeric_limits::quiet_NaN()); } else { - result = msg.set(field_name, static_cast(field_value.asFloat())); + result = msg.set(field_name, field_value.get()); } } else if (field.type.base_type == ::mav::FieldType::BaseType::DOUBLE) { - if (field_value.isNull()) { + if (field_value.is_null()) { result = msg.set(field_name, std::numeric_limits::quiet_NaN()); } else { - result = msg.set(field_name, field_value.asDouble()); + result = msg.set(field_name, field_value.get()); } } else { LogWarn("Field {} is not a float or double field", field_name); @@ -71,14 +71,15 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) LogWarn( "Failed to set float/double field {} = {}", field_name, - (field_value.isNull() ? "null" : std::to_string(field_value.asDouble()))); + (field_value.is_null() ? "null" : std::to_string(field_value.get()))); } - } else if (field_value.isString()) { - auto result = msg.setString(field_name, field_value.asString()); + } else if (field_value.is_string()) { + auto value = field_value.get(); + auto result = msg.setString(field_name, value); if (result != ::mav::MessageResult::Success) { - LogWarn("Failed to set string field {} = {}", field_name, field_value.asString()); + LogWarn("Failed to set string field {} = {}", field_name, value); } - } else if (field_value.isArray()) { + } else if (field_value.is_array()) { // Handle array fields with proper type detection auto array_size = field_value.size(); @@ -101,9 +102,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::UINT8: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - vec.push_back(elem.isNumeric() ? static_cast(elem.asUInt()) : 0); + for (const auto& elem : field_value) { + vec.push_back( + elem.is_number() ? static_cast(elem.get()) : 0); } result = msg.set(field_name, vec); break; @@ -111,9 +112,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::UINT16: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - vec.push_back(elem.isNumeric() ? static_cast(elem.asUInt()) : 0); + for (const auto& elem : field_value) { + vec.push_back( + elem.is_number() ? static_cast(elem.get()) : 0); } result = msg.set(field_name, vec); break; @@ -121,9 +122,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::UINT32: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - vec.push_back(elem.isNumeric() ? static_cast(elem.asUInt()) : 0); + for (const auto& elem : field_value) { + vec.push_back( + elem.is_number() ? static_cast(elem.get()) : 0); } result = msg.set(field_name, vec); break; @@ -131,10 +132,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::UINT64: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; + for (const auto& elem : field_value) { vec.push_back( - elem.isNumeric() ? static_cast(elem.asUInt64()) : 0); + elem.is_number() ? static_cast(elem.get()) : 0); } result = msg.set(field_name, vec); break; @@ -142,9 +142,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::INT8: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - vec.push_back(elem.isNumeric() ? static_cast(elem.asInt()) : 0); + for (const auto& elem : field_value) { + vec.push_back( + elem.is_number() ? static_cast(elem.get()) : 0); } result = msg.set(field_name, vec); break; @@ -152,9 +152,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::INT16: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - vec.push_back(elem.isNumeric() ? static_cast(elem.asInt()) : 0); + for (const auto& elem : field_value) { + vec.push_back( + elem.is_number() ? static_cast(elem.get()) : 0); } result = msg.set(field_name, vec); break; @@ -162,9 +162,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::INT32: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - vec.push_back(elem.isNumeric() ? static_cast(elem.asInt()) : 0); + for (const auto& elem : field_value) { + vec.push_back( + elem.is_number() ? static_cast(elem.get()) : 0); } result = msg.set(field_name, vec); break; @@ -172,9 +172,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::INT64: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - vec.push_back(elem.isNumeric() ? static_cast(elem.asInt64()) : 0); + for (const auto& elem : field_value) { + vec.push_back( + elem.is_number() ? static_cast(elem.get()) : 0); } result = msg.set(field_name, vec); break; @@ -182,10 +182,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::FLOAT: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - if (elem.isNumeric()) { - vec.push_back(static_cast(elem.asFloat())); + for (const auto& elem : field_value) { + if (elem.is_number()) { + vec.push_back(elem.get()); } else { // For non-numeric values (including null), use NaN vec.push_back(std::numeric_limits::quiet_NaN()); @@ -197,10 +196,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::DOUBLE: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - if (elem.isNumeric()) { - vec.push_back(elem.asDouble()); + for (const auto& elem : field_value) { + if (elem.is_number()) { + vec.push_back(elem.get()); } else { // For non-numeric values (including null), use NaN vec.push_back(std::numeric_limits::quiet_NaN()); @@ -212,9 +210,9 @@ bool json_to_libmav_message(const std::string& json_string, mav::Message& msg) case ::mav::FieldType::BaseType::CHAR: { std::vector vec; vec.reserve(array_size); - for (Json::ArrayIndex i = 0; i < array_size; ++i) { - const auto& elem = field_value[i]; - vec.push_back(elem.isNumeric() ? static_cast(elem.asInt()) : 0); + for (const auto& elem : field_value) { + vec.push_back( + elem.is_number() ? static_cast(elem.get()) : 0); } result = msg.set(field_name, vec); break; diff --git a/cpp/src/mavsdk/core/libmav_receiver.cpp b/cpp/src/mavsdk/core/libmav_receiver.cpp index 25cd793b8a..1b53b28484 100644 --- a/cpp/src/mavsdk/core/libmav_receiver.cpp +++ b/cpp/src/mavsdk/core/libmav_receiver.cpp @@ -2,11 +2,9 @@ #include "mavsdk_impl.hpp" #include #include -#include +#include #include #include -#include -#include #include "log.hpp" namespace mavsdk { @@ -127,10 +125,12 @@ bool LibmavReceiver::parse_libmav_message_from_buffer() std::string LibmavReceiver::libmav_message_to_json(const mav::Message& msg) const { - std::ostringstream json_stream; - json_stream << "{"; - json_stream << "\"message_id\":" << msg.id(); - json_stream << ",\"message_name\":\"" << msg.name() << "\""; + // Use ordered_json so fields keep their MAVLink XML-definition order + // (plain nlohmann::json would sort keys alphabetically). nlohmann handles + // string escaping and renders NaN/Inf as null on dump(). + nlohmann::ordered_json json; + json["message_id"] = msg.id(); + json["message_name"] = msg.name(); // Get message definition to iterate through all fields (using thread-safe method) auto message_def_opt = _mavsdk_impl.get_message_definition_safe(static_cast(msg.id())); @@ -141,97 +141,35 @@ std::string LibmavReceiver::libmav_message_to_json(const mav::Message& msg) cons // Get field names and iterate through them auto field_names = message_def.fieldNames(); for (const auto& field_name : field_names) { - json_stream << ",\"" << field_name << "\":"; - // Extract field value based on type and convert to JSON auto variant_opt = msg.getAsNativeTypeInVariant(field_name); if (variant_opt) { - const auto& variant = variant_opt.value(); - - // Convert variant to JSON string based on the field type std::visit( - [&json_stream](const auto& value) { + [&json, &field_name](const auto& value) { using T = std::decay_t; - if constexpr ( - std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v) { - json_stream << static_cast(value); - } else if constexpr (std::is_same_v) { - json_stream << static_cast(value); - } else if constexpr ( - std::is_same_v || std::is_same_v) { - if (!std::isfinite(value)) { - json_stream << "null"; - } else { - json_stream << value; - } - } else if constexpr (std::is_same_v) { - json_stream << "\"" << value << "\""; - } else if constexpr ( - std::is_same_v> || - std::is_same_v>) { - // Handle uint8_t/int8_t vectors specially to avoid character output - json_stream << "["; - bool first = true; - for (const auto& elem : value) { - if (!first) - json_stream << ","; - first = false; - json_stream << static_cast(elem); - } - json_stream << "]"; - } else if constexpr ( - std::is_same_v> || - std::is_same_v> || - std::is_same_v> || - std::is_same_v> || - std::is_same_v> || - std::is_same_v>) { - // Handle integer vector types - json_stream << "["; - bool first = true; - for (const auto& elem : value) { - if (!first) - json_stream << ","; - first = false; - json_stream << elem; - } - json_stream << "]"; - } else if constexpr ( - std::is_same_v> || - std::is_same_v>) { - // Handle float/double vector types with NaN check - json_stream << "["; - bool first = true; - for (const auto& elem : value) { - if (!first) - json_stream << ","; - first = false; - if (!std::isfinite(elem)) { - json_stream << "null"; - } else { - json_stream << elem; - } - } - json_stream << "]"; + if constexpr (std::is_same_v) { + // Represent a scalar char as its numeric value. + json[field_name] = static_cast(value); } else { - // Fallback for unknown types - json_stream << "null"; + // Scalars and vectors (incl. uint8/int8, which + // nlohmann serializes as number arrays) assign + // directly; NaN/Inf become null on dump(). + json[field_name] = value; } }, - variant); + variant_opt.value()); } else { // Field not present or failed to extract - json_stream << "null"; + json[field_name] = nullptr; } } } - json_stream << "}"; - return json_stream.str(); + // MAVLink char[] fields can carry raw binary (non-UTF-8) bytes. Use the + // replace error handler so dump() emits U+FFFD instead of throwing (which, + // under -fno-exceptions, would abort). + return json.dump(-1, ' ', false, nlohmann::ordered_json::error_handler_t::replace); } std::optional LibmavReceiver::message_id_to_name(uint32_t id) const diff --git a/cpp/src/mavsdk/core/mavlink_component_metadata.cpp b/cpp/src/mavsdk/core/mavlink_component_metadata.cpp index 992b2ea7ca..00ccd151bc 100644 --- a/cpp/src/mavsdk/core/mavlink_component_metadata.cpp +++ b/cpp/src/mavsdk/core/mavlink_component_metadata.cpp @@ -421,41 +421,41 @@ MavlinkComponentMetadata::get_metadata(uint32_t compid, MetadataType type) void MavlinkComponentMetadata::parse_component_metadata_general( uint8_t compid, const std::string& json_metadata) { - Json::Value metadata; - Json::Reader reader; - bool parsing_successful = reader.parse(json_metadata, metadata); - if (!parsing_successful) { - LogErr("Failed to parse{}", reader.getFormattedErrorMessages()); + // Parse without exceptions (MAVSDK is built with -fno-exceptions). + auto metadata = nlohmann::json::parse(json_metadata, nullptr, false); + if (metadata.is_discarded() || !metadata.is_object()) { + LogErr("Failed to parse component metadata JSON"); return; } - if (!metadata.isMember("version")) { + if (!metadata.contains("version") || !metadata["version"].is_number_integer()) { LogErr("version not found"); return; } - if (metadata["version"].asInt() != 1) { - LogWarn("version {} not supported", metadata["version"].asInt()); + if (metadata["version"].get() != 1) { + LogWarn("version {} not supported", metadata["version"].get()); return; } - if (!metadata.isMember("metadataTypes")) { + if (!metadata.contains("metadataTypes") || !metadata["metadataTypes"].is_array()) { LogErr("metadataTypes not found"); return; } for (const auto& metadata_type : metadata["metadataTypes"]) { - if (!metadata_type.isMember("type")) { + if (!metadata_type.is_object() || !metadata_type.contains("type") || + !metadata_type["type"].is_number_integer()) { LogErr("type missing"); continue; } - auto type = static_cast(metadata_type["type"].asInt()); + auto type = static_cast(metadata_type["type"].get()); auto& components = _mavlink_components[compid].components; if (components.find(type) != components.end()) { LogErr("component type already added: {}", static_cast(type)); continue; } - if (!metadata_type.isMember("uri")) { + if (!metadata_type.contains("uri")) { LogErr("uri missing"); continue; } @@ -502,27 +502,27 @@ void MavlinkComponentMetadata::on_all_types_completed(uint8_t compid) [this](const auto& func) { _system_impl.call_user_callback(func); }); } -MetadataComponentUris::MetadataComponentUris(const Json::Value& value) +MetadataComponentUris::MetadataComponentUris(const nlohmann::json& value) { - if (value["uri"].isString()) { - _uri_metadata = value["uri"].asString(); + if (value.contains("uri") && value["uri"].is_string()) { + _uri_metadata = value["uri"].get(); } - if (value["fileCrc"].isUInt()) { - _crc_metadata = value["fileCrc"].asUInt(); + if (value.contains("fileCrc") && value["fileCrc"].is_number_unsigned()) { + _crc_metadata = value["fileCrc"].get(); _crc_metadata_valid = true; } - if (value["uriFallback"].isString()) { - _uri_metadata_fallback = value["uriFallback"].asString(); + if (value.contains("uriFallback") && value["uriFallback"].is_string()) { + _uri_metadata_fallback = value["uriFallback"].get(); } - if (value["fileCrcFallback"].isUInt()) { - _crc_metadata_fallback = value["fileCrcFallback"].asUInt(); + if (value.contains("fileCrcFallback") && value["fileCrcFallback"].is_number_unsigned()) { + _crc_metadata_fallback = value["fileCrcFallback"].get(); _crc_metadata_fallback_valid = true; } - if (value["translationUri"].isString()) { - _uri_translation = value["translationUri"].asString(); + if (value.contains("translationUri") && value["translationUri"].is_string()) { + _uri_translation = value["translationUri"].get(); } - if (value["translationUriFallback"].isString()) { - _uri_translation_fallback = value["translationUriFallback"].asString(); + if (value.contains("translationUriFallback") && value["translationUriFallback"].is_string()) { + _uri_translation_fallback = value["translationUriFallback"].get(); } } diff --git a/cpp/src/mavsdk/core/mavlink_component_metadata.hpp b/cpp/src/mavsdk/core/mavlink_component_metadata.hpp index 516a905aa3..0eb6057ab3 100644 --- a/cpp/src/mavsdk/core/mavlink_component_metadata.hpp +++ b/cpp/src/mavsdk/core/mavlink_component_metadata.hpp @@ -4,7 +4,7 @@ #include "mavsdk_export.h" #include "file_cache.hpp" #include "mavlink_command_sender.hpp" -#include +#include #if BUILD_WITHOUT_CURL != 1 #include "http_loader.hpp" @@ -26,7 +26,7 @@ class MetadataComponentUris { _crc_metadata(metadata_crc), _uri_metadata(std::move(metadata_uri)) {} - explicit MetadataComponentUris(const Json::Value& value); + explicit MetadataComponentUris(const nlohmann::json& value); const std::string& uri_metadata() const { return _uri_metadata; } const std::string& uri_metadata_fallback() const { return _uri_metadata_fallback; } diff --git a/cpp/src/mavsdk/plugins/component_metadata_server/component_metadata_server_impl.cpp b/cpp/src/mavsdk/plugins/component_metadata_server/component_metadata_server_impl.cpp index 8bb9f4b337..8107cbeb69 100644 --- a/cpp/src/mavsdk/plugins/component_metadata_server/component_metadata_server_impl.cpp +++ b/cpp/src/mavsdk/plugins/component_metadata_server/component_metadata_server_impl.cpp @@ -7,7 +7,7 @@ #include "crc32.hpp" #include -#include +#include namespace mavsdk { @@ -133,15 +133,17 @@ void ComponentMetadataServerImpl::set_metadata( bool ComponentMetadataServerImpl::generate_component_metadata_general_file() { - Json::Value root; + // ordered_json keeps the human-readable key order (version, then + // metadataTypes); the parser reads keys by name so order is not required. + nlohmann::ordered_json root; root["version"] = 1; - Json::Value metadata_types = Json::arrayValue; + auto metadata_types = nlohmann::ordered_json::array(); for (const auto& metadata : _metadata) { - Json::Value metadata_type; - metadata_type["type"] = Json::Int{metadata.type}; + nlohmann::ordered_json metadata_type; + metadata_type["type"] = static_cast(metadata.type); metadata_type["uri"] = "mftp://" + metadata.filename; - metadata_type["fileCrc"] = Json::UInt{metadata.crc}; - metadata_types.append(metadata_type); + metadata_type["fileCrc"] = metadata.crc; + metadata_types.push_back(metadata_type); } root["metadataTypes"] = metadata_types; @@ -151,7 +153,8 @@ bool ComponentMetadataServerImpl::generate_component_metadata_general_file() LogErr("Failed to open {}", path.string()); return false; } - const std::string json_data = root.toStyledString(); + const std::string json_data = + root.dump(4, ' ', false, nlohmann::ordered_json::error_handler_t::replace); Crc32 crc{}; crc.add(reinterpret_cast(json_data.data()), json_data.length()); _comp_info_general_crc = crc.get(); diff --git a/cpp/src/mavsdk/plugins/mission_raw/mission_import.cpp b/cpp/src/mavsdk/plugins/mission_raw/mission_import.cpp index d2a55135da..6f8e31566f 100644 --- a/cpp/src/mavsdk/plugins/mission_raw/mission_import.cpp +++ b/cpp/src/mavsdk/plugins/mission_raw/mission_import.cpp @@ -6,16 +6,62 @@ namespace mavsdk { +namespace { + +// nlohmann's const operator[] aborts on a missing key (MAVSDK is built with +// -fno-exceptions), unlike jsoncpp's which returned a null value. These helpers +// restore that null-tolerant behavior for both object keys and array indices. +const nlohmann::json& json_at(const nlohmann::json& j, const char* key) +{ + static const nlohmann::json null_json{}; + if (j.is_object()) { + const auto it = j.find(key); + if (it != j.end()) { + return *it; + } + } + return null_json; +} + +// Index overload takes int (not size_t) so a literal 0 is an exact match and +// does not become ambiguous with the const char* key overload. +const nlohmann::json& json_at(const nlohmann::json& j, int index) +{ + static const nlohmann::json null_json{}; + if (j.is_array() && index >= 0 && static_cast(index) < j.size()) { + return j[static_cast(index)]; + } + return null_json; +} + +int json_get_int(const nlohmann::json& j) +{ + return j.is_number() ? j.get() : 0; +} + +bool json_get_bool(const nlohmann::json& j) +{ + return j.is_boolean() ? j.get() : false; +} + +std::string json_to_string(const nlohmann::json& j) +{ + // Use the replace error handler so non-UTF-8 content can't make dump() + // abort under -fno-exceptions. + return j.is_string() ? j.get() : + j.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace); +} + +} // namespace + std::pair MissionImport::parse_json(const std::string& raw_json, Autopilot autopilot) { - Json::CharReaderBuilder builder; - const std::unique_ptr reader(builder.newCharReader()); - Json::Value root; - JSONCPP_STRING err; + // Parse without exceptions (MAVSDK is built with -fno-exceptions). + auto root = nlohmann::json::parse(raw_json, nullptr, false); - if (!reader->parse(raw_json.c_str(), raw_json.c_str() + raw_json.length(), &root, &err)) { - LogErr("Parse error: {}", err); + if (root.is_discarded()) { + LogErr("Parse error"); return {MissionRaw::Result::FailedToParseQgcPlan, {}}; } @@ -46,14 +92,15 @@ MissionImport::parse_json(const std::string& raw_json, Autopilot autopilot) return {MissionRaw::Result::Success, import_data}; } -bool MissionImport::check_overall_version(const Json::Value& root) +bool MissionImport::check_overall_version(const nlohmann::json& root) { const auto supported_overall_version = 1; - const auto overall_version = root["version"]; - if (overall_version.empty() || overall_version.asInt() != supported_overall_version) { + const auto& overall_version = json_at(root, "version"); + if (!overall_version.is_number_integer() || + overall_version.get() != supported_overall_version) { LogErr( "Overall .plan version not supported, found version: {}, supported: {}", - overall_version.asInt(), + json_get_int(overall_version), supported_overall_version); return false; } @@ -62,10 +109,10 @@ bool MissionImport::check_overall_version(const Json::Value& root) } std::optional> -MissionImport::import_mission(const Json::Value& root, Autopilot autopilot) +MissionImport::import_mission(const nlohmann::json& root, Autopilot autopilot) { // We need a mission part. - const auto mission = root["mission"]; + const auto& mission = json_at(root, "mission"); if (mission.empty()) { LogErr("No mission found in .plan."); return std::nullopt; @@ -73,11 +120,12 @@ MissionImport::import_mission(const Json::Value& root, Autopilot autopilot) // Check the mission version. const auto supported_mission_version = 2; - const auto mission_version = mission["version"]; - if (mission_version.empty() || mission_version.asInt() != supported_mission_version) { + const auto& mission_version = json_at(mission, "version"); + if (!mission_version.is_number_integer() || + mission_version.get() != supported_mission_version) { LogErr( "mission version for .plan not supported, found version: {}, supported: {}", - mission_version.asInt(), + json_get_int(mission_version), supported_mission_version); return std::nullopt; } @@ -85,10 +133,10 @@ MissionImport::import_mission(const Json::Value& root, Autopilot autopilot) std::vector mission_items; // Go through items - for (const auto& json_item : mission["items"]) { - const auto type = json_item["type"]; + for (const auto& json_item : json_at(mission, "items")) { + const auto& type = json_at(json_item, "type"); - if (!type.isNull() && type.asString() == "SimpleItem") { + if (type.is_string() && type.get() == "SimpleItem") { const auto maybe_item = import_simple_mission_item(json_item); if (maybe_item.has_value()) { mission_items.push_back(maybe_item.value()); @@ -96,7 +144,7 @@ MissionImport::import_mission(const Json::Value& root, Autopilot autopilot) return std::nullopt; } - } else if (!type.isNull() && type.asString() == "ComplexItem") { + } else if (type.is_string() && type.get() == "ComplexItem") { const auto maybe_items = import_complex_mission_items(json_item); if (maybe_items.has_value()) { mission_items.insert( @@ -106,7 +154,7 @@ MissionImport::import_mission(const Json::Value& root, Autopilot autopilot) } } else { - LogErr("Type {} not understood.", type.asString()); + LogErr("Type {} not understood.", json_to_string(type)); return std::nullopt; } } @@ -118,9 +166,9 @@ MissionImport::import_mission(const Json::Value& root, Autopilot autopilot) // Add home position at 0 for ArduPilot if (autopilot == Autopilot::ArduPilot) { - const auto home = mission["plannedHomePosition"]; + const auto& home = json_at(mission, "plannedHomePosition"); if (!home.empty()) { - if (home.isArray() && home.size() != 3) { + if (!home.is_array() || home.size() != 3) { LogErr("Unknown plannedHomePosition format"); return std::nullopt; } @@ -145,9 +193,9 @@ MissionImport::import_mission(const Json::Value& root, Autopilot autopilot) 0.0f, 0.0f, 0.0f, - set_int32(home[0], MAV_FRAME_GLOBAL_INT), - set_int32(home[1], MAV_FRAME_GLOBAL_INT), - home[2].asFloat(), + set_int32(json_at(home, 0), MAV_FRAME_GLOBAL_INT), + set_int32(json_at(home, 1), MAV_FRAME_GLOBAL_INT), + set_float(json_at(home, 2)), MAV_MISSION_TYPE_MISSION}); #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic pop @@ -166,34 +214,35 @@ MissionImport::import_mission(const Json::Value& root, Autopilot autopilot) } std::optional> -MissionImport::import_geofence(const Json::Value& root) +MissionImport::import_geofence(const nlohmann::json& root) { std::vector geofence_items; // Return early if there are no geofence items. - const auto geofence = root["geoFence"]; + const auto& geofence = json_at(root, "geoFence"); if (geofence.empty()) { return std::nullopt; } // Check the mission version. const auto supported_geofence_version = 2; - const auto geofence_version = geofence["version"]; - if (geofence_version.empty() || geofence_version.asInt() != supported_geofence_version) { + const auto& geofence_version = json_at(geofence, "version"); + if (!geofence_version.is_number_integer() || + geofence_version.get() != supported_geofence_version) { LogErr( "geofence version for .plan not supported, found version: {}, supported: {}", - geofence_version.asInt(), + json_get_int(geofence_version), supported_geofence_version); return std::nullopt; } // Import polygon geofences std::vector polygon_geofences; - polygon_geofences = import_polygon_geofences(geofence["polygons"]); + polygon_geofences = import_polygon_geofences(json_at(geofence, "polygons")); // Import circular geofences std::vector circular_geofences; - circular_geofences = import_circular_geofences(geofence["circles"]); + circular_geofences = import_circular_geofences(json_at(geofence, "circles")); geofence_items.insert(geofence_items.end(), polygon_geofences.begin(), polygon_geofences.end()); geofence_items.insert( @@ -214,38 +263,38 @@ MissionImport::import_geofence(const Json::Value& root) } std::optional> -MissionImport::import_rally_points(const Json::Value& root) +MissionImport::import_rally_points(const nlohmann::json& root) { std::vector rally_items; // Return early if there are no rally points. - const auto rally_points = root["rallyPoints"]; + const auto& rally_points = json_at(root, "rallyPoints"); if (rally_points.empty()) { return std::nullopt; } // Check the rally points version. const auto supported_rally_points_version = 2; - const auto rally_points_version = rally_points["version"]; - if (rally_points_version.empty() || - rally_points_version.asInt() != supported_rally_points_version) { + const auto& rally_points_version = json_at(rally_points, "version"); + if (!rally_points_version.is_number_integer() || + rally_points_version.get() != supported_rally_points_version) { LogErr( "rally points version for .plan not supported, found version: {}, supported: {}", - rally_points_version.asInt(), + json_get_int(rally_points_version), supported_rally_points_version); return std::nullopt; } // Go through items - for (const auto& point : rally_points["points"]) { + for (const auto& point : json_at(rally_points, "points")) { MissionRaw::MissionItem item{}; item.command = MAV_CMD_NAV_RALLY_POINT; item.frame = MAV_FRAME_GLOBAL_RELATIVE_ALT; item.mission_type = MAV_MISSION_TYPE_RALLY; - item.x = set_int32(point[0], item.frame); - item.y = set_int32(point[1], item.frame); - item.z = set_float(point[2]); + item.x = set_int32(json_at(point, 0), item.frame); + item.y = set_int32(json_at(point, 1), item.frame); + item.z = set_float(json_at(point, 2)); rally_items.push_back(item); } @@ -265,47 +314,48 @@ MissionImport::import_rally_points(const Json::Value& root) } std::optional -MissionImport::import_simple_mission_item(const Json::Value& json_item) +MissionImport::import_simple_mission_item(const nlohmann::json& json_item) { - if (json_item["command"].empty() || json_item["autoContinue"].empty() || - json_item["frame"].empty() || json_item["params"].empty()) { + if (json_at(json_item, "command").empty() || json_at(json_item, "autoContinue").empty() || + json_at(json_item, "frame").empty() || json_at(json_item, "params").empty()) { LogErr("Missing mission item field."); return std::nullopt; } - if (!json_item["params"].isArray()) { + const auto& params = json_at(json_item, "params"); + if (!params.is_array()) { LogErr("No param array found."); return std::nullopt; } MissionRaw::MissionItem item{}; - item.command = json_item["command"].asInt(); - item.autocontinue = json_item["autoContinue"].asBool() ? 1 : 0; - item.frame = json_item["frame"].asInt(); + item.command = json_get_int(json_at(json_item, "command")); + item.autocontinue = json_get_bool(json_at(json_item, "autoContinue")) ? 1 : 0; + item.frame = json_get_int(json_at(json_item, "frame")); item.mission_type = MAV_MISSION_TYPE_MISSION; for (unsigned i = 0; i < 7; ++i) { switch (i) { case 0: - item.param1 = set_float(json_item["params"][i]); + item.param1 = set_float(json_at(params, i)); break; case 1: - item.param2 = set_float(json_item["params"][i]); + item.param2 = set_float(json_at(params, i)); break; case 2: - item.param3 = set_float(json_item["params"][i]); + item.param3 = set_float(json_at(params, i)); break; case 3: - item.param4 = set_float(json_item["params"][i]); + item.param4 = set_float(json_at(params, i)); break; case 4: - item.x = set_int32(json_item["params"][i], item.frame); + item.x = set_int32(json_at(params, i), item.frame); break; case 5: - item.y = set_int32(json_item["params"][i], item.frame); + item.y = set_int32(json_at(params, i), item.frame); break; case 6: - item.z = set_float(json_item["params"][i]); + item.z = set_float(json_at(params, i)); break; default: // should never happen @@ -317,25 +367,27 @@ MissionImport::import_simple_mission_item(const Json::Value& json_item) } std::optional> -MissionImport::import_complex_mission_items(const Json::Value& json_item) +MissionImport::import_complex_mission_items(const nlohmann::json& json_item) { - if (json_item["complexItemType"].empty()) { + const auto& complex_item_type = json_at(json_item, "complexItemType"); + if (complex_item_type.empty()) { LogErr("Could not determine complexItemType"); return std::nullopt; } - if (json_item["complexItemType"] != "survey") { - LogErr("complexItemType: {} not supported", json_item["complexItemType"].asString()); + if (complex_item_type != "survey") { + LogErr("complexItemType: {} not supported", json_to_string(complex_item_type)); return std::nullopt; } - if (json_item["version"].empty()) { + const auto& version = json_at(json_item, "version"); + if (version.empty()) { LogErr("version of complexItem not found"); return std::nullopt; } const int supported_complex_item_version = 5; - const int found_version = json_item["version"].asInt(); + const int found_version = json_get_int(version); if (found_version != 5) { LogErr( "version of complexItem not supported, found version: {}, supported: {}", @@ -344,20 +396,21 @@ MissionImport::import_complex_mission_items(const Json::Value& json_item) return std::nullopt; } - if (json_item["TransectStyleComplexItem"].empty()) { + const auto& transect_style = json_at(json_item, "TransectStyleComplexItem"); + if (transect_style.empty()) { LogErr("TransectStyleComplexItem not found"); return std::nullopt; } // These are Items (capitalized!) inside the TransectStyleComplexItem. - if (json_item["TransectStyleComplexItem"]["Items"].empty() || - !json_item["TransectStyleComplexItem"]["Items"].isArray()) { + const auto& items = json_at(transect_style, "Items"); + if (items.empty() || !items.is_array()) { LogErr("No survey items found"); return std::nullopt; } std::vector mission_items; - for (const auto& json_item_subitem : json_item["TransectStyleComplexItem"]["Items"]) { + for (const auto& json_item_subitem : items) { const auto maybe_item = import_simple_mission_item(json_item_subitem); if (maybe_item.has_value()) { mission_items.push_back(maybe_item.value()); @@ -368,23 +421,24 @@ MissionImport::import_complex_mission_items(const Json::Value& json_item) } std::vector -MissionImport::import_polygon_geofences(const Json::Value& polygons) +MissionImport::import_polygon_geofences(const nlohmann::json& polygons) { std::vector polygon_geofences; for (const auto& polygon : polygons) { - bool inclusion_missing = polygon["inclusion"].isNull(); - bool inclusion = inclusion_missing ? true : polygon["inclusion"].asBool(); - const auto& points = polygon["polygon"]; + const auto& inclusion_value = json_at(polygon, "inclusion"); + bool inclusion_missing = inclusion_value.is_null(); + bool inclusion = inclusion_missing ? true : json_get_bool(inclusion_value); + const auto& points = json_at(polygon, "polygon"); for (const auto& point : points) { MissionRaw::MissionItem item{}; item.command = inclusion ? MAV_CMD_NAV_FENCE_POLYGON_VERTEX_INCLUSION : MAV_CMD_NAV_FENCE_POLYGON_VERTEX_EXCLUSION; item.frame = MAV_FRAME_GLOBAL; - item.param1 = set_float(points.size()); - item.x = set_int32(point[0], item.frame); - item.y = set_int32(point[1], item.frame); + item.param1 = static_cast(points.size()); + item.x = set_int32(json_at(point, 0), item.frame); + item.y = set_int32(json_at(point, 1), item.frame); item.mission_type = MAV_MISSION_TYPE_FENCE; polygon_geofences.push_back(item); @@ -395,14 +449,15 @@ MissionImport::import_polygon_geofences(const Json::Value& polygons) } std::vector -MissionImport::import_circular_geofences(const Json::Value& circles) +MissionImport::import_circular_geofences(const nlohmann::json& circles) { std::vector circular_geofences; for (const auto& circle : circles) { - bool inclusion = circle["inclusion"].asBool(); - const auto& center = circle["circle"]["center"]; - const auto& radius = circle["circle"]["radius"]; + bool inclusion = json_get_bool(json_at(circle, "inclusion")); + const auto& circle_obj = json_at(circle, "circle"); + const auto& center = json_at(circle_obj, "center"); + const auto& radius = json_at(circle_obj, "radius"); MissionRaw::MissionItem item{}; @@ -410,8 +465,8 @@ MissionImport::import_circular_geofences(const Json::Value& circles) inclusion ? MAV_CMD_NAV_FENCE_CIRCLE_INCLUSION : MAV_CMD_NAV_FENCE_CIRCLE_EXCLUSION; item.frame = MAV_FRAME_GLOBAL; item.param1 = set_float(radius); - item.x = set_int32(center[0], item.frame); - item.y = set_int32(center[1], item.frame); + item.x = set_int32(json_at(center, 0), item.frame); + item.y = set_int32(json_at(center, 1), item.frame); item.mission_type = MAV_MISSION_TYPE_FENCE; circular_geofences.push_back(item); @@ -420,18 +475,18 @@ MissionImport::import_circular_geofences(const Json::Value& circles) return circular_geofences; } -float MissionImport::set_float(const Json::Value& val) +float MissionImport::set_float(const nlohmann::json& val) { - return val.isNull() ? NAN : val.asFloat(); + return val.is_number() ? val.get() : NAN; } -int32_t MissionImport::set_int32(const Json::Value& val, uint32_t frame) +int32_t MissionImport::set_int32(const nlohmann::json& val, uint32_t frame) { // Don't apply 10^7 conversion for MAV_FRAME_MISSION if (frame == MAV_FRAME_MISSION) { - return static_cast(val.isNull() ? 0 : val.asFloat()); + return static_cast(val.is_number() ? val.get() : 0.0f); } else { - return static_cast(val.isNull() ? 0 : (std::round(val.asDouble() * 1e7))); + return static_cast(val.is_number() ? std::round(val.get() * 1e7) : 0.0); } } diff --git a/cpp/src/mavsdk/plugins/mission_raw/mission_import.hpp b/cpp/src/mavsdk/plugins/mission_raw/mission_import.hpp index 74a6cb2b61..3f5684f4f3 100644 --- a/cpp/src/mavsdk/plugins/mission_raw/mission_import.hpp +++ b/cpp/src/mavsdk/plugins/mission_raw/mission_import.hpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include namespace mavsdk { @@ -20,23 +20,23 @@ class MAVSDK_TEST_EXPORT MissionImport { parse_mission_planner(const std::string& raw_mission_planner, Autopilot autopilot); private: - static bool check_overall_version(const Json::Value& root); + static bool check_overall_version(const nlohmann::json& root); static std::optional> - import_geofence(const Json::Value& root); + import_geofence(const nlohmann::json& root); static std::optional> - import_rally_points(const Json::Value& root); + import_rally_points(const nlohmann::json& root); static std::optional> - import_mission(const Json::Value& root, Autopilot autopilot); + import_mission(const nlohmann::json& root, Autopilot autopilot); static std::optional - import_simple_mission_item(const Json::Value& json_item); + import_simple_mission_item(const nlohmann::json& json_item); static std::optional> - import_complex_mission_items(const Json::Value& json_item); + import_complex_mission_items(const nlohmann::json& json_item); static std::vector - import_polygon_geofences(const Json::Value& json_item); + import_polygon_geofences(const nlohmann::json& json_item); static std::vector - import_circular_geofences(const Json::Value& json_item); - static float set_float(const Json::Value& val); - static int32_t set_int32(const Json::Value& val, uint32_t frame); + import_circular_geofences(const nlohmann::json& json_item); + static float set_float(const nlohmann::json& val); + static int32_t set_int32(const nlohmann::json& val, uint32_t frame); // Mission Planner format parsing methods static std::optional> diff --git a/cpp/src/system_tests/CMakeLists.txt b/cpp/src/system_tests/CMakeLists.txt index cb1f03e02e..16f508f21b 100644 --- a/cpp/src/system_tests/CMakeLists.txt +++ b/cpp/src/system_tests/CMakeLists.txt @@ -53,7 +53,7 @@ target_include_directories(system_tests_runner find_package(GTest REQUIRED) -find_package(jsoncpp REQUIRED) +find_package(nlohmann_json REQUIRED) find_package(fmt REQUIRED) @@ -63,7 +63,7 @@ target_link_libraries(system_tests_runner GTest::gtest GTest::gtest_main GTest::gmock - JsonCpp::JsonCpp + nlohmann_json::nlohmann_json fmt::fmt ) diff --git a/cpp/src/system_tests/intercept.cpp b/cpp/src/system_tests/intercept.cpp index 744d802954..bfc7e61875 100644 --- a/cpp/src/system_tests/intercept.cpp +++ b/cpp/src/system_tests/intercept.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include using namespace mavsdk; @@ -269,23 +269,23 @@ TEST(Intercept, JsonIncoming) EXPECT_GT(intercepted_message.fields_json.length(), 0u); // Parse JSON to verify field values - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(intercepted_message.fields_json, json)); + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(intercepted_message.fields_json, nullptr, false)) + .is_discarded())); // Verify position fields from the intercepted message - EXPECT_NEAR(json["lat"].asInt() / 1e7, position.latitude_deg, 1e-6); - EXPECT_NEAR(json["lon"].asInt() / 1e7, position.longitude_deg, 1e-6); - EXPECT_NEAR(json["alt"].asInt() / 1e3, position.absolute_altitude_m, 1.0); - EXPECT_NEAR(json["relative_alt"].asInt() / 1e3, position.relative_altitude_m, 1.0); + EXPECT_NEAR(json["lat"].get() / 1e7, position.latitude_deg, 1e-6); + EXPECT_NEAR(json["lon"].get() / 1e7, position.longitude_deg, 1e-6); + EXPECT_NEAR(json["alt"].get() / 1e3, position.absolute_altitude_m, 1.0); + EXPECT_NEAR(json["relative_alt"].get() / 1e3, position.relative_altitude_m, 1.0); // Verify velocity fields - EXPECT_NEAR(json["vx"].asInt() / 1e2, velocity.north_m_s, 0.1); - EXPECT_NEAR(json["vy"].asInt() / 1e2, velocity.east_m_s, 0.1); - EXPECT_NEAR(json["vz"].asInt() / 1e2, velocity.down_m_s, 0.1); + EXPECT_NEAR(json["vx"].get() / 1e2, velocity.north_m_s, 0.1); + EXPECT_NEAR(json["vy"].get() / 1e2, velocity.east_m_s, 0.1); + EXPECT_NEAR(json["vz"].get() / 1e2, velocity.down_m_s, 0.1); // Verify heading field - EXPECT_NEAR(json["hdg"].asInt() / 1e2, heading.heading_deg, 1.0); + EXPECT_NEAR(json["hdg"].get() / 1e2, heading.heading_deg, 1.0); LogInfo("Successfully tested incoming JSON message interceptio"); LogInfo(" - Message name: {}", intercepted_message.message_name); @@ -382,21 +382,21 @@ TEST(Intercept, JsonOutgoing) EXPECT_GT(intercepted_message.fields_json.length(), 0u); // Parse JSON to verify field values - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(intercepted_message.fields_json, json)); + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(intercepted_message.fields_json, nullptr, false)) + .is_discarded())); // Verify GPS fields from the intercepted message - EXPECT_EQ(json["time_usec"].asUInt64(), raw_gps.timestamp_us); - EXPECT_EQ(json["fix_type"].asUInt(), static_cast(gps_info.fix_type)); - EXPECT_NEAR(json["lat"].asInt() / 1e7, raw_gps.latitude_deg, 1e-6); - EXPECT_NEAR(json["lon"].asInt() / 1e7, raw_gps.longitude_deg, 1e-6); - EXPECT_NEAR(json["alt"].asInt() / 1e3, raw_gps.absolute_altitude_m, 1.0); - EXPECT_NEAR(json["eph"].asInt() / 1e2, raw_gps.hdop, 0.1); - EXPECT_NEAR(json["epv"].asInt() / 1e2, raw_gps.vdop, 0.1); - EXPECT_NEAR(json["vel"].asInt() / 1e2, raw_gps.velocity_m_s, 0.1); - EXPECT_NEAR(json["cog"].asInt() / 1e2, raw_gps.cog_deg, 1.0); - EXPECT_EQ(json["satellites_visible"].asUInt(), gps_info.num_satellites); + EXPECT_EQ(json["time_usec"].get(), raw_gps.timestamp_us); + EXPECT_EQ(json["fix_type"].get(), static_cast(gps_info.fix_type)); + EXPECT_NEAR(json["lat"].get() / 1e7, raw_gps.latitude_deg, 1e-6); + EXPECT_NEAR(json["lon"].get() / 1e7, raw_gps.longitude_deg, 1e-6); + EXPECT_NEAR(json["alt"].get() / 1e3, raw_gps.absolute_altitude_m, 1.0); + EXPECT_NEAR(json["eph"].get() / 1e2, raw_gps.hdop, 0.1); + EXPECT_NEAR(json["epv"].get() / 1e2, raw_gps.vdop, 0.1); + EXPECT_NEAR(json["vel"].get() / 1e2, raw_gps.velocity_m_s, 0.1); + EXPECT_NEAR(json["cog"].get() / 1e2, raw_gps.cog_deg, 1.0); + EXPECT_EQ(json["satellites_visible"].get(), gps_info.num_satellites); LogInfo("Successfully tested outgoing JSON message interceptio"); LogInfo(" - Message name: {}", intercepted_message.message_name); diff --git a/cpp/src/system_tests/mavlink_direct.cpp b/cpp/src/system_tests/mavlink_direct.cpp index d1d5937e54..d79f541ec8 100644 --- a/cpp/src/system_tests/mavlink_direct.cpp +++ b/cpp/src/system_tests/mavlink_direct.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include using namespace mavsdk; @@ -158,41 +158,43 @@ TEST(MavlinkDirect, ExtendedFields) EXPECT_EQ(received_full.message_name, "SYS_STATUS"); // Parse JSON to verify field values - Json::Value compact_json, full_json; - Json::Reader reader; + nlohmann::json compact_json, full_json; - ASSERT_TRUE(reader.parse(received_compact.fields_json, compact_json)); - ASSERT_TRUE(reader.parse(received_full.fields_json, full_json)); + ASSERT_TRUE( + !((compact_json = nlohmann::json::parse(received_compact.fields_json, nullptr, false)) + .is_discarded())); + ASSERT_TRUE(!((full_json = nlohmann::json::parse(received_full.fields_json, nullptr, false)) + .is_discarded())); // Verify basic fields are present and correct in both messages - EXPECT_EQ(compact_json["onboard_control_sensors_present"].asUInt(), 1u); - EXPECT_EQ(compact_json["load"].asUInt(), 500u); - EXPECT_EQ(compact_json["voltage_battery"].asUInt(), 12000u); - EXPECT_EQ(compact_json["current_battery"].asInt(), 1000); - EXPECT_EQ(compact_json["battery_remaining"].asInt(), 75); - - EXPECT_EQ(full_json["onboard_control_sensors_present"].asUInt(), 1u); - EXPECT_EQ(full_json["load"].asUInt(), 500u); - EXPECT_EQ(full_json["voltage_battery"].asUInt(), 12000u); - EXPECT_EQ(full_json["current_battery"].asInt(), 1000); - EXPECT_EQ(full_json["battery_remaining"].asInt(), 75); + EXPECT_EQ(compact_json["onboard_control_sensors_present"].get(), 1u); + EXPECT_EQ(compact_json["load"].get(), 500u); + EXPECT_EQ(compact_json["voltage_battery"].get(), 12000u); + EXPECT_EQ(compact_json["current_battery"].get(), 1000); + EXPECT_EQ(compact_json["battery_remaining"].get(), 75); + + EXPECT_EQ(full_json["onboard_control_sensors_present"].get(), 1u); + EXPECT_EQ(full_json["load"].get(), 500u); + EXPECT_EQ(full_json["voltage_battery"].get(), 12000u); + EXPECT_EQ(full_json["current_battery"].get(), 1000); + EXPECT_EQ(full_json["battery_remaining"].get(), 75); // Verify compact message HAS extended fields with zero values (MAVLink v2 zero-truncation) - EXPECT_TRUE(compact_json.isMember("onboard_control_sensors_present_extended")); - EXPECT_TRUE(compact_json.isMember("onboard_control_sensors_enabled_extended")); - EXPECT_TRUE(compact_json.isMember("onboard_control_sensors_health_extended")); - EXPECT_EQ(compact_json["onboard_control_sensors_present_extended"].asUInt(), 0u); - EXPECT_EQ(compact_json["onboard_control_sensors_enabled_extended"].asUInt(), 0u); - EXPECT_EQ(compact_json["onboard_control_sensors_health_extended"].asUInt(), 0u); + EXPECT_TRUE(compact_json.contains("onboard_control_sensors_present_extended")); + EXPECT_TRUE(compact_json.contains("onboard_control_sensors_enabled_extended")); + EXPECT_TRUE(compact_json.contains("onboard_control_sensors_health_extended")); + EXPECT_EQ(compact_json["onboard_control_sensors_present_extended"].get(), 0u); + EXPECT_EQ(compact_json["onboard_control_sensors_enabled_extended"].get(), 0u); + EXPECT_EQ(compact_json["onboard_control_sensors_health_extended"].get(), 0u); // Verify full message HAS extended fields with correct values - EXPECT_TRUE(full_json.isMember("onboard_control_sensors_present_extended")); - EXPECT_TRUE(full_json.isMember("onboard_control_sensors_enabled_extended")); - EXPECT_TRUE(full_json.isMember("onboard_control_sensors_health_extended")); + EXPECT_TRUE(full_json.contains("onboard_control_sensors_present_extended")); + EXPECT_TRUE(full_json.contains("onboard_control_sensors_enabled_extended")); + EXPECT_TRUE(full_json.contains("onboard_control_sensors_health_extended")); - EXPECT_EQ(full_json["onboard_control_sensors_present_extended"].asUInt(), 123u); - EXPECT_EQ(full_json["onboard_control_sensors_enabled_extended"].asUInt(), 456u); - EXPECT_EQ(full_json["onboard_control_sensors_health_extended"].asUInt(), 789u); + EXPECT_EQ(full_json["onboard_control_sensors_present_extended"].get(), 123u); + EXPECT_EQ(full_json["onboard_control_sensors_enabled_extended"].get(), 456u); + EXPECT_EQ(full_json["onboard_control_sensors_health_extended"].get(), 789u); receiver_mavlink_direct.unsubscribe_message(handle); std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -408,28 +410,30 @@ TEST(MavlinkDirect, ArrayFields) EXPECT_EQ(received_full.message_name, "GPS_STATUS"); // Parse JSON to verify field values - Json::Value partial_json, full_json; - Json::Reader reader; + nlohmann::json partial_json, full_json; - ASSERT_TRUE(reader.parse(received_partial.fields_json, partial_json)); - ASSERT_TRUE(reader.parse(received_full.fields_json, full_json)); + ASSERT_TRUE( + !((partial_json = nlohmann::json::parse(received_partial.fields_json, nullptr, false)) + .is_discarded())); + ASSERT_TRUE(!((full_json = nlohmann::json::parse(received_full.fields_json, nullptr, false)) + .is_discarded())); // Verify scalar field - EXPECT_EQ(partial_json["satellites_visible"].asUInt(), 3u); - EXPECT_EQ(full_json["satellites_visible"].asUInt(), 20u); + EXPECT_EQ(partial_json["satellites_visible"].get(), 3u); + EXPECT_EQ(full_json["satellites_visible"].get(), 20u); // Verify array fields are present and have correct type (arrays) - EXPECT_TRUE(partial_json["satellite_prn"].isArray()); - EXPECT_TRUE(partial_json["satellite_used"].isArray()); - EXPECT_TRUE(partial_json["satellite_elevation"].isArray()); - EXPECT_TRUE(partial_json["satellite_azimuth"].isArray()); - EXPECT_TRUE(partial_json["satellite_snr"].isArray()); - - EXPECT_TRUE(full_json["satellite_prn"].isArray()); - EXPECT_TRUE(full_json["satellite_used"].isArray()); - EXPECT_TRUE(full_json["satellite_elevation"].isArray()); - EXPECT_TRUE(full_json["satellite_azimuth"].isArray()); - EXPECT_TRUE(full_json["satellite_snr"].isArray()); + EXPECT_TRUE(partial_json["satellite_prn"].is_array()); + EXPECT_TRUE(partial_json["satellite_used"].is_array()); + EXPECT_TRUE(partial_json["satellite_elevation"].is_array()); + EXPECT_TRUE(partial_json["satellite_azimuth"].is_array()); + EXPECT_TRUE(partial_json["satellite_snr"].is_array()); + + EXPECT_TRUE(full_json["satellite_prn"].is_array()); + EXPECT_TRUE(full_json["satellite_used"].is_array()); + EXPECT_TRUE(full_json["satellite_elevation"].is_array()); + EXPECT_TRUE(full_json["satellite_azimuth"].is_array()); + EXPECT_TRUE(full_json["satellite_snr"].is_array()); // Verify array lengths (should be 20 elements each) EXPECT_EQ(partial_json["satellite_prn"].size(), 20u); @@ -445,33 +449,33 @@ TEST(MavlinkDirect, ArrayFields) EXPECT_EQ(full_json["satellite_snr"].size(), 20u); // Verify specific array element values for partial message - EXPECT_EQ(partial_json["satellite_prn"][0].asUInt(), 1u); - EXPECT_EQ(partial_json["satellite_prn"][1].asUInt(), 2u); - EXPECT_EQ(partial_json["satellite_prn"][2].asUInt(), 3u); - EXPECT_EQ(partial_json["satellite_prn"][3].asUInt(), 0u); // Should be zero - EXPECT_EQ(partial_json["satellite_prn"][19].asUInt(), 0u); // Last element should be zero - - EXPECT_EQ(partial_json["satellite_used"][0].asUInt(), 1u); - EXPECT_EQ(partial_json["satellite_used"][1].asUInt(), 1u); - EXPECT_EQ(partial_json["satellite_used"][2].asUInt(), 0u); - EXPECT_EQ(partial_json["satellite_used"][3].asUInt(), 0u); - - EXPECT_EQ(partial_json["satellite_elevation"][0].asUInt(), 45u); - EXPECT_EQ(partial_json["satellite_elevation"][1].asUInt(), 60u); - EXPECT_EQ(partial_json["satellite_elevation"][2].asUInt(), 30u); - EXPECT_EQ(partial_json["satellite_elevation"][3].asUInt(), 0u); + EXPECT_EQ(partial_json["satellite_prn"][0].get(), 1u); + EXPECT_EQ(partial_json["satellite_prn"][1].get(), 2u); + EXPECT_EQ(partial_json["satellite_prn"][2].get(), 3u); + EXPECT_EQ(partial_json["satellite_prn"][3].get(), 0u); // Should be zero + EXPECT_EQ(partial_json["satellite_prn"][19].get(), 0u); // Last element should be zero + + EXPECT_EQ(partial_json["satellite_used"][0].get(), 1u); + EXPECT_EQ(partial_json["satellite_used"][1].get(), 1u); + EXPECT_EQ(partial_json["satellite_used"][2].get(), 0u); + EXPECT_EQ(partial_json["satellite_used"][3].get(), 0u); + + EXPECT_EQ(partial_json["satellite_elevation"][0].get(), 45u); + EXPECT_EQ(partial_json["satellite_elevation"][1].get(), 60u); + EXPECT_EQ(partial_json["satellite_elevation"][2].get(), 30u); + EXPECT_EQ(partial_json["satellite_elevation"][3].get(), 0u); // Verify specific array element values for full message - EXPECT_EQ(full_json["satellite_prn"][0].asUInt(), 1u); - EXPECT_EQ(full_json["satellite_prn"][9].asUInt(), 10u); - EXPECT_EQ(full_json["satellite_prn"][19].asUInt(), 20u); // Last element + EXPECT_EQ(full_json["satellite_prn"][0].get(), 1u); + EXPECT_EQ(full_json["satellite_prn"][9].get(), 10u); + EXPECT_EQ(full_json["satellite_prn"][19].get(), 20u); // Last element - EXPECT_EQ(full_json["satellite_used"][0].asUInt(), 1u); - EXPECT_EQ(full_json["satellite_used"][5].asUInt(), 0u); // Some unused satellites - EXPECT_EQ(full_json["satellite_used"][10].asUInt(), 1u); + EXPECT_EQ(full_json["satellite_used"][0].get(), 1u); + EXPECT_EQ(full_json["satellite_used"][5].get(), 0u); // Some unused satellites + EXPECT_EQ(full_json["satellite_used"][10].get(), 1u); - EXPECT_EQ(full_json["satellite_snr"][0].asUInt(), 25u); - EXPECT_EQ(full_json["satellite_snr"][19].asUInt(), 27u); // Last element + EXPECT_EQ(full_json["satellite_snr"][0].get(), 25u); + EXPECT_EQ(full_json["satellite_snr"][19].get(), 27u); // Last element receiver_mavlink_direct.unsubscribe_message(handle); std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -551,14 +555,14 @@ TEST(MavlinkDirect, LoadCustomXml) EXPECT_EQ(received_message.message_name, "CUSTOM_TEST_MESSAGE"); // Parse JSON to verify field values - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(received_message.fields_json, json)); + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(received_message.fields_json, nullptr, false)) + .is_discarded())); // Verify custom message fields - EXPECT_EQ(json["test_value"].asUInt(), 42u); - EXPECT_EQ(json["counter"].asUInt(), 1337u); - EXPECT_EQ(json["status"].asUInt(), 5u); + EXPECT_EQ(json["test_value"].get(), 42u); + EXPECT_EQ(json["counter"].get(), 1337u); + EXPECT_EQ(json["status"].get(), 5u); receiver_mavlink_direct.unsubscribe_message(handle); std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -612,13 +616,13 @@ TEST(MavlinkDirect, ArdupilotmegaMessage) EXPECT_EQ(received_message.message_name, "MEMINFO"); // Parse JSON to verify field values - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(received_message.fields_json, json)); + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(received_message.fields_json, nullptr, false)) + .is_discarded())); // Verify MEMINFO message fields - EXPECT_EQ(json["brkval"].asUInt(), 32768u); // Heap top - EXPECT_EQ(json["freemem"].asUInt(), 8192u); // Free memory + EXPECT_EQ(json["brkval"].get(), 32768u); // Heap top + EXPECT_EQ(json["freemem"].get(), 8192u); // Free memory LogInfo("Successfully tested ArduPilot-specific MEMINFO message from ardupilotmega.xml"); receiver_mavlink_direct.unsubscribe_message(handle); @@ -711,49 +715,49 @@ TEST(MavlinkDirect, NanInfinityJsonHandling) EXPECT_EQ(received_message.message_name, "FLOAT_TEST_MESSAGE"); // Parse JSON to verify it's valid JSON (most important test) - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(received_message.fields_json, json)) + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(received_message.fields_json, nullptr, false)) + .is_discarded())) << "JSON parsing failed, indicating invalid JSON was generated. " << "JSON content: " << received_message.fields_json; // Verify all expected fields are present - EXPECT_TRUE(json.isMember("normal_float")); - EXPECT_TRUE(json.isMember("nan_float")); - EXPECT_TRUE(json.isMember("pos_inf_float")); - EXPECT_TRUE(json.isMember("neg_inf_float")); - EXPECT_TRUE(json.isMember("normal_double")); - EXPECT_TRUE(json.isMember("nan_double")); - EXPECT_TRUE(json.isMember("float_array")); + EXPECT_TRUE(json.contains("normal_float")); + EXPECT_TRUE(json.contains("nan_float")); + EXPECT_TRUE(json.contains("pos_inf_float")); + EXPECT_TRUE(json.contains("neg_inf_float")); + EXPECT_TRUE(json.contains("normal_double")); + EXPECT_TRUE(json.contains("nan_double")); + EXPECT_TRUE(json.contains("float_array")); // Verify that normal values are preserved correctly - EXPECT_TRUE(json["normal_float"].isNumeric()); - EXPECT_NEAR(json["normal_float"].asFloat(), 3.14f, 0.001f); - EXPECT_TRUE(json["normal_double"].isNumeric()); - EXPECT_NEAR(json["normal_double"].asDouble(), 2.718, 0.001); + EXPECT_TRUE(json["normal_float"].is_number()); + EXPECT_NEAR(json["normal_float"].get(), 3.14f, 0.001f); + EXPECT_TRUE(json["normal_double"].is_number()); + EXPECT_NEAR(json["normal_double"].get(), 2.718, 0.001); // The key test: verify that null values in input JSON were converted to NaN // in the MAVLink message, then back to null in the output JSON - EXPECT_TRUE(json["nan_float"].isNull()) + EXPECT_TRUE(json["nan_float"].is_null()) << "nan_float should be null, got: " << json["nan_float"]; - EXPECT_TRUE(json["pos_inf_float"].isNull()) + EXPECT_TRUE(json["pos_inf_float"].is_null()) << "pos_inf_float should be null, got: " << json["pos_inf_float"]; - EXPECT_TRUE(json["neg_inf_float"].isNull()) + EXPECT_TRUE(json["neg_inf_float"].is_null()) << "neg_inf_float should be null, got: " << json["neg_inf_float"]; - EXPECT_TRUE(json["nan_double"].isNull()) + EXPECT_TRUE(json["nan_double"].is_null()) << "nan_double should be null, got: " << json["nan_double"]; // Verify array handling: normal values preserved, null values round-trip as null (via NaN) - EXPECT_TRUE(json["float_array"].isArray()); + EXPECT_TRUE(json["float_array"].is_array()); EXPECT_EQ(json["float_array"].size(), 4u); - EXPECT_TRUE(json["float_array"][0].isNumeric()); - EXPECT_NEAR(json["float_array"][0].asFloat(), 1.0f, 0.001f); - EXPECT_TRUE(json["float_array"][1].isNull()) + EXPECT_TRUE(json["float_array"][0].is_number()); + EXPECT_NEAR(json["float_array"][0].get(), 1.0f, 0.001f); + EXPECT_TRUE(json["float_array"][1].is_null()) << "float_array[1] should be null (converted from NaN), got: " << json["float_array"][1]; - EXPECT_TRUE(json["float_array"][2].isNull()) + EXPECT_TRUE(json["float_array"][2].is_null()) << "float_array[2] should be null (converted from NaN), got: " << json["float_array"][2]; - EXPECT_TRUE(json["float_array"][3].isNumeric()); - EXPECT_NEAR(json["float_array"][3].asFloat(), 4.0f, 0.001f); + EXPECT_TRUE(json["float_array"][3].is_number()); + EXPECT_NEAR(json["float_array"][3].get(), 4.0f, 0.001f); LogInfo("Successfully verified that float/double JSON handling produces valid JSON"); receiver_mavlink_direct.unsubscribe_message(handle); @@ -1192,22 +1196,23 @@ TEST(MavlinkDirect, LargeUint64) EXPECT_EQ(received_message.component_id, 1); // Parse JSON to verify uint64 field value is preserved - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(received_message.fields_json, json)) + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(received_message.fields_json, nullptr, false)) + .is_discarded())) << "Failed to parse received JSON: " << received_message.fields_json; // Verify time_usec field is present and has the correct large value - ASSERT_TRUE(json.isMember("time_usec")) << "time_usec field missing from JSON"; - EXPECT_EQ(json["time_usec"].asUInt64(), 5000000000ULL) - << "time_usec value incorrect: expected 5000000000, got " << json["time_usec"].asUInt64(); + ASSERT_TRUE(json.contains("time_usec")) << "time_usec field missing from JSON"; + EXPECT_EQ(json["time_usec"].get(), 5000000000ULL) + << "time_usec value incorrect: expected 5000000000, got " + << json["time_usec"].get(); // Verify other fields for completeness - EXPECT_EQ(json["fix_type"].asUInt(), 3u); - EXPECT_EQ(json["lat"].asInt(), 473977418); - EXPECT_EQ(json["lon"].asInt(), -1223974560); - EXPECT_EQ(json["alt"].asInt(), 100500); - EXPECT_EQ(json["satellites_visible"].asUInt(), 12u); + EXPECT_EQ(json["fix_type"].get(), 3u); + EXPECT_EQ(json["lat"].get(), 473977418); + EXPECT_EQ(json["lon"].get(), -1223974560); + EXPECT_EQ(json["alt"].get(), 100500); + EXPECT_EQ(json["satellites_visible"].get(), 12u); LogInfo("Successfully verified uint64 handling for time_usec > 2^32"); receiver_mavlink_direct.unsubscribe_message(handle); diff --git a/cpp/src/system_tests/mavlink_direct_forwarding.cpp b/cpp/src/system_tests/mavlink_direct_forwarding.cpp index 75d17b0e05..255c837b29 100644 --- a/cpp/src/system_tests/mavlink_direct_forwarding.cpp +++ b/cpp/src/system_tests/mavlink_direct_forwarding.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include using namespace mavsdk; @@ -100,9 +100,9 @@ TEST(MavlinkDirect, ForwardingKnownMessage) EXPECT_EQ(received_message.component_id, 1); // Parse and verify JSON content - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(received_message.fields_json, json)); + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(received_message.fields_json, nullptr, false)) + .is_discarded())); // The JSON format may vary but should contain the message information // For now, just verify it's not empty and contains the message name @@ -230,14 +230,14 @@ TEST(MavlinkDirect, ForwardingUnknownMessage) EXPECT_EQ(received_message.component_id, 1); // Parse and verify JSON content - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(received_message.fields_json, json)); - - EXPECT_EQ(json["test_id"].asUInt(), 12345u); - EXPECT_EQ(json["sequence"].asUInt(), 1u); - EXPECT_EQ(json["status"].asUInt(), 42u); - EXPECT_EQ(json["message"].asString(), "Hello through forwarder!"); + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(received_message.fields_json, nullptr, false)) + .is_discarded())); + + EXPECT_EQ(json["test_id"].get(), 12345u); + EXPECT_EQ(json["sequence"].get(), 1u); + EXPECT_EQ(json["status"].get(), 42u); + EXPECT_EQ(json["message"].get(), "Hello through forwarder!"); receiver_mavlink_direct.unsubscribe_message(handle); } diff --git a/cpp/src/system_tests/mavlink_direct_server.cpp b/cpp/src/system_tests/mavlink_direct_server.cpp index 7302102308..7fdaeef14f 100644 --- a/cpp/src/system_tests/mavlink_direct_server.cpp +++ b/cpp/src/system_tests/mavlink_direct_server.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include using namespace mavsdk; @@ -64,11 +64,11 @@ TEST(MavlinkDirectServer, BroadcastToClient) EXPECT_EQ(received_message.system_id, 1); EXPECT_EQ(received_message.component_id, 1); - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(received_message.fields_json, json)); - EXPECT_EQ(json["lat"].asInt(), 473977418); - EXPECT_EQ(json["lon"].asInt(), -1223974560); + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(received_message.fields_json, nullptr, false)) + .is_discarded())); + EXPECT_EQ(json["lat"].get(), 473977418); + EXPECT_EQ(json["lon"].get(), -1223974560); receiver.unsubscribe_message(handle); std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -122,10 +122,10 @@ TEST(MavlinkDirectServer, SubscribeFromClient) // The ground station sends as system 245, component 190 (MAV_COMP_ID_MISSIONPLANNER). EXPECT_EQ(received_message.component_id, 190); - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(received_message.fields_json, json)); - EXPECT_EQ(json["lat"].asInt(), 473977418); + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(received_message.fields_json, nullptr, false)) + .is_discarded())); + EXPECT_EQ(json["lat"].get(), 473977418); receiver.unsubscribe_message(handle); std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -191,12 +191,12 @@ TEST(MavlinkDirectServer, LoadCustomXml) auto received_message = fut.get(); EXPECT_EQ(received_message.message_name, "CUSTOM_TEST_MESSAGE"); - Json::Value json; - Json::Reader reader; - ASSERT_TRUE(reader.parse(received_message.fields_json, json)); - EXPECT_EQ(json["test_value"].asUInt(), 42u); - EXPECT_EQ(json["counter"].asUInt(), 1337u); - EXPECT_EQ(json["status"].asUInt(), 5u); + nlohmann::json json; + ASSERT_TRUE(!((json = nlohmann::json::parse(received_message.fields_json, nullptr, false)) + .is_discarded())); + EXPECT_EQ(json["test_value"].get(), 42u); + EXPECT_EQ(json["counter"].get(), 1337u); + EXPECT_EQ(json["status"].get(), 5u); receiver.unsubscribe_message(handle); std::this_thread::sleep_for(std::chrono::milliseconds(100)); diff --git a/cpp/src/system_tests/param_change_notification.cpp b/cpp/src/system_tests/param_change_notification.cpp index c690cc1050..ffa4576ed2 100644 --- a/cpp/src/system_tests/param_change_notification.cpp +++ b/cpp/src/system_tests/param_change_notification.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include using namespace mavsdk; @@ -25,30 +25,28 @@ struct ValueCollector { void add_param_value(const std::string& json) { - Json::Value root; - Json::Reader reader; - if (!reader.parse(json, root)) { + nlohmann::json root; + if (((root = nlohmann::json::parse(json, nullptr, false)).is_discarded())) { return; } - if (root["param_id"].asString().rfind(param_name, 0) != 0) { + if (root["param_id"].get().rfind(param_name, 0) != 0) { return; } std::lock_guard lock(mutex); - param_values.push_back(root["param_value"].asFloat()); + param_values.push_back(root["param_value"].get()); } void add_param_ext_value(const std::string& json) { - Json::Value root; - Json::Reader reader; - if (!reader.parse(json, root)) { + nlohmann::json root; + if (((root = nlohmann::json::parse(json, nullptr, false)).is_discarded())) { return; } - if (root["param_id"].asString().rfind(param_name, 0) != 0) { + if (root["param_id"].get().rfind(param_name, 0) != 0) { return; } std::lock_guard lock(mutex); - param_ext_values.push_back(root["param_value"].asString()); + param_ext_values.push_back(root["param_value"].get()); } }; diff --git a/cpp/src/system_tests/request_message_rapid.cpp b/cpp/src/system_tests/request_message_rapid.cpp index 295eb7eaae..e4067f0ddf 100644 --- a/cpp/src/system_tests/request_message_rapid.cpp +++ b/cpp/src/system_tests/request_message_rapid.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include using namespace mavsdk; @@ -72,12 +72,12 @@ TEST(SystemTest, RequestMessageRapid) return; } - Json::Value root; - Json::Reader reader; - if (!reader.parse(command.fields_json, root)) { + nlohmann::json root; + if (((root = nlohmann::json::parse(command.fields_json, nullptr, false)) + .is_discarded())) { return; } - const int command_id = root["command"].asInt(); + const int command_id = root["command"].get(); MavlinkDirect::MavlinkMessage ack; ack.message_name = "COMMAND_ACK"; diff --git a/cpp/src/unit_tests/CMakeLists.txt b/cpp/src/unit_tests/CMakeLists.txt index b4fdaec0ec..1059984fb5 100644 --- a/cpp/src/unit_tests/CMakeLists.txt +++ b/cpp/src/unit_tests/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(jsoncpp REQUIRED) +find_package(nlohmann_json REQUIRED) add_executable(unit_tests_runner ${UNIT_TEST_SOURCES} @@ -16,7 +16,7 @@ find_package(fmt REQUIRED) target_link_libraries(unit_tests_runner mavsdk - JsonCpp::JsonCpp + nlohmann_json::nlohmann_json tinyxml2::tinyxml2 GTest::gtest GTest::gtest_main diff --git a/cpp/third_party/CMakeLists.txt b/cpp/third_party/CMakeLists.txt index 3f2e1b7b17..391b2b0998 100644 --- a/cpp/third_party/CMakeLists.txt +++ b/cpp/third_party/CMakeLists.txt @@ -26,7 +26,7 @@ if(SUPERBUILD) build_target(picosha2) build_target(tinyxml2) build_target(libmavlike) - build_target(jsoncpp) + build_target(nlohmann_json) build_target(liblzma) if(NOT BUILD_WITHOUT_CURL) diff --git a/cpp/third_party/jsoncpp/CMakeLists.txt b/cpp/third_party/nlohmann_json/CMakeLists.txt similarity index 70% rename from cpp/third_party/jsoncpp/CMakeLists.txt rename to cpp/third_party/nlohmann_json/CMakeLists.txt index ba5f5b6513..e8580584d6 100644 --- a/cpp/third_party/jsoncpp/CMakeLists.txt +++ b/cpp/third_party/nlohmann_json/CMakeLists.txt @@ -1,17 +1,17 @@ cmake_minimum_required(VERSION 3.13) -project(external-jsoncpp) +project(external-nlohmann_json) include(ExternalProject) list(APPEND CMAKE_ARGS "-DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH}" "-DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_INSTALL_PREFIX}" "-DCMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE}" - "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}" + "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" "-DBUILD_SHARED_LIBS=OFF" - "-DJSONCPP_WITH_CMAKE_PACKAGE=ON" - "-DJSONCPP_WITH_TESTS=OFF" + "-DJSON_BuildTests=OFF" + "-DJSON_Install=ON" ) if(IOS) @@ -22,15 +22,15 @@ if(IOS) ) endif() -message(STATUS "Preparing external project \"jsoncpp\" with args:") +message(STATUS "Preparing external project \"nlohmann_json\" with args:") foreach(CMAKE_ARG ${CMAKE_ARGS}) message(STATUS "-- ${CMAKE_ARG}") endforeach() -ExternalProject_Add( - jsoncpp - GIT_REPOSITORY https://github.com/open-source-parsers/jsoncpp - GIT_TAG 1.9.6 - PREFIX jsoncpp +ExternalProject_add( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json + GIT_TAG v3.11.3 + PREFIX nlohmann_json CMAKE_ARGS "${CMAKE_ARGS}" ) diff --git a/cpp/tools/build-with-system-deps.sh b/cpp/tools/build-with-system-deps.sh index 20e246aeda..f601d02426 100755 --- a/cpp/tools/build-with-system-deps.sh +++ b/cpp/tools/build-with-system-deps.sh @@ -7,7 +7,7 @@ # # Prerequisites (install via apt on Ubuntu/Debian): # sudo apt install build-essential cmake git python3 python3-pip \ -# liblzma-dev libtinyxml2-dev libjsoncpp-dev \ +# liblzma-dev libtinyxml2-dev nlohmann-json3-dev \ # libcurl4-openssl-dev libssl-dev libasio-dev libfmt-dev # # Usage: @@ -58,7 +58,7 @@ cp "${DEPS_DIR}/mavlink/message_definitions/v1.0/minimal.xml" \ # Build libevents # Use the same version as MAVSDK's superbuild (see third_party/libevents/CMakeLists.txt) -LIBEVENTS_VERSION="7c1720749dfe555ec2e71d5f9f753e6ac1244e1c" +LIBEVENTS_VERSION="840a88ea226d4eb0fd4c391ce860317422756435" echo "=== Building libevents (${LIBEVENTS_VERSION}) ===" if [ ! -d "${DEPS_DIR}/libevents" ]; then git clone https://github.com/mavlink/libevents.git "${DEPS_DIR}/libevents" diff --git a/docs/en/cpp/guide/build_linux.md b/docs/en/cpp/guide/build_linux.md index adf6503693..d4067a0e22 100644 --- a/docs/en/cpp/guide/build_linux.md +++ b/docs/en/cpp/guide/build_linux.md @@ -142,7 +142,7 @@ This script: Prerequisites (install before running the script): ```bash sudo apt install build-essential cmake git python3 python3-pip \ - liblzma-dev libtinyxml2-dev libjsoncpp-dev \ + liblzma-dev libtinyxml2-dev nlohmann-json3-dev \ libcurl4-openssl-dev libssl-dev ``` diff --git a/docs/en/cpp/guide/mavlink_direct.md b/docs/en/cpp/guide/mavlink_direct.md index ac8cf3973d..2611f6e45a 100644 --- a/docs/en/cpp/guide/mavlink_direct.md +++ b/docs/en/cpp/guide/mavlink_direct.md @@ -100,7 +100,7 @@ obstacle_distance.target_component_id = 0; // Does not apply for this message // On 3 segments we see an obstacle, the closest is at 7m. // // In a real application this JSON message content would of course be -// assembled manually, or using a json library like jsoncpp or nlohmann/json. +// assembled manually, or using a json library like nlohmann/json. obstacle_distance.fields_json = R"({ "time_usec": 12345678, "sensor_type": 3, @@ -149,14 +149,11 @@ auto handle = mavlink_direct.subscribe_message( [](MavlinkDirect::MavlinkMessage message) { std::cout << "Received position: " << message.fields_json << std::endl; - // And this could now be parsed by jsoncpp or nlohmann/json: - // Json::Value json; - // Json::Reader reader; - // if (reader.parse(message.fields_json, json)) { - // auto lat = json["lat"].asInt() / 1e7; // Convert from degrees * 1e7 - // auto lon = json["lon"].asInt() / 1e7; - // std::cout << "Position: " << lat << ", " << lon << std::endl; - // } + // And this could now be parsed by nlohmann/json: + // auto json = nlohmann::json::parse(message.fields_json); + // auto lat = json["lat"].get() / 1e7; // Convert from degrees * 1e7 + // auto lon = json["lon"].get() / 1e7; + // std::cout << "Position: " << lat << ", " << lon << std::endl; } ); ``` From 6242c4742dfb0c855e4279fcc826ef954d23606e Mon Sep 17 00:00:00 2001 From: Julian Oes Date: Mon, 13 Jul 2026 13:38:59 +1200 Subject: [PATCH 2/5] mavlink_direct: represent binary char[] param fields as JSON byte arrays PARAM_EXT_{VALUE,SET,ACK}.param_value is declared as char[128] in the MAVLink definition but the extended-parameter protocol uses it to carry the raw bytes of a typed value. libmav treats char[] as a NUL-terminated string (getString/strnlen), so any value containing a zero byte is truncated on receive, and the bytes are usually not valid UTF-8. Represent these fields (via an explicit, documented allow-list) as JSON byte arrays, read as raw bytes so nothing is truncated. The field stays declared char, so the message CRC_EXTRA is unchanged and wire compatibility is preserved. The parse direction already accepts a number array for char[] fields, so this round-trips. Add a system test covering the round-trip (including an interior NUL) and adapt the param-change-notification collector, which previously read param_value as a string. --- cpp/src/mavsdk/core/libmav_receiver.cpp | 43 ++++++++++ cpp/src/system_tests/mavlink_direct.cpp | 84 +++++++++++++++++++ .../param_change_notification.cpp | 5 +- 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/cpp/src/mavsdk/core/libmav_receiver.cpp b/cpp/src/mavsdk/core/libmav_receiver.cpp index 1b53b28484..23e5efe3fa 100644 --- a/cpp/src/mavsdk/core/libmav_receiver.cpp +++ b/cpp/src/mavsdk/core/libmav_receiver.cpp @@ -5,10 +5,41 @@ #include #include #include +#include +#include +#include +#include #include "log.hpp" namespace mavsdk { +namespace { + +// A handful of MAVLink messages declare a field as char[] but actually use it +// to carry raw binary data. The most common case is the extended-parameter +// protocol, which packs a typed parameter value as little-endian bytes into +// PARAM_EXT_{VALUE,SET,ACK}.param_value (char[128]). +// +// Such fields must be represented in JSON as a byte array, not a string: +// - libmav's getString() truncates char[] at the first NUL (strnlen), which +// corrupts any value containing a zero byte, and +// - the bytes are usually not valid UTF-8. +// +// This is an explicit, deliberately small allow-list. It intentionally does NOT +// match PARAM_VALUE.param_value, which is a normal float. New entries should be +// added only for fields genuinely carrying binary in a char[]. +bool is_binary_char_field(const std::string& message_name, const std::string& field_name) +{ + static const std::set> binary_fields{ + {"PARAM_EXT_VALUE", "param_value"}, + {"PARAM_EXT_SET", "param_value"}, + {"PARAM_EXT_ACK", "param_value"}, + }; + return binary_fields.find({message_name, field_name}) != binary_fields.end(); +} + +} // namespace + LibmavReceiver::LibmavReceiver(MavsdkImpl& mavsdk_impl) : _mavsdk_impl(mavsdk_impl) { // No need for individual BufferParser - we'll use MavsdkImpl's thread-safe parsing @@ -141,6 +172,18 @@ std::string LibmavReceiver::libmav_message_to_json(const mav::Message& msg) cons // Get field names and iterate through them auto field_names = message_def.fieldNames(); for (const auto& field_name : field_names) { + // Binary char[] fields (see allow-list) are emitted as a byte array + // of the full field width, read directly as raw bytes so the value + // is not NUL-truncated. The field stays declared as char, so the + // message CRC_EXTRA is unaffected. + if (is_binary_char_field(msg.name(), field_name)) { + std::vector raw; + if (msg.get(field_name, raw) == ::mav::MessageResult::Success) { + json[field_name] = raw; + continue; + } + } + // Extract field value based on type and convert to JSON auto variant_opt = msg.getAsNativeTypeInVariant(field_name); if (variant_opt) { diff --git a/cpp/src/system_tests/mavlink_direct.cpp b/cpp/src/system_tests/mavlink_direct.cpp index d79f541ec8..9278bf3ab7 100644 --- a/cpp/src/system_tests/mavlink_direct.cpp +++ b/cpp/src/system_tests/mavlink_direct.cpp @@ -1218,3 +1218,87 @@ TEST(MavlinkDirect, LargeUint64) receiver_mavlink_direct.unsubscribe_message(handle); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + +// Regression test for binary data carried in a char[] field. +// +// PARAM_EXT_VALUE.param_value is declared as char[128] in the MAVLink XML, but +// the extended-parameter protocol uses it to carry the raw bytes of the typed +// value (little-endian). Treated as a NUL-terminated string it would be +// truncated at the first zero byte, so MavlinkDirect represents this field as a +// JSON byte array instead (see the allow-list in libmav_receiver.cpp). This +// test sends a value with an interior NUL and verifies every byte round-trips. +TEST(MavlinkDirect, ParamExtValueBinaryRoundtrip) +{ + Mavsdk mavsdk_groundstation{Mavsdk::Configuration{ComponentType::GroundStation}}; + Mavsdk mavsdk_autopilot{Mavsdk::Configuration{ComponentType::Autopilot}}; + + ASSERT_EQ( + mavsdk_groundstation.add_any_connection("udpin://0.0.0.0:18010"), + ConnectionResult::Success); + ASSERT_EQ( + mavsdk_autopilot.add_any_connection("udpout://127.0.0.1:18010"), ConnectionResult::Success); + + auto maybe_system = mavsdk_groundstation.first_autopilot(10.0); + ASSERT_TRUE(maybe_system); + auto system = maybe_system.value(); + ASSERT_TRUE(system->has_autopilot()); + + auto receiver_mavlink_direct = MavlinkDirect{system}; + auto sender_mavlink_direct = MavlinkDirectServer{mavsdk_autopilot.server_component()}; + + auto prom = std::promise(); + auto fut = prom.get_future(); + + auto handle = receiver_mavlink_direct.subscribe_message( + "PARAM_EXT_VALUE", [&prom](MavlinkDirect::MavlinkMessage message) { + LogInfo("Received PARAM_EXT_VALUE: {}", message.fields_json); + prom.set_value(message); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // A uint32 value of 0x00020001 = 131073, little-endian bytes: 01 00 02 00. + // The interior zero byte at index 1 is what used to truncate the value, and + // the 0x02 at index 2 is the byte that used to be lost. + const std::vector param_value_bytes{1, 0, 2, 0}; + + nlohmann::json send_fields; + send_fields["param_id"] = "TEST_BIN"; + send_fields["param_value"] = param_value_bytes; // sent as a JSON byte array + send_fields["param_type"] = 6; // MAV_PARAM_EXT_TYPE_INT32 + send_fields["param_count"] = 1; + send_fields["param_index"] = 0; + + MavlinkDirectServer::MavlinkMessage test_message; + test_message.message_name = "PARAM_EXT_VALUE"; + test_message.system_id = 1; + test_message.component_id = 1; + test_message.target_system_id = 0; + test_message.target_component_id = 0; + test_message.fields_json = send_fields.dump(); + + LogInfo("Sending PARAM_EXT_VALUE with binary param_value"); + ASSERT_EQ( + sender_mavlink_direct.send_message(test_message), MavlinkDirectServer::Result::Success); + + ASSERT_EQ(fut.wait_for(std::chrono::seconds(1)), std::future_status::ready); + auto received_message = fut.get(); + + auto received = nlohmann::json::parse(received_message.fields_json, nullptr, false); + ASSERT_FALSE(received.is_discarded()); + ASSERT_TRUE(received.contains("param_value")); + const auto& received_value = received["param_value"]; + + // param_value must come back as a byte array covering the full field width, + // with the meaningful bytes (including the interior NUL and the byte after + // it) preserved -- i.e. no NUL truncation. + ASSERT_TRUE(received_value.is_array()); + ASSERT_GE(received_value.size(), param_value_bytes.size()); + EXPECT_EQ(received_value[0].get(), 1); + EXPECT_EQ(received_value[1].get(), 0); // interior NUL preserved + EXPECT_EQ(received_value[2].get(), 2); // byte after the NUL preserved + EXPECT_EQ(received_value[3].get(), 0); + + receiver_mavlink_direct.unsubscribe_message(handle); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} diff --git a/cpp/src/system_tests/param_change_notification.cpp b/cpp/src/system_tests/param_change_notification.cpp index ffa4576ed2..d3e2922ea5 100644 --- a/cpp/src/system_tests/param_change_notification.cpp +++ b/cpp/src/system_tests/param_change_notification.cpp @@ -46,7 +46,10 @@ struct ValueCollector { return; } std::lock_guard lock(mutex); - param_ext_values.push_back(root["param_value"].get()); + // PARAM_EXT_VALUE.param_value is a binary field and is serialized as a + // JSON byte array; dump() gives a stable string form (this collector + // only counts notifications, it does not inspect the value). + param_ext_values.push_back(root["param_value"].dump()); } }; From 947eaec91f791e52a1ad1b70dd877aa5f75437a7 Mon Sep 17 00:00:00 2001 From: Julian Oes Date: Mon, 13 Jul 2026 13:53:11 +1200 Subject: [PATCH 3/5] third_party: bump nlohmann/json to v3.12.0 --- cpp/third_party/nlohmann_json/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/third_party/nlohmann_json/CMakeLists.txt b/cpp/third_party/nlohmann_json/CMakeLists.txt index e8580584d6..303899e382 100644 --- a/cpp/third_party/nlohmann_json/CMakeLists.txt +++ b/cpp/third_party/nlohmann_json/CMakeLists.txt @@ -30,7 +30,7 @@ endforeach() ExternalProject_add( nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json - GIT_TAG v3.11.3 + GIT_TAG v3.12.0 PREFIX nlohmann_json CMAKE_ARGS "${CMAKE_ARGS}" ) From d8459798b1c12159030efe55654338c6426e36d0 Mon Sep 17 00:00:00 2001 From: Julian Oes Date: Tue, 14 Jul 2026 10:13:26 +1200 Subject: [PATCH 4/5] mavlink_direct: avoid static container with destructor in binary-field lookup The binary char[] allow-list used a function-local static std::set>. Such a static has a runtime destructor that runs at process exit on the main thread, while a lingering io-context thread may still be serializing a message and calling find() on it -- the MAVLink fuzzer caught this as a heap-use-after-free. Replace the set with a constexpr std::array of std::string_view literals: no static object with a destructor, and no per-lookup heap allocation of a temporary pair. Behaviour is unchanged. --- cpp/src/mavsdk/core/libmav_receiver.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/cpp/src/mavsdk/core/libmav_receiver.cpp b/cpp/src/mavsdk/core/libmav_receiver.cpp index 23e5efe3fa..adcc54bb61 100644 --- a/cpp/src/mavsdk/core/libmav_receiver.cpp +++ b/cpp/src/mavsdk/core/libmav_receiver.cpp @@ -5,9 +5,9 @@ #include #include #include -#include +#include #include -#include +#include #include #include "log.hpp" @@ -30,12 +30,24 @@ namespace { // added only for fields genuinely carrying binary in a char[]. bool is_binary_char_field(const std::string& message_name, const std::string& field_name) { - static const std::set> binary_fields{ + struct BinaryField { + std::string_view message; + std::string_view field; + }; + // Kept as string-view literals in a constexpr array (no static object with a + // runtime destructor and no per-lookup allocation) so it is safe to read + // from the message-delivery thread, including during process teardown. + static constexpr std::array binary_fields{{ {"PARAM_EXT_VALUE", "param_value"}, {"PARAM_EXT_SET", "param_value"}, {"PARAM_EXT_ACK", "param_value"}, - }; - return binary_fields.find({message_name, field_name}) != binary_fields.end(); + }}; + for (const auto& binary_field : binary_fields) { + if (field_name == binary_field.field && message_name == binary_field.message) { + return true; + } + } + return false; } } // namespace From 9c61a461c6b76f872d2503f9893306abcb93d90d Mon Sep 17 00:00:00 2001 From: Julian Oes Date: Tue, 14 Jul 2026 10:24:36 +1200 Subject: [PATCH 5/5] third_party: use archive rather than git --- cpp/third_party/nlohmann_json/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/third_party/nlohmann_json/CMakeLists.txt b/cpp/third_party/nlohmann_json/CMakeLists.txt index 303899e382..a071db0457 100644 --- a/cpp/third_party/nlohmann_json/CMakeLists.txt +++ b/cpp/third_party/nlohmann_json/CMakeLists.txt @@ -29,8 +29,7 @@ endforeach() ExternalProject_add( nlohmann_json - GIT_REPOSITORY https://github.com/nlohmann/json - GIT_TAG v3.12.0 + URL https://github.com/nlohmann/json/archive/refs/tags/v3.12.0.tar.gz PREFIX nlohmann_json CMAKE_ARGS "${CMAKE_ARGS}" )