Skip to content

Commit

Permalink
Merge pull request #291 from eProsima/develop
Browse files Browse the repository at this point in the history
Release v2.1.0
  • Loading branch information
pablogs9 authored Dec 1, 2021
2 parents 0aea832 + 129580f commit 070c438
Show file tree
Hide file tree
Showing 73 changed files with 6,016 additions and 945 deletions.
41 changes: 21 additions & 20 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ option(UAGENT_USE_INTERNAL_GTEST "Enable internal GTest libraries." OFF)
option(BUILD_SHARED_LIBS "Control shared/static building." ON)

option(UAGENT_USE_SYSTEM_FASTDDS "Force find and use system installed Fast-DDS." OFF)
option(UAGENT_USE_SYSTEM_FASTCDR "Force find and use system installed Fast-CDR." OFF)
option(UAGENT_FAST_PROFILE "Build FastMiddleware profile." ON)
option(UAGENT_CED_PROFILE "Build CedMiddleware profile." ON)
option(UAGENT_DISCOVERY_PROFILE "Build Discovery profile." ON)
Expand All @@ -37,8 +38,8 @@ option(UAGENT_SECURITY_PROFILE "Build security profile." OFF)
option(UAGENT_BUILD_EXECUTABLE "Build Micro XRCE-DDS Agent provided executable." ON)
option(UAGENT_BUILD_USAGE_EXAMPLES "Build Micro XRCE-DDS Agent built-in usage examples" OFF)

set(UAGENT_P2P_CLIENT_VERSION 2.0.0 CACHE STRING "Sets Micro XRCE-DDS client version for P2P")
set(UAGENT_P2P_CLIENT_TAG develop CACHE STRING "Sets Micro XRCE-DDS client tag for P2P")
set(UAGENT_P2P_CLIENT_VERSION 2.1.0 CACHE STRING "Sets Micro XRCE-DDS client version for P2P")
set(UAGENT_P2P_CLIENT_TAG v2.1.0 CACHE STRING "Sets Micro XRCE-DDS client tag for P2P")

option(UAGENT_BUILD_CI_TESTS "Build CI test cases.")
if(UAGENT_BUILD_CI_TESTS)
Expand All @@ -62,13 +63,20 @@ set(UAGENT_CONFIG_SERVER_QUEUE_MAX_SIZE 32000 CACHE STRING "Maximum se
set(UAGENT_CONFIG_CLIENT_DEAD_TIME 30000 CACHE STRING "Client dead time in milliseconds.")
set(UAGENT_SERVER_BUFFER_SIZE 65535 CACHE STRING "Server buffer size.")

# Off-standard features and tweaks
option(UAGENT_TWEAK_XRCE_WRITE_LIMIT "This feature uses a tweak to allow XRCE WRITE DATA submessages greater than 64 kB." ON)

###############################################################################
# Dependencies
###############################################################################
set(_deps "")

set(_fastcdr_version 1.0.13)
set(_fastcdr_tag v1.0.13)
if(UAGENT_USE_SYSTEM_FASTCDR)
set(_fastcdr_version 1)
else()
set(_fastcdr_version 1.0.22)
set(_fastcdr_tag v1.0.22)
endif()
list(APPEND _deps "fastcdr\;${_fastcdr_version}")

if(UAGENT_P2P_PROFILE)
Expand All @@ -81,10 +89,10 @@ if(UAGENT_FAST_PROFILE)
if(UAGENT_USE_SYSTEM_FASTDDS)
set(_fastdds_version 2)
else()
set(_fastdds_version 2.0.2)
set(_fastdds_tag v2.0.2)
set(_foonathan_memory_tag c619113) # This tag should be updated every time it gets updated in foonathan_memory_vendor eProsima's package
endif()
set(_fastdds_version 2.4.1)
set(_fastdds_tag v2.4.1)
set(_foonathan_memory_tag v0.7-1) # This tag should be updated every time it gets updated in foonathan_memory_vendor eProsima's package
endif()
list(APPEND _deps "fastrtps\;${_fastdds_version}")
endif()

Expand All @@ -99,7 +107,7 @@ endif()
###############################################################################
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
if(NOT UAGENT_SUPERBUILD)
project(microxrcedds_agent VERSION "2.0.0" LANGUAGES C CXX)
project(microxrcedds_agent VERSION "2.1.0" LANGUAGES C CXX)
else()
project(uagent_superbuild NONE)
include(${PROJECT_SOURCE_DIR}/cmake/SuperBuild.cmake)
Expand Down Expand Up @@ -135,27 +143,21 @@ foreach(d ${_deps})
find_package(${_name} ${_version} REQUIRED)
endforeach()

###############################################################################
# Load external CMake Modules.
###############################################################################
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${SANITIZERS_ROOT}/cmake)
find_package(Sanitizers REQUIRED)
if(SANITIZE_ADDRESS)
message(STATUS "Preloading AddressSanitizer library could be done using \"${ASan_WRAPPER}\" to run your program.")
endif()

###############################################################################
# Sources
###############################################################################
# Check platform.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "Android")
set(TRANSPORT_SRCS
src/cpp/transport/udp/UDPv4AgentLinux.cpp
src/cpp/transport/udp/UDPv6AgentLinux.cpp
src/cpp/transport/tcp/TCPv4AgentLinux.cpp
src/cpp/transport/tcp/TCPv6AgentLinux.cpp
src/cpp/transport/can/CanAgentLinux.cpp
src/cpp/transport/serial/SerialAgentLinux.cpp
src/cpp/transport/serial/TermiosAgentLinux.cpp
src/cpp/transport/serial/MultiSerialAgentLinux.cpp
src/cpp/transport/serial/MultiTermiosAgentLinux.cpp
src/cpp/transport/serial/PseudoTerminalAgentLinux.cpp
$<$<BOOL:${UAGENT_DISCOVERY_PROFILE}>:src/cpp/transport/discovery/DiscoveryServerLinux.cpp>
$<$<BOOL:${UAGENT_P2P_PROFILE}>:src/cpp/transport/p2p/AgentDiscovererLinux.cpp>
Expand Down Expand Up @@ -233,7 +235,6 @@ endif()
###############################################################################
# Library
add_library(${PROJECT_NAME} ${SRCS})
add_sanitizers(${PROJECT_NAME})

set_target_properties(${PROJECT_NAME} PROPERTIES
VERSION
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@

<a href="http://www.eprosima.com"><img src="https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcSd0PDlVz1U_7MgdTe0FRIWD0Jc9_YH-gGi0ZpLkr-qgCI6ZEoJZ5GBqQ" align="left" hspace="8" vspace="2" width="100" height="100" ></a>

*The Micro XRCE-DDS Agent* acts as a server to bridge the DDS dataspace network with *Micro XRCE-DDS Client* applications.
*eProsima Micro XRCE-DDS* is a library implementing the [DDS-XRCE protocol](https://www.omg.org/spec/DDS-XRCE/About-DDS-XRCE/) as defined and maintained by the OMG, whose aim is to allow resource constrained devices such as microcontrollers to communicate with the [DDS](https://www.omg.org/spec/DDS/About-DDS/) world as any other DDS actor would do.
It follows a client/server paradigm and is composed by two libraries, the *Micro XRCE-DDS Client* and the *Micro XRCE-DDS Agent*. The *Micro XRCE-DDS Clients* are lightweight entities meant to be compiled on e**X**tremely **R**esource **C**onstrained **E**nvironments, while the *Micro XRCE-DDS Agent* is a broker which bridges the *Clients* with the DDS world.

The *Micro XRCE-DDS Agents* receive messages containing request operations from the *Clients* to publish and subscribe to topics in the DDS global dataspace. Remote procedure calls, as defined by the DDS-RPC standard, are also supported, allowing to communicate according to a request/reply paradigm.
The *Agents* then process these XRCE requests and send back a response with the operation status result and with the requested data, in the case of subscribe/reply operations.
<p align="center"> <img src="https://github.com/eProsima/Micro-XRCE-DDS-Agent/blob/master/docs/General.png?raw=true" alt="General architecture" width="70%"/> </p>

*Agents* keep track of the *Clients* by means of a dedicated *ProxyClient* entity that acts on behalf of the latter.
This is made possible by the creation of *DDS Entities* on the *Agent* as a result of *Clients*' operations, such as *Participants*, *Topics*, *Publishers*, and *Subscribers*, which can interact with the DDS Global dataspace.
The *Micro XRCE-DDS Agent* receives messages containing request operations from the *Clients* to publish and subscribe to topics in the DDS global dataspace. Remote procedure calls, as defined by the [DDS-RPC standard](https://www.omg.org/spec/DDS-RPC/About-DDS-RPC/), are also supported, allowing to communicate according to a request/reply paradigm.
The *Agent* then processes these requests and sends back a response with the operation status result and with the requested data, in the case of subscribe/reply operations.

*Agents* keep track of the *Clients* by means of a dedicated `ProxyClient` entity that acts on behalf of the latter.
This is made possible by the creation of *DDS Entities* on the *Agent* as a result of *Clients*' operations, such as *Participants*, *Topics*, *Publishers*, and *Subscribers*, which can interact with the DDS global dataspace.

<p align="center"> <img src="https://github.com/eProsima/Micro-XRCE-DDS-Agent/blob/master/docs/Agent.png?raw=true" alt="Agent architecture" width="80%"/> </p>

The communication between a *Micro XRCE-DDS Client* and a *Micro XRCE-DDS Agent* is achieved by means of several kinds of built-in transports: **UDPv4**, **UDPv6**, **TCPv4**, **TCPv6** and **Serial** communication. In addition, there is the possibility for the user to generate its own **Custom** transport.
You can use an *Agent* with these transports by means of the standalone executable generated when building the project, which comes with a built-in CLI tool to select one of the transports listed above.
An *Agent* using any of these built-in transports can be launched by means of the standalone executable generated when building the project, which comes with a built-in CLI tool to select among the transports listed above.

This built-in *Agent* can also be installed and launched using the provided [Snap package](https://snapcraft.io/micro-xrce-dds-agent) or the provided [Docker image](https://hub.docker.com/r/eprosima/micro-xrce-dds-agent/).

Expand Down
95 changes: 44 additions & 51 deletions cmake/SuperBuild.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ unset(_deps)
enable_language(C)
enable_language(CXX)

if(ANDROID)
set(CROSS_CMAKE_ARGS
-DCMAKE_SYSTEM_VERSION:STRING=${CMAKE_SYSTEM_VERSION}
-DCMAKE_ANDROID_ARCH_ABI:STRING=${CMAKE_ANDROID_ARCH_ABI}
)
endif()

if(UAGENT_P2P_PROFILE)
# Micro XRCE-DDS Client.
unset(microxrcedds_client_DIR CACHE)
Expand All @@ -32,7 +39,7 @@ if(UAGENT_P2P_PROFILE)
PREFIX
${PROJECT_BINARY_DIR}/microxrcedds_client
INSTALL_DIR
${PROJECT_BINARY_DIR}/temp_install/microxrcedds_client-${_microxrcedds_client_version}
${PROJECT_BINARY_DIR}/temp_install
CMAKE_CACHE_ARGS
-DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
-DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH}
Expand All @@ -42,39 +49,44 @@ if(UAGENT_P2P_PROFILE)
-DBUILD_SHARED_LIBS:BOOL=${BUILD_SHARED_LIBS}
-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
-DCMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE}
${CROSS_CMAKE_ARGS}
-DUCLIENT_ISOLATED_INSTALL:BOOL=ON
)
list(APPEND _deps microxrcedds_client)
endif()
endif()

# Fast CDR.
unset(fastcdr_DIR CACHE)
find_package(fastcdr ${_fastcdr_version} EXACT QUIET)
if(NOT fastcdr_FOUND)
ExternalProject_Add(fastcdr
GIT_REPOSITORY
https://github.com/eProsima/Fast-CDR.git
GIT_TAG
${_fastcdr_tag}
PREFIX
${PROJECT_BINARY_DIR}/fastcdr
INSTALL_DIR
${PROJECT_BINARY_DIR}/temp_install/fastcdr-${_fastcdr_version}
CMAKE_CACHE_ARGS
-DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
-DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH}
-DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER}
-DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER}
-DBUILD_SHARED_LIBS:BOOL=${BUILD_SHARED_LIBS}
-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
-DCMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE}
UPDATE_COMMAND
COMMAND ${CMAKE_COMMAND} -E copy <SOURCE_DIR>/src/cpp/CMakeLists.txt <SOURCE_DIR>/src/cpp/CMakeLists.txt.bak
COMMAND ${CMAKE_COMMAND} -DSOVERSION_FILE=<SOURCE_DIR>/src/cpp/CMakeLists.txt -P ${PROJECT_SOURCE_DIR}/cmake/Soversion.cmake
TEST_COMMAND
COMMAND ${CMAKE_COMMAND} -E rename <SOURCE_DIR>/src/cpp/CMakeLists.txt.bak <SOURCE_DIR>/src/cpp/CMakeLists.txt
)
list(APPEND _deps fastcdr)
if(NOT UAGENT_USE_SYSTEM_FASTCDR)
unset(fastcdr_DIR CACHE)
find_package(fastcdr ${_fastcdr_version} EXACT QUIET)
if(NOT fastcdr_FOUND)
ExternalProject_Add(fastcdr
GIT_REPOSITORY
https://github.com/eProsima/Fast-CDR.git
GIT_TAG
${_fastcdr_tag}
PREFIX
${PROJECT_BINARY_DIR}/fastcdr
INSTALL_DIR
${PROJECT_BINARY_DIR}/temp_install/fastcdr-${_fastcdr_version}
CMAKE_CACHE_ARGS
-DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
-DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH}
-DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER}
-DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER}
-DBUILD_SHARED_LIBS:BOOL=${BUILD_SHARED_LIBS}
-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
-DCMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE}
${CROSS_CMAKE_ARGS}
UPDATE_COMMAND
COMMAND ${CMAKE_COMMAND} -E copy <SOURCE_DIR>/src/cpp/CMakeLists.txt <SOURCE_DIR>/src/cpp/CMakeLists.txt.bak
COMMAND ${CMAKE_COMMAND} -DSOVERSION_FILE=<SOURCE_DIR>/src/cpp/CMakeLists.txt -P ${PROJECT_SOURCE_DIR}/cmake/Soversion.cmake
TEST_COMMAND
COMMAND ${CMAKE_COMMAND} -E rename <SOURCE_DIR>/src/cpp/CMakeLists.txt.bak <SOURCE_DIR>/src/cpp/CMakeLists.txt
)
list(APPEND _deps fastcdr)
endif()
endif()

if(UAGENT_FAST_PROFILE AND NOT UAGENT_USE_SYSTEM_FASTDDS)
Expand All @@ -98,6 +110,8 @@ if(UAGENT_FAST_PROFILE AND NOT UAGENT_USE_SYSTEM_FASTDDS)
-DFOONATHAN_MEMORY_BUILD_TOOLS:BOOL=ON
-DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=ON
-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
-DCMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE}
${CROSS_CMAKE_ARGS}
)
endif()

Expand All @@ -119,6 +133,7 @@ if(UAGENT_FAST_PROFILE AND NOT UAGENT_USE_SYSTEM_FASTDDS)
-DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH};${PROJECT_BINARY_DIR}/temp_install
-DBUILD_SHARED_LIBS:BOOL=${BUILD_SHARED_LIBS}
-DCMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE}
${CROSS_CMAKE_ARGS}
-DCMAKE_FIND_ROOT_PATH:PATH=${PROJECT_BINARY_DIR}/temp_install
-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
-DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER}
Expand Down Expand Up @@ -159,6 +174,7 @@ if(UAGENT_LOGGER_PROFILE)
-DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH};${CMAKE_INSTALL_PREFIX}
-DBUILD_SHARED_LIBS:BOOL=${BUILD_SHARED_LIBS}
-DCMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE}
${CROSS_CMAKE_ARGS}
-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
-DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER}
-DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER}
Expand Down Expand Up @@ -207,29 +223,6 @@ if(UAGENT_BUILD_TESTS)
endif()
endif()

# sanitizers.
unset(Sanitizers_DIR CACHE)
find_package(Sanitizers QUIET)
if(NOT Sanitizers_FOUND)
ExternalProject_Add(sanitizers
GIT_REPOSITORY
https://github.com/arsenm/sanitizers-cmake
PREFIX
${PROJECT_BINARY_DIR}/sanitizers
BUILD_COMMAND
""
INSTALL_COMMAND
""
CMAKE_ARGS
$<$<VERSION_GREATER_EQUAL:${CMAKE_VERSION},3.16.3>:-DCMAKE_POLICY_DEFAULT_CMP0077=OLD> # Disable CMP0077 unset warning
CMAKE_CACHE_ARGS
-DCMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE}
)
ExternalProject_Get_Property(sanitizers SOURCE_DIR)
set(SANITIZERS_ROOT ${SOURCE_DIR} CACHE INTERNAL "")
list(APPEND _deps sanitizers)
endif()

# Main project.
ExternalProject_Add(uagent
SOURCE_DIR
Expand Down
3 changes: 2 additions & 1 deletion colcon.pkg
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
],
"cmake-args":[
"-DUAGENT_ISOLATED_INSTALL=OFF",
"-DUAGENT_USE_SYSTEM_FASTCDR=ON",
"-DUAGENT_USE_SYSTEM_FASTDDS=ON"
]
}
}
8 changes: 5 additions & 3 deletions include/uxr/agent/AgentInstance.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ class AgentInstance
*/
UXR_AGENT_EXPORT void run();

#ifndef _WIN32
UXR_AGENT_EXPORT void stop();
#endif

/**
* @brief Sets a callback function for a specific create/delete middleware entity operation.
* Note that not some middlewares might not implement every defined operation, or even
Expand All @@ -99,9 +103,7 @@ class AgentInstance

private:
std::thread agent_thread_;
#ifndef _WIN32
sigset_t signals_;
#endif // _WIN32
std::condition_variable exit_signal;
middleware::CallbackFactory& callback_factory_;
};
} // uxr
Expand Down
2 changes: 2 additions & 0 deletions include/uxr/agent/config.hpp.in
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ constexpr std::chrono::milliseconds CLIENT_DEAD_TIME{@UAGENT_CONFIG_CLIENT_DEAD_

const uint16_t SERVER_BUFFER_SIZE = @UAGENT_SERVER_BUFFER_SIZE@;

#cmakedefine UAGENT_TWEAK_XRCE_WRITE_LIMIT

} // namespace uxr
} // namespace eprosima

Expand Down
15 changes: 15 additions & 0 deletions include/uxr/agent/logger/Logger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#endif

#define UXR_CLIENT_KEY_STR "client_key"
#define UXR_FILE_FD_STR "fd"
#define UXR_SESSION_ID_STR "session_id"
#define UXR_OBJECT_ID_STR "object_id"
#define UXR_PARTICIPANT_ID_STR "participant_id"
Expand All @@ -55,6 +56,7 @@
#define UXR_DATA_STR "data"

#define UXR_CLIENT_KEY_FORMAT "0x{:08X}"
#define UXR_FILE_FD_FORMAT "{}"
#define UXR_SESSION_ID_FORMAT "0x{:02X}"
#define UXR_OBJECT_ID_FORMAT "0x{:04X}"
#define UXR_PARTICIPANT_ID_FORMAT "0x{:03X}(1)"
Expand Down Expand Up @@ -89,6 +91,7 @@
#define UXR_CREATE_REPLIER_PATTERN UXR_CREATE_FORMAT_BASE(REPLIER_ID) UXR_ADD_FIELD(PARTICIPANT_ID)
#define UXR_MESSAGE_PATTERN UXR_FIELD(CLIENT_KEY) UXR_ADD_FIELD(LEN)
#define UXR_MESSAGE_WITH_DATA_PATTERN UXR_MESSAGE_PATTERN UXR_ADD_FIELD(DATA)
#define UXR_MESSAGE_WITH_FD_PATTERN UXR_CREATE_FORMAT_BASE(FILE_FD) UXR_ADD_FIELD(LEN) UXR_ADD_FIELD(DATA)



Expand Down Expand Up @@ -145,8 +148,20 @@
UXR_AGENT_LOG_DEBUG(STATUS, UXR_MESSAGE_PATTERN, CLIENT_KEY, LEN, spdlog::to_hex(BUF, BUF + LEN)); \
} \
void(0)

#define UXR_MULTIAGENT_LOG_MESSAGE(STATUS, CLIENT_KEY, FD, BUF, LEN) \
if (spdlog::default_logger()->should_log(spdlog::level::trace)) \
{ \
UXR_AGENT_LOG_DEBUG(STATUS, UXR_MESSAGE_WITH_FD_PATTERN, CLIENT_KEY, FD, LEN, spdlog::to_hex(BUF, BUF + LEN)); \
} \
else \
{ \
UXR_AGENT_LOG_DEBUG(STATUS, UXR_MESSAGE_PATTERN, CLIENT_KEY, FD, LEN, spdlog::to_hex(BUF, BUF + LEN)); \
} \
void(0)
#else
#define UXR_AGENT_LOG_MESSAGE(...) void(0)
#define UXR_MULTIAGENT_LOG_MESSAGE(...) void(0)
#endif

#endif // UXR_AGENT_LOGGER_LOGGER_HPP_
Loading

0 comments on commit 070c438

Please sign in to comment.